SwiftVLC: a Swift wrapper around libVLC for Apple's platforms
A Swift wrapper around libVLC for iOS, macOS, tvOS, visionOS, and Mac Catalyst
At a glance
- What is it?
- SwiftVLC binds the VLC engine directly in Swift, with no Objective-C layer in between, and it is built for the modern Apple stack: an observable player that drives SwiftUI, a single view that owns the video rendering lifecycle, typed errors, and an asynchronous event stream. The pitch is not that it plays video, but that the way it plays video looks like it was written this decade.
- Who is it for?
- SwiftVLC fits an Apple platform application that needs formats or protocols the native stack does not carry, and that would rather not write the bridging layer itself. It does not fit an application whose requirements the native framework already meets, since adding a large media engine for that is a poor trade, and it does not fit a project that cannot accept a multi hundred megabyte dependency.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 32 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Why not the framework you already have
The argument for the library starts with a concession: the native media stack on Apple platforms is excellent, but its container, codec, subtitle, and network protocol support is limited to what the vendor ships.
That is a precise limitation and it is the whole case for the project. An application that needs a Matroska container, a particular styled subtitle format, a file share over a network protocol, or a network discovery protocol needs a broader engine than the system provides, and the options are to write the decoder yourself, to embed a different engine, or to accept a restricted format list.
The engine chosen here is the one from the VLC project, and the reason is stated in the readme: it supports a broad set of codecs, containers, subtitles, and network protocols through embeddable C interfaces.
So the project exists at the seam where a mature cross platform C library meets a platform whose native answer is deliberately narrow. That seam is where wrappers live, and the quality of the wrapper decides whether the library is pleasant or merely possible.
And the interesting part of the readme is that it does not claim to be a better media engine. It claims to be a better way to reach this one from Swift, and the argument for that is made in the next section.
The requirements are recent: a recent Swift toolchain, and a recent operating system on each of the four platforms, plus the Mac Catalyst variant. The project is MIT licensed, published as a Swift package, with a run of weekly beta releases in the autumn of 2026 that show an active rewrite of the binding layer.
The wrapper in Objective-C, and the wrapper in Swift
The comparison the readme draws is with the vendor's own wrapper for the Apple platforms, and the description of it is unusually blunt.
That wrapper is written primarily in Objective-C, and it uses delegates, key value observing, a notification centre, and manual thread management. The readme's summary of that is that it is a faithful reflection of the era in which it was designed.
That is a fair and pointed description, and it explains the shape of the difference without any need to disparage the older library. The technologies it names are all still perfectly good; the point is that they are the technologies of a particular period, and a project written today against them has to work around their conventions rather than adopt them.
The alternative here is to wrap the engine directly in Swift, with no Objective-C layer in between, and to build for the three things the modern Apple stack has made standard: the observation macro, which lets a plain object drive an interface declaratively, asynchronous and await based concurrency, and a single view that owns the rendering lifecycle.
The last of those deserves a note, because it is the one with the most practical effect on a user's code. The older approach requires the application to supply the view, set it up, and configure the drawable, and every one of those steps is a place where an application can get it subtly wrong and end up with a black rectangle. Handing the whole lifecycle to one view that takes the player removes an entire category of integration work, and it is the difference between a library you use and a library you fight.
So the design argument is coherent: keep the engine, which is excellent, and replace only the layer that is dated. The comparison table in the readme is a list of that principle applied row by row, and it is worth reading in full because it is the clearest statement of what the project is for.
The comparison, row by row
The table has two columns, one for this project and one for the vendor's wrapper, and ten rows. Reading it as a list of design decisions rather than as a feature comparison is the more useful way to take it.
The language row is the one everything else follows from: Swift 6 against Objective-C. And the bindings row makes the mechanical difference explicit. One goes from the C interface straight to Swift. The other goes from the C interface to Objective-C and then across a bridging layer into Swift, and every crossing is a place where types get lost, optionality gets flattened, and error handling gets converted into something less precise.
The state management row is the one that changes how a user writes code. Observation means the player is a normal object whose properties drive an interface, so the interface is a function of the state rather than a set of callbacks that mutate the interface. The older approach uses key value observing, a notification centre, and delegates, which is three mechanisms where one would do.
The concurrency row is the one with the deepest consequences. The actor isolation annotation and the sendable marker mean the compiler enforces the threading rules, and the asynchronous and await model means the call sites read sequentially. The older approach uses manual thread dispatch with no isolation, which means the rules exist in documentation rather than in the type system.
The errors row is the one that will change your code most immediately. Library failures here are thrown with a typed and exhaustive error type, so the compiler tells you every case you have to handle and the switch is checked. The older approach gives you error codes, which are strings or numbers at the call site and which nothing checks.
And the events row is the mirror of the state management row. An asynchronous stream of player events with multiple consumers, against a notification centre.
Then come the rows that are about the engine and the platform rather than about Swift: the video rendering row, the generation of the engine the wrapper targets, the picture in picture row, and finally a row that says it plainly: this one is safe under Swift 6 with strict concurrency, and the other is not.
The features, and the ones that are really different
The feature list is short, and a few of the entries are worth more than the others.
The player is observable, and the state it exposes is the state an application actually needs: the current time, the duration, the tracks, and the volume. Because those drive the interface directly, a video player screen is a handful of bindings rather than a set of update handlers.
The video view handles the rendering lifecycle in a single view, as discussed above, and the readme lists it as its own bullet because it is the piece that removes the most work for the user.
The errors are typed and exhaustive, and the readme makes the point twice, in the feature list and in the comparison table, because it is the difference that a compiler can help with.
Media parsing is asynchronous, with the try and await form and with cancellation support. That is a real feature rather than a stylistic one: parsing a large file takes time, and a user who navigates away should not have the parse finish anyway.
The rest of the list is feature completeness, and it is substantial. A ten band equaliser using the engine's own presets, so the presets are the ones the engine already knows. Looping between two points, playback rate control, and independent delays for the subtitles and the audio, which is the kind of control that only matters to somebody whose players disagree about the timing. Picture in picture on the mobile platform with full playback controls. Media and renderer discovery through the services the bundled plugins expose. Spherical video with full control over the viewpoint, so the yaw, the pitch, the roll, and the field of view are all adjustable. Asynchronous thumbnail generation at a timestamp you choose, which is what a media browser needs. And a list player with loop and repeat modes for playlists.
The one caveat in the list is the picture in picture on the desktop, which is only available through an explicit opt in to a private interface, and the readme is clear that it is opt in and that the mobile path is the supported one.
The playback rate caveat, and what honesty costs
There is a paragraph in the readme about playback rate that is worth reading twice, because it is the most unusual thing in the document.
The point is that applying a playback rate is asynchronous inside the engine. A successful setter return confirms that the request was accepted immediately, and it does not confirm that the active input actually kept that rate. The input might have been replaced, the stream might not support the rate, and the engine resolves the effective rate later.
So the API distinguishes the request from the effect. On the newer extension builds there is a property that reports the effective control state, and the readme is careful to say exactly what that property does not claim: it does not claim request correlation and it does not claim measured throughput. The live value remains the authoritative one.
This is worth dwelling on because it is the kind of API that a lot of libraries would paper over. A setter that returns a boolean and an application that believes it means the rate changed is a bug waiting for a particular file format. Here the documentation says what the return value means, and it says what the other property means, and it names what neither of them means.
It also tells you something about the wrapper's relationship with the engine. The engine is a C library with a callback and polling model, and a Swift interface over it has to decide what a setter means. The decision here is that a setter means the request was accepted, and the truth is elsewhere, and the library exposes the truth in a separate property rather than pretending the two are the same thing.
The same care shows in the playlist handling. The play method expects a direct media stream or a file address, and it does not resolve the playlist container formats automatically, and the readme explains why those are different: the streaming manifest format is supported because it is a manifest of a stream, whereas the classic playlist formats are lists of separate media addresses, and those need either the list player or a step that fetches and parses the playlist first.
That is a distinction most wrappers would paper over too, and it is exactly the kind of detail that costs somebody an afternoon if it is not in the documentation.
The package, the binary, and the sample apps
The installation is the standard package manager route, and the readme gives both forms. In the development environment you add the package dependency by address and the tool picks up the latest release. In a manifest you add the dependency and pin it to a release, and the readme points at the releases page for the version string.
.package(url: "https://github.com/harflabs/SwiftVLC.git", from: "1.0.0")And then the sentence that is the most important operational fact in the readme: the pre-built engine framework downloads automatically through the package manager, and it is a large binary, several gigabytes before stripping, and a few hundred megabytes in the release archive.
That is a serious number, and it is the price of embedding a media engine. It is the trade that every application embedding one of these engines makes, and the readme puts it in the installation section where somebody will see it before they commit rather than after.
The repository layout shows the shape of the project. There is a sources directory, a tests directory, an assets directory with the light and dark logos, a fixtures directory for the tests, a sample apps directory, a documentation directory, and a scripts directory. There is a package manifest, a build configuration for the formatting tool, a lint configuration, a configuration for the package registry service, and a coverage configuration.
The sample apps directory is the interesting one, because the readme says it contains separate folders, targets, and schemes for each sample apps lane, and it names the first two. The mobile one is a full featured application target that is also enabled for the Mac Catalyst, and the desktop one is a native desktop application target with the same coverage adapted into a sidebar driven interface. So the sample apps is not one example, it is one example per platform, with the same feature set expressed in the idiom of each.
There are also two files whose names say what they are without any further explanation. There is an architecture document at the root, and there is a file with a double extension that reads as a command you can run, named for validating the library. A validation script at the top level of a repository that wraps a C library is a sensible thing to have, because the failure mode of such a wrapper is a mismatch between what the binding says and what the engine does, and a script that checks that across platforms is worth more than a paragraph in the readme.
And the documentation is hosted on the package index, with the readme making a point about the link it gives: the unversioned link follows the most recent tag that the service has finished building, which is a small but real detail, because a documentation site that lags the release is worse than one that points at a specific version.
Editorial conclusion
SwiftVLC fits an Apple platform application that needs formats or protocols the native stack does not carry, and that would rather not write the bridging layer itself. It does not fit an application whose requirements the native framework already meets, since adding a large media engine for that is a poor trade, and it does not fit a project that cannot accept a multi hundred megabyte dependency. Verify first which formats and protocols you actually need, since that is the whole justification, that the deployment targets you need are in the supported list, that the binary size is acceptable for your distribution channel, and that the library generation you get matches the one your other dependencies expect, since the wrapper is written against a specific major version of the engine.
Frequently asked questions
What is SwiftVLC?
It is a Swift package that wraps the VLC engine directly, without an Objective-C layer in between, for iOS, macOS, tvOS, visionOS, and the Mac Catalyst variant. It is built for the observation macro, asynchronous and await concurrency, and a single SwiftUI view that owns the video rendering lifecycle.
When would I use SwiftVLC instead of the native media framework?
When you need formats or protocols the native stack does not carry. The readme names Matroska containers, styled subtitle formats, the file share protocol, and network discovery as the cases that need a broader engine, because the native framework is limited to what the vendor ships.
How does SwiftVLC differ from the vendor's Objective-C wrapper?
It goes from the C interface straight to Swift rather than through a bridging layer, uses observation instead of key value observing, a notification centre and delegates, uses actor isolation and asynchronous and await instead of manual thread dispatch, throws a typed and exhaustive error instead of error codes, and exposes events as an asynchronous stream with multiple consumers.
What does the SwiftVLC playback rate property actually tell me?
That applying a rate is asynchronous inside the engine, so a successful setter return confirms the request was accepted and not that the active input kept that rate. A separate property reports the effective control state without claiming request correlation or measured throughput, and the live value remains the authoritative one.
How large is the SwiftVLC dependency?
The pre-built engine framework downloads automatically through the package manager, and the readme warns that it is a large binary, several gigabytes before stripping and a few hundred megabytes in the release archive. The package itself is installed by address in the development environment or pinned in a manifest.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/harflabs-swiftvlc)Was this useful?