From 7395ca137bfbd9cd76fb0ab0c008f4200ab94ff0 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 14 Feb 2025 11:01:48 +0100 Subject: [PATCH 01/40] 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 02/40] 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 03/40] 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 04/40] 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 05/40] 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 06/40] 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 07/40] 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 08/40] 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 09/40] 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 10/40] 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 11/40] 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 12/40] 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 13/40] 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 14/40] 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 15/40] 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 16/40] 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 17/40] 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 18/40] 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 19/40] 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 20/40] 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 21/40] 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 22/40] 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 23/40] 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 24/40] 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 25/40] 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 26/40] 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 27/40] 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 28/40] 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 29/40] 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 30/40] 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 31/40] 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 32/40] 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 33/40] 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 34/40] 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 35/40] 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 36/40] 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 37/40] 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 38/40] 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 39/40] 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 40/40] 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.