From e1d682965acbf07e0b4d37583f4192b74780f89e Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 09:58:07 +0100 Subject: [PATCH 001/103] Update README.md --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index bdefdda..5a4d59f 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,44 @@ Please note that using videos from URLs requires ensuring that you have the righ ## Commands +### Handling Commands + ```swift + @State public var playbackCommand: PlaybackCommand = .idle + ``` +`@State` updates are asynchronous and batched in SwiftUI. When you assign: + ```swift + playbackCommand = .play + playbackCommand = .pause + ``` +SwiftUI only registers the last assignment (`.pause`) in the same run loop cycle, ignoring `.play`. +To ensure .play is applied before .pause, you can use `Task` to schedule the second update on the next run loop iteration: +**.play → .pause** + ```swift + playbackCommand = .play + Task { @MainActor in + playbackCommand = .pause + } + ``` +**.play → .pause → .play** + + ```swift + playbackCommand = .play + + Task { + playbackCommand = .pause + Task { playbackCommand = .play } // This runs AFTER `.pause` + } + ``` + If playbackCommand is critical for playback logic, use an ObservableObject with @Published instead of @State. This avoids SwiftUI’s state batching +```swift + @Published var playbackCommand: PlaybackCommand = .pause +``` +then works +```swift + playbackCommand = .play + playbackCommand = .pause +``` + ### Handling Sequential Similar Commands When using the video player controls in your SwiftUI application, it's important to understand how command processing works. Specifically, issuing two identical commands consecutively will result in the second command being ignored. This is due to the underlying implementation in SwiftUI that prevents redundant command execution to optimize performance and user experience. From 5a6c2445b09c796c8aa924ac5c636b79ae451024 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 09:59:32 +0100 Subject: [PATCH 002/103] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5a4d59f..cad074e 100644 --- a/README.md +++ b/README.md @@ -163,17 +163,17 @@ To ensure .play is applied before .pause, you can use `Task` to schedule the sec Task { @MainActor in playbackCommand = .pause } - ``` +``` **.play → .pause → .play** - ```swift +```swift playbackCommand = .play Task { playbackCommand = .pause Task { playbackCommand = .play } // This runs AFTER `.pause` } - ``` +``` If playbackCommand is critical for playback logic, use an ObservableObject with @Published instead of @State. This avoids SwiftUI’s state batching ```swift @Published var playbackCommand: PlaybackCommand = .pause From 01a03e54adec71e898e9cb20c71f12518055db4f Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 10:00:05 +0100 Subject: [PATCH 003/103] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cad074e..514c3b2 100644 --- a/README.md +++ b/README.md @@ -167,12 +167,12 @@ To ensure .play is applied before .pause, you can use `Task` to schedule the sec **.play → .pause → .play** ```swift - playbackCommand = .play + playbackCommand = .play - Task { - playbackCommand = .pause - Task { playbackCommand = .play } // This runs AFTER `.pause` - } + Task { + playbackCommand = .pause + Task { playbackCommand = .play } // This runs AFTER `.pause` + } ``` If playbackCommand is critical for playback logic, use an ObservableObject with @Published instead of @State. This avoids SwiftUI’s state batching ```swift From b573993c29aebc83d763c40fe9af960dc35b2441 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 10:00:34 +0100 Subject: [PATCH 004/103] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 514c3b2..32ce6ef 100644 --- a/README.md +++ b/README.md @@ -167,12 +167,12 @@ To ensure .play is applied before .pause, you can use `Task` to schedule the sec **.play → .pause → .play** ```swift - playbackCommand = .play + playbackCommand = .play - Task { - playbackCommand = .pause - Task { playbackCommand = .play } // This runs AFTER `.pause` - } + Task { + playbackCommand = .pause + Task { playbackCommand = .play } // This runs AFTER `.pause` + } ``` If playbackCommand is critical for playback logic, use an ObservableObject with @Published instead of @State. This avoids SwiftUI’s state batching ```swift From 3e817165eee351e971915768a64530f7e235fb52 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 10:03:53 +0100 Subject: [PATCH 005/103] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 32ce6ef..8545858 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ Please note that using videos from URLs requires ensuring that you have the righ ``` SwiftUI only registers the last assignment (`.pause`) in the same run loop cycle, ignoring `.play`. To ensure .play is applied before .pause, you can use `Task` to schedule the second update on the next run loop iteration: + **.play → .pause** ```swift playbackCommand = .play From 6fe7d6ea130182ccad587eb9a1994d7112cbe169 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 10:12:18 +0100 Subject: [PATCH 006/103] Update README.md --- README.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/README.md b/README.md index 8545858..0a352d1 100644 --- a/README.md +++ b/README.md @@ -170,20 +170,11 @@ To ensure .play is applied before .pause, you can use `Task` to schedule the sec ```swift playbackCommand = .play - Task { + Task { @MainActor playbackCommand = .pause Task { playbackCommand = .play } // This runs AFTER `.pause` } ``` - If playbackCommand is critical for playback logic, use an ObservableObject with @Published instead of @State. This avoids SwiftUI’s state batching -```swift - @Published var playbackCommand: PlaybackCommand = .pause -``` -then works -```swift - playbackCommand = .play - playbackCommand = .pause -``` ### Handling Sequential Similar Commands From bfc88db463c5aec1c182a59fbe85db118131796e Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 10:17:08 +0100 Subject: [PATCH 007/103] Update README.md --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0a352d1..2d305c5 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ To ensure .play is applied before .pause, you can use `Task` to schedule the sec ```swift playbackCommand = .play - Task { @MainActor + Task { @MainActor in playbackCommand = .pause Task { playbackCommand = .play } // This runs AFTER `.pause` } @@ -178,7 +178,7 @@ To ensure .play is applied before .pause, you can use `Task` to schedule the sec ### Handling Sequential Similar Commands -When using the video player controls in your SwiftUI application, it's important to understand how command processing works. Specifically, issuing two identical commands consecutively will result in the second command being ignored. This is due to the underlying implementation in SwiftUI that prevents redundant command execution to optimize performance and user experience. +When using the video player controls in your SwiftUI application, it's important to understand how command processing works. Specifically, issuing two identical commands consecutively will result in the second command being ignored. This is due to the underlying implementation that prevents redundant command execution to optimize performance and user experience in terms of UI updates. ### Common Scenario @@ -188,6 +188,16 @@ For example, if you attempt to pause the video player twice in a row, the second In cases where you need to re-issue a command that might appear redundant but is necessary under specific conditions, you must insert an `idle` command between the two similar commands. The `idle` command resets the command state of the player, allowing subsequent commands to be processed as new actions. +**.play → .idle → .play** + +```swift + playbackCommand = .play + + Task { @MainActor in + playbackCommand = .idle + Task { playbackCommand = .play } // This runs AFTER `.idle` + + ### Playback Commands | Command | Description | From db0788be8d82c83e3993c509317476bc9aa619e4 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 10:17:50 +0100 Subject: [PATCH 008/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d305c5..2496055 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ In cases where you need to re-issue a command that might appear redundant but is Task { @MainActor in playbackCommand = .idle Task { playbackCommand = .play } // This runs AFTER `.idle` - +``` ### Playback Commands From 5bb671be6c07f4af31576060bd6e4bb1458b767f Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 10:18:24 +0100 Subject: [PATCH 009/103] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2496055..4894b2e 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,8 @@ In cases where you need to re-issue a command that might appear redundant but is Task { @MainActor in playbackCommand = .idle Task { playbackCommand = .play } // This runs AFTER `.idle` -``` + } +``` ### Playback Commands From 1f3e380943d622512b2297ef1a02342b8d2ed730 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 10:32:57 +0100 Subject: [PATCH 010/103] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4894b2e..94f94cd 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ To ensure .play is applied before .pause, you can use `Task` to schedule the sec **.play → .pause** ```swift playbackCommand = .play - Task { @MainActor in + Task { playbackCommand = .pause } ``` @@ -170,7 +170,7 @@ To ensure .play is applied before .pause, you can use `Task` to schedule the sec ```swift playbackCommand = .play - Task { @MainActor in + Task { playbackCommand = .pause Task { playbackCommand = .play } // This runs AFTER `.pause` } From e9142057d11987f55c1ba1d4b2c3de4517fd7796 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 10:33:22 +0100 Subject: [PATCH 011/103] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 94f94cd..be42309 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ To ensure .play is applied before .pause, you can use `Task` to schedule the sec **.play → .pause** ```swift playbackCommand = .play + Task { playbackCommand = .pause } From 534dcc449721d2d7d910a1bd6551b342bd3b7896 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 10:34:39 +0100 Subject: [PATCH 012/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index be42309..869df38 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ In cases where you need to re-issue a command that might appear redundant but is ```swift playbackCommand = .play - Task { @MainActor in + Task { playbackCommand = .idle Task { playbackCommand = .play } // This runs AFTER `.idle` } From 6073130209348d25ef761d855dfebc1860e1ea35 Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 10 Feb 2025 16:48:08 +0100 Subject: [PATCH 013/103] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 869df38..9a071ae 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoPlayer%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoPlayer) ## Why if we have Apple’s VideoPlayer ?! -Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for example it doesn’t allow you to hide or customize the default video controls UI, limiting its use for custom scenarios. In contrast, this solution provides full control over playback, including the ability to disable or hide UI elements, making it suitable for background videos, tooltips, and video hints etc. Additionally, it supports advanced features like subtitles, seamless looping and real-time filter application, adding vector graphics upon the video stream etc. This package uses a declarative approach to declare parameters for the video component based on building blocks. This implementation might give some insights into how SwiftUI works under the hood. You can also pass parameters in the common way. +Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for example it doesn’t allow you to hide or customize the default video controls UI, limiting its use for custom scenarios. In contrast, this solution provides full control over playback, including the ability to disable or hide UI elements, making it suitable for background videos, tooltips, and video hints etc. Additionally, it supports advanced features like subtitles, seamless looping and real-time filter application, adding vector graphics upon the video stream etc. This package uses a declarative approach to declare parameters for the video component based on building blocks. This implementation might give some insights into how SwiftUI works under the hood. You can also pass parameters in the common way. *If you profile the package, do it on a real device.* It is a pure package without any third-party libraries. My main focus was on performance. Especially if you need to add a video in the background as a design element, in such cases, you’d want a lightweight component without a lot of unnecessary features. **I hope it serves you well**. -*If you profile the package, do it on a real device. There’s an enormous difference in results compared to the simulator.* + ## SwiftUI app example [follow the link](https://github.com/swiftuiux/swiftui-video-player-example) From 97586e85246c6bdc4b5b55d10562caff78c76507 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 09:32:04 +0100 Subject: [PATCH 014/103] update --- .../enum/VPErrors.swift | 36 +++++++++++-------- .../protocol/player/ExtPlayerProtocol.swift | 5 +-- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/enum/VPErrors.swift b/Sources/swiftui-loop-videoplayer/enum/VPErrors.swift index a350354..0adb444 100644 --- a/Sources/swiftui-loop-videoplayer/enum/VPErrors.swift +++ b/Sources/swiftui-loop-videoplayer/enum/VPErrors.swift @@ -22,29 +22,30 @@ public enum VPErrors: Error, CustomStringConvertible, Sendable { /// Error case for when settings are not unique. case settingsNotUnique - /// Picture-in-Picture (PiP) is not supported + /// Picture-in-Picture (PiP) is not supported. case notSupportedPiP - /// Failed to load - case failedToLoad + /// Failed to load. + /// - Parameter error: The error encountered during loading. + case failedToLoad(Error?) /// A description of the error, suitable for display. public var description: String { switch self { - case .sourceNotFound(let name): - return "Source not found: \(name)" + case .sourceNotFound(let name): + return "Source not found: \(name)" - case .notSupportedPiP: - return "Picture-in-Picture (PiP) is not supported on this device." + case .notSupportedPiP: + return "Picture-in-Picture (PiP) is not supported on this device." - case .settingsNotUnique: - return "Settings are not unique" + case .settingsNotUnique: + return "Settings are not unique." - case .remoteVideoError(let error): - return "Playback error: \(String(describing: error?.localizedDescription))" + case .remoteVideoError(let error): + return "Playback error: \(error?.localizedDescription ?? "Unknown error.")" - case .failedToLoad: - return "Failed to load the video." + case .failedToLoad(let error): + return "Failed to load the video: \(error?.localizedDescription ?? "Unknown error.")" } } } @@ -57,12 +58,19 @@ extension VPErrors: Equatable { switch (lhs, rhs) { case (.remoteVideoError(let a), .remoteVideoError(let b)): return a?.localizedDescription == b?.localizedDescription + case (.sourceNotFound(let a), .sourceNotFound(let b)): return a == b + case (.settingsNotUnique, .settingsNotUnique): return true - case (.failedToLoad, .failedToLoad): + + case (.notSupportedPiP, .notSupportedPiP): return true + + case (.failedToLoad(let a), .failedToLoad(let b)): + return a?.localizedDescription == b?.localizedDescription + default: return false } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 28bccce..cf63bed 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -230,11 +230,12 @@ internal extension ExtPlayerProtocol { } case .failed: Task { @MainActor in - self?.onError(.failedToLoad) + let error = self?.currentItem?.error + self?.onError(.failedToLoad(error)) } @unknown default: Task { @MainActor in - self?.onError(.failedToLoad) + self?.onError(.failedToLoad(nil)) } } } From 8a8558d52b445d76c6ee4a7e6bfd285e71aa9cc9 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 11:01:58 +0100 Subject: [PATCH 015/103] update --- README.md | 18 ++++++++++++++++++ .../protocol/player/ExtPlayerProtocol.swift | 7 +++++-- .../view/player/ios/ExtPlayerUIView.swift | 16 +++++++--------- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9a071ae..244194e 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,24 @@ video_main.m3u8 | `itemStatusChanged(AVPlayerItem.Status)` | Indicates that the AVPlayerItem's status has changed. Possible statuses: `.unknown`, `.readyToPlay`, `.failed`. | | `duration(CMTime)` | Provides the duration of the AVPlayerItem when it is ready to play. The duration is given in `CMTime`. | +### Additional Notes on Errors +When the URL is syntactically valid but the resource does not actually exist (e.g., a 404 response or an unreachable server), AVPlayerItem.status can remain .unknown indefinitely. It may never transition to .failed, and the .AVPlayerItemFailedToPlayToEndTime notification won’t fire if playback never starts. +**Workarounds and Best Practices** +*Pre-Check the URL With HEAD* +If you want to ensure that a URL is valid before passing it to AVPlayerItem, use for example a simple HEAD request via URLSession to check for a valid 2xx response. + +```swift +func checkURLExists(_ url: URL) async throws -> Bool { + var request = URLRequest(url: url) + request.httpMethod = "HEAD" + + let (_, response) = try await URLSession.shared.data(for: request) + if let httpResponse = response as? HTTPURLResponse { + return (200...299).contains(httpResponse.statusCode) + } + return false +} +``` ### Additional Notes on Adding and Removing Vector Graphics diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index cf63bed..f90eb67 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -186,6 +186,7 @@ internal extension ExtPlayerProtocol { } observeItemStatus(newItem) + insert(newItem) if settings.loop{ @@ -210,7 +211,8 @@ internal extension ExtPlayerProtocol { self?.onError(.sourceNotFound(name)) } } - + + /// Observes the status of an AVPlayerItem and notifies the delegate when the status changes. /// - Parameter item: The AVPlayerItem whose status should be observed. private func observeItemStatus(_ item: AVPlayerItem) { @@ -235,7 +237,8 @@ internal extension ExtPlayerProtocol { } @unknown default: Task { @MainActor in - self?.onError(.failedToLoad(nil)) + let error = self?.currentItem?.error + self?.onError(.failedToLoad(error)) } } } diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 2373e13..5f61632 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -22,10 +22,10 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol{ /// `filters` is an array that stores CIFilter objects used to apply different image processing effects internal var filters: [CIFilter] = [] - + /// `brightness` represents the adjustment level for the brightness of the video content. internal var brightness: Float = 0 - + /// `contrast` indicates the level of contrast adjustment for the video content. internal var contrast: Float = 1 @@ -64,7 +64,7 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol{ /// The delegate to be notified about errors encountered by the player. weak var delegate: PlayerDelegateProtocol? - + /// The Picture-in-Picture (PiP) controller for managing PiP functionality. internal var pipController: AVPictureInPictureController? @@ -81,17 +81,17 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol{ addPlayerLayer() addCompositeLayer(settings) - + setupPlayerComponents( settings: settings ) - + } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + /// Lays out subviews and adjusts the frame of the player layer to match the view's bounds. override func layoutSubviews() { super.layoutSubviews() @@ -100,8 +100,6 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol{ layoutCompositeLayer() } - - /// Updates the composite layer and all its sublayers' frames. public func layoutCompositeLayer() { guard let compositeLayer = compositeLayer else { return } From 5e58aa8573bec0e09b0e4d1dcad8e54585a8cd43 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 11:03:08 +0100 Subject: [PATCH 016/103] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 244194e..7d6b232 100644 --- a/README.md +++ b/README.md @@ -292,8 +292,10 @@ video_main.m3u8 ### Additional Notes on Errors When the URL is syntactically valid but the resource does not actually exist (e.g., a 404 response or an unreachable server), AVPlayerItem.status can remain .unknown indefinitely. It may never transition to .failed, and the .AVPlayerItemFailedToPlayToEndTime notification won’t fire if playback never starts. + **Workarounds and Best Practices** *Pre-Check the URL With HEAD* + If you want to ensure that a URL is valid before passing it to AVPlayerItem, use for example a simple HEAD request via URLSession to check for a valid 2xx response. ```swift From 9a64cc697da4539ca32d2edd221d3557b5eb3897 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 11:09:20 +0100 Subject: [PATCH 017/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d6b232..d3f7329 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,7 @@ When the URL is syntactically valid but the resource does not actually exist (e. **Workarounds and Best Practices** *Pre-Check the URL With HEAD* -If you want to ensure that a URL is valid before passing it to AVPlayerItem, use for example a simple HEAD request via URLSession to check for a valid 2xx response. +If you want to ensure that a URL is valid before passing it to the component (AVPlayerItem), use for example a simple HEAD request via URLSession to check for a valid 2xx response. ```swift func checkURLExists(_ url: URL) async throws -> Bool { From 96e3165addf5d270046a80ea84f4cdd0a9fa3c93 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 14:55:32 +0100 Subject: [PATCH 018/103] Update ExtPlayerProtocol.swift --- .../protocol/player/ExtPlayerProtocol.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index f90eb67..c9d9fb6 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -212,7 +212,6 @@ internal extension ExtPlayerProtocol { } } - /// Observes the status of an AVPlayerItem and notifies the delegate when the status changes. /// - Parameter item: The AVPlayerItem whose status should be observed. private func observeItemStatus(_ item: AVPlayerItem) { From 79d281070f6a51d62e3dd12c9228e51ba6829cea Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 15:06:47 +0100 Subject: [PATCH 019/103] Update ExtPlayerProtocol.swift --- .../protocol/player/ExtPlayerProtocol.swift | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index c9d9fb6..1182aa5 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -224,22 +224,22 @@ internal extension ExtPlayerProtocol { } switch item.status { - case .unknown: break - case .readyToPlay: - Task { @MainActor in - self?.delegate?.duration(item.duration) - } - case .failed: - Task { @MainActor in - let error = self?.currentItem?.error - self?.onError(.failedToLoad(error)) + case .unknown: break + case .readyToPlay: + Task { @MainActor in + self?.delegate?.duration(item.duration) + } + case .failed: + Task { @MainActor in + let error = self?.currentItem?.error + self?.onError(.failedToLoad(error)) + } + @unknown default: + Task { @MainActor in + let error = self?.currentItem?.error + self?.onError(.failedToLoad(error)) + } } - @unknown default: - Task { @MainActor in - let error = self?.currentItem?.error - self?.onError(.failedToLoad(error)) - } - } } } From d963971aa3db7e2284cbdee01716c40db8bd2167 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 15:11:33 +0100 Subject: [PATCH 020/103] Update ExtPlayerProtocol.swift --- .../protocol/player/ExtPlayerProtocol.swift | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 1182aa5..c5675e4 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -264,24 +264,24 @@ internal extension ExtPlayerProtocol { timeControlObserver = player.observe(\.timeControlStatus, options: [.new, .old]) { [weak self] player, change in switch player.timeControlStatus { - case .paused: - // This could mean playback has stopped, but it's not specific to end of playback - Task { @MainActor in - self?.delegate?.didPausePlayback() - } - case .waitingToPlayAtSpecifiedRate: - // Player is waiting to play (e.g., buffering) - Task { @MainActor in - self?.delegate?.isWaitingToPlay() - } - case .playing: - // Player is currently playing - Task { @MainActor in - self?.delegate?.didStartPlaying() + case .paused: + // This could mean playback has stopped, but it's not specific to end of playback + Task { @MainActor in + self?.delegate?.didPausePlayback() + } + case .waitingToPlayAtSpecifiedRate: + // Player is waiting to play (e.g., buffering) + Task { @MainActor in + self?.delegate?.isWaitingToPlay() + } + case .playing: + // Player is currently playing + Task { @MainActor in + self?.delegate?.didStartPlaying() + } + @unknown default: + break } - @unknown default: - break - } } currentItemObserver = player.observe(\.currentItem, options: [.new, .old, .initial]) { [weak self] player, change in From cf3b86ffc1e62a01ac19582daf5d2e86d96b2f46 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 16:19:30 +0100 Subject: [PATCH 021/103] Update ExtPlayerProtocol.swift --- .../protocol/player/ExtPlayerProtocol.swift | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index c5675e4..504a05d 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -345,26 +345,45 @@ internal extension ExtPlayerProtocol { } /// Sets the playback command for the video player. - /// - Parameter value: The `PlaybackCommand` to set. This can be one of the following: - /// - `play`: Command to play the video. - /// - `pause`: Command to pause the video. - /// - `seek(to:)`: Command to seek to a specific time in the video. - /// - `begin`: Command to position the video at the beginning. - /// - `end`: Command to position the video at the end. - /// - `mute`: Command to mute the video. - /// - `unmute`: Command to unmute the video. - /// - `volume`: Command to adjust the volume of the video playback. - /// - `subtitles`: Command to set subtitles to a specified language or turn them off. - /// - `playbackSpeed`: Command to adjust the playback speed of the video. - /// - `loop`: Command to enable looping of the video playback. - /// - `unloop`: Command to disable looping of the video playback. - /// - `brightness`: Command to adjust the brightness of the video playback. - /// - `contrast`: Command to adjust the contrast of the video playback. - /// - `filter`: Command to apply a specific Core Image filter to the video. - /// - `removeAllFilters`: Command to remove all applied filters from the video playback. - /// - `audioTrack`: Command to select a specific audio track based on language code. - /// - `vector`: Sets a vector graphic operation on the video player. - /// - `removeAllVectors`: Clears all vector graphics from the video player. + /// + /// - Parameter value: The `PlaybackCommand` to set. Available commands include: + /// + /// ### Playback Controls + /// - `play`: Starts video playback. + /// - `pause`: Pauses video playback. + /// - `seek(to:play:)`: Moves to a specified time in the video, with an option to start playing. + /// - `begin`: Moves the video to the beginning. + /// - `end`: Moves the video to the end. + /// + /// ### Audio & Volume + /// - `mute`: Mutes the video. + /// - `unmute`: Unmutes the video. + /// - `volume(level)`: Adjusts the volume to the specified level. + /// - `audioTrack(languageCode)`: Selects an audio track based on the given language code. + /// + /// ### Subtitles & Playback Speed + /// - `subtitles(language)`: Sets subtitles to a specified language or disables them. + /// - `playbackSpeed(speed)`: Adjusts the video playback speed. + /// + /// ### Looping + /// - `loop`: Enables video looping. + /// - `unloop`: Disables video looping. + /// + /// ### Video Adjustments + /// - `brightness(level)`: Adjusts the brightness of the video playback. + /// - `contrast(level)`: Adjusts the contrast of the video playback. + /// + /// ### Filters + /// - `filter(value, clear)`: Applies a specific Core Image filter to the video, optionally clearing previous filters. + /// - `removeAllFilters`: Removes all applied filters from the video playback. + /// + /// ### Vector Graphics + /// - `addVector(builder, clear)`: Adds a vector graphic overlay to the video player, with an option to clear previous vectors. + /// - `removeAllVectors`: Removes all vector graphics from the video player. + /// + /// ### Platform-Specific Features + /// - `startPiP` (iOS only): Starts Picture-in-Picture mode. + /// - `stopPiP` (iOS only): Stops Picture-in-Picture mode. func setCommand(_ value: PlaybackCommand) { switch value { case .play: From b738c9bfd7c954ec8d50f69cfddfb651ba1b874b Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 17:06:57 +0100 Subject: [PATCH 022/103] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d3f7329..542ddc5 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,6 @@ It is a pure package without any third-party libraries. My main focus was on per } ``` -![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/swiftui_video_player.gif) - -## [Documentation(API)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer/main/documentation/swiftui_loop_videoplayer) - ## Philosophy of Player Dynamics The player's functionality is designed around a dual ⇆ interaction model: @@ -34,6 +30,11 @@ The player's functionality is designed around a dual ⇆ interaction model: - **Event Feedback**: Through event handling, the player communicates back to the application, informing it of internal changes that may need attention. Due to the nature of media players, especially in environments with dynamic content or user interactions, the flow of events can become flooded. To manage this effectively and prevent the application from being overwhelmed by the volume of incoming events, the **system collects these events every second and returns them as a batch** +![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/swiftui_video_player.gif) + +## [Documentation(API)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer/main/documentation/swiftui_loop_videoplayer) + + ## Specs | **Feature Category** | **Feature Name** | **Description** | From f7037870e2a97f1ae4836cc6e0a8ef9dad9879a3 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 17:08:38 +0100 Subject: [PATCH 023/103] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 542ddc5..4c12fc4 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,9 @@ The player's functionality is designed around a dual ⇆ interaction model: - **Event Feedback**: Through event handling, the player communicates back to the application, informing it of internal changes that may need attention. Due to the nature of media players, especially in environments with dynamic content or user interactions, the flow of events can become flooded. To manage this effectively and prevent the application from being overwhelmed by the volume of incoming events, the **system collects these events every second and returns them as a batch** -![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/swiftui_video_player.gif) - ## [Documentation(API)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer/main/documentation/swiftui_loop_videoplayer) +![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/swiftui_video_player.gif) ## Specs From e32b8d69b25bf7e1236947b3c0af8d498edccdf6 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 17:14:45 +0100 Subject: [PATCH 024/103] Update VideoSettings.swift --- .../utils/VideoSettings.swift | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift index 61293cd..e424678 100644 --- a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift +++ b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift @@ -58,14 +58,20 @@ public struct VideoSettings: Equatable{ // MARK: - Life circle /// Initializes a new instance of `VideoSettings` with specified values for various video properties. + /// /// - Parameters: - /// - name: The name of the video. - /// - ext: The video file extension. - /// - loop: A Boolean indicating whether the video should loop. - /// - mute: A Boolean indicating whether the video should be muted. - /// - notAutoPlay: A Boolean indicating whether the video should not auto-play. - /// - timePublishing: A `CMTime` value representing the interval for time publishing updates, or `nil`. - /// - gravity: The `AVLayerVideoGravity` value defining how the video should be displayed in its layer. + /// - name: The name of the video file (excluding the extension). + /// - ext: The file extension of the video (e.g., `"mp4"`, `"mov"`). + /// - subtitles: The subtitle file name or identifier to be used for the video. + /// - loop: A Boolean indicating whether the video should continuously loop after playback ends. + /// - pictureInPicture: A Boolean indicating whether Picture-in-Picture (PiP) mode is enabled. + /// - mute: A Boolean indicating whether the video should start muted. + /// - notAutoPlay: A Boolean indicating whether the video should not start playing automatically. + /// - timePublishing: A `CMTime` value representing the interval for time update callbacks, or `nil` if disabled. + /// - gravity: The `AVLayerVideoGravity` value defining how the video should be displayed within its layer. + /// - enableVector: A Boolean indicating whether vector graphics rendering should be enabled for overlays. + /// + /// All parameters must be provided, except `timePublishing`, which can be `nil`, and `enableVector`, which defaults to `false`. public init(name: String, ext: String, subtitles: String, loop: Bool, pictureInPicture: Bool, mute: Bool, notAutoPlay: Bool, timePublishing: CMTime?, gravity: AVLayerVideoGravity, enableVector : Bool = false) { self.name = name self.ext = ext @@ -112,11 +118,6 @@ public struct VideoSettings: Equatable{ @available(iOS 14.0, macOS 11.0, tvOS 14.0, *) public extension VideoSettings { - /// Returns a new instance of VideoSettings with loop set to false and notAutoPlay set to true, keeping other settings unchanged. - var settingsWithAutoPlay : VideoSettings { - VideoSettings(name: self.name, ext: self.ext, subtitles: self.subtitles, loop: self.loop, pictureInPicture: self.pictureInPicture, mute: self.mute, notAutoPlay: false, timePublishing: self.timePublishing, gravity: self.gravity, enableVector: self.vector) - } - func getAssets()-> AVURLAsset?{ assetFor(self) } From 37dc3dcc2c4430834d8225d50382d2749d4a3845 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 17:41:43 +0100 Subject: [PATCH 025/103] update --- .../utils/VideoSettings.swift | 6 +----- .../view/helpers/PlayerCoordinator.swift | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift index e424678..477f7ac 100644 --- a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift +++ b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift @@ -117,11 +117,7 @@ public struct VideoSettings: Equatable{ @available(iOS 14.0, macOS 11.0, tvOS 14.0, *) public extension VideoSettings { - - func getAssets()-> AVURLAsset?{ - assetFor(self) - } - + /// Checks if the asset has changed based on the provided settings and current asset. /// - Parameters: /// - asset: The current asset being played. diff --git a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift index f567527..efe0514 100644 --- a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift +++ b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift @@ -144,12 +144,27 @@ internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { #if os(iOS) extension PlayerCoordinator: AVPictureInPictureControllerDelegate{ + /// Called when Picture-in-Picture (PiP) mode starts. + /// + /// - Parameter pictureInPictureController: The `AVPictureInPictureController` instance managing the PiP session. + /// + /// This method is marked as `nonisolated` to avoid being tied to the actor's execution context, + /// allowing it to be called from any thread. It publishes a `.startedPiP` event on the `eventPublisher` + /// within a `Task` running on the `MainActor`, ensuring UI updates are handled on the main thread. nonisolated func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { Task{ @MainActor in eventPublisher.send(.startedPiP) } } + + /// Called when Picture-in-Picture (PiP) mode stops. + /// + /// - Parameter pictureInPictureController: The `AVPictureInPictureController` instance managing the PiP session. + /// + /// Like its counterpart for starting PiP, this method is `nonisolated`, allowing it to be executed from any thread. + /// It sends a `.stoppedPiP` event via `eventPublisher` on the `MainActor`, ensuring any UI-related handling + /// occurs safely on the main thread. nonisolated func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { Task{ @MainActor in eventPublisher.send(.stoppedPiP) From a43c2480df0ffd46060c7daa449adf8410f1e4e1 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 17:49:05 +0100 Subject: [PATCH 026/103] Update fn+.swift --- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index dd287fe..adca4b7 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -66,7 +66,7 @@ fileprivate func assetFrom(name: String, fileExtension: String?) -> AVURLAsset? /// Attempts to create a valid `URL` from a string that starts with `"file://"`. /// - Parameter rawString: A file URL string, e.g. `"file:///Users/igor/My Folder/File.mp4"`. /// - Returns: A `URL` if successfully parsed; otherwise `nil`. -func fileURL(from rawString: String) -> URL? { +public func fileURL(from rawString: String) -> URL? { guard rawString.hasPrefix("file://") else { // Not a file URL scheme return nil @@ -110,7 +110,7 @@ fileprivate func extractExtension(from name: String) -> String? { /// - contrast: A Float value representing the contrast adjustment to apply. /// /// - Returns: An array of CIFilter objects, including the original filters and the added brightness and contrast adjustments. -internal func combineFilters(_ filters: [CIFilter],_ brightness: Float,_ contrast: Float) -> [CIFilter] { +func combineFilters(_ filters: [CIFilter],_ brightness: Float,_ contrast: Float) -> [CIFilter] { var allFilters = filters if let filter = CIFilter(name: "CIColorControls", parameters: [kCIInputBrightnessKey: brightness]) { allFilters.append(filter) @@ -130,7 +130,7 @@ internal func combineFilters(_ filters: [CIFilter],_ brightness: Float,_ contra /// /// The function starts by clamping the source image to ensure coordinates remain within the image bounds, /// applies each filter in the provided array, and completes by returning the modified image to the composition request. -internal func handleVideoComposition(request: AVAsynchronousCIImageFilteringRequest, filters: [CIFilter]) { +func handleVideoComposition(request: AVAsynchronousCIImageFilteringRequest, filters: [CIFilter]) { // Start with the source image, ensuring it's clamped to avoid any coordinate issues var currentImage = request.sourceImage.clampedToExtent() From d0491dbb0f93035b223af045feb08b2976d38393 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 18:00:44 +0100 Subject: [PATCH 027/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c12fc4..3f38685 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | Name | Description | Default | |---------------|-----------------------------------------------------------------------------------------------------|---------| -| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). **Local File URL** If the name is a valid local file path (file:// scheme). **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | +| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). `
` **Local File URL** If the name is a valid local file path (file:// scheme). `
` **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | | **Ext** | File extension for the video, used when loading from local resources. This is optional when a URL is provided and the URL ends with the video file extension. | "mp4" | | **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | - | | **Gravity** | How the video content should be resized to fit the player's bounds. | .resizeAspect | From 3f21358c229685975269513cb4751781ca267b4c Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 18:01:47 +0100 Subject: [PATCH 028/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f38685..e3d785d 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | Name | Description | Default | |---------------|-----------------------------------------------------------------------------------------------------|---------| -| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). `
` **Local File URL** If the name is a valid local file path (file:// scheme). `
` **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | +| **SourceName** | `**Direct URL String** If the name represents a valid URL ( HTTP etc). `
` **Local File URL** If the name is a valid local file path (file:// scheme). `
` **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:)` | - | | **Ext** | File extension for the video, used when loading from local resources. This is optional when a URL is provided and the URL ends with the video file extension. | "mp4" | | **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | - | | **Gravity** | How the video content should be resized to fit the player's bounds. | .resizeAspect | From 059bf420683a2e4efbfa39e375cbaf2757ac6c39 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 18:02:06 +0100 Subject: [PATCH 029/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3d785d..4c12fc4 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | Name | Description | Default | |---------------|-----------------------------------------------------------------------------------------------------|---------| -| **SourceName** | `**Direct URL String** If the name represents a valid URL ( HTTP etc). `
` **Local File URL** If the name is a valid local file path (file:// scheme). `
` **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:)` | - | +| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). **Local File URL** If the name is a valid local file path (file:// scheme). **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | | **Ext** | File extension for the video, used when loading from local resources. This is optional when a URL is provided and the URL ends with the video file extension. | "mp4" | | **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | - | | **Gravity** | How the video content should be resized to fit the player's bounds. | .resizeAspect | From fb89d7951189f22878e4efa9a2084f51364107df Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 18:02:33 +0100 Subject: [PATCH 030/103] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c12fc4..682d79a 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,9 @@ Please note that using videos from URLs requires ensuring that you have the righ | Name | Description | Default | |---------------|-----------------------------------------------------------------------------------------------------|---------| -| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). **Local File URL** If the name is a valid local file path (file:// scheme). **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | +| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). +**Local File URL** If the name is a valid local file path (file:// scheme). +**Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | | **Ext** | File extension for the video, used when loading from local resources. This is optional when a URL is provided and the URL ends with the video file extension. | "mp4" | | **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | - | | **Gravity** | How the video content should be resized to fit the player's bounds. | .resizeAspect | From 06700549c8f59ad306977cc5e80514dc0b2e048a Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 18:02:52 +0100 Subject: [PATCH 031/103] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 682d79a..4c12fc4 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | Name | Description | Default | |---------------|-----------------------------------------------------------------------------------------------------|---------| -| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). -**Local File URL** If the name is a valid local file path (file:// scheme). -**Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | +| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). **Local File URL** If the name is a valid local file path (file:// scheme). **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | | **Ext** | File extension for the video, used when loading from local resources. This is optional when a URL is provided and the URL ends with the video file extension. | "mp4" | | **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | - | | **Gravity** | How the video content should be resized to fit the player's bounds. | .resizeAspect | From c20d5581819cef16dcdea0e42009e8b7c49e8409 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 18:04:09 +0100 Subject: [PATCH 032/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c12fc4..ae8e548 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | Name | Description | Default | |---------------|-----------------------------------------------------------------------------------------------------|---------| -| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). **Local File URL** If the name is a valid local file path (file:// scheme). **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | +| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). **Local File URL** If the name is a valid local file path (file:// scheme). **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | | **Ext** | File extension for the video, used when loading from local resources. This is optional when a URL is provided and the URL ends with the video file extension. | "mp4" | | **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | - | | **Gravity** | How the video content should be resized to fit the player's bounds. | .resizeAspect | From 2dc37551df74d2ca63cebd509327ccbec7ce41fe Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 18:05:12 +0100 Subject: [PATCH 033/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae8e548..b840fb0 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | Name | Description | Default | |---------------|-----------------------------------------------------------------------------------------------------|---------| -| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). **Local File URL** If the name is a valid local file path (file:// scheme). **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | +| **SourceName** | - **Direct URL String** If the name represents a valid URL ( HTTP etc). - **Local File URL** If the name is a valid local file path (file:// scheme). - **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | | **Ext** | File extension for the video, used when loading from local resources. This is optional when a URL is provided and the URL ends with the video file extension. | "mp4" | | **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | - | | **Gravity** | How the video content should be resized to fit the player's bounds. | .resizeAspect | From c7fc95510d78b858a6e5b9fe4b7980a2ec0e8607 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 18:05:36 +0100 Subject: [PATCH 034/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b840fb0..4c12fc4 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | Name | Description | Default | |---------------|-----------------------------------------------------------------------------------------------------|---------| -| **SourceName** | - **Direct URL String** If the name represents a valid URL ( HTTP etc). - **Local File URL** If the name is a valid local file path (file:// scheme). - **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | +| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). **Local File URL** If the name is a valid local file path (file:// scheme). **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | | **Ext** | File extension for the video, used when loading from local resources. This is optional when a URL is provided and the URL ends with the video file extension. | "mp4" | | **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | - | | **Gravity** | How the video content should be resized to fit the player's bounds. | .resizeAspect | From bb59ecef83df0c5b65e6e35470214512687433b6 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 11 Feb 2025 18:07:19 +0100 Subject: [PATCH 035/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c12fc4..3bffd23 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | Name | Description | Default | |---------------|-----------------------------------------------------------------------------------------------------|---------| -| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc). **Local File URL** If the name is a valid local file path (file:// scheme). **Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | +| **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc).
**Local File URL** If the name is a valid local file path (file:// scheme).
**Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | | **Ext** | File extension for the video, used when loading from local resources. This is optional when a URL is provided and the URL ends with the video file extension. | "mp4" | | **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | - | | **Gravity** | How the video content should be resized to fit the player's bounds. | .resizeAspect | From dbc9a95fa56a52fdbf615c1211ed9b99c5c0c0d8 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 12:16:18 +0100 Subject: [PATCH 036/103] update --- README.md | 4 + .../ExtVideoPlayer.swift | 17 ++- .../enum/PlayerEvent.swift | 2 +- .../enum/PlayerEventFilter.swift | 115 ++++++++++++++++++ .../enum/Setting.swift | 2 + .../settings/Events.swift | 28 +++++ .../utils/VideoSettings.swift | 9 +- 7 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift create mode 100644 Sources/swiftui-loop-videoplayer/settings/Events.swift diff --git a/README.md b/README.md index 3bffd23..0a63e73 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,8 @@ Please note that using videos from URLs requires ensuring that you have the righ | **NotAutoPlay** | Indicates if the video should not play after initialization. Notice that if you use `command` as a control flow for the player the start command should be `.idle` | false | | **EnableVector** | Use this struct to activate settings that allow the addition of vector-based overlays via commands. If it is not passed via settings, any commands to `addVector` or `removeAllVectors` will have no effect. | Not Enabled | |**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| +|**Events([.durationAny, .itemStatusChangedAny])**| if `Events` is not passed then events mecanism is disabled. You can specify wich events you'd like to receave like `.itemStatusChangedAny` or simply passed `.all` and receave any avalble events. This settings added to improve performance as events emition soes via @State change that triggers view update if you don't need observe any events then disabled events improve performance significantly | - | +| Events([.durationAny, .itemStatusChangedAny]) | If Events is not passed, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | - | ### Additional Notes on Settings @@ -274,6 +276,8 @@ video_main.m3u8 ## Player Events + *If Events is not passed in the settings, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance.* + | Event | Description | |------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| | `seek(Bool, currentTime: Double)` | Represents an end seek action within the player. The first parameter (`Bool`) indicates whether the seek was successful, and the second parameter (`currentTime`) provides the time (in seconds) to which the player is seeking. | diff --git a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift index 1abb91a..1a9c9cb 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -106,9 +106,24 @@ public struct ExtVideoPlayer: View{ currentTime = time }) .onReceive(eventPublisher.collect(.byTime(DispatchQueue.main, .seconds(1))), perform: { event in - playerEvent = event + settings.events + playerEvent = filterEvents(with: settings, for: event) }) .preference(key: CurrentTimePreferenceKey.self, value: currentTime) .preference(key: PlayerEventPreferenceKey.self, value: playerEvent) } } + +fileprivate func filterEvents(with settings: VideoSettings, for events: [PlayerEvent]) -> [PlayerEvent] { + let filters = settings.events // `[PlayerEventFilter]` + + // If no filters are provided, return an empty array (or all events—your choice). + guard !filters.isEmpty else { + return [] + } + + // Keep each `PlayerEvent` only if it matches *at least* one filter in `filters`. + return events.filter { event in + filters.contains(event) + } +} diff --git a/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift b/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift index 6a1a169..0398f7b 100644 --- a/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift +++ b/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift @@ -11,7 +11,7 @@ import AVFoundation /// An enumeration representing various events that can occur within a media player. @available(iOS 14.0, macOS 11.0, tvOS 14.0, *) public enum PlayerEvent: Equatable { - + /// Represents an end seek action within the player. /// - Parameters: /// - Bool: Indicates whether the seek was successful. diff --git a/Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift b/Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift new file mode 100644 index 0000000..1ccd096 --- /dev/null +++ b/Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift @@ -0,0 +1,115 @@ +// +// PlayerEventFilter.swift +// swiftui-loop-videoplayer +// +// Created by Igor on 12.02.25. +// + +import Foundation + +/// A "parallel" structure for filtering PlayerEvent. +/// Each case here: +/// 1) Either ignores associated values (xxxAny) +/// 2) Or matches cases that have no associated values in PlayerEvent. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, *) +public enum PlayerEventFilter { + /// Matches any `.seek(...)` case, regardless of Bool or currentTime + case seekAny + + /// Matches `.paused` exactly (no associated values) + case paused + + /// Matches `.waitingToPlayAtSpecifiedRate` (no associated values) + case waitingToPlayAtSpecifiedRate + + /// Matches `.playing` (no associated values) + case playing + + /// Matches any `.currentItemChanged(...)` case + case currentItemChangedAny + + /// Matches `.currentItemRemoved` exactly (no associated values) + case currentItemRemoved + + /// Matches any `.volumeChanged(...)` case + case volumeChangedAny + + /// Matches any `.error(...)` case + case errorAny + + /// Matches any `.boundsChanged(...)` case + case boundsChangedAny + + /// Matches `.startedPiP` (no associated values) + case startedPiP + + /// Matches `.stoppedPiP` (no associated values) + case stoppedPiP + + /// Matches any `.itemStatusChanged(...)` case + case itemStatusChangedAny + + /// Matches any `.duration(...)` case + case durationAny + + /// Matches every possible event + case all +} + +extension PlayerEventFilter { + /// Checks whether a given `PlayerEvent` matches this filter. + /// + /// - Parameter event: The `PlayerEvent` to inspect. + /// - Returns: `true` if the event belongs to this case (ignoring parameters), `false` otherwise. + func matches(_ event: PlayerEvent) -> Bool { + switch (self, event) { + /// Universal case + case (.all, _): + return true + + // Compare by case name only, ignoring associated values + case (.seekAny, .seek): + return true + case (.paused, .paused): + return true + case (.waitingToPlayAtSpecifiedRate, .waitingToPlayAtSpecifiedRate): + return true + case (.playing, .playing): + return true + case (.currentItemChangedAny, .currentItemChanged): + return true + case (.currentItemRemoved, .currentItemRemoved): + return true + case (.volumeChangedAny, .volumeChanged): + return true + case (.errorAny, .error): + return true + case (.boundsChangedAny, .boundsChanged): + return true + case (.startedPiP, .startedPiP): + return true + case (.stoppedPiP, .stoppedPiP): + return true + case (.itemStatusChangedAny, .itemStatusChanged): + return true + case (.durationAny, .duration): + return true + + // Default fallback: no match + default: + return false + } + } +} + +extension Collection where Element == PlayerEventFilter { + /// Checks whether any filter in this collection matches the given `PlayerEvent`. + /// + /// - Parameter event: The `PlayerEvent` to test. + /// - Returns: `true` if at least one `PlayerEventFilter` in this collection matches the `event`; otherwise, `false`. + func contains(_ event: PlayerEvent) -> Bool { + return self.contains { filter in + filter.matches(event) + } + } +} diff --git a/Sources/swiftui-loop-videoplayer/enum/Setting.swift b/Sources/swiftui-loop-videoplayer/enum/Setting.swift index d4c57a0..e4cf3ca 100644 --- a/Sources/swiftui-loop-videoplayer/enum/Setting.swift +++ b/Sources/swiftui-loop-videoplayer/enum/Setting.swift @@ -21,6 +21,8 @@ public enum Setting: Equatable, SettingsConvertible{ [self] } + case events([PlayerEventFilter]) + ///Enable vector layer to add overlay vector graphics case vector diff --git a/Sources/swiftui-loop-videoplayer/settings/Events.swift b/Sources/swiftui-loop-videoplayer/settings/Events.swift new file mode 100644 index 0000000..674f63c --- /dev/null +++ b/Sources/swiftui-loop-videoplayer/settings/Events.swift @@ -0,0 +1,28 @@ +// +// Events.swift +// +// +// Created by Igor Shelopaev on 14.01.25. +// + +import Foundation + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, *) +public struct Events: SettingsConvertible{ + + /// Holds the specific AVLayerVideoGravity setting defining how video content should align within its layer. + private let value : [PlayerEventFilter] + + // MARK: - Life circle + + /// Initializes a new instance + public init(_ value : [PlayerEventFilter]) { + self.value = value + } + + /// Fetch settings + @_spi(Private) + public func asSettings() -> [Setting] { + [.events(value)] + } +} diff --git a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift index 477f7ac..542b206 100644 --- a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift +++ b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift @@ -35,6 +35,9 @@ public struct VideoSettings: Equatable{ /// Enable vector layer to add overlay vector graphics public let vector: Bool + /// Disable events + public let events: [PlayerEventFilter] + /// Don't auto play video after initialization public let notAutoPlay: Bool @@ -72,7 +75,7 @@ public struct VideoSettings: Equatable{ /// - enableVector: A Boolean indicating whether vector graphics rendering should be enabled for overlays. /// /// All parameters must be provided, except `timePublishing`, which can be `nil`, and `enableVector`, which defaults to `false`. - public init(name: String, ext: String, subtitles: String, loop: Bool, pictureInPicture: Bool, mute: Bool, notAutoPlay: Bool, timePublishing: CMTime?, gravity: AVLayerVideoGravity, enableVector : Bool = false) { + public init(name: String, ext: String, subtitles: String, loop: Bool, pictureInPicture: Bool, mute: Bool, notAutoPlay: Bool, timePublishing: CMTime?, gravity: AVLayerVideoGravity, enableVector : Bool = false, events : [PlayerEventFilter] = []) { self.name = name self.ext = ext self.subtitles = subtitles @@ -83,6 +86,7 @@ public struct VideoSettings: Equatable{ self.timePublishing = timePublishing self.gravity = gravity self.vector = enableVector + self.events = events self.unique = true } @@ -112,6 +116,8 @@ public struct VideoSettings: Equatable{ notAutoPlay = settings.contains(.notAutoPlay) vector = settings.contains(.vector) + + events = settings.fetch(by : "events", defaulted: []) } } @@ -145,4 +151,3 @@ fileprivate func check(_ settings : [Setting]) -> Bool{ let set = Set(cases) return cases.count == set.count } - From b3fd9285f9691147acb5da2ef42e440837627112 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 12:18:46 +0100 Subject: [PATCH 037/103] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0a63e73..d5b51a2 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | **EnableVector** | Use this struct to activate settings that allow the addition of vector-based overlays via commands. If it is not passed via settings, any commands to `addVector` or `removeAllVectors` will have no effect. | Not Enabled | |**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| |**Events([.durationAny, .itemStatusChangedAny])**| if `Events` is not passed then events mecanism is disabled. You can specify wich events you'd like to receave like `.itemStatusChangedAny` or simply passed `.all` and receave any avalble events. This settings added to improve performance as events emition soes via @State change that triggers view update if you don't need observe any events then disabled events improve performance significantly | - | -| Events([.durationAny, .itemStatusChangedAny]) | If Events is not passed, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | - | +| Events([.durationAny, .itemStatusChangedAny]) | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | - | ### Additional Notes on Settings @@ -276,7 +276,7 @@ video_main.m3u8 ## Player Events - *If Events is not passed in the settings, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance.* + *If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance.* | Event | Description | |------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| From a51cdf87aec92a6b6cd7ff9ab6c9e8a1bbce9a3b Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 12:19:23 +0100 Subject: [PATCH 038/103] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index d5b51a2..4d1a303 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,6 @@ Please note that using videos from URLs requires ensuring that you have the righ | **NotAutoPlay** | Indicates if the video should not play after initialization. Notice that if you use `command` as a control flow for the player the start command should be `.idle` | false | | **EnableVector** | Use this struct to activate settings that allow the addition of vector-based overlays via commands. If it is not passed via settings, any commands to `addVector` or `removeAllVectors` will have no effect. | Not Enabled | |**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| -|**Events([.durationAny, .itemStatusChangedAny])**| if `Events` is not passed then events mecanism is disabled. You can specify wich events you'd like to receave like `.itemStatusChangedAny` or simply passed `.all` and receave any avalble events. This settings added to improve performance as events emition soes via @State change that triggers view update if you don't need observe any events then disabled events improve performance significantly | - | | Events([.durationAny, .itemStatusChangedAny]) | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | - | ### Additional Notes on Settings From aa4630a2c6f2768bf2d7cd1f086a3a7d42cc7a0a Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 12:19:50 +0100 Subject: [PATCH 039/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d1a303..325533f 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | **NotAutoPlay** | Indicates if the video should not play after initialization. Notice that if you use `command` as a control flow for the player the start command should be `.idle` | false | | **EnableVector** | Use this struct to activate settings that allow the addition of vector-based overlays via commands. If it is not passed via settings, any commands to `addVector` or `removeAllVectors` will have no effect. | Not Enabled | |**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| -| Events([.durationAny, .itemStatusChangedAny]) | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | - | +| **Events([.durationAny, .itemStatusChangedAny])** | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | - | ### Additional Notes on Settings From 9eb004c3ce3a4e31541200184c46afb6ad625369 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 12:20:35 +0100 Subject: [PATCH 040/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 325533f..40d87bc 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | **Mute** | Indicates if the video should play without sound. | false | | **NotAutoPlay** | Indicates if the video should not play after initialization. Notice that if you use `command` as a control flow for the player the start command should be `.idle` | false | | **EnableVector** | Use this struct to activate settings that allow the addition of vector-based overlays via commands. If it is not passed via settings, any commands to `addVector` or `removeAllVectors` will have no effect. | Not Enabled | -|**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| +|**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| - | | **Events([.durationAny, .itemStatusChangedAny])** | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | - | ### Additional Notes on Settings From 1108a1a08f7568fd33c585ac66cf688244aa4fc3 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 12:21:50 +0100 Subject: [PATCH 041/103] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 40d87bc..2b73c14 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ It is a pure package without any third-party libraries. My main focus was on per ```swift ExtVideoPlayer{ VideoSettings{ - SourceName("swipe") + SourceName("swipe") + Events([.all]) } } ``` From 47960c28de0545ff543e8b496a5c8ab0b1675ed0 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 12:23:16 +0100 Subject: [PATCH 042/103] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2b73c14..e471130 100644 --- a/README.md +++ b/README.md @@ -351,6 +351,7 @@ or in a declarative way Ext("mp8") // Set default extension here If not provided then mp4 is default Gravity(.resizeAspectFill) TimePublishing() + Events([.durationAny, .itemStatusChangedAny]) } } .onPlayerTimeChange { newTime in From 34eda22f761eeb07833def14653e7835b1217c73 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 12:25:54 +0100 Subject: [PATCH 043/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e471130..ccd86ca 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The player's functionality is designed around a dual ⇆ interaction model: - **Commands and Settings**: Through these, you instruct the player on what to do and how to do it. Settings define the environment and initial state, while commands offer real-time control. As for now, you can conveniently pass command by command; perhaps later I’ll add support for batch commands -- **Event Feedback**: Through event handling, the player communicates back to the application, informing it of internal changes that may need attention. Due to the nature of media players, especially in environments with dynamic content or user interactions, the flow of events can become flooded. To manage this effectively and prevent the application from being overwhelmed by the volume of incoming events, the **system collects these events every second and returns them as a batch** +- **Event Feedback**: Through event handling, the player communicates back to the application, informing it of internal changes that may need attention. Due to the nature of media players, especially in environments with dynamic content or user interactions, the flow of events can become flooded. To manage this effectively and prevent the application from being overwhelmed by the volume of incoming events, the **system collects these events every second and returns them as a batch** Pass `Events([.all])` in the `settings` to enable event mechanism. ## [Documentation(API)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer/main/documentation/swiftui_loop_videoplayer) From 2da461e55732a1f0568c4b11b95c0e4689477867 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 12:34:38 +0100 Subject: [PATCH 044/103] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index ccd86ca..d35e3fa 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,7 @@ It is a pure package without any third-party libraries. My main focus was on per ```swift ExtVideoPlayer{ VideoSettings{ - SourceName("swipe") - Events([.all]) + SourceName("swipe") } } ``` From e0aabe2b0af76a3ef6ec50a6ac89489ed991ca34 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 12:57:58 +0100 Subject: [PATCH 045/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d35e3fa..77b7617 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The player's functionality is designed around a dual ⇆ interaction model: - **Commands and Settings**: Through these, you instruct the player on what to do and how to do it. Settings define the environment and initial state, while commands offer real-time control. As for now, you can conveniently pass command by command; perhaps later I’ll add support for batch commands -- **Event Feedback**: Through event handling, the player communicates back to the application, informing it of internal changes that may need attention. Due to the nature of media players, especially in environments with dynamic content or user interactions, the flow of events can become flooded. To manage this effectively and prevent the application from being overwhelmed by the volume of incoming events, the **system collects these events every second and returns them as a batch** Pass `Events([.all])` in the `settings` to enable event mechanism. +- **Event Feedback**: Through event handling, the player communicates back to the application, informing it of internal changes that may need attention. Due to the nature of media players, especially in environments with dynamic content or user interactions, the flow of events can become flooded. To manage this effectively and prevent the application from being overwhelmed by the volume of incoming events, the **system collects these events every second and returns them as a batch**. Pass `Events([.all])` in the `settings` to enable event mechanism. ## [Documentation(API)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer/main/documentation/swiftui_loop_videoplayer) From d5535c71935be686dff1ecc690ad572b43109847 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 16:01:56 +0100 Subject: [PATCH 046/103] update --- README.md | 35 +++++++++++++++++++ .../ExtVideoPlayer.swift | 1 - 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 77b7617..abbb1a9 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,41 @@ video_main.m3u8 | `itemStatusChanged(AVPlayerItem.Status)` | Indicates that the AVPlayerItem's status has changed. Possible statuses: `.unknown`, `.readyToPlay`, `.failed`. | | `duration(CMTime)` | Provides the duration of the AVPlayerItem when it is ready to play. The duration is given in `CMTime`. | +## Player event filter +This enum provides a structured way to filter `PlayerEvent` cases. + ```swift + ExtVideoPlayer{ + VideoSettings{ + SourceName("swipe") + Events([.durationAny, .itemStatusChangedAny]) + } + } + .onPlayerTimeChange { newTime in + // Hear comes only events [.durationAny, .itemStatusChangedAny] Any duration event and any itemStatus value events + } +``` + +### Event filter table + +| **Filter** | **Description** | +|------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| +| `seekAny` | Matches any `.seek(...)` case, regardless of whether the seek was successful (`Bool`) or the target seek time (`currentTime: Double`). | +| `paused` | Matches exactly the `.paused` event, which indicates that playback has been paused by the user or programmatically. | +| `waitingToPlayAtSpecifiedRate` | Matches exactly the `.waitingToPlayAtSpecifiedRate` event, which occurs when the player is buffering or waiting for sufficient data. | +| `playing` | Matches exactly the `.playing` event, indicating that the player is actively playing media. | +| `currentItemChangedAny` | Matches any `.currentItemChanged(...)` case, triggered when the player's `currentItem` is updated to a new media item. | +| `currentItemRemoved` | Matches exactly the `.currentItemRemoved` event, occurring when the player's `currentItem` is set to `nil`. | +| `errorAny` | Matches any `.error(...)` case, representing an error within the player, with a `VPErrors` enum indicating the specific issue. | +| `volumeChangedAny` | Matches any `.volumeChanged(...)` case, triggered when the player's volume level is adjusted. | +| `boundsChangedAny` | Matches any `.boundsChanged(...)` case, triggered when the bounds of the main layer change. | +| `startedPiP` | Matches exactly the `.startedPiP` event, triggered when Picture-in-Picture (PiP) mode starts. | +| `stoppedPiP` | Matches exactly the `.stoppedPiP` event, triggered when Picture-in-Picture (PiP) mode stops. | +| `itemStatusChangedAny` | Matches any `.itemStatusChanged(...)` case, indicating that the AVPlayerItem's status has changed (e.g., `.unknown`, `.readyToPlay`, `.failed`). | +| `durationAny` | Matches any `.duration(...)` case, which provides the duration of the media item when ready to play. | +| `all` | Matches every possible player event. | + + + ### Additional Notes on Errors When the URL is syntactically valid but the resource does not actually exist (e.g., a 404 response or an unreachable server), AVPlayerItem.status can remain .unknown indefinitely. It may never transition to .failed, and the .AVPlayerItemFailedToPlayToEndTime notification won’t fire if playback never starts. diff --git a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift index 1a9c9cb..0d61a2e 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -106,7 +106,6 @@ public struct ExtVideoPlayer: View{ currentTime = time }) .onReceive(eventPublisher.collect(.byTime(DispatchQueue.main, .seconds(1))), perform: { event in - settings.events playerEvent = filterEvents(with: settings, for: event) }) .preference(key: CurrentTimePreferenceKey.self, value: currentTime) From cea0e661683aca585f00977c3ba688326513d4fe Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 16:05:52 +0100 Subject: [PATCH 047/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index abbb1a9..9d7622c 100644 --- a/README.md +++ b/README.md @@ -299,7 +299,7 @@ This enum provides a structured way to filter `PlayerEvent` cases. ExtVideoPlayer{ VideoSettings{ SourceName("swipe") - Events([.durationAny, .itemStatusChangedAny]) + Events([.durationAny, .itemStatusChangedAny]) // *Events([PlayerEventFilter])* } } .onPlayerTimeChange { newTime in From beec3b70db4df30c0f0d5a83f1cde55d0aac9df9 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 16:14:04 +0100 Subject: [PATCH 048/103] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9d7622c..b1c7b0f 100644 --- a/README.md +++ b/README.md @@ -303,8 +303,8 @@ This enum provides a structured way to filter `PlayerEvent` cases. } } .onPlayerTimeChange { newTime in - // Hear comes only events [.durationAny, .itemStatusChangedAny] Any duration event and any itemStatus value events - } + // Here come only events [.durationAny, .itemStatusChangedAny]: any duration and any item status change events. + } ``` ### Event filter table From 4890f5068e98199501a9248d87eb3a82b89b30c8 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 16:20:53 +0100 Subject: [PATCH 049/103] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b1c7b0f..e194cfe 100644 --- a/README.md +++ b/README.md @@ -294,8 +294,9 @@ video_main.m3u8 | `duration(CMTime)` | Provides the duration of the AVPlayerItem when it is ready to play. The duration is given in `CMTime`. | ## Player event filter -This enum provides a structured way to filter `PlayerEvent` cases. - ```swift +`PlayerEventFilter` - this enum provides a structured way to filter `PlayerEvent` cases. I decided to introduce an additional structure that corresponds to PlayerEvent. All cases with parameters are replicated as *eventName***Any** to match any variation of that event, regardless of its associated values. + +```swift ExtVideoPlayer{ VideoSettings{ SourceName("swipe") From bf1afa3d62fa7aef2ddd245e8b56a947027db2ff Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 12 Feb 2025 16:21:44 +0100 Subject: [PATCH 050/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e194cfe..a89e520 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ video_main.m3u8 | `duration(CMTime)` | Provides the duration of the AVPlayerItem when it is ready to play. The duration is given in `CMTime`. | ## Player event filter -`PlayerEventFilter` - this enum provides a structured way to filter `PlayerEvent` cases. I decided to introduce an additional structure that corresponds to PlayerEvent. All cases with parameters are replicated as *eventName***Any** to match any variation of that event, regardless of its associated values. +`PlayerEventFilter` - this enum provides a structured way to filter `PlayerEvent` cases. I decided to introduce an additional structure that corresponds to `PlayerEvent`. All cases with parameters are replicated as *eventName***Any** to match any variation of that event, regardless of its associated values. ```swift ExtVideoPlayer{ From 8b02317b6123750818b993b1561347489be4c0f3 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 13 Feb 2025 11:09:49 +0100 Subject: [PATCH 051/103] Update ExtVideoPlayer.swift --- Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift index 0d61a2e..bb1e6be 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -113,10 +113,18 @@ public struct ExtVideoPlayer: View{ } } +// MARK: - Fileprivate + +/// Filters a list of `PlayerEvent` instances based on the provided `VideoSettings`. +/// +/// - Parameters: +/// - settings: The video settings containing event filters. +/// - events: The list of events to be filtered. +/// - Returns: A filtered list of `PlayerEvent` that match at least one filter in `settings`. fileprivate func filterEvents(with settings: VideoSettings, for events: [PlayerEvent]) -> [PlayerEvent] { let filters = settings.events // `[PlayerEventFilter]` - // If no filters are provided, return an empty array (or all events—your choice). + // If no filters are provided, return an empty array. guard !filters.isEmpty else { return [] } From f76e93754db3365704d4669edaec671902ed5981 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 13 Feb 2025 14:52:16 +0100 Subject: [PATCH 052/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a89e520..207a161 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,7 @@ video_main.m3u8 Events([.durationAny, .itemStatusChangedAny]) // *Events([PlayerEventFilter])* } } - .onPlayerTimeChange { newTime in + .onPlayerEventChange { events in // Here come only events [.durationAny, .itemStatusChangedAny]: any duration and any item status change events. } ``` From 588028731e5bbf8836ee9e7e1588c61c63888eb5 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 10:02:12 +0100 Subject: [PATCH 053/103] Update README.md --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 207a161..a235c1b 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,13 @@ [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoplayer%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoPlayer%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoPlayer) +It is a pure package without any third-party libraries. My main focus was on performance. Especially if you need to add a video in the background as a design element, in such cases, you’d want a lightweight component without a lot of unnecessary features. **I hope it serves you well**. + ## Why if we have Apple’s VideoPlayer ?! -Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for example it doesn’t allow you to hide or customize the default video controls UI, limiting its use for custom scenarios. In contrast, this solution provides full control over playback, including the ability to disable or hide UI elements, making it suitable for background videos, tooltips, and video hints etc. Additionally, it supports advanced features like subtitles, seamless looping and real-time filter application, adding vector graphics upon the video stream etc. This package uses a declarative approach to declare parameters for the video component based on building blocks. This implementation might give some insights into how SwiftUI works under the hood. You can also pass parameters in the common way. *If you profile the package, do it on a real device.* -It is a pure package without any third-party libraries. My main focus was on performance. Especially if you need to add a video in the background as a design element, in such cases, you’d want a lightweight component without a lot of unnecessary features. **I hope it serves you well**. +Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI. However, it does not allow you to hide or customize the default video controls UI, limiting its use in custom scenarios. Alternatively, you would need to use AVPlayerViewController, which includes a lot of functionality just to disable the controls. In contrast, this solution acts as a modular framework, allowing you to integrate only the functionalities you need while keeping the core component lightweight. It provides full control over playback, including the ability to add custom UI elements, making it ideal for background videos, tooltips, video hints, and other custom scenarios. Additionally, it supports advanced features like subtitles, seamless looping and real-time filter application, adding vector graphics upon the video stream etc. This package uses a declarative approach to declare parameters for the video component based on building blocks. This implementation might give some insights into how SwiftUI works under the hood. You can also pass parameters in the common way. *If you profile the package, do it on a real device.* + + From 40b3babdc6259b074db441d290b3bbca7cf0b99a Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 10:08:34 +0100 Subject: [PATCH 054/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a235c1b..0d64d87 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is a pure package without any third-party libraries. My main focus was on per ## Why if we have Apple’s VideoPlayer ?! -Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI. However, it does not allow you to hide or customize the default video controls UI, limiting its use in custom scenarios. Alternatively, you would need to use AVPlayerViewController, which includes a lot of functionality just to disable the controls. In contrast, this solution acts as a modular framework, allowing you to integrate only the functionalities you need while keeping the core component lightweight. It provides full control over playback, including the ability to add custom UI elements, making it ideal for background videos, tooltips, video hints, and other custom scenarios. Additionally, it supports advanced features like subtitles, seamless looping and real-time filter application, adding vector graphics upon the video stream etc. This package uses a declarative approach to declare parameters for the video component based on building blocks. This implementation might give some insights into how SwiftUI works under the hood. You can also pass parameters in the common way. *If you profile the package, do it on a real device.* +Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI. However, it does not allow you to hide or customize the default video controls UI, limiting its use in custom scenarios. Alternatively, you would need to use AVPlayerViewController, which includes a lot of functionality just to disable the controls. In contrast, this solution acts as a modular framework, allowing you to integrate only the functionalities you need while keeping the core component lightweight. It provides full control over playback, including the ability to add custom UI elements, making it ideal for background videos, tooltips, video hints, and other custom scenarios. Additionally, it supports advanced features such as subtitles, seamless looping, and real-time filter application. It also allows adding vector graphics to the video stream, accessing frame data for custom filtering, and applying ML or AI algorithms to video processing, enabling a wide range of enhancements and modifications etc. This package uses a declarative approach to declare parameters for the video component based on building blocks. This implementation might give some insights into how SwiftUI works under the hood. You can also pass parameters in the common way. *If you profile the package, do it on a real device.* From eb962f50969d1208fdb7dfbae25f27c2dec6e3f5 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 10:09:09 +0100 Subject: [PATCH 055/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d64d87..72caff2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is a pure package without any third-party libraries. My main focus was on per ## Why if we have Apple’s VideoPlayer ?! -Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI. However, it does not allow you to hide or customize the default video controls UI, limiting its use in custom scenarios. Alternatively, you would need to use AVPlayerViewController, which includes a lot of functionality just to disable the controls. In contrast, this solution acts as a modular framework, allowing you to integrate only the functionalities you need while keeping the core component lightweight. It provides full control over playback, including the ability to add custom UI elements, making it ideal for background videos, tooltips, video hints, and other custom scenarios. Additionally, it supports advanced features such as subtitles, seamless looping, and real-time filter application. It also allows adding vector graphics to the video stream, accessing frame data for custom filtering, and applying ML or AI algorithms to video processing, enabling a wide range of enhancements and modifications etc. This package uses a declarative approach to declare parameters for the video component based on building blocks. This implementation might give some insights into how SwiftUI works under the hood. You can also pass parameters in the common way. *If you profile the package, do it on a real device.* +Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI. However, it does not allow you to hide or customize the default video controls UI, limiting its use in custom scenarios. Alternatively, you would need to use AVPlayerViewController, which includes a lot of functionality just to disable the controls. In contrast, this solution acts as a modular framework, allowing you to integrate only the functionalities you need while keeping the core component lightweight. It provides full control over playback, including the ability to add custom UI elements, making it ideal for background videos, tooltips, video hints, and other custom scenarios. Additionally, it supports advanced features such as subtitles, seamless looping, and real-time filter application. It also allows adding vector graphics to the video stream, accessing frame data for custom filtering, and **applying ML or AI algorithms to video processing**, enabling a wide range of enhancements and modifications etc. This package uses a declarative approach to declare parameters for the video component based on building blocks. This implementation might give some insights into how SwiftUI works under the hood. You can also pass parameters in the common way. *If you profile the package, do it on a real device.* From 8c132e398ac9851372a5c2c938c93f5361e68e6f Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 10:10:43 +0100 Subject: [PATCH 056/103] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 72caff2..b68f052 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ It is a pure package without any third-party libraries. My main focus was on per ## Why if we have Apple’s VideoPlayer ?! -Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI. However, it does not allow you to hide or customize the default video controls UI, limiting its use in custom scenarios. Alternatively, you would need to use AVPlayerViewController, which includes a lot of functionality just to disable the controls. In contrast, this solution acts as a modular framework, allowing you to integrate only the functionalities you need while keeping the core component lightweight. It provides full control over playback, including the ability to add custom UI elements, making it ideal for background videos, tooltips, video hints, and other custom scenarios. Additionally, it supports advanced features such as subtitles, seamless looping, and real-time filter application. It also allows adding vector graphics to the video stream, accessing frame data for custom filtering, and **applying ML or AI algorithms to video processing**, enabling a wide range of enhancements and modifications etc. This package uses a declarative approach to declare parameters for the video component based on building blocks. This implementation might give some insights into how SwiftUI works under the hood. You can also pass parameters in the common way. *If you profile the package, do it on a real device.* +Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI. However, it does not allow you to hide or customize the default video controls UI, limiting its use in custom scenarios. Alternatively, you would need to use AVPlayerViewController, which includes a lot of functionality just to disable the controls. In contrast, this solution acts as a modular framework, allowing you to integrate only the functionalities you need while keeping the core component lightweight. It provides full control over playback, including the ability to add custom UI elements, making it ideal for background videos, tooltips, video hints, and other custom scenarios. Additionally, it supports advanced features such as subtitles, seamless looping, and real-time filter application. It also allows adding vector graphics to the video stream, accessing frame data for custom filtering, and **applying ML or AI algorithms to video processing**, enabling a wide range of enhancements and modifications etc. + +*This package uses a declarative approach to declare parameters for the video component based on building blocks. This implementation might give some insights into how SwiftUI works under the hood. You can also pass parameters in the common way. If you profile the package, do it on a real device.* From 9187f6ba52b7eb679ac1d8f94c713afd849e667d Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 10:12:21 +0100 Subject: [PATCH 057/103] Update README.md --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index b68f052..0a300af 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,8 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI. Howeve *This package uses a declarative approach to declare parameters for the video component based on building blocks. This implementation might give some insights into how SwiftUI works under the hood. You can also pass parameters in the common way. If you profile the package, do it on a real device.* - - - - ## SwiftUI app example [follow the link](https://github.com/swiftuiux/swiftui-video-player-example) - ```swift ExtVideoPlayer{ VideoSettings{ From 9c732509397ebdcb2cb012fe689563e7de6bb406 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 10:23:44 +0100 Subject: [PATCH 058/103] update --- README.md | 4 +--- Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift | 6 +++++- .../swiftui-loop-videoplayer/enum/PlayerEventFilter.swift | 7 ------- Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift | 4 ++-- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0a300af..ad90d98 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | **NotAutoPlay** | Indicates if the video should not play after initialization. Notice that if you use `command` as a control flow for the player the start command should be `.idle` | false | | **EnableVector** | Use this struct to activate settings that allow the addition of vector-based overlays via commands. If it is not passed via settings, any commands to `addVector` or `removeAllVectors` will have no effect. | Not Enabled | |**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| - | -| **Events([.durationAny, .itemStatusChangedAny])** | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | - | +| **Events([.durationAny, .itemStatusChangedAny])** | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass `Events([])` to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | - | ### Additional Notes on Settings @@ -325,8 +325,6 @@ video_main.m3u8 | `stoppedPiP` | Matches exactly the `.stoppedPiP` event, triggered when Picture-in-Picture (PiP) mode stops. | | `itemStatusChangedAny` | Matches any `.itemStatusChanged(...)` case, indicating that the AVPlayerItem's status has changed (e.g., `.unknown`, `.readyToPlay`, `.failed`). | | `durationAny` | Matches any `.duration(...)` case, which provides the duration of the media item when ready to play. | -| `all` | Matches every possible player event. | - ### Additional Notes on Errors diff --git a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift index bb1e6be..305406a 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -125,10 +125,14 @@ fileprivate func filterEvents(with settings: VideoSettings, for events: [PlayerE let filters = settings.events // `[PlayerEventFilter]` // If no filters are provided, return an empty array. - guard !filters.isEmpty else { + guard let filters else { return [] } + guard !filters.isEmpty else{ + return events + } + // Keep each `PlayerEvent` only if it matches *at least* one filter in `filters`. return events.filter { event in filters.contains(event) diff --git a/Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift b/Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift index 1ccd096..11e6a1e 100644 --- a/Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift +++ b/Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift @@ -51,9 +51,6 @@ public enum PlayerEventFilter { /// Matches any `.duration(...)` case case durationAny - - /// Matches every possible event - case all } extension PlayerEventFilter { @@ -63,10 +60,6 @@ extension PlayerEventFilter { /// - Returns: `true` if the event belongs to this case (ignoring parameters), `false` otherwise. func matches(_ event: PlayerEvent) -> Bool { switch (self, event) { - /// Universal case - case (.all, _): - return true - // Compare by case name only, ignoring associated values case (.seekAny, .seek): return true diff --git a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift index 542b206..f9877b4 100644 --- a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift +++ b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift @@ -36,7 +36,7 @@ public struct VideoSettings: Equatable{ public let vector: Bool /// Disable events - public let events: [PlayerEventFilter] + public let events: [PlayerEventFilter]? /// Don't auto play video after initialization public let notAutoPlay: Bool @@ -117,7 +117,7 @@ public struct VideoSettings: Equatable{ vector = settings.contains(.vector) - events = settings.fetch(by : "events", defaulted: []) + events = settings.fetch(by : "events", defaulted: nil) } } From 5f9fedce6cd5f312ec86c0ef37b23852e5793697 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 10:27:41 +0100 Subject: [PATCH 059/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad90d98..27bcdc3 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The player's functionality is designed around a dual ⇆ interaction model: - **Commands and Settings**: Through these, you instruct the player on what to do and how to do it. Settings define the environment and initial state, while commands offer real-time control. As for now, you can conveniently pass command by command; perhaps later I’ll add support for batch commands -- **Event Feedback**: Through event handling, the player communicates back to the application, informing it of internal changes that may need attention. Due to the nature of media players, especially in environments with dynamic content or user interactions, the flow of events can become flooded. To manage this effectively and prevent the application from being overwhelmed by the volume of incoming events, the **system collects these events every second and returns them as a batch**. Pass `Events([.all])` in the `settings` to enable event mechanism. +- **Event Feedback**: Through event handling, the player communicates back to the application, informing it of internal changes that may need attention. Due to the nature of media players, especially in environments with dynamic content or user interactions, the flow of events can become flooded. To manage this effectively and prevent the application from being overwhelmed by the volume of incoming events, the **system collects these events every second and returns them as a batch**. Pass `Events([])` in the `settings` to enable event mechanism. ## [Documentation(API)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer/main/documentation/swiftui_loop_videoplayer) From 0fcd379760a83dab59bc364708dced569233a504 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 10:49:12 +0100 Subject: [PATCH 060/103] update --- README.md | 4 ++-- Sources/swiftui-loop-videoplayer/enum/Setting.swift | 2 +- .../swiftui-loop-videoplayer/settings/Events.swift | 4 ++-- .../utils/VideoSettings.swift | 13 ++++++++++++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 27bcdc3..e8cb0d5 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The player's functionality is designed around a dual ⇆ interaction model: - **Commands and Settings**: Through these, you instruct the player on what to do and how to do it. Settings define the environment and initial state, while commands offer real-time control. As for now, you can conveniently pass command by command; perhaps later I’ll add support for batch commands -- **Event Feedback**: Through event handling, the player communicates back to the application, informing it of internal changes that may need attention. Due to the nature of media players, especially in environments with dynamic content or user interactions, the flow of events can become flooded. To manage this effectively and prevent the application from being overwhelmed by the volume of incoming events, the **system collects these events every second and returns them as a batch**. Pass `Events([])` in the `settings` to enable event mechanism. +- **Event Feedback**: Through event handling, the player communicates back to the application, informing it of internal changes that may need attention. Due to the nature of media players, especially in environments with dynamic content or user interactions, the flow of events can become flooded. To manage this effectively and prevent the application from being overwhelmed by the volume of incoming events, the **system collects these events every second and returns them as a batch**. Pass `Events()` in the `settings` to enable event mechanism. ## [Documentation(API)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer/main/documentation/swiftui_loop_videoplayer) @@ -134,7 +134,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | **NotAutoPlay** | Indicates if the video should not play after initialization. Notice that if you use `command` as a control flow for the player the start command should be `.idle` | false | | **EnableVector** | Use this struct to activate settings that allow the addition of vector-based overlays via commands. If it is not passed via settings, any commands to `addVector` or `removeAllVectors` will have no effect. | Not Enabled | |**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| - | -| **Events([.durationAny, .itemStatusChangedAny])** | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass `Events([])` to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | - | +| **Events([.durationAny, .itemStatusChangedAny])** | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass `Events()` to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | - | ### Additional Notes on Settings diff --git a/Sources/swiftui-loop-videoplayer/enum/Setting.swift b/Sources/swiftui-loop-videoplayer/enum/Setting.swift index e4cf3ca..f3575b0 100644 --- a/Sources/swiftui-loop-videoplayer/enum/Setting.swift +++ b/Sources/swiftui-loop-videoplayer/enum/Setting.swift @@ -21,7 +21,7 @@ public enum Setting: Equatable, SettingsConvertible{ [self] } - case events([PlayerEventFilter]) + case events([PlayerEventFilter]?) ///Enable vector layer to add overlay vector graphics case vector diff --git a/Sources/swiftui-loop-videoplayer/settings/Events.swift b/Sources/swiftui-loop-videoplayer/settings/Events.swift index 674f63c..76c0b00 100644 --- a/Sources/swiftui-loop-videoplayer/settings/Events.swift +++ b/Sources/swiftui-loop-videoplayer/settings/Events.swift @@ -11,12 +11,12 @@ import Foundation public struct Events: SettingsConvertible{ /// Holds the specific AVLayerVideoGravity setting defining how video content should align within its layer. - private let value : [PlayerEventFilter] + private let value : [PlayerEventFilter]? // MARK: - Life circle /// Initializes a new instance - public init(_ value : [PlayerEventFilter]) { + public init(_ value : [PlayerEventFilter]? = nil) { self.value = value } diff --git a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift index f9877b4..d2c3f57 100644 --- a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift +++ b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift @@ -117,7 +117,18 @@ public struct VideoSettings: Equatable{ vector = settings.contains(.vector) - events = settings.fetch(by : "events", defaulted: nil) + let hasEvents = settings.contains { + if case .events = $0 { + return true + } + return false + } + + if hasEvents{ + events = settings.fetch(by : "events", defaulted: []) ?? [] + }else{ + events = nil + } } } From 119cbc3617b8a6b381de3ec5c6c9c7677e279dda Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 10:49:24 +0100 Subject: [PATCH 061/103] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index e8cb0d5..908f764 100644 --- a/README.md +++ b/README.md @@ -393,8 +393,7 @@ or in a declarative way .onPlayerEventChange { events in // Player events } -``` - +``` ```swift ExtVideoPlayer{ From c55984b35f152026216a5aff26d18008f403c79b Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 10:54:38 +0100 Subject: [PATCH 062/103] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 908f764..65549a5 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI. Howeve } ``` +or + + ```swift + ExtVideoPlayer(fileName: 'swipe') +``` + ## Philosophy of Player Dynamics The player's functionality is designed around a dual ⇆ interaction model: @@ -371,7 +377,7 @@ Integrating vector graphics into SwiftUI views, particularly during lifecycle ev ### 1. Create LoopPlayerView ```swift -ExtVideoPlayer(fileName: 'swipe') + ExtVideoPlayer(fileName: 'swipe') ``` or in a declarative way From 22ff6e03e7e415ce3765c34bcec2315f8c27561c Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 10:56:43 +0100 Subject: [PATCH 063/103] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 65549a5..d00afd9 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,17 @@ [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoplayer%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoPlayer%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoPlayer) + It is a pure package without any third-party libraries. My main focus was on performance. Especially if you need to add a video in the background as a design element, in such cases, you’d want a lightweight component without a lot of unnecessary features. **I hope it serves you well**. +### SwiftUI app example [follow the link](https://github.com/swiftuiux/swiftui-video-player-example) + ## Why if we have Apple’s VideoPlayer ?! Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI. However, it does not allow you to hide or customize the default video controls UI, limiting its use in custom scenarios. Alternatively, you would need to use AVPlayerViewController, which includes a lot of functionality just to disable the controls. In contrast, this solution acts as a modular framework, allowing you to integrate only the functionalities you need while keeping the core component lightweight. It provides full control over playback, including the ability to add custom UI elements, making it ideal for background videos, tooltips, video hints, and other custom scenarios. Additionally, it supports advanced features such as subtitles, seamless looping, and real-time filter application. It also allows adding vector graphics to the video stream, accessing frame data for custom filtering, and **applying ML or AI algorithms to video processing**, enabling a wide range of enhancements and modifications etc. *This package uses a declarative approach to declare parameters for the video component based on building blocks. This implementation might give some insights into how SwiftUI works under the hood. You can also pass parameters in the common way. If you profile the package, do it on a real device.* -## SwiftUI app example [follow the link](https://github.com/swiftuiux/swiftui-video-player-example) - ```swift ExtVideoPlayer{ VideoSettings{ From 7395ca137bfbd9cd76fb0ab0c008f4200ab94ff0 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 11:01:48 +0100 Subject: [PATCH 064/103] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d00afd9..d17bfdd 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,8 @@ Please note that using videos from URLs requires ensuring that you have the righ | **Mute** | Indicates if the video should play without sound. | false | | **NotAutoPlay** | Indicates if the video should not play after initialization. Notice that if you use `command` as a control flow for the player the start command should be `.idle` | false | | **EnableVector** | Use this struct to activate settings that allow the addition of vector-based overlays via commands. If it is not passed via settings, any commands to `addVector` or `removeAllVectors` will have no effect. | Not Enabled | -|**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| - | -| **Events([.durationAny, .itemStatusChangedAny])** | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass `Events()` to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | - | +|**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| Not Enabled | +| **Events([.durationAny, .itemStatusChangedAny])** | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass `Events()` to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | Not Enabled | ### Additional Notes on Settings From f759d72eede0632db8fde8a08866acaae3619b62 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 11:04:34 +0100 Subject: [PATCH 065/103] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d17bfdd..384f0e1 100644 --- a/README.md +++ b/README.md @@ -133,9 +133,9 @@ Please note that using videos from URLs requires ensuring that you have the righ |---------------|-----------------------------------------------------------------------------------------------------|---------| | **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc).
**Local File URL** If the name is a valid local file path (file:// scheme).
**Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | | **Ext** | File extension for the video, used when loading from local resources. This is optional when a URL is provided and the URL ends with the video file extension. | "mp4" | -| **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | - | +| **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | Not Enabled | | **Gravity** | How the video content should be resized to fit the player's bounds. | .resizeAspect | -| **TimePublishing** | Specifies the interval at which the player publishes the current playback time. | - | +| **TimePublishing** | Specifies the interval at which the player publishes the current playback time. | Not Enabled | | **Loop** | Whether the video should automatically restart when it reaches the end. If not explicitly passed, the video will not loop. | false | | **Mute** | Indicates if the video should play without sound. | false | | **NotAutoPlay** | Indicates if the video should not play after initialization. Notice that if you use `command` as a control flow for the player the start command should be `.idle` | false | From b09cd30b6be96897547304f6d2e095f7cbf24531 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 11:05:19 +0100 Subject: [PATCH 066/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 384f0e1..c151362 100644 --- a/README.md +++ b/README.md @@ -133,12 +133,12 @@ Please note that using videos from URLs requires ensuring that you have the righ |---------------|-----------------------------------------------------------------------------------------------------|---------| | **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc).
**Local File URL** If the name is a valid local file path (file:// scheme).
**Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | | **Ext** | File extension for the video, used when loading from local resources. This is optional when a URL is provided and the URL ends with the video file extension. | "mp4" | -| **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | Not Enabled | | **Gravity** | How the video content should be resized to fit the player's bounds. | .resizeAspect | | **TimePublishing** | Specifies the interval at which the player publishes the current playback time. | Not Enabled | | **Loop** | Whether the video should automatically restart when it reaches the end. If not explicitly passed, the video will not loop. | false | | **Mute** | Indicates if the video should play without sound. | false | | **NotAutoPlay** | Indicates if the video should not play after initialization. Notice that if you use `command` as a control flow for the player the start command should be `.idle` | false | +| **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | Not Enabled | | **EnableVector** | Use this struct to activate settings that allow the addition of vector-based overlays via commands. If it is not passed via settings, any commands to `addVector` or `removeAllVectors` will have no effect. | Not Enabled | |**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| Not Enabled | | **Events([.durationAny, .itemStatusChangedAny])** | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass `Events()` to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | Not Enabled | From 4c46d9a32c6618a773c65c01e78bd2970efc517e Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 11:06:10 +0100 Subject: [PATCH 067/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c151362..d37ec2b 100644 --- a/README.md +++ b/README.md @@ -134,10 +134,10 @@ Please note that using videos from URLs requires ensuring that you have the righ | **SourceName** | **Direct URL String** If the name represents a valid URL ( HTTP etc).
**Local File URL** If the name is a valid local file path (file:// scheme).
**Bundle Resource** It tries to locate the file in the main bundle using Bundle.main.url(forResource:withExtension:) | - | | **Ext** | File extension for the video, used when loading from local resources. This is optional when a URL is provided and the URL ends with the video file extension. | "mp4" | | **Gravity** | How the video content should be resized to fit the player's bounds. | .resizeAspect | -| **TimePublishing** | Specifies the interval at which the player publishes the current playback time. | Not Enabled | | **Loop** | Whether the video should automatically restart when it reaches the end. If not explicitly passed, the video will not loop. | false | | **Mute** | Indicates if the video should play without sound. | false | | **NotAutoPlay** | Indicates if the video should not play after initialization. Notice that if you use `command` as a control flow for the player the start command should be `.idle` | false | +| **TimePublishing** | Specifies the interval at which the player publishes the current playback time. | Not Enabled | | **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | Not Enabled | | **EnableVector** | Use this struct to activate settings that allow the addition of vector-based overlays via commands. If it is not passed via settings, any commands to `addVector` or `removeAllVectors` will have no effect. | Not Enabled | |**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| Not Enabled | From 12f278ade38934b5013a6296c8918803533a97ee Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 16:41:00 +0100 Subject: [PATCH 068/103] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d37ec2b..8117cd8 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | **Subtitles** | The URL or local filename of the WebVTT (.vtt) subtitles file to be merged with the video. With a AVMutableComposition approach that is used currently in the package, you cannot directly change the position or size of subtitles. AVFoundation’s built-in handling of “text” tracks simply renders them in a default style, without allowing additional layout options. Take a look on the implementation in the example app *Video8.swift* | Not Enabled | | **EnableVector** | Use this struct to activate settings that allow the addition of vector-based overlays via commands. If it is not passed via settings, any commands to `addVector` or `removeAllVectors` will have no effect. | Not Enabled | |**PictureInPicture**| Enable Picture-in-Picture (PiP) support. If not passed than any command like `startPiP` or `stopPiP` have no effect. Take a look the example app *Video11.swift*. It does not work on simulator. You can observe the feature only on real devices.| Not Enabled | -| **Events([.durationAny, .itemStatusChangedAny])** | If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass `Events()` to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | Not Enabled | +| **Events([.durationAny, .itemStatusChangedAny])** | If `Events()` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass `Events()` to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance. Take a look on the implementation in the example app *Video8.swift* | Not Enabled | ### Additional Notes on Settings @@ -282,7 +282,7 @@ video_main.m3u8 ## Player Events - *If `Events([])` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance.* + *If `Events()` is not passed in the `settings: VideoSettings`, the event mechanism is disabled. You can specify exactly which events you want to receive (e.g., .itemStatusChangedAny) or simply pass .all to receive all available events. This setting was added to improve performance because events are emitted via @State changes, which trigger view updates. If you don’t need to observe any events, disabling them can significantly boost performance.* | Event | Description | |------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| From 97c50b96d9f1dd636bd27190a1e52c92f0f9c72f Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 16:45:27 +0100 Subject: [PATCH 069/103] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8117cd8..b7b87b5 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,10 @@ You can reach out the effect simply via mask modifier ```swift ExtVideoPlayer( settings : $settings, - command: $playbackCommand + command: $playbackCommand, + VideoSettings{ + SourceName("swipe") + } ) .mask{ RoundedRectangle(cornerRadius: 25) From e84d4fbc006905896552548c511121167240889d Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 19 Feb 2025 12:02:59 +0100 Subject: [PATCH 070/103] Update Events.swift --- .../settings/Events.swift | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/settings/Events.swift b/Sources/swiftui-loop-videoplayer/settings/Events.swift index 76c0b00..369a198 100644 --- a/Sources/swiftui-loop-videoplayer/settings/Events.swift +++ b/Sources/swiftui-loop-videoplayer/settings/Events.swift @@ -7,20 +7,25 @@ import Foundation +/// Represents a collection of event filters that can be converted into settings. +/// This struct is used to encapsulate `PlayerEventFilter` instances and provide a method +/// to transform them into an array of `Setting` objects. @available(iOS 14.0, macOS 11.0, tvOS 14.0, *) -public struct Events: SettingsConvertible{ +public struct Events: SettingsConvertible { - /// Holds the specific AVLayerVideoGravity setting defining how video content should align within its layer. - private let value : [PlayerEventFilter]? + // An optional array of PlayerEventFilter objects representing event filters + private let value: [PlayerEventFilter]? - // MARK: - Life circle + // MARK: - Life cycle - /// Initializes a new instance - public init(_ value : [PlayerEventFilter]? = nil) { + /// Initializes a new instance of `Events` + /// - Parameter value: An optional array of `PlayerEventFilter` objects, defaulting to `nil` + public init(_ value: [PlayerEventFilter]? = nil) { self.value = value } - /// Fetch settings + /// Converts the event filters into an array of `Setting` objects + /// Used for fetching settings in the application @_spi(Private) public func asSettings() -> [Setting] { [.events(value)] From 688a6a03d5e9d7e83383fc5b60101a82a8220224 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 19 Feb 2025 12:05:27 +0100 Subject: [PATCH 071/103] Update Setting.swift --- .../enum/Setting.swift | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/enum/Setting.swift b/Sources/swiftui-loop-videoplayer/enum/Setting.swift index f3575b0..4c7c341 100644 --- a/Sources/swiftui-loop-videoplayer/enum/Setting.swift +++ b/Sources/swiftui-loop-videoplayer/enum/Setting.swift @@ -11,61 +11,60 @@ import SwiftUI import AVKit #endif -/// Settings for loop video player +/// Configuration settings for a loop video player. +/// These settings control various playback and UI behaviors. @available(iOS 14.0, macOS 11.0, tvOS 14.0, *) -public enum Setting: Equatable, SettingsConvertible{ - - /// Converts the current setting to an array containing only this setting. - /// - Returns: An array of `Setting` containing the single instance of this setting. +public enum Setting: Equatable, SettingsConvertible { + + /// Converts the current setting into an array containing itself. + /// - Returns: An array with a single instance of `Setting`. public func asSettings() -> [Setting] { [self] } - + + /// Event filters to monitor specific player events. case events([PlayerEventFilter]?) - - ///Enable vector layer to add overlay vector graphics + + /// Enables a vector layer for overlaying vector graphics. case vector - - /// Loop video + + /// Enables looping of the video playback. case loop - - /// Mute video + + /// Mutes the video. case mute - - /// Don't auto play video after initialization + + /// Prevents automatic playback after initialization. case notAutoPlay - - /// File name + + /// Specifies the file name of the video. case name(String) - /// File extension + /// Specifies the file extension of the video. case ext(String) - - /// Subtitles + + /// Sets subtitles for the video. case subtitles(String) - - /// Support Picture-in-Picture + + /// Enables Picture-in-Picture (PiP) mode support. case pictureInPicture - - /// A CMTime value representing the interval at which the player's current time should be published. - /// If set, the player will publish periodic time updates based on this interval. + + /// Defines the interval at which the player's current time should be published. case timePublishing(CMTime) - /// Video gravity + /// Sets the video gravity (e.g., aspect fit, aspect fill). case gravity(AVLayerVideoGravity = .resizeAspect) - /// Case name + /// Retrieves the name of the current case. var caseName: String { Mirror(reflecting: self).children.first?.label ?? "\(self)" } - - /// Associated value + + /// Retrieves the associated value of the case, if any. var associatedValue: Any? { - guard let firstChild = Mirror(reflecting: self).children.first else { return nil } - return firstChild.value } } From 76dd127df06f939b08b6909d6222c0dce6c3ad7e Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 20 Feb 2025 12:39:31 +0100 Subject: [PATCH 072/103] update --- Sources/swiftui-loop-videoplayer/ext+/Array+.swift | 1 - .../view/player/main/ExtPlayerMultiPlatform.swift | 4 ---- 2 files changed, 5 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/ext+/Array+.swift b/Sources/swiftui-loop-videoplayer/ext+/Array+.swift index 0b172a2..606cd14 100644 --- a/Sources/swiftui-loop-videoplayer/ext+/Array+.swift +++ b/Sources/swiftui-loop-videoplayer/ext+/Array+.swift @@ -16,7 +16,6 @@ extension Array where Element == Setting{ self.first(where: { $0.caseName == name }) } - /// Fetch associated value /// - Parameters: /// - name: Case name diff --git a/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift index 6f5de91..0d79ac1 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift @@ -8,10 +8,6 @@ import SwiftUI import Combine -#if canImport(AVKit) -import AVKit -#endif - #if canImport(UIKit) import UIKit #endif From 41c0b136f9263e0287543f5254912d2f9bbdfa20 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 21 Feb 2025 16:25:18 +0100 Subject: [PATCH 073/103] update --- README.md | 2 +- .../view/helpers/PlayerCoordinator.swift | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b7b87b5..9f0fbd7 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,7 @@ video_main.m3u8 | `duration(CMTime)` | Provides the duration of the AVPlayerItem when it is ready to play. The duration is given in `CMTime`. | ## Player event filter -`PlayerEventFilter` - this enum provides a structured way to filter `PlayerEvent` cases. I decided to introduce an additional structure that corresponds to `PlayerEvent`. All cases with parameters are replicated as *eventName***Any** to match any variation of that event, regardless of its associated values. +`PlayerEventFilter` - this enum provides a structured way to filter `PlayerEvent` cases. I decided to introduce an additional structure that corresponds to `PlayerEvent`. All cases with parameters are replicated as *eventName***Any** to match any variation of that event, regardless of its associated values. If you need specific events that match certain event parameters, let me know, and I will add them. ```swift ExtVideoPlayer{ diff --git a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift index efe0514..781d72a 100644 --- a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift +++ b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift @@ -15,8 +15,10 @@ import AVKit @MainActor internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { + /// Publisher that emits player events, allowing observers to react to changes in playback state let eventPublisher: PassthroughSubject + /// Publisher that emits the current playback time as a Double, allowing real-time tracking of progress let timePublisher: PassthroughSubject /// Stores the last command applied to the player. From 53c89fd8dbcbb4d32dc2405257dfbadbf368e9e1 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 25 Jun 2025 09:47:40 +0200 Subject: [PATCH 074/103] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 9f0fbd7..addd05c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # SwiftUI video player iOS 14+, macOS 11+, tvOS 14+ -### *Please star the repository if you believe continuing the development of this package is worthwhile. This will help me understand which package deserves more effort.* [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoplayer%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoPlayer%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoPlayer) From 549152ee0ca783ba513e93c3aed5f55bc3566b4b Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 3 Jul 2025 08:45:30 +0200 Subject: [PATCH 075/103] Revert "Update README.md" This reverts commit 53c89fd8dbcbb4d32dc2405257dfbadbf368e9e1. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index addd05c..9f0fbd7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # SwiftUI video player iOS 14+, macOS 11+, tvOS 14+ +### *Please star the repository if you believe continuing the development of this package is worthwhile. This will help me understand which package deserves more effort.* [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoplayer%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoPlayer%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoPlayer) From 89a8f587bb4d9752e563711abc54db8573313914 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 14 Aug 2025 14:17:41 +0200 Subject: [PATCH 076/103] update --- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 25 +++++-------------- .../protocol/player/AbstractPlayer.swift | 3 ++- .../protocol/player/ExtPlayerProtocol.swift | 11 +++++--- .../view/player/ios/ExtPlayerUIView.swift | 5 ++-- .../view/player/mac/ExtPlayerNSView.swift | 1 + 5 files changed, 19 insertions(+), 26 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index adca4b7..93ae307 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -243,27 +243,14 @@ func mergeAssetWithSubtitles(videoAsset: AVURLAsset, subtitleAsset: AVURLAsset) /// - duration: A `CMTime` value representing the total duration of the media. /// This value must be valid for the calculation to work correctly. /// - Returns: A `CMTime` value representing the resolved seek position within the media. -func getSeekTime(for time: Double, duration : CMTime) -> CMTime?{ +@MainActor +func getSeekTime(for time: Double, duration: CMTime) -> CMTime? { + guard duration.isNumeric && duration.value != 0 else { return nil } - guard duration.value != 0 else{ return nil } + let endSeconds = CMTimeGetSeconds(duration) + let clampedSeconds = max(0, min(time, endSeconds)) - let endTime = CMTimeGetSeconds(duration) - let seekTime : CMTime - - if time < 0 { - // If the time is negative, seek to the start of the video - seekTime = .zero - } else if time >= endTime { - // If the time exceeds the video duration, seek to the end of the video - let endCMTime = CMTime(seconds: endTime, preferredTimescale: duration.timescale) - seekTime = endCMTime - } else { - // Otherwise, seek to the specified time - let seekCMTime = CMTime(seconds: time, preferredTimescale: duration.timescale) - seekTime = seekCMTime - } - - return seekTime + return CMTime(seconds: clampedSeconds, preferredTimescale: duration.timescale) } /// Creates an `AVPlayerItem` with optional subtitle merging. diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 677d931..90606db 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -137,7 +137,8 @@ public protocol AbstractPlayer: AnyObject { func update(settings: VideoSettings) } -extension AbstractPlayer{ +@MainActor +public extension AbstractPlayer{ /// Retrieves the current item being played. /// diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 504a05d..5b66495 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -71,6 +71,7 @@ public protocol ExtPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ func onError(_ error : VPErrors) } +@MainActor internal extension ExtPlayerProtocol { /// Initializes a new player view with a video asset and custom settings. @@ -133,11 +134,13 @@ internal extension ExtPlayerProtocol { /// - player: The `AVQueuePlayer` instance to which the time observer will be added. /// - settings: A `VideoSettings` object containing the time publishing interval and related configuration. func configureTimePublishing(_ player: AVQueuePlayer, _ settings: VideoSettings) { - if let timePublishing = settings.timePublishing{ - timeObserver = player.addPeriodicTimeObserver(forInterval: timePublishing, queue: .global()) { [weak self] time in - guard let self = self else{ return } + if let timePublishing = settings.timePublishing { + timeObserver = player.addPeriodicTimeObserver( + forInterval: timePublishing, + queue: .main + ) { [weak self] time in Task { @MainActor in - self.delegate?.didPassedTime(seconds: time.seconds) + self?.delegate?.didPassedTime(seconds: time.seconds) } } } diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 5f61632..8cd390a 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -141,8 +141,9 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol{ func setupPiP(delegate: AVPictureInPictureControllerDelegate) { // Check if PiP is supported guard AVPictureInPictureController.isPictureInPictureSupported() else { - DispatchQueue.main.asyncAfter(deadline: .now() + 1){ [weak self] in - self?.onError(.notSupportedPiP) + Task { @MainActor in + try? await Task.sleep(nanoseconds: 1_000_000_000) + self.onError(.notSupportedPiP) } return } diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index f2667aa..c6ed05a 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -77,6 +77,7 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { player = AVQueuePlayer(items: []) super.init(frame: .zero) + self.wantsLayer = true addPlayerLayer() addCompositeLayer(settings) From 64094663a06cbba238da259f80d075449f4d4376 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 14 Aug 2025 14:22:00 +0200 Subject: [PATCH 077/103] Update fn+.swift --- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index 93ae307..70b1a61 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -244,13 +244,28 @@ func mergeAssetWithSubtitles(videoAsset: AVURLAsset, subtitleAsset: AVURLAsset) /// This value must be valid for the calculation to work correctly. /// - Returns: A `CMTime` value representing the resolved seek position within the media. @MainActor -func getSeekTime(for time: Double, duration: CMTime) -> CMTime? { - guard duration.isNumeric && duration.value != 0 else { return nil } +func getSeekTime(for time: Double, duration : CMTime) -> CMTime?{ - let endSeconds = CMTimeGetSeconds(duration) - let clampedSeconds = max(0, min(time, endSeconds)) + guard duration.value != 0 else{ return nil } + + + let endTime = CMTimeGetSeconds(duration) + let seekTime : CMTime + + if time < 0 { + // If the time is negative, seek to the start of the video + seekTime = .zero + } else if time >= endTime { + // If the time exceeds the video duration, seek to the end of the video + let endCMTime = CMTime(seconds: endTime, preferredTimescale: duration.timescale) + seekTime = endCMTime + } else { + // Otherwise, seek to the specified time + let seekCMTime = CMTime(seconds: time, preferredTimescale: duration.timescale) + seekTime = seekCMTime + } - return CMTime(seconds: clampedSeconds, preferredTimescale: duration.timescale) + return seekTime } /// Creates an `AVPlayerItem` with optional subtitle merging. From 1dc6e543d86482a9766d71adafee5bb3e639297f Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 14 Aug 2025 14:23:17 +0200 Subject: [PATCH 078/103] Revert "Update fn+.swift" This reverts commit 64094663a06cbba238da259f80d075449f4d4376. --- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index 70b1a61..93ae307 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -244,28 +244,13 @@ func mergeAssetWithSubtitles(videoAsset: AVURLAsset, subtitleAsset: AVURLAsset) /// This value must be valid for the calculation to work correctly. /// - Returns: A `CMTime` value representing the resolved seek position within the media. @MainActor -func getSeekTime(for time: Double, duration : CMTime) -> CMTime?{ +func getSeekTime(for time: Double, duration: CMTime) -> CMTime? { + guard duration.isNumeric && duration.value != 0 else { return nil } - guard duration.value != 0 else{ return nil } - - - let endTime = CMTimeGetSeconds(duration) - let seekTime : CMTime - - if time < 0 { - // If the time is negative, seek to the start of the video - seekTime = .zero - } else if time >= endTime { - // If the time exceeds the video duration, seek to the end of the video - let endCMTime = CMTime(seconds: endTime, preferredTimescale: duration.timescale) - seekTime = endCMTime - } else { - // Otherwise, seek to the specified time - let seekCMTime = CMTime(seconds: time, preferredTimescale: duration.timescale) - seekTime = seekCMTime - } + let endSeconds = CMTimeGetSeconds(duration) + let clampedSeconds = max(0, min(time, endSeconds)) - return seekTime + return CMTime(seconds: clampedSeconds, preferredTimescale: duration.timescale) } /// Creates an `AVPlayerItem` with optional subtitle merging. From bfdbb5719b0bba0b4b2cac2dfd32182623e04732 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 14 Aug 2025 14:23:43 +0200 Subject: [PATCH 079/103] Revert "update" This reverts commit 89a8f587bb4d9752e563711abc54db8573313914. --- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 25 ++++++++++++++----- .../protocol/player/AbstractPlayer.swift | 3 +-- .../protocol/player/ExtPlayerProtocol.swift | 11 +++----- .../view/player/ios/ExtPlayerUIView.swift | 5 ++-- .../view/player/mac/ExtPlayerNSView.swift | 1 - 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index 93ae307..adca4b7 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -243,14 +243,27 @@ func mergeAssetWithSubtitles(videoAsset: AVURLAsset, subtitleAsset: AVURLAsset) /// - duration: A `CMTime` value representing the total duration of the media. /// This value must be valid for the calculation to work correctly. /// - Returns: A `CMTime` value representing the resolved seek position within the media. -@MainActor -func getSeekTime(for time: Double, duration: CMTime) -> CMTime? { - guard duration.isNumeric && duration.value != 0 else { return nil } +func getSeekTime(for time: Double, duration : CMTime) -> CMTime?{ - let endSeconds = CMTimeGetSeconds(duration) - let clampedSeconds = max(0, min(time, endSeconds)) + guard duration.value != 0 else{ return nil } - return CMTime(seconds: clampedSeconds, preferredTimescale: duration.timescale) + let endTime = CMTimeGetSeconds(duration) + let seekTime : CMTime + + if time < 0 { + // If the time is negative, seek to the start of the video + seekTime = .zero + } else if time >= endTime { + // If the time exceeds the video duration, seek to the end of the video + let endCMTime = CMTime(seconds: endTime, preferredTimescale: duration.timescale) + seekTime = endCMTime + } else { + // Otherwise, seek to the specified time + let seekCMTime = CMTime(seconds: time, preferredTimescale: duration.timescale) + seekTime = seekCMTime + } + + return seekTime } /// Creates an `AVPlayerItem` with optional subtitle merging. diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 90606db..677d931 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -137,8 +137,7 @@ public protocol AbstractPlayer: AnyObject { func update(settings: VideoSettings) } -@MainActor -public extension AbstractPlayer{ +extension AbstractPlayer{ /// Retrieves the current item being played. /// diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 5b66495..504a05d 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -71,7 +71,6 @@ public protocol ExtPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ func onError(_ error : VPErrors) } -@MainActor internal extension ExtPlayerProtocol { /// Initializes a new player view with a video asset and custom settings. @@ -134,13 +133,11 @@ internal extension ExtPlayerProtocol { /// - player: The `AVQueuePlayer` instance to which the time observer will be added. /// - settings: A `VideoSettings` object containing the time publishing interval and related configuration. func configureTimePublishing(_ player: AVQueuePlayer, _ settings: VideoSettings) { - if let timePublishing = settings.timePublishing { - timeObserver = player.addPeriodicTimeObserver( - forInterval: timePublishing, - queue: .main - ) { [weak self] time in + if let timePublishing = settings.timePublishing{ + timeObserver = player.addPeriodicTimeObserver(forInterval: timePublishing, queue: .global()) { [weak self] time in + guard let self = self else{ return } Task { @MainActor in - self?.delegate?.didPassedTime(seconds: time.seconds) + self.delegate?.didPassedTime(seconds: time.seconds) } } } diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 8cd390a..5f61632 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -141,9 +141,8 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol{ func setupPiP(delegate: AVPictureInPictureControllerDelegate) { // Check if PiP is supported guard AVPictureInPictureController.isPictureInPictureSupported() else { - Task { @MainActor in - try? await Task.sleep(nanoseconds: 1_000_000_000) - self.onError(.notSupportedPiP) + DispatchQueue.main.asyncAfter(deadline: .now() + 1){ [weak self] in + self?.onError(.notSupportedPiP) } return } diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index c6ed05a..f2667aa 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -77,7 +77,6 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { player = AVQueuePlayer(items: []) super.init(frame: .zero) - self.wantsLayer = true addPlayerLayer() addCompositeLayer(settings) From a5f177e76480148f9f1211ffc23cba2bbcad1cdd Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 14 Aug 2025 14:51:15 +0200 Subject: [PATCH 080/103] Update fn+.swift --- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index adca4b7..2b1748d 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -243,27 +243,22 @@ func mergeAssetWithSubtitles(videoAsset: AVURLAsset, subtitleAsset: AVURLAsset) /// - duration: A `CMTime` value representing the total duration of the media. /// This value must be valid for the calculation to work correctly. /// - Returns: A `CMTime` value representing the resolved seek position within the media. -func getSeekTime(for time: Double, duration : CMTime) -> CMTime?{ - - guard duration.value != 0 else{ return nil } - - let endTime = CMTimeGetSeconds(duration) - let seekTime : CMTime - - if time < 0 { - // If the time is negative, seek to the start of the video - seekTime = .zero - } else if time >= endTime { - // If the time exceeds the video duration, seek to the end of the video - let endCMTime = CMTime(seconds: endTime, preferredTimescale: duration.timescale) - seekTime = endCMTime - } else { - // Otherwise, seek to the specified time - let seekCMTime = CMTime(seconds: time, preferredTimescale: duration.timescale) - seekTime = seekCMTime - } - - return seekTime +func getSeekTime(for time: Double, duration: CMTime) -> CMTime? { + guard duration.isValid, + duration.isNumeric, + duration.timescale != 0 + else { return nil } + + let endSeconds = CMTimeGetSeconds(duration) + guard endSeconds.isFinite, endSeconds >= 0 else { return nil } + + if time <= 0 { return .zero } + if time >= endSeconds { return duration } + + let clamped = max(0, min(time, endSeconds)) + + let scale = max(Int32(600), duration.timescale) + return CMTime(seconds: clamped, preferredTimescale: scale) } /// Creates an `AVPlayerItem` with optional subtitle merging. From c1a1bd94b9df705355263c437751cf29671a91f7 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 14 Aug 2025 15:05:42 +0200 Subject: [PATCH 081/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f0fbd7..a9adbb8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ It is a pure package without any third-party libraries. My main focus was on performance. Especially if you need to add a video in the background as a design element, in such cases, you’d want a lightweight component without a lot of unnecessary features. **I hope it serves you well**. -### SwiftUI app example [follow the link](https://github.com/swiftuiux/swiftui-video-player-example) +### 🟩 SwiftUI app example [follow the link](https://github.com/swiftuiux/swiftui-video-player-example) ## Why if we have Apple’s VideoPlayer ?! From 25f889f6bbbeab52cce4fad134d97637502f2c9e Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 14 Aug 2025 15:06:20 +0200 Subject: [PATCH 082/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a9adbb8..d0cc37c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ It is a pure package without any third-party libraries. My main focus was on performance. Especially if you need to add a video in the background as a design element, in such cases, you’d want a lightweight component without a lot of unnecessary features. **I hope it serves you well**. -### 🟩 SwiftUI app example [follow the link](https://github.com/swiftuiux/swiftui-video-player-example) +### 🟩 Demo project showing video player usage and features: [follow the link](https://github.com/swiftuiux/swiftui-video-player-example) ## Why if we have Apple’s VideoPlayer ?! From 4e1bf86ed48a59318cf1c38a056a9b9e1408f045 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 15 Aug 2025 17:56:26 +0200 Subject: [PATCH 083/103] Update ExtPlayerProtocol.swift --- .../protocol/player/ExtPlayerProtocol.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 504a05d..f7c04e7 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -284,7 +284,7 @@ internal extension ExtPlayerProtocol { } } - currentItemObserver = player.observe(\.currentItem, options: [.new, .old, .initial]) { [weak self] player, change in + currentItemObserver = player.observe(\.currentItem, options: [.new]) { [weak self] player, change in // Detecting when the current item is changed if let newItem = change.newValue as? AVPlayerItem { Task { @MainActor in From 04eb09ea6f479720028ceb316324c809c4039aab Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 15 Aug 2025 18:05:19 +0200 Subject: [PATCH 084/103] Update ExtVideoPlayer.swift --- Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift index 305406a..cae132c 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -105,7 +105,9 @@ public struct ExtVideoPlayer: View{ .onReceive(timePublisher.receive(on: DispatchQueue.main), perform: { time in currentTime = time }) - .onReceive(eventPublisher.collect(.byTime(DispatchQueue.main, .seconds(1))), perform: { event in + .onReceive(eventPublisher + .delay(for: .seconds(1), scheduler: DispatchQueue.main) + .collect(.byTime(DispatchQueue.main, .seconds(1))), perform: { event in playerEvent = filterEvents(with: settings, for: event) }) .preference(key: CurrentTimePreferenceKey.self, value: currentTime) From 93e8e51614450dfc13c35efd5719856e47672811 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 15 Aug 2025 18:06:30 +0200 Subject: [PATCH 085/103] Update ExtVideoPlayer.swift --- Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift index cae132c..e263242 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -106,7 +106,7 @@ public struct ExtVideoPlayer: View{ currentTime = time }) .onReceive(eventPublisher - .delay(for: .seconds(1), scheduler: DispatchQueue.main) + .delay(for: .seconds(2), scheduler: DispatchQueue.main) .collect(.byTime(DispatchQueue.main, .seconds(1))), perform: { event in playerEvent = filterEvents(with: settings, for: event) }) From bb2eab204ffa7aaab052a842d51f3c13688ad849 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 15 Aug 2025 18:07:33 +0200 Subject: [PATCH 086/103] Update ExtVideoPlayer.swift --- Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift index e263242..a0421f5 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -106,7 +106,6 @@ public struct ExtVideoPlayer: View{ currentTime = time }) .onReceive(eventPublisher - .delay(for: .seconds(2), scheduler: DispatchQueue.main) .collect(.byTime(DispatchQueue.main, .seconds(1))), perform: { event in playerEvent = filterEvents(with: settings, for: event) }) From 2b37d934ff4a3d8796c0f6da7e188d81bec04f42 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 15 Aug 2025 18:22:37 +0200 Subject: [PATCH 087/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0cc37c..4b033a2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # SwiftUI video player iOS 14+, macOS 11+, tvOS 14+ -### *Please star the repository if you believe continuing the development of this package is worthwhile. This will help me understand which package deserves more effort.* +## ⭐ Star it — so I know it’s worth my time to keep improving it. [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoplayer%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoPlayer%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoPlayer) From 06c6b5facc3b8fbb87f15f9bd03c074e100ba81e Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 15 Aug 2025 18:34:37 +0200 Subject: [PATCH 088/103] Update README.md --- README.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/README.md b/README.md index 4b033a2..1b3c828 100644 --- a/README.md +++ b/README.md @@ -453,27 +453,6 @@ ExtVideoPlayer{ | HLS Streams | Yes | HLS streams are supported and can be used for live streaming purposes. | -## New Functionality: Playback Commands - -The package now supports playback commands, allowing you to control video playback actions such as play, pause, and seek to specific times. - -```swift -struct VideoView: View { - @State private var playbackCommand: PlaybackCommand = .play - - var body: some View { - ExtVideoPlayer( - { - VideoSettings { - SourceName("swipe") - } - }, - command: $playbackCommand - ) - } -} -``` - ## Practical ideas for the package You can introduce video hints about some functionality into the app, for example how to add positions to favorites. Put loop video hint into background or open as popup. From eac1e43a401068f6d734e10accff83527dd5ba6e Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 19 Aug 2025 16:09:09 +0200 Subject: [PATCH 089/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b3c828..28020f7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # SwiftUI video player iOS 14+, macOS 11+, tvOS 14+ -## ⭐ Star it — so I know it’s worth my time to keep improving it. +## https://img.shields.io/github/stars/swiftuiux/swiftui-loop-videoPlayer?style=social ⭐ Star it — so I know it’s worth my time to keep improving it. [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoplayer%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoPlayer%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoPlayer) From feb5bfe07f8a4b83ca870a0d638943a99ed27b5c Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 19 Aug 2025 16:09:39 +0200 Subject: [PATCH 090/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 28020f7..c3ec028 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # SwiftUI video player iOS 14+, macOS 11+, tvOS 14+ -## https://img.shields.io/github/stars/swiftuiux/swiftui-loop-videoPlayer?style=social ⭐ Star it — so I know it’s worth my time to keep improving it. +## ![GitHub Repo stars](https://img.shields.io/github/stars/swiftuiux/swiftui-loop-videoPlayer?style=social) ⭐ Star it — so I know it’s worth my time to keep improving it. [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoplayer%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoPlayer%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoPlayer) From 50bfe24d194f996696c87ed6f37f5544644560ce Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 20 Aug 2025 15:06:55 +0200 Subject: [PATCH 091/103] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c3ec028..beef928 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ The player's functionality is designed around a dual ⇆ interaction model: ![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/swiftui_video_player.gif) -## Specs +## Implemented Specs +### The sample app demonstrates the majority of the specs implemented in the component | **Feature Category** | **Feature Name** | **Description** | |----------------------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------| From 938e1e69793bd81b8ea6ab78db39b85e2c8c202e Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 20 Aug 2025 15:08:03 +0200 Subject: [PATCH 092/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index beef928..7d39a31 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ The player's functionality is designed around a dual ⇆ interaction model: ![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/swiftui_video_player.gif) ## Implemented Specs -### The sample app demonstrates the majority of the specs implemented in the component +*The sample app demonstrates the majority of the specs implemented in the component* | **Feature Category** | **Feature Name** | **Description** | |----------------------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------| From 44709e96b7c966a81541fa24cd9fce35c31e4829 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 20 Aug 2025 15:09:07 +0200 Subject: [PATCH 093/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d39a31..ff9fb67 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ The player's functionality is designed around a dual ⇆ interaction model: ![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/swiftui_video_player.gif) ## Implemented Specs -*The sample app demonstrates the majority of the specs implemented in the component* +*The [sample app](https://github.com/swiftuiux/swiftui-video-player-example) demonstrates the majority of the specs implemented in the component* | **Feature Category** | **Feature Name** | **Description** | |----------------------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------| From 6569591592e283507606405bb79b74945d9af79f Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 20 Aug 2025 16:22:16 +0200 Subject: [PATCH 094/103] update --- .../swiftui-loop-videoplayer/ext+/URL+.swift | 41 ++++---- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 2 +- .../ext+/testURL.swift | 94 +++++++++++++++++++ 3 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 Tests/swiftui-loop-videoplayerTests/ext+/testURL.swift diff --git a/Sources/swiftui-loop-videoplayer/ext+/URL+.swift b/Sources/swiftui-loop-videoplayer/ext+/URL+.swift index f797374..9188d39 100644 --- a/Sources/swiftui-loop-videoplayer/ext+/URL+.swift +++ b/Sources/swiftui-loop-videoplayer/ext+/URL+.swift @@ -10,23 +10,32 @@ import Foundation extension URL { - /// Validates a string as a well-formed HTTP or HTTPS URL and returns a URL object if valid. - /// - /// - Parameter urlString: The string to validate as a URL. - /// - Returns: An optional URL object if the string is a valid URL. - /// - Throws: An error if the URL is not valid or cannot be created. - static func validURLFromString(_ string: String) -> URL? { - let pattern = "^(https?:\\/\\/)(([a-zA-Z0-9-]+\\.)*[a-zA-Z0-9-]+\\.[a-zA-Z]{2,})(:\\d{1,5})?(\\/[\\S]*)?$" - let regex = try? NSRegularExpression(pattern: pattern, options: []) - - let matches = regex?.matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) - - guard let _ = matches, !matches!.isEmpty else { - // If no matches are found, the URL is not valid - return nil + /// Validates and returns an HTTP/HTTPS URL or nil. + /// Strategy: + /// 1) Parse once to detect an existing scheme (mailto, ftp, etc.). + /// 2) If a scheme exists and it's not http/https -> reject. + /// 3) If no scheme exists -> optionally prepend https:// and parse again. + static func validURLFromString(from raw: String, assumeHTTPSIfMissing: Bool = true) -> URL? { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + + // First parse to detect an existing scheme. + if let pre = URLComponents(string: trimmed), let scheme = pre.scheme?.lowercased() { + // Reject anything that is not http/https. + guard scheme == "http" || scheme == "https" else { return nil } + + let comps = pre + // Require a host + guard let host = comps.host, !host.isEmpty else { return nil } + // Validate port range + if let port = comps.port, !(1...65535).contains(port) { return nil } + return comps.url } - // If a match is found, attempt to create a URL object - return URL(string: string) + // No scheme present -> optionally add https:// + guard assumeHTTPSIfMissing else { return nil } + guard let comps = URLComponents(string: "https://" + trimmed) else { return nil } + guard let host = comps.host, !host.isEmpty else { return nil } + if let port = comps.port, !(1...65535).contains(port) { return nil } + return comps.url } } diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index 2b1748d..a9204a3 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -44,7 +44,7 @@ func subtitlesAssetFor(_ settings: VideoSettings) -> AVURLAsset? { /// - Returns: An optional `AVURLAsset`, or `nil` if neither a valid URL nor a local resource file is found. fileprivate func assetFrom(name: String, fileExtension: String?) -> AVURLAsset? { // Attempt to create a valid URL from the provided string. - if let url = URL.validURLFromString(name) { + if let url = URL.validURLFromString(from: name) { return AVURLAsset(url: url) } diff --git a/Tests/swiftui-loop-videoplayerTests/ext+/testURL.swift b/Tests/swiftui-loop-videoplayerTests/ext+/testURL.swift new file mode 100644 index 0000000..5f42dab --- /dev/null +++ b/Tests/swiftui-loop-videoplayerTests/ext+/testURL.swift @@ -0,0 +1,94 @@ +// +// testURL+.swift +// swiftui-loop-videoplayer +// +// Created by Igor Shelopaev on 20.08.25. +// + +import XCTest +@testable import swiftui_loop_videoplayer + +final class testURL: XCTestCase { + + // MARK: - Positive cases (should pass) + + func testSampleVideoURLsPass() { + // Given: four sample URLs from the sandbox dictionary + let urls = [ + "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", + "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8" + ] + + // When/Then + for raw in urls { + let url = URL.validURLFromString(from: raw) + XCTAssertNotNil(url, "Expected to parse: \(raw)") + XCTAssertEqual(url?.scheme?.lowercased(), "https") + } + } + + func testAddsHTTPSIfMissing() { + // Given + let raw = "example.com/path?x=1#y" + + // When + let url = URL.validURLFromString(from: raw) + + // Then + XCTAssertNotNil(url) + XCTAssertEqual(url?.scheme, "https") + XCTAssertEqual(url?.host, "example.com") + XCTAssertEqual(url?.path, "/path") + } + + func testTrimsWhitespace() { + let raw = " https://example.com/video.m3u8 " + let url = URL.validURLFromString(from: raw) + XCTAssertNotNil(url) + XCTAssertEqual(url?.host, "example.com") + XCTAssertEqual(url?.path, "/video.m3u8") + } + + func testIPv6AndLocalHosts() { + // IPv6 loopback + XCTAssertNotNil(URL.validURLFromString(from: "https://[::1]")) + // localhost + XCTAssertNotNil(URL.validURLFromString(from: "http://localhost")) + // IPv4 with port and query/fragment + XCTAssertNotNil(URL.validURLFromString(from: "http://127.0.0.1:8080/path?a=1#x")) + } + + func testIDNUnicodeHost() { + // Unicode host (IDN). URLComponents should handle this. + let url = URL.validURLFromString(from: "https://bücher.de") + XCTAssertNotNil(url) + XCTAssertEqual(url?.scheme, "https") + XCTAssertNotNil(url?.host) + } + + // MARK: - Negative cases (should fail) + + func testRejectsNonHTTP() { + XCTAssertNil(URL.validURLFromString(from: "ftp://example.com/file.mp4")) + XCTAssertNil(URL.validURLFromString(from: "mailto:user@example.com")) + XCTAssertNil(URL.validURLFromString(from: "file:///Users/me/movie.mp4")) + } + + func testRejectsInvalidPort() { + XCTAssertNil(URL.validURLFromString(from: "https://example.com:0")) + XCTAssertNil(URL.validURLFromString(from: "https://example.com:65536")) + XCTAssertNotNil(URL.validURLFromString(from: "https://example.com:65535")) + } + + func testRejectsMissingHost() { + XCTAssertNil(URL.validURLFromString(from: "https://")) + XCTAssertNil(URL.validURLFromString(from: "https:///path-only")) + } + + func testNoAutoSchemeOption() { + // When auto-scheme is disabled, a bare host should fail. + XCTAssertNil(URL.validURLFromString(from: "example.com", assumeHTTPSIfMissing: false)) + } +} From 18106816958861dd09f5fd9035b1d056dbe2fc78 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 20 Aug 2025 16:30:05 +0200 Subject: [PATCH 095/103] update --- .../swiftui-loop-videoplayer/ext+/URL+.swift | 10 ++-------- .../ext+/testURL.swift | 19 ------------------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/ext+/URL+.swift b/Sources/swiftui-loop-videoplayer/ext+/URL+.swift index 9188d39..0186998 100644 --- a/Sources/swiftui-loop-videoplayer/ext+/URL+.swift +++ b/Sources/swiftui-loop-videoplayer/ext+/URL+.swift @@ -14,8 +14,7 @@ extension URL { /// Strategy: /// 1) Parse once to detect an existing scheme (mailto, ftp, etc.). /// 2) If a scheme exists and it's not http/https -> reject. - /// 3) If no scheme exists -> optionally prepend https:// and parse again. - static func validURLFromString(from raw: String, assumeHTTPSIfMissing: Bool = true) -> URL? { + static func validURLFromString(from raw: String) -> URL? { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) // First parse to detect an existing scheme. @@ -31,11 +30,6 @@ extension URL { return comps.url } - // No scheme present -> optionally add https:// - guard assumeHTTPSIfMissing else { return nil } - guard let comps = URLComponents(string: "https://" + trimmed) else { return nil } - guard let host = comps.host, !host.isEmpty else { return nil } - if let port = comps.port, !(1...65535).contains(port) { return nil } - return comps.url + return nil } } diff --git a/Tests/swiftui-loop-videoplayerTests/ext+/testURL.swift b/Tests/swiftui-loop-videoplayerTests/ext+/testURL.swift index 5f42dab..dd462b5 100644 --- a/Tests/swiftui-loop-videoplayerTests/ext+/testURL.swift +++ b/Tests/swiftui-loop-videoplayerTests/ext+/testURL.swift @@ -29,20 +29,6 @@ final class testURL: XCTestCase { } } - func testAddsHTTPSIfMissing() { - // Given - let raw = "example.com/path?x=1#y" - - // When - let url = URL.validURLFromString(from: raw) - - // Then - XCTAssertNotNil(url) - XCTAssertEqual(url?.scheme, "https") - XCTAssertEqual(url?.host, "example.com") - XCTAssertEqual(url?.path, "/path") - } - func testTrimsWhitespace() { let raw = " https://example.com/video.m3u8 " let url = URL.validURLFromString(from: raw) @@ -86,9 +72,4 @@ final class testURL: XCTestCase { XCTAssertNil(URL.validURLFromString(from: "https://")) XCTAssertNil(URL.validURLFromString(from: "https:///path-only")) } - - func testNoAutoSchemeOption() { - // When auto-scheme is disabled, a bare host should fail. - XCTAssertNil(URL.validURLFromString(from: "example.com", assumeHTTPSIfMissing: false)) - } } From 5321eb86a488730f1b72a865077493bd129a0c3e Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 21 Aug 2025 09:58:31 +0200 Subject: [PATCH 096/103] update --- README.md | 4 +- .../ext+/textArray.swift | 111 ++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 Tests/swiftui-loop-videoplayerTests/ext+/textArray.swift diff --git a/README.md b/README.md index ff9fb67..0eb18e6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # SwiftUI video player iOS 14+, macOS 11+, tvOS 14+ -## ![GitHub Repo stars](https://img.shields.io/github/stars/swiftuiux/swiftui-loop-videoPlayer?style=social) ⭐ Star it — so I know it’s worth my time to keep improving it. +## ⭐ Star it — so I know it’s worth my time to keep improving it. [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoplayer%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoPlayer%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoPlayer) @@ -113,6 +113,8 @@ You can reach out the effect simply via mask modifier [Perhaps that might be enough for your needs](https://github.com/swiftuiux/swiftui-loop-videoPlayer/issues/7#issuecomment-2341268743) + + ## Testing The package includes unit tests that cover key functionality. While not exhaustive, these tests help ensure the core components work as expected. UI tests are in progress and are being developed [in the example application](https://github.com/swiftuiux/swiftui-video-player-example). The run_tests.sh is an example script that automates testing by encapsulating test commands into a single executable file, simplifying the execution process. You can configure the script to run specific testing environment relevant to your projects. diff --git a/Tests/swiftui-loop-videoplayerTests/ext+/textArray.swift b/Tests/swiftui-loop-videoplayerTests/ext+/textArray.swift new file mode 100644 index 0000000..ff6ac7c --- /dev/null +++ b/Tests/swiftui-loop-videoplayerTests/ext+/textArray.swift @@ -0,0 +1,111 @@ +// +// textArray.swift +// swiftui-loop-videoplayer +// +// Created by Igor on 21.08.25. +// + +import XCTest +import AVFoundation +@testable import swiftui_loop_videoplayer + +final class SettingsTests: XCTestCase { + + // MARK: - events([PlayerEventFilter]?) + + func testFetch_Events_WithArray() { + let settings: [Setting] = [.events([.playing, .paused])] + let events: [PlayerEventFilter] = settings.fetch(by: "events", defaulted: []) + XCTAssertEqual(events, [.playing, .paused]) + } + + func testFetch_Events_NilAssociatedValue() { + let settings: [Setting] = [.events(nil)] + // Optional(nil) won't cast to [PlayerEventFilter] → returns default + let events: [PlayerEventFilter] = settings.fetch(by: "events", defaulted: []) + XCTAssertTrue(events.isEmpty) + } + + // MARK: - name / ext / subtitles + + func testFetch_Name_ReturnsStoredString() { + let settings: [Setting] = [.name("teaser")] + let value: String = settings.fetch(by: "name", defaulted: "") + XCTAssertEqual(value, "teaser") + } + + func testFetch_Ext_ReturnsStoredString() { + let settings: [Setting] = [.ext("mp4")] + let value: String = settings.fetch(by: "ext", defaulted: "mov") + XCTAssertEqual(value, "mp4") + } + + func testFetch_Subtitles_ReturnsStoredString() { + let settings: [Setting] = [.subtitles("de")] + let value: String = settings.fetch(by: "subtitles", defaulted: "en") + XCTAssertEqual(value, "de") + } + + // MARK: - Missing / mismatch + + func testFetch_ReturnsDefault_WhenNameMissing() { + let settings: [Setting] = [.name("teaser")] + let value: Int = settings.fetch(by: "fontSize", defaulted: 12) + XCTAssertEqual(value, 12) + } + + func testFetch_ReturnsDefault_WhenTypeMismatch() { + let settings: [Setting] = [.name("teaser")] + let value: Int = settings.fetch(by: "name", defaulted: 0) + XCTAssertEqual(value, 0) + } + + // MARK: - First match precedence + + func testFetch_PrefersFirstMatch_WhenMultipleWithSameName() { + let settings: [Setting] = [.name("first"), .name("second")] + let value: String = settings.fetch(by: "name", defaulted: "") + XCTAssertEqual(value, "first") + } + + // MARK: - Value-less cases → default + + func testFetch_Vector_ReturnsDefault() { + let settings: [Setting] = [.vector] + let value: Bool = settings.fetch(by: "vector", defaulted: false) + XCTAssertFalse(value) + } + + func testFetch_Loop_ReturnsDefault() { + let settings: [Setting] = [.loop] + let value: String = settings.fetch(by: "loop", defaulted: "no") + XCTAssertEqual(value, "no") + } + + func testFetch_PictureInPicture_ReturnsDefault() { + let settings: [Setting] = [.pictureInPicture] + let pip: Bool = settings.fetch(by: "pictureInPicture", defaulted: false) + XCTAssertFalse(pip) + } + + func testFetch_Mute_ReturnsDefault() { + let settings: [Setting] = [.mute] + let muted: Bool = settings.fetch(by: "mute", defaulted: false) + XCTAssertFalse(muted) + } + + // MARK: - timePublishing / gravity + + func testFetch_TimePublishing_CMTime() { + let t = CMTime(seconds: 0.5, preferredTimescale: 600) + let settings: [Setting] = [.timePublishing(t)] + let fetched: CMTime = settings.fetch(by: "timePublishing", defaulted: .zero) + XCTAssertEqual(CMTimeCompare(fetched, t), 0) + } + + func testFetch_Gravity_CustomAssociatedValue() { + let settings: [Setting] = [.gravity(.resizeAspectFill)] + let gravity: AVLayerVideoGravity = settings.fetch(by: "gravity", defaulted: .resize) + XCTAssertEqual(gravity, .resizeAspectFill) + } +} From fd0bae9c2716b020fa3a7ff88d59743bf0a6b00c Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 21 Aug 2025 10:00:29 +0200 Subject: [PATCH 097/103] Update PlayerEventFilter.swift --- Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift b/Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift index 11e6a1e..bb0b711 100644 --- a/Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift +++ b/Sources/swiftui-loop-videoplayer/enum/PlayerEventFilter.swift @@ -2,7 +2,7 @@ // PlayerEventFilter.swift // swiftui-loop-videoplayer // -// Created by Igor on 12.02.25. +// Created by Igor Shelopaev on 12.02.25. // import Foundation From 4b55978e52c1572a7edb2fbf67d6cd042cfb8ba1 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 22 Aug 2025 13:51:13 +0200 Subject: [PATCH 098/103] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 0eb18e6..51332b6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # SwiftUI video player iOS 14+, macOS 11+, tvOS 14+ ## ⭐ Star it — so I know it’s worth my time to keep improving it. +## Coming soon: Metal shaders for video +Support for applying **Metal shaders** directly to **video frames**. +Real-time effects on the timeline, no extra copies or heavy post-processing. + [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoplayer%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswiftuiux%2Fswiftui-loop-videoPlayer%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoPlayer) @@ -9,6 +13,8 @@ It is a pure package without any third-party libraries. My main focus was on per ### 🟩 Demo project showing video player usage and features: [follow the link](https://github.com/swiftuiux/swiftui-video-player-example) + + ## Why if we have Apple’s VideoPlayer ?! Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI. However, it does not allow you to hide or customize the default video controls UI, limiting its use in custom scenarios. Alternatively, you would need to use AVPlayerViewController, which includes a lot of functionality just to disable the controls. In contrast, this solution acts as a modular framework, allowing you to integrate only the functionalities you need while keeping the core component lightweight. It provides full control over playback, including the ability to add custom UI elements, making it ideal for background videos, tooltips, video hints, and other custom scenarios. Additionally, it supports advanced features such as subtitles, seamless looping, and real-time filter application. It also allows adding vector graphics to the video stream, accessing frame data for custom filtering, and **applying ML or AI algorithms to video processing**, enabling a wide range of enhancements and modifications etc. From 937a9198cd1eb27d71a2a967ebf57a6d1e435fc5 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 27 Aug 2025 16:50:44 +0200 Subject: [PATCH 099/103] update --- .../shaders/ArtFilter.metal | 31 +++++++++++++++++++ .../ext+/textArray.swift | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 Sources/swiftui-loop-videoplayer/shaders/ArtFilter.metal diff --git a/Sources/swiftui-loop-videoplayer/shaders/ArtFilter.metal b/Sources/swiftui-loop-videoplayer/shaders/ArtFilter.metal new file mode 100644 index 0000000..bcc388a --- /dev/null +++ b/Sources/swiftui-loop-videoplayer/shaders/ArtFilter.metal @@ -0,0 +1,31 @@ +// +// ArtFilter.metal +// swiftui-loop-videoplayer-example +// +// Created by Igor Shelopaev on 21.08.25. +// + + +#include +#include +using namespace metal; + +extern "C" { namespace coreimage { + +float4 artEffect(sampler src, float t, destination dest) { + float2 d = dest.coord(); + float2 uv = samplerTransform(src, d); + float2 sz = samplerSize(src); + + const float a = 0.01; + const float lambda = 48.0; + const float k = 6.28318530718 / lambda; + const float w = 1.0; + + float yOff = a * sin(d.x * k - w * t); + float2 uv2 = float2(uv.x, clamp(uv.y + yOff, 0.0, sz.y - 1.0)); + + return src.sample(uv2); +} + +} } diff --git a/Tests/swiftui-loop-videoplayerTests/ext+/textArray.swift b/Tests/swiftui-loop-videoplayerTests/ext+/textArray.swift index ff6ac7c..61d8bad 100644 --- a/Tests/swiftui-loop-videoplayerTests/ext+/textArray.swift +++ b/Tests/swiftui-loop-videoplayerTests/ext+/textArray.swift @@ -2,7 +2,7 @@ // textArray.swift // swiftui-loop-videoplayer // -// Created by Igor on 21.08.25. +// Created by Igor Shelopaev on 21.08.25. // import XCTest From 3aecf579213fd3062ea39eba2b5845876ff51bfd Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 28 Aug 2025 11:22:24 +0200 Subject: [PATCH 100/103] Delete ArtFilter.metal --- .../shaders/ArtFilter.metal | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 Sources/swiftui-loop-videoplayer/shaders/ArtFilter.metal diff --git a/Sources/swiftui-loop-videoplayer/shaders/ArtFilter.metal b/Sources/swiftui-loop-videoplayer/shaders/ArtFilter.metal deleted file mode 100644 index bcc388a..0000000 --- a/Sources/swiftui-loop-videoplayer/shaders/ArtFilter.metal +++ /dev/null @@ -1,31 +0,0 @@ -// -// ArtFilter.metal -// swiftui-loop-videoplayer-example -// -// Created by Igor Shelopaev on 21.08.25. -// - - -#include -#include -using namespace metal; - -extern "C" { namespace coreimage { - -float4 artEffect(sampler src, float t, destination dest) { - float2 d = dest.coord(); - float2 uv = samplerTransform(src, d); - float2 sz = samplerSize(src); - - const float a = 0.01; - const float lambda = 48.0; - const float k = 6.28318530718 / lambda; - const float w = 1.0; - - float yOff = a * sin(d.x * k - w * t); - float2 uv2 = float2(uv.x, clamp(uv.y + yOff, 0.0, sz.y - 1.0)); - - return src.sample(uv2); -} - -} } From bcb583ea13b595524760d4783f4fde46c3444ab4 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 3 Sep 2025 17:10:15 +0200 Subject: [PATCH 101/103] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 51332b6..9939da9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -# SwiftUI video player iOS 14+, macOS 11+, tvOS 14+ +# Coming soon: Metal shaders for video +iOS 14+, macOS 11+, tvOS 14+ + ## ⭐ Star it — so I know it’s worth my time to keep improving it. -## Coming soon: Metal shaders for video Support for applying **Metal shaders** directly to **video frames**. Real-time effects on the timeline, no extra copies or heavy post-processing. From bcf5f1c1fd2a6db08fd078affd41cfe408e7dcf9 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 3 Sep 2025 17:18:39 +0200 Subject: [PATCH 102/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9939da9..cbc5499 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Coming soon: Metal shaders for video iOS 14+, macOS 11+, tvOS 14+ -## ⭐ Star it — so I know it’s worth my time to keep improving it. +## ⭐ Star it — so I know it’s worth to keep improving it. Support for applying **Metal shaders** directly to **video frames**. Real-time effects on the timeline, no extra copies or heavy post-processing. From 0a26e7e8d77531347d56fb3f575ab616e483a9a6 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 9 Sep 2025 11:07:04 +0200 Subject: [PATCH 103/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbc5499..04ce52f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Coming soon: Metal shaders for video iOS 14+, macOS 11+, tvOS 14+ -## ⭐ Star it — so I know it’s worth to keep improving it. +## ⭐ Star it, please — so I know it’s worth improving further. Support for applying **Metal shaders** directly to **video frames**. Real-time effects on the timeline, no extra copies or heavy post-processing.