Skip to content

Commit 1cd2bcf

Browse files
committed
added time publishing
1 parent f15ef54 commit 1cd2bcf

20 files changed

+260
-69
lines changed

Sources/swiftui-loop-videoplayer/LoopPlayerView.swift

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,27 @@
66
//
77

88
import SwiftUI
9+
import Combine
910
#if canImport(AVKit)
1011
import AVKit
1112
#endif
1213

1314
/// Player view for running a video in loop
1415
@available(iOS 14.0, macOS 11.0, tvOS 14.0, *)
15-
public struct LoopPlayerView: View {
16+
public struct LoopPlayerView: View {
1617

1718
/// Set of settings for video the player
1819
@Binding public var settings: VideoSettings
1920

2021
/// Binding to a playback command that controls playback actions
2122
@Binding public var command: PlaybackCommand
2223

24+
/// The current playback time, represented as a Double.
25+
@State private var currentTime: Double = 0.0
26+
27+
/// A publisher that emits the current time as a Double value.
28+
@State var timePublisher = PassthroughSubject<Double, Never>()
29+
2330
private var videoId : String{
2431
[settings.name, settings.ext].joined(separator: ".")
2532
}
@@ -28,33 +35,42 @@ public struct LoopPlayerView: View {
2835

2936
/// Player initializer
3037
/// - Parameters:
31-
/// - fileName: Name of the video to play
32-
/// - ext: Video extension
33-
/// - gravity: A structure that defines how a layer displays a player’s visual content within the layer’s bounds
34-
/// - eColor: Color of the error message text if the file is not found
35-
/// - eFontSize: Size of the error text
36-
/// - command: A binding to control playback actions
38+
/// - fileName: The name of the video file.
39+
/// - ext: The file extension, with a default value of "mp4".
40+
/// - gravity: The video gravity setting, with a default value of `.resizeAspect`.
41+
/// - timePublishing: An optional `CMTime` value for time publishing, with a default value of 1 second.
42+
/// - eColor: The color to be used, with a default value of `.accentColor`.
43+
/// - eFontSize: The font size to be used, with a default value of 17.0.
44+
/// - command: A binding to the playback command, with a default value of `.play`.
3745
public init(
3846
fileName: String,
3947
ext: String = "mp4",
4048
gravity: AVLayerVideoGravity = .resizeAspect,
49+
timePublishing : CMTime? = CMTime(seconds: 1, preferredTimescale: 600),
4150
eColor: Color = .accentColor,
4251
eFontSize: CGFloat = 17.0,
4352
command : Binding<PlaybackCommand> = .constant(.play)
4453
) {
4554
self._command = command
55+
56+
func description(@SettingsBuilder content: () -> [Setting]) -> [Setting] {
57+
return content()
58+
}
4659

47-
_settings = .constant(
48-
VideoSettings {
49-
SourceName(fileName)
50-
Ext(ext)
51-
Gravity(gravity)
52-
ErrorGroup {
53-
EColor(eColor)
54-
EFontSize(eFontSize)
55-
}
60+
let settings: VideoSettings = VideoSettings {
61+
SourceName(fileName)
62+
Ext(ext)
63+
Gravity(gravity)
64+
if let timePublishing{
65+
timePublishing
66+
}
67+
ErrorGroup {
68+
EColor(eColor)
69+
EFontSize(eFontSize)
5670
}
57-
)
71+
}
72+
73+
_settings = .constant(settings)
5874
}
5975

6076
/// Player initializer in a declarative way
@@ -85,7 +101,11 @@ public struct LoopPlayerView: View {
85101
// MARK: - API
86102

87103
public var body: some View {
88-
LoopPlayerMultiPlatform(settings: $settings, command: $command)
104+
LoopPlayerMultiPlatform(settings: $settings, command: $command, timePublisher: timePublisher)
89105
.frame(maxWidth: .infinity, maxHeight: .infinity)
106+
.onReceive(timePublisher, perform: { time in
107+
currentTime = time
108+
})
109+
.preference(key: CurrentTimePreferenceKey.self, value: currentTime)
90110
}
91111
}

Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1+
//
2+
// PlaybackCommand.swift
3+
//
4+
//
5+
// Created by Igor Shelopaev on 05.08.24.
6+
//
7+
18
import AVFoundation
9+
#if canImport(CoreImage)
210
import CoreImage
11+
#endif
312

413
/// An enumeration of possible playback commands.
514
@available(iOS 14.0, macOS 11.0, tvOS 14.0, *)

Sources/swiftui-loop-videoplayer/enum/Setting.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@ import SwiftUI
1111
import AVKit
1212
#endif
1313

14-
1514
/// Settings for loop video player
1615
@available(iOS 14.0, macOS 11.0, tvOS 14.0, *)
17-
public enum Setting: Equatable{
16+
public enum Setting: Equatable, SettingsConvertible{
17+
18+
public func asSettings() -> [Setting] {
19+
[self]
20+
}
1821

1922
/// File name
2023
case name(String)
2124

2225
/// File extension
2326
case ext(String)
27+
28+
/// A CMTime value representing the interval at which the player's current time should be published.
29+
/// If set, the player will publish periodic time updates based on this interval.
30+
case timePublishing(CMTime)
2431

2532
/// Video gravity
2633
case gravity(AVLayerVideoGravity = .resizeAspect)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// CMTime+.swift
3+
//
4+
//
5+
// Created by Igor on 15.08.24.
6+
//
7+
8+
#if canImport(AVKit)
9+
import AVKit
10+
#endif
11+
12+
extension CMTime : SettingsConvertible{
13+
public func asSettings() -> [Setting] {
14+
[.timePublishing(self)]
15+
}
16+
}

Sources/swiftui-loop-videoplayer/fn/fn+.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
import Foundation
99
import AVFoundation
10+
#if canImport(CoreImage)
1011
import CoreImage
12+
#endif
1113

1214
/// Retrieves an `AVURLAsset` based on specified video settings.
1315
/// - Parameter settings: The `VideoSettings` object containing details like name and extension of the video.

Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerErrorDelegate.swift renamed to Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// PlayerErrorDelegate.swift
2+
// PlayerDelegateProtocol.swift
33
//
44
//
55
// Created by Igor Shelopaev on 05.08.24.
@@ -11,7 +11,7 @@ import Foundation
1111
///
1212
/// Conforming to this protocol allows a class to respond to error events that occur within a media player context.
1313
@available(iOS 14, macOS 11, tvOS 14, *)
14-
public protocol PlayerErrorDelegate: AnyObject {
14+
public protocol PlayerDelegateProtocol: AnyObject {
1515
/// Called when an error is encountered within the media player.
1616
///
1717
/// This method provides a way for delegate objects to respond to error conditions, allowing them to handle or
@@ -20,4 +20,6 @@ public protocol PlayerErrorDelegate: AnyObject {
2020
/// - Parameter error: The specific `VPErrors` instance describing what went wrong.
2121
@MainActor
2222
func didReceiveError(_ error: VPErrors)
23+
24+
func didPassedTime(seconds : Double)
2325
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ public protocol SettingsConvertible {
1515
/// - Returns: Array of settings
1616
func asSettings() -> [Setting]
1717
}
18+

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
//
77

88
import AVFoundation
9+
#if canImport(CoreImage)
910
import CoreImage
11+
#endif
1012

1113
@available(iOS 14, macOS 11, tvOS 14, *)
1214
@MainActor @preconcurrency
@@ -339,7 +341,7 @@ extension AbstractPlayer{
339341
/// - player: A reference to the AVQueuePlayer to be cleaned up. Modified directly to deallocate resources.
340342
/// - playerLooper: A reference to the AVPlayerLooper associated with the player. It's disabled and set to nil.
341343
/// - errorObserver: A reference to an NSKeyValueObservation monitoring the player, which is invalidated and set to nil.
342-
internal func cleanUp(player: inout AVQueuePlayer?, playerLooper: inout AVPlayerLooper?, errorObserver: inout NSKeyValueObservation?) {
344+
internal func cleanUp(player: inout AVQueuePlayer?, playerLooper: inout AVPlayerLooper?, errorObserver: inout NSKeyValueObservation?, timeObserverToken: inout Any?) {
343345
errorObserver?.invalidate()
344346
errorObserver = nil
345347

@@ -353,6 +355,11 @@ internal func cleanUp(player: inout AVQueuePlayer?, playerLooper: inout AVPlayer
353355
player?.remove(item)
354356
}
355357

358+
if let observerToken = timeObserverToken {
359+
player?.removeTimeObserver(observerToken)
360+
timeObserverToken = nil
361+
}
362+
356363
player = nil
357364

358365
#if DEBUG

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

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,23 @@ public protocol LoopingPlayerProtocol: AbstractPlayer, LayerMakerProtocol{
3030
var playerLayer : AVPlayerLayer { get }
3131

3232
/// The delegate to be notified about errors encountered by the player.
33-
var delegate: PlayerErrorDelegate? { get set }
33+
var delegate: PlayerDelegateProtocol? { get set }
3434

3535
/// An optional NSKeyValueObservation to monitor errors encountered by the video player.
3636
/// This observer should be configured to detect and handle errors from the AVQueuePlayer,
3737
/// ensuring that all playback errors are managed and reported appropriately.
3838
var errorObserver: NSKeyValueObservation? { get set }
39+
40+
/// Declare a variable to hold the time observer token outside the if statement
41+
var timeObserverToken: Any? { get set }
3942

40-
/// Initializes a video player with a specified media asset and layer gravity.
43+
/// Initializes a new instance of the view
44+
///
4145
/// - Parameters:
42-
/// - asset: The `AVURLAsset` representing the media content to be played. This asset encapsulates the properties of the media file.
43-
/// - gravity: The `AVLayerVideoGravity` that determines how the video content is displayed within the bounds of the player layer. Common values are `.resizeAspect`, `.resizeAspectFill`, and `.resize` to control the scaling and filling behavior of the video content.
44-
init(asset: AVURLAsset, gravity: AVLayerVideoGravity)
46+
/// - asset: The AVURLAsset to be used in the player.
47+
/// - gravity: Specifies how the video content should be displayed within the layer bounds.
48+
/// - timePublishing: Optional CMTime that determines the interval at which the video current time should be published. Pass nil to disable time publishing.
49+
init(asset: AVURLAsset, gravity: AVLayerVideoGravity, timePublishing: CMTime?)
4550

4651
/// Sets up the necessary observers on the AVPlayerItem and AVQueuePlayer to monitor changes and errors.
4752
///
@@ -90,23 +95,24 @@ internal extension LoopingPlayerProtocol {
9095
})
9196
}
9297

93-
/// Sets up the player components using the provided asset and video gravity.
98+
/// Sets up the player components with the specified media asset, display properties, and optional time publishing interval.
9499
///
95100
/// - Parameters:
96-
/// - asset: The AVURLAsset to be played.
97-
/// - gravity: The AVLayerVideoGravity to be applied to the video layer.
98-
func setupPlayerComponents(asset: AVURLAsset, gravity: AVLayerVideoGravity) {
99-
// Create an AVPlayerItem with the provided asset
101+
/// - asset: The AVURLAsset representing the video content.
102+
/// - gravity: Determines how the video content is scaled or fit within the player view.
103+
/// - timePublishing: Optional interval for publishing the current playback time; nil disables this feature.
104+
func setupPlayerComponents(
105+
asset: AVURLAsset,
106+
gravity: AVLayerVideoGravity,
107+
timePublishing: CMTime?
108+
) {
100109
let item = AVPlayerItem(asset: asset)
101110

102-
// Initialize an AVQueuePlayer with the player item
103111
let player = AVQueuePlayer(items: [item])
104112
self.player = player
105113

106-
// Configure the player with the specified gravity
107-
configurePlayer(player, gravity: gravity)
114+
configurePlayer(player, gravity: gravity, timePublishing: timePublishing)
108115

109-
// Set up observers to monitor status and errors
110116
setupObservers(for: item, player: player)
111117
}
112118

@@ -115,7 +121,12 @@ internal extension LoopingPlayerProtocol {
115121
/// - Parameters:
116122
/// - player: The AVQueuePlayer to be configured.
117123
/// - gravity: The AVLayerVideoGravity determining how the video content should be scaled or fit within the player layer.
118-
func configurePlayer(_ player: AVQueuePlayer, gravity: AVLayerVideoGravity) {
124+
/// - timePublishing: Optional interval for publishing the current playback time; nil disables this feature.
125+
func configurePlayer(
126+
_ player: AVQueuePlayer,
127+
gravity: AVLayerVideoGravity,
128+
timePublishing: CMTime?
129+
) {
119130
player.isMuted = true
120131
playerLayer.player = player
121132
playerLayer.videoGravity = gravity
@@ -133,9 +144,18 @@ internal extension LoopingPlayerProtocol {
133144
#endif
134145
compositeLayer.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height)
135146
loop()
147+
136148
if !filters.isEmpty{ // have an idea for the feature
137149
applyVideoComposition()
138150
}
151+
152+
if let timePublishing{
153+
timeObserverToken = player.addPeriodicTimeObserver(forInterval: timePublishing, queue: .main) { [weak self] time in
154+
self?.delegate?.didPassedTime(seconds: time.seconds)
155+
print(time.seconds)
156+
}
157+
}
158+
139159
player.play()
140160
}
141161

Sources/swiftui-loop-videoplayer/protocol/vector/ShapeLayerBuilderProtocol.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
// Created by Igor on 13.08.24.
66
//
77

8-
import QuartzCore
98
import CoreGraphics
109

10+
#if canImport(QuartzCore)
11+
import QuartzCore
12+
#endif
13+
14+
1115
/// A protocol defining a builder for creating shape layers with a unique identifier.
1216
///
1317
/// Conforming types will be able to construct a CAShapeLayer based on provided frame, bounds, and center.

Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import UIKit
1010
#elseif canImport(AppKit)
1111
import AppKit
1212
#endif
13+
#if canImport(QuartzCore)
1314
import QuartzCore
15+
#endif
1416

1517
/// A protocol that defines methods and properties for managing vector layers within a composite layer.
1618
///

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import AVFoundation
99
import SwiftUI
10+
import Combine
1011

1112
/// Protocol that defines the common functionalities and properties
1213
/// for looping video players on different platforms.
@@ -42,7 +43,7 @@ public protocol LoopPlayerViewProtocol {
4243
/// - command: A binding to a `PlaybackCommand` that controls playback actions.
4344
///
4445
/// This initializer sets up the necessary configuration and command bindings for playback functionality.
45-
init(settings: Binding<VideoSettings>, command: Binding<PlaybackCommand>)
46+
init(settings: Binding<VideoSettings>, command: Binding<PlaybackCommand>, timePublisher : PassthroughSubject<Double, Never>)
4647

4748
}
4849

@@ -83,7 +84,7 @@ public extension LoopPlayerViewProtocol{
8384
asset: AVURLAsset?) -> PlayerView? {
8485

8586
if let asset{
86-
let player = PlayerView(asset: asset, gravity: settings.gravity)
87+
let player = PlayerView(asset: asset, gravity: settings.gravity, timePublishing: settings.timePublishing)
8788
container.addSubview(player)
8889
activateFullScreenConstraints(for: player, in: container)
8990
return player

0 commit comments

Comments
 (0)