Skip to content

Feature: Offline Playback and Offline DRM - #106

Merged
daytime-em merged 33 commits into
mainfrom
feature/offline-drm
Aug 12, 2026
Merged

daytime-em merged 33 commits into
mainfrom
feature/offline-drm

Conversation

@daytime-em

@daytime-em daytime-em commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

Time to merge the feature branch!


Note

High Risk
Large new surface area touching DRM licensing, foreground download services, on-disk cache/index, and playback routing—integration and permission mistakes can break downloads or Play policy.

Overview
Adds offline download and playback for Mux HLS (including CMAF), with optional offline Widevine licenses, progress via a foreground MuxDownloadService, and playback through mux_offline media items.

The library introduces MuxDownloadManager (start/remove/listen/query, including EXPIRED when offline licenses are spent), MuxPlayerDownloadStore, MuxHlsDownloadCallback (manual stream keys for Mux CMAF + license keySetId), and MuxOfflineCmafHlsMediaSource (captures playlist DRM init data). MediaItems.forMuxDownload and MuxMediaSourceFactory route mux_offline:// items to OfflinePlaybackMediaSource (cache-only, local DRM playback). Sensitive manifest entries are not merged from the library; app and automatedtests document required permissions and services.

The sample app gains a Compose “Offline Downloads” flow (list, pick asset, play from disk). executePost is fixed for DRM provisioning; unit and instrumentation tests cover playlist capture, download callbacks, license expiry, and end-to-end download/playback.

Reviewed by Cursor Bugbot for commit ab22071. Bugbot is set up for automated code reviews on this repo. Configure here.

daytime-em and others added 30 commits June 5, 2026 10:51
Design for offline playback of Widevine-protected HLS on Media3 1.9.2
(verified through 1.10.1). Covers the playlist-parser observer approach
for acquiring a single offline license, the com.mux.player.offline
architecture/API (store, wired DownloadHelper, MuxDrmDownloadCallback,
provider extensions, SDK-provided DownloadService + facade), download
orchestration, and deferred encrypted-audio multi-key work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert the §5 end-to-end flow and §8 orchestration ASCII diagrams to
mermaid, and add a §9 component diagram showing the com.mux.player.offline
reuse boundary (reusable vs Mux-example) and how the facade, store, and
service wire together.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ls as background

Lead with the v1 architecture & API; recast the Media3-internals material
(PSSH location, why the naive DownloadHelper path fails, the observer, and
download orchestration) as a Background section explaining the one piece of
added PSSH-extraction logic. Drop the standalone "where the PSSH lives"
section, folding its protectionSchemes gotcha into the observer background.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lead §1 with the customer-visible entry points (facade, createDownloadHelper)
and the SDK-provided service, then the internal implementation. Mark
createHlsDownloadHelper internal — the public factory is createDownloadHelper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document writer-safe removal via DownloadManager.removeDownload (cancel +
delete serialized per-id on the manager thread; never hand-delete the cache),
the remove(playbackId) flow with keyset release, and playback restore. Use
playbackId in the facade remove() signature. Flag keyset-release creds /
token-expiry as an open question.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop offline keysets locally via MediaDrm.removeOfflineLicense (API 29+),
never the networked MODE_RELEASE — Mux enforces no offline-license quota;
pre-29 drops the reference only. Make the playbackId→keySetId association
explicit: it's a single DownloadIndex row (id = playbackId via the id-taking
getDownloadRequest overload, license in the key_set_id column), no separate
keyset store. Resolves the keyset-release open question.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cement the requirement that the callback selects the top video variant plus
every audio and subtitle rendition (SelectionOverride per group across audio
and text renderers). These are clear today, so it only widens which media is
fetched — single video key is unchanged. Reframe §4 so rendition selection is
v1 and only the multi-key DRM handling stays deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the playback strategy: MediaSource building block keyed by playbackId
(createOfflineMediaSource over the download cache + MODE_PLAYBACK restore),
and Mux-typed enumeration from the DownloadIndex. Enumeration is suspend
(blocking SQLite; COMPLETED is terminal so it's not in getCurrentDownloads);
null is the not-downloaded signal — no isDownloaded. Add cacheOnlyDataSource
to the store; reconcile §1.8/§2.4 playback restore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap offline MediaSource creation for SDK users: MediaItems.fromMuxDownload
builds a synchronous mux_offline://<playbackId> MediaItem, and
MuxMediaSourceFactory.createMediaSource routes that scheme to an internal
offline source builder (single indexed getDownload + MODE_PLAYBACK +
cache-only), delegating all online items to the inner factory unchanged.
Async is now needed only to LIST downloads; building a MediaItem and
preparing is synchronous. Drop the public suspend createOfflineMediaSource.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose *Future twins (downloadsFuture/downloadedFuture/downloadFuture)
alongside the suspend reads, sharing one blocking query on the store's
ioExecutor. Guava is already on the classpath via Media3; CompletableFuture
is API 24 (above minSdk 23). Facade stays a plain object with no @JvmStatic
so Java calls via INSTANCE and the methods stay mockable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design for offline playback of Widevine-protected HLS on Media3 1.9.2
(verified through 1.10.1). Covers the playlist-parser observer approach
for acquiring a single offline license, the com.mux.player.offline
architecture/API (store, wired DownloadHelper, MuxDrmDownloadCallback,
provider extensions, SDK-provided DownloadService + facade), download
orchestration, and deferred encrypted-audio multi-key work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert the §5 end-to-end flow and §8 orchestration ASCII diagrams to
mermaid, and add a §9 component diagram showing the com.mux.player.offline
reuse boundary (reusable vs Mux-example) and how the facade, store, and
service wire together.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ls as background

Lead with the v1 architecture & API; recast the Media3-internals material
(PSSH location, why the naive DownloadHelper path fails, the observer, and
download orchestration) as a Background section explaining the one piece of
added PSSH-extraction logic. Drop the standalone "where the PSSH lives"
section, folding its protectionSchemes gotcha into the observer background.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lead §1 with the customer-visible entry points (facade, createDownloadHelper)
and the SDK-provided service, then the internal implementation. Mark
createHlsDownloadHelper internal — the public factory is createDownloadHelper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document writer-safe removal via DownloadManager.removeDownload (cancel +
delete serialized per-id on the manager thread; never hand-delete the cache),
the remove(playbackId) flow with keyset release, and playback restore. Use
playbackId in the facade remove() signature. Flag keyset-release creds /
token-expiry as an open question.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop offline keysets locally via MediaDrm.removeOfflineLicense (API 29+),
never the networked MODE_RELEASE — Mux enforces no offline-license quota;
pre-29 drops the reference only. Make the playbackId→keySetId association
explicit: it's a single DownloadIndex row (id = playbackId via the id-taking
getDownloadRequest overload, license in the key_set_id column), no separate
keyset store. Resolves the keyset-release open question.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cement the requirement that the callback selects the top video variant plus
every audio and subtitle rendition (SelectionOverride per group across audio
and text renderers). These are clear today, so it only widens which media is
fetched — single video key is unchanged. Reframe §4 so rendition selection is
v1 and only the multi-key DRM handling stays deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the playback strategy: MediaSource building block keyed by playbackId
(createOfflineMediaSource over the download cache + MODE_PLAYBACK restore),
and Mux-typed enumeration from the DownloadIndex. Enumeration is suspend
(blocking SQLite; COMPLETED is terminal so it's not in getCurrentDownloads);
null is the not-downloaded signal — no isDownloaded. Add cacheOnlyDataSource
to the store; reconcile §1.8/§2.4 playback restore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap offline MediaSource creation for SDK users: MediaItems.fromMuxDownload
builds a synchronous mux_offline://<playbackId> MediaItem, and
MuxMediaSourceFactory.createMediaSource routes that scheme to an internal
offline source builder (single indexed getDownload + MODE_PLAYBACK +
cache-only), delegating all online items to the inner factory unchanged.
Async is now needed only to LIST downloads; building a MediaItem and
preparing is synchronous. Drop the public suspend createOfflineMediaSource.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose *Future twins (downloadsFuture/downloadedFuture/downloadFuture)
alongside the suspend reads, sharing one blocking query on the store's
ioExecutor. Guava is already on the classpath via Media3; CompletableFuture
is API 24 (above minSdk 23). Facade stays a plain object with no @JvmStatic
so Java calls via INSTANCE and the methods stay mockable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add playback path via MediaItems and MuxMediaSourceFactory

* Add MediaItems.forMuxDownload

* Add kdoc

* oh wait lol

* Path segments

* And we do the lookup in a MediaSource like thus

* Brief comments

* And we don't need to worry about SimpleCache init

* Poll for download percentage
* Add Example screen for offline downloads

* Fix unstable-api
@daytime-em
daytime-em requested a review from a team as a code owner August 12, 2026 23:13
@github-actions github-actions Bot added the review:medium 1 human approval required label Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

🔎 Observe-only. This grade is informational and does not block merging — no approvals are enforced. We are collecting rulings before turning on any policy.

AI Review Classification — review:medium

Requirement to merge: 1 human approval

Layer Result
Rules (engine) score 4.7 (medium), fired floor: medium
Routing route-to-llm
Learned calibration 5.6 (medium)
LLM judge 4.5 (medium) — Large new offline-download/DRM feature (foreground service, PlatformScheduler, SQLite index, Widevine offline license acquisition) added to a video SDK, but it's cleanly gated behind a new opt-in code path — the library manifest declares nothing and existing streaming only sees a minor createMediaSource override plus an executePost signature change. Main risks are the small touch to the shared DRM provisioning path (executePost now defaults headers and forces POST) and untested-in-CI DRM instrumentation, but blast radius on existing users is contained.
Full audit — which factors fired and why

PR Risk Assessment — MEDIUM (score 4.7/10)

Signals observed:

  • auth_session_token (+2.0, floor=medium) — added lines matched weak auth marker /(?i)\btoken\b/ (non-test)
  • production_code_change (+2.8) — 20 production source file(s) changed — additive-dominant scaffolding, damped
  • new_abstraction_complexity (+0.4) — new file app/src/main/java/com/mux/player/media3/examples/offline/OfflineDownloadsActivity.kt introduces abstraction (/(?m)^\s*(public\s+)?(abstract\s+)?(class|interface)\s+\w+/) [stacking decay x0.4 at rank 4]
  • production_repo (+0.1) — pull request targets a production repository [stacking decay x0.15 at rank 6]
  • large_diff (+1.2) — 3438 lines changed (>= 500 tier) [stacking decay x0.6 at rank 3]
  • gradle_build_chain (+0.2) — core build-system change (app/build.gradle.kts) [stacking decay x0.25 at rank 5]

Reductions applied:

  • tests_cover_changed_code (-2.0) — test automatedtests/build.gradle.kts covers changed app/build.gradle.kts

Scoring:

  • base (inherent): 6.7
  • after reductions: 4.7
  • binding floor: none
  • final: 4.7 → medium

Recommended review: review:medium — one human reviewer recommended before merge.

Grade this call — react on this comment: 👍 right · 👎 wrong · 🚀 graded too low · 👀 graded too high.
Strongly disagree? Reply /ai-review disagree should=<trivial|low|medium|high> <reason>.

@daytime-em daytime-em changed the title Feature/ Offline Playback and Offline DRM Feature: Offline Playback and Offline DRM Aug 12, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ab22071. Configure here.

@daytime-em
daytime-em merged commit c254447 into main Aug 12, 2026
3 checks passed
@daytime-em
daytime-em deleted the feature/offline-drm branch August 12, 2026 23:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:medium 1 human approval required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants