A Swift wrapper around libVLC for iOS, macOS, tvOS, visionOS, and Mac Catalyst.
AVFoundation is excellent for Apple's native media stack, but its container, codec, subtitle, and network-protocol support is limited to what Apple ships. Apps that need MKV, SSA/ASS subtitles, RTSP, SMB, UPnP, or other VLC-backed formats and protocols need a broader engine.
VLC's engine, libVLC, supports a broad set of codecs, containers, subtitles, and network protocols through embeddable C APIs.
VideoLAN's Apple wrapper, VLCKit, is written primarily in Objective-C. It uses delegates, KVO, NSNotificationCenter, and manual thread management, which is a faithful reflection of the era it was designed in.
SwiftVLC wraps libVLC 4.0 directly in Swift, with no Objective-C layer in between. It is built for @Observable, async/await, and VideoView(player).
| SwiftVLC | VLCKit | |
|---|---|---|
| Language | Swift 6 | Objective-C |
| Bindings | Direct C → Swift | C → Objective-C → Swift bridging |
| State management | @Observable, drives SwiftUI directly |
KVO, NSNotificationCenter, and delegates |
| Concurrency | @MainActor, Sendable, async/await |
Manual thread dispatch, no isolation |
| Video rendering | VideoView(player) |
App-supplied view setup plus drawable configuration |
| Errors | throws(VLCError), typed and exhaustive |
NSError codes |
| Events | AsyncStream<PlayerEvent> with multiple consumers |
NSNotificationCenter |
| libVLC generation | 4.0 | 3.x stable line; 4.0 alpha packages exist |
| SwiftUI PiP | iOS via public AVKit sample buffers; macOS private backend is SPI opt-in | App-supplied integration |
| Swift 6 safe | Yes, with strict concurrency | No |
@Observableplayer: state, current time, duration, tracks, and volume drive SwiftUI directly.VideoView(player)handles the rendering lifecycle in a single SwiftUI view.- Typed errors via
throws(VLCError)instead of error codes. - Asynchronous media parsing:
try await media.parse()with cancellation support. - 10-band equalizer with libVLC's built-in presets.
- A-B looping, playback rate control, and subtitle and audio delay.
- Picture-in-Picture on iOS with full playback controls; macOS native PiP is available only through an explicit private-API SPI opt-in.
- Media discovery and renderer discovery through services exposed by the bundled libVLC plugins.
- 360° video with full viewpoint control over yaw, pitch, roll, and field of view.
- Asynchronous thumbnail generation at arbitrary timestamps.
MediaListPlayerfor playlist playback with loop and repeat modes.
- Swift 6.3+ / Xcode 26+
- iOS 18+ / macOS 15+ / tvOS 18+ / visionOS 2+ / Mac Catalyst 18+
In Xcode, choose File → Add Package Dependencies, paste the repo URL, and Xcode will pick up the latest release automatically:
https://github.com/harflabs/SwiftVLC.git
From a Package.swift manifest, add a dependency and pin to the
current release. The version string lives on the
releases page.
.package(url: "https://github.com/harflabs/SwiftVLC.git", from: "x.y.z")The pre-built libVLC xcframework downloads automatically via SPM. It's a large binary (multi-GB unstripped; the release zip is a few hundred MB).
import SwiftUI
import SwiftVLC
struct PlayerView: View {
@State private var player = Player()
var body: some View {
VideoView(player)
.onAppear {
try? player.play(url: URL(string: "https://example.com/video.mp4")!)
}
}
}Player.play(url:) expects a direct media stream or file URL. It does
not auto-resolve .pls or classic .m3u playlist containers; use
MediaListPlayer or fetch and parse the playlist to its inner stream
URL before passing it to Player. HLS .m3u8 URLs are supported here
because they are streaming manifests rather than playlists of separate
media URLs.
// Playback
let player = Player()
try player.play(url: videoURL)
player.pause()
player.stop()
try player.seek(to: PlaybackPosition(0.5)) // Seek to 50%
try player.setPlaybackRate(1.5) // 1.5x speed
try player.setAudioVolume(0.8) // 80% volume
player.isMuted = true
// Tracks
player.selectedSubtitleTrack = player.subtitleTracks[1]
// Metadata
let media = try Media(url: videoURL)
let metadata = try await media.parse()
print(metadata.title, metadata.duration)
// Events
for await event in player.events {
switch event {
case .stateChanged(let state): ...
case .timeChanged(let time): ...
default: break
}
}Full API reference is hosted on Swift Package Index: swiftpackageindex.com/harflabs/swiftvlc/documentation
The Showcase/ directory contains separate folders, targets, and schemes for each showcase lane:
- iOS. Full-featured app target, also enabled for Mac Catalyst.
- macOS. Native macOS app target with the same showcase coverage, adapted into sidebar-driven Mac UI.
- tvOS. Native tvOS showcase app target with TV-focused focus navigation and Siri Remote controls.
- visionOS. Native visionOS app target with a focused simple playback showcase.
Showcase UI tests live under Showcase/UITests/. iOSUITests covers
the broad showcase flows, macOSUITests covers native macOS PiP, and
tvOSUITests is a placeholder target. The visionOS showcase does not
have a UI-test target.
The core package uses a comprehensive
Swift Testing suite
against the real libVLC binary, so regressions in the C bridge surface
immediately rather than hiding behind a fake. Showcase UI tests use
XCTest separately. CI runs package tests, lint, doc coverage, and
Showcase builds on pull requests and on main.
swift testSee ARCHITECTURE.md for test tags, fixtures, and structure.
git clone https://github.com/harflabs/SwiftVLC.git
cd SwiftVLC
./scripts/setup-dev.sh
swift testmain tracks the latest released url + checksum form of the libVLC binary target. setup-dev.sh downloads libvlc.xcframework.zip into Vendor/ and idempotently flips Package.swift plus the Showcase package reference to repo-local sources so package development and Showcase builds use the checkout on disk.
setup-dev.sh flag |
Effect |
|---|---|
| (none) | Download the latest release if Vendor/ is empty; otherwise keep existing. |
vX.Y.Z (positional) |
Pin to a specific release tag. |
--force |
Re-download even if Vendor/ already exists. |
--skip-download |
Only flip local references (Package.swift and the Showcase app). Expects Vendor/ to already exist, which is useful after running build-libvlc.sh. |
Needed only when bumping VLC_HASH, modifying build patches, or preparing a release. Day-to-day Swift development doesn't require it.
brew install autoconf automake libtool cmake pkg-config gettext
./scripts/build-libvlc.sh --allExpect a full --all build to take tens of minutes on Apple Silicon. The script clones VLC at a pinned commit into scripts/.build-libvlc/, applies the source patches below, builds every contrib (FFmpeg, dav1d, x264, libass, …) per slice, and assembles the result into Vendor/libvlc.xcframework.
| Flag | Platforms |
|---|---|
| (default) | iOS device + simulator |
--all |
iOS, tvOS, visionOS, macOS, Mac Catalyst (eight slices) |
--ios-only / --tvos-only / --visionos-only / --macos-only / --catalyst-only |
Replaces Vendor/ with that single platform |
--tvos / --visionos / --macos / --catalyst |
Adds a platform to the default set |
--clean / --clean-build |
Wipe scripts/.build-libvlc/ (the latter rebuilds afterwards) |
--hash=<sha> |
Override the pinned VLC commit |
*-onlyflags replace the xcframework; any slices already inVendor/are lost.
VLC master requires a few local patches for SwiftVLC's supported Apple toolchains. The script applies them in-tree on every invocation, idempotently:
- Mac Catalyst. Teaches VLC's build system the
macabitarget triple and guards OpenGLES-only code paths. - visionOS deployment target. Adds the
xrostarget triple so object files are stamped with visionOS 2.0 instead of the installed SDK version. - Xcode 26 LDFLAGS. Adds
-isysrootto linker invocations so libSystem resolves. - libtool 2.5 OBJC tag. Adds
_LIBTOOLFLAGS = --tag=CCto theMakefile.amfiles that contain.msources. Older libtool versions inferred the tag; 2.5 refuses. - Rust contribs disabled. VLC's contribs pin
cargo-c 0.9.29, which pullstime 0.3.31and fails type inference under the supported Rust toolchain. The only Rust contrib on Apple israv1e(AV1 encoder);dav1dhandles decoding. dup3/pipe2. Forced unavailable via autoconf cache vars. iOS Simulator SDK 26 exports these Linux-only syscalls from libSystem, fooling configure into using them.
git reset --hard only runs when HEAD is not at VLC_HASH, so the patches and per-platform build dirs survive repeated runs.
Releases advance main: release.sh rewrites Package.swift to the new remote xcframework URL + checksum, pins the Showcase app to that exact SwiftVLC version, tags that commit, uploads the zip as a GitHub Release asset, and then pushes main to that same commit. setup-dev.sh is what flips a working checkout back to local sources for day-to-day development.
./scripts/build-libvlc.sh --all # produces Vendor/libvlc.xcframework
./scripts/release.sh X.Y.Z --dry-run # strip + zip + checksum, no push
./scripts/release.sh X.Y.Z # cut the releaseWhat release.sh does:
- Verifies all eight platform slices are present in the xcframework.
- Copies it to a temp dir, strips debug symbols, zips with
ditto. - Computes SHA-256 via
swift package compute-checksum. - Rewrites
Package.swiftto the remote URL and checksum, and pins the Showcase app toSwiftVLCexact versionX.Y.Z. - Commits that change and tags it as
vX.Y.Z. - Pushes the tag first so GitHub can attach the release asset to that exact commit.
- Uploads the zip to a new GitHub Release.
- Pushes
mainto the same commit, somainalways references the latest published xcframework and Showcase package version.
Preflight refuses non-main branches, uncommitted changes in Package.swift or the Showcase project, pre-existing local or remote tags, and unauthenticated gh. If a pre-commit rewrite fails, the script restores Package.swift and the Showcase project before exiting. If the tag push succeeds but a later step fails, origin/main is still untouched; finish the GitHub Release (or delete the tag) before retrying.
For internals, including module design, C interop, the concurrency model, the event system, memory management, and the PiP rendering pipeline, see ARCHITECTURE.md.
MIT. See LICENSE.
libVLC is licensed under LGPLv2.1. Static linking may have licensing implications. See the VLC licensing FAQ.
SwiftVLC stands on the work of the VideoLAN community. VLC and libVLC represent decades of media playback work by hundreds of contributors.
Thanks also to VLCKit for establishing libVLC on Apple platforms.