I am currently developing three open-source packages. 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.
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.
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
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. 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.
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.
Property/Method | Type | Description |
---|---|---|
settings |
Binding<VideoSettings> |
A binding to the video player settings, which configure various aspects of the player's behavior. |
command |
Binding<PlaybackCommand> |
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(settings: () -> VideoSettings, command:) |
Constructor | Initializes the player in a declarative way with a settings block and a playback command binding. |
init(settings: Binding<VideoSettings>, command:) |
Constructor | Initializes the player with bindings to the video settings and a playback command. |
Name | Description | Default |
---|---|---|
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" |
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 |
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
-
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 specificCMTime
value to set a custom interval. | 1 second (CMTime with 1 second and preferred timescale of 600) If noTimePublishing
is provided, the player will not emit time events, which can improve performance when timing information is not needed. -
SourceName: If a valid URL (http or https) is provided, the video will be streamed from the URL. If not a URL, the system will check if a video with the given name exists in the local bundle. The local name provided can either include an extension or be without one. The system first checks if the local name contains an extension. If the local name includes an extension, it extracts this extension and uses it as the default. If the local name does not contain an extension, the system assigns a default extension of .mp4 The default file extension can be set up via Ext param.
-
Loop: Whether the video should automatically restart when it reaches the end. If not explicitly passed, the video will not loop.
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.
For example, if you attempt to pause the video player twice in a row, the second pause command will have no effect because the player is already in a paused state. Similarly, sending two consecutive play commands will not re-trigger playback if the video is already playing.
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.
Command | Description |
---|---|
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. |
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. |
unmute |
Command to unmute the video. |
volume(Float) |
Command to adjust the volume of the video playback. The volume parameter is a Float value between 0.0 (mute) and 1.0 (full volume). If a value outside this range is passed, it will be clamped to the nearest valid value (0.0 or 1.0). |
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. |
Command | Description |
---|---|
brightness(Float) |
Command to adjust the brightness of the video playback. The brightness parameter is a Float value typically ranging from -1.0 (darkest) to 1.0 (brightest). Values outside this range will be clamped to the nearest valid value. |
contrast(Float) |
Command to adjust the contrast of the video playback. The contrast parameter is a Float value typically ranging from 0.0 (no contrast) to 4.0 (high contrast). Values outside this range will be clamped to the nearest valid value. |
filter(CIFilter, clear: Bool) |
Applies a specific Core Image filter to the video. If clear is true, any existing filters on the stack are removed before applying the new filter; otherwise, the new filter is added to the existing stack. |
removeAllFilters |
Command to remove all applied filters from the video playback. |
Command | Description |
---|---|
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. |
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. |
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. |
paused |
Indicates that the player's playback is currently paused. This state occurs when the player has been manually paused by the user or programmatically through a method like pause() . The player is not playing any content while in this state. |
waitingToPlayAtSpecifiedRate |
Indicates that the player is currently waiting to play at the specified rate. This state generally occurs when the player is buffering or waiting for sufficient data to continue playback. It can also occur if the playback rate is temporarily reduced to zero due to external factors, such as network conditions or system resource limitations. |
playing |
Indicates that the player is actively playing content. This state occurs when the player is currently playing video or audio content at the specified playback rate. This is the active state where media is being rendered to the user. |
currentItemChanged |
Triggered when the player's currentItem is updated to a new AVPlayerItem . This event indicates a change in the media item currently being played. |
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). |
When you use the addVector
command, you can dynamically add a new vector graphic layer (such as a logo or animated vector) over the video stream. This is particularly useful for enhancing the user experience with overlays, such as branding elements, animated graphics.
Adding a Vector Layer:
- The
addVector
command takes aShapeLayerBuilderProtocol
instance. This protocol defines the necessary method to build aCAShapeLayer
based on the given geometry (frame, bounds). - The
clear
parameter determines whether existing vector layers should be removed before adding the new one. If set totrue
, all existing vector layers are cleared, and only the new layer will be displayed. - The vector layer will be laid out directly over the video stream, allowing it to appear as part of the video playback experience.
Important Lifecycle Consideration: Integrating vector graphics into SwiftUI views, particularly during lifecycle events such as onAppear, requires careful consideration of underlying system behaviors. When vectors are added as the view appears, developers might encounter issues where the builder receives frame and bounds values of zero. This discrepancy stems from the inherent mismatch between the lifecycle of SwiftUI views and the lifecycle of UIView or NSView, depending on the platform. SwiftUI defers much of its view layout and rendering to a later stage in the view lifecycle. To mitigate these issues, a small delay can be introduced during onAppear. I'll try to add this command in the initial config later to cover the case when you need a vector layer at the very early stage of the video streaming.
- Brightness and Contrast: These settings function also filters but are managed separately from the filter stack. Adjustments to brightness and contrast are applied additionally and independently of the image filters.
- Persistent Settings: Changes to brightness and contrast do not reset when the filter stack is cleared. They remain at their last set values and must be adjusted or reset separately by the developer as needed.
- Independent Management: Developers should manage brightness and contrast adjustments through their dedicated methods or properties to ensure these settings are accurately reflected in the video output.
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:
Feature | Description |
---|---|
Hardware accelerator | AVQueuePlayer uses hardware acceleration by default where available. |
4k/HDR/HDR10/HDR10+/Dolby Vision | These high-definition and high-dynamic-range formats are natively supported by AVQueuePlayer . |
Multichannel Audio/Dolby Atmos/Spatial Audio | AVQueuePlayer supports advanced audio formats natively. |
Text subtitle/Image subtitle/Closed Captions | Subtitle and caption tracks included in the video file are automatically detected and rendered. |
Automatically switch to multi-bitrate streams based on network | Adaptive bitrate streaming is handled automatically by AVQueuePlayer when streaming from a source that supports it. |
External playback control support | Supports playback control through external accessories like headphones and Bluetooth devices. |
AirPlay support | Natively supports streaming audio and video via AirPlay to compatible devices without additional setup. |
ExtVideoPlayer(fileName: 'swipe')
or in a declarative way
ExtVideoPlayer{
VideoSettings{
SourceName("swipe")
Ext("mp8") // Set default extension here If not provided then mp4 is default
Gravity(.resizeAspectFill)
TimePublishing()
ErrorGroup{
EColor(.accentColor)
EFontSize(27)
}
}
}
.onPlayerTimeChange { newTime in
// Current video playback time
}
.onPlayerEventChange { events in
// Player events
}
ExtVideoPlayer{
VideoSettings{
SourceName("swipe")
Gravity(.resizeAspectFill)
EFontSize(27)
}
}
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 AVFoundation framework used in the package supports a wide range of video formats and codecs, including both file-based media and streaming protocols. Below is a list of supported video types, codecs, and streaming protocols organized into a grid according to Apple’s documentation. Sorry, didn’t check all codecs and files.
Supported File Types | Supported Codecs | Supported Streaming Protocols |
---|---|---|
3GP | H.264 | HTTP Live Streaming (HLS) |
.3gp , .3g2 |
H.265 (HEVC) | .m3u8 |
MKV (Limited support) | MPEG-4 Part 2 | |
.mkv |
AAC (audio) | |
MP4 | MP3 (audio) | |
.mp4 |
||
MOV | ||
.mov |
||
M4V | ||
.m4v |
The package now supports using remote video URLs, allowing you to stream videos directly from web resources. This is an extension to the existing functionality that primarily focused on local video files. Here's how to specify a remote URL within the settings:
ExtVideoPlayer{
VideoSettings{
SourceName('https://example.com/video')
Gravity(.resizeAspectFill) // Video content fit
ErrorGroup{
EColor(.red) // Error text color
EFontSize(18) // Error text font size
}
}
}
Video Source | Possible to Use | Comments |
---|---|---|
YouTube | No | Violates YouTube's policy as it doesn't allow direct video streaming outside its platform. |
Direct MP4 URLs | Yes | Directly accessible MP4 URLs can be used if they are hosted on servers that permit CORS. |
HLS Streams | Yes | HLS streams are supported and can be used for live streaming purposes. |
The package now supports playback commands, allowing you to control video playback actions such as play, pause, and seek to specific times.
struct VideoView: View {
@State private var playbackCommand: PlaybackCommand = .play
var body: some View {
ExtVideoPlayer(
{
VideoSettings {
SourceName("swipe")
}
},
command: $playbackCommand
)
}
}
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.