A production playback layer on top of AndroidX Media3 on
Android, and on AVFoundation's AVPlayer on iOS and tvOS — policy, resilience, observability, and
lifecycle.
It is not a fork of either engine and not a new player. On Android SuperPlayer is a
Media3 Player, so existing PlayerView, MediaSession and Compose surfaces take it unchanged, and
player.exoPlayer is there for everything this library does not cover. On Apple platforms it owns
an AVPlayer and hands it out as player.avPlayer, so AVPlayerViewController, Picture in Picture
and AirPlay keep working. The two share the vocabulary, the documents and the test fixtures, never
the code.
| Android | iOS and tvOS | |
|---|---|---|
| Engine | Media3 1.11.0 | AVFoundation (AVPlayer) |
| Language | Kotlin, JDK 17 | Swift 6, strict concurrency |
| Floor | minSdk 24 |
iOS 17, tvOS 17 |
| Where | android/ — four Gradle builds |
Package.swift and ios/ |
Apache-2.0.
Read this before the feature list, because the feature list is longer than the guarantees.
- Nothing is published. There is no remote repository and no credentials anywhere in this
build.
(cd android && ./gradlew publishToMavenLocal)writes to your own~/.m2, which is how the demo resolves it and the only way anyone resolves it. A cut release is a commit and a local tag. The Swift package resolves by this repository's URL, but with no tag cut it resolves at a branch or a revision, which gets you whatever is on it (docs/ios.md). 0.x, and under0.xa minor may break you. What counts as breaking is the tracked API surface rather than anyone's judgement —docs/adr/0017decides it anddocs/compatibility.mdsays what it means for you.- It is verified under Robolectric to an unusual depth and on real hardware barely at all.
Most phases' device exit criteria are still open — DRM on L1/L3 hardware, the TV path, download
survival across a reboot, the feed's time-to-first-frame, and the diagnostics bundle from a
device. On iOS every test runs on a simulator, so FairPlay, tvOS display-mode switching and a
background download across a process's death are listed as a device's rather than faked.
Everything the test suite cannot show is written down in
docs/testing.mdrather than left to be assumed. - The benchmark's Phase 3 verdict is a committed finding, not a win.
android/benchmark/phase3/reports what the adaptive policy did against three other arms on a fixed matrix, including where it lost. Moving those numbers is Phase 11 and is open. superplayer-moqandSuperPlayerMoQare local-only on purpose. Each links a native library built on one machine. They are excluded from any release and from the tracked API surface, and #369 is what publishing it would have to answer first.
If you want something to depend on today, this is not it. If you want to read how a playback layer gets argued into shape, the 21 decision records are the point.
The problems are inventoried in PRD.md as F1–F8 — each a symptom teams actually report, traced
to a root cause, and assigned to a module:
| Symptom | Where it is addressed | |
|---|---|---|
| F1 | "Works on WiFi, falls apart on mobile data" | Per-transport policy and an estimate reseeded on a transport change, not carried across it |
| F2 | DefaultLoadControl constants re-derived per app from forum answers |
One policy boundary naming no Media3 type |
| F3 | "Instant start" as a product expectation | Preload, warm decoders, a content-keyed cache |
| F4 | DRM frame drops on low-end devices; downloads draining battery | Secure-decoder budgets; a battery- and network-aware download schedule |
| F5 | Sub-100 ms start via a loopback proxy, then a second workaround to undo the first | No proxy — ADR-0002 refuses the whole approach |
| F6 | Fixed buffer watermarks are wrong in both directions for live | Buffer targets recomputed on observed conditions, capped against the app's heap |
| F7 | Every mature in-house library re-converges on the same pieces | Those pieces, behind one seam each |
| F8 | QoE definitions differ per app, so nothing compares | CTA-2066, with every departure from it cited |
val player = SuperPlayer.Builder(context)
.setProfile(PlaybackProfile.VIDEO_ON_DEMAND)
.setTelemetry(QoeCollector(LogcatSink))
.build()
playerView.player = player // a stock Media3 PlayerView, no adapter
player.setMediaRequest(
MediaRequest.Builder("catalog:sintel") // content identity, not a URL
.addSource("https://cdn.example/sintel.m3u8")
.addSource("https://backup.example/sintel.mpd") // fallback, a different protocol
.setStartPosition(MediaRequest.StartPosition.ResumeFromLastKnown)
.build(),
)
player.playWhenReady = true
player.prepare()The same request on iOS, where a player is configured by a value rather than built:
var configuration = SuperPlayer.Configuration(profile: .videoOnDemand)
configuration.telemetry = { QoeCollector(sink: OSLogSink()) }
let player = SuperPlayer(configuration: configuration)
player.setMediaRequest(
MediaRequest(
contentId: "catalog:sintel",
sources: [URL(string: "https://cdn.example/sintel.m3u8")!],
startPosition: .resumeFromLastKnown))
player.avPlayer.play() // AVPlayerViewController and SwiftUI's VideoPlayer take it as isMediaRequest is the shape of the idea: content has a stable id rather than a URL, an ordered
list of sources the fallback ladder climbs through, and a start position that can say resume
without the app tracking where. The id is what a cache key, a CMCD session, a telemetry row and a
notification all agree on.
Everything else is additive and costs nothing when unused — a player built without a cache, a policy, resilience, DRM or telemetry registers nothing and allocates nothing for them, and there is a test counting that for each.
superplayer-core is the only one an app needs. Dependencies point inward, and no module may
depend on one from a later phase (docs/modules.md).
| Module | What it adds |
|---|---|
superplayer-core |
The facade, MediaRequest, profiles, the player pool, sessions, the transfer-chain seam |
superplayer-telemetry |
CTA-2066 QoE collection, a logcat sink, session traces |
superplayer-abr |
Bandwidth estimation, adaptive buffering, network-aware track selection |
superplayer-cache |
A cache keyed by content identity rather than URL |
superplayer-preload |
Feed prefetch and decoder warm-up over one shared engine |
superplayer-resilience |
One error taxonomy and a six-rung fallback ladder |
superplayer-drm |
Widevine online and offline, provisioning, the security-level ladder |
superplayer-offline |
Downloads into the cache you opened, on a battery-aware schedule |
superplayer-tv |
Frame-rate matching, display and audio changes, D-pad controls |
superplayer-effects |
A decoded-frame hook on Media3's GL effect pipeline (setFrameOutput), off on a protected or realtime player |
superplayer-diagnostics |
A doctor that names a manifest's pathology, a trace bundle, a debug HUD |
superplayer-realtime |
Encoded frames pushed into Media3's sample queues under a live timeline |
superplayer-moq |
Media over QUIC over that seam — local-only, see Status |
superplayer-testkit |
The deterministic harness: shaped networks, fault injection, golden traces |
superplayer-testmedia |
Synthetic HLS and DASH streams, and a corpus of deliberately broken ones |
SuperPlayerCore is the only product an app needs; the rest are additive and depend inward on it.
Installing, the configuration value, what ports and what is not ported by rule are
docs/ios.md.
| Product | What it adds | Platforms |
|---|---|---|
SuperPlayerCore |
SuperPlayer, MediaRequest, profiles, snapshots, PlaybackSession, PlayerPool, the policy boundary, PlayerView/PlayerLayerView |
iOS, tvOS |
SuperPlayerTelemetry |
CTA-2066 QoE collection, an os_log sink, session traces |
iOS, tvOS |
SuperPlayerABR |
Adaptive buffering and selection over a per-network throughput memory | iOS, tvOS |
SuperPlayerPreload |
Feed prefetch over a PlayerPool |
iOS, tvOS |
SuperPlayerResilience |
The same failure taxonomy, and the rungs of the fallback ladder that port | iOS, tvOS |
SuperPlayerDRM |
FairPlay, and an offline licence store | iOS, tvOS; offline licences iOS only |
SuperPlayerOffline |
HLS downloads into a directory you name | iOS only |
SuperPlayerTV |
The display matched to the content | tvOS |
SuperPlayerDiagnostics |
The manifest doctor, the trace bundle, a debug HUD | iOS, tvOS |
SuperPlayerRealtime |
RealtimePlayer: encoded frames from a transport you supply |
iOS, tvOS |
SuperPlayerTestkit |
A loopback-origin harness, fault scripts, throughput traces, FrameSourceConformance |
iOS, tvOS |
SuperPlayerTestMedia |
Synthetic HLS streams and the hostile-manifest corpus | iOS, tvOS |
SuperPlayerMoQ (Media over QUIC through RealtimePlayer) is a separate, unpublished package under
ios/MoQ/, iOS only — see Status.
Needs JDK 17 and an Android SDK (ANDROID_HOME, or sdk.dir in android/local.properties). The Gradle
wrapper fetches everything else.
(cd android && ./gradlew assemble check) # build, tests, lint, API surface, format, repo verificationcheck is the complete definition of passing — the same command CI runs. There are four Gradle
builds under android/, not one: the library, build-logic/ (an included build of convention plugins),
demo/ and benchmark/. The last two are separate builds that resolve the library from Maven
coordinates, exactly as an adopter would, so a malformed POM or a missing transitive dependency
fails here instead of in someone else's app:
(cd android && ./gradlew publishToMavenLocal)
(cd android/demo && ./gradlew assembleDebug lintDebug spotlessCheck)Needs macOS and Xcode with an iOS simulator runtime; a tvOS runtime for the tvOS build.
ios/check # format, simulator tests, tvOS build, API baselinesios/check is the Swift package's whole definition of passing, and the macOS CI job runs it alone
(ADR-0021
says why it is a script rather than a Gradle task). The demo is an Xcode project under
ios/Demo/ that resolves the package by path.
This is the part worth copying even if the library is not for you.
Tests drive the library through its public API, under Robolectric, against Media3's own fakes, with no device and no network. Nothing asserts past the facade. A shaped network is a replayed throughput trace; a CDN failure is an injected fault addressed at a host; a Widevine device and a licence server are stated, not mocked at the call site.
Two rules make it hold up. Every stand-in's limits are written down — docs/testing.md says
what each one cannot show, and a claim that needs a device is labelled as needing one rather than
faked. And behaviour changes appear in review as a readable diff: golden session traces are
committed, so a change that moves a metric shows up as a line, not as a number someone notices
later.
On iOS the same rules hold with one honest difference: AVPlayer takes no fake transport, so the
tests are XCTest on a simulator against an HTTP origin on the loopback interface, with faults
and shaping applied at that server in the Android harness's vocabulary. Time there is real, so no
test asserts a duration and the golden traces are structural
(ADR-0021).
The synthetic streams and the hostile corpus are the same fixtures on both platforms, generated
from the Android build and committed under ios/Tests/Fixtures/.
Two suites are written to run outside this repository, against code we did not write:
HttpTransportConformance and FrameSourceConformance, each naming the rule a failure broke.
docs/README.md indexes every document and says which reader each is for. The
ones to start from:
docs/adr/README.md— the 21 decision records,ADR-0001throughADR-0022. Why the library is shaped as it is on both platforms, and the rules a change is held to.docs/ios.md— SuperPlayer on iOS and tvOS for an app author: installing by SwiftPM, the products, what ports and what is not ported by rule, and what your app declares.docs/compatibility.md— what you may depend on, and what a release does not promise.docs/testing.md— the testing strategy and every stand-in's limits.docs/telemetry-schema.md— what each metric means, with its citation.PRD.md— the problem inventory, the architecture, and the phase roadmap the issues are cut from.
Read CONTRIBUTING.md first — it is short and two of its rules are unusual:
- Clean-room discipline. No proprietary material and no mirroring a commercial SDK's API
shape. Every non-obvious algorithm carries a public
// spec:or// ref:citation, and permissive licences only. - A change that contradicts an ADR says so and argues the case. Winning that argument produces a superseding record, not an undocumented exception.
On Android, (cd android && ./gradlew assemble check) must pass, (cd android && ./gradlew spotlessApply) formats, and a public API change is committed together with its regenerated api/
diff. On iOS, ios/check must pass, and a public API change is committed with its regenerated
baseline (ios/check --update-api).
Apache-2.0 — see LICENSE. Third-party dependencies and their licences are registered
in THIRD_PARTY.md, and the one dependency this repository builds rather than
resolves is documented with its crate licences in
third-party/moq/.