A small, native-Swift library for extracting direct video stream URLs from web pages — no Python, no server. Pure Swift Concurrency, with a pluggable per-site extractor architecture.
It fills a gap in the ecosystem: the only existing native-Swift extractors are
YouTube-only. SwiftStreamExtractor is site-agnostic at its core and lets you
add site-specific handlers as needed.
import StreamExtractor
let extractor = StreamExtractor()
let streams = try await extractor.streams(for: pageURL)
if let best = streams.first {
// best.url -> direct .m3u8 / .mp4 URL
// best.kind -> .hls or .progressive
// best.httpHeaders -> e.g. ["Referer": ...] to hand to your player
play(best.url, headers: best.httpHeaders)
}Most "watch this web video elsewhere" features need one thing: the real media URL
behind a page. Doing that on Apple platforms usually means either embedding a
Python runtime to run yt-dlp (heavy, and awkward on visionOS) or reverse-engineering
each site by hand. This library is the lightweight middle ground — Foundation +
WebKit, nothing else — and it runs anywhere Swift does, including visionOS.
Swift Package Manager:
.package(url: "https://github.com/tgeselle/SwiftStreamExtractor.git", from: "0.1.0")Then add "StreamExtractor" to your target's dependencies.
Platforms: iOS 15+, macOS 12+, tvOS 15+, visionOS 1+, watchOS 8+.
Extraction runs as an ordered chain of SiteExtractors. The first one that handles
the URL and yields a result wins. Two strategies ship by default:
-
GenericExtractor(Foundation only) — fetches the page HTML and looks for media URLs that are server-rendered:og:videometa tags, JSON-LDVideoObjectcontentUrl/embedUrl, and any.m3u8/.mp4URL in the markup or inline scripts (including\/-escaped ones). Fast, works everywhere. -
WebViewExtractor(WebKit; iOS/macOS/visionOS) — loads the page in an off-screenWKWebView, hooksfetch/XMLHttpRequestand scans the DOM in every frame, and captures media URLs the page requests at playback time. This is what catches custom and MSE/blob:players that never expose a URL in the HTML. Often needs the media to start playing, so allow a couple of seconds.
The default StreamExtractor() tries GenericExtractor first (cheap), then falls
back to WebViewExtractor where available.
struct MySiteExtractor: SiteExtractor {
let name = "mysite"
func canHandle(_ url: URL) -> Bool { url.host?.contains("mysite.com") == true }
func extract(_ url: URL, context: ExtractionContext) async throws -> [MediaStream] {
let (html, finalURL) = try await context.fetchHTML(url)
// ...parse the site's player config...
return MediaPatterns.mediaURLs(in: html).map {
MediaStream(url: $0, kind: MediaPatterns.kind(for: $0),
quality: MediaPatterns.quality(for: $0),
httpHeaders: ["Referer": finalURL.absoluteString])
}
}
}
let extractor = StreamExtractor(extractors: [MySiteExtractor()] + StreamExtractor.defaultExtractors)MediaPatterns exposes the reusable helpers (mediaURLs(in:), kind(for:),
quality(for:), rank(_:), isMedia(_:)).
This is a focused library, not a full yt-dlp replacement:
- DRM (FairPlay/Widevine) is out of scope and impossible to bypass.
- Players that build the stream entirely in JavaScript and feed MSE without ever fetching a manifest URL expose nothing to capture.
- Login/cookie-gated streams need you to pass the right cookies via
MediaStream.httpHeaders/ your player. - Sites change. Site-specific extractors are inherently maintenance-prone.
let asset = AVURLAsset(
url: best.url,
options: best.httpHeaders.isEmpty ? nil
: ["AVURLAssetHTTPHeaderFieldsKey": best.httpHeaders]
)
player.replaceCurrentItem(with: AVPlayerItem(asset: asset))MIT © Thomas Geselle