Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions WEB_DRM_SUPPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Web DRM Support (Widevine)

This adds Widevine DRM support for web platforms (WASM/JS) using dash.js for DASH manifest parsing and EME for license acquisition.

## Features

- Widevine DRM playback on web browsers (Chrome, Firefox, Edge)
- DASH/MPD manifest support via dash.js
- Custom license headers for authentication
- Sample app with DRM testing UI

## Installation

### 1. Add dash.js to your HTML

In your `src/webMain/resources/index.html`, add the dash.js script before your app:

```html
<head>
<!-- dash.js for DASH/DRM playback -->
<script src="https://cdn.dashjs.org/latest/dash.all.min.js"></script>
<!-- DRM helper module -->
<script src="drm-helper.js"></script>
</head>
```

### 2. Copy drm-helper.js

Copy `mediaplayer/src/webMain/resources/drm-helper.js` to your project's `src/webMain/resources/` folder.

### 3. Enable HTTPS (Required for DRM)

EME requires a secure context. Create `webpack.config.d/https.config.js`:

```javascript
config.devServer = config.devServer || {};
config.devServer.server = 'https';
config.devServer.host = '0.0.0.0';
```

## Usage

### Basic Widevine Playback

```kotlin
import io.github.kdroidfilter.composemediaplayer.DrmConfiguration
import io.github.kdroidfilter.composemediaplayer.DrmType
import io.github.kdroidfilter.composemediaplayer.rememberVideoPlayerState

@Composable
fun DrmVideoPlayer() {
val playerState = rememberVideoPlayerState()

// Create DRM configuration
val drmConfig = DrmConfiguration(
drmType = DrmType.WIDEVINE,
licenseUrl = "https://your-license-server.com/acquire",
licenseHeaders = mapOf(
"Authorization" to "Bearer your-token"
)
)

// Open DRM-protected content
LaunchedEffect(Unit) {
playerState.openUri(
uri = "https://example.com/content.mpd",
drmConfiguration = drmConfig
)
}

VideoPlayerSurface(
playerState = playerState,
modifier = Modifier.fillMaxSize()
)
}
```

### With Custom Headers (AxDRM Example)

```kotlin
val drmConfig = DrmConfiguration(
drmType = DrmType.WIDEVINE,
licenseUrl = "https://drm-widevine-licensing.axtest.net/AcquireLicense",
licenseHeaders = mapOf(
"X-AxDRM-Message" to "eyJhbGciOiJIUzI1NiIs..." // Your JWT token
)
)

playerState.openUri(
uri = "https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd",
drmConfiguration = drmConfig
)
```

### Non-DRM Playback (Unchanged)

For regular content without DRM, use the standard API:

```kotlin
playerState.openUri("https://example.com/video.mp4")
```

## DrmConfiguration Options

| Parameter | Type | Description |
|-----------|------|-------------|
| `drmType` | `DrmType` | DRM system: `WIDEVINE`, `PLAYREADY`, or `CLEARKEY` |
| `licenseUrl` | `String` | License server URL |
| `licenseHeaders` | `Map<String, String>` | Custom HTTP headers for license requests |

## Platform Support

| Platform | Status |
|----------|--------|
| Web (WASM/JS) | ✅ Widevine supported |
| Android | 🔜 Planned (ExoPlayer DRM) |
| iOS | 🔜 Planned (FairPlay) |
| Desktop | 🔜 Planned |

## Troubleshooting

### "No supported version of EME detected"
- Make sure you're accessing the page via HTTPS (or localhost)
- Check that the browser supports Widevine

### License request fails
- Verify the license URL is correct
- Check that custom headers are properly formatted
- Ensure CORS is configured on the license server

### Video doesn't play
- Confirm the manifest URL is a valid DASH/MPD file
- Check browser console for `[DRM]` log messages
- Verify the content is encrypted with Widevine

## Test Streams

AxDRM test content for development:

```
URL: https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd
License: https://drm-widevine-licensing.axtest.net/AcquireLicense
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.github.kdroidfilter.composemediaplayer

/**
* DRM type enumeration for supported DRM systems.
* Currently only Widevine is supported for web platform.
* ClearKey is included for testing purposes.
*/
enum class DrmType(val keySystem: String) {
WIDEVINE("com.widevine.alpha"),
PLAYREADY("com.microsoft.playready"),
CLEARKEY("org.w3.clearkey")
}

/**
* Configuration for DRM-protected content playback.
*
* @property drmType The DRM system to use
* @property licenseUrl The URL to request licenses from
* @property licenseHeaders Optional HTTP headers to include in license requests
* @property initDataTypes Initialization data types (e.g., "cenc", "keyids", "webm")
*/
data class DrmConfiguration(
val drmType: DrmType,
val licenseUrl: String,
val licenseHeaders: Map<String, String> = emptyMap(),
val initDataTypes: List<String> = listOf("cenc", "keyids", "webm")
) {
companion object {
/**
* Creates a Widevine DRM configuration.
*
* @param licenseUrl The Widevine license server URL
* @param headers Optional headers for license requests
*/
fun widevine(
licenseUrl: String,
headers: Map<String, String> = emptyMap()
) = DrmConfiguration(
drmType = DrmType.WIDEVINE,
licenseUrl = licenseUrl,
licenseHeaders = headers
)

/**
* Creates a ClearKey DRM configuration.
*
* @param licenseUrl The ClearKey license server URL
* @param headers Optional headers for license requests
*/
fun clearKey(
licenseUrl: String,
headers: Map<String, String> = emptyMap()
) = DrmConfiguration(
drmType = DrmType.CLEARKEY,
licenseUrl = licenseUrl,
licenseHeaders = headers
)

/**
* Creates a PlayReady DRM configuration.
*
* @param licenseUrl The PlayReady license server URL
* @param headers Optional headers for license requests
*/
fun playReady(
licenseUrl: String,
headers: Map<String, String> = emptyMap()
) = DrmConfiguration(
drmType = DrmType.PLAYREADY,
licenseUrl = licenseUrl,
licenseHeaders = headers
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,27 @@ interface VideoPlayerState {
* Opens a video file or URL for playback.
*/
fun openUri(uri: String, initializeplayerState: InitialPlayerState = InitialPlayerState.PLAY)

/**
* Opens a video URL with DRM configuration for protected content.
*
* On web platforms, this will initialize EME (Encrypted Media Extensions) with the
* specified DRM configuration. On other platforms, DRM may not be supported and
* the drmConfiguration parameter will be ignored.
*
* @param uri The media URI to open
* @param drmConfiguration The DRM configuration (license URL, headers, etc.)
* @param initializeplayerState Controls whether playback should start automatically
*/
fun openUri(
uri: String,
drmConfiguration: DrmConfiguration?,
initializeplayerState: InitialPlayerState = InitialPlayerState.PLAY
) {
// Default implementation ignores DRM config - platforms override as needed
openUri(uri, initializeplayerState)
}

fun openFile(file: PlatformFile, initializeplayerState: InitialPlayerState = InitialPlayerState.PLAY)

// Error handling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ open class DefaultVideoPlayerState: VideoPlayerState {
// Source URI of the current media
private var _sourceUri by mutableStateOf<String?>(null)
val sourceUri: String? get() = _sourceUri

// DRM configuration for protected content
private var _drmConfiguration by mutableStateOf<DrmConfiguration?>(null)
val drmConfiguration: DrmConfiguration? get() = _drmConfiguration

// Playback state properties
private var _isPlaying by mutableStateOf(false)
Expand Down Expand Up @@ -232,12 +236,31 @@ open class DefaultVideoPlayerState: VideoPlayerState {
* @param initializeplayerState Controls whether playback should start automatically after opening
*/
override fun openUri(uri: String, initializeplayerState: InitialPlayerState) {
openUri(uri, null, initializeplayerState)
}

/**
* Opens a media source from the given URI with optional DRM configuration.
*
* On web platforms, this will initialize EME (Encrypted Media Extensions) with the
* specified DRM configuration when provided.
*
* @param uri The URI of the media to open
* @param drmConfiguration The DRM configuration for protected content, or null for unprotected content
* @param initializeplayerState Controls whether playback should start automatically after opening
*/
override fun openUri(
uri: String,
drmConfiguration: DrmConfiguration?,
initializeplayerState: InitialPlayerState
) {
playerScope.coroutineContext.cancelChildren()

// Store the URI for potential replay after stop
lastUri = uri

_sourceUri = uri
_drmConfiguration = drmConfiguration
_hasMedia = true
_isLoading = true // Set initial loading state
_error = null
Expand Down Expand Up @@ -299,6 +322,7 @@ open class DefaultVideoPlayerState: VideoPlayerState {
override fun stop() {
_isPlaying = false
_sourceUri = null
_drmConfiguration = null
_hasMedia = false
_isLoading = false
sliderPos = 0.0f
Expand Down
Loading