SwiftVLC Documentation

repository·main·Indexed 19 days ago

https://github.com/harflabs/swiftvlc

A Swift 6 wrapper around libVLC 4.0 providing a SwiftUI-friendly media engine for Apple platforms. Designed to replace AVFoundation for broad codec, container, and network protocol support (such as MKV, SMB, and UPnP), it includes components like VideoView and an @Observable Player class. Supports iOS 18+, macOS 15+, tvOS 18+, visionOS 2+, and Mac Catalyst 18+.

Tokens
29.7K
Snippets
83
Records
126
Agent score
64%

What's inside SwiftVLC

  1. Overview of SwiftVLC

    main

    SwiftVLC is a Swift 6 wrapper around libVLC 4.0 designed specifically for SwiftUI applications. It provides a modern Swift interface by binding directly to the libVLC C API without an Objective-C intermediary.

    Key architectural features include:

    • Observable state: The Player class is @Observable and @MainActor, allowing SwiftUI views to automatically track properties like state, currentTime, duration, and track lists.
    • Typed errors: Errors use throws(VLCError), providing a full, type-safe error surface.
    • Structured events: Playback, discovery, logging, and dialog prompts are surfaced via AsyncStream, supporting multiple concurrent consumers.
    • Ownership-aware overlays: Components like Marquee, Logo, and VideoAdjustments are ~Copyable and ~Escapable, scoped to the player's lifetime to prevent dangling pointers.
  2. Project Directory Structure Overview

    main

    The repository is organized into the following main components:

    • Sources/CLibVLC/: The C bridging layer containing libVLC 4.0 C headers and a shim.c for va_list formatting.
    • Sources/SwiftVLC/: The main Swift library, organized by domain:
      • Core/: VLCInstance, VLCError, Logging, Duration
      • Player/: Player, EventBridge, PlayerState, Events, ABLoop
      • Media/: Media, Metadata, Track, Thumbnails, Statistics
      • Audio/: AudioOutput, Equalizer, ChannelModes
      • Video/: VideoView, AspectRatio, Adjustments, Marquee, Logo, Viewpoint
      • Playlist/: MediaList, MediaListPlayer, PlaybackMode
      • Discovery/: MediaDiscoverer, RendererDiscoverer
      • PiP/: PiPController, PiPVideoView, PixelBufferRenderer
    • Tests/SwiftVLCTests/: The Swift Testing suite.
    • Showcase/: Platform-specific showcase apps for iOS, macOS, tvOS, and visionOS.
    • Vendor/: Contains the libvlc.xcframework.
    • scripts/: Build, setup, and release automation scripts.
  3. SwiftVLC Tech Stack and Requirements

    main

    SwiftVLC is built using a modern Swift stack to ensure high performance and safety.

    Core Requirements:

    • Language: Swift 6.3+ with Xcode 26.4+
    • Platforms: iOS 18+, macOS 15+, tvOS 18+, visionOS 2+, Mac Catalyst 18+

    Key Technologies:

    • C Bindings: Direct access to the libVLC 4.0 C API (no Objective-C intermediary).
    • State Management: @Observable and @MainActor for SwiftUI integration.
    • Concurrency: AsyncStream<PlayerEvent> for native structured concurrency.
    • Video Rendering: Platform-native UIView / NSView via set_nsobject for zero-copy rendering.
    • Testing: Swift Testing framework.
  4. Understand the Player central type

    main

    The Player class is the core of SwiftVLC. It is @Observable and @MainActor. Each instance manages a single libvlc_media_player_t and a stream of PlayerEvent values.

    Lifecycle & Performance:

    • Construction allocates underlying libVLC resources.
    • deinit handles native release calls off the main actor to prevent UI thread blocking.
    • To optimize app launch, call VLCInstance/prewarmShared(priority:) during startup to move libVLC's one-time initialization work away from the first player screen.
    @State private var player = Player()
  5. Understand VideoView rendering architecture

    main

    SwiftVLC uses a VideoView architecture that integrates directly with libVLC's internal rendering engine.

    Key characteristics:

    • There is no manual CALayer, MTKView, or AVPlayerLayer configuration required.
    • libVLC handles all rendering internally.
    • The VideoSurface (a UIView on iOS or NSView on macOS) is passed to libVLC via set_nsobject(view pointer).
    • On resize, layoutSubviews() is used to synchronize sublayer frames.
    • When the view is dismantled, set_nsobject(nil) is called to clean up.
  6. How the Direct vmem Callback Pipeline works

    main

    The direct renderer provides a low-latency path for video by using vmem callbacks to pipe raw frames into an AVSampleBufferDisplayLayer.

    The Pipeline Flow:

    1. formatCallback: Sets the BGRA format and creates a CVPixelBufferPool.
    2. lockCallback: Retrieves a buffer from the pool and locks its base address.
    3. unlockCallback: Unlocks the base address.
    4. displayCallback: Wraps the buffer as a CMSampleBuffer and enqueues it to the layer on a dedicated serial queue.
    5. cleanupCallback: Releases the pool.

    Technical Note: This path uses a device-RGB Core Image target for resizing, meaning it is limited to SDR content and does not preserve HDR metadata.

  7. Subscribe to playback events and logs via AsyncStream

    main

    SwiftVLC replaces the delegate/KVO pattern used in VLCKit with AsyncStream. This allows multiple concurrent consumers to subscribe to events independently.

    Event Streams and Policies

    When subscribing to streams, you can choose a buffering policy based on the importance of the data:

    • Player/events(policy:filter:): The raw stream. Defaults to newest-64 (suitable for ~30 Hz clock samples). Use .unbounded to ensure no events are lost.
    • Player/timingEvents: Coalesced stream. Only the newest clock sample is emitted.
    • Unbounded Streams: The following streams are one-shot and should use unbounded buffering because missing an event (like a terminal state or a dialog prompt) cannot be recovered:
      • Player/controlEvents
      • Player/stateTransitions
      • PiPController/pipEvents
      • DialogHandler/dialogs
      • RendererDiscoverer/events

    Other Streams

    • Logging: Access via AsyncStream<LogEntry> with level filtering.
    • Dialogs: Access via AsyncStream<DialogEvent>.
  8. Use PiPVideoView for Picture-in-Picture

    main

    On iOS, use PiPVideoView instead of VideoView to enable Picture-in-Picture (PiP) functionality. PiPVideoView uses libVLC's native iOS drawable path to interface with the AVKit PiP controller.

    Important Constraints:

    • Do not share players: VideoView and PiPVideoView should not share the same player instance.
    • macOS Support: On macOS, PiPVideoView provides a native drawable container for inline playback, but native PiP functionality is not part of the stable public API. It requires a build opting into the PrivateMacOSPiP SPI, which uses private Apple framework symbols and is outside the public compatibility contract.
  9. Drive the UI from Player state

    main

    Because Player is @Observable, you can bind SwiftUI controls directly to its properties to create a reactive interface. Common properties include:

    • player.state: The current playback state.
    • player.position: The current playback position (for progress bars).
    • player.isPlaying: A boolean indicating if playback is active.

    Use player.togglePlayPause() to switch between playing and paused states.

    Text(player.state.description)
    ProgressView(value: player.position)
    Button(player.isPlaying ? "Pause" : "Play") {
        player.togglePlayPause()
    }
  10. How to set or change the playback renderer

    main

    SwiftVLC follows libVLC's rule that renderer selection should ideally happen before the first play. There are two ways to manage the output device:

    1. Before playback starts: Use Player.setRenderer(_:) to specify the target device. This ensures the media starts playing directly on the renderer.
    2. During active playback: Use Player.recast(to:). This method replaces the native handle and restarts the current media on the new device while keeping the same Player instance.

    To return playback to the local device, pass nil to either setRenderer(_:) or recast(to:).

  11. Understand the SwiftVLC Concurrency and Threading Model

    main

    SwiftVLC uses a multi-layered isolation strategy to bridge libVLC's internal C threads with Swift's modern concurrency model:

    1. @MainActor (UI Layer): Types like Player, MediaListPlayer, Equalizer, and VideoAdjustments own mutable state that SwiftUI observes. All property access and mutations must occur on the main actor.
    2. Sendable (Data Layer): Types like VLCInstance, Media, MediaList, Track, and Metadata are safe to pass between any isolation domain (MainActor or libVLC threads) because they are either immutable value types or use internal synchronization (like Mutex<T>).
    3. libVLC Internal Threads (C Layer): C callbacks (events, logging, decoding) fire on libVLC's internal threads. These are bridged to the Swift layer using AsyncStream continuations or by hopping to the @MainActor for UI-related updates.

    Key Rule: Never use Int(bitPattern:) to pass pointers through Sendable boundaries; use nonisolated(unsafe) for local scope captures or Mutex<State> with @unchecked Sendable for persistent, thread-safe storage.

  12. Understand how deinit works for MainActor types

    main

    To prevent stalling SwiftUI transitions, several @MainActor types release their underlying C resources on a background utility queue during deinit.

    When a type is released, the event manager is detached first, and then stop and release operations are executed asynchronously. Because this happens on a background queue, the deinit call returns immediately to the caller, and no explicit waiting is required.