Skip to content
Projects

josejuanqm/VersaPlayer has 819 GitHub stars and 121 forks. GitHot measured 0 stars in 24 hours. Last checked 2026-11-10 03:29 UTC. Last push 2026-10-29.

How these counts are measured

Versatile Video Player implementation for iOS, macOS, and tvOS

Measured star growth

in 24h
0
in 7 days
Collecting history
in 15 days
Collecting history
in 1 month
Collecting history

Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.

README.md

Original

CI Platforms Swift License

News

:tada: VersaPlayer 4.0 is a SwiftUI rewrite. Coming from 3.x? See MIGRATION.md.

VersaPlayer is a SwiftUI video player built on AVFoundation: an observable player model, a ready-made controls overlay, and building blocks for your own controls. Supports HLS, tracks, caption styling, FairPlay, Picture in Picture and AirPlay on iOS, tvOS, macOS and visionOS.

Installation

Swift Package Manager:

.package(url: "https://github.com/josejuanqm/VersaPlayer.git", from: "4.0.0")

Or in Xcode: File › Add Package Dependencies… and paste the repository URL.

Usage

Basic

import SwiftUI
import VersaPlayer

struct PlayerScreen: View {
    @State private var model = VersaPlayerModel()

    var body: some View {
        VersaVideoPlayer(model: model)
            .aspectRatio(16 / 9, contentMode: .fit)
            .task { model.load(VersaPlayerItem(url: url)) }
    }
}

load autoplays by default. Pass autoplay: false to start paused.

State and events

VersaPlayerModel is @Observable, so views update on their own:

model.isPlaying, model.isBuffering, model.currentTime, model.duration, model.isLive, model.status

For one-off events (ready, ended, failed, ...):

.task {
    for await event in model.events {
        if case .failed(let error) = event { print(error) }
    }
}

To block playback (for example, to show an ad first):

model.shouldBeginPlayback = { _ in
    await ads.playPreRoll()
    return true
}

Custom controls

Pass any view as controls. Use the building blocks, which read the player from the environment:

VersaVideoPlayer(model: model) {
    VStack {
        Spacer()
        HStack {
            PlayPauseButton()
            Scrubber()
            TimeLabel(.remaining)
            TrackPicker(.legible)
        }
        .padding()
    }
}

Building blocks: PlayPauseButton, SkipButton(seconds:), RateButton(rate:), Scrubber, TimeLabel, BufferingIndicator, TrackPicker, PiPButton, AirPlayButton, FullscreenButton. For video without any overlay, use VideoSurface(model:).

Options

VersaVideoPlayer(model: model)
    .videoGravity(.resizeAspectFill)
    .controlsAutohide(after: .seconds(5))   // nil keeps controls visible
    .doubleTapTogglesGravity(false)

Fullscreen

The player asks your app to toggle fullscreen; your app presents it. The fullscreen button appears once you register the request:

@State private var isFullscreen = false

VersaVideoPlayer(model: model)
    .onFullscreenRequest(isFullscreen: false) { isFullscreen = true }
    .fullScreenCover(isPresented: $isFullscreen) {
        VersaVideoPlayer(model: model)
            .onFullscreenRequest(isFullscreen: true) { isFullscreen = false }
            .ignoresSafeArea()
    }

Tracks

model.tracks(of: .audio)        // [MediaTrack]
model.tracks(of: .legible)      // subtitles and captions
model.select(track)
model.deselect(.legible)        // subtitles off
model.selectedTracks[.audio]

For adaptive (HLS) quality caps:

model.setQuality(QualityPreference(peakBitRate: 2_000_000))

Caption styling

var style = CaptionStyle()
style.foregroundColor = .init(red: 1, green: 0.85, blue: 0)
style.relativeFontSize = 150
style.edgeStyle = .dropShadow
model.setCaptionStyle(style)

FairPlay

let provider = FairPlayHTTPProvider(
    certificateURL: URL(string: "https://example.com/fps/cert")!,
    licenseURL: URL(string: "https://example.com/fps/license")!
)
model.load(VersaPlayerItem(url: url, drm: DRMConfiguration(provider: provider)))

For a custom license protocol, implement ContentKeyProvider. The FairPlay flow is unit tested with fakes only; test it against your own license server.

tvOS

The default controls work with the Siri Remote. While the controls are hidden, select shows them, left/right skips 10 seconds, and play/pause toggles playback. While they're visible, the seek bar has focus: left/right scrubs (faster with quick repeated presses), and select jumps there. Swipe up for audio, subtitles and Picture in Picture. Menu hides the controls.

Custom controls with focusable buttons should add .controlsTakeFocus(), so focus moves into them while they're visible.

Picture in Picture and AirPlay

PiPButton and AirPlayButton are in the default controls. For Picture in Picture on iOS, enable the Audio background mode and set the audio session category to .playback.

Plugins

final class Analytics: VersaPlayerPlugin {
    func attach(to model: VersaPlayerModel) {
        Task { for await event in model.events { track(event) } }
    }
}
model.add(Analytics())

Built in: NowPlayingPlugin shows the title, artwork and progress on the lock screen and in Control Center, and handles remote play/pause/skip/seek. It uses VersaPlayerItem.metadata:

model.add(NowPlayingPlugin())
model.load(VersaPlayerItem(url: url, metadata: .init(title: "Episode 1", subtitle: "My Show", artworkURL: artwork)))

UIKit / AppKit

let controller = VersaPlayerViewController()
controller.model.load(VersaPlayerItem(url: url))

Example

brew install xcodegen
cd Examples/VersaPlayerDemo && xcodegen generate && open VersaPlayerDemo.xcodeproj

One app target for iOS, tvOS, macOS and visionOS.

Author

Jose Quintero - [email protected]

Star History

Contributors

People that make VersaPlayer possible, Thank you!

josejuanqm pbeast danibachar HuseyinVural

Donation

If you like this project or has been helpful to you, you can buy me a cup of coffe :) Appreciate it!

paypal

License

VersaPlayer is available under the MIT license. See the LICENSE file for more info.

Explore related projects