From 6f1f20999c0500021af6d3addd4b6b4dbe21e35a Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 10 Jan 2025 12:12:08 +0100 Subject: [PATCH 001/209] update --- README.md | 25 ++++++++++++++++++- .../protocol/player/AbstractPlayer.swift | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ce95975..7b05da7 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,30 @@ In cases where you need to re-issue a command that might appear redundant but is | Command | Description | |-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| | `audioTrack(String)` | Command to select a specific audio track based on language code. The `languageCode` parameter specifies the desired audio track's language (e.g., "en" for English). | -| `subtitles(String?)` | Command to set subtitles to a specified language or turn them off. Pass a language code (e.g., "en" for English) to set subtitles, or `nil` to turn them off. This command works only for embedded subtitles within the video file. | +| `subtitles(String?)` | This command sets subtitles to a specified language or turns them off. Provide a language code (for example, `"en"` for English) to display that language's subtitles, or pass `nil` to disable subtitles altogether. **Note**: This only applies when the video file has embedded subtitles tracks. | + +### Additional Notes on for subtitles command +This functionality is designed for use cases where the video file already contains multiple subtitle tracks (i.e., legible media tracks) embedded in its metadata. In other words, the container format (such as MP4, MOV, or QuickTime) holds one or more subtitle or closed-caption tracks that can be selected at runtime. By calling this function and providing a language code (e.g., “en”, “fr”, “de”), you instruct the AVPlayerItem to look for the corresponding subtitle track in the asset’s media selection group. If it finds a match, it will activate that subtitle track; otherwise, no subtitles will appear. Passing nil disables subtitles altogether. This approach is convenient when you want to switch between multiple embedded subtitle languages or turn them off without relying on external subtitle files (like SRT or WebVTT). +### Configuring HLS Playlist with English Subtitles + +Here’s an example of an HLS playlist configured with English subtitles. The subtitles are defined as a separate track using WebVTT or a similar format, referenced within the master playlist. This setup allows seamless subtitle rendering during video playback, synchronized with the video stream. + +```plaintext +#EXTM3U +#EXT-X-MEDIA:TYPE=SUBTITLES, + GROUP-ID="subs", + NAME="English Subtitles", + LANGUAGE="en", + AUTOSELECT=YES, + DEFAULT=YES, + URI="subtitles_en.m3u8" + +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3000000, + RESOLUTION=1280x720, + SUBTITLES="subs" +video_main.m3u8 +``` + ## Player Events diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index f1ac411..9eeff06 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -282,6 +282,7 @@ extension AbstractPlayer{ } /// Sets the subtitles for the video playback to a specified language or turns them off. + /// This function is designed for use cases where the video file already contains multiple subtitle tracks (i.e., legible media tracks) embedded in its metadata. In other words, the container format (such as MP4, MOV, or QuickTime) holds one or more subtitle or closed-caption tracks that can be selected at runtime. By calling this function and providing a language code (e.g., “en”, “fr”, “de”), you instruct the AVPlayerItem to look for the corresponding subtitle track in the asset’s media selection group. If it finds a match, it will activate that subtitle track; otherwise, no subtitles will appear. Passing nil disables subtitles altogether. This approach is convenient when you want to switch between multiple embedded subtitle languages or turn them off without relying on external subtitle files (like SRT or WebVTT). /// - Parameters: /// - language: The language code (e.g., "en" for English) for the desired subtitles. /// Pass `nil` to turn off subtitles. From 20feca1d2c8dd740065d9b08080cf3ff4d2b247e Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 10 Jan 2025 12:12:44 +0100 Subject: [PATCH 002/209] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b05da7..afe44bb 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,8 @@ In cases where you need to re-issue a command that might appear redundant but is ### Additional Notes on for subtitles command This functionality is designed for use cases where the video file already contains multiple subtitle tracks (i.e., legible media tracks) embedded in its metadata. In other words, the container format (such as MP4, MOV, or QuickTime) holds one or more subtitle or closed-caption tracks that can be selected at runtime. By calling this function and providing a language code (e.g., “en”, “fr”, “de”), you instruct the AVPlayerItem to look for the corresponding subtitle track in the asset’s media selection group. If it finds a match, it will activate that subtitle track; otherwise, no subtitles will appear. Passing nil disables subtitles altogether. This approach is convenient when you want to switch between multiple embedded subtitle languages or turn them off without relying on external subtitle files (like SRT or WebVTT). -### Configuring HLS Playlist with English Subtitles + +**Configuring HLS Playlist with English Subtitles** Here’s an example of an HLS playlist configured with English subtitles. The subtitles are defined as a separate track using WebVTT or a similar format, referenced within the master playlist. This setup allows seamless subtitle rendering during video playback, synchronized with the video stream. From e935f58d2daeb69ec484d52451fb5cf03cc855b2 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 10 Jan 2025 12:13:32 +0100 Subject: [PATCH 003/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index afe44bb..c21df90 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ In cases where you need to re-issue a command that might appear redundant but is | `subtitles(String?)` | This command sets subtitles to a specified language or turns them off. Provide a language code (for example, `"en"` for English) to display that language's subtitles, or pass `nil` to disable subtitles altogether. **Note**: This only applies when the video file has embedded subtitles tracks. | ### Additional Notes on for subtitles command -This functionality is designed for use cases where the video file already contains multiple subtitle tracks (i.e., legible media tracks) embedded in its metadata. In other words, the container format (such as MP4, MOV, or QuickTime) holds one or more subtitle or closed-caption tracks that can be selected at runtime. By calling this function and providing a language code (e.g., “en”, “fr”, “de”), you instruct the AVPlayerItem to look for the corresponding subtitle track in the asset’s media selection group. If it finds a match, it will activate that subtitle track; otherwise, no subtitles will appear. Passing nil disables subtitles altogether. This approach is convenient when you want to switch between multiple embedded subtitle languages or turn them off without relying on external subtitle files (like SRT or WebVTT). +This functionality is designed for use cases where the video file already contains multiple subtitle tracks (i.e., legible media tracks) embedded in its metadata. In other words, the container format (such as MP4, MOV, or QuickTime) holds one or more subtitle or closed-caption tracks that can be selected at runtime. By calling this function and providing a language code (e.g., “en”, “fr”, “de”), you instruct the component to look for the corresponding subtitle track in the asset’s media selection group. If it finds a match, it will activate that subtitle track; otherwise, no subtitles will appear. Passing nil disables subtitles altogether. This approach is convenient when you want to switch between multiple embedded subtitle languages or turn them off without relying on external subtitle files (like SRT or WebVTT). **Configuring HLS Playlist with English Subtitles** From 216afb7baab7b817266bdb16af7f85673653917d Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 10 Jan 2025 12:15:10 +0100 Subject: [PATCH 004/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c21df90..6dc6d80 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ In cases where you need to re-issue a command that might appear redundant but is | `audioTrack(String)` | Command to select a specific audio track based on language code. The `languageCode` parameter specifies the desired audio track's language (e.g., "en" for English). | | `subtitles(String?)` | This command sets subtitles to a specified language or turns them off. Provide a language code (for example, `"en"` for English) to display that language's subtitles, or pass `nil` to disable subtitles altogether. **Note**: This only applies when the video file has embedded subtitles tracks. | -### Additional Notes on for subtitles command +### Additional Notes on the subtitles command This functionality is designed for use cases where the video file already contains multiple subtitle tracks (i.e., legible media tracks) embedded in its metadata. In other words, the container format (such as MP4, MOV, or QuickTime) holds one or more subtitle or closed-caption tracks that can be selected at runtime. By calling this function and providing a language code (e.g., “en”, “fr”, “de”), you instruct the component to look for the corresponding subtitle track in the asset’s media selection group. If it finds a match, it will activate that subtitle track; otherwise, no subtitles will appear. Passing nil disables subtitles altogether. This approach is convenient when you want to switch between multiple embedded subtitle languages or turn them off without relying on external subtitle files (like SRT or WebVTT). **Configuring HLS Playlist with English Subtitles** From e6196c5f7eedf13f026285e35dcf1ad52c71c6cd Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 10 Jan 2025 12:22:20 +0100 Subject: [PATCH 005/209] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6dc6d80..49660ed 100644 --- a/README.md +++ b/README.md @@ -136,9 +136,11 @@ In cases where you need to re-issue a command that might appear redundant but is | `audioTrack(String)` | Command to select a specific audio track based on language code. The `languageCode` parameter specifies the desired audio track's language (e.g., "en" for English). | | `subtitles(String?)` | This command sets subtitles to a specified language or turns them off. Provide a language code (for example, `"en"` for English) to display that language's subtitles, or pass `nil` to disable subtitles altogether. **Note**: This only applies when the video file has embedded subtitles tracks. | -### Additional Notes on the subtitles command +### Additional Notes on the subtitles Command This functionality is designed for use cases where the video file already contains multiple subtitle tracks (i.e., legible media tracks) embedded in its metadata. In other words, the container format (such as MP4, MOV, or QuickTime) holds one or more subtitle or closed-caption tracks that can be selected at runtime. By calling this function and providing a language code (e.g., “en”, “fr”, “de”), you instruct the component to look for the corresponding subtitle track in the asset’s media selection group. If it finds a match, it will activate that subtitle track; otherwise, no subtitles will appear. Passing nil disables subtitles altogether. This approach is convenient when you want to switch between multiple embedded subtitle languages or turn them off without relying on external subtitle files (like SRT or WebVTT). +Another option to add subtitles is by using **Settings** (take a look above), where you can provide subtitles as a separate source file (e.g., SRT or WebVTT). In this case, subtitles are dynamically loaded and managed alongside the video without requiring them to be embedded in the video file itself. Both of these methods — using embedded subtitle tracks or adding subtitles via Settings as external files — do not merge and save the resulting video with subtitles locally. Instead, the subtitles are rendered dynamically during playback. + **Configuring HLS Playlist with English Subtitles** Here’s an example of an HLS playlist configured with English subtitles. The subtitles are defined as a separate track using WebVTT or a similar format, referenced within the master playlist. This setup allows seamless subtitle rendering during video playback, synchronized with the video stream. @@ -159,7 +161,6 @@ Here’s an example of an HLS playlist configured with English subtitles. The su video_main.m3u8 ``` - ## Player Events | Event | Description | From 43a540d66d90f684d1968cf2b988f1df9ee3ded8 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 10 Jan 2025 12:31:24 +0100 Subject: [PATCH 006/209] Update README.md --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 49660ed..f2140a9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,15 @@ ## 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. Additionally, it supports advanced features like 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. + ```swift + ExtVideoPlayer{ + VideoSettings{ + SourceName("swipe") + Gravity(.resizeAspectFill) + } + } +``` + ## [SwiftUI app example](https://github.com/swiftuiux/swiftui-video-player-example) ## [Documentation(API)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer/main/documentation/swiftui_loop_videoplayer) @@ -57,6 +66,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | `init(settings: () -> VideoSettings, command:)` | Constructor | Initializes the player in a declarative way with a settings block and a playback command binding. | | `init(settings: Binding, command:)` | Constructor | Initializes the player with bindings to the video settings and a playback command. | + ## Settings | Name | Description | Default | @@ -225,15 +235,6 @@ or in a declarative way } ``` - ```swift - ExtVideoPlayer{ - VideoSettings{ - SourceName("swipe") - Gravity(.resizeAspectFill) - EFontSize(27) - } - } -``` ```swift ExtVideoPlayer{ From a6e774c7511149ce5cc46b5b5649f70c55f95092 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 10 Jan 2025 12:32:21 +0100 Subject: [PATCH 007/209] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f2140a9..d91de6f 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ 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. Additionally, it supports advanced features like 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. ```swift - ExtVideoPlayer{ - VideoSettings{ - SourceName("swipe") - Gravity(.resizeAspectFill) - } - } + ExtVideoPlayer{ + VideoSettings{ + SourceName("swipe") + Gravity(.resizeAspectFill) + } + } ``` ## [SwiftUI app example](https://github.com/swiftuiux/swiftui-video-player-example) From 869467ee869e843327aec154dfa71614dc54f50c Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 10 Jan 2025 15:14:33 +0100 Subject: [PATCH 008/209] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index d91de6f..0048ff3 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,7 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for ```swift ExtVideoPlayer{ VideoSettings{ - SourceName("swipe") - Gravity(.resizeAspectFill) + SourceName("swipe") } } ``` From 03e504d98e3af88c43d0a50839af0c7d115aaf59 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 10 Jan 2025 18:42:44 +0100 Subject: [PATCH 009/209] Update README.md --- README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0048ff3..68614cd 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,8 @@ In cases where you need to re-issue a command that might appear redundant but is ### Additional Notes on the subtitles Command This functionality is designed for use cases where the video file already contains multiple subtitle tracks (i.e., legible media tracks) embedded in its metadata. In other words, the container format (such as MP4, MOV, or QuickTime) holds one or more subtitle or closed-caption tracks that can be selected at runtime. By calling this function and providing a language code (e.g., “en”, “fr”, “de”), you instruct the component to look for the corresponding subtitle track in the asset’s media selection group. If it finds a match, it will activate that subtitle track; otherwise, no subtitles will appear. Passing nil disables subtitles altogether. This approach is convenient when you want to switch between multiple embedded subtitle languages or turn them off without relying on external subtitle files (like SRT or WebVTT). -Another option to add subtitles is by using **Settings** (take a look above), where you can provide subtitles as a separate source file (e.g., SRT or WebVTT). In this case, subtitles are dynamically loaded and managed alongside the video without requiring them to be embedded in the video file itself. Both of these methods — using embedded subtitle tracks or adding subtitles via Settings as external files — do not merge and save the resulting video with subtitles locally. Instead, the subtitles are rendered dynamically during playback. +Another option to add subtitles is by using **Settings** (take a look above), where you can provide subtitles as a separate source file (e.g., SRT or WebVTT). In this case, subtitles are dynamically loaded and managed alongside the video without requiring them to be embedded in the video file itself. +Both of these methods — using embedded subtitle tracks or adding subtitles via Settings as external files — do not merge and save the resulting video with subtitles locally. Instead, the subtitles are rendered dynamically during playback. **Configuring HLS Playlist with English Subtitles** @@ -318,6 +319,31 @@ You can introduce video hints about some functionality into the app, for example ![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/tip_video_swiftui.gif) +## HLS with Adaptive Quality + +### How Adaptive Quality Switching Works + +1. **Multiple Bitrates** + - The video is encoded in multiple quality levels (e.g., 240p, 360p, 720p, 1080p), each with different bitrates. + +2. **Manifest File** + - The server provides a manifest file: + - **In HLS**: A `.m3u8` file that contains links to video segments for each quality level. + +3. **Segments** + - The video is divided into short segments, typically 2–10 seconds long. + +4. **Dynamic Switching** + - The client (e.g., `AVQueuePlayer`) dynamically adjusts playback quality based on the current internet speed: + - Starts playback with the most suitable quality. + - Switches to higher or lower quality during playback as the connection speed changes. + +### Why This is the Best Option + +- **On-the-fly quality adjustment**: Ensures smooth transitions between quality levels without interrupting playback. +- **Minimal pauses and interruptions**: Reduces buffering and improves user experience. +- **Bandwidth efficiency**: The server sends only the appropriate stream, saving network traffic. + ## AVQueuePlayer features out of the box In the core of this package, I use `AVQueuePlayer`. Here are the supported features that are automatically enabled by `AVQueuePlayer` without passing any extra parameters: @@ -346,3 +372,4 @@ In the core of this package, I use `AVQueuePlayer`. Here are the supported featu | **Buffering and Caching** | Efficiently manages buffering of streaming content to reduce playback interruptions. | | **Error Handling and Recovery** | Provides built-in mechanisms to handle playback errors and attempt recovery without crashing the application. | | **Accessibility Features** | Supports VoiceOver and other accessibility features to make media content accessible to all users. | + From 3e411a1adaf8879368e6b49d2f7f8faf44407ae5 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 11 Jan 2025 11:28:48 +0100 Subject: [PATCH 010/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68614cd..fdfd5b6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![](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. Additionally, it supports advanced features like 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. 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. ```swift ExtVideoPlayer{ From f62949558aa954089369b9830f68b821a4e558a5 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 11 Jan 2025 12:19:30 +0100 Subject: [PATCH 011/209] Update README.md --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fdfd5b6..86c5a86 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,65 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for } ``` +| **Feature Category** | **Feature Name** | **Description** | +|----------------------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------| +| **General** | SwiftUI Declarative Syntax | Easily integrate using declarative syntax. | +| | Platform Compatibility | Supports iOS 14+, macOS 11+, tvOS 14+. | +| | Loop Playback | Automatically restart videos when they end. | +| | Local and Remote Video URLs | Supports playback from local files or remote URLs. | +| | Adaptive HLS Streaming | Handles HLS streaming with dynamic quality adjustment. | +| | Error Handling | Customizable error messages and visual displays. | +| | Subtitle Support | Add external `.vtt` files or use embedded subtitle tracks. | +| | Background Playback | Continue playback in the background with appropriate audio session setup. | +| | Custom Overlays | Add vector graphics and custom overlays over the video. | + +| **Playback Commands** | Idle Command | Initialize without specific playback actions. | +| | Play/Pause | Control playback state. | +| | Seek Command | Move to specific video timestamps. | +| | Mute/Unmute | Toggle audio playback. | +| | Volume Control | Adjust audio levels. | +| | Playback Speed | Dynamically modify playback speed. | +| | Loop/Unloop | Toggle looping behavior. | +| | Apply Filters | Add Core Image filters to the video stream. | +| | Remove Filters | Clear all applied filters. | +| | Add Vector Graphics | Overlay custom vector graphics onto the video. | + +| **Settings** | SourceName | Define video source (local or remote). | +| | File Extension | Default extension for video files (e.g., `.mp4`). | +| | Gravity | Set content resizing behavior (e.g., `.resizeAspect`). | +| | Time Publishing | Control playback time reporting intervals. | +| | AutoPlay | Toggle automatic playback on load. | +| | Mute by Default | Initialize playback without sound. | +| | Subtitle Integration | Configure subtitles from embedded tracks or external files. | +| | Error Widget Customization | Change error text color, font size, or disable built-in error widgets. | + +| **Visual Features** | Rounded Corners | Apply rounded corners using SwiftUI's `.mask` modifier. | +| | Overlay Graphics | Add vector graphics over video for custom effects. | +| | Brightness Adjustment | Control brightness levels dynamically. | +| | Contrast Adjustment | Modify video contrast in real time. | + +| **Playback Features** | Adaptive HLS Streaming | Dynamic quality adjustment based on network speed. | +| | Seamless Item Transitions | Smooth transitions between video items. | +| | Multichannel Audio | Play Dolby Atmos, 5.1 surround, and spatial audio tracks. | +| | Subtitles and Captions | Support for multiple subtitle and caption formats. | + +| **Event Handling** | Batch Event Processing | Collects and processes events in batches to avoid flooding. | +| | Playback State Events | `playing`, `paused`, `seek`, `waitingToPlayAtSpecifiedRate`, etc. | +| | Current Item State | Detect when the current item changes or is removed. | +| | Volume Change Events | Listen for changes in volume levels. | + +| **Testing & Development** | Unit Testing | Includes unit tests for core functionality. | +| | UI Testing | Integration of UI tests in the example app. | +| | Example Scripts | Automated testing scripts for easier test execution. | + +| **Media Support** | File Types | `.mp4`, `.mov`, `.m4v`, `.3gp`, `.mkv` (limited support). | +| | Codecs | H.264, H.265 (HEVC), MPEG-4, AAC, MP3. | +| | Streaming Protocols | HLS (`.m3u8`) support for adaptive streaming. | + ## [SwiftUI app example](https://github.com/swiftuiux/swiftui-video-player-example) ## [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) ## Philosophy of Player Dynamics @@ -31,7 +85,7 @@ 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** ### CornerRadius -You can reach out the effect via mask modifier +You can reach out the effect simply via mask modifier ```swift ExtVideoPlayer( settings : $settings, From 7b78701141594ec704f1f7c6ddf86e47159801a0 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 11 Jan 2025 12:20:02 +0100 Subject: [PATCH 012/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 86c5a86..8686d28 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for | | Background Playback | Continue playback in the background with appropriate audio session setup. | | | Custom Overlays | Add vector graphics and custom overlays over the video. | -| **Playback Commands** | Idle Command | Initialize without specific playback actions. | +| **Playback Commands** | Idle Command | Initialize without specific playback actions. | | | Play/Pause | Control playback state. | | | Seek Command | Move to specific video timestamps. | | | Mute/Unmute | Toggle audio playback. | From a25fccf4e8dc06caeff0dfed8ac2dba9bdbd687d Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 11 Jan 2025 12:20:36 +0100 Subject: [PATCH 013/209] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8686d28..7b2d3f6 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for | | Background Playback | Continue playback in the background with appropriate audio session setup. | | | Custom Overlays | Add vector graphics and custom overlays over the video. | -| **Playback Commands** | Idle Command | Initialize without specific playback actions. | +| **Playback Commands** | Idle Command | Initialize without specific playback actions. | | | Play/Pause | Control playback state. | | | Seek Command | Move to specific video timestamps. | | | Mute/Unmute | Toggle audio playback. | @@ -54,6 +54,7 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for | **Playback Features** | Adaptive HLS Streaming | Dynamic quality adjustment based on network speed. | | | Seamless Item Transitions | Smooth transitions between video items. | +| | Picture-in-Picture (PiP) | Support for PiP mode on compatible devices. | | | Multichannel Audio | Play Dolby Atmos, 5.1 surround, and spatial audio tracks. | | | Subtitles and Captions | Support for multiple subtitle and caption formats. | @@ -69,7 +70,6 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for | **Media Support** | File Types | `.mp4`, `.mov`, `.m4v`, `.3gp`, `.mkv` (limited support). | | | Codecs | H.264, H.265 (HEVC), MPEG-4, AAC, MP3. | | | Streaming Protocols | HLS (`.m3u8`) support for adaptive streaming. | - ## [SwiftUI app example](https://github.com/swiftuiux/swiftui-video-player-example) ## [Documentation(API)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer/main/documentation/swiftui_loop_videoplayer) From 298d4bfb8735d2de4fd675f068d7ccbb2103977e Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 11 Jan 2025 12:21:16 +0100 Subject: [PATCH 014/209] Update README.md --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 7b2d3f6..7832267 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for | | Subtitle Support | Add external `.vtt` files or use embedded subtitle tracks. | | | Background Playback | Continue playback in the background with appropriate audio session setup. | | | Custom Overlays | Add vector graphics and custom overlays over the video. | - | **Playback Commands** | Idle Command | Initialize without specific playback actions. | | | Play/Pause | Control playback state. | | | Seek Command | Move to specific video timestamps. | @@ -37,7 +36,6 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for | | Apply Filters | Add Core Image filters to the video stream. | | | Remove Filters | Clear all applied filters. | | | Add Vector Graphics | Overlay custom vector graphics onto the video. | - | **Settings** | SourceName | Define video source (local or remote). | | | File Extension | Default extension for video files (e.g., `.mp4`). | | | Gravity | Set content resizing behavior (e.g., `.resizeAspect`). | @@ -46,27 +44,22 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for | | Mute by Default | Initialize playback without sound. | | | Subtitle Integration | Configure subtitles from embedded tracks or external files. | | | Error Widget Customization | Change error text color, font size, or disable built-in error widgets. | - | **Visual Features** | Rounded Corners | Apply rounded corners using SwiftUI's `.mask` modifier. | | | Overlay Graphics | Add vector graphics over video for custom effects. | | | Brightness Adjustment | Control brightness levels dynamically. | | | Contrast Adjustment | Modify video contrast in real time. | - | **Playback Features** | Adaptive HLS Streaming | Dynamic quality adjustment based on network speed. | | | Seamless Item Transitions | Smooth transitions between video items. | | | Picture-in-Picture (PiP) | Support for PiP mode on compatible devices. | | | Multichannel Audio | Play Dolby Atmos, 5.1 surround, and spatial audio tracks. | | | Subtitles and Captions | Support for multiple subtitle and caption formats. | - | **Event Handling** | Batch Event Processing | Collects and processes events in batches to avoid flooding. | | | Playback State Events | `playing`, `paused`, `seek`, `waitingToPlayAtSpecifiedRate`, etc. | | | Current Item State | Detect when the current item changes or is removed. | | | Volume Change Events | Listen for changes in volume levels. | - | **Testing & Development** | Unit Testing | Includes unit tests for core functionality. | | | UI Testing | Integration of UI tests in the example app. | | | Example Scripts | Automated testing scripts for easier test execution. | - | **Media Support** | File Types | `.mp4`, `.mov`, `.m4v`, `.3gp`, `.mkv` (limited support). | | | Codecs | H.264, H.265 (HEVC), MPEG-4, AAC, MP3. | | | Streaming Protocols | HLS (`.m3u8`) support for adaptive streaming. | From e8252e8ddfbb4405b341ffb73e70b0932c748c47 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 11 Jan 2025 12:22:18 +0100 Subject: [PATCH 015/209] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7832267..970b301 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for } ``` +## [SwiftUI app example](https://github.com/swiftuiux/swiftui-video-player-example) + +## [Documentation(API)](https://swiftpackageindex.com/swiftuiux/swiftui-loop-videoplayer/main/documentation/swiftui_loop_videoplayer) + | **Feature Category** | **Feature Name** | **Description** | |----------------------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------| | **General** | SwiftUI Declarative Syntax | Easily integrate using declarative syntax. | @@ -50,7 +54,6 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for | | Contrast Adjustment | Modify video contrast in real time. | | **Playback Features** | Adaptive HLS Streaming | Dynamic quality adjustment based on network speed. | | | Seamless Item Transitions | Smooth transitions between video items. | -| | Picture-in-Picture (PiP) | Support for PiP mode on compatible devices. | | | Multichannel Audio | Play Dolby Atmos, 5.1 surround, and spatial audio tracks. | | | Subtitles and Captions | Support for multiple subtitle and caption formats. | | **Event Handling** | Batch Event Processing | Collects and processes events in batches to avoid flooding. | @@ -63,9 +66,7 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for | **Media Support** | File Types | `.mp4`, `.mov`, `.m4v`, `.3gp`, `.mkv` (limited support). | | | Codecs | H.264, H.265 (HEVC), MPEG-4, AAC, MP3. | | | Streaming Protocols | HLS (`.m3u8`) support for adaptive streaming. | -## [SwiftUI app example](https://github.com/swiftuiux/swiftui-video-player-example) -## [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) From baabf283cd52ea81e4c2ac4094f9941ecc1fda1f Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 11 Jan 2025 12:23:12 +0100 Subject: [PATCH 016/209] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 970b301..6bc12ca 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,9 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for } } ``` - ## [SwiftUI app example](https://github.com/swiftuiux/swiftui-video-player-example) -## [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) | **Feature Category** | **Feature Name** | **Description** | |----------------------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------| @@ -67,8 +66,7 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for | | Codecs | H.264, H.265 (HEVC), MPEG-4, AAC, MP3. | | | Streaming Protocols | HLS (`.m3u8`) support for adaptive streaming. | - -![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 From 1ae7306b9b3b8e6bff7584fc30faab09a8594ab4 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 11 Jan 2025 12:23:57 +0100 Subject: [PATCH 017/209] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 6bc12ca..a341df0 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for | | Adaptive HLS Streaming | Handles HLS streaming with dynamic quality adjustment. | | | Error Handling | Customizable error messages and visual displays. | | | Subtitle Support | Add external `.vtt` files or use embedded subtitle tracks. | -| | Background Playback | Continue playback in the background with appropriate audio session setup. | | | Custom Overlays | Add vector graphics and custom overlays over the video. | | **Playback Commands** | Idle Command | Initialize without specific playback actions. | | | Play/Pause | Control playback state. | From 3082085ed171bfb60155eec0e9bff0a625195cd8 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 11 Jan 2025 12:26:09 +0100 Subject: [PATCH 018/209] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a341df0..6829aec 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ ## 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. 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. + +## [SwiftUI app example](https://github.com/swiftuiux/swiftui-video-player-example) + ```swift ExtVideoPlayer{ VideoSettings{ @@ -14,7 +17,6 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for } } ``` -## [SwiftUI app example](https://github.com/swiftuiux/swiftui-video-player-example) ![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/swiftui_video_player.gif) From fbfc268715a5390e5d1dcb7aedabd370ccec4f55 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 11 Jan 2025 12:27:26 +0100 Subject: [PATCH 019/209] Update README.md --- README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6829aec..a58e213 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,14 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for ## [SwiftUI app example](https://github.com/swiftuiux/swiftui-video-player-example) - ```swift - ExtVideoPlayer{ - VideoSettings{ - SourceName("swipe") - } - } -``` - -![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/swiftui_video_player.gif) +| **Code Example** | **App Example** | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------| +| ```swift | ![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/swiftui_video_player.gif) | +| ExtVideoPlayer{ | | +| VideoSettings{ | | +| SourceName("swipe") | | +| } | | +| } | | | **Feature Category** | **Feature Name** | **Description** | |----------------------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------| From 3e07828ef7cbdf9880ac7c293577690be67c9e6b Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 11 Jan 2025 12:29:43 +0100 Subject: [PATCH 020/209] Update README.md --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a58e213..88de165 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ ## 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. 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. - ## [SwiftUI app example](https://github.com/swiftuiux/swiftui-video-player-example) -| **Code Example** | **App Example** | -|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------| -| ```swift | ![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/swiftui_video_player.gif) | -| ExtVideoPlayer{ | | -| VideoSettings{ | | -| SourceName("swipe") | | -| } | | -| } | | + ```swift + ExtVideoPlayer{ + VideoSettings{ + SourceName("swipe") + } + } +``` + +![The concept](https://github.com/swiftuiux/swiftui-video-player-example/blob/main/swiftui-loop-videoplayer-example/img/swiftui_video_player.gif) | **Feature Category** | **Feature Name** | **Description** | |----------------------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------| From bc5937ce1ae3f1ef701e493273c0260befecded1 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 14 Jan 2025 14:14:42 +0100 Subject: [PATCH 021/209] added new setting --- README.md | 1 + .../enum/Setting.swift | 3 ++ .../player/LoopingPlayerProtocol.swift | 39 ++++++++++++------- .../protocol/vector/VectorLayerProtocol.swift | 6 +-- .../settings/EnableVector.swift | 29 ++++++++++++++ .../utils/VideoSettings.swift | 10 ++++- .../view/player/ios/LoopingPlayerUIView.swift | 13 ++++++- .../view/player/mac/LoopingPlayerNSView.swift | 15 +++++-- 8 files changed, 91 insertions(+), 25 deletions(-) create mode 100644 Sources/swiftui-loop-videoplayer/settings/EnableVector.swift diff --git a/README.md b/README.md index 88de165..4828bab 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ Please note that using videos from URLs requires ensuring that you have the righ | **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 | +| **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 | | **EColor** | Error message text color. | .red | | **EFontSize** | Size of the error text. | 17.0 | | **ErrorWidgetOff** | Do not show inner error showcase component. In case you'd like to implement your own error Alert widget. | - | diff --git a/Sources/swiftui-loop-videoplayer/enum/Setting.swift b/Sources/swiftui-loop-videoplayer/enum/Setting.swift index 5e6c73c..f363545 100644 --- a/Sources/swiftui-loop-videoplayer/enum/Setting.swift +++ b/Sources/swiftui-loop-videoplayer/enum/Setting.swift @@ -21,6 +21,9 @@ public enum Setting: Equatable, SettingsConvertible{ [self] } + ///Enable vector layer to add overlay vector graphics + case vector + /// Loop video case loop diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift index f1ea3cc..3c9ca7f 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift @@ -84,8 +84,7 @@ internal extension LoopingPlayerProtocol { timePublishing: CMTime? ) { - let player = AVQueuePlayer(items: []) - self.player = player + guard let player else { return } configurePlayer(player, settings: settings, timePublishing: timePublishing) @@ -108,19 +107,19 @@ internal extension LoopingPlayerProtocol { player.isMuted = settings.mute playerLayer.player = player playerLayer.videoGravity = settings.gravity + #if canImport(UIKit) playerLayer.backgroundColor = UIColor.clear.cgColor layer.addSublayer(playerLayer) - layer.addSublayer(compositeLayer) #elseif canImport(AppKit) playerLayer.backgroundColor = NSColor.clear.cgColor let layer = CALayer() layer.addSublayer(playerLayer) - layer.addSublayer(compositeLayer) self.layer = layer self.wantsLayer = true #endif - compositeLayer.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height) + + configureCompositeLayer() if let timePublishing{ timeObserver = player.addPeriodicTimeObserver(forInterval: timePublishing, queue: .main) { [weak self] time in @@ -132,14 +131,18 @@ internal extension LoopingPlayerProtocol { } } - /// Clears all items from the player's queue. - func clearPlayerQueue() { - guard let items = player?.items() else { return } - for item in items { - player?.remove(item) - } - } - + func configureCompositeLayer() { + + compositeLayer?.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height) + + guard let compositeLayer else { return } + + #if canImport(UIKit) + layer.addSublayer(compositeLayer) + #elseif canImport(AppKit) + self.layer?.addSublayer(compositeLayer) + #endif + } /// Updates the current playback asset, settings, and initializes playback or a specific action when the asset is ready. /// @@ -241,6 +244,14 @@ internal extension LoopingPlayerProtocol { } } + /// Clears all items from the player's queue. + func clearPlayerQueue() { + guard let items = player?.items() else { return } + for item in items { + player?.remove(item) + } + } + /// Removes observers for handling errors. /// /// This method ensures that the error observer is properly invalidated and the reference is cleared. @@ -261,8 +272,6 @@ internal extension LoopingPlayerProtocol { delegate?.didReceiveError(.remoteVideoError(error)) } - - /// 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. diff --git a/Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift index 2e3c926..5f09e8c 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift @@ -26,7 +26,7 @@ public protocol LayerMakerProtocol { /// The composite layer that contains all the sublayers, including vector layers. /// /// This layer acts as a container for all vector layers added through the protocol methods. - var compositeLayer: CALayer { get } + var compositeLayer: CALayer? { get set } /// The frame of the composite layer. /// @@ -61,14 +61,14 @@ extension LayerMakerProtocol{ func addVectorLayer(builder : any ShapeLayerBuilderProtocol, clear: Bool){ if clear{ removeAllVectors() } let layer = builder.build(with: (frame, bounds)) - compositeLayer.addSublayer(layer) + compositeLayer?.addSublayer(layer) } /// Removes all vector layers from the composite layer. @MainActor func removeAllVectors(){ - compositeLayer.sublayers?.forEach { $0.removeFromSuperlayer() } + compositeLayer?.sublayers?.forEach { $0.removeFromSuperlayer() } } } diff --git a/Sources/swiftui-loop-videoplayer/settings/EnableVector.swift b/Sources/swiftui-loop-videoplayer/settings/EnableVector.swift new file mode 100644 index 0000000..f386581 --- /dev/null +++ b/Sources/swiftui-loop-videoplayer/settings/EnableVector.swift @@ -0,0 +1,29 @@ +// +// EnableVector.swift +// +// +// Created by Igor Shelopaev on 14.01.25. +// + +import Foundation + +/// A structure to enable a vector layer for overlaying vector graphics. +/// +/// Use this struct to activate settings that allow the addition of vector-based +/// overlays via commands, such as shapes, paths, or other scalable graphics, on top of existing content. +/// This structure is designed with optimization in mind, ensuring that extra layers +/// are not added if they are unnecessary, reducing overhead and improving performance. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, *) +public struct EnableVector: SettingsConvertible{ + + // MARK: - Life circle + + /// Initializes a new instance + public init() {} + + /// Fetch settings + @_spi(Private) + public func asSettings() -> [Setting] { + [.vector] + } +} diff --git a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift index 4453b85..0cbe091 100644 --- a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift +++ b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift @@ -29,6 +29,9 @@ public struct VideoSettings: Equatable{ /// Mute video public let mute: Bool + /// Enable vector layer to add overlay vector graphics + public let vector: Bool + /// Don't auto play video after initialization public let notAutoPlay: Bool @@ -72,7 +75,7 @@ public struct VideoSettings: Equatable{ /// - errorColor: The color used for error messages. /// - errorFontSize: The font size for error messages. /// - errorWidgetOff: A Boolean indicating whether the error widget should be turned off. - public init(name: String, ext: String, subtitles: String, loop: Bool, mute: Bool, notAutoPlay: Bool, timePublishing: CMTime?, gravity: AVLayerVideoGravity, errorColor: Color, errorFontSize: CGFloat, errorWidgetOff: Bool) { + public init(name: String, ext: String, subtitles: String, loop: Bool, mute: Bool, notAutoPlay: Bool, timePublishing: CMTime?, gravity: AVLayerVideoGravity, errorColor: Color, errorFontSize: CGFloat, errorWidgetOff: Bool, enableVector : Bool = false) { self.name = name self.ext = ext self.subtitles = subtitles @@ -84,6 +87,7 @@ public struct VideoSettings: Equatable{ self.errorColor = errorColor self.errorFontSize = errorFontSize self.errorWidgetOff = errorWidgetOff + self.vector = enableVector self.unique = true } @@ -115,6 +119,8 @@ public struct VideoSettings: Equatable{ notAutoPlay = settings.contains(.notAutoPlay) errorWidgetOff = settings.contains(.errorWidgetOff) + + vector = settings.contains(.vector) } } @@ -123,7 +129,7 @@ public extension VideoSettings { /// Returns a new instance of VideoSettings with loop set to false and notAutoPlay set to true, keeping other settings unchanged. var GetSettingsWithNotAutoPlay : VideoSettings { - VideoSettings(name: self.name, ext: self.ext, subtitles: self.subtitles, loop: self.loop, mute: self.mute, notAutoPlay: true, timePublishing: self.timePublishing, gravity: self.gravity, errorColor: self.errorColor, errorFontSize: self.errorFontSize, errorWidgetOff: self.errorWidgetOff) + VideoSettings(name: self.name, ext: self.ext, subtitles: self.subtitles, loop: self.loop, mute: self.mute, notAutoPlay: true, timePublishing: self.timePublishing, gravity: self.gravity, errorColor: self.errorColor, errorFontSize: self.errorFontSize, errorWidgetOff: self.errorWidgetOff, enableVector: self.vector) } /// Checks if the asset has changed based on the provided settings and current asset. diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift index def6c4d..1f2d483 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift @@ -30,10 +30,10 @@ internal class LoopingPlayerUIView: UIView, LoopingPlayerProtocol { internal var contrast: Float = 1 /// A CALayer instance used for composing content, accessible only within the module. - internal let compositeLayer = CALayer() + internal var compositeLayer : CALayer? /// The AVPlayerLayer that displays the video content. - internal let playerLayer = AVPlayerLayer() + internal let playerLayer : AVPlayerLayer /// The looper responsible for continuous video playback. internal var playerLooper: AVPlayerLooper? @@ -72,7 +72,16 @@ internal class LoopingPlayerUIView: UIView, LoopingPlayerProtocol { /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. /// - timePublishing: Optional `CMTime` that specifies a particular time for publishing or triggering an event. required init(asset: AVURLAsset, settings: VideoSettings, timePublishing: CMTime?){ + + player = AVQueuePlayer(items: []) + + playerLayer = AVPlayerLayer() + if settings.vector{ + compositeLayer = CALayer() + } + super.init(frame: .zero) + setupPlayerComponents( asset: asset, settings: settings, timePublishing : timePublishing ) diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift index 0688c0d..bd23ecb 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift @@ -32,16 +32,16 @@ internal class LoopingPlayerNSView: NSView, LoopingPlayerProtocol { internal var contrast: Float = 1 /// A CALayer instance used for composing content, accessible only within the module. - internal let compositeLayer = CALayer() + internal var compositeLayer : CALayer? /// The AVPlayerLayer that displays the video content. - internal let playerLayer = AVPlayerLayer() + internal let playerLayer : AVPlayerLayer /// The looper responsible for continuous video playback. internal var playerLooper: AVPlayerLooper? /// The queue player that plays the video items. - internal var player: AVQueuePlayer? + internal var player: AVQueuePlayer? = AVQueuePlayer(items: []) /// Declare a variable to hold the time observer token outside the if statement internal var timeObserver: Any? @@ -74,7 +74,16 @@ internal class LoopingPlayerNSView: NSView, LoopingPlayerProtocol { /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. /// - timePublishing: Optional `CMTime` for publishing or triggering an event at a specific time. required init(asset: AVURLAsset, settings: VideoSettings, timePublishing: CMTime?) { + + player = AVQueuePlayer(items: []) + + playerLayer = AVPlayerLayer() + if settings.vector{ + compositeLayer = CALayer() + } + super.init(frame: .zero) + setupPlayerComponents( asset: asset, settings: settings, timePublishing: timePublishing ) From a9722aa347c694c8cd343085d30ef50bb7ae2b38 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 14 Jan 2025 14:55:47 +0100 Subject: [PATCH 022/209] Update ExtVideoPlayer.swift --- Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift index 9b10eaf..ae93699 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -104,14 +104,12 @@ public struct ExtVideoPlayer: View{ /// The body property defines the view hierarchy for the user interface. public var body: some View { - LoopPlayerMultiPlatform( settings: $settings, command: $command, timePublisher: timePublisher, eventPublisher: eventPublisher ) - .frame(maxWidth: .infinity, maxHeight: .infinity) .onReceive(timePublisher.receive(on: DispatchQueue.main), perform: { time in currentTime = time }) From af61a7000dbbd33309f8fc39cf85190e5d7e8d55 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 14 Jan 2025 15:17:37 +0100 Subject: [PATCH 023/209] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4828bab..132b9ed 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ [![](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. 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. 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. + +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](https://github.com/swiftuiux/swiftui-video-player-example) From 3039b06b3f2a1a11d28838665f9b50194038c693 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 14 Jan 2025 15:25:26 +0100 Subject: [PATCH 024/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 132b9ed..59feaa4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Apple’s VideoPlayer offers a quick setup for video playback in SwiftUI but for 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](https://github.com/swiftuiux/swiftui-video-player-example) +## SwiftUI app example [follow the link](https://github.com/swiftuiux/swiftui-video-player-example) ```swift ExtVideoPlayer{ From 56dfa4e466af358a12014fca55d0e5dc49c319d0 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 14 Jan 2025 15:26:01 +0100 Subject: [PATCH 025/209] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 59feaa4..d61f4cf 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ It is a pure package without any third-party libraries. My main focus was on per ## SwiftUI app example [follow the link](https://github.com/swiftuiux/swiftui-video-player-example) + ```swift ExtVideoPlayer{ VideoSettings{ From 58c76c058c9a1336d7d1d36f6bdaf7b631512d42 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 14 Jan 2025 17:19:20 +0100 Subject: [PATCH 026/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d61f4cf..b1d4b70 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## 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. 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. -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. +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) From 548a7b2a64e629ebbe1d407dffadfd615382a0a7 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 14:22:04 +0100 Subject: [PATCH 027/209] update --- .../player/LoopingPlayerProtocol.swift | 82 +++++++++++-------- .../view/LoopPlayerViewProtocol.swift | 2 +- .../view/player/ios/LoopingPlayerUIView.swift | 4 +- .../view/player/mac/LoopingPlayerNSView.swift | 4 +- 4 files changed, 55 insertions(+), 37 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift index 3c9ca7f..5270071 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift @@ -54,8 +54,7 @@ public protocol LoopingPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ /// - Parameters: /// - asset: The `AVURLAsset` used for video playback. /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. - /// - timePublishing: Optional `CMTime` that specifies a particular time for publishing or triggering an event. - init(asset: AVURLAsset, settings: VideoSettings, timePublishing: CMTime?) + init(asset: AVURLAsset, settings: VideoSettings) /// Sets up the necessary observers on the AVPlayerItem and AVQueuePlayer to monitor changes and errors. /// @@ -80,16 +79,13 @@ internal extension LoopingPlayerProtocol { /// - timePublishing: Optional `CMTime` that specifies a particular time for publishing or triggering an event. func setupPlayerComponents( asset: AVURLAsset, - settings: VideoSettings, - timePublishing: CMTime? + settings: VideoSettings ) { guard let player else { return } - configurePlayer(player, settings: settings, timePublishing: timePublishing) - + configurePlayer(player, settings: settings) update(asset: asset, settings: settings) - setupObservers(for: player) } @@ -98,13 +94,24 @@ internal extension LoopingPlayerProtocol { /// - Parameters: /// - player: The AVQueuePlayer to be configured. /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. - /// - timePublishing: Optional interval for publishing the current playback time; nil disables this feature. func configurePlayer( _ player: AVQueuePlayer, - settings: VideoSettings, - timePublishing: CMTime? + settings: VideoSettings ) { + player.isMuted = settings.mute + + configurePlayerLayer(player, settings) + configureCompositeLayer(settings) + configureTimePublishing(player, settings) + + } + + /// Configures the player layer for the specified video player using the provided settings. + /// - Parameters: + /// - player: The `AVQueuePlayer` instance for which the player layer will be configured. + /// - settings: A `VideoSettings` object containing configuration details for the player layer. + func configurePlayerLayer(_ player: AVQueuePlayer, _ settings: VideoSettings) { playerLayer.player = player playerLayer.videoGravity = settings.gravity @@ -118,10 +125,14 @@ internal extension LoopingPlayerProtocol { self.layer = layer self.wantsLayer = true #endif - - configureCompositeLayer() - - if let timePublishing{ + } + + /// Configures the time publishing observer for the specified video player. + /// - Parameters: + /// - 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 guard let self = self else{ return } DispatchQueue.main.async { @@ -131,7 +142,11 @@ internal extension LoopingPlayerProtocol { } } - func configureCompositeLayer() { + /// Configures the composite layer for the view based on the provided video settings. + /// - Parameter settings: A `VideoSettings` object containing configuration details for the composite layer. + func configureCompositeLayer(_ settings: VideoSettings) { + + guard settings.vector else { return } compositeLayer?.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height) @@ -161,44 +176,47 @@ internal extension LoopingPlayerProtocol { guard let player = player else { return } currentSettings = settings + player.pause() - if !player.items().isEmpty { - // Cleaning + if !player.items().isEmpty { // Cleaning unloop() clearPlayerQueue() removeAllFilters() } - let newItem: AVPlayerItem + let newItem = createPlayerItem(with: asset, settings: settings) - // try to retrieve the .vtt subtitle - if let subtitleAsset = subtitlesAssetFor(settings), - let mergedAsset = mergeAssetWithSubtitles(videoAsset: asset, subtitleAsset: subtitleAsset) { - // Create a new AVPlayerItem from the merged asset - newItem = AVPlayerItem(asset: mergedAsset) - }else{ - // Create a new AVPlayerItem from the merged asset - newItem = AVPlayerItem(asset: asset) - } - - // Insert the new item into the player queue player.insert(newItem, after: nil) - // Loop if required if settings.loop { loop() } - + // Observe status changes setupStateItemStatusObserver(newItem: newItem, callback: callback) - // Autoplay if allowed if !settings.notAutoPlay { play() } } + /// Creates an `AVPlayerItem` with optional subtitle merging. + /// - Parameters: + /// - asset: The main video asset. + /// - settings: A `VideoSettings` object containing subtitle configuration. + /// - Returns: A new `AVPlayerItem` configured with the merged or original asset. + func createPlayerItem(with asset: AVURLAsset, settings: VideoSettings) -> AVPlayerItem { + // Attempt to retrieve the subtitle asset + if let subtitleAsset = subtitlesAssetFor(settings), + let mergedAsset = mergeAssetWithSubtitles(videoAsset: asset, subtitleAsset: subtitleAsset) { + // Create and return a new `AVPlayerItem` using the merged asset + return AVPlayerItem(asset: mergedAsset) + } else { + // Create and return a new `AVPlayerItem` using the original asset + return AVPlayerItem(asset: asset) + } + } /// Sets up observers on the player item and the player to track their status and error states. /// diff --git a/Sources/swiftui-loop-videoplayer/protocol/view/LoopPlayerViewProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/view/LoopPlayerViewProtocol.swift index 733b757..0f86d4c 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/view/LoopPlayerViewProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/view/LoopPlayerViewProtocol.swift @@ -98,7 +98,7 @@ public extension LoopPlayerViewProtocol{ asset: AVURLAsset?) -> PlayerView? { if let asset{ - let player = PlayerView(asset: asset, settings: settings, timePublishing: settings.timePublishing) + let player = PlayerView(asset: asset, settings: settings) container.addSubview(player) activateFullScreenConstraints(for: player, in: container) return player diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift index 1f2d483..f570b7b 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift @@ -71,7 +71,7 @@ internal class LoopingPlayerUIView: UIView, LoopingPlayerProtocol { /// - asset: The `AVURLAsset` used for video playback. /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. /// - timePublishing: Optional `CMTime` that specifies a particular time for publishing or triggering an event. - required init(asset: AVURLAsset, settings: VideoSettings, timePublishing: CMTime?){ + required init(asset: AVURLAsset, settings: VideoSettings){ player = AVQueuePlayer(items: []) @@ -83,7 +83,7 @@ internal class LoopingPlayerUIView: UIView, LoopingPlayerProtocol { super.init(frame: .zero) setupPlayerComponents( - asset: asset, settings: settings, timePublishing : timePublishing + asset: asset, settings: settings ) } diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift index bd23ecb..b8fe0f5 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift @@ -73,7 +73,7 @@ internal class LoopingPlayerNSView: NSView, LoopingPlayerProtocol { /// - asset: The `AVURLAsset` for video playback. /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. /// - timePublishing: Optional `CMTime` for publishing or triggering an event at a specific time. - required init(asset: AVURLAsset, settings: VideoSettings, timePublishing: CMTime?) { + required init(asset: AVURLAsset, settings: VideoSettings) { player = AVQueuePlayer(items: []) @@ -85,7 +85,7 @@ internal class LoopingPlayerNSView: NSView, LoopingPlayerProtocol { super.init(frame: .zero) setupPlayerComponents( - asset: asset, settings: settings, timePublishing: timePublishing + asset: asset, settings: settings ) } From 20c2d75ed2e7854996afeb20a58c8012e13439bf Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 14:22:32 +0100 Subject: [PATCH 028/209] Update LoopingPlayerProtocol.swift --- .../protocol/player/LoopingPlayerProtocol.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift index 5270071..dc2a6c2 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift @@ -104,7 +104,6 @@ internal extension LoopingPlayerProtocol { configurePlayerLayer(player, settings) configureCompositeLayer(settings) configureTimePublishing(player, settings) - } /// Configures the player layer for the specified video player using the provided settings. From be0283ef871ef4c8d533ac93e8f7044faff39b9a Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 14:58:41 +0100 Subject: [PATCH 029/209] update --- .../view/player/ios/LoopingPlayerUIView.swift | 1 - .../view/player/mac/LoopingPlayerNSView.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift index f570b7b..449f1ff 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift @@ -70,7 +70,6 @@ internal class LoopingPlayerUIView: UIView, LoopingPlayerProtocol { /// - Parameters: /// - asset: The `AVURLAsset` used for video playback. /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. - /// - timePublishing: Optional `CMTime` that specifies a particular time for publishing or triggering an event. required init(asset: AVURLAsset, settings: VideoSettings){ player = AVQueuePlayer(items: []) diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift index b8fe0f5..968355c 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift @@ -72,7 +72,6 @@ internal class LoopingPlayerNSView: NSView, LoopingPlayerProtocol { /// - Parameters: /// - asset: The `AVURLAsset` for video playback. /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. - /// - timePublishing: Optional `CMTime` for publishing or triggering an event at a specific time. required init(asset: AVURLAsset, settings: VideoSettings) { player = AVQueuePlayer(items: []) From 41c65e64b525bb16b86b193d1fbd9e3186f9c234 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 15:06:57 +0100 Subject: [PATCH 030/209] refactor subtitles --- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 20 +++++++++---------- .../protocol/vector/VectorLayerProtocol.swift | 1 - 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index dccd4c0..87e7e0f 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -154,10 +154,18 @@ func mergeAssetWithSubtitles(videoAsset: AVURLAsset, subtitleAsset: AVURLAsset) #if !os(visionOS) + // 1) Find the TEXT track in the subtitle asset + guard let textTrack = subtitleAsset.tracks(withMediaType: .text).first else { + #if DEBUG + print("No text track found in subtitle file.") + #endif + return videoAsset // Return just videoAsset if no text track + } + // Create a new composition let composition = AVMutableComposition() - // 1) Copy the VIDEO track (and AUDIO track if available) from the original video + // 2) Copy the VIDEO track (and AUDIO track if available) from the original video do { // VIDEO if let videoTrack = videoAsset.tracks(withMediaType: .video).first { @@ -190,14 +198,6 @@ func mergeAssetWithSubtitles(videoAsset: AVURLAsset, subtitleAsset: AVURLAsset) return nil } - // 2) Find the TEXT track in the subtitle asset - guard let textTrack = subtitleAsset.tracks(withMediaType: .text).first else { - #if DEBUG - print("No text track found in subtitle file.") - #endif - return composition // Return just the video/audio if no text track - } - // 3) Insert the subtitle track into the composition do { let compTextTrack = composition.addMutableTrack( @@ -218,6 +218,6 @@ func mergeAssetWithSubtitles(videoAsset: AVURLAsset, subtitleAsset: AVURLAsset) return composition #else - return nil + return videoAsset #endif } diff --git a/Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift index 5f09e8c..3c58af7 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift @@ -70,5 +70,4 @@ extension LayerMakerProtocol{ func removeAllVectors(){ compositeLayer?.sublayers?.forEach { $0.removeFromSuperlayer() } } - } From 5318bbab45c0a3338f2ba71a963d96fd74eabdfa Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 15:09:05 +0100 Subject: [PATCH 031/209] Update fn+.swift --- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index 87e7e0f..4322363 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -195,7 +195,7 @@ func mergeAssetWithSubtitles(videoAsset: AVURLAsset, subtitleAsset: AVURLAsset) #if DEBUG print("Error adding video/audio tracks: \(error)") #endif - return nil + return videoAsset } // 3) Insert the subtitle track into the composition @@ -213,6 +213,7 @@ func mergeAssetWithSubtitles(videoAsset: AVURLAsset, subtitleAsset: AVURLAsset) #if DEBUG print("Error adding text track: \(error)") #endif + return videoAsset } return composition From dfc629ed76ef84a9a4d670c57cd29e99e115a38b Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 15:19:29 +0100 Subject: [PATCH 032/209] Update LoopingPlayerProtocol.swift --- .../protocol/player/LoopingPlayerProtocol.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift index dc2a6c2..025034f 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift @@ -263,10 +263,7 @@ internal extension LoopingPlayerProtocol { /// Clears all items from the player's queue. func clearPlayerQueue() { - guard let items = player?.items() else { return } - for item in items { - player?.remove(item) - } + player?.removeAllItems() } /// Removes observers for handling errors. From 499edd5d33231bb32c32b9239077116cdc789768 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 16:25:01 +0100 Subject: [PATCH 033/209] update seek command --- .../protocol/player/AbstractPlayer.swift | 7 +++++++ .../protocol/player/LoopingPlayerProtocol.swift | 3 +-- .../protocol/view/LoopPlayerViewProtocol.swift | 17 +++++------------ .../view/main/LoopPlayerMultiPlatform.swift | 3 +-- .../view/player/ios/LoopingPlayerUIView.swift | 10 ++++++---- .../view/player/mac/LoopingPlayerNSView.swift | 10 ++++++---- 6 files changed, 26 insertions(+), 24 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 9eeff06..d518049 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -188,8 +188,15 @@ extension AbstractPlayer{ /// - Parameter time: The target time to seek to in the video timeline. func seek(to time: Double) { guard let player = player, let duration = player.currentItem?.duration else { + if let settings = currentSettings, let asset = assetFor(settings){ + update(asset: asset, settings: settings, callback: nil) + seek(to: time) + return + } + delegate?.didSeek(value: false, currentTime: time) return + } guard currentItem?.status == .readyToPlay else{ diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift index 025034f..44d8ee9 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift @@ -52,9 +52,8 @@ public protocol LoopingPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ /// Initializes a new player view with a video asset and custom settings. /// /// - Parameters: - /// - asset: The `AVURLAsset` used for video playback. /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. - init(asset: AVURLAsset, settings: VideoSettings) + init(settings: VideoSettings) /// Sets up the necessary observers on the AVPlayerItem and AVQueuePlayer to monitor changes and errors. /// diff --git a/Sources/swiftui-loop-videoplayer/protocol/view/LoopPlayerViewProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/view/LoopPlayerViewProtocol.swift index 0f86d4c..ea0e510 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/view/LoopPlayerViewProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/view/LoopPlayerViewProtocol.swift @@ -90,20 +90,13 @@ public extension LoopPlayerViewProtocol{ /// Creates a player view for looping video content. /// - Parameters: /// - context: The UIViewRepresentable context providing environment data and coordinator. - /// - asset: The AVURLAsset to be used for video playback. /// - Returns: A PlayerView instance conforming to LoopingPlayerProtocol. @MainActor - func makePlayerView( - _ container: View, - asset: AVURLAsset?) -> PlayerView? { + func makePlayerView(_ container: View) -> PlayerView? { - if let asset{ - let player = PlayerView(asset: asset, settings: settings) - container.addSubview(player) - activateFullScreenConstraints(for: player, in: container) - return player - } - - return nil + let player = PlayerView(settings: settings) + container.addSubview(player) + activateFullScreenConstraints(for: player, in: container) + return player } } diff --git a/Sources/swiftui-loop-videoplayer/view/main/LoopPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/main/LoopPlayerMultiPlatform.swift index 01bbffe..42e435c 100644 --- a/Sources/swiftui-loop-videoplayer/view/main/LoopPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/main/LoopPlayerMultiPlatform.swift @@ -97,8 +97,7 @@ extension LoopPlayerMultiPlatform: UIViewRepresentable{ let container = UIView() if let player: PlayerView = makePlayerView( - container, - asset: asset){ + container){ player.delegate = context.coordinator } diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift index 449f1ff..bd0784f 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift @@ -70,7 +70,7 @@ internal class LoopingPlayerUIView: UIView, LoopingPlayerProtocol { /// - Parameters: /// - asset: The `AVURLAsset` used for video playback. /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. - required init(asset: AVURLAsset, settings: VideoSettings){ + required init(settings: VideoSettings){ player = AVQueuePlayer(items: []) @@ -81,9 +81,11 @@ internal class LoopingPlayerUIView: UIView, LoopingPlayerProtocol { super.init(frame: .zero) - setupPlayerComponents( - asset: asset, settings: settings - ) + if let asset = assetFor(settings){ + setupPlayerComponents( + asset: asset, settings: settings + ) + } } required init?(coder: NSCoder) { diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift index 968355c..9077201 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift @@ -72,7 +72,7 @@ internal class LoopingPlayerNSView: NSView, LoopingPlayerProtocol { /// - Parameters: /// - asset: The `AVURLAsset` for video playback. /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. - required init(asset: AVURLAsset, settings: VideoSettings) { + required init(settings: VideoSettings) { player = AVQueuePlayer(items: []) @@ -83,9 +83,11 @@ internal class LoopingPlayerNSView: NSView, LoopingPlayerProtocol { super.init(frame: .zero) - setupPlayerComponents( - asset: asset, settings: settings - ) + if let asset = assetFor(settings){ + setupPlayerComponents( + asset: asset, settings: settings + ) + } } required init?(coder: NSCoder) { From 2abc71b1a49aa4dda1c9d08d85e68f36576f559d Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 16:29:51 +0100 Subject: [PATCH 034/209] Update LoopPlayerMultiPlatform.swift --- .../view/main/LoopPlayerMultiPlatform.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/view/main/LoopPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/main/LoopPlayerMultiPlatform.swift index 42e435c..2a49bd8 100644 --- a/Sources/swiftui-loop-videoplayer/view/main/LoopPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/main/LoopPlayerMultiPlatform.swift @@ -144,8 +144,7 @@ extension LoopPlayerMultiPlatform: NSViewRepresentable{ let container = NSView() if let player: PlayerView = makePlayerView( - container, - asset: asset){ + container){ player.delegate = context.coordinator } From b2f3e663c82abf3c47bc8602be1e1874c95882fd Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 16:36:45 +0100 Subject: [PATCH 035/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b1d4b70..50b4446 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## 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. 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. -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. +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) From 76e9799c7adfbcdbfaffde1a536bc531b4ccc6d6 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 16:37:04 +0100 Subject: [PATCH 036/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 50b4446..1af86f3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## 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. 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. -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*. +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) From 340e89bcc1a90355f8e001a95100529126ada7b2 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 16:40:03 +0100 Subject: [PATCH 037/209] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1af86f3..c2646fb 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ It is a pure package without any third-party libraries. My main focus was on per |----------------------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------| | **General** | SwiftUI Declarative Syntax | Easily integrate using declarative syntax. | | | Platform Compatibility | Supports iOS 14+, macOS 11+, tvOS 14+. | +| | Swift Compatibility | Alined with Swift 5 and Swift 6 | | | Loop Playback | Automatically restart videos when they end. | | | Local and Remote Video URLs | Supports playback from local files or remote URLs. | | | Adaptive HLS Streaming | Handles HLS streaming with dynamic quality adjustment. | From 87f1153ec393a63e7d1ea88417046c4f5d1c96d7 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 16:42:45 +0100 Subject: [PATCH 038/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2646fb..a569dfd 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ It is a pure package without any third-party libraries. My main focus was on per |----------------------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------| | **General** | SwiftUI Declarative Syntax | Easily integrate using declarative syntax. | | | Platform Compatibility | Supports iOS 14+, macOS 11+, tvOS 14+. | -| | Swift Compatibility | Alined with Swift 5 and Swift 6 | +| | Swift Compatibility | Alined with Swift 5 and ready for Swift 6 | | | Loop Playback | Automatically restart videos when they end. | | | Local and Remote Video URLs | Supports playback from local files or remote URLs. | | | Adaptive HLS Streaming | Handles HLS streaming with dynamic quality adjustment. | From 5425c543cef216ae63fc9c770bad502b9eb58829 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 17:46:38 +0100 Subject: [PATCH 039/209] refactoring names --- Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift | 2 +- ...ingPlayerProtocol.swift => ExtPlayerProtocol.swift} | 4 ++-- ...rViewProtocol.swift => ExtPlayerViewProtocol.swift} | 10 +++++----- ...ultiPlatform.swift => ExtPlayerMultiPlatform.swift} | 10 +++++----- ...LoopingPlayerUIView.swift => ExtPlayerUIView.swift} | 2 +- ...LoopingPlayerNSView.swift => ExtPlayerNSView.swift} | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) rename Sources/swiftui-loop-videoplayer/protocol/player/{LoopingPlayerProtocol.swift => ExtPlayerProtocol.swift} (99%) rename Sources/swiftui-loop-videoplayer/protocol/view/{LoopPlayerViewProtocol.swift => ExtPlayerViewProtocol.swift} (93%) rename Sources/swiftui-loop-videoplayer/view/main/{LoopPlayerMultiPlatform.swift => ExtPlayerMultiPlatform.swift} (95%) rename Sources/swiftui-loop-videoplayer/view/player/ios/{LoopingPlayerUIView.swift => ExtPlayerUIView.swift} (98%) rename Sources/swiftui-loop-videoplayer/view/player/mac/{LoopingPlayerNSView.swift => ExtPlayerNSView.swift} (98%) diff --git a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift index ae93699..ec117cd 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -104,7 +104,7 @@ public struct ExtVideoPlayer: View{ /// The body property defines the view hierarchy for the user interface. public var body: some View { - LoopPlayerMultiPlatform( + ExtPlayerMultiPlatform( settings: $settings, command: $command, timePublisher: timePublisher, diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift similarity index 99% rename from Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift rename to Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 44d8ee9..327cba8 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/LoopingPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -18,7 +18,7 @@ import AppKit /// handle errors, and notify a delegate of important events. @available(iOS 14, macOS 11, tvOS 14, *) @MainActor @preconcurrency -public protocol LoopingPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ +public protocol ExtPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ #if canImport(UIKit) /// Provides a non-optional `CALayer` for use within UIKit environments. @@ -68,7 +68,7 @@ public protocol LoopingPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ func handlePlayerError(_ player: AVPlayer) } -internal extension LoopingPlayerProtocol { +internal extension ExtPlayerProtocol { /// Initializes a new player view with a video asset and custom settings. /// diff --git a/Sources/swiftui-loop-videoplayer/protocol/view/LoopPlayerViewProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift similarity index 93% rename from Sources/swiftui-loop-videoplayer/protocol/view/LoopPlayerViewProtocol.swift rename to Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift index ea0e510..d5eb73d 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/view/LoopPlayerViewProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift @@ -13,7 +13,7 @@ import Combine /// for looping video players on different platforms. @available(iOS 14, macOS 11, tvOS 14, *) @MainActor @preconcurrency -public protocol LoopPlayerViewProtocol { +public protocol ExtPlayerViewProtocol { #if canImport(UIKit) /// Typealias for the main view on iOS, using `UIView`. @@ -31,13 +31,13 @@ public protocol LoopPlayerViewProtocol { #if canImport(UIKit) /// Typealias for the player view on iOS, conforming to `LoopingPlayerProtocol` and using `UIView`. - associatedtype PlayerView: LoopingPlayerProtocol, UIView + associatedtype PlayerView: ExtPlayerProtocol, UIView #elseif os(macOS) /// Typealias for the player view on macOS, conforming to `LoopingPlayerProtocol` and using `NSView`. - associatedtype PlayerView: LoopingPlayerProtocol, NSView + associatedtype PlayerView: ExtPlayerProtocol, NSView #else /// Typealias for a custom player view on other platforms, conforming to `LoopingPlayerProtocol`. - associatedtype PlayerView: LoopingPlayerProtocol, CustomView + associatedtype PlayerView: ExtPlayerProtocol, CustomView #endif /// Settings for configuring the video player. @@ -58,7 +58,7 @@ public protocol LoopPlayerViewProtocol { } @available(iOS 14, macOS 11, tvOS 14, *) -public extension LoopPlayerViewProtocol{ +public extension ExtPlayerViewProtocol{ /// Updates the view by removing existing error messages and displaying a new one if an error is present. /// - Parameters: diff --git a/Sources/swiftui-loop-videoplayer/view/main/LoopPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift similarity index 95% rename from Sources/swiftui-loop-videoplayer/view/main/LoopPlayerMultiPlatform.swift rename to Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift index 2a49bd8..914ae64 100644 --- a/Sources/swiftui-loop-videoplayer/view/main/LoopPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift @@ -21,20 +21,20 @@ import AppKit #endif @MainActor -internal struct LoopPlayerMultiPlatform: LoopPlayerViewProtocol { +internal struct ExtPlayerMultiPlatform: ExtPlayerViewProtocol { #if canImport(UIKit) typealias View = UIView typealias ErrorView = ErrorMsgViewIOS - typealias PlayerView = LoopingPlayerUIView + typealias PlayerView = ExtPlayerUIView #elseif canImport(AppKit) typealias View = NSView typealias ErrorView = ErrorMsgViewMacOS - typealias PlayerView = LoopingPlayerNSView + typealias PlayerView = ExtPlayerNSView #endif /// A publisher that emits the current playback time as a `Double`. @@ -89,7 +89,7 @@ internal struct LoopPlayerMultiPlatform: LoopPlayerViewProtocol { } #if canImport(UIKit) -extension LoopPlayerMultiPlatform: UIViewRepresentable{ +extension ExtPlayerMultiPlatform: UIViewRepresentable{ /// Creates the container view with the player view and error view if needed /// - Parameter context: The context for the view /// - Returns: A configured UIView @@ -136,7 +136,7 @@ extension LoopPlayerMultiPlatform: UIViewRepresentable{ #endif #if canImport(AppKit) -extension LoopPlayerMultiPlatform: NSViewRepresentable{ +extension ExtPlayerMultiPlatform: NSViewRepresentable{ /// Creates the NSView for the representable component. It initializes the view, configures it with a player if available, and adds an error view if necessary. /// - Parameter context: The context containing environment and state information used during view creation. /// - Returns: A fully configured NSView containing both the media player and potentially an error message display. diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift similarity index 98% rename from Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift rename to Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index bd0784f..7e706e0 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/LoopingPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -15,7 +15,7 @@ import AVKit import UIKit @MainActor @preconcurrency -internal class LoopingPlayerUIView: UIView, LoopingPlayerProtocol { +internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { /// This property holds an instance of `VideoSettings` internal var currentSettings : VideoSettings? diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift similarity index 98% rename from Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift rename to Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index 9077201..74639be 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/LoopingPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -17,7 +17,7 @@ import AppKit /// A NSView subclass that loops video using AVFoundation on macOS. /// This class handles the initialization and management of a looping video player with customizable video gravity. @MainActor @preconcurrency -internal class LoopingPlayerNSView: NSView, LoopingPlayerProtocol { +internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { /// This property holds an instance of `VideoSettings` internal var currentSettings : VideoSettings? From 18782dda3face4fabbd5d28753ec82db30f718ef Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 18:05:04 +0100 Subject: [PATCH 040/209] update --- .../protocol/player/ExtPlayerProtocol.swift | 2 +- .../protocol/view/ExtPlayerViewProtocol.swift | 2 +- .../view/main/ExtPlayerMultiPlatform.swift | 2 +- .../view/player/ios/ExtPlayerUIView.swift | 2 +- .../view/player/mac/ExtPlayerNSView.swift | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 327cba8..541a13f 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -1,5 +1,5 @@ // -// LoopingPlayerProtocol.swift +// ExtPlayerProtocol.swift // // // Created by Igor Shelopaev on 05.08.24. diff --git a/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift index d5eb73d..0121b1d 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift @@ -1,5 +1,5 @@ // -// LoopPlayerViewProtocol.swift +// ExtPlayerViewProtocol.swift // // // Created by Igor Shelopaev on 06.08.24. diff --git a/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift index 914ae64..57dafc4 100644 --- a/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift @@ -1,5 +1,5 @@ // -// LoopPlayerMultiPlatform.swift +// ExtPlayerMultiPlatform.swift // // // Created by Igor Shelopaev on 05.08.24. diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 7e706e0..3c5387d 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -1,5 +1,5 @@ // -// LoopingPlayerProtocol.swift +// ExtPlayerUIView.swift // // // Created by Igor Shelopaev on 05.08.24. diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index 74639be..b109b0e 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -1,5 +1,5 @@ // -// LoopingPlayerNSView.swift +// ExtPlayerNSView.swift // // // Created by Igor Shelopaev on 05.08.24. From 927db4763c71b46ddd2229eb45e66e5b32662fc5 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 18:51:49 +0100 Subject: [PATCH 041/209] Update AbstractPlayer.swift --- .../protocol/player/AbstractPlayer.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index d518049..89a1f79 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -476,6 +476,7 @@ internal func cleanUp( statusObserver: inout NSKeyValueObservation?, timeObserver: inout Any? ) { + player?.pause() errorObserver?.invalidate() errorObserver = nil @@ -492,16 +493,11 @@ internal func cleanUp( statusObserver?.invalidate() statusObserver = nil - player?.pause() - playerLooper?.disableLooping() playerLooper = nil - guard let items = player?.items() else { return } - for item in items { - player?.remove(item) - } - + player?.removeAllItems() + if let observerToken = timeObserver { player?.removeTimeObserver(observerToken) timeObserver = nil From 7100a9d01b40f9728d922db8f5e8f4eb1623bb83 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Jan 2025 19:10:49 +0100 Subject: [PATCH 042/209] Update AbstractPlayer.swift --- .../protocol/player/AbstractPlayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 89a1f79..33aecf0 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -161,7 +161,7 @@ extension AbstractPlayer{ /// - newItem: The `AVPlayerItem` whose status is to be observed. /// - callback: A closure that is called when the item's status changes to `.readyToPlay` or `.failed`. func setupStateItemStatusObserver(newItem: AVPlayerItem, callback: ((AVPlayerItem.Status) -> Void)?) { - statusObserver?.invalidate() + clearStatusObserver() if let callback = callback { //.unknown: This state is essentially the default, indicating that the player item is new or has not yet attempted to load its assets. From dc50cf2afc30fe3595a6f8779141cee1c5fba99d Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 10:52:56 +0100 Subject: [PATCH 043/209] refactoring --- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 15 ------ .../protocol/player/AbstractPlayer.swift | 14 +++--- .../protocol/player/ExtPlayerProtocol.swift | 46 +++++++++++-------- .../protocol/view/ExtPlayerViewProtocol.swift | 29 +----------- .../view/helpers/PlayerCoordinator.swift | 6 --- .../view/main/ExtPlayerMultiPlatform.swift | 37 +++------------ .../view/player/ios/ExtPlayerUIView.swift | 10 ++-- 7 files changed, 46 insertions(+), 111 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index 4322363..737b61c 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -75,21 +75,6 @@ fileprivate func extractExtension(from name: String) -> String? { return nil } -/// Detects and returns the appropriate error based on settings and asset. -/// - Parameters: -/// - settings: The settings for the video player. -/// - asset: The asset for the video player. -/// - Returns: The detected error or nil if no error. -func detectError(settings: VideoSettings, asset: AVURLAsset?) -> VPErrors? { - if !settings.areUnique { - return .settingsNotUnique - } else if asset == nil { - return .sourceNotFound(settings.name) - } else { - return nil - } -} - /// Combines an array of CIFilters with additional brightness and contrast adjustments. /// /// This function appends brightness and contrast adjustments as CIFilters to the existing array of filters. diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 33aecf0..6927c4a 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -110,7 +110,7 @@ public protocol AbstractPlayer: AnyObject { func applyVideoComposition() /// Updates the current playback asset, settings, and initializes playback or a specific action when the asset is ready. - func update(asset: AVURLAsset, settings: VideoSettings, callback: ((AVPlayerItem.Status) -> Void)?) + func update(settings: VideoSettings, callback: ((AVPlayerItem.Status) -> Void)?) } extension AbstractPlayer{ @@ -189,7 +189,7 @@ extension AbstractPlayer{ func seek(to time: Double) { guard let player = player, let duration = player.currentItem?.duration else { if let settings = currentSettings, let asset = assetFor(settings){ - update(asset: asset, settings: settings, callback: nil) + update(settings: settings, callback: nil) seek(to: time) return } @@ -477,7 +477,7 @@ internal func cleanUp( timeObserver: inout Any? ) { player?.pause() - + errorObserver?.invalidate() errorObserver = nil @@ -493,13 +493,15 @@ internal func cleanUp( statusObserver?.invalidate() statusObserver = nil - playerLooper?.disableLooping() - playerLooper = nil + if let looper = playerLooper { + looper.disableLooping() + playerLooper = nil // Optionally, set the looper to nil to deallocate it + } player?.removeAllItems() if let observerToken = timeObserver { - player?.removeTimeObserver(observerToken) + player?.removeTimeObserver(observerToken) timeObserver = nil } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 541a13f..4209f24 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -73,18 +73,16 @@ internal extension ExtPlayerProtocol { /// Initializes a new player view with a video asset and custom settings. /// /// - Parameters: - /// - asset: The `AVURLAsset` used for video playback. /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. /// - timePublishing: Optional `CMTime` that specifies a particular time for publishing or triggering an event. func setupPlayerComponents( - asset: AVURLAsset, settings: VideoSettings ) { guard let player else { return } configurePlayer(player, settings: settings) - update(asset: asset, settings: settings) + update(settings: settings) setupObservers(for: player) } @@ -131,11 +129,9 @@ internal extension ExtPlayerProtocol { /// - 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 + timeObserver = player.addPeriodicTimeObserver(forInterval: timePublishing, queue: .global()) { [weak self] time in guard let self = self else{ return } - DispatchQueue.main.async { - self.delegate?.didPassedTime(seconds: time.seconds) - } + self.delegate?.didPassedTime(seconds: time.seconds) } } } @@ -167,25 +163,21 @@ internal extension ExtPlayerProtocol { /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. /// - callback: An optional closure to be called when the asset is ready to play. func update( - asset: AVURLAsset, settings: VideoSettings, callback: ((AVPlayerItem.Status) -> Void)? = nil ) { - guard let player = player else { return } - + guard let player else { return } + guard let asset = settings.getAssetIfDifferent(currentSettings) else { + delegate?.didReceiveError(.sourceNotFound(settings.name)) + return } + + stop(player) + currentSettings = settings - player.pause() - - if !player.items().isEmpty { // Cleaning - unloop() - clearPlayerQueue() - removeAllFilters() - } - let newItem = createPlayerItem(with: asset, settings: settings) - player.insert(newItem, after: nil) + insert(player, newItem) if settings.loop { loop() @@ -199,6 +191,22 @@ internal extension ExtPlayerProtocol { } } + func insert(_ player: AVQueuePlayer,_ item : AVPlayerItem){ + player.insert(item, after: nil) + } + + /// Stop and clean player + func stop(_ player: AVQueuePlayer){ + + player.pause() + + if !player.items().isEmpty { // Cleaning + unloop() + clearPlayerQueue() + removeAllFilters() + } + } + /// Creates an `AVPlayerItem` with optional subtitle merging. /// - Parameters: /// - asset: The main video asset. diff --git a/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift index 0121b1d..5c648ca 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift @@ -59,34 +59,7 @@ public protocol ExtPlayerViewProtocol { @available(iOS 14, macOS 11, tvOS 14, *) public extension ExtPlayerViewProtocol{ - - /// Updates the view by removing existing error messages and displaying a new one if an error is present. - /// - Parameters: - /// - view: The view that needs to be updated with potential error messages. - /// - error: The optional error that might need to be displayed. - @MainActor - func updateView(_ view: View, error: VPErrors?) { - - makeErrorView(view, error: error) - } - - /// Constructs an error view and adds it to the specified view if an error is present. - /// - Parameters: - /// - view: The view to which the error message view will be added. - /// - error: The optional error which, if present, triggers the creation and addition of an error-specific view. - @MainActor - func makeErrorView(_ view: View, error: VPErrors?) { - - /// Check if error widget is off in settings - guard settings.errorWidgetOff == false else{ return } - - if let error = error { - let errorView = errorTpl(error, settings.errorColor, settings.errorFontSize) - view.addSubview(errorView) - activateFullScreenConstraints(for: errorView, in: view) - } - } - + /// Creates a player view for looping video content. /// - Parameters: /// - context: The UIViewRepresentable context providing environment data and coordinator. diff --git a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift index e96fd45..9374de7 100644 --- a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift +++ b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift @@ -17,16 +17,11 @@ internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { /// Stores the last command applied to the player. private var lastCommand: PlaybackCommand? - - /// A binding to an optional `VPErrors` instance, used to report errors back to the parent view. - @Binding private var error: VPErrors? init( - _ error: Binding, timePublisher: PassthroughSubject, eventPublisher: PassthroughSubject ) { - self._error = error self.timePublisher = timePublisher self.eventPublisher = eventPublisher } @@ -42,7 +37,6 @@ internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { /// This method is called when an error is encountered during playback or other operations. /// - Parameter error: The error received. func didReceiveError(_ error: VPErrors) { - self.error = error eventPublisher.send(.error(error)) } diff --git a/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift index 57dafc4..6acaf99 100644 --- a/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift @@ -48,14 +48,7 @@ internal struct ExtPlayerMultiPlatform: ExtPlayerViewProtocol { /// Settings for the player view @Binding public var settings: VideoSettings - - /// State to store any error that occurs - @State private var error: VPErrors? - var asset : AVURLAsset?{ - assetFor(settings) - } - /// Initializes a new instance of `ExtPlayerView`. /// - Parameters: /// - settings: A binding to the video settings used by the player. @@ -73,18 +66,11 @@ internal struct ExtPlayerMultiPlatform: ExtPlayerViewProtocol { self._settings = settings self._command = command let settings = settings.wrappedValue - let asset = assetFor(settings) - let e = detectError(settings: settings, asset: asset) - if let e { - self._error = State(initialValue: e) - } } - - /// Creates a coordinator that handles error-related updates and interactions between the SwiftUI view and its underlying model. /// - Returns: An instance of PlayerErrorCoordinator that can be used to manage error states and communicate between the view and model. func makeCoordinator() -> PlayerCoordinator { - PlayerCoordinator($error, timePublisher: timePublisher, eventPublisher: eventPublisher) + PlayerCoordinator(timePublisher: timePublisher, eventPublisher: eventPublisher) } } @@ -96,14 +82,11 @@ extension ExtPlayerMultiPlatform: UIViewRepresentable{ @MainActor func makeUIView(context: Context) -> UIView { let container = UIView() - if let player: PlayerView = makePlayerView( - container){ + if let player: PlayerView = makePlayerView(container){ player.delegate = context.coordinator } - - makeErrorView(container, error: error) - return container + return container } /// Updates the container view, removing any existing error views and adding a new one if needed @@ -114,23 +97,15 @@ extension ExtPlayerMultiPlatform: UIViewRepresentable{ let player = uiView.findFirstSubview(ofType: PlayerView.self) if let player{ - if let asset = settings.getAssetIfDifferent(player.currentSettings) { - player.update(asset: asset, settings: settings) - } + + player.update(settings: settings) // Check if command changed before applying it if context.coordinator.getLastCommand != command { player.setCommand(command) context.coordinator.setLastCommand(command) // Update the last command in the coordinator } - } - - if let e = error { - eventPublisher - .send(.error(e)) - } - - updateView(uiView, error: error) + } } } #endif diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 3c5387d..967e76f 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -15,7 +15,7 @@ import AVKit import UIKit @MainActor @preconcurrency -internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { +internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { /// This property holds an instance of `VideoSettings` internal var currentSettings : VideoSettings? @@ -81,11 +81,9 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { super.init(frame: .zero) - if let asset = assetFor(settings){ - setupPlayerComponents( - asset: asset, settings: settings - ) - } + setupPlayerComponents( + settings: settings + ) } required init?(coder: NSCoder) { From 4d48c72b6c9275451b79f223a8581fc9bf7ebdef Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 11:05:56 +0100 Subject: [PATCH 044/209] refactoring --- README.md | 17 +--------- .../ExtVideoPlayer.swift | 8 ----- .../enum/Setting.swift | 12 ------- .../protocol/player/AbstractPlayer.swift | 2 +- .../settings/errors/EColor.swift | 28 ----------------- .../settings/errors/EFontSize.swift | 29 ----------------- .../settings/errors/ErrorGroup.swift | 31 ------------------- .../settings/errors/ErrorWidgetOff.swift | 25 --------------- .../utils/VideoSettings.swift | 29 +++-------------- .../view/main/ExtPlayerMultiPlatform.swift | 1 - .../testPlayerInitialization.swift | 24 -------------- 11 files changed, 6 insertions(+), 200 deletions(-) delete mode 100644 Sources/swiftui-loop-videoplayer/settings/errors/EColor.swift delete mode 100644 Sources/swiftui-loop-videoplayer/settings/errors/EFontSize.swift delete mode 100644 Sources/swiftui-loop-videoplayer/settings/errors/ErrorGroup.swift delete mode 100644 Sources/swiftui-loop-videoplayer/settings/errors/ErrorWidgetOff.swift diff --git a/README.md b/README.md index a569dfd..ae114f2 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,6 @@ It is a pure package without any third-party libraries. My main focus was on per | | AutoPlay | Toggle automatic playback on load. | | | Mute by Default | Initialize playback without sound. | | | Subtitle Integration | Configure subtitles from embedded tracks or external files. | -| | Error Widget Customization | Change error text color, font size, or disable built-in error widgets. | | **Visual Features** | Rounded Corners | Apply rounded corners using SwiftUI's `.mask` modifier. | | | Overlay Graphics | Add vector graphics over video for custom effects. | | | Brightness Adjustment | Control brightness levels dynamically. | @@ -129,9 +128,6 @@ 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 | -| **EColor** | Error message text color. | .red | -| **EFontSize** | Size of the error text. | 17.0 | -| **ErrorWidgetOff** | Do not show inner error showcase component. In case you'd like to implement your own error Alert widget. | - | *Additional Notes on Settings* @@ -272,10 +268,6 @@ or in a declarative way Ext("mp8") // Set default extension here If not provided then mp4 is default Gravity(.resizeAspectFill) TimePublishing() - ErrorGroup{ - EColor(.accentColor) - EFontSize(27) - } } } .onPlayerTimeChange { newTime in @@ -291,14 +283,11 @@ or in a declarative way ExtVideoPlayer{ VideoSettings{ SourceName('https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8') - ErrorGroup{ - EFontSize(27) - } } } ``` -You can group error settings in group **ErrorGroup** or just pass all settings as a linear list of settings. You don't need to follow some specific order for settings, just pass in an arbitrary order you are interested in. The only required setting is now **SourceName**. +The only required setting is now **SourceName**. ### Supported Video Types and Formats @@ -325,10 +314,6 @@ ExtVideoPlayer{ VideoSettings{ SourceName('https://example.com/video') Gravity(.resizeAspectFill) // Video content fit - ErrorGroup{ - EColor(.red) // Error text color - EFontSize(18) // Error text font size - } } } ``` diff --git a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift index ec117cd..a455f3f 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -41,16 +41,12 @@ public struct ExtVideoPlayer: View{ /// - ext: The file extension, with a default value of "mp4". /// - gravity: The video gravity setting, with a default value of `.resizeAspect`. /// - timePublishing: An optional `CMTime` value for time publishing, with a default value of 1 second. - /// - eColor: The color to be used, with a default value of `.accentColor`. - /// - eFontSize: The font size to be used, with a default value of 17.0. /// - command: A binding to the playback command, with a default value of `.play`. public init( fileName: String, ext: String = "mp4", gravity: AVLayerVideoGravity = .resizeAspect, timePublishing : CMTime? = CMTime(seconds: 1, preferredTimescale: 600), - eColor: Color = .accentColor, - eFontSize: CGFloat = 17.0, command : Binding = .constant(.play) ) { self._command = command @@ -66,10 +62,6 @@ public struct ExtVideoPlayer: View{ if let timePublishing{ timePublishing } - ErrorGroup { - EColor(eColor) - EFontSize(eFontSize) - } } _settings = .constant(settings) diff --git a/Sources/swiftui-loop-videoplayer/enum/Setting.swift b/Sources/swiftui-loop-videoplayer/enum/Setting.swift index f363545..21dfa84 100644 --- a/Sources/swiftui-loop-videoplayer/enum/Setting.swift +++ b/Sources/swiftui-loop-videoplayer/enum/Setting.swift @@ -49,18 +49,6 @@ public enum Setting: Equatable, SettingsConvertible{ /// Video gravity case gravity(AVLayerVideoGravity = .resizeAspect) - /// Error text is resource is not found - case errorText(String) - - /// Size of the error text - case errorFontSize(CGFloat) - - /// Color of the error text - case errorColor(Color) - - /// Do not show inner error showcase component - case errorWidgetOff - /// Case name var caseName: String { Mirror(reflecting: self).children.first?.label ?? "\(self)" diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 6927c4a..4677823 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -188,7 +188,7 @@ extension AbstractPlayer{ /// - Parameter time: The target time to seek to in the video timeline. func seek(to time: Double) { guard let player = player, let duration = player.currentItem?.duration else { - if let settings = currentSettings, let asset = assetFor(settings){ + if let settings = currentSettings{ update(settings: settings, callback: nil) seek(to: time) return diff --git a/Sources/swiftui-loop-videoplayer/settings/errors/EColor.swift b/Sources/swiftui-loop-videoplayer/settings/errors/EColor.swift deleted file mode 100644 index 1f1fdf3..0000000 --- a/Sources/swiftui-loop-videoplayer/settings/errors/EColor.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// EText.swift -// -// -// Created by Igor Shelopaev on 07.07.2023. -// - -import SwiftUI - -/// Defines a structure for error text colors that can be converted to settings. -@available(iOS 14.0, macOS 11.0, tvOS 14.0, *) -public struct EColor: SettingsConvertible{ - - /// The color value used for errors. - private let value: Color - - // MARK: - Life cycle - - /// Initializes a new instance of `EColor` with a specified color for errors. - /// - Parameter value: The error color. - public init(_ value: Color) { self.value = value } - - /// Fetch settings - @_spi(Private) - public func asSettings() -> [Setting] { - [.errorColor(value)] - } -} diff --git a/Sources/swiftui-loop-videoplayer/settings/errors/EFontSize.swift b/Sources/swiftui-loop-videoplayer/settings/errors/EFontSize.swift deleted file mode 100644 index 61885d9..0000000 --- a/Sources/swiftui-loop-videoplayer/settings/errors/EFontSize.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// EFontSize.swift -// -// -// Created by Igor Shelopaev on 07.07.2023. -// - -import Foundation -import CoreGraphics - -/// Represents a structure for defining error font sizes that can be converted into settings. -@available(iOS 14.0, macOS 11.0, tvOS 14.0, *) -public struct EFontSize: SettingsConvertible{ - - /// The font size value. - private let value : CGFloat - - // MARK: - Life circle - - /// Initializes a new instance of `EFontSize` with a specific font size. - /// - Parameter value: The font size as a `CGFloat`. - public init(_ value: CGFloat) { self.value = value } - - /// Fetch settings - @_spi(Private) - public func asSettings() -> [Setting] { - [.errorFontSize(value)] - } -} diff --git a/Sources/swiftui-loop-videoplayer/settings/errors/ErrorGroup.swift b/Sources/swiftui-loop-videoplayer/settings/errors/ErrorGroup.swift deleted file mode 100644 index cb9db90..0000000 --- a/Sources/swiftui-loop-videoplayer/settings/errors/ErrorGroup.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ErrorGroup.swift -// -// -// Created by Igor Shelopaev on 07.07.2023. -// - -import Foundation - - -/// Represents a grouping structure for error-related settings, conforming to the `SettingsConvertible` protocol. -@available(iOS 14.0, macOS 11.0, tvOS 14.0, *) -public struct ErrorGroup: SettingsConvertible{ - - /// Errors settings - private let settings : [Setting] - - // MARK: - Life circle - - /// Initializes a new instance of `ErrorGroup` with a settings builder. - /// - Parameter builder: A closure that constructs and returns an array of `Setting` instances. - public init(@SettingsBuilder builder: () -> [Setting] ) { - settings = builder() - } - - /// Fetch settings - @_spi(Private) - public func asSettings() -> [Setting] { - settings - } -} diff --git a/Sources/swiftui-loop-videoplayer/settings/errors/ErrorWidgetOff.swift b/Sources/swiftui-loop-videoplayer/settings/errors/ErrorWidgetOff.swift deleted file mode 100644 index d548b26..0000000 --- a/Sources/swiftui-loop-videoplayer/settings/errors/ErrorWidgetOff.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ErrorWidgetOff.swift -// -// -// Created by Igor Shelopaev on 04.09.24. -// - -import Foundation - - -/// Represents a structure to hide the error widget, conforming to the `SettingsConvertible` protocol. -@available(iOS 14.0, macOS 11.0, tvOS 14.0, *) -public struct ErrorWidgetOff: SettingsConvertible{ - - // MARK: - Life circle - - /// Initializes a new instance - public init() {} - - /// Fetch settings - @_spi(Private) - public func asSettings() -> [Setting] { - [.errorWidgetOff] - } -} diff --git a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift index 0cbe091..11ba484 100644 --- a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift +++ b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift @@ -41,16 +41,7 @@ public struct VideoSettings: Equatable{ /// A structure that defines how a layer displays a player’s visual content within the layer’s bounds public let gravity: AVLayerVideoGravity - - /// Error message text color - public let errorColor : Color - - /// Size of the error text Default : 17.0 - public let errorFontSize : CGFloat - - /// Do not show inner error showcase component - public let errorWidgetOff: Bool - + /// Are the params unique public var areUnique : Bool { unique @@ -72,10 +63,7 @@ public struct VideoSettings: Equatable{ /// - 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. - /// - errorColor: The color used for error messages. - /// - errorFontSize: The font size for error messages. - /// - errorWidgetOff: A Boolean indicating whether the error widget should be turned off. - public init(name: String, ext: String, subtitles: String, loop: Bool, mute: Bool, notAutoPlay: Bool, timePublishing: CMTime?, gravity: AVLayerVideoGravity, errorColor: Color, errorFontSize: CGFloat, errorWidgetOff: Bool, enableVector : Bool = false) { + public init(name: String, ext: String, subtitles: String, loop: Bool, mute: Bool, notAutoPlay: Bool, timePublishing: CMTime?, gravity: AVLayerVideoGravity, enableVector : Bool = false) { self.name = name self.ext = ext self.subtitles = subtitles @@ -84,9 +72,6 @@ public struct VideoSettings: Equatable{ self.notAutoPlay = notAutoPlay self.timePublishing = timePublishing self.gravity = gravity - self.errorColor = errorColor - self.errorFontSize = errorFontSize - self.errorWidgetOff = errorWidgetOff self.vector = enableVector self.unique = true } @@ -105,11 +90,7 @@ public struct VideoSettings: Equatable{ subtitles = settings.fetch(by : "subtitles", defaulted: "") gravity = settings.fetch(by : "gravity", defaulted: .resizeAspect) - - errorColor = settings.fetch(by : "errorColor", defaulted: .red) - - errorFontSize = settings.fetch(by : "errorFontSize", defaulted: 17) - + timePublishing = settings.fetch(by : "timePublishing", defaulted: nil) loop = settings.contains(.loop) @@ -118,8 +99,6 @@ public struct VideoSettings: Equatable{ notAutoPlay = settings.contains(.notAutoPlay) - errorWidgetOff = settings.contains(.errorWidgetOff) - vector = settings.contains(.vector) } } @@ -129,7 +108,7 @@ public extension VideoSettings { /// Returns a new instance of VideoSettings with loop set to false and notAutoPlay set to true, keeping other settings unchanged. var GetSettingsWithNotAutoPlay : VideoSettings { - VideoSettings(name: self.name, ext: self.ext, subtitles: self.subtitles, loop: self.loop, mute: self.mute, notAutoPlay: true, timePublishing: self.timePublishing, gravity: self.gravity, errorColor: self.errorColor, errorFontSize: self.errorFontSize, errorWidgetOff: self.errorWidgetOff, enableVector: self.vector) + VideoSettings(name: self.name, ext: self.ext, subtitles: self.subtitles, loop: self.loop, mute: self.mute, notAutoPlay: true, timePublishing: self.timePublishing, gravity: self.gravity, enableVector: self.vector) } /// Checks if the asset has changed based on the provided settings and current asset. diff --git a/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift index 6acaf99..1cc2452 100644 --- a/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift @@ -65,7 +65,6 @@ internal struct ExtPlayerMultiPlatform: ExtPlayerViewProtocol { self.eventPublisher = eventPublisher self._settings = settings self._command = command - let settings = settings.wrappedValue } /// Creates a coordinator that handles error-related updates and interactions between the SwiftUI view and its underlying model. /// - Returns: An instance of PlayerErrorCoordinator that can be used to manage error states and communicate between the view and model. diff --git a/Tests/swiftui-loop-videoplayerTests/testPlayerInitialization.swift b/Tests/swiftui-loop-videoplayerTests/testPlayerInitialization.swift index 57d24b2..ae19f9c 100644 --- a/Tests/swiftui-loop-videoplayerTests/testPlayerInitialization.swift +++ b/Tests/swiftui-loop-videoplayerTests/testPlayerInitialization.swift @@ -21,8 +21,6 @@ final class testPlayerInitialization: XCTestCase { ext: "mov", gravity: .resizeAspectFill, timePublishing: CMTime(seconds: 1.5, preferredTimescale: 600), - eColor: .blue, - eFontSize: 20.0, command: commandBinding ) @@ -31,8 +29,6 @@ final class testPlayerInitialization: XCTestCase { XCTAssertEqual(playerView.settings.gravity, .resizeAspectFill) XCTAssertEqual(playerView.settings.timePublishing?.seconds, 1.5) XCTAssertEqual(playerView.settings.timePublishing?.timescale, 600) - XCTAssertEqual(playerView.settings.errorColor, .blue) - XCTAssertEqual(playerView.settings.errorFontSize, 20.0) XCTAssertEqual(playerView.command, playbackCommand) } @@ -45,8 +41,6 @@ final class testPlayerInitialization: XCTestCase { XCTAssertEqual(playerView.settings.gravity, .resizeAspect) // Default gravity XCTAssertNotNil(playerView.settings.timePublishing) // Default should not be nil XCTAssertEqual(playerView.settings.timePublishing?.seconds, 1) - XCTAssertEqual(playerView.settings.errorColor, .accentColor) // Default color - XCTAssertEqual(playerView.settings.errorFontSize, 17.0) // Default font size XCTAssertEqual(playerView.command, .play) // Default command } @@ -58,18 +52,12 @@ final class testPlayerInitialization: XCTestCase { Ext("mp8") // Set default extension here If not provided then mp4 is default Gravity(.resizeAspectFill) TimePublishing() - ErrorGroup{ - EColor(.accentColor) - EFontSize(27) - } } } XCTAssertEqual(playerView.settings.name, "swipe") XCTAssertEqual(playerView.settings.ext, "mp8") XCTAssertEqual(playerView.settings.gravity, .resizeAspectFill) XCTAssertNotEqual(playerView.settings.timePublishing, nil) - XCTAssertEqual(playerView.settings.errorColor, .accentColor) - XCTAssertEqual(playerView.settings.errorFontSize, 27) XCTAssertEqual(playerView.command, .play) } @@ -81,18 +69,12 @@ final class testPlayerInitialization: XCTestCase { Ext("mp8") Gravity(.resizeAspectFill) TimePublishing(CMTime(seconds: 2, preferredTimescale: 600)) - ErrorGroup { - EColor(.red) - EFontSize(15.0) - } } } XCTAssertEqual(playerView.settings.name, "swipe") XCTAssertEqual(playerView.settings.ext, "mp8") XCTAssertEqual(playerView.settings.gravity, .resizeAspectFill) XCTAssertEqual(playerView.settings.timePublishing?.seconds, 2) - XCTAssertEqual(playerView.settings.errorColor, .red) - XCTAssertEqual(playerView.settings.errorFontSize, 15.0) XCTAssertEqual(playerView.command, .play) } @@ -103,10 +85,6 @@ final class testPlayerInitialization: XCTestCase { Ext("mkv") Gravity(.resizeAspect) TimePublishing(CMTime(seconds: 1, preferredTimescale: 600)) - ErrorGroup { - EColor(.green) - EFontSize(12.0) - } } let settings = Binding.constant(initialSettings) let playerView = ExtVideoPlayer(settings: settings, command: .constant(.pause)) @@ -115,8 +93,6 @@ final class testPlayerInitialization: XCTestCase { XCTAssertEqual(settings.wrappedValue.ext, "mkv") XCTAssertEqual(settings.wrappedValue.gravity, .resizeAspect) XCTAssertEqual(settings.wrappedValue.timePublishing?.seconds, 1) - XCTAssertEqual(settings.wrappedValue.errorColor, .green) - XCTAssertEqual(settings.wrappedValue.errorFontSize, 12.0) XCTAssertEqual(playerView.command, .pause) } From c23b79edbe96cd48491325bcef23ea45793853c8 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 11:11:46 +0100 Subject: [PATCH 045/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae114f2..b5c88dd 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Please note that using videos from URLs requires ensuring that you have the righ |-------------------------------------------------------------|-------------------------------|------------------------------------------------------------------------------------------------------| | `settings` | `Binding` | A binding to the video player settings, which configure various aspects of the player's behavior. | | `command` | `Binding` | A binding to control playback actions, such as play, pause, or seek. | -| `init(fileName:ext:gravity:timePublishing:`
`eColor:eFontSize:command:)` | Constructor | Initializes the player with specific video parameters, such as file name, extension, gravity, time publishing, color, font size, and a playback command binding. | +| `init(fileName:ext:gravity:timePublishing:`
`command:)` | Constructor | Initializes the player with specific video parameters, such as file name, extension, gravity, time publishing and a playback command binding. | | `init(settings: () -> VideoSettings, command:)` | Constructor | Initializes the player in a declarative way with a settings block and a playback command binding. | | `init(settings: Binding, command:)` | Constructor | Initializes the player with bindings to the video settings and a playback command. | From 4973ee0bd3cce22810aec0f0239ba32c93a5f88b Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 11:33:41 +0100 Subject: [PATCH 046/209] update --- .../protocol/player/AbstractPlayer.swift | 5 +++-- .../protocol/player/ExtPlayerProtocol.swift | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 4677823..ff3f1d5 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -110,7 +110,7 @@ public protocol AbstractPlayer: AnyObject { func applyVideoComposition() /// Updates the current playback asset, settings, and initializes playback or a specific action when the asset is ready. - func update(settings: VideoSettings, callback: ((AVPlayerItem.Status) -> Void)?) + func update(settings: VideoSettings, asset : AVURLAsset?, callback: ((AVPlayerItem.Status) -> Void)?) } extension AbstractPlayer{ @@ -189,7 +189,8 @@ extension AbstractPlayer{ func seek(to time: Double) { guard let player = player, let duration = player.currentItem?.duration else { if let settings = currentSettings{ - update(settings: settings, callback: nil) + let asset = assetFor(settings) + update(settings: settings, asset: asset, callback: nil) seek(to: time) return } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 4209f24..9fc0849 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -159,18 +159,20 @@ internal extension ExtPlayerProtocol { /// If provided, a callback is executed when the asset is ready to play. /// /// - Parameters: - /// - asset: The AVURLAsset to be loaded into the player. /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. /// - callback: An optional closure to be called when the asset is ready to play. func update( settings: VideoSettings, + asset : AVURLAsset? = nil, callback: ((AVPlayerItem.Status) -> Void)? = nil ) { - guard let player else { return } - guard let asset = settings.getAssetIfDifferent(currentSettings) else { + guard let player else { return } + + guard let asset = asset ?? settings.getAssetIfDifferent(currentSettings) else { delegate?.didReceiveError(.sourceNotFound(settings.name)) return } + stop(player) currentSettings = settings From 4f52e864be7f2498639de1181c5835571a749546 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 11:39:36 +0100 Subject: [PATCH 047/209] Update ExtPlayerProtocol.swift --- .../protocol/player/ExtPlayerProtocol.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 9fc0849..4cc37a6 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -153,14 +153,21 @@ internal extension ExtPlayerProtocol { #endif } - /// Updates the current playback asset, settings, and initializes playback or a specific action when the asset is ready. + /// Updates the player with a new asset and applies specified video settings. + /// Initializes playback or performs a specified action once the asset is ready. /// - /// This method sets a new asset to be played, optionally loops it, and can automatically start playback. - /// If provided, a callback is executed when the asset is ready to play. + /// This method sets a new `AVURLAsset` to be played based on the provided settings. + /// It can configure looping and muting options, and automatically starts playback if specified. + /// A callback is executed when the asset transitions to the `.readyToPlay` status, allowing for + /// further actions dependent on the readiness of the asset. /// /// - Parameters: - /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. - /// - callback: An optional closure to be called when the asset is ready to play. + /// - settings: A `VideoSettings` struct containing configurations such as playback gravity, + /// whether to loop the content, and whether to mute the audio. + /// - asset: An optional `AVURLAsset` representing the new video content to be loaded. If nil, + /// the current asset continues playing with updated settings. + /// - callback: An optional closure executed when the asset reaches `.readyToPlay` status, + /// providing the new status as its parameter for handling additional setup or errors. func update( settings: VideoSettings, asset : AVURLAsset? = nil, @@ -172,7 +179,6 @@ internal extension ExtPlayerProtocol { delegate?.didReceiveError(.sourceNotFound(settings.name)) return } - stop(player) currentSettings = settings From b4b001e1df23195ee5629389e1cb800c39eb7596 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 13:33:29 +0100 Subject: [PATCH 048/209] update --- .../protocol/player/AbstractPlayer.swift | 57 +++++++++++++---- .../protocol/player/ExtPlayerProtocol.swift | 64 ++++++++++++------- .../utils/VideoSettings.swift | 12 ++-- .../view/player/ios/ExtPlayerUIView.swift | 4 +- .../view/player/mac/ExtPlayerNSView.swift | 12 ++-- 5 files changed, 101 insertions(+), 48 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index ff3f1d5..74686da 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -151,6 +151,40 @@ extension AbstractPlayer{ player?.pause() } + /// Clears all items from the player's queue. + func clearPlayerQueue() { + player?.removeAllItems() + } + + /// Determines whether the media queue of the player is empty. + func isEmptyQueue() -> Bool{ + if let player{ + return player.items().isEmpty + }else{ + return true + } + } + + /// Stop and clean player + func stop(){ + + pause() + + if !isEmptyQueue() { // Cleaning + if isLooping(){ + unloop() + } + + removeAllFilters() + clearPlayerQueue() + } + } + /// Inserts a new player item into the media queue of the player. + /// - Parameter item: The AVPlayerItem to be inserted into the queue. + func insert(_ item : AVPlayerItem){ + player?.insert(item, after: nil) + } + /// Sets up an observer for the status of the provided `AVPlayerItem`. /// /// This method observes changes in the status of `newItem` and triggers the provided callback @@ -241,6 +275,7 @@ extension AbstractPlayer{ } player.seek(to: seekTime){ [weak self] value in + print(Thread.current, "***********************") let currentTime = CMTimeGetSeconds(player.currentTime()) self?.delegate?.didSeek(value: value, currentTime: currentTime) } @@ -318,6 +353,11 @@ extension AbstractPlayer{ #endif } + /// Check if looping is applied + func isLooping() -> Bool{ + playerLooper != nil + } + /// Enables looping for the current video item. /// This method sets up the `playerLooper` to loop the currently playing item indefinitely. func loop() { @@ -326,7 +366,7 @@ extension AbstractPlayer{ } // Check if the video is already being looped - if playerLooper != nil { + if isLooping() { return } @@ -337,7 +377,7 @@ extension AbstractPlayer{ /// This method removes the `playerLooper`, stopping the loop. func unloop() { // Check if the video is not looped (i.e., playerLooper is nil) - guard playerLooper != nil else { + guard isLooping() else { return // Not looped, no need to unloop } @@ -415,7 +455,7 @@ extension AbstractPlayer{ if wasPlaying { player.pause() } - + player.items().forEach{ item in let videoComposition = AVVideoComposition(asset: item.asset, applyingCIFiltersWithHandler: { request in @@ -469,7 +509,6 @@ extension AbstractPlayer{ /// - timeObserver: An inout reference to a generic observer (e.g., a time observer token) that needs to be removed to prevent memory leaks. It is cleared and set to nil. internal func cleanUp( player: inout AVQueuePlayer?, - playerLooper: inout AVPlayerLooper?, errorObserver: inout NSKeyValueObservation?, timeControlObserver: inout NSKeyValueObservation?, currentItemObserver: inout NSKeyValueObservation?, @@ -477,8 +516,7 @@ internal func cleanUp( statusObserver: inout NSKeyValueObservation?, timeObserver: inout Any? ) { - player?.pause() - + errorObserver?.invalidate() errorObserver = nil @@ -493,13 +531,6 @@ internal func cleanUp( statusObserver?.invalidate() statusObserver = nil - - if let looper = playerLooper { - looper.disableLooping() - playerLooper = nil // Optionally, set the looper to nil to deallocate it - } - - player?.removeAllItems() if let observerToken = timeObserver { player?.removeTimeObserver(observerToken) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 4cc37a6..f0b2d9d 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -173,19 +173,17 @@ internal extension ExtPlayerProtocol { asset : AVURLAsset? = nil, callback: ((AVPlayerItem.Status) -> Void)? = nil ) { - guard let player else { return } + guard let player else { return } - guard let asset = asset ?? settings.getAssetIfDifferent(currentSettings) else { - delegate?.didReceiveError(.sourceNotFound(settings.name)) - return } + guard let asset = prepareAsset(settings, asset) else { return } - stop(player) + stop() currentSettings = settings let newItem = createPlayerItem(with: asset, settings: settings) - insert(player, newItem) + insert(newItem) if settings.loop { loop() @@ -199,20 +197,42 @@ internal extension ExtPlayerProtocol { } } - func insert(_ player: AVQueuePlayer,_ item : AVPlayerItem){ - player.insert(item, after: nil) - } - - /// Stop and clean player - func stop(_ player: AVQueuePlayer){ + /// Prepares and validates the media asset for playback based on the given settings. + /// + /// This function determines the appropriate `AVURLAsset` to use for media playback. + /// If a specific asset is provided, it uses that asset; otherwise, it attempts to retrieve + /// an asset based on the provided settings. If the settings have changed from the current settings, + /// it fetches a new asset using a method presumed to be `getAssets()`. If no valid asset is found or + /// provided, it notifies a delegate of the error. + /// + /// - Parameters: + /// - settings: The `VideoSettings` containing configuration and asset retrieval logic. + /// - asset: An optional `AVURLAsset` to be used directly if provided. If nil, an asset is attempted + /// to be retrieved based on the `settings`. + /// + /// - Returns: An optional `AVURLAsset` if a valid asset is found or provided; otherwise, nil if no + /// valid asset could be located or an error occurred. + /// + /// - Note: This function calls `didReceiveError` on the delegate with an error of `.sourceNotFound` + /// if no valid asset is found, providing context for the failure. + func prepareAsset(_ settings: VideoSettings, + _ asset : AVURLAsset? = nil) -> AVURLAsset?{ + let value : AVURLAsset? - player.pause() - - if !player.items().isEmpty { // Cleaning - unloop() - clearPlayerQueue() - removeAllFilters() + if let asset{ + value = asset + }else if !settings.isEqual(currentSettings), let asset = settings.getAssets(){ + value = asset + }else{ + value = nil } + + guard let asset = value else{ + delegate?.didReceiveError(.sourceNotFound(settings.name)) + return nil + } + + return value } /// Creates an `AVPlayerItem` with optional subtitle merging. @@ -231,7 +251,7 @@ internal extension ExtPlayerProtocol { return AVPlayerItem(asset: asset) } } - + /// Sets up observers on the player item and the player to track their status and error states. /// /// - Parameters: @@ -275,11 +295,7 @@ internal extension ExtPlayerProtocol { } } } - - /// Clears all items from the player's queue. - func clearPlayerQueue() { - player?.removeAllItems() - } + /// Removes observers for handling errors. /// diff --git a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift index 11ba484..9b45dd0 100644 --- a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift +++ b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift @@ -111,22 +111,26 @@ public extension VideoSettings { VideoSettings(name: self.name, ext: self.ext, subtitles: self.subtitles, loop: self.loop, mute: self.mute, notAutoPlay: true, timePublishing: self.timePublishing, gravity: self.gravity, enableVector: self.vector) } + 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. /// - Returns: A new `AVURLAsset` if the asset has changed, or `nil` if the asset remains the same. - func getAssetIfDifferent(_ settings : VideoSettings?) -> AVURLAsset?{ + func isEqual(_ settings : VideoSettings?) -> Bool{ let newAsset = assetFor(self) - guard let settings = settings else{ return newAsset } + guard let settings = settings else{ return false } let oldAsset = assetFor(settings) if let newUrl = newAsset?.url, let oldUrl = oldAsset?.url, newUrl != oldUrl{ - return newAsset + return false } - return nil + return true } } diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 967e76f..3b18abe 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -101,9 +101,11 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { /// This method invalidates the status and error observers to prevent memory leaks, /// pauses the player, and clears out player-related references to assist in clean deinitialization. deinit { + + stop() + cleanUp( player: &player, - playerLooper: &playerLooper, errorObserver: &errorObserver, timeControlObserver : &timeControlObserver, currentItemObserver: ¤tItemObserver, diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index b109b0e..bfc4fb5 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -83,11 +83,9 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { super.init(frame: .zero) - if let asset = assetFor(settings){ - setupPlayerComponents( - asset: asset, settings: settings - ) - } + setupPlayerComponents( + asset: asset, settings: settings + ) } required init?(coder: NSCoder) { @@ -105,9 +103,11 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { /// This method invalidates the status and error observers to prevent memory leaks, /// pauses the player, and clears out player-related references to assist in clean deinitialization. deinit { + + stop() + cleanUp( player: &player, - playerLooper: &playerLooper, errorObserver: &errorObserver, timeControlObserver : &timeControlObserver, currentItemObserver: ¤tItemObserver, From f6f4893abdfee4c2a70e87559b6eb75a368e6823 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 13:33:57 +0100 Subject: [PATCH 049/209] Update AbstractPlayer.swift --- .../protocol/player/AbstractPlayer.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 74686da..5ecbdff 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -275,7 +275,6 @@ extension AbstractPlayer{ } player.seek(to: seekTime){ [weak self] value in - print(Thread.current, "***********************") let currentTime = CMTimeGetSeconds(player.currentTime()) self?.delegate?.didSeek(value: value, currentTime: currentTime) } From 025e9c09acb23bdaaa8bcf1e9e060ea7b4eb6911 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 14:17:51 +0100 Subject: [PATCH 050/209] update --- .../protocol/player/AbstractPlayer.swift | 51 ------------------- .../protocol/player/ExtPlayerProtocol.swift | 35 +++++++++---- .../view/player/ios/ExtPlayerUIView.swift | 15 +++--- .../view/player/mac/ExtPlayerNSView.swift | 15 +++--- 4 files changed, 38 insertions(+), 78 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 5ecbdff..9e284b0 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -492,54 +492,3 @@ extension AbstractPlayer{ #endif } } - -/// Cleans up resources associated with an AVQueuePlayer and its related components. -/// This function stops the player, invalidates and clears all observers, and removes all items from the player's queue. -/// It also disables any looping mechanisms, sets all involved components to nil, and ensures proper deinitialization to prevent memory leaks. -/// -/// - Parameters: -/// - player: An inout reference to the AVQueuePlayer that is being cleaned up. The player is paused, and its resources are freed. -/// - playerLooper: An inout reference to the AVPlayerLooper associated with the player. It's disabled to stop any ongoing loops and set to nil. -/// - errorObserver: An inout reference to an NSKeyValueObservation that monitors for errors. It is invalidated and set to nil to stop observing. -/// - timeControlObserver: An inout reference to an NSKeyValueObservation that monitors the player's time control status. It is invalidated and set to nil. -/// - currentItemObserver: An inout reference to an NSKeyValueObservation that tracks changes to the player's current item. It is invalidated and set to nil. -/// - volumeObserver: An inout reference to an NSKeyValueObservation that monitors volume changes. It is invalidated and set to nil. -/// - statusObserver: An inout reference to an NSKeyValueObservation that observes the player's status. It is invalidated and set to nil. -/// - timeObserver: An inout reference to a generic observer (e.g., a time observer token) that needs to be removed to prevent memory leaks. It is cleared and set to nil. -internal func cleanUp( - player: inout AVQueuePlayer?, - errorObserver: inout NSKeyValueObservation?, - timeControlObserver: inout NSKeyValueObservation?, - currentItemObserver: inout NSKeyValueObservation?, - volumeObserver: inout NSKeyValueObservation?, - statusObserver: inout NSKeyValueObservation?, - timeObserver: inout Any? -) { - - errorObserver?.invalidate() - errorObserver = nil - - timeControlObserver?.invalidate() - timeControlObserver = nil - - currentItemObserver?.invalidate() - currentItemObserver = nil - - volumeObserver?.invalidate() - volumeObserver = nil - - statusObserver?.invalidate() - statusObserver = nil - - if let observerToken = timeObserver { - player?.removeTimeObserver(observerToken) - timeObserver = nil - } - - player = nil - - #if DEBUG - print("Cleaned up.") - #endif -} - diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index f0b2d9d..583a2e7 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -12,6 +12,7 @@ import UIKit #elseif canImport(AppKit) import AppKit #endif + /// A protocol defining the requirements for a looping video player. /// /// Conforming types are expected to manage a video player that can loop content continuously, @@ -296,17 +297,6 @@ internal extension ExtPlayerProtocol { } } - - /// Removes observers for handling errors. - /// - /// This method ensures that the error observer is properly invalidated and the reference is cleared. - /// It is important to call this method to prevent memory leaks and remove any unwanted side effects - /// from obsolete observers. - func removeObservers() { - errorObserver?.invalidate() - errorObserver = nil - } - /// Responds to errors reported by the AVPlayer. /// /// If an error is present, this method notifies the delegate of the encountered error, @@ -317,6 +307,29 @@ internal extension ExtPlayerProtocol { delegate?.didReceiveError(.remoteVideoError(error)) } + /// Clear observers + func clearObservers(){ + + errorObserver?.invalidate() + errorObserver = nil + + timeControlObserver?.invalidate() + timeControlObserver = nil + + currentItemObserver?.invalidate() + currentItemObserver = nil + + volumeObserver?.invalidate() + volumeObserver = nil + + clearStatusObserver() + + if let observerToken = timeObserver { + player?.removeTimeObserver(observerToken) + timeObserver = nil + } + } + /// 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. diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 3b18abe..5c5a1d4 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -104,14 +104,13 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { stop() - cleanUp( - player: &player, - errorObserver: &errorObserver, - timeControlObserver : &timeControlObserver, - currentItemObserver: ¤tItemObserver, - volumeObserver: &volumeObserver, - statusObserver: &statusObserver, - timeObserver: &timeObserver) + clearObservers() + + player = nil + + #if DEBUG + print("Cleaned up.") + #endif } } #endif diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index bfc4fb5..bb89d1a 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -106,14 +106,13 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { stop() - cleanUp( - player: &player, - errorObserver: &errorObserver, - timeControlObserver : &timeControlObserver, - currentItemObserver: ¤tItemObserver, - volumeObserver: &volumeObserver, - statusObserver: &statusObserver, - timeObserver: &timeObserver) + clearObservers() + + player = nil + + #if DEBUG + print("Cleaned up.") + #endif } } #endif From 8cc437f44030495c44d89bac46d7d617d9819878 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 18:04:23 +0100 Subject: [PATCH 051/209] update --- .../protocol/player/AbstractPlayer.swift | 9 ++--- .../protocol/player/ExtPlayerProtocol.swift | 34 ++++++++++++---- .../view/player/ios/ExtPlayerUIView.swift | 39 +++++++++++++------ .../view/player/mac/ExtPlayerNSView.swift | 32 +++++++++++---- 4 files changed, 82 insertions(+), 32 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 9e284b0..d846895 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -158,11 +158,7 @@ extension AbstractPlayer{ /// Determines whether the media queue of the player is empty. func isEmptyQueue() -> Bool{ - if let player{ - return player.items().isEmpty - }else{ - return true - } + player?.items().isEmpty ?? true } /// Stop and clean player @@ -172,7 +168,7 @@ extension AbstractPlayer{ if !isEmptyQueue() { // Cleaning if isLooping(){ - unloop() + unloop() } removeAllFilters() @@ -491,4 +487,5 @@ extension AbstractPlayer{ } #endif } + } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 583a2e7..8705229 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -31,7 +31,7 @@ public protocol ExtPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ #endif /// Provides a `AVPlayerLayer` specific to the player implementation, applicable across all platforms. - var playerLayer: AVPlayerLayer { get } + var playerLayer: AVPlayerLayer? { get set } /// An optional NSKeyValueObservation to monitor errors encountered by the video player. /// This observer should be configured to detect and handle errors from the AVQueuePlayer, @@ -109,16 +109,20 @@ internal extension ExtPlayerProtocol { /// - player: The `AVQueuePlayer` instance for which the player layer will be configured. /// - settings: A `VideoSettings` object containing configuration details for the player layer. func configurePlayerLayer(_ player: AVQueuePlayer, _ settings: VideoSettings) { - playerLayer.player = player - playerLayer.videoGravity = settings.gravity + playerLayer?.player = player + playerLayer?.videoGravity = settings.gravity #if canImport(UIKit) - playerLayer.backgroundColor = UIColor.clear.cgColor - layer.addSublayer(playerLayer) + playerLayer?.backgroundColor = UIColor.clear.cgColor + if let playerLayer{ + layer.addSublayer(playerLayer) + } #elseif canImport(AppKit) - playerLayer.backgroundColor = NSColor.clear.cgColor + playerLayer?.backgroundColor = NSColor.clear.cgColor let layer = CALayer() - layer.addSublayer(playerLayer) + if let playerLayer{ + layer.addSublayer(playerLayer) + } self.layer = layer self.wantsLayer = true #endif @@ -187,7 +191,7 @@ internal extension ExtPlayerProtocol { insert(newItem) if settings.loop { - loop() + loop() } // Observe status changes @@ -330,6 +334,20 @@ internal extension ExtPlayerProtocol { } } + func addPlayerLayer(){ + playerLayer = AVPlayerLayer() + } + + /// Removes the player layer from its super layer. + /// + /// This method checks if the player layer is associated with a super layer and removes it to clean up resources + /// and prevent potential retain cycles or unwanted video display when the player is no longer needed. + func removePlayerLayer() { + playerLayer?.player = nil + playerLayer?.removeFromSuperlayer() + playerLayer = nil + } + /// 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. diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 5c5a1d4..8a959d0 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -30,10 +30,10 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { internal var contrast: Float = 1 /// A CALayer instance used for composing content, accessible only within the module. - internal var compositeLayer : CALayer? + internal var compositeLayer : CALayer? = nil /// The AVPlayerLayer that displays the video content. - internal let playerLayer : AVPlayerLayer + internal var playerLayer : AVPlayerLayer? = nil /// The looper responsible for continuous video playback. internal var playerLooper: AVPlayerLooper? @@ -74,13 +74,11 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { player = AVQueuePlayer(items: []) - playerLayer = AVPlayerLayer() - if settings.vector{ - compositeLayer = CALayer() - } - super.init(frame: .zero) + addPlayerLayer() + addCompositeLayer(settings) + setupPlayerComponents( settings: settings ) @@ -93,7 +91,18 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { /// Lays out subviews and adjusts the frame of the player layer to match the view's bounds. override func layoutSubviews() { super.layoutSubviews() - playerLayer.frame = bounds + playerLayer?.frame = bounds + } + + private func addCompositeLayer(_ settings : VideoSettings){ + if settings.vector{ + compositeLayer = CALayer() + } + } + + private func removeCompositeLayer() { + compositeLayer?.removeFromSuperlayer() + compositeLayer = nil } /// Cleans up resources and observers associated with the player. @@ -101,15 +110,23 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { /// This method invalidates the status and error observers to prevent memory leaks, /// pauses the player, and clears out player-related references to assist in clean deinitialization. deinit { + // First, clear all observers to prevent memory leaks + clearObservers() + // Stop the player to ensure it's not playing any media stop() - clearObservers() + // Remove visual layers to clean up the UI components + removePlayerLayer() + removeCompositeLayer() + // Finally, release player and delegate references to free up memory player = nil - + delegate = nil + + // Log the cleanup process for debugging purposes #if DEBUG - print("Cleaned up.") + print("Player deinitialized and resources cleaned up.") #endif } } diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index bb89d1a..507f704 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -76,13 +76,11 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { player = AVQueuePlayer(items: []) - playerLayer = AVPlayerLayer() - if settings.vector{ - compositeLayer = CALayer() - } - super.init(frame: .zero) + addPlayerLayer() + addCompositeLayer(settings) + setupPlayerComponents( asset: asset, settings: settings ) @@ -97,21 +95,41 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { super.layout() playerLayer.frame = bounds } + + private func addCompositeLayer(_ settings : VideoSettings){ + if settings.vector{ + compositeLayer = CALayer() + } + } + + private func removeCompositeLayer() { + compositeLayer?.removeFromSuperlayer() + compositeLayer = nil + } /// Cleans up resources and observers associated with the player. /// /// This method invalidates the status and error observers to prevent memory leaks, /// pauses the player, and clears out player-related references to assist in clean deinitialization. deinit { + + // First, clear all observers to prevent memory leaks + clearObservers() + // Stop the player to ensure it's not playing any media stop() - clearObservers() + // Remove visual layers to clean up the UI components + removePlayerLayer() + removeCompositeLayer() + // Finally, release player and delegate references to free up memory player = nil + delegate = nil + // Log the cleanup process for debugging purposes #if DEBUG - print("Cleaned up.") + print("Player deinitialized and resources cleaned up.") #endif } } From 469afc03cb506654643651783cd6bb523afa3c30 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 18:09:14 +0100 Subject: [PATCH 052/209] update --- .../view/main/ExtPlayerMultiPlatform.swift | 19 ++++--------------- .../view/player/mac/ExtPlayerNSView.swift | 8 +++----- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift index 1cc2452..0a0b911 100644 --- a/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift @@ -117,14 +117,10 @@ extension ExtPlayerMultiPlatform: NSViewRepresentable{ @MainActor func makeNSView(context: Context) -> NSView { let container = NSView() - if let player: PlayerView = makePlayerView( - container){ + if let player: PlayerView = makePlayerView(container){ player.delegate = context.coordinator } - - makeErrorView(container, error: error) - return container } @@ -135,9 +131,9 @@ extension ExtPlayerMultiPlatform: NSViewRepresentable{ @MainActor func updateNSView(_ nsView: NSView, context: Context) { let player = nsView.findFirstSubview(ofType: PlayerView.self) if let player { - if let asset = settings.getAssetIfDifferent(player.currentSettings){ - player.update(asset: asset, settings: settings) - } + + player.update(settings: settings) + // Check if command changed before applying it if context.coordinator.getLastCommand != command { player.setCommand(command) @@ -145,13 +141,6 @@ extension ExtPlayerMultiPlatform: NSViewRepresentable{ } } - - if let e = error { - eventPublisher - .send(.error(e)) - } - - updateView(nsView, error: error) } } #endif diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index 507f704..f2bdd5a 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -35,7 +35,7 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { internal var compositeLayer : CALayer? /// The AVPlayerLayer that displays the video content. - internal let playerLayer : AVPlayerLayer + internal var playerLayer : AVPlayerLayer? /// The looper responsible for continuous video playback. internal var playerLooper: AVPlayerLooper? @@ -81,9 +81,7 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { addPlayerLayer() addCompositeLayer(settings) - setupPlayerComponents( - asset: asset, settings: settings - ) + setupPlayerComponents(settings: settings) } required init?(coder: NSCoder) { @@ -93,7 +91,7 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { /// Lays out subviews and adjusts the frame of the player layer to match the view's bounds. override func layout() { super.layout() - playerLayer.frame = bounds + playerLayer?.frame = bounds } private func addCompositeLayer(_ settings : VideoSettings){ From 2f836cad3239b21d910facb5c74e6e70849db7a6 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 18:17:28 +0100 Subject: [PATCH 053/209] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b5c88dd..4cacc8b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ ## 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. 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 real device. There's enormous difference with results on the simulator. + 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) From f7ac314c8f817a0d9f93366d81a83cf4550cf874 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 18:35:33 +0100 Subject: [PATCH 054/209] Update ExtPlayerProtocol.swift --- .../protocol/player/ExtPlayerProtocol.swift | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 8705229..77d079f 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -220,24 +220,22 @@ internal extension ExtPlayerProtocol { /// /// - Note: This function calls `didReceiveError` on the delegate with an error of `.sourceNotFound` /// if no valid asset is found, providing context for the failure. - func prepareAsset(_ settings: VideoSettings, - _ asset : AVURLAsset? = nil) -> AVURLAsset?{ - let value : AVURLAsset? - - if let asset{ - value = asset - }else if !settings.isEqual(currentSettings), let asset = settings.getAssets(){ - value = asset - }else{ - value = nil + func prepareAsset(_ settings: VideoSettings, _ asset: AVURLAsset? = nil) -> AVURLAsset? { + if let asset = asset { + return asset } - guard let asset = value else{ + let newAsset = settings.getAssets() + + if !settings.isEqual(currentSettings), let newAsset{ + return newAsset + } + + if newAsset == nil { delegate?.didReceiveError(.sourceNotFound(settings.name)) - return nil } - return value + return nil } /// Creates an `AVPlayerItem` with optional subtitle merging. From 847d9aac1bbf4e06405463b5e2f0ffb1f205f20e Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 18:36:16 +0100 Subject: [PATCH 055/209] Update AbstractPlayer.swift --- .../protocol/player/AbstractPlayer.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index d846895..c68451e 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -487,5 +487,4 @@ extension AbstractPlayer{ } #endif } - } From a20a0a79733fd14fab84bdb40b8a51fa180a75c7 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 18:37:11 +0100 Subject: [PATCH 056/209] Update ExtPlayerProtocol.swift --- .../protocol/player/ExtPlayerProtocol.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 77d079f..ed21c7d 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -332,6 +332,7 @@ internal extension ExtPlayerProtocol { } } + /// Add player layer func addPlayerLayer(){ playerLayer = AVPlayerLayer() } From 779e67326aa266448731e99c83564e97d2aebfb5 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 18:48:01 +0100 Subject: [PATCH 057/209] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cacc8b..ab58f0c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ ## 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. 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 real device. There's enormous difference with results on the simulator. + +*if you profile the package do it on real device. There's enormous difference with results on the simulator.* 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**. From 2703f51e9df1361838fb98321d0c0ff65dfe0631 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 18:48:59 +0100 Subject: [PATCH 058/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ab58f0c..74630a6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ 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. 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 real device. There's enormous difference with results on the simulator.* +*If you profile the package, do it on a real device. There’s an enormous difference in results compared to the simulator.* 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**. From e36dbb692144710651c0a3c3d9e50918241ebfee Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 18:49:39 +0100 Subject: [PATCH 059/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74630a6..f6e28df 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. +### *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 3c428bb528660653c6c0edcb7342b50eff6437ac Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 16 Jan 2025 19:37:11 +0100 Subject: [PATCH 060/209] update --- .../protocol/player/ExtPlayerProtocol.swift | 2 -- .../view/player/ios/ExtPlayerUIView.swift | 1 - .../view/player/mac/ExtPlayerNSView.swift | 3 +-- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index ed21c7d..e0a21cd 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -178,8 +178,6 @@ internal extension ExtPlayerProtocol { asset : AVURLAsset? = nil, callback: ((AVPlayerItem.Status) -> Void)? = nil ) { - guard let player else { return } - guard let asset = prepareAsset(settings, asset) else { return } stop() diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 8a959d0..0633333 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -14,7 +14,6 @@ import AVKit #if canImport(UIKit) import UIKit -@MainActor @preconcurrency internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { /// This property holds an instance of `VideoSettings` diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index f2bdd5a..f2476a9 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -16,8 +16,7 @@ import AppKit /// A NSView subclass that loops video using AVFoundation on macOS. /// This class handles the initialization and management of a looping video player with customizable video gravity. -@MainActor @preconcurrency -internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { +internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { /// This property holds an instance of `VideoSettings` internal var currentSettings : VideoSettings? From a42766facc2293d0754bda10699d35344e145237 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 17 Jan 2025 11:26:27 +0100 Subject: [PATCH 061/209] update --- .../ExtVideoPlayer.swift | 12 ++- .../helpers/PlayerDelegateProtocol.swift | 1 + .../protocol/player/AbstractPlayer.swift | 10 ++- .../protocol/player/ExtPlayerProtocol.swift | 39 ++++++--- .../protocol/view/ExtPlayerViewProtocol.swift | 7 +- .../view/helpers/PlayerCoordinator.swift | 10 ++- .../view/player/ios/ExtPlayerUIView.swift | 16 ++-- .../player/ios/error/ErrorMsgViewIOS.swift | 44 ---------- .../player/mac/error/ErrorMsgViewMacOS.swift | 85 ------------------- .../main/ExtPlayerMultiPlatform.swift | 15 ++-- 10 files changed, 70 insertions(+), 169 deletions(-) delete mode 100644 Sources/swiftui-loop-videoplayer/view/player/ios/error/ErrorMsgViewIOS.swift delete mode 100644 Sources/swiftui-loop-videoplayer/view/player/mac/error/ErrorMsgViewMacOS.swift rename Sources/swiftui-loop-videoplayer/view/{ => player}/main/ExtPlayerMultiPlatform.swift (92%) diff --git a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift index a455f3f..1ae9481 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -97,11 +97,15 @@ public struct ExtVideoPlayer: View{ /// The body property defines the view hierarchy for the user interface. public var body: some View { ExtPlayerMultiPlatform( - settings: $settings, - command: $command, - timePublisher: timePublisher, - eventPublisher: eventPublisher + settings: $settings, + command: $command, + timePublisher: timePublisher, + eventPublisher: eventPublisher ) + .onDisappear{ + //player?.onDeinit() + //player = nil + } .onReceive(timePublisher.receive(on: DispatchQueue.main), perform: { time in currentTime = time }) diff --git a/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift index 4d33298..6944eaf 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift @@ -12,6 +12,7 @@ import AVFoundation /// /// Conforming to this protocol allows a class to respond to error events that occur within a media player context. @available(iOS 14, macOS 11, tvOS 14, *) +@MainActor public protocol PlayerDelegateProtocol: AnyObject { /// Called when an error is encountered within the media player. /// diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index c68451e..1698043 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -13,7 +13,7 @@ import CoreImage /// Defines an abstract player protocol to be implemented by player objects, ensuring main-thread safety and compatibility with specific OS versions. /// This protocol is designed for use with classes (reference types) only. @available(iOS 14, macOS 11, tvOS 14, *) -@MainActor @preconcurrency +@MainActor public protocol AbstractPlayer: AnyObject { /// Observes the status property of the new player item. @@ -201,7 +201,9 @@ extension AbstractPlayer{ } callback(item.status) - self?.clearStatusObserver() + Task { @MainActor in + self?.clearStatusObserver() + } } } } @@ -272,7 +274,9 @@ extension AbstractPlayer{ player.seek(to: seekTime){ [weak self] value in let currentTime = CMTimeGetSeconds(player.currentTime()) - self?.delegate?.didSeek(value: value, currentTime: currentTime) + Task { @MainActor in + self?.delegate?.didSeek(value: value, currentTime: currentTime) + } } } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index e0a21cd..b7362ed 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -18,7 +18,7 @@ import AppKit /// Conforming types are expected to manage a video player that can loop content continuously, /// handle errors, and notify a delegate of important events. @available(iOS 14, macOS 11, tvOS 14, *) -@MainActor @preconcurrency +@MainActor public protocol ExtPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ #if canImport(UIKit) @@ -136,7 +136,9 @@ internal extension ExtPlayerProtocol { if let timePublishing = settings.timePublishing{ timeObserver = player.addPeriodicTimeObserver(forInterval: timePublishing, queue: .global()) { [weak self] time in guard let self = self else{ return } - self.delegate?.didPassedTime(seconds: time.seconds) + Task { @MainActor in + self.delegate?.didPassedTime(seconds: time.seconds) + } } } } @@ -260,20 +262,28 @@ internal extension ExtPlayerProtocol { /// - player: The player to observe. func setupObservers(for player: AVQueuePlayer) { errorObserver = player.observe(\.error, options: [.new]) { [weak self] player, _ in - self?.handlePlayerError(player) + Task { @MainActor in + self?.handlePlayerError(player) + } } 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 - self?.delegate?.didPausePlayback() + Task { @MainActor in + self?.delegate?.didPausePlayback() + } case .waitingToPlayAtSpecifiedRate: // Player is waiting to play (e.g., buffering) - self?.delegate?.isWaitingToPlay() + Task { @MainActor in + self?.delegate?.isWaitingToPlay() + } case .playing: // Player is currently playing - self?.delegate?.didStartPlaying() + Task { @MainActor in + self?.delegate?.didStartPlaying() + } @unknown default: break } @@ -282,17 +292,24 @@ internal extension ExtPlayerProtocol { currentItemObserver = player.observe(\.currentItem, options: [.new, .old]) { [weak self] player, change in // Detecting when the current item is changed if let newItem = change.newValue as? AVPlayerItem { - self?.delegate?.currentItemDidChange(to: newItem) + Task { @MainActor in + self?.delegate?.currentItemDidChange(to: newItem) + } } else if change.newValue == nil { - self?.delegate?.currentItemWasRemoved() + Task { @MainActor in + self?.delegate?.currentItemWasRemoved() + } + } + Task { @MainActor in + self?.clearStatusObserver() } - - self?.clearStatusObserver() } volumeObserver = player.observe(\.volume, options: [.new, .old]) { [weak self] player, change in if let newVolume = change.newValue{ - self?.delegate?.volumeDidChange(to: newVolume) + Task { @MainActor in + self?.delegate?.volumeDidChange(to: newVolume) + } } } } diff --git a/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift index 5c648ca..ab4acd9 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/view/ExtPlayerViewProtocol.swift @@ -13,7 +13,7 @@ import Combine /// for looping video players on different platforms. @available(iOS 14, macOS 11, tvOS 14, *) @MainActor @preconcurrency -public protocol ExtPlayerViewProtocol { +protocol ExtPlayerViewProtocol { #if canImport(UIKit) /// Typealias for the main view on iOS, using `UIView`. @@ -26,9 +26,6 @@ public protocol ExtPlayerViewProtocol { associatedtype View: CustomView #endif - /// Typealias for the view used to display errors. - associatedtype ErrorView - #if canImport(UIKit) /// Typealias for the player view on iOS, conforming to `LoopingPlayerProtocol` and using `UIView`. associatedtype PlayerView: ExtPlayerProtocol, UIView @@ -58,7 +55,7 @@ public protocol ExtPlayerViewProtocol { } @available(iOS 14, macOS 11, tvOS 14, *) -public extension ExtPlayerViewProtocol{ +extension ExtPlayerViewProtocol{ /// Creates a player view for looping video content. /// - Parameters: diff --git a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift index 9374de7..9a39d0c 100644 --- a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift +++ b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift @@ -9,6 +9,12 @@ import SwiftUI import Combine import AVFoundation +@MainActor +protocol PlayerDeinit: AnyObject{ + func onDeinit() +} + +@MainActor internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { let eventPublisher: PassthroughSubject @@ -17,6 +23,8 @@ internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { /// Stores the last command applied to the player. private var lastCommand: PlaybackCommand? + + weak var delegate : PlayerDeinit? init( timePublisher: PassthroughSubject, @@ -29,7 +37,7 @@ internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { /// Deinitializes the coordinator and prints a debug message if in DEBUG mode. deinit { #if DEBUG - print("deinit Coordinator") + print("Deinit Coordinator") #endif } diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 0633333..8442610 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -103,12 +103,8 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { compositeLayer?.removeFromSuperlayer() compositeLayer = nil } - - /// Cleans up resources and observers associated with the player. - /// - /// This method invalidates the status and error observers to prevent memory leaks, - /// pauses the player, and clears out player-related references to assist in clean deinitialization. - deinit { + + func onDeinit(){ // First, clear all observers to prevent memory leaks clearObservers() @@ -128,5 +124,13 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { print("Player deinitialized and resources cleaned up.") #endif } + + /// Cleans up resources and observers associated with the player. + /// + /// This method invalidates the status and error observers to prevent memory leaks, + /// pauses the player, and clears out player-related references to assist in clean deinitialization. + deinit { + onDeinit() + } } #endif diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/error/ErrorMsgViewIOS.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/error/ErrorMsgViewIOS.swift deleted file mode 100644 index 4c3d559..0000000 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/error/ErrorMsgViewIOS.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ErrorMsgViewIOS.swift -// -// -// Created by Igor Shelopaev on 06.08.24. -// - -import SwiftUI -import Foundation - -#if canImport(UIKit) -import UIKit - -internal class ErrorMsgViewIOS: UITextView { - - /// Adjusts the top content inset to vertically center the text. - override var contentSize: CGSize { - didSet { - var top = (bounds.size.height - contentSize.height * zoomScale) / 2.0 - top = max(0, top) - contentInset = UIEdgeInsets(top: top, left: 0, bottom: 0, right: 0) - } - } -} - -/// Creates an error message view for iOS with the specified error, color, and font size. -/// -/// - Parameters: -/// - error: The error to display. -/// - color: The color of the error text. -/// - fontSize: The font size of the error text. -/// - Returns: A configured UIView displaying the error message. -@MainActor -internal func errorTpl(_ error: VPErrors, _ color: Color, _ fontSize: CGFloat) -> UIView { - let textView = ErrorMsgViewIOS() - textView.backgroundColor = .clear - textView.text = error.description - textView.textAlignment = .center - textView.font = UIFont.systemFont(ofSize: fontSize) - textView.textColor = UIColor(color) - return textView -} - -#endif diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/error/ErrorMsgViewMacOS.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/error/ErrorMsgViewMacOS.swift deleted file mode 100644 index 16b4453..0000000 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/error/ErrorMsgViewMacOS.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// ErrorMsgViewMacOS.swift -// -// -// Created by Igor Shelopaev on 06.08.24. -// - -import Foundation -import SwiftUI - -#if canImport(AppKit) -import AppKit - -/// A custom NSTextView for displaying error messages on macOS. -internal class ErrorMsgViewMacOS: NSTextView { - - /// Overrides the intrinsic content size to allow flexible width and height. - override var intrinsicContentSize: NSSize { - return NSSize(width: NSView.noIntrinsicMetric, height: NSView.noIntrinsicMetric) - } - - /// Called when the view is added to a superview. Sets up the constraints for the view. - override func viewDidMoveToSuperview() { - super.viewDidMoveToSuperview() - guard let superview = superview else { return } - - translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: 10), - trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: -10), - topAnchor.constraint(equalTo: superview.topAnchor), - bottomAnchor.constraint(equalTo: superview.bottomAnchor) - ]) - } - - /// Adjusts the layout to center the text vertically within the view. - override func layout() { - super.layout() - - guard let layoutManager = layoutManager, let textContainer = textContainer else { - return - } - - let textHeight = layoutManager.usedRect(for: textContainer).size.height - let containerHeight = bounds.size.height - let verticalInset = max(0, (containerHeight - textHeight) / 2) - - textContainerInset = NSSize(width: 0, height: verticalInset) - } -} - -/// Creates a custom error view for macOS displaying an error message. -/// - Parameters: -/// - error: The error object containing the error description. -/// - color: The color to be used for the error text. -/// - fontSize: The font size to be used for the error text. -/// - Returns: An `NSView` containing the error message text view centered with padding. -internal func errorTpl(_ error: VPErrors, _ color: Color, _ fontSize: CGFloat) -> NSView { - let textView = ErrorMsgViewMacOS() - textView.isEditable = false - textView.isSelectable = false - textView.drawsBackground = false - textView.string = error.description - textView.alignment = .center - textView.font = NSFont.systemFont(ofSize: fontSize) - textView.textColor = NSColor(color) - - let containerView = NSView() - containerView.addSubview(textView) - - textView.translatesAutoresizingMaskIntoConstraints = false - - // Center textView in containerView with padding - NSLayoutConstraint.activate([ - textView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), - textView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), - textView.leadingAnchor.constraint(greaterThanOrEqualTo: containerView.leadingAnchor, constant: 10), - textView.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor, constant: -10) - ]) - - return containerView -} - -#endif diff --git a/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift similarity index 92% rename from Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift rename to Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift index 0a0b911..1610202 100644 --- a/Sources/swiftui-loop-videoplayer/view/main/ExtPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift @@ -26,13 +26,9 @@ internal struct ExtPlayerMultiPlatform: ExtPlayerViewProtocol { #if canImport(UIKit) typealias View = UIView - typealias ErrorView = ErrorMsgViewIOS - typealias PlayerView = ExtPlayerUIView #elseif canImport(AppKit) - typealias View = NSView - - typealias ErrorView = ErrorMsgViewMacOS + typealias View = NSView typealias PlayerView = ExtPlayerNSView #endif @@ -78,7 +74,7 @@ extension ExtPlayerMultiPlatform: UIViewRepresentable{ /// Creates the container view with the player view and error view if needed /// - Parameter context: The context for the view /// - Returns: A configured UIView - @MainActor func makeUIView(context: Context) -> UIView { + func makeUIView(context: Context) -> UIView { let container = UIView() if let player: PlayerView = makePlayerView(container){ @@ -92,11 +88,10 @@ extension ExtPlayerMultiPlatform: UIViewRepresentable{ /// - Parameters: /// - uiView: The UIView to update /// - context: The context for the view - @MainActor func updateUIView(_ uiView: UIView, context: Context) { + func updateUIView(_ uiView: UIView, context: Context) { let player = uiView.findFirstSubview(ofType: PlayerView.self) if let player{ - player.update(settings: settings) // Check if command changed before applying it @@ -114,7 +109,7 @@ extension ExtPlayerMultiPlatform: NSViewRepresentable{ /// Creates the NSView for the representable component. It initializes the view, configures it with a player if available, and adds an error view if necessary. /// - Parameter context: The context containing environment and state information used during view creation. /// - Returns: A fully configured NSView containing both the media player and potentially an error message display. - @MainActor func makeNSView(context: Context) -> NSView { + func makeNSView(context: Context) -> NSView { let container = NSView() if let player: PlayerView = makePlayerView(container){ @@ -128,7 +123,7 @@ extension ExtPlayerMultiPlatform: NSViewRepresentable{ /// - Parameters: /// - nsView: The NSView that needs updating. /// - context: The context containing environment and state information used during the view update. - @MainActor func updateNSView(_ nsView: NSView, context: Context) { + func updateNSView(_ nsView: NSView, context: Context) { let player = nsView.findFirstSubview(ofType: PlayerView.self) if let player { From 24e1bc8dcfe2ff092ee97ee0dc62ac7a5e9048f8 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 17 Jan 2025 12:24:14 +0100 Subject: [PATCH 062/209] code refactoring --- .../enum/PlaybackCommand.swift | 6 +-- .../helpers/PlayerDelegateProtocol.swift | 1 - .../protocol/player/AbstractPlayer.swift | 17 +++++---- .../protocol/player/ExtPlayerProtocol.swift | 37 ++++--------------- .../view/helpers/PlayerCoordinator.swift | 7 ---- .../view/player/ios/ExtPlayerUIView.swift | 13 ++----- .../view/player/mac/ExtPlayerNSView.swift | 8 +--- .../player/main/ExtPlayerMultiPlatform.swift | 27 +++++++++++++- 8 files changed, 51 insertions(+), 65 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift b/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift index ae00771..f8adfbb 100644 --- a/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift +++ b/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift @@ -25,7 +25,7 @@ public enum PlaybackCommand: Equatable { /// Command to seek to a specific time in the video. /// - Parameter time: The target position to seek to in the video, represented in seconds. - case seek(to: Double) + case seek(to: Double, play: Bool = true) /// Command to position the video at the beginning. case begin @@ -97,8 +97,8 @@ public enum PlaybackCommand: Equatable { (.removeAllFilters, .removeAllFilters), (.removeAllVectors, .removeAllVectors): return true - case (.seek(let lhsTime), .seek(let rhsTime)): - return lhsTime == rhsTime + case (.seek(let lhsTime, let lhsPlay), .seek(let rhsTime, let rhsPlay)): + return lhsTime == rhsTime && lhsPlay == rhsPlay case (.volume(let lhsVolume), .volume(let rhsVolume)): return lhsVolume == rhsVolume diff --git a/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift index 6944eaf..4d33298 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift @@ -12,7 +12,6 @@ import AVFoundation /// /// Conforming to this protocol allows a class to respond to error events that occur within a media player context. @available(iOS 14, macOS 11, tvOS 14, *) -@MainActor public protocol PlayerDelegateProtocol: AnyObject { /// Called when an error is encountered within the media player. /// diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 1698043..125af4a 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -62,7 +62,7 @@ public protocol AbstractPlayer: AnyObject { /// Seeks the video to a specific time. /// This method moves the playback position to the specified time with precise accuracy. /// - Parameter time: The target time to seek to in the video timeline. - func seek(to time: Double) + func seek(to time: Double, play: Bool) /// Seeks to the start of the video. /// This method positions the playback at the beginning of the video. @@ -218,12 +218,12 @@ extension AbstractPlayer{ /// This method moves the playback position to the specified time with precise accuracy. /// If the specified time is out of bounds, it will be clamped to the nearest valid time. /// - Parameter time: The target time to seek to in the video timeline. - func seek(to time: Double) { + func seek(to time: Double, play: Bool = false) { guard let player = player, let duration = player.currentItem?.duration else { if let settings = currentSettings{ let asset = assetFor(settings) update(settings: settings, asset: asset, callback: nil) - seek(to: time) + seek(to: time, play: play) return } @@ -238,7 +238,7 @@ extension AbstractPlayer{ let callback : ((AVPlayerItem.Status) -> Void)? = { [weak self] status in if status == .readyToPlay{ - self?.seek(to: time) + self?.seek(to: time, play: play) }else { self?.delegate?.didSeek(value: false, currentTime: time) } @@ -273,10 +273,13 @@ extension AbstractPlayer{ } player.seek(to: seekTime){ [weak self] value in - let currentTime = CMTimeGetSeconds(player.currentTime()) - Task { @MainActor in - self?.delegate?.didSeek(value: value, currentTime: currentTime) + let currentTime = CMTimeGetSeconds(player.currentTime()) + self?.delegate?.didSeek(value: value, currentTime: currentTime) + Task{ @MainActor in + if play{ + self?.play() } + } } } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index b7362ed..0a0a67d 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -63,10 +63,6 @@ public protocol ExtPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ /// - player: The AVQueuePlayer to observe for errors. func setupObservers(for player: AVQueuePlayer) - /// Responds to errors reported by the AVQueuePlayer. - /// - /// - Parameter player: The AVQueuePlayer that encountered an error. - func handlePlayerError(_ player: AVPlayer) } internal extension ExtPlayerProtocol { @@ -262,9 +258,8 @@ internal extension ExtPlayerProtocol { /// - player: The player to observe. func setupObservers(for player: AVQueuePlayer) { errorObserver = player.observe(\.error, options: [.new]) { [weak self] player, _ in - Task { @MainActor in - self?.handlePlayerError(player) - } + guard let error = player.error else { return } + self?.delegate?.didReceiveError(.remoteVideoError(error)) } timeControlObserver = player.observe(\.timeControlStatus, options: [.new, .old]) { [weak self] player, change in @@ -292,37 +287,19 @@ internal extension ExtPlayerProtocol { currentItemObserver = player.observe(\.currentItem, options: [.new, .old]) { [weak self] player, change in // Detecting when the current item is changed if let newItem = change.newValue as? AVPlayerItem { - Task { @MainActor in - self?.delegate?.currentItemDidChange(to: newItem) - } + self?.delegate?.currentItemDidChange(to: newItem) } else if change.newValue == nil { - Task { @MainActor in self?.delegate?.currentItemWasRemoved() - } - } - Task { @MainActor in - self?.clearStatusObserver() } + self?.clearStatusObserver() } volumeObserver = player.observe(\.volume, options: [.new, .old]) { [weak self] player, change in if let newVolume = change.newValue{ - Task { @MainActor in - self?.delegate?.volumeDidChange(to: newVolume) - } + self?.delegate?.volumeDidChange(to: newVolume) } } } - - /// Responds to errors reported by the AVPlayer. - /// - /// If an error is present, this method notifies the delegate of the encountered error, - /// encapsulated within a `remoteVideoError`. - /// - Parameter player: The AVPlayer that encountered an error to be evaluated. - func handlePlayerError(_ player: AVPlayer) { - guard let error = player.error else { return } - delegate?.didReceiveError(.remoteVideoError(error)) - } /// Clear observers func clearObservers(){ @@ -389,8 +366,8 @@ internal extension ExtPlayerProtocol { play() case .pause: pause() - case .seek(to: let time): - seek(to: time) + case .seek(to: let time, play: let play): + seek(to: time, play: play) case .begin: seekToStart() case .end: diff --git a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift index 9a39d0c..7028305 100644 --- a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift +++ b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift @@ -9,12 +9,7 @@ import SwiftUI import Combine import AVFoundation -@MainActor -protocol PlayerDeinit: AnyObject{ - func onDeinit() -} -@MainActor internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { let eventPublisher: PassthroughSubject @@ -23,8 +18,6 @@ internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { /// Stores the last command applied to the player. private var lastCommand: PlaybackCommand? - - weak var delegate : PlayerDeinit? init( timePublisher: PassthroughSubject, diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 8442610..ff9713a 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -14,7 +14,8 @@ import AVKit #if canImport(UIKit) import UIKit -internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { +@MainActor +internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { /// This property holds an instance of `VideoSettings` internal var currentSettings : VideoSettings? @@ -104,7 +105,7 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { compositeLayer = nil } - func onDeinit(){ + func onDisappear(){ // First, clear all observers to prevent memory leaks clearObservers() @@ -124,13 +125,5 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { print("Player deinitialized and resources cleaned up.") #endif } - - /// Cleans up resources and observers associated with the player. - /// - /// This method invalidates the status and error observers to prevent memory leaks, - /// pauses the player, and clears out player-related references to assist in clean deinitialization. - deinit { - onDeinit() - } } #endif diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index f2476a9..4ea4183 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -16,6 +16,7 @@ import AppKit /// A NSView subclass that loops video using AVFoundation on macOS. /// This class handles the initialization and management of a looping video player with customizable video gravity. +@MainActor internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { /// This property holds an instance of `VideoSettings` @@ -104,12 +105,7 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { compositeLayer = nil } - /// Cleans up resources and observers associated with the player. - /// - /// This method invalidates the status and error observers to prevent memory leaks, - /// pauses the player, and clears out player-related references to assist in clean deinitialization. - deinit { - + func onDisappear(){ // First, clear all observers to prevent memory leaks clearObservers() diff --git a/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift index 1610202..3e81c0b 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift @@ -101,6 +101,19 @@ extension ExtPlayerMultiPlatform: UIViewRepresentable{ } } } + + /// Called by SwiftUI to dismantle the UIView when the associated SwiftUI view is removed from the view hierarchy. + /// + /// - Parameters: + /// - uiView: The UIView instance being dismantled. + /// - coordinator: The coordinator instance that manages interactions between SwiftUI and the UIView. + static func dismantleUIView(_ uiView: UIView, coordinator: PlayerCoordinator) { + // Called by SwiftUI when this view is removed from the hierarchy + let player = uiView.findFirstSubview(ofType: PlayerView.self) + if let player{ + player.onDisappear() + } + } } #endif @@ -134,7 +147,19 @@ extension ExtPlayerMultiPlatform: NSViewRepresentable{ player.setCommand(command) context.coordinator.setLastCommand(command) // Update the last command in the coordinator } - + } + } + + /// Called by SwiftUI to dismantle the NSView when the associated SwiftUI view is removed from the view hierarchy. + /// + /// - Parameters: + /// - uiView: The NSView instance being dismantled. + /// - coordinator: The coordinator instance that manages interactions between SwiftUI and the NSView. + static func dismantleUIView(_ uiView: NSView, coordinator: PlayerCoordinator) { + // Called by SwiftUI when this view is removed from the hierarchy + let player = uiView.findFirstSubview(ofType: PlayerView.self) + if let player{ + player.onDisappear() } } } From 2f5d3d3529b79f04687a9c6cb58c7623094cd393 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 17 Jan 2025 12:28:18 +0100 Subject: [PATCH 063/209] update --- .../protocol/player/AbstractPlayer.swift | 4 ++-- .../protocol/player/ExtPlayerProtocol.swift | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 125af4a..d9e5053 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -274,8 +274,8 @@ extension AbstractPlayer{ player.seek(to: seekTime){ [weak self] value in let currentTime = CMTimeGetSeconds(player.currentTime()) - self?.delegate?.didSeek(value: value, currentTime: currentTime) - Task{ @MainActor in + Task { @MainActor in + self?.delegate?.didSeek(value: value, currentTime: currentTime) if play{ self?.play() } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 0a0a67d..dc87fe3 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -259,7 +259,9 @@ internal extension ExtPlayerProtocol { func setupObservers(for player: AVQueuePlayer) { errorObserver = player.observe(\.error, options: [.new]) { [weak self] player, _ in guard let error = player.error else { return } - self?.delegate?.didReceiveError(.remoteVideoError(error)) + Task { @MainActor in + self?.delegate?.didReceiveError(.remoteVideoError(error)) + } } timeControlObserver = player.observe(\.timeControlStatus, options: [.new, .old]) { [weak self] player, change in @@ -287,16 +289,24 @@ internal extension ExtPlayerProtocol { currentItemObserver = player.observe(\.currentItem, options: [.new, .old]) { [weak self] player, change in // Detecting when the current item is changed if let newItem = change.newValue as? AVPlayerItem { - self?.delegate?.currentItemDidChange(to: newItem) + Task { @MainActor in + self?.delegate?.currentItemDidChange(to: newItem) + } } else if change.newValue == nil { + Task { @MainActor in self?.delegate?.currentItemWasRemoved() + } + } + Task { @MainActor in + self?.clearStatusObserver() } - self?.clearStatusObserver() } volumeObserver = player.observe(\.volume, options: [.new, .old]) { [weak self] player, change in if let newVolume = change.newValue{ - self?.delegate?.volumeDidChange(to: newVolume) + Task { @MainActor in + self?.delegate?.volumeDidChange(to: newVolume) + } } } } From 10b1d6acdd1f04c0cdc19878162c243e7fbc3b0e Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 17 Jan 2025 12:30:00 +0100 Subject: [PATCH 064/209] update --- .../protocol/helpers/PlayerDelegateProtocol.swift | 1 + .../view/helpers/PlayerCoordinator.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift index 4d33298..6944eaf 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift @@ -12,6 +12,7 @@ import AVFoundation /// /// Conforming to this protocol allows a class to respond to error events that occur within a media player context. @available(iOS 14, macOS 11, tvOS 14, *) +@MainActor public protocol PlayerDelegateProtocol: AnyObject { /// Called when an error is encountered within the media player. /// diff --git a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift index 7028305..590ea52 100644 --- a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift +++ b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift @@ -9,7 +9,7 @@ import SwiftUI import Combine import AVFoundation - +@MainActor internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { let eventPublisher: PassthroughSubject From a0e119a457ac2c93ead80675ef354178391ba345 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 17 Jan 2025 12:37:26 +0100 Subject: [PATCH 065/209] Update ExtPlayerMultiPlatform.swift --- .../view/player/main/ExtPlayerMultiPlatform.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift index 3e81c0b..29c07ab 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift @@ -103,7 +103,7 @@ extension ExtPlayerMultiPlatform: UIViewRepresentable{ } /// Called by SwiftUI to dismantle the UIView when the associated SwiftUI view is removed from the view hierarchy. - /// + /// https://developer.apple.com/documentation/swiftui/uiviewrepresentable/dismantleuiview(_:coordinator:) /// - Parameters: /// - uiView: The UIView instance being dismantled. /// - coordinator: The coordinator instance that manages interactions between SwiftUI and the UIView. From 7e93ef250a3ed50ba03a44df916f145361996816 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 17 Jan 2025 12:45:55 +0100 Subject: [PATCH 066/209] update --- .../view/player/mac/ExtPlayerNSView.swift | 1 - .../view/player/main/ExtPlayerMultiPlatform.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index 4ea4183..d13db59 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -16,7 +16,6 @@ import AppKit /// A NSView subclass that loops video using AVFoundation on macOS. /// This class handles the initialization and management of a looping video player with customizable video gravity. -@MainActor internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { /// This property holds an instance of `VideoSettings` diff --git a/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift index 29c07ab..fc68193 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift @@ -155,7 +155,7 @@ extension ExtPlayerMultiPlatform: NSViewRepresentable{ /// - Parameters: /// - uiView: The NSView instance being dismantled. /// - coordinator: The coordinator instance that manages interactions between SwiftUI and the NSView. - static func dismantleUIView(_ uiView: NSView, coordinator: PlayerCoordinator) { + static func dismantleNSView(_ uiView: NSView, coordinator: PlayerCoordinator) { // Called by SwiftUI when this view is removed from the hierarchy let player = uiView.findFirstSubview(ofType: PlayerView.self) if let player{ From 6f96b162d152586a6cb42273818935757e7ff2a0 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 17 Jan 2025 12:57:58 +0100 Subject: [PATCH 067/209] update --- README.md | 2 +- .../swiftui-loop-videoplayer/enum/PlaybackCommand.swift | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f6e28df..bb10a64 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ In cases where you need to re-issue a command that might appear redundant but is | `idle` | Start without any actions. Any command passed during initialization will be executed. If you'd like to start without any actions based on settings values just setup command to `.idle` | | `play` | Command to play the video. | | `pause` | Command to pause the video. | -| `seek(to: Double)` | Command to seek to a specific time in the video. The parameter is the target position in seconds. If the time is negative, the playback will move to the start of the video. If the time exceeds the video's duration, the playback will move to the end of the video. If the time is within the video’s duration, the playback will move to the specified time. | +| `seek(to: Double, play: Bool = true)` | Command to seek to a specific time in the video. The time parameter specifies the target position in seconds. If time is negative, playback will jump to the start of the video. If time exceeds the video’s duration, playback will move to the end of the video. For valid values within the video’s duration, playback will move precisely to the specified time. The play parameter determines whether playback should resume automatically after seeking, with a default value of true. | | `begin` | Command to position the video at the beginning. | | `end` | Command to position the video at the end. | | `mute` | Command to mute the video. By default, the player is muted. | diff --git a/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift b/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift index f8adfbb..d1e8bb2 100644 --- a/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift +++ b/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift @@ -24,7 +24,13 @@ public enum PlaybackCommand: Equatable { case pause /// Command to seek to a specific time in the video. - /// - Parameter time: The target position to seek to in the video, represented in seconds. + /// + /// This case allows seeking to a specified time position in the video and optionally starts playback immediately. + /// + /// - Parameters: + /// - time: The target time position in the video, specified in seconds. + /// - play: A Boolean value indicating whether playback should automatically resume after seeking. + /// Defaults to `true`. case seek(to: Double, play: Bool = true) /// Command to position the video at the beginning. From 47a970a5131a3d579554e1aef33ee2438e731aac Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 17 Jan 2025 13:00:13 +0100 Subject: [PATCH 068/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb10a64..e1f5621 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ In cases where you need to re-issue a command that might appear redundant but is | `idle` | Start without any actions. Any command passed during initialization will be executed. If you'd like to start without any actions based on settings values just setup command to `.idle` | | `play` | Command to play the video. | | `pause` | Command to pause the video. | -| `seek(to: Double, play: Bool = true)` | Command to seek to a specific time in the video. The time parameter specifies the target position in seconds. If time is negative, playback will jump to the start of the video. If time exceeds the video’s duration, playback will move to the end of the video. For valid values within the video’s duration, playback will move precisely to the specified time. The play parameter determines whether playback should resume automatically after seeking, with a default value of true. | +| `seek(to: Double, play: Bool)` | Command to seek to a specific time in the video. The time parameter specifies the target position in seconds. If time is negative, playback will jump to the start of the video. If time exceeds the video’s duration, playback will move to the end of the video. For valid values within the video’s duration, playback will move precisely to the specified time. The play parameter determines whether playback should resume automatically after seeking, with a default value of true. | | `begin` | Command to position the video at the beginning. | | `end` | Command to position the video at the end. | | `mute` | Command to mute the video. By default, the player is muted. | From 1ad37914cdfcae2a450fb73672749ebe669a05d2 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 17 Jan 2025 15:36:38 +0100 Subject: [PATCH 069/209] update --- .../protocol/player/AbstractPlayer.swift | 64 +++++++++---------- .../protocol/player/ExtPlayerProtocol.swift | 8 +-- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index d9e5053..6f82f31 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -110,7 +110,7 @@ public protocol AbstractPlayer: AnyObject { func applyVideoComposition() /// Updates the current playback asset, settings, and initializes playback or a specific action when the asset is ready. - func update(settings: VideoSettings, asset : AVURLAsset?, callback: ((AVPlayerItem.Status) -> Void)?) + func update(settings: VideoSettings, asset : AVURLAsset?) } extension AbstractPlayer{ @@ -190,28 +190,34 @@ extension AbstractPlayer{ /// - Parameters: /// - newItem: The `AVPlayerItem` whose status is to be observed. /// - callback: A closure that is called when the item's status changes to `.readyToPlay` or `.failed`. - func setupStateItemStatusObserver(newItem: AVPlayerItem, callback: ((AVPlayerItem.Status) -> Void)?) { + func setupStateItemStatusObserver(newItem: AVPlayerItem, callback : @escaping (AVPlayerItem.Status) -> Void) { + clearStatusObserver() - if let callback = callback { - //.unknown: This state is essentially the default, indicating that the player item is new or has not yet attempted to load its assets. - statusObserver = newItem.observe(\.status, options: [.new, .old]) { [weak self] item, _ in - guard item.status == .readyToPlay || item.status == .failed else { - return - } - - callback(item.status) - Task { @MainActor in - self?.clearStatusObserver() - } + guard newItem.status == .unknown else{ + callback(newItem.status) + return + } + + //.unknown: This state is essentially the default, indicating that the player item is new or has not yet attempted to load its assets. + statusObserver = newItem.observe(\.status, options: [.new, .old]) { [weak self] item, _ in + guard item.status == .readyToPlay || item.status == .failed else { + return + } + + callback(item.status) + Task { @MainActor in + self?.clearStatusObserver() } } } /// Clear status observer func clearStatusObserver(){ - statusObserver?.invalidate() - statusObserver = nil + if statusObserver != nil{ + statusObserver?.invalidate() + statusObserver = nil + } } /// Seeks the video to a specific time. @@ -221,33 +227,23 @@ extension AbstractPlayer{ func seek(to time: Double, play: Bool = false) { guard let player = player, let duration = player.currentItem?.duration else { if let settings = currentSettings{ - let asset = assetFor(settings) - update(settings: settings, asset: asset, callback: nil) - seek(to: time, play: play) - return - } - - delegate?.didSeek(value: false, currentTime: time) - return - - } - - guard currentItem?.status == .readyToPlay else{ - /// The case when the video is finished and we are trying to seek back - if let currentItem{ - - let callback : ((AVPlayerItem.Status) -> Void)? = { [weak self] status in + let callback : (AVPlayerItem.Status) -> Void = { [weak self] status in if status == .readyToPlay{ self?.seek(to: time, play: play) }else { self?.delegate?.didSeek(value: false, currentTime: time) } } - - setupStateItemStatusObserver(newItem: currentItem, callback: callback) + update(settings: settings, asset: assetFor(settings)) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){ + if let item = self.currentItem{ + self.setupStateItemStatusObserver(newItem: item, callback: callback) + } + } + }else{ + delegate?.didSeek(value: false, currentTime: time) } - delegate?.didSeek(value: false, currentTime: time) return } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index dc87fe3..868e8a2 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -173,8 +173,7 @@ internal extension ExtPlayerProtocol { /// providing the new status as its parameter for handling additional setup or errors. func update( settings: VideoSettings, - asset : AVURLAsset? = nil, - callback: ((AVPlayerItem.Status) -> Void)? = nil + asset : AVURLAsset? = nil ) { guard let asset = prepareAsset(settings, asset) else { return } @@ -185,13 +184,10 @@ internal extension ExtPlayerProtocol { let newItem = createPlayerItem(with: asset, settings: settings) insert(newItem) - + if settings.loop { loop() } - - // Observe status changes - setupStateItemStatusObserver(newItem: newItem, callback: callback) if !settings.notAutoPlay { play() From 509c13aa36b0a7a04389e33481b12d1d289f30d5 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 17 Jan 2025 18:00:15 +0100 Subject: [PATCH 070/209] update --- .../protocol/player/AbstractPlayer.swift | 70 ++++++++++++------ .../protocol/player/ExtPlayerProtocol.swift | 71 +++---------------- 2 files changed, 55 insertions(+), 86 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 6f82f31..be16ac4 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -110,7 +110,7 @@ public protocol AbstractPlayer: AnyObject { func applyVideoComposition() /// Updates the current playback asset, settings, and initializes playback or a specific action when the asset is ready. - func update(settings: VideoSettings, asset : AVURLAsset?) + func update(settings: VideoSettings, doUpdate : Bool) } extension AbstractPlayer{ @@ -188,24 +188,25 @@ extension AbstractPlayer{ /// the observer is invalidated, ensuring that the callback is called only once. /// /// - Parameters: - /// - newItem: The `AVPlayerItem` whose status is to be observed. + /// - item: The `AVPlayerItem` whose status is to be observed. /// - callback: A closure that is called when the item's status changes to `.readyToPlay` or `.failed`. - func setupStateItemStatusObserver(newItem: AVPlayerItem, callback : @escaping (AVPlayerItem.Status) -> Void) { + func setupStateStatusObserver(for item: AVPlayerItem, callback : @escaping (AVPlayerItem.Status) -> Void) { clearStatusObserver() - guard newItem.status == .unknown else{ - callback(newItem.status) + guard item.status == .unknown else{ + callback(item.status) return } - //.unknown: This state is essentially the default, indicating that the player item is new or has not yet attempted to load its assets. - statusObserver = newItem.observe(\.status, options: [.new, .old]) { [weak self] item, _ in + statusObserver = item.observe(\.status, options: [.new, .initial]) { [weak self] item, change in + print(item.status.rawValue, "status") guard item.status == .readyToPlay || item.status == .failed else { return } callback(item.status) + Task { @MainActor in self?.clearStatusObserver() } @@ -219,6 +220,28 @@ extension AbstractPlayer{ statusObserver = nil } } + + /// Creates an `AVPlayerItem` with optional subtitle merging. + /// - Parameters: + /// - asset: The main video asset. + /// - settings: A `VideoSettings` object containing subtitle configuration. + /// - Returns: A new `AVPlayerItem` configured with the merged or original asset. + func createPlayerItem(with settings: VideoSettings) -> AVPlayerItem? { + + guard let asset = assetFor(settings) else{ + delegate?.didReceiveError(.sourceNotFound(settings.name)) + return nil + } + + if let subtitleAsset = subtitlesAssetFor(settings), + let mergedAsset = mergeAssetWithSubtitles(videoAsset: asset, subtitleAsset: subtitleAsset) { + // Create and return a new `AVPlayerItem` using the merged asset + return AVPlayerItem(asset: mergedAsset) + } else { + // Create and return a new `AVPlayerItem` using the original asset + return AVPlayerItem(asset: asset) + } + } /// Seeks the video to a specific time. /// This method moves the playback position to the specified time with precise accuracy. @@ -226,24 +249,23 @@ extension AbstractPlayer{ /// - Parameter time: The target time to seek to in the video timeline. func seek(to time: Double, play: Bool = false) { guard let player = player, let duration = player.currentItem?.duration else { - if let settings = currentSettings{ - let callback : (AVPlayerItem.Status) -> Void = { [weak self] status in - if status == .readyToPlay{ - self?.seek(to: time, play: play) - }else { - self?.delegate?.didSeek(value: false, currentTime: time) - } - } - update(settings: settings, asset: assetFor(settings)) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){ - if let item = self.currentItem{ - self.setupStateItemStatusObserver(newItem: item, callback: callback) - } - } - }else{ + guard let settings = currentSettings else{ delegate?.didSeek(value: false, currentTime: time) + return + } + update(settings: settings, doUpdate: true) + let callback : (AVPlayerItem.Status) -> Void = { [weak self] status in + guard status == .readyToPlay else { + self?.delegate?.didSeek(value: false, currentTime: time) + return + } + self?.seek(to: time, play: play) + } + /// Need to refactor + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){ [weak self] in + guard let item = self?.currentItem else { return } + self?.setupStateStatusObserver(for: item, callback: callback) } - return } @@ -274,6 +296,8 @@ extension AbstractPlayer{ self?.delegate?.didSeek(value: value, currentTime: currentTime) if play{ self?.play() + }else{ + self?.pause() } } } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 868e8a2..906475b 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -167,21 +167,19 @@ internal extension ExtPlayerProtocol { /// - Parameters: /// - settings: A `VideoSettings` struct containing configurations such as playback gravity, /// whether to loop the content, and whether to mute the audio. - /// - asset: An optional `AVURLAsset` representing the new video content to be loaded. If nil, - /// the current asset continues playing with updated settings. - /// - callback: An optional closure executed when the asset reaches `.readyToPlay` status, - /// providing the new status as its parameter for handling additional setup or errors. - func update( - settings: VideoSettings, - asset : AVURLAsset? = nil - ) { - guard let asset = prepareAsset(settings, asset) else { return } + func update(settings: VideoSettings, doUpdate : Bool = false) { + + if doUpdate == false && settings.isEqual(currentSettings){ + return + } stop() currentSettings = settings - let newItem = createPlayerItem(with: asset, settings: settings) + guard let newItem = createPlayerItem(with: settings) else{ + return + } insert(newItem) @@ -193,59 +191,6 @@ internal extension ExtPlayerProtocol { play() } } - - /// Prepares and validates the media asset for playback based on the given settings. - /// - /// This function determines the appropriate `AVURLAsset` to use for media playback. - /// If a specific asset is provided, it uses that asset; otherwise, it attempts to retrieve - /// an asset based on the provided settings. If the settings have changed from the current settings, - /// it fetches a new asset using a method presumed to be `getAssets()`. If no valid asset is found or - /// provided, it notifies a delegate of the error. - /// - /// - Parameters: - /// - settings: The `VideoSettings` containing configuration and asset retrieval logic. - /// - asset: An optional `AVURLAsset` to be used directly if provided. If nil, an asset is attempted - /// to be retrieved based on the `settings`. - /// - /// - Returns: An optional `AVURLAsset` if a valid asset is found or provided; otherwise, nil if no - /// valid asset could be located or an error occurred. - /// - /// - Note: This function calls `didReceiveError` on the delegate with an error of `.sourceNotFound` - /// if no valid asset is found, providing context for the failure. - func prepareAsset(_ settings: VideoSettings, _ asset: AVURLAsset? = nil) -> AVURLAsset? { - if let asset = asset { - return asset - } - - let newAsset = settings.getAssets() - - if !settings.isEqual(currentSettings), let newAsset{ - return newAsset - } - - if newAsset == nil { - delegate?.didReceiveError(.sourceNotFound(settings.name)) - } - - return nil - } - - /// Creates an `AVPlayerItem` with optional subtitle merging. - /// - Parameters: - /// - asset: The main video asset. - /// - settings: A `VideoSettings` object containing subtitle configuration. - /// - Returns: A new `AVPlayerItem` configured with the merged or original asset. - func createPlayerItem(with asset: AVURLAsset, settings: VideoSettings) -> AVPlayerItem { - // Attempt to retrieve the subtitle asset - if let subtitleAsset = subtitlesAssetFor(settings), - let mergedAsset = mergeAssetWithSubtitles(videoAsset: asset, subtitleAsset: subtitleAsset) { - // Create and return a new `AVPlayerItem` using the merged asset - return AVPlayerItem(asset: mergedAsset) - } else { - // Create and return a new `AVPlayerItem` using the original asset - return AVPlayerItem(asset: asset) - } - } /// Sets up observers on the player item and the player to track their status and error states. /// From 4ef6426dc9021ab9edeb41316d3c11536710fb62 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 18 Jan 2025 12:18:36 +0100 Subject: [PATCH 071/209] update --- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 30 +++++++++++++++++++ .../protocol/player/AbstractPlayer.swift | 30 +++++++------------ 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index 737b61c..a93c168 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -207,3 +207,33 @@ func mergeAssetWithSubtitles(videoAsset: AVURLAsset, subtitleAsset: AVURLAsset) return videoAsset #endif } + +/// Determines the seek time as a `CMTime` based on a specified time and the total duration of the media. +/// The function ensures that the seek time is within valid bounds (start to end of the media). +/// +/// - Parameters: +/// - time: A `Double` value representing the desired time to seek to, in seconds. +/// If the value is negative, the function will seek to the start of the media. +/// If the value exceeds the total duration, the function will seek to the end. +/// - 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{ + 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 +} diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index be16ac4..f339c55 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -243,10 +243,16 @@ extension AbstractPlayer{ } } - /// Seeks the video to a specific time. - /// This method moves the playback position to the specified time with precise accuracy. - /// If the specified time is out of bounds, it will be clamped to the nearest valid time. - /// - Parameter time: The target time to seek to in the video timeline. + /// Seeks the video to a specific time in the timeline. + /// This method adjusts the playback position to the specified time with precise accuracy. + /// If the target time is out of bounds (negative or beyond the duration), it will be clamped to the nearest valid time (start or end of the video). + /// + /// - Parameters: + /// - time: A `Double` value representing the target time (in seconds) to seek to in the video timeline. + /// If the value is less than 0, the playback position will be set to the start of the video. + /// If the value exceeds the video's duration, it will be set to the end. + /// - play: A `Bool` value indicating whether to start playback immediately after seeking. + /// Defaults to `false`, meaning playback will remain paused after the seek operation. func seek(to time: Double, play: Bool = false) { guard let player = player, let duration = player.currentItem?.duration else { guard let settings = currentSettings else{ @@ -274,21 +280,7 @@ extension AbstractPlayer{ return } - 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 seekTime = getSeekTime(for: time, duration: duration) player.seek(to: seekTime){ [weak self] value in let currentTime = CMTimeGetSeconds(player.currentTime()) From 3ddad6e34ce72b015bbfe77ee23a650e0ea59c47 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 18 Jan 2025 13:51:01 +0100 Subject: [PATCH 072/209] update --- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 5 ++++- .../protocol/player/AbstractPlayer.swift | 6 ++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index a93c168..a0ed4ed 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -218,7 +218,10 @@ 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{ +func getSeekTime(for time: Double, duration : CMTime) -> CMTime?{ + + guard duration.value != 0 else{ return nil } + let endTime = CMTimeGetSeconds(duration) let seekTime : CMTime diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index f339c55..bb0f00a 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -274,13 +274,11 @@ extension AbstractPlayer{ } return } - - guard duration.value != 0 else{ + + guard let seekTime = getSeekTime(for: time, duration: duration) else{ delegate?.didSeek(value: false, currentTime: time) return } - - let seekTime = getSeekTime(for: time, duration: duration) player.seek(to: seekTime){ [weak self] value in let currentTime = CMTimeGetSeconds(player.currentTime()) From f3cbfa20d732b37db4f51affb95445ceed1d26aa Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 18 Jan 2025 14:03:50 +0100 Subject: [PATCH 073/209] update --- .../protocol/player/AbstractPlayer.swift | 21 +++++++------- .../protocol/player/ExtPlayerProtocol.swift | 29 +++++++++++++------ 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index bb0f00a..f35e9df 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -110,7 +110,7 @@ public protocol AbstractPlayer: AnyObject { func applyVideoComposition() /// Updates the current playback asset, settings, and initializes playback or a specific action when the asset is ready. - func update(settings: VideoSettings, doUpdate : Bool) + func update(settings: VideoSettings, doUpdate : Bool, callback : ((AVPlayerItem) -> Void)?) } extension AbstractPlayer{ @@ -215,10 +215,9 @@ extension AbstractPlayer{ /// Clear status observer func clearStatusObserver(){ - if statusObserver != nil{ - statusObserver?.invalidate() - statusObserver = nil - } + guard statusObserver != nil else { return } + statusObserver?.invalidate() + statusObserver = nil } /// Creates an `AVPlayerItem` with optional subtitle merging. @@ -259,7 +258,6 @@ extension AbstractPlayer{ delegate?.didSeek(value: false, currentTime: time) return } - update(settings: settings, doUpdate: true) let callback : (AVPlayerItem.Status) -> Void = { [weak self] status in guard status == .readyToPlay else { self?.delegate?.didSeek(value: false, currentTime: time) @@ -267,11 +265,14 @@ extension AbstractPlayer{ } self?.seek(to: time, play: play) } - /// Need to refactor - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){ [weak self] in - guard let item = self?.currentItem else { return } - self?.setupStateStatusObserver(for: item, callback: callback) + + update(settings: settings, doUpdate: true){ [weak self] item in + /// Need to refactor + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){ + self?.setupStateStatusObserver(for: item, callback: callback) + } } + return } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 906475b..e055089 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -156,18 +156,27 @@ internal extension ExtPlayerProtocol { #endif } - /// Updates the player with a new asset and applies specified video settings. - /// Initializes playback or performs a specified action once the asset is ready. + /// Updates the player with a new asset and applies the specified video settings. /// - /// This method sets a new `AVURLAsset` to be played based on the provided settings. - /// It can configure looping and muting options, and automatically starts playback if specified. - /// A callback is executed when the asset transitions to the `.readyToPlay` status, allowing for - /// further actions dependent on the readiness of the asset. + /// This method sets a new `AVURLAsset` for playback and configures it according to the provided settings. + /// It can adjust options such as playback gravity, looping, and muting. If `doUpdate` is `true`, the player is + /// updated immediately with the new asset. The method also provides an optional callback that is executed when + /// the asset transitions to the `.readyToPlay` status, enabling additional actions to be performed once the + /// player item is ready for playback. /// /// - Parameters: - /// - settings: A `VideoSettings` struct containing configurations such as playback gravity, - /// whether to loop the content, and whether to mute the audio. - func update(settings: VideoSettings, doUpdate : Bool = false) { + /// - settings: A `VideoSettings` struct containing configurations such as playback gravity, looping behavior, + /// and whether the audio should be muted. + /// - doUpdate: A `Bool` value indicating whether the player should update immediately with the new asset. + /// Defaults to `false`, meaning the player will not change unless explicitly triggered. + /// - callback: An optional closure that takes an `AVPlayerItem` as its parameter. This is called when the + /// player item transitions to the `.readyToPlay` status, allowing for additional customization + /// or actions once the asset is prepared. + func update( + settings: VideoSettings, + doUpdate : Bool = false, + callback : ((AVPlayerItem) -> Void)? = nil + ) { if doUpdate == false && settings.isEqual(currentSettings){ return @@ -181,6 +190,8 @@ internal extension ExtPlayerProtocol { return } + callback?(newItem) + insert(newItem) if settings.loop { From 8d066aa75f89151dc6c9a1bffa056f86534df72f Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 20 Jan 2025 13:31:41 +0100 Subject: [PATCH 074/209] Update AbstractPlayer.swift --- .../protocol/player/AbstractPlayer.swift | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index f35e9df..b5dbc76 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -181,6 +181,35 @@ extension AbstractPlayer{ player?.insert(item, after: nil) } + /// Creates an `AVPlayerItem` with optional subtitle merging. + /// - Parameters: + /// - asset: The main video asset. + /// - settings: A `VideoSettings` object containing subtitle configuration. + /// - Returns: A new `AVPlayerItem` configured with the merged or original asset. + func createPlayerItem(with settings: VideoSettings) -> AVPlayerItem? { + + guard let asset = assetFor(settings) else{ + delegate?.didReceiveError(.sourceNotFound(settings.name)) + return nil + } + + if let subtitleAsset = subtitlesAssetFor(settings), + let mergedAsset = mergeAssetWithSubtitles(videoAsset: asset, subtitleAsset: subtitleAsset) { + // Create and return a new `AVPlayerItem` using the merged asset + return AVPlayerItem(asset: mergedAsset) + } else { + // Create and return a new `AVPlayerItem` using the original asset + return AVPlayerItem(asset: asset) + } + } + + /// Clear status observer + func clearStatusObserver(){ + guard statusObserver != nil else { return } + statusObserver?.invalidate() + statusObserver = nil + } + /// Sets up an observer for the status of the provided `AVPlayerItem`. /// /// This method observes changes in the status of `newItem` and triggers the provided callback @@ -199,7 +228,7 @@ extension AbstractPlayer{ return } - statusObserver = item.observe(\.status, options: [.new, .initial]) { [weak self] item, change in + statusObserver = item.observe(\.status, options: [.new, .initial, .old]) { [weak self] item, change in print(item.status.rawValue, "status") guard item.status == .readyToPlay || item.status == .failed else { return @@ -212,35 +241,6 @@ extension AbstractPlayer{ } } } - - /// Clear status observer - func clearStatusObserver(){ - guard statusObserver != nil else { return } - statusObserver?.invalidate() - statusObserver = nil - } - - /// Creates an `AVPlayerItem` with optional subtitle merging. - /// - Parameters: - /// - asset: The main video asset. - /// - settings: A `VideoSettings` object containing subtitle configuration. - /// - Returns: A new `AVPlayerItem` configured with the merged or original asset. - func createPlayerItem(with settings: VideoSettings) -> AVPlayerItem? { - - guard let asset = assetFor(settings) else{ - delegate?.didReceiveError(.sourceNotFound(settings.name)) - return nil - } - - if let subtitleAsset = subtitlesAssetFor(settings), - let mergedAsset = mergeAssetWithSubtitles(videoAsset: asset, subtitleAsset: subtitleAsset) { - // Create and return a new `AVPlayerItem` using the merged asset - return AVPlayerItem(asset: mergedAsset) - } else { - // Create and return a new `AVPlayerItem` using the original asset - return AVPlayerItem(asset: asset) - } - } /// Seeks the video to a specific time in the timeline. /// This method adjusts the playback position to the specified time with precise accuracy. From 620df968e25a8af0d0e0b95696f0c81a74e3ab9f Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 20 Jan 2025 15:24:38 +0100 Subject: [PATCH 075/209] update --- .../protocol/player/AbstractPlayer.swift | 86 +++++++++++-------- .../protocol/player/ExtPlayerProtocol.swift | 1 - .../view/player/ios/ExtPlayerUIView.swift | 2 +- 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index b5dbc76..405815e 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -16,8 +16,10 @@ import CoreImage @MainActor public protocol AbstractPlayer: AnyObject { + typealias ItemStatusCallback = (AVPlayerItem.Status) -> Void + /// Observes the status property of the new player item. - var statusObserver: NSKeyValueObservation? { get set } + var itemStatusObserver: NSKeyValueObservation? { get set } /// An optional property that stores the current video settings. /// @@ -205,9 +207,9 @@ extension AbstractPlayer{ /// Clear status observer func clearStatusObserver(){ - guard statusObserver != nil else { return } - statusObserver?.invalidate() - statusObserver = nil + guard itemStatusObserver != nil else { return } + itemStatusObserver?.invalidate() + itemStatusObserver = nil } /// Sets up an observer for the status of the provided `AVPlayerItem`. @@ -219,7 +221,7 @@ extension AbstractPlayer{ /// - Parameters: /// - item: The `AVPlayerItem` whose status is to be observed. /// - callback: A closure that is called when the item's status changes to `.readyToPlay` or `.failed`. - func setupStateStatusObserver(for item: AVPlayerItem, callback : @escaping (AVPlayerItem.Status) -> Void) { + func setupStateStatusObserver(for item: AVPlayerItem, callback : @escaping ItemStatusCallback) { clearStatusObserver() @@ -228,9 +230,10 @@ extension AbstractPlayer{ return } - statusObserver = item.observe(\.status, options: [.new, .initial, .old]) { [weak self] item, change in + itemStatusObserver = item.observe(\.status, options: [.new, .initial, .old]) { [weak self] item, change in print(item.status.rawValue, "status") - guard item.status == .readyToPlay || item.status == .failed else { + + guard [.readyToPlay, .failed].contains(item.status) else { return } @@ -254,42 +257,51 @@ extension AbstractPlayer{ /// Defaults to `false`, meaning playback will remain paused after the seek operation. func seek(to time: Double, play: Bool = false) { guard let player = player, let duration = player.currentItem?.duration else { - guard let settings = currentSettings else{ - delegate?.didSeek(value: false, currentTime: time) - return - } - let callback : (AVPlayerItem.Status) -> Void = { [weak self] status in - guard status == .readyToPlay else { - self?.delegate?.didSeek(value: false, currentTime: time) - return - } - self?.seek(to: time, play: play) - } - - update(settings: settings, doUpdate: true){ [weak self] item in - /// Need to refactor - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){ - self?.setupStateStatusObserver(for: item, callback: callback) - } - } - + onUnavailableDuration(for: time, play: play) return } - - guard let seekTime = getSeekTime(for: time, duration: duration) else{ + + guard let seekTime = getSeekTime(for: time, duration: duration) else { delegate?.didSeek(value: false, currentTime: time) return } + + player.seek(to: seekTime, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] success in + self?.seekCompletion(success: success, autoPlay: play) + } + } + + private func onUnavailableDuration(for time: Double, play: Bool) { + guard let settings = currentSettings else { + delegate?.didSeek(value: false, currentTime: time) + return + } + + let callback: ItemStatusCallback = { [weak self] status in + if status == .readyToPlay { + self?.seek(to: time, play: play) + } else { + self?.delegate?.didSeek(value: false, currentTime: time) + } + } + + update(settings: settings, doUpdate: true) { [weak self] item in + /// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){ + self?.setupStateStatusObserver(for: item, callback: callback) + /// } + } + } - player.seek(to: seekTime){ [weak self] value in - let currentTime = CMTimeGetSeconds(player.currentTime()) - Task { @MainActor in - self?.delegate?.didSeek(value: value, currentTime: currentTime) - if play{ - self?.play() - }else{ - self?.pause() - } + private func seekCompletion(success: Bool, autoPlay: Bool) { + guard let player = player else { return } + let currentTime = CMTimeGetSeconds(player.currentTime()) + + Task { @MainActor in + delegate?.didSeek(value: success, currentTime: currentTime) + if autoPlay { + play() + } else { + pause() } } } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index e055089..02ef9a9 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -191,7 +191,6 @@ internal extension ExtPlayerProtocol { } callback?(newItem) - insert(newItem) if settings.loop { diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index ff9713a..de90408 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -60,7 +60,7 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { internal var volumeObserver: NSKeyValueObservation? /// Observes the status property of the new player item. - internal var statusObserver: NSKeyValueObservation? + internal var itemStatusObserver: NSKeyValueObservation? /// The delegate to be notified about errors encountered by the player. weak var delegate: PlayerDelegateProtocol? From 9ddd4313243614fd03e3ad67f16df26801f05cf9 Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 20 Jan 2025 16:02:18 +0100 Subject: [PATCH 076/209] update --- .../protocol/player/AbstractPlayer.swift | 16 ++++++++-------- .../protocol/player/ExtPlayerProtocol.swift | 5 +++-- .../utils/VideoSettings.swift | 4 ++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 405815e..b0b3e0b 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -230,14 +230,14 @@ extension AbstractPlayer{ return } - itemStatusObserver = item.observe(\.status, options: [.new, .initial, .old]) { [weak self] item, change in - print(item.status.rawValue, "status") + itemStatusObserver = item.observe(\.status, options: [.new, .initial]) { [weak self] observedItem, change in + print(observedItem.status.rawValue, "status") - guard [.readyToPlay, .failed].contains(item.status) else { + guard [.readyToPlay, .failed].contains(observedItem.status) else { return } - callback(item.status) + callback(observedItem.status) Task { @MainActor in self?.clearStatusObserver() @@ -276,7 +276,7 @@ extension AbstractPlayer{ delegate?.didSeek(value: false, currentTime: time) return } - + let callback: ItemStatusCallback = { [weak self] status in if status == .readyToPlay { self?.seek(to: time, play: play) @@ -286,9 +286,9 @@ extension AbstractPlayer{ } update(settings: settings, doUpdate: true) { [weak self] item in - /// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){ - self?.setupStateStatusObserver(for: item, callback: callback) - /// } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){ + self?.setupStateStatusObserver(for: item, callback: callback) + } } } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 02ef9a9..886e83f 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -191,13 +191,14 @@ internal extension ExtPlayerProtocol { } callback?(newItem) + insert(newItem) - if settings.loop { + if settings.loop{ loop() } - if !settings.notAutoPlay { + if !settings.notAutoPlay{ play() } } diff --git a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift index 9b45dd0..532a1b6 100644 --- a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift +++ b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift @@ -107,8 +107,8 @@ public struct VideoSettings: Equatable{ public extension VideoSettings { /// Returns a new instance of VideoSettings with loop set to false and notAutoPlay set to true, keeping other settings unchanged. - var GetSettingsWithNotAutoPlay : VideoSettings { - VideoSettings(name: self.name, ext: self.ext, subtitles: self.subtitles, loop: self.loop, mute: self.mute, notAutoPlay: true, timePublishing: self.timePublishing, gravity: self.gravity, enableVector: self.vector) + var settingsWithAutoPlay : VideoSettings { + VideoSettings(name: self.name, ext: self.ext, subtitles: self.subtitles, loop: self.loop, mute: self.mute, notAutoPlay: false, timePublishing: self.timePublishing, gravity: self.gravity, enableVector: self.vector) } func getAssets()-> AVURLAsset?{ From 347761f435b0a8a5db3b3a8afbcd3c88456a62dc Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 20 Jan 2025 16:28:43 +0100 Subject: [PATCH 077/209] update --- .../protocol/player/AbstractPlayer.swift | 75 +------------------ .../protocol/player/ExtPlayerProtocol.swift | 15 ++-- 2 files changed, 9 insertions(+), 81 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index b0b3e0b..40383e8 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -16,8 +16,6 @@ import CoreImage @MainActor public protocol AbstractPlayer: AnyObject { - typealias ItemStatusCallback = (AVPlayerItem.Status) -> Void - /// Observes the status property of the new player item. var itemStatusObserver: NSKeyValueObservation? { get set } @@ -112,7 +110,7 @@ public protocol AbstractPlayer: AnyObject { func applyVideoComposition() /// Updates the current playback asset, settings, and initializes playback or a specific action when the asset is ready. - func update(settings: VideoSettings, doUpdate : Bool, callback : ((AVPlayerItem) -> Void)?) + func update(settings: VideoSettings, doUpdate : Bool) } extension AbstractPlayer{ @@ -204,46 +202,6 @@ extension AbstractPlayer{ return AVPlayerItem(asset: asset) } } - - /// Clear status observer - func clearStatusObserver(){ - guard itemStatusObserver != nil else { return } - itemStatusObserver?.invalidate() - itemStatusObserver = nil - } - - /// Sets up an observer for the status of the provided `AVPlayerItem`. - /// - /// This method observes changes in the status of `newItem` and triggers the provided callback - /// whenever the status changes to `.readyToPlay` or `.failed`. Once the callback is invoked, - /// the observer is invalidated, ensuring that the callback is called only once. - /// - /// - Parameters: - /// - item: The `AVPlayerItem` whose status is to be observed. - /// - callback: A closure that is called when the item's status changes to `.readyToPlay` or `.failed`. - func setupStateStatusObserver(for item: AVPlayerItem, callback : @escaping ItemStatusCallback) { - - clearStatusObserver() - - guard item.status == .unknown else{ - callback(item.status) - return - } - - itemStatusObserver = item.observe(\.status, options: [.new, .initial]) { [weak self] observedItem, change in - print(observedItem.status.rawValue, "status") - - guard [.readyToPlay, .failed].contains(observedItem.status) else { - return - } - - callback(observedItem.status) - - Task { @MainActor in - self?.clearStatusObserver() - } - } - } /// Seeks the video to a specific time in the timeline. /// This method adjusts the playback position to the specified time with precise accuracy. @@ -257,7 +215,7 @@ extension AbstractPlayer{ /// Defaults to `false`, meaning playback will remain paused after the seek operation. func seek(to time: Double, play: Bool = false) { guard let player = player, let duration = player.currentItem?.duration else { - onUnavailableDuration(for: time, play: play) + delegate?.didSeek(value: false, currentTime: time) return } @@ -266,31 +224,10 @@ extension AbstractPlayer{ return } - player.seek(to: seekTime, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] success in + player.seek(to: seekTime) { [weak self] success in self?.seekCompletion(success: success, autoPlay: play) } } - - private func onUnavailableDuration(for time: Double, play: Bool) { - guard let settings = currentSettings else { - delegate?.didSeek(value: false, currentTime: time) - return - } - - let callback: ItemStatusCallback = { [weak self] status in - if status == .readyToPlay { - self?.seek(to: time, play: play) - } else { - self?.delegate?.didSeek(value: false, currentTime: time) - } - } - - update(settings: settings, doUpdate: true) { [weak self] item in - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){ - self?.setupStateStatusObserver(for: item, callback: callback) - } - } - } private func seekCompletion(success: Bool, autoPlay: Bool) { guard let player = player else { return } @@ -298,11 +235,7 @@ extension AbstractPlayer{ Task { @MainActor in delegate?.didSeek(value: success, currentTime: currentTime) - if autoPlay { - play() - } else { - pause() - } + autoPlay ? play() : pause() } } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 886e83f..35f43f0 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -94,7 +94,10 @@ internal extension ExtPlayerProtocol { ) { player.isMuted = settings.mute - + if !settings.loop{ + player.actionAtItemEnd = .pause + } + configurePlayerLayer(player, settings) configureCompositeLayer(settings) configureTimePublishing(player, settings) @@ -174,8 +177,7 @@ internal extension ExtPlayerProtocol { /// or actions once the asset is prepared. func update( settings: VideoSettings, - doUpdate : Bool = false, - callback : ((AVPlayerItem) -> Void)? = nil + doUpdate : Bool = false ) { if doUpdate == false && settings.isEqual(currentSettings){ @@ -190,8 +192,6 @@ internal extension ExtPlayerProtocol { return } - callback?(newItem) - insert(newItem) if settings.loop{ @@ -249,9 +249,6 @@ internal extension ExtPlayerProtocol { self?.delegate?.currentItemWasRemoved() } } - Task { @MainActor in - self?.clearStatusObserver() - } } volumeObserver = player.observe(\.volume, options: [.new, .old]) { [weak self] player, change in @@ -277,8 +274,6 @@ internal extension ExtPlayerProtocol { volumeObserver?.invalidate() volumeObserver = nil - - clearStatusObserver() if let observerToken = timeObserver { player?.removeTimeObserver(observerToken) From d0e9c26fe883a4c764ca15b24a79e2b55e9b33cb Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 20 Jan 2025 16:38:06 +0100 Subject: [PATCH 078/209] update --- .../protocol/player/AbstractPlayer.swift | 10 +++------- .../view/player/ios/ExtPlayerUIView.swift | 3 --- .../view/player/mac/ExtPlayerNSView.swift | 3 --- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 40383e8..ce1614d 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -16,9 +16,6 @@ import CoreImage @MainActor public protocol AbstractPlayer: AnyObject { - /// Observes the status property of the new player item. - var itemStatusObserver: NSKeyValueObservation? { get set } - /// An optional property that stores the current video settings. /// /// This property holds an instance of `VideoSettings` or nil if no settings have been configured yet. @@ -225,18 +222,17 @@ extension AbstractPlayer{ } player.seek(to: seekTime) { [weak self] success in - self?.seekCompletion(success: success, autoPlay: play) + Task { @MainActor in + self?.seekCompletion(success: success, autoPlay: play) + } } } private func seekCompletion(success: Bool, autoPlay: Bool) { guard let player = player else { return } let currentTime = CMTimeGetSeconds(player.currentTime()) - - Task { @MainActor in delegate?.didSeek(value: success, currentTime: currentTime) autoPlay ? play() : pause() - } } /// Seeks to the start of the video. diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index de90408..832cf9d 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -59,9 +59,6 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { /// of an `AVPlayer`. internal var volumeObserver: NSKeyValueObservation? - /// Observes the status property of the new player item. - internal var itemStatusObserver: NSKeyValueObservation? - /// The delegate to be notified about errors encountered by the player. weak var delegate: PlayerDelegateProtocol? diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index d13db59..ba37876 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -60,9 +60,6 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { /// of an `AVPlayer`. internal var volumeObserver: NSKeyValueObservation? - /// Observes the status property of the new player item. - internal var statusObserver: NSKeyValueObservation? - /// The delegate to be notified about errors encountered by the player. weak var delegate: PlayerDelegateProtocol? From 0b061d639197a2135bfcca7449591619a1fb3169 Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 20 Jan 2025 17:02:23 +0100 Subject: [PATCH 079/209] update --- .../protocol/player/AbstractPlayer.swift | 2 +- .../protocol/player/ExtPlayerProtocol.swift | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index ce1614d..7cf3bc0 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -107,7 +107,7 @@ public protocol AbstractPlayer: AnyObject { func applyVideoComposition() /// Updates the current playback asset, settings, and initializes playback or a specific action when the asset is ready. - func update(settings: VideoSettings, doUpdate : Bool) + func update(settings: VideoSettings) } extension AbstractPlayer{ diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 35f43f0..ba53343 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -170,17 +170,9 @@ internal extension ExtPlayerProtocol { /// - Parameters: /// - settings: A `VideoSettings` struct containing configurations such as playback gravity, looping behavior, /// and whether the audio should be muted. - /// - doUpdate: A `Bool` value indicating whether the player should update immediately with the new asset. - /// Defaults to `false`, meaning the player will not change unless explicitly triggered. - /// - callback: An optional closure that takes an `AVPlayerItem` as its parameter. This is called when the - /// player item transitions to the `.readyToPlay` status, allowing for additional customization - /// or actions once the asset is prepared. - func update( - settings: VideoSettings, - doUpdate : Bool = false - ) { + func update(settings: VideoSettings) { - if doUpdate == false && settings.isEqual(currentSettings){ + if settings.isEqual(currentSettings){ return } From 1a2f553f47f9ada4c4264f88e069f18f4381ea9b Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 20 Jan 2025 17:09:44 +0100 Subject: [PATCH 080/209] Update README.md --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e1f5621..caffe3c 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,16 @@ 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: + +- **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** + | **Feature Category** | **Feature Name** | **Description** | |----------------------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------| | **General** | SwiftUI Declarative Syntax | Easily integrate using declarative syntax. | @@ -72,16 +82,6 @@ It is a pure package without any third-party libraries. My main focus was on per | | Codecs | H.264, H.265 (HEVC), MPEG-4, AAC, MP3. | | | Streaming Protocols | HLS (`.m3u8`) support for adaptive streaming. | -## [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: - -- **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** - ### CornerRadius You can reach out the effect simply via mask modifier ```swift From 5e6a85e43b374d4f1eb81c9019f4d1616038bda5 Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 20 Jan 2025 17:10:33 +0100 Subject: [PATCH 081/209] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index caffe3c..12c5672 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ 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** +## Specs + | **Feature Category** | **Feature Name** | **Description** | |----------------------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------| | **General** | SwiftUI Declarative Syntax | Easily integrate using declarative syntax. | From 683f0adc87d58275ae696779d8362f10e2e7beaa Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 20 Jan 2025 17:11:52 +0100 Subject: [PATCH 082/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 12c5672..ce2fdbd 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ You can reach out the effect simply via mask modifier 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. ## Disclaimer on Video Sources like YouTube -Please note that using videos from URLs requires ensuring that you have the right to use and stream these videos. Videos hosted on platforms like YouTube cannot be used directly due to restrictions in their terms of service. Always ensure the video URL is compliant with copyright laws and platform policies. +Please note that using videos from URLs requires ensuring that you have the right to use and stream these videos. Videos hosted on platforms like YouTube cannot be used directly due to restrictions in their terms of service. ## API From ff27a9375c9807d2e65fee614dc4086cfc4b7c01 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 09:11:46 +0100 Subject: [PATCH 083/209] update --- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 21 +++++++ .../protocol/player/AbstractPlayer.swift | 59 +++++++++---------- .../protocol/player/ExtPlayerProtocol.swift | 1 + 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index a0ed4ed..e4201bf 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -240,3 +240,24 @@ func getSeekTime(for time: Double, duration : CMTime) -> CMTime?{ return seekTime } + +/// Creates an `AVPlayerItem` with optional subtitle merging. +/// - Parameters: +/// - asset: The main video asset. +/// - settings: A `VideoSettings` object containing subtitle configuration. +/// - Returns: A new `AVPlayerItem` configured with the merged or original asset. +func createPlayerItem(with settings: VideoSettings) -> AVPlayerItem? { + + guard let asset = assetFor(settings) else{ + return nil + } + + if let subtitleAsset = subtitlesAssetFor(settings), + let mergedAsset = mergeAssetWithSubtitles(videoAsset: asset, subtitleAsset: subtitleAsset) { + // Create and return a new `AVPlayerItem` using the merged asset + return AVPlayerItem(asset: mergedAsset) + } else { + // Create and return a new `AVPlayerItem` using the original asset + return AVPlayerItem(asset: asset) + } +} diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 7cf3bc0..7078cdc 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -31,6 +31,9 @@ public protocol AbstractPlayer: AnyObject { /// The current asset being played, if available. var currentAsset : AVURLAsset? { get } + /// Check if looping is applied + var isLooping : Bool { get } + /// Adjusts the brightness of the video. Default is 0 (no change), with positive values increasing and negative values decreasing brightness. var brightness: Float { get set } @@ -55,6 +58,12 @@ public protocol AbstractPlayer: AnyObject { /// Pauses the current video playback. /// This method should be implemented to pause the video, allowing it to be resumed later from the same position. func pause() + + /// Stop and clean player + func stop() + + /// Inserts a new player item into the media queue of the player. + func insert(_ item : AVPlayerItem) /// Seeks the video to a specific time. /// This method moves the playback position to the specified time with precise accuracy. @@ -81,7 +90,19 @@ public protocol AbstractPlayer: AnyObject { /// - Parameter volume: A `Float` value between 0.0 (mute) and 1.0 (full volume). /// If the value is out of range, it will be clamped to the nearest valid value. func setVolume(_ volume: Float) - + + /// Sets the playback speed for the video playback. + func setPlaybackSpeed(_ speed: Float) + + /// Sets the subtitles for the video playback to a specified language or turns them off. + func setSubtitles(to language: String?) + + /// Enables looping for the current video item. + func loop() + + /// Disables looping for the current video item. + func unloop() + /// Adjusts the brightness of the video playback. /// - Parameter brightness: A `Float` value representing the brightness level. Typically ranges from -1.0 to 1.0. func adjustBrightness(to brightness: Float) @@ -103,9 +124,6 @@ public protocol AbstractPlayer: AnyObject { /// Sets the playback command for the video player. func setCommand(_ value: PlaybackCommand) - /// Applies the current set of filters to the video using an AVVideoComposition. - func applyVideoComposition() - /// Updates the current playback asset, settings, and initializes playback or a specific action when the asset is ready. func update(settings: VideoSettings) } @@ -164,7 +182,7 @@ extension AbstractPlayer{ pause() if !isEmptyQueue() { // Cleaning - if isLooping(){ + if isLooping{ unloop() } @@ -177,28 +195,6 @@ extension AbstractPlayer{ func insert(_ item : AVPlayerItem){ player?.insert(item, after: nil) } - - /// Creates an `AVPlayerItem` with optional subtitle merging. - /// - Parameters: - /// - asset: The main video asset. - /// - settings: A `VideoSettings` object containing subtitle configuration. - /// - Returns: A new `AVPlayerItem` configured with the merged or original asset. - func createPlayerItem(with settings: VideoSettings) -> AVPlayerItem? { - - guard let asset = assetFor(settings) else{ - delegate?.didReceiveError(.sourceNotFound(settings.name)) - return nil - } - - if let subtitleAsset = subtitlesAssetFor(settings), - let mergedAsset = mergeAssetWithSubtitles(videoAsset: asset, subtitleAsset: subtitleAsset) { - // Create and return a new `AVPlayerItem` using the merged asset - return AVPlayerItem(asset: mergedAsset) - } else { - // Create and return a new `AVPlayerItem` using the original asset - return AVPlayerItem(asset: asset) - } - } /// Seeks the video to a specific time in the timeline. /// This method adjusts the playback position to the specified time with precise accuracy. @@ -308,7 +304,7 @@ extension AbstractPlayer{ } /// Check if looping is applied - func isLooping() -> Bool{ + var isLooping : Bool{ playerLooper != nil } @@ -320,7 +316,7 @@ extension AbstractPlayer{ } // Check if the video is already being looped - if isLooping() { + if isLooping { return } @@ -331,7 +327,7 @@ extension AbstractPlayer{ /// This method removes the `playerLooper`, stopping the loop. func unloop() { // Check if the video is not looped (i.e., playerLooper is nil) - guard isLooping() else { + guard isLooping else { return // Not looped, no need to unloop } @@ -373,7 +369,6 @@ extension AbstractPlayer{ filters.append(value) } - /// Removes all applied CIFilters from the video playback. /// /// This function clears the array of filters and optionally re-applies the video composition @@ -397,7 +392,7 @@ extension AbstractPlayer{ /// This method combines the existing filters and brightness/contrast adjustments, creates a new video composition, /// and assigns it to the current AVPlayerItem. The video is paused during this process to ensure smooth application. /// This method is not supported on Vision OS. - func applyVideoComposition() { + private func applyVideoComposition() { guard let player = player else { return } let allFilters = combineFilters(filters, brightness, contrast) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index ba53343..619f7fb 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -181,6 +181,7 @@ internal extension ExtPlayerProtocol { currentSettings = settings guard let newItem = createPlayerItem(with: settings) else{ + delegate?.didReceiveError(.sourceNotFound(settings.name)) return } From 1a395f861815b039e317d79e2292833e39f7c53f Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 09:21:35 +0100 Subject: [PATCH 084/209] update --- .../protocol/player/AbstractPlayer.swift | 24 +++++++++++-------- .../protocol/player/ExtPlayerProtocol.swift | 3 +-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 7078cdc..43d5fb1 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -16,6 +16,8 @@ import CoreImage @MainActor public protocol AbstractPlayer: AnyObject { + // MARK: - Properties + /// An optional property that stores the current video settings. /// /// This property holds an instance of `VideoSettings` or nil if no settings have been configured yet. @@ -25,15 +27,6 @@ public protocol AbstractPlayer: AnyObject { /// The delegate to be notified about errors encountered by the player. var delegate: PlayerDelegateProtocol? { get set } - /// Retrieves the current item being played. - var currentItem : AVPlayerItem? { get } - - /// The current asset being played, if available. - var currentAsset : AVURLAsset? { get } - - /// Check if looping is applied - var isLooping : Bool { get } - /// Adjusts the brightness of the video. Default is 0 (no change), with positive values increasing and negative values decreasing brightness. var brightness: Float { get set } @@ -49,7 +42,18 @@ public protocol AbstractPlayer: AnyObject { /// The queue player that plays the video items. var player: AVQueuePlayer? { get set } - // Playback control methods + // MARK: - Calculated properties + + /// Retrieves the current item being played. + var currentItem : AVPlayerItem? { get } + + /// The current asset being played, if available. + var currentAsset : AVURLAsset? { get } + + /// Check if looping is applied + var isLooping : Bool { get } + + // MARK: - Playback control methods /// Initiates or resumes playback of the video. /// This method should be implemented to start playing the video from its current position. diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 619f7fb..819d77d 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -168,8 +168,7 @@ internal extension ExtPlayerProtocol { /// player item is ready for playback. /// /// - Parameters: - /// - settings: A `VideoSettings` struct containing configurations such as playback gravity, looping behavior, - /// and whether the audio should be muted. + /// - settings: A `VideoSettings` struct containing configurations such as playback gravity, looping behavior, whether the audio should be muted. func update(settings: VideoSettings) { if settings.isEqual(currentSettings){ From 2e927af62a44095c80a5147878cf36c187eab922 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 09:22:32 +0100 Subject: [PATCH 085/209] Update ExtPlayerProtocol.swift --- .../protocol/player/ExtPlayerProtocol.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 819d77d..e263129 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -72,9 +72,7 @@ internal extension ExtPlayerProtocol { /// - Parameters: /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. /// - timePublishing: Optional `CMTime` that specifies a particular time for publishing or triggering an event. - func setupPlayerComponents( - settings: VideoSettings - ) { + func setupPlayerComponents(settings: VideoSettings) { guard let player else { return } @@ -88,10 +86,7 @@ internal extension ExtPlayerProtocol { /// - Parameters: /// - player: The AVQueuePlayer to be configured. /// - settings: The `VideoSettings` struct that includes all necessary configurations like gravity, loop, and mute. - func configurePlayer( - _ player: AVQueuePlayer, - settings: VideoSettings - ) { + func configurePlayer(_ player: AVQueuePlayer, settings: VideoSettings) { player.isMuted = settings.mute if !settings.loop{ From 0b64413972a2ff202aea0eebf01522276ff78a59 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 09:38:36 +0100 Subject: [PATCH 086/209] Update ExtPlayerProtocol.swift --- .../protocol/player/ExtPlayerProtocol.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index e263129..27e13d7 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -175,7 +175,7 @@ internal extension ExtPlayerProtocol { currentSettings = settings guard let newItem = createPlayerItem(with: settings) else{ - delegate?.didReceiveError(.sourceNotFound(settings.name)) + onError(.sourceNotFound(settings.name)) return } @@ -189,6 +189,12 @@ internal extension ExtPlayerProtocol { play() } } + + /// Handles errors + /// - Parameter error: An instance of `VPErrors` representing the error to be handled. + private func onError(_ error : VPErrors){ + delegate?.didReceiveError(error) + } /// Sets up observers on the player item and the player to track their status and error states. /// @@ -199,7 +205,7 @@ internal extension ExtPlayerProtocol { errorObserver = player.observe(\.error, options: [.new]) { [weak self] player, _ in guard let error = player.error else { return } Task { @MainActor in - self?.delegate?.didReceiveError(.remoteVideoError(error)) + self?.onError(.remoteVideoError(error)) } } From f9e62cb7702143db1729db4ea1f046db203dd7f5 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 09:42:04 +0100 Subject: [PATCH 087/209] Update ExtPlayerProtocol.swift --- .../protocol/player/ExtPlayerProtocol.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 27e13d7..9485484 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -27,6 +27,7 @@ public protocol ExtPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ #elseif canImport(AppKit) /// Provides an optional `CALayer` which can be set, and a property to indicate if the layer is wanted, for use within AppKit environments. var layer: CALayer? { get set } + /// WantsLayer is necessary. Otherwise, your NSView will not render the layer-based content at all. var wantsLayer: Bool { get set } #endif From 9b7acdd5b4b664be33ddf0d3726f649eeb0cf6cf Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 10:08:29 +0100 Subject: [PATCH 088/209] update --- .../protocol/vector/VectorLayerProtocol.swift | 16 +++++++++++++++- .../view/player/ios/ExtPlayerUIView.swift | 12 +----------- .../view/player/mac/ExtPlayerNSView.swift | 13 ++----------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift index 3c58af7..57d144b 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/vector/VectorLayerProtocol.swift @@ -21,7 +21,7 @@ import QuartzCore /// @available(iOS 14, macOS 11, tvOS 14, *) @MainActor -public protocol LayerMakerProtocol { +public protocol LayerMakerProtocol: AnyObject { /// The composite layer that contains all the sublayers, including vector layers. /// @@ -51,6 +51,20 @@ public protocol LayerMakerProtocol { extension LayerMakerProtocol{ + /// Adds a composite layer if vector mode is enabled in the provided `VideoSettings`. + @MainActor + func addCompositeLayer(_ settings: VideoSettings) { + if settings.vector { + compositeLayer = CALayer() + } + } + + /// Removes the composite layer from its superlayer and sets `compositeLayer` to `nil`. + @MainActor + func removeCompositeLayer() { + compositeLayer?.removeFromSuperlayer() + compositeLayer = nil + } /// Adds a vector layer to the composite layer using a specified builder. /// diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 832cf9d..b4e7151 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -89,17 +89,7 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { override func layoutSubviews() { super.layoutSubviews() playerLayer?.frame = bounds - } - - private func addCompositeLayer(_ settings : VideoSettings){ - if settings.vector{ - compositeLayer = CALayer() - } - } - - private func removeCompositeLayer() { - compositeLayer?.removeFromSuperlayer() - compositeLayer = nil + compositeLayer?.frame = bounds } func onDisappear(){ diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index ba37876..56a2c17 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -88,18 +88,9 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { override func layout() { super.layout() playerLayer?.frame = bounds + compositeLayer?.frame = bounds } - - private func addCompositeLayer(_ settings : VideoSettings){ - if settings.vector{ - compositeLayer = CALayer() - } - } - - private func removeCompositeLayer() { - compositeLayer?.removeFromSuperlayer() - compositeLayer = nil - } + func onDisappear(){ // First, clear all observers to prevent memory leaks From 6d89524ec5e95783f480ca99672af8cccc799a7f Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 11:34:49 +0100 Subject: [PATCH 089/209] added new Event boundsChanged --- README.md | 3 +++ .../enum/PlayerEvent.swift | 5 +++++ .../helpers/PlayerDelegateProtocol.swift | 5 +++++ .../vector/ShapeLayerBuilderProtocol.swift | 1 - .../view/helpers/PlayerCoordinator.swift | 8 ++++++++ .../view/player/ios/ExtPlayerUIView.swift | 18 +++++++++++++++++- .../view/player/mac/ExtPlayerNSView.swift | 19 ++++++++++++++++++- 7 files changed, 56 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ce2fdbd..8397744 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,8 @@ In cases where you need to re-issue a command that might appear redundant but is |----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| | `addVector(ShapeLayerBuilderProtocol, clear: Bool)` | Command to add a vector graphic layer over the video stream. The `builder` parameter is an instance conforming to `ShapeLayerBuilderProtocol`. The `clear` parameter specifies whether to clear existing vector layers before adding the new one. | | `removeAllVectors` | Command to remove all vector graphic layers from the video stream. | +### Additional Notes on the subtitles Command +The boundsChanged event(`boundsChanged(CGRect)`) is triggered when the main layer’s bounds are updated. This approach is particularly useful when overlays or custom vector layers need to adapt dynamically to changes in video player dimensions or other layout adjustments. To handle the frequent boundsChanged events effectively and improve performance, you can use a **throttle** function to limit how often the updates occur. ### Audio & Language Commands @@ -235,6 +237,7 @@ video_main.m3u8 | `currentItemRemoved` | Occurs when the player's `currentItem` is set to `nil`, indicating that the current media item has been removed from the player. | | `error(VPErrors)` | Represents an occurrence of an error within the player. The event provides a `VPErrors` enum value indicating the specific type of error encountered. | | `volumeChanged` | Happens when the player's volume level is adjusted. This event provides the new volume level, which ranges from 0.0 (muted) to 1.0 (maximum volume). | +| `boundsChanged(CGRect)` | Triggered when the bounds of the main layer change, allowing the developer to recalculate and update all vector layers within the CompositeLayer. | ### Additional Notes on Adding and Removing Vector Graphics diff --git a/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift b/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift index d64da52..451e4cc 100644 --- a/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift +++ b/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift @@ -59,6 +59,9 @@ public enum PlayerEvent: Equatable { /// /// - Parameter VPErrors: The error from the VPErrors enum associated with this case. case error(VPErrors) + + + case boundsChanged(CGRect) } extension PlayerEvent: CustomStringConvertible { @@ -80,6 +83,8 @@ extension PlayerEvent: CustomStringConvertible { return "VolumeChanged" case .error(let e): return "\(e.description)" + case .boundsChanged(let bounds): + return "Bounds changed \(bounds)" } } } diff --git a/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift index 6944eaf..1b94a81 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift @@ -64,4 +64,9 @@ public protocol PlayerDelegateProtocol: AnyObject { /// - Parameter newVolume: The new volume level, expressed as a float between 0.0 (muted) and 1.0 (maximum volume). func volumeDidChange(to newVolume: Float) + /// Notifies that the bounds have changed. + /// + /// - Parameter bounds: The new bounds of the main layer where we keep the video player and all vector layers. This allows a developer to recalculate and update all vector layers that lie in the CompositeLayer. + + func boundsDidChange(to bounds: CGRect) } diff --git a/Sources/swiftui-loop-videoplayer/protocol/vector/ShapeLayerBuilderProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/vector/ShapeLayerBuilderProtocol.swift index fcad618..d3d8eb6 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/vector/ShapeLayerBuilderProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/vector/ShapeLayerBuilderProtocol.swift @@ -28,5 +28,4 @@ public protocol ShapeLayerBuilderProtocol: Identifiable { /// - Returns: A configured `CAShapeLayer`. @MainActor func build(with geometry: (frame: CGRect, bounds: CGRect)) -> CAShapeLayer - } diff --git a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift index 590ea52..cc3001c 100644 --- a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift +++ b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift @@ -111,4 +111,12 @@ internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { func volumeDidChange(to newVolume: Float){ eventPublisher.send(.volumeChanged(newVolume: newVolume)) } + + /// Notifies that the bounds have changed. + /// + /// - Parameter bounds: The new bounds of the main layer where we keep the video player and all vector layers. This allows a developer to recalculate and update all vector layers that lie in the CompositeLayer. + + func boundsDidChange(to bounds: CGRect) { + eventPublisher.send(.boundsChanged(bounds)) + } } diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index b4e7151..1c4dde9 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -89,7 +89,23 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { override func layoutSubviews() { super.layoutSubviews() playerLayer?.frame = bounds - compositeLayer?.frame = bounds + // Update the composite layer (and sublayers) + layoutCompositeLayer() + } + + /// Updates the composite layer and all its sublayers' frames. + public func layoutCompositeLayer() { + guard let compositeLayer = compositeLayer else { return } + + // Update the composite layer's frame to match the parent + compositeLayer.frame = bounds + + // Adjust each sublayer's frame (if they should fill the entire composite layer) + compositeLayer.sublayers?.forEach { sublayer in + sublayer.frame = compositeLayer.bounds + } + + delegate?.boundsDidChange(to: bounds) } func onDisappear(){ diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index 56a2c17..b315fbc 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -16,6 +16,7 @@ import AppKit /// A NSView subclass that loops video using AVFoundation on macOS. /// This class handles the initialization and management of a looping video player with customizable video gravity. +@MainActor internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { /// This property holds an instance of `VideoSettings` @@ -88,7 +89,23 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { override func layout() { super.layout() playerLayer?.frame = bounds - compositeLayer?.frame = bounds + // Update the composite layer (and sublayers) + layoutCompositeLayer() + } + + /// Updates the composite layer and all its sublayers' frames. + public func layoutCompositeLayer() { + guard let compositeLayer = compositeLayer else { return } + + // Update the composite layer's frame to match the parent + compositeLayer.frame = bounds + + // Adjust each sublayer's frame (if they should fill the entire composite layer) + compositeLayer.sublayers?.forEach { sublayer in + sublayer.frame = compositeLayer.bounds + } + + delegate?.boundsDidChange(to: bounds) } From 2b72e0b2afab0c0c769c2cf689183d5d9705b47b Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 11:37:43 +0100 Subject: [PATCH 090/209] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8397744..e0b8276 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,9 @@ In cases where you need to re-issue a command that might appear redundant but is | `addVector(ShapeLayerBuilderProtocol, clear: Bool)` | Command to add a vector graphic layer over the video stream. The `builder` parameter is an instance conforming to `ShapeLayerBuilderProtocol`. The `clear` parameter specifies whether to clear existing vector layers before adding the new one. | | `removeAllVectors` | Command to remove all vector graphic layers from the video stream. | ### Additional Notes on the subtitles Command -The boundsChanged event(`boundsChanged(CGRect)`) is triggered when the main layer’s bounds are updated. This approach is particularly useful when overlays or custom vector layers need to adapt dynamically to changes in video player dimensions or other layout adjustments. To handle the frequent boundsChanged events effectively and improve performance, you can use a **throttle** function to limit how often the updates occur. +1. To use these commands, don’t forget to enable the Vector layer in settings via the EnableVector() setting. +2. The boundsChanged event(`boundsChanged(CGRect)`) is triggered when the main layer’s bounds are updated. This approach is particularly useful when overlays or custom vector layers need to adapt dynamically to changes in video player dimensions or other layout adjustments. To handle the frequent boundsChanged events effectively and improve performance, you can use a **throttle** function to limit how often the updates occur. + ### Audio & Language Commands From 021d181a93d03cd6df8b4e5df0a038255dd440d7 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 11:38:23 +0100 Subject: [PATCH 091/209] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e0b8276..3aa5e70 100644 --- a/README.md +++ b/README.md @@ -190,8 +190,8 @@ In cases where you need to re-issue a command that might appear redundant but is | `addVector(ShapeLayerBuilderProtocol, clear: Bool)` | Command to add a vector graphic layer over the video stream. The `builder` parameter is an instance conforming to `ShapeLayerBuilderProtocol`. The `clear` parameter specifies whether to clear existing vector layers before adding the new one. | | `removeAllVectors` | Command to remove all vector graphic layers from the video stream. | ### Additional Notes on the subtitles Command -1. To use these commands, don’t forget to enable the Vector layer in settings via the EnableVector() setting. -2. The boundsChanged event(`boundsChanged(CGRect)`) is triggered when the main layer’s bounds are updated. This approach is particularly useful when overlays or custom vector layers need to adapt dynamically to changes in video player dimensions or other layout adjustments. To handle the frequent boundsChanged events effectively and improve performance, you can use a **throttle** function to limit how often the updates occur. +- To use these commands, don’t forget to enable the Vector layer in settings via the EnableVector() setting. +- The boundsChanged event(`boundsChanged(CGRect)`) is triggered when the main layer’s bounds are updated. This approach is particularly useful when overlays or custom vector layers need to adapt dynamically to changes in video player dimensions or other layout adjustments. To handle the frequent boundsChanged events effectively and improve performance, you can use a **throttle** function to limit how often the updates occur. ### Audio & Language Commands From 86d7b09421bbd92f614f0438f4fc5dca4916d8f3 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 11:39:16 +0100 Subject: [PATCH 092/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3aa5e70..80df80a 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ In cases where you need to re-issue a command that might appear redundant but is |----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| | `addVector(ShapeLayerBuilderProtocol, clear: Bool)` | Command to add a vector graphic layer over the video stream. The `builder` parameter is an instance conforming to `ShapeLayerBuilderProtocol`. The `clear` parameter specifies whether to clear existing vector layers before adding the new one. | | `removeAllVectors` | Command to remove all vector graphic layers from the video stream. | -### Additional Notes on the subtitles Command +### Additional Notes on Vector Graphics Commands - To use these commands, don’t forget to enable the Vector layer in settings via the EnableVector() setting. - The boundsChanged event(`boundsChanged(CGRect)`) is triggered when the main layer’s bounds are updated. This approach is particularly useful when overlays or custom vector layers need to adapt dynamically to changes in video player dimensions or other layout adjustments. To handle the frequent boundsChanged events effectively and improve performance, you can use a **throttle** function to limit how often the updates occur. From 155d07130721eabf90e1d7645a62d69d0237fec9 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 11:40:54 +0100 Subject: [PATCH 093/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 80df80a..a87b5fa 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 | -*Additional Notes on Settings* +### Additional Notes on Settings - **Time Publishing:** If the parameter is passed during initialization, the player will publish the time according to the input settings. You can pass just `TimePublishing` without any value to use the default interval of 1 second, or you can pass a specific `CMTime` value to set a custom interval. | 1 second (CMTime with 1 second and preferred timescale of 600) If no `TimePublishing` is provided, the player will not emit time events, which can improve performance when timing information is not needed. From 7f02e9957db9cc54c9ca819c1ad2a040a3fca00c Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 12:45:43 +0100 Subject: [PATCH 094/209] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index a87b5fa..3331ef9 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,10 @@ ## 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. 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. +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.* -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) From be93d12affb4ed2dc52e36de4f3ecded1aead671 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 12:46:42 +0100 Subject: [PATCH 095/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3331ef9..44b6819 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![](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. 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. 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**. From 6d82e977a1b9c005606a636cf4ce682d214a5996 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 16:44:40 +0100 Subject: [PATCH 096/209] added Picture-in-Picture support --- .../enum/PlaybackCommand.swift | 13 +++++++-- .../enum/PlayerEvent.swift | 8 +++++ .../enum/Setting.swift | 3 ++ .../enum/VPErrors.swift | 6 ++++ .../helpers/PlayerDelegateProtocol.swift | 11 ++++++- .../protocol/player/AbstractPlayer.swift | 26 +++++++++++++++++ .../protocol/player/ExtPlayerProtocol.swift | 10 +++++-- .../settings/PictureInPicture .swift | 25 ++++++++++++++++ .../utils/VideoSettings.swift | 10 +++++-- .../view/helpers/PlayerCoordinator.swift | 21 ++++++++++++++ .../view/player/ios/ExtPlayerUIView.swift | 29 +++++++++++++++++-- .../player/main/ExtPlayerMultiPlatform.swift | 5 ++++ 12 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 Sources/swiftui-loop-videoplayer/settings/PictureInPicture .swift diff --git a/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift b/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift index d1e8bb2..4d1ec54 100644 --- a/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift +++ b/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift @@ -95,14 +95,23 @@ public enum PlaybackCommand: Equatable { /// Command to select a specific audio track based on language code. /// - Parameter languageCode: The language code (e.g., "en" for English) of the desired audio track. case audioTrack(languageCode: String) - + + #if os(iOS) + case startPiP + + case stopPiP + #endif + public static func == (lhs: PlaybackCommand, rhs: PlaybackCommand) -> Bool { switch (lhs, rhs) { case (.idle, .idle), (.play, .play), (.pause, .pause), (.begin, .begin), (.end, .end), (.mute, .mute), (.unmute, .unmute), (.loop, .loop), (.unloop, .unloop), (.removeAllFilters, .removeAllFilters), (.removeAllVectors, .removeAllVectors): return true - + #if os(iOS) + case (.startPiP, .startPiP), (.stopPiP, .stopPiP): + return true + #endif case (.seek(let lhsTime, let lhsPlay), .seek(let rhsTime, let rhsPlay)): return lhsTime == rhsTime && lhsPlay == rhsPlay diff --git a/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift b/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift index 451e4cc..1f81baf 100644 --- a/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift +++ b/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift @@ -62,6 +62,10 @@ public enum PlayerEvent: Equatable { case boundsChanged(CGRect) + + case startedPiP + + case stoppedPiP } extension PlayerEvent: CustomStringConvertible { @@ -85,6 +89,10 @@ extension PlayerEvent: CustomStringConvertible { return "\(e.description)" case .boundsChanged(let bounds): return "Bounds changed \(bounds)" + case .startedPiP: + return "Started PiP" + case .stoppedPiP: + return "Stopped PiP" } } } diff --git a/Sources/swiftui-loop-videoplayer/enum/Setting.swift b/Sources/swiftui-loop-videoplayer/enum/Setting.swift index 21dfa84..d4c57a0 100644 --- a/Sources/swiftui-loop-videoplayer/enum/Setting.swift +++ b/Sources/swiftui-loop-videoplayer/enum/Setting.swift @@ -42,6 +42,9 @@ public enum Setting: Equatable, SettingsConvertible{ /// Subtitles case subtitles(String) + /// Support Picture-in-Picture + 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. case timePublishing(CMTime) diff --git a/Sources/swiftui-loop-videoplayer/enum/VPErrors.swift b/Sources/swiftui-loop-videoplayer/enum/VPErrors.swift index b49b314..32545a3 100644 --- a/Sources/swiftui-loop-videoplayer/enum/VPErrors.swift +++ b/Sources/swiftui-loop-videoplayer/enum/VPErrors.swift @@ -22,6 +22,9 @@ public enum VPErrors: Error, CustomStringConvertible, Sendable{ /// Error case for when settings are not unique. case settingsNotUnique + /// Picture-in-Picture (PiP) is not supported + case notSupportedPiP + /// A description of the error, suitable for display. public var description: String { switch self { @@ -30,6 +33,9 @@ public enum VPErrors: Error, CustomStringConvertible, Sendable{ case .sourceNotFound(let name): return "Source not found: \(name)" + case .notSupportedPiP: + return "Picture-in-Picture (PiP) is not supported on this device." + /// Returns a description indicating that the settings are not unique. case .settingsNotUnique: return "Settings are not unique" diff --git a/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift index 1b94a81..9716a85 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift @@ -7,13 +7,16 @@ import Foundation import AVFoundation +#if os(iOS) +import AVKit +#endif /// Protocol to handle player-related errors. /// /// Conforming to this protocol allows a class to respond to error events that occur within a media player context. @available(iOS 14, macOS 11, tvOS 14, *) @MainActor -public protocol PlayerDelegateProtocol: AnyObject { +public protocol PlayerDelegateProtocol: AnyObject{ /// Called when an error is encountered within the media player. /// /// This method provides a way for delegate objects to respond to error conditions, allowing them to handle or @@ -69,4 +72,10 @@ public protocol PlayerDelegateProtocol: AnyObject { /// - Parameter bounds: The new bounds of the main layer where we keep the video player and all vector layers. This allows a developer to recalculate and update all vector layers that lie in the CompositeLayer. func boundsDidChange(to bounds: CGRect) + +#if os(iOS) + func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) + + func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) +#endif } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 43d5fb1..684e3d3 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -8,6 +8,7 @@ import AVFoundation #if canImport(CoreImage) import CoreImage +import AVKit #endif /// Defines an abstract player protocol to be implemented by player objects, ensuring main-thread safety and compatibility with specific OS versions. @@ -18,6 +19,10 @@ public protocol AbstractPlayer: AnyObject { // MARK: - Properties + #if os(iOS) + var pipController: AVPictureInPictureController? { get set } + #endif + /// An optional property that stores the current video settings. /// /// This property holds an instance of `VideoSettings` or nil if no settings have been configured yet. @@ -445,4 +450,25 @@ extension AbstractPlayer{ } #endif } + + #if os(iOS) + func startPiP() { + guard let pipController = pipController else { return } + + if !pipController.isPictureInPictureActive { + pipController.startPictureInPicture() + + } + } + + func stopPiP() { + guard let pipController = pipController else { return } + + if pipController.isPictureInPictureActive { + // Stop PiP + pipController.stopPictureInPicture() + } + } + + #endif } diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 9485484..c8195ee 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -63,7 +63,9 @@ public protocol ExtPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ /// - item: The AVPlayerItem to observe for status changes. /// - player: The AVQueuePlayer to observe for errors. func setupObservers(for player: AVQueuePlayer) - + + /// Handles errors + func onError(_ error : VPErrors) } internal extension ExtPlayerProtocol { @@ -193,7 +195,7 @@ internal extension ExtPlayerProtocol { /// Handles errors /// - Parameter error: An instance of `VPErrors` representing the error to be handled. - private func onError(_ error : VPErrors){ + func onError(_ error : VPErrors){ delegate?.didReceiveError(error) } @@ -351,6 +353,10 @@ internal extension ExtPlayerProtocol { addVectorLayer(builder: builder, clear: clear) case .removeAllVectors: removeAllVectors() + #if os(iOS) + case .startPiP: startPiP() + case .stopPiP: stopPiP() + #endif default : return } } diff --git a/Sources/swiftui-loop-videoplayer/settings/PictureInPicture .swift b/Sources/swiftui-loop-videoplayer/settings/PictureInPicture .swift new file mode 100644 index 0000000..356c07b --- /dev/null +++ b/Sources/swiftui-loop-videoplayer/settings/PictureInPicture .swift @@ -0,0 +1,25 @@ +// +// PictureInPicture .swift +// +// +// Created by Igor Shelopaev on 21.01.25. +// + +import Foundation + + +/// Represents a settings structure that enables looping functionality, conforming to `SettingsConvertible`. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, *) +public struct PictureInPicture : SettingsConvertible{ + + // MARK: - Life circle + + /// Initializes a new instance + public init() {} + + /// Fetch settings + @_spi(Private) + public func asSettings() -> [Setting] { + [.pictureInPicture] + } +} diff --git a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift index 532a1b6..61293cd 100644 --- a/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift +++ b/Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift @@ -26,6 +26,9 @@ public struct VideoSettings: Equatable{ /// Loop video public let loop: Bool + /// Loop video + public let pictureInPicture: Bool + /// Mute video public let mute: Bool @@ -63,11 +66,12 @@ public struct VideoSettings: Equatable{ /// - 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. - public init(name: String, ext: String, subtitles: String, loop: 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) { self.name = name self.ext = ext self.subtitles = subtitles self.loop = loop + self.pictureInPicture = pictureInPicture self.mute = mute self.notAutoPlay = notAutoPlay self.timePublishing = timePublishing @@ -95,6 +99,8 @@ public struct VideoSettings: Equatable{ loop = settings.contains(.loop) + pictureInPicture = settings.contains(.pictureInPicture) + mute = settings.contains(.mute) notAutoPlay = settings.contains(.notAutoPlay) @@ -108,7 +114,7 @@ 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, mute: self.mute, notAutoPlay: false, timePublishing: self.timePublishing, gravity: self.gravity, enableVector: self.vector) + 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?{ diff --git a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift index cc3001c..2a7c632 100644 --- a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift +++ b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift @@ -8,6 +8,9 @@ import SwiftUI import Combine import AVFoundation +#if os(iOS) +import AVKit +#endif @MainActor internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { @@ -119,4 +122,22 @@ internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { func boundsDidChange(to bounds: CGRect) { eventPublisher.send(.boundsChanged(bounds)) } + +} + +#if os(iOS) +extension PlayerCoordinator: AVPictureInPictureControllerDelegate{ + + nonisolated func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + Task{ @MainActor in + eventPublisher.send(.startedPiP) + } + } + + nonisolated func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + Task{ @MainActor in + eventPublisher.send(.stoppedPiP) + } + } } +#endif diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 1c4dde9..9ce1da1 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -15,7 +15,7 @@ import AVKit import UIKit @MainActor -internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { +internal class ExtPlayerUIView: UIView, ExtPlayerProtocol{ /// This property holds an instance of `VideoSettings` internal var currentSettings : VideoSettings? @@ -62,6 +62,8 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { /// The delegate to be notified about errors encountered by the player. weak var delegate: PlayerDelegateProtocol? + internal var pipController: AVPictureInPictureController? + /// Initializes a new player view with a video asset and custom settings. /// /// - Parameters: @@ -75,10 +77,11 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { addPlayerLayer() addCompositeLayer(settings) - + setupPlayerComponents( settings: settings ) + } required init?(coder: NSCoder) { @@ -93,6 +96,8 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { layoutCompositeLayer() } + + /// Updates the composite layer and all its sublayers' frames. public func layoutCompositeLayer() { guard let compositeLayer = compositeLayer else { return } @@ -128,5 +133,25 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol { print("Player deinitialized and resources cleaned up.") #endif } + + #if os(iOS) + /// Called by the Coordinator to set up PiP + 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) + } + return + } + + guard let playerLayer else{ + return + } + + pipController = AVPictureInPictureController(playerLayer: playerLayer) + pipController?.delegate = delegate + } + #endif } #endif diff --git a/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift b/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift index fc68193..6f5de91 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/main/ExtPlayerMultiPlatform.swift @@ -79,6 +79,11 @@ extension ExtPlayerMultiPlatform: UIViewRepresentable{ if let player: PlayerView = makePlayerView(container){ player.delegate = context.coordinator + #if os(iOS) + if settings.pictureInPicture{ + player.setupPiP(delegate: context.coordinator) + } + #endif } return container From 9c454669d05bc935e28fc48246930e2f0dc22b31 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 17:27:26 +0100 Subject: [PATCH 097/209] update --- README.md | 6 +++++- .../swiftui-loop-videoplayer/enum/PlaybackCommand.swift | 3 +++ Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift | 9 ++++++--- .../settings/PictureInPicture .swift | 4 ++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 44b6819..0a862e6 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ The player's functionality is designed around a dual ⇆ interaction model: | | Error Handling | Customizable error messages and visual displays. | | | Subtitle Support | Add external `.vtt` files or use embedded subtitle tracks. | | | Custom Overlays | Add vector graphics and custom overlays over the video. | +| | Picture In Picture (PiP) | Picture-in-Picture (PiP) is supported on iOS and iPadOS | | **Playback Commands** | Idle Command | Initialize without specific playback actions. | | | Play/Pause | Control playback state. | | | Seek Command | Move to specific video timestamps. | @@ -125,13 +126,14 @@ Please note that using videos from URLs requires ensuring that you have the righ |---------------|-----------------------------------------------------------------------------------------------------|---------| | **SourceName** | The URL or local filename of the video. | - | | **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* | - | | **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. | - | | **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 | | **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* | ### Additional Notes on Settings @@ -172,6 +174,8 @@ In cases where you need to re-issue a command that might appear redundant but is | `playbackSpeed(Float)` | Command to adjust the playback speed of the video. The `speed` parameter is a `Float` value representing the playback speed (e.g., 1.0 for normal speed, 0.5 for half speed, 2.0 for double speed). If a negative value is passed, it will be clamped to 0.0. | | `loop` | Command to enable looping of the video playback. By default, looping is enabled, so this command will have no effect if looping is already active. | | `unloop` | Command to disable looping of the video playback. This command will only take effect if the video is currently being looped. | +| `startPiP` | Command to initiate **Picture-in-Picture (PiP)** mode for video playback. If the PiP feature is already active, this command will have no additional effect. | +| `stopPiP` | Command to terminate **Picture-in-Picture (PiP)** mode, returning the video playback to its inline view. If PiP is not active, this command will have no effect. | ### Visual Adjustment Commands diff --git a/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift b/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift index 4d1ec54..1e8dc80 100644 --- a/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift +++ b/Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift @@ -97,9 +97,12 @@ public enum PlaybackCommand: Equatable { case audioTrack(languageCode: String) #if os(iOS) + /// Command to initiate Picture-in-Picture (PiP) mode for video playback. If the PiP feature is already active, this command will have no additional effect. case startPiP + /// Command to terminate Picture-in-Picture (PiP) mode, returning the video playback to its inline view. If PiP is not active, this command will have no effect. case stopPiP + #endif public static func == (lhs: PlaybackCommand, rhs: PlaybackCommand) -> Bool { diff --git a/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift b/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift index 1f81baf..1768941 100644 --- a/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift +++ b/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift @@ -60,11 +60,14 @@ public enum PlayerEvent: Equatable { /// - Parameter VPErrors: The error from the VPErrors enum associated with this case. case error(VPErrors) - + /// Event triggered when the bounds of the video player change. + /// - Parameter CGRect: The new bounds of the video player. case boundsChanged(CGRect) - + + /// Event triggered when Picture-in-Picture (PiP) mode starts. case startedPiP - + + /// Event triggered when Picture-in-Picture (PiP) mode stops. case stoppedPiP } diff --git a/Sources/swiftui-loop-videoplayer/settings/PictureInPicture .swift b/Sources/swiftui-loop-videoplayer/settings/PictureInPicture .swift index 356c07b..c98b49a 100644 --- a/Sources/swiftui-loop-videoplayer/settings/PictureInPicture .swift +++ b/Sources/swiftui-loop-videoplayer/settings/PictureInPicture .swift @@ -1,5 +1,5 @@ // -// PictureInPicture .swift +// PictureInPicture.swift // // // Created by Igor Shelopaev on 21.01.25. @@ -8,7 +8,7 @@ import Foundation -/// Represents a settings structure that enables looping functionality, conforming to `SettingsConvertible`. +/// Represents a PictureInPicture functionality, conforming to `SettingsConvertible`. @available(iOS 14.0, macOS 11.0, tvOS 14.0, *) public struct PictureInPicture : SettingsConvertible{ From 25d8f28e1fdcafa36db1a746f506edec9203d128 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 17:29:23 +0100 Subject: [PATCH 098/209] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0a862e6..9c5f72b 100644 --- a/README.md +++ b/README.md @@ -174,8 +174,8 @@ In cases where you need to re-issue a command that might appear redundant but is | `playbackSpeed(Float)` | Command to adjust the playback speed of the video. The `speed` parameter is a `Float` value representing the playback speed (e.g., 1.0 for normal speed, 0.5 for half speed, 2.0 for double speed). If a negative value is passed, it will be clamped to 0.0. | | `loop` | Command to enable looping of the video playback. By default, looping is enabled, so this command will have no effect if looping is already active. | | `unloop` | Command to disable looping of the video playback. This command will only take effect if the video is currently being looped. | -| `startPiP` | Command to initiate **Picture-in-Picture (PiP)** mode for video playback. If the PiP feature is already active, this command will have no additional effect. | -| `stopPiP` | Command to terminate **Picture-in-Picture (PiP)** mode, returning the video playback to its inline view. If PiP is not active, this command will have no effect. | +| `startPiP` | Command to initiate **Picture-in-Picture (PiP)** mode for video playback. If the PiP feature is already active, this command will have no additional effect. Don't forget to add PictureInPicture() in settings to enable the PiP feature. | +| `stopPiP` | Command to terminate **Picture-in-Picture (PiP)** mode, returning the video playback to its inline view. If PiP is not active, this command will have no effect. Don't forget to add PictureInPicture() in settings to enable the PiP feature. | ### Visual Adjustment Commands From 06f075d9e992696836f1095fd56f73594a720d37 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 21 Jan 2025 17:29:51 +0100 Subject: [PATCH 099/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c5f72b..17517ba 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* | +|**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* | ### Additional Notes on Settings From b10236aff3aa9e86664a55e77353930586990199 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 22 Jan 2025 16:09:18 +0100 Subject: [PATCH 100/209] added support for Local File URL --- README.md | 2 +- Sources/swiftui-loop-videoplayer/fn/fn+.swift | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 17517ba..e6b8c8f 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** | The URL or local filename of the video. | - | +| **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 | diff --git a/Sources/swiftui-loop-videoplayer/fn/fn+.swift b/Sources/swiftui-loop-videoplayer/fn/fn+.swift index e4201bf..dd287fe 100644 --- a/Sources/swiftui-loop-videoplayer/fn/fn+.swift +++ b/Sources/swiftui-loop-videoplayer/fn/fn+.swift @@ -48,6 +48,10 @@ fileprivate func assetFrom(name: String, fileExtension: String?) -> AVURLAsset? return AVURLAsset(url: url) } + if let url = fileURL(from: name){ + return AVURLAsset(url: url) + } + // If not a valid URL, try to locate the file in the main bundle with the specified extension. if let fileExtension = fileExtension, let fileUrl = Bundle.main.url(forResource: name, withExtension: fileExtension) { @@ -58,6 +62,27 @@ fileprivate func assetFrom(name: String, fileExtension: String?) -> AVURLAsset? return nil } + +/// 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? { + guard rawString.hasPrefix("file://") else { + // Not a file URL scheme + return nil + } + // Strip off "file://" + let pathIndex = rawString.index(rawString.startIndex, offsetBy: 7) + let pathPortion = rawString[pathIndex...] + + guard let encodedPath = pathPortion + .addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) + else { return nil } + + let finalString = "file://\(encodedPath)" + return URL(string: finalString) +} + /// Checks whether a given filename contains an extension and returns the extension if it exists. /// /// - Parameter name: The filename to check. From 923f3f44c1253064feae77a0cd03e830fd95a060 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 22 Jan 2025 16:23:53 +0100 Subject: [PATCH 101/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e6b8c8f..6a6f1aa 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* | +|**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.| ### Additional Notes on Settings From 9d66fc5ddf2ae270207f304cdb04c7fc3f00a5be Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 6 Feb 2025 16:04:05 +0100 Subject: [PATCH 102/209] update --- README.md | 4 ++ .../enum/PlayerEvent.swift | 20 ++++++++++ .../enum/VPErrors.swift | 20 +++++----- .../helpers/PlayerDelegateProtocol.swift | 13 +++++++ .../protocol/player/ExtPlayerProtocol.swift | 39 ++++++++++++++++++- .../view/helpers/PlayerCoordinator.swift | 18 ++++++++- .../view/player/ios/ExtPlayerUIView.swift | 4 ++ .../view/player/mac/ExtPlayerNSView.swift | 3 ++ 8 files changed, 108 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6a6f1aa..0966f96 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,10 @@ video_main.m3u8 | `error(VPErrors)` | Represents an occurrence of an error within the player. The event provides a `VPErrors` enum value indicating the specific type of error encountered. | | `volumeChanged` | Happens when the player's volume level is adjusted. This event provides the new volume level, which ranges from 0.0 (muted) to 1.0 (maximum volume). | | `boundsChanged(CGRect)` | Triggered when the bounds of the main layer change, allowing the developer to recalculate and update all vector layers within the CompositeLayer. | +| `startedPiP` | Event triggered when Picture-in-Picture (PiP) mode starts. | +| `stoppedPiP` | Event triggered when Picture-in-Picture (PiP) mode stops. | +| `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 Adding and Removing Vector Graphics diff --git a/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift b/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift index 1768941..bde483c 100644 --- a/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift +++ b/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift @@ -69,6 +69,14 @@ public enum PlayerEvent: Equatable { /// Event triggered when Picture-in-Picture (PiP) mode stops. case stoppedPiP + + /// Indicates that the AVPlayerItem's status has changed. + /// - Parameter status: The new status of the AVPlayerItem. + case itemStatusChanged(AVPlayerItem.Status) + + /// Provides the duration of the AVPlayerItem when it is ready to play. + /// - Parameter duration: The total duration of the media item in `CMTime`. + case duration(CMTime) } extension PlayerEvent: CustomStringConvertible { @@ -96,6 +104,18 @@ extension PlayerEvent: CustomStringConvertible { return "Started PiP" case .stoppedPiP: return "Stopped PiP" + case .itemStatusChanged(let status): + switch status { + case .unknown: + return "Status: Unknown" + case .readyToPlay: + return "Status: ReadyToPlay" + case .failed: + return "Status: FailedToLoad" + } + case .duration(let value): + let roundedString = String(format: "%.0f", value.seconds) + return "Duration \(roundedString) sec" } } } diff --git a/Sources/swiftui-loop-videoplayer/enum/VPErrors.swift b/Sources/swiftui-loop-videoplayer/enum/VPErrors.swift index 32545a3..a350354 100644 --- a/Sources/swiftui-loop-videoplayer/enum/VPErrors.swift +++ b/Sources/swiftui-loop-videoplayer/enum/VPErrors.swift @@ -9,7 +9,7 @@ import Foundation @available(iOS 14.0, macOS 11.0, tvOS 14.0, *) /// An enumeration of possible errors that can occur in the video player. -public enum VPErrors: Error, CustomStringConvertible, Sendable{ +public enum VPErrors: Error, CustomStringConvertible, Sendable { /// Error case for when there is an error with remote video playback. /// - Parameter error: The error that occurred during remote video playback. @@ -25,25 +25,26 @@ public enum VPErrors: Error, CustomStringConvertible, Sendable{ /// Picture-in-Picture (PiP) is not supported case notSupportedPiP + /// Failed to load + case failedToLoad + /// A description of the error, suitable for display. public var description: String { switch self { - /// Returns a description indicating that the specified file was not found. - /// - Parameter name: The name of the file that was not found. case .sourceNotFound(let name): return "Source not found: \(name)" case .notSupportedPiP: return "Picture-in-Picture (PiP) is not supported on this device." - /// Returns a description indicating that the settings are not unique. case .settingsNotUnique: return "Settings are not unique" - /// Returns a description indicating a playback error with the remote video. - /// - Parameter error: The error that occurred during remote video playback. case .remoteVideoError(let error): return "Playback error: \(String(describing: error?.localizedDescription))" + + case .failedToLoad: + return "Failed to load the video." } } } @@ -52,10 +53,6 @@ public enum VPErrors: Error, CustomStringConvertible, Sendable{ extension VPErrors: Equatable { /// Compares two `VPErrors` instances for equality based on specific error conditions. - /// - Parameters: - /// - lhs: The left-hand side `VPErrors` instance to compare. - /// - rhs: The right-hand side `VPErrors` instance to compare. - /// - Returns: A Boolean value indicating whether the two `VPErrors` instances are considered equal. public static func ==(lhs: VPErrors, rhs: VPErrors) -> Bool { switch (lhs, rhs) { case (.remoteVideoError(let a), .remoteVideoError(let b)): @@ -64,9 +61,10 @@ extension VPErrors: Equatable { return a == b case (.settingsNotUnique, .settingsNotUnique): return true + case (.failedToLoad, .failedToLoad): + return true default: return false } } } - diff --git a/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift index 9716a85..816451f 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift @@ -73,6 +73,19 @@ public protocol PlayerDelegateProtocol: AnyObject{ func boundsDidChange(to bounds: CGRect) + /// Called when the AVPlayerItem's status changes. + /// - Parameter status: The new status of the AVPlayerItem. + /// - `.unknown`: The item is still loading or its status is not yet determined. + /// - `.readyToPlay`: The item is fully loaded and ready to play. + /// - `.failed`: The item failed to load due to an error. + func itemStatusChanged(_ status: AVPlayerItem.Status) + + /// Called when the duration of the AVPlayerItem is available. + /// - Parameter time: The total duration of the media item in `CMTime`. + /// - This method is only called when the item reaches `.readyToPlay`, + /// ensuring that the duration value is valid. + func duration(_ time: CMTime) + #if os(iOS) func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index c8195ee..9851566 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -44,6 +44,9 @@ public protocol ExtPlayerProtocol: AbstractPlayer, LayerMakerProtocol{ /// An optional observer for monitoring changes to the player's `currentItem` property. var currentItemObserver: NSKeyValueObservation? { get set } + + /// Item status observer + var itemStatusObserver: NSKeyValueObservation? { get set } /// An optional observer for monitoring changes to the player's `volume` property. var volumeObserver: NSKeyValueObservation? { get set } @@ -182,6 +185,7 @@ internal extension ExtPlayerProtocol { return } + observeItemStatus(newItem) insert(newItem) if settings.loop{ @@ -198,6 +202,37 @@ internal extension ExtPlayerProtocol { func onError(_ error : VPErrors){ delegate?.didReceiveError(error) } + + /// 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) { + + removeItemObserver() + + itemStatusObserver = item.observe(\.status, options: [.new, .initial]) { [weak self] item, _ in + Task { @MainActor in + self?.delegate?.itemStatusChanged(item.status) + } + + switch item.status { + case .unknown: break + case .readyToPlay: + Task { @MainActor in + self?.delegate?.duration(item.duration) + } + case .failed: + Task { @MainActor in + self?.onError(.failedToLoad) + } + } + } + } + + /// Removes the current AVPlayerItem observer, if any, to prevent memory leaks. + private func removeItemObserver() { + itemStatusObserver?.invalidate() + itemStatusObserver = nil + } /// Sets up observers on the player item and the player to track their status and error states. /// @@ -234,7 +269,7 @@ internal extension ExtPlayerProtocol { } } - currentItemObserver = player.observe(\.currentItem, options: [.new, .old]) { [weak self] player, change in + currentItemObserver = player.observe(\.currentItem, options: [.new, .old, .initial]) { [weak self] player, change in // Detecting when the current item is changed if let newItem = change.newValue as? AVPlayerItem { Task { @MainActor in @@ -259,6 +294,8 @@ internal extension ExtPlayerProtocol { /// Clear observers func clearObservers(){ + removeItemObserver() + errorObserver?.invalidate() errorObserver = nil diff --git a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift index 2a7c632..f567527 100644 --- a/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift +++ b/Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift @@ -118,10 +118,26 @@ internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol { /// Notifies that the bounds have changed. /// /// - Parameter bounds: The new bounds of the main layer where we keep the video player and all vector layers. This allows a developer to recalculate and update all vector layers that lie in the CompositeLayer. - func boundsDidChange(to bounds: CGRect) { eventPublisher.send(.boundsChanged(bounds)) } + + /// Called when the AVPlayerItem's status changes. + /// - Parameter status: The new status of the AVPlayerItem. + /// - `.unknown`: The item is still loading or its status is not yet determined. + /// - `.readyToPlay`: The item is fully loaded and ready to play. + /// - `.failed`: The item failed to load due to an error. + func itemStatusChanged(_ status: AVPlayerItem.Status) { + eventPublisher.send(.itemStatusChanged(status)) + } + + /// Called when the duration of the AVPlayerItem is available. + /// - Parameter time: The total duration of the media item in `CMTime`. + /// - This method is only called when the item reaches `.readyToPlay`, + /// ensuring that the duration value is valid. + func duration(_ time: CMTime) { + eventPublisher.send(.duration(time)) + } } diff --git a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift index 9ce1da1..2373e13 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/ios/ExtPlayerUIView.swift @@ -53,6 +53,9 @@ internal class ExtPlayerUIView: UIView, ExtPlayerProtocol{ /// An optional observer for monitoring changes to the player's `currentItem` property. internal var currentItemObserver: NSKeyValueObservation? + /// Item status observer + internal var itemStatusObserver: NSKeyValueObservation? + /// An optional observer for monitoring changes to the player's `volume` property. /// /// This property holds an instance of `NSKeyValueObservation`, which observes the `volume` @@ -62,6 +65,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? /// Initializes a new player view with a video asset and custom settings. diff --git a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift index b315fbc..f2667aa 100644 --- a/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift +++ b/Sources/swiftui-loop-videoplayer/view/player/mac/ExtPlayerNSView.swift @@ -55,6 +55,9 @@ internal class ExtPlayerNSView: NSView, ExtPlayerProtocol { /// An optional observer for monitoring changes to the player's `currentItem` property. internal var currentItemObserver: NSKeyValueObservation? + /// Item status observer + internal var itemStatusObserver: NSKeyValueObservation? + /// An optional observer for monitoring changes to the player's `volume` property. /// /// This property holds an instance of `NSKeyValueObservation`, which observes the `volume` From 80bda98307ab8b64bc5c876a663a23a4eefea851 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 6 Feb 2025 16:07:40 +0100 Subject: [PATCH 103/209] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0966f96..bdefdda 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ The player's functionality is designed around a dual ⇆ interaction model: | | Multichannel Audio | Play Dolby Atmos, 5.1 surround, and spatial audio tracks. | | | Subtitles and Captions | Support for multiple subtitle and caption formats. | | **Event Handling** | Batch Event Processing | Collects and processes events in batches to avoid flooding. | -| | Playback State Events | `playing`, `paused`, `seek`, `waitingToPlayAtSpecifiedRate`, etc. | +| | Playback State Events | `playing`, `paused`, `seek`, `duration(CMTime)`, etc. | | | Current Item State | Detect when the current item changes or is removed. | | | Volume Change Events | Listen for changes in volume levels. | | **Testing & Development** | Unit Testing | Includes unit tests for core functionality. | From 73596f0abb9e35e45cf766ddc466223b80d4baec Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 6 Feb 2025 16:45:30 +0100 Subject: [PATCH 104/209] 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 1ae9481..e1a798e 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -109,7 +109,7 @@ 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.collect(.byTime(DispatchQueue.main, .seconds(2))), perform: { event in playerEvent = event }) .preference(key: CurrentTimePreferenceKey.self, value: currentTime) From 679027332be602951adeecd1ad3cc7c0dc2290af Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 6 Feb 2025 16:46:19 +0100 Subject: [PATCH 105/209] 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 e1a798e..1ae9481 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -109,7 +109,7 @@ public struct ExtVideoPlayer: View{ .onReceive(timePublisher.receive(on: DispatchQueue.main), perform: { time in currentTime = time }) - .onReceive(eventPublisher.collect(.byTime(DispatchQueue.main, .seconds(2))), perform: { event in + .onReceive(eventPublisher.collect(.byTime(DispatchQueue.main, .seconds(1))), perform: { event in playerEvent = event }) .preference(key: CurrentTimePreferenceKey.self, value: currentTime) From 9f2e631bfdc2d72cd76b7e103c55ec2138eb58a4 Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 6 Feb 2025 17:18:55 +0100 Subject: [PATCH 106/209] update --- .../swiftui-loop-videoplayer/ExtVideoPlayer.swift | 4 ---- .../enum/PlayerEvent.swift | 2 ++ .../protocol/player/AbstractPlayer.swift | 2 +- .../protocol/player/ExtPlayerProtocol.swift | 14 +++++++++++++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift index 1ae9481..1abb91a 100644 --- a/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/ExtVideoPlayer.swift @@ -102,10 +102,6 @@ public struct ExtVideoPlayer: View{ timePublisher: timePublisher, eventPublisher: eventPublisher ) - .onDisappear{ - //player?.onDeinit() - //player = nil - } .onReceive(timePublisher.receive(on: DispatchQueue.main), perform: { time in currentTime = time }) diff --git a/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift b/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift index bde483c..6a1a169 100644 --- a/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift +++ b/Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift @@ -112,6 +112,8 @@ extension PlayerEvent: CustomStringConvertible { return "Status: ReadyToPlay" case .failed: return "Status: FailedToLoad" + @unknown default: + return "Unknown status" } case .duration(let value): let roundedString = String(format: "%.0f", value.seconds) diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift index 684e3d3..677d931 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift @@ -7,7 +7,7 @@ import AVFoundation #if canImport(CoreImage) -import CoreImage +@preconcurrency import CoreImage import AVKit #endif diff --git a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift index 9851566..28bccce 100644 --- a/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift +++ b/Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift @@ -181,7 +181,7 @@ internal extension ExtPlayerProtocol { currentSettings = settings guard let newItem = createPlayerItem(with: settings) else{ - onError(.sourceNotFound(settings.name)) + itemNotFound(with: settings.name) return } @@ -203,6 +203,14 @@ internal extension ExtPlayerProtocol { delegate?.didReceiveError(error) } + /// Emit the error "Item not found" with delay + /// - Parameter name: resource name + func itemNotFound(with name: String){ + Task{ @MainActor [weak self] in + 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) { @@ -224,6 +232,10 @@ internal extension ExtPlayerProtocol { Task { @MainActor in self?.onError(.failedToLoad) } + @unknown default: + Task { @MainActor in + self?.onError(.failedToLoad) + } } } } From e1d682965acbf07e0b4d37583f4192b74780f89e Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 7 Feb 2025 09:58:07 +0100 Subject: [PATCH 107/209] 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 108/209] 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 109/209] 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 110/209] 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 111/209] 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 112/209] 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 113/209] 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 114/209] 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 115/209] 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 116/209] 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 117/209] 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 118/209] 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 119/209] 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 120/209] 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 121/209] 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 122/209] 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 123/209] 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 124/209] 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 125/209] 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 126/209] 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 127/209] 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 128/209] 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 129/209] 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 130/209] 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 131/209] 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 132/209] 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 133/209] 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 134/209] 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 135/209] 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 136/209] 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 137/209] 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 138/209] 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 139/209] 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 140/209] 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 141/209] 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 142/209] 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 143/209] 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 144/209] 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 145/209] 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 146/209] 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 147/209] 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 148/209] 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 149/209] 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 150/209] 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 151/209] 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 152/209] 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 153/209] 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 154/209] 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 155/209] 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 156/209] 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 157/209] 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 158/209] 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 159/209] 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 160/209] 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 161/209] 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 162/209] 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 163/209] 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 164/209] 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 165/209] 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 166/209] 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 167/209] 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 168/209] 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 169/209] 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 170/209] 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 171/209] 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 172/209] 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 173/209] 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 174/209] 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 175/209] 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 176/209] 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 177/209] 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 178/209] 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 179/209] 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 180/209] 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 181/209] 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 182/209] 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 183/209] 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 184/209] 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 185/209] 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 186/209] 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 187/209] 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 188/209] 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 189/209] 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 190/209] 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 191/209] 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 192/209] 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 193/209] 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 194/209] 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 195/209] 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 196/209] 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 197/209] 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 198/209] 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 199/209] 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 200/209] 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 201/209] 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 202/209] 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 203/209] 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 204/209] 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 205/209] 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 206/209] 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 207/209] 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 208/209] 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 209/209] 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.