Repository navigation
Feature: Offline Playback and Offline DRM - #106
Conversation
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>
…droid into feature/offline-drm
…ur CMAF DRM streams (#97)
* 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
AI Review Classification —
|
| 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, dampednew_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>.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
❌ 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.

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 throughmux_offlinemedia items.The library introduces
MuxDownloadManager(start/remove/listen/query, includingEXPIREDwhen offline licenses are spent),MuxPlayerDownloadStore,MuxHlsDownloadCallback(manual stream keys for Mux CMAF + licensekeySetId), andMuxOfflineCmafHlsMediaSource(captures playlist DRM init data).MediaItems.forMuxDownloadandMuxMediaSourceFactoryroutemux_offline://items toOfflinePlaybackMediaSource(cache-only, local DRM playback). Sensitive manifest entries are not merged from the library;appandautomatedtestsdocument required permissions and services.The sample app gains a Compose “Offline Downloads” flow (list, pick asset, play from disk).
executePostis 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.