640 lines
19 KiB
Markdown
640 lines
19 KiB
Markdown
# VLC Player Migration Plan for Android App
|
|
|
|
## Current State Analysis
|
|
- **Location**: `/home/ren/futbol/`
|
|
- **Current Player**: ExoPlayer/Media3 1.5.0
|
|
- **Main Files**:
|
|
- `app/src/main/java/com/streamplayer/PlayerActivity.java`
|
|
- `app/src/main/java/com/streamplayer/StreamUrlResolver.java`
|
|
- `app/src/main/res/layout/activity_player.xml`
|
|
- **Key Features**: HLS streams, custom headers, retry logic, loading indicators, error handling
|
|
|
|
## Target State
|
|
- **New Player**: libvlc for Android (VLC Android SDK)
|
|
- **DRM Support**: ClearKey DRM for Disney+ streams
|
|
- **Preserve**: All existing functionality
|
|
|
|
---
|
|
|
|
## Phase 1: Build Configuration Changes
|
|
|
|
### File: `app/build.gradle`
|
|
|
|
**Remove Media3 dependencies:**
|
|
```gradle
|
|
// REMOVE these lines (52-57):
|
|
implementation 'androidx.media3:media3-exoplayer:1.5.0'
|
|
implementation 'androidx.media3:media3-exoplayer-hls:1.5.0'
|
|
implementation 'androidx.media3:media3-datasource-okhttp:1.5.0'
|
|
implementation 'androidx.media3:media3-ui:1.5.0'
|
|
implementation 'androidx.media3:media3-ui-leanback:1.5.0'
|
|
implementation 'androidx.media3:media3-session:1.5.0'
|
|
```
|
|
|
|
**Add VLC Android SDK dependency:**
|
|
```gradle
|
|
// ADD after line 57:
|
|
// VLC Android SDK
|
|
implementation 'org.videolan.android:libvlc-all:3.5.4'
|
|
// Alternative: Use specific modules for smaller APK
|
|
// implementation 'org.videolan.android:libvlc:3.5.4'
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 2: Layout Changes
|
|
|
|
### File: `app/src/main/res/layout/activity_player.xml`
|
|
|
|
**Current (lines 10-15):**
|
|
```xml
|
|
<androidx.media3.ui.PlayerView
|
|
android:id="@+id/player_view"
|
|
android:layout_width="match_parent"
|
|
android:layout_height="match_parent"
|
|
app:resize_mode="fill"
|
|
app:use_controller="true" />
|
|
```
|
|
|
|
**Replace with VLC SurfaceView:**
|
|
```xml
|
|
<org.videolan.libvlc.util.VLCVideoLayout
|
|
android:id="@+id/player_view"
|
|
android:layout_width="match_parent"
|
|
android:layout_height="match_parent" />
|
|
|
|
<!-- OR use SurfaceView directly for more control -->
|
|
<SurfaceView
|
|
android:id="@+id/player_surface"
|
|
android:layout_width="match_parent"
|
|
android:layout_height="match_parent" />
|
|
```
|
|
|
|
**Note**: Keep all other UI elements unchanged (toolbar, loading indicator, error message).
|
|
|
|
---
|
|
|
|
## Phase 3: PlayerActivity Rewrite
|
|
|
|
### File: `app/src/main/java/com/streamplayer/PlayerActivity.java`
|
|
|
|
**Import Changes:**
|
|
```java
|
|
// REMOVE these imports (14-28):
|
|
import androidx.media3.exoplayer.ExoPlayer;
|
|
import androidx.media3.common.MediaItem;
|
|
import androidx.media3.common.PlaybackException;
|
|
import androidx.media3.common.Player;
|
|
import androidx.media3.exoplayer.DefaultRenderersFactory;
|
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
|
|
import androidx.media3.datasource.okhttp.OkHttpDataSource;
|
|
import androidx.media3.exoplayer.source.MediaSource;
|
|
import androidx.media3.exoplayer.hls.HlsMediaSource;
|
|
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
|
import androidx.media3.ui.PlayerView;
|
|
import androidx.media3.common.util.Util;
|
|
import androidx.annotation.OptIn;
|
|
import androidx.media3.common.util.UnstableApi;
|
|
|
|
// ADD VLC imports:
|
|
import org.videolan.libvlc.LibVLC;
|
|
import org.videolan.libvlc.Media;
|
|
import org.videolan.libvlc.MediaPlayer;
|
|
import org.videolan.libvlc.interfaces.IVLCVout;
|
|
import org.videolan.libvlc.util.VLCVideoLayout;
|
|
import android.view.SurfaceView;
|
|
import android.view.SurfaceHolder;
|
|
```
|
|
|
|
**Class Member Variables (lines 51-66):**
|
|
```java
|
|
// REPLACE:
|
|
private PlayerView playerView;
|
|
private ExoPlayer player;
|
|
private DefaultTrackSelector trackSelector;
|
|
|
|
// WITH:
|
|
private VLCVideoLayout playerView; // OR SurfaceView playerSurface
|
|
private LibVLC libVlc;
|
|
private MediaPlayer mediaPlayer;
|
|
private SurfaceView surfaceView;
|
|
```
|
|
|
|
**onCreate Method (lines 68-98):**
|
|
```java
|
|
// MODIFY initViews() call:
|
|
private void initViews() {
|
|
playerView = findViewById(R.id.player_view); // VLCVideoLayout
|
|
// OR if using SurfaceView:
|
|
surfaceView = findViewById(R.id.player_surface);
|
|
|
|
loadingIndicator = findViewById(R.id.loading_indicator);
|
|
errorMessage = findViewById(R.id.error_message);
|
|
channelLabel = findViewById(R.id.player_channel_label);
|
|
closeButton = findViewById(R.id.close_button);
|
|
playerToolbar = findViewById(R.id.player_toolbar);
|
|
|
|
closeButton.setOnClickListener(v -> finish());
|
|
playerView.setOnClickListener(v -> toggleOverlay());
|
|
// For SurfaceView:
|
|
// surfaceView.setOnClickListener(v -> toggleOverlay());
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 4: VLC Player Implementation Details
|
|
|
|
### 4.1 Initialize VLC with Custom Headers
|
|
|
|
**New Method: `startPlayback(String streamUrl)`**
|
|
```java
|
|
private void startPlayback(String streamUrl) {
|
|
try {
|
|
releasePlayer();
|
|
lastStreamUrl = streamUrl;
|
|
retryCount = 0;
|
|
|
|
// Create LibVLC instance with options
|
|
ArrayList<String> options = new ArrayList<>();
|
|
|
|
// Network options
|
|
options.add("--network-caching=1500");
|
|
options.add("--clock-jitter=0");
|
|
options.add("--clock-synchro=0");
|
|
|
|
// HTTP options for headers
|
|
options.add(":http-user-agent=" + USER_AGENT);
|
|
options.add(":http-referrer=" + "http://streamtp10.com/");
|
|
|
|
// SSL options (accept all certificates)
|
|
options.add("--no-xlib");
|
|
options.add(":rtsp-tcp");
|
|
options.add(":no-cert-check");
|
|
|
|
// Create LibVLC
|
|
libVlc = new LibVLC(this, options);
|
|
|
|
// Create MediaPlayer
|
|
mediaPlayer = new MediaPlayer(libVlc);
|
|
|
|
// Set up event listeners
|
|
setupMediaPlayerListeners();
|
|
|
|
// Create Media with custom headers
|
|
Media media = new Media(libVlc, android.net.Uri.parse(streamUrl));
|
|
|
|
// Add headers via Media options
|
|
Map<String, String> headers = new HashMap<>();
|
|
headers.put("Referer", "http://streamtp10.com/");
|
|
headers.put("User-Agent", USER_AGENT);
|
|
media.addOption(":http-user-agent=" + USER_AGENT);
|
|
media.addOption(":http-referrer=http://streamtp10.com/");
|
|
|
|
mediaPlayer.setMedia(media);
|
|
media.release();
|
|
|
|
// Set up video output
|
|
IVLCVout vout = mediaPlayer.getVLCVout();
|
|
if (playerView instanceof VLCVideoLayout) {
|
|
vout.setVideoView(playerView);
|
|
} else if (surfaceView != null) {
|
|
vout.setVideoView(surfaceView);
|
|
}
|
|
vout.attachViews();
|
|
|
|
// Start playback
|
|
mediaPlayer.play();
|
|
setOverlayVisible(false);
|
|
|
|
} catch (Exception e) {
|
|
showError("Error al inicializar reproductor: " + e.getMessage());
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4.2 Player Event Listeners
|
|
|
|
**New Method: `setupMediaPlayerListeners()`**
|
|
```java
|
|
private void setupMediaPlayerListeners() {
|
|
// MediaPlayer.EventListener using the VLC event system
|
|
mediaPlayer.setEventListener(new MediaPlayer.EventListener() {
|
|
@Override
|
|
public void onEvent(MediaPlayer.Event event) {
|
|
switch (event.type) {
|
|
case MediaPlayer.Event.Playing:
|
|
// Equivalent to STATE_READY
|
|
runOnUiThread(() -> {
|
|
showLoading(false);
|
|
retryCount = 0;
|
|
});
|
|
break;
|
|
|
|
case MediaPlayer.Event.Buffering:
|
|
// Buffering state (0.0 to 1.0)
|
|
float bufferPercent = event.getBuffering();
|
|
runOnUiThread(() -> showLoading(bufferPercent < 1.0f));
|
|
break;
|
|
|
|
case MediaPlayer.Event.EncounteredError:
|
|
// Error occurred
|
|
runOnUiThread(() -> handlePlaybackError("Error de reproducción VLC"));
|
|
break;
|
|
|
|
case MediaPlayer.Event.EndReached:
|
|
// Stream ended
|
|
runOnUiThread(() -> finish());
|
|
break;
|
|
|
|
case MediaPlayer.Event.Stopped:
|
|
// Playback stopped
|
|
break;
|
|
|
|
case MediaPlayer.Event.Paused:
|
|
// Playback paused
|
|
break;
|
|
|
|
case MediaPlayer.Event.Opening:
|
|
// Stream opening
|
|
runOnUiThread(() -> showLoading(true));
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
### 4.3 Error Handling with Retry
|
|
|
|
**New Method: `handlePlaybackError(String errorMessage)`**
|
|
```java
|
|
private void handlePlaybackError(String errorMsg) {
|
|
boolean isRetryableError =
|
|
errorMsg.contains("404") ||
|
|
errorMsg.contains("403") ||
|
|
errorMsg.contains("timeout") ||
|
|
errorMsg.contains("Network") ||
|
|
errorMsg.contains("Connection");
|
|
|
|
if (isRetryableError && retryCount < MAX_RETRIES) {
|
|
retryCount++;
|
|
runOnUiThread(() -> {
|
|
showLoading(true);
|
|
showError("Error de conexión. Reintentando... (" + retryCount + "/" + MAX_RETRIES + ")");
|
|
});
|
|
|
|
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
|
|
if (lastStreamUrl != null) {
|
|
startPlayback(lastStreamUrl);
|
|
} else {
|
|
loadChannel();
|
|
}
|
|
}, 2000);
|
|
} else {
|
|
String finalMessage = "Error al reproducir: " + errorMsg;
|
|
if (retryCount >= MAX_RETRIES) {
|
|
finalMessage += "\n\nSe agotaron los reintentos (" + MAX_RETRIES + ").";
|
|
}
|
|
showError(finalMessage);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4.4 Player Lifecycle Methods
|
|
|
|
**Replace releasePlayer() (lines 257-262):**
|
|
```java
|
|
private void releasePlayer() {
|
|
if (mediaPlayer != null) {
|
|
mediaPlayer.stop();
|
|
mediaPlayer.getVLCVout().detachViews();
|
|
mediaPlayer.release();
|
|
mediaPlayer = null;
|
|
}
|
|
if (libVlc != null) {
|
|
libVlc.release();
|
|
libVlc = null;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Update lifecycle methods:**
|
|
```java
|
|
@Override
|
|
protected void onStart() {
|
|
super.onStart();
|
|
if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
|
|
mediaPlayer.play();
|
|
} else if (channelUrl != null && libVlc == null) {
|
|
loadChannel();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onResume() {
|
|
super.onResume();
|
|
if (mediaPlayer != null) {
|
|
mediaPlayer.play();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onPause() {
|
|
super.onPause();
|
|
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
|
|
mediaPlayer.pause();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onStop() {
|
|
super.onStop();
|
|
// Keep player for quick resume, don't release
|
|
}
|
|
|
|
@Override
|
|
protected void onDestroy() {
|
|
super.onDestroy();
|
|
releasePlayer();
|
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5: DRM Support (ClearKey)
|
|
|
|
### ClearKey DRM Implementation for Disney+
|
|
|
|
**New Class: `VlcDrmManager.java`**
|
|
**Location**: `app/src/main/java/com/streamplayer/VlcDrmManager.java`
|
|
|
|
```java
|
|
package com.streamplayer;
|
|
|
|
import org.videolan.libvlc.Media;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* Handles DRM configuration for VLC Media Player
|
|
* Supports ClearKey DRM for Disney+ and other streaming services
|
|
*/
|
|
public class VlcDrmManager {
|
|
|
|
// ClearKey DRM configuration
|
|
private static final String CLEARKEY_KEY_SYSTEM = "org.w3.clearkey";
|
|
|
|
/**
|
|
* Configure ClearKey DRM for a Media object
|
|
* @param media The VLC Media object
|
|
* @param keyId The key ID (extracted from manifest or license server)
|
|
* @param key The content key
|
|
*/
|
|
public static void configureClearKey(Media media, String keyId, String key) {
|
|
if (media == null || keyId == null || key == null) {
|
|
return;
|
|
}
|
|
|
|
// VLC uses a specific format for ClearKey
|
|
// Format: keyid:key
|
|
String keyPair = keyId + ":" + key;
|
|
media.addOption(":demux=avformat");
|
|
media.addOption(":key-format=" + CLEARKEY_KEY_SYSTEM);
|
|
media.addOption(":key=" + keyPair);
|
|
}
|
|
|
|
/**
|
|
* Configure ClearKey DRM with multiple keys
|
|
* @param media The VLC Media object
|
|
* @param keys Map of keyId -> key pairs
|
|
*/
|
|
public static void configureClearKey(Media media, Map<String, String> keys) {
|
|
if (media == null || keys == null || keys.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
media.addOption(":demux=avformat");
|
|
media.addOption(":key-format=" + CLEARKEY_KEY_SYSTEM);
|
|
|
|
for (Map.Entry<String, String> entry : keys.entrySet()) {
|
|
String keyPair = entry.getKey() + ":" + entry.getValue();
|
|
media.addOption(":key=" + keyPair);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configure Widevine DRM (for reference, if needed later)
|
|
* VLC supports Widevine via specific options
|
|
*/
|
|
public static void configureWidevine(Media media, String drmServerUrl) {
|
|
if (media == null || drmServerUrl == null) {
|
|
return;
|
|
}
|
|
|
|
media.addOption(":drm=widevine");
|
|
media.addOption(":aes-key=" + drmServerUrl);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Extracting ClearKey Keys from Stream
|
|
|
|
**Update `StreamUrlResolver.java` to extract DRM info:**
|
|
|
|
Add this method to `StreamUrlResolver`:
|
|
```java
|
|
/**
|
|
* Extract ClearKey DRM keys from JSON in HTML
|
|
* @return Map of keyId -> key pairs, or null if no DRM found
|
|
*/
|
|
public static Map<String, String> extractClearKeyKeys(String html) {
|
|
Map<String, String> keys = new HashMap<>();
|
|
|
|
try {
|
|
// Pattern to find ClearKey key IDs and keys
|
|
// Common patterns in Disney+ and similar services
|
|
Pattern clearkeyPattern = Pattern.compile(
|
|
"\"kid\"\\s*:\\s*\"([^\"]+)\".*?\"k\"\\s*:\\s*\"([^\"]+)\"",
|
|
Pattern.DOTALL
|
|
);
|
|
|
|
Matcher matcher = clearkeyPattern.matcher(html);
|
|
while (matcher.find()) {
|
|
String keyId = matcher.group(1);
|
|
String key = matcher.group(2);
|
|
keys.put(keyId, key);
|
|
}
|
|
|
|
// Alternative pattern for JWPlayer with DRM
|
|
Pattern jwDrmPattern = Pattern.compile(
|
|
"\"drm\"\\s*:\\s*\\{[^}]*\"clearkey\"\\s*:\\s*\\{[^}]*\"keyId\"\\s*:\\s*\"([^\"]+)\"[^}]*\"key\"\\s*:\\s*\"([^\"]+)\"",
|
|
Pattern.DOTALL
|
|
);
|
|
|
|
Matcher jwMatcher = jwDrmPattern.matcher(html);
|
|
while (jwMatcher.find()) {
|
|
String keyId = jwMatcher.group(1);
|
|
String key = jwMatcher.group(2);
|
|
keys.put(keyId, key);
|
|
}
|
|
} catch (Exception e) {
|
|
// Return empty map on error
|
|
}
|
|
|
|
return keys.isEmpty() ? null : keys;
|
|
}
|
|
```
|
|
|
|
### Using DRM in PlayerActivity
|
|
|
|
**Modify `startPlayback()` to handle DRM:**
|
|
```java
|
|
// After loading stream URL, also check for DRM keys
|
|
new Thread(() -> {
|
|
try {
|
|
String resolvedUrl = StreamUrlResolver.resolve(channelUrl);
|
|
Map<String, String> drmKeys = StreamUrlResolver.extractClearKeyKeys(html); // Need to modify resolver to also return HTML
|
|
|
|
runOnUiThread(() -> startPlayback(resolvedUrl, drmKeys));
|
|
} catch (IOException e) {
|
|
runOnUiThread(() -> showError("No se pudo conectar con el canal: " + e.getMessage()));
|
|
}
|
|
}).start();
|
|
|
|
private void startPlayback(String streamUrl, Map<String, String> drmKeys) {
|
|
// ... existing VLC setup code ...
|
|
|
|
Media media = new Media(libVlc, android.net.Uri.parse(streamUrl));
|
|
|
|
// Add headers
|
|
media.addOption(":http-user-agent=" + USER_AGENT);
|
|
media.addOption(":http-referrer=http://streamtp10.com/");
|
|
|
|
// Add DRM if available
|
|
if (drmKeys != null && !drmKeys.isEmpty()) {
|
|
VlcDrmManager.configureClearKey(media, drmKeys);
|
|
}
|
|
|
|
// ... rest of the setup ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 6: Additional Files
|
|
|
|
### New File: `VlcPlayerConfig.java`
|
|
**Location**: `app/src/main/java/com/streamplayer/VlcPlayerConfig.java`
|
|
|
|
```java
|
|
package com.streamplayer;
|
|
|
|
/**
|
|
* Configuration constants for VLC Player
|
|
*/
|
|
public class VlcPlayerConfig {
|
|
|
|
// Network caching (ms)
|
|
public static final int NETWORK_CACHING = 1500;
|
|
|
|
// Live streaming caching (ms)
|
|
public static final int LIVE_CACHING = 5000;
|
|
|
|
// User Agent
|
|
public static final String USER_AGENT =
|
|
"Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36";
|
|
|
|
// Hardware acceleration options
|
|
public static final String HW_ACCELERATION = "automatic"; // or "full", "none", "decoding", "rendering"
|
|
|
|
// Chroma format
|
|
public static final String CHROMA = "RV32"; // or YV12, NV12
|
|
|
|
// Audio output
|
|
public static final String AUDIO_OUTPUT = "opensles";
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 7: Summary of File Changes
|
|
|
|
| File Path | Change Type | Description |
|
|
|-----------|-------------|-------------|
|
|
| `app/build.gradle` | Modify | Remove Media3 deps, add VLC SDK |
|
|
| `app/src/main/res/layout/activity_player.xml` | Modify | Replace PlayerView with VLCVideoLayout |
|
|
| `app/src/main/java/com/streamplayer/PlayerActivity.java` | Rewrite | Complete VLC integration |
|
|
| `app/src/main/java/com/streamplayer/VlcDrmManager.java` | New | DRM configuration handler |
|
|
| `app/src/main/java/com/streamplayer/VlcPlayerConfig.java` | New | VLC configuration constants |
|
|
| `app/src/main/java/com/streamplayer/StreamUrlResolver.java` | Modify | Add DRM key extraction |
|
|
|
|
---
|
|
|
|
## Phase 8: VLC-Specific Notes
|
|
|
|
### HTTP Headers in VLC
|
|
VLC handles HTTP headers differently than ExoPlayer:
|
|
- Headers are set via Media.addOption() with colon prefix
|
|
- Format: `:http-header-name=value`
|
|
- Common options:
|
|
- `:http-user-agent=<value>`
|
|
- `:http-referrer=<value>`
|
|
- `:http-cookie=<value>`
|
|
|
|
### VLC Events vs ExoPlayer States
|
|
|
|
| ExoPlayer State | VLC Event |
|
|
|----------------|-----------|
|
|
| STATE_IDLE | N/A (not initialized) |
|
|
| STATE_BUFFERING | Event.Buffering |
|
|
| STATE_READY | Event.Playing |
|
|
| STATE_ENDED | Event.EndReached |
|
|
|
|
### DRM Support Comparison
|
|
| Feature | ExoPlayer | VLC |
|
|
|---------|-----------|-----|
|
|
| Widevine | Native | Limited |
|
|
| ClearKey | Via DRM module | Via libavformat |
|
|
| PlayReady | Native | Limited |
|
|
| FairPlay | No | Limited |
|
|
|
|
---
|
|
|
|
## Phase 9: Testing Checklist
|
|
|
|
1. [ ] Basic HLS stream playback
|
|
2. [ ] HTTP headers (Referer, User-Agent) applied correctly
|
|
3. [ ] Loading indicator shows/hides correctly
|
|
4. [ ] Error messages display properly
|
|
5. [ ] Retry logic works on connection failure
|
|
6. [ ] Screen stays on during playback
|
|
7. [ ] Overlay toggle works on tap
|
|
8. [ ] Close button returns to main activity
|
|
9. [ ] App handles pause/resume correctly
|
|
10. [ ] Memory leaks checked (no retained VLC instances)
|
|
11. [ ] DRM streams play correctly (Disney+)
|
|
12. [ ] SSL certificate bypass works
|
|
|
|
---
|
|
|
|
## Phase 10: Fallback Strategy
|
|
|
|
If VLC doesn't work as expected, consider:
|
|
1. **Hybrid approach**: Keep ExoPlayer as fallback, use VLC for DRM-only streams
|
|
2. **Alternative libraries**:
|
|
- Vitamio (deprecated but still works)
|
|
- NKD-Player (wrapper around FFmpeg)
|
|
- Build custom FFmpeg integration
|
|
3. **Webview approach**: Use embedded browser for DRM content
|
|
|
|
---
|
|
|
|
## Appendix: VLC SDK Documentation Links
|
|
|
|
- VLC Android SDK: https://code.videolan.org/videolan/vlc-android
|
|
- VLC LibVLC API: https://code.videolan.org/videolan/vlc-android/-/tree/master/libvlc
|
|
- VLC Wiki on Android: https://wiki.videolan.org/AndroidCompile
|
|
- ClearKey DRM spec: https://w3c.github.io/encrypted-media/format-registry/initdata/cenc.html
|