Initial commit: dashdrm-mp4ff plugin

Add a Streamlink plugin (plugin/dashdrm_mp4ff.py) to support DRM-protected MPEG-DASH streams with optional mp4ff-decrypt integration. Include README with usage and installation notes, an interactive helper (gen.py) that reads vault.json and prints Streamlink commands, a vault.json example, pyproject.toml (declares inquirerpy dependency), .python-version, and .gitignore. This provides the plugin, packaging metadata, and tooling needed to run and test DASH-DRM streams locally.
This commit is contained in:
√(noham)²
2026-05-23 19:13:16 +02:00
commit 08e66f21d7
8 changed files with 1290 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
vault.json
.DS_Store
*__pycache__

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.11

75
README.md Normal file
View File

@@ -0,0 +1,75 @@
# dashdrm-mp4ff
Custom Streamlink plugin for DRM-protected MPEG-DASH streams.
This project is based on [titus-au/streamlink-plugin-dashdrm](https://github.com/titus-au/streamlink-plugin-dashdrm), which itself is based on Streamlink's [`dash.py`](https://github.com/streamlink/streamlink/blob/master/src/streamlink/stream/dash/dash.py).
## Install `mp4ff-decrypt`
```bash
go install github.com/Eyevinn/mp4ff/cmd/mp4ff-decrypt@latest
```
Make sure the Go bin directory is in your `PATH`, then verify:
```bash
mp4ff-decrypt -h
```
If you do not want to use `mp4ff-decrypt`, you can still use the plugin without `--dashdrm-mp4ff-use-mp4ff`.
## Usage
Run Streamlink with the local plugin directory:
```bash
streamlink --plugin-dirs "plugin" "dashdrm://https://example.com/live.mpd" best
```
With `mp4ff-decrypt` and `kid:key` pairs:
```bash
streamlink \
--plugin-dirs "plugin" \
--dashdrm-mp4ff-use-mp4ff \
--dashdrm-mp4ff-decryption-key "kid1:key1,kid2:key2,kid3:key3" \
"dashdrm://https://example.com/live.mpd" \
best
```
Notes:
- Keys can be provided as raw keys or as `kid:key`.
- When `kid:key` pairs are provided, the plugin parses the manifest and matches each track with the correct key.
- The plugin file is [`plugin/dashdrm_mp4ff.py`](./plugin/dashdrm_mp4ff.py).
## `gen.py`
[`gen.py`](./gen.py) is a small helper that reads channels from `vault.json`, lets you pick one interactively, and prints the full Streamlink command.
Install the helper dependency:
```bash
uv sync
```
Create your own `vault.json` from [`vault.json.example`](./vault.json.example), then run:
```bash
python gen.py
```
The expected structure is:
```json
{
"channels": [
{
"id": "channel-id",
"number": "1",
"name": "Channel name",
"mpd_url": "https://example.com/live.mpd",
"keys": [
"kid1:key1",
"kid2:key2"
]
}
]
}
```

46
gen.py Normal file
View File

@@ -0,0 +1,46 @@
import json
from InquirerPy import prompt
from InquirerPy.base.control import Choice
VAULT_FILE = "vault.json"
def load_vault():
try:
with open(VAULT_FILE, "r") as f:
return json.load(f)["channels"]
except FileNotFoundError:
return []
def main():
channels = load_vault()
if not channels:
print("No channels found in vault.json")
return
choices = [
Choice(value=channel, name=f"{channel.get('number', '?')} - {channel.get('name', 'Unknown')}")
for channel in channels
]
questions = [
{
"type": "list",
"name": "channel",
"message": "Select a channel to generate the streamlink command for:",
"choices": choices,
}
]
result = prompt(questions)
selected_channel = result.get("channel")
if selected_channel:
mpd_url = selected_channel.get("mpd_url", "")
keys_list = selected_channel.get("keys", [])
keys_str = ",".join(keys_list)
command = f'streamlink -l debug --plugin-dirs "./plugin" --dashdrm-mp4ff-use-mp4ff --dashdrm-mp4ff-decryption-key "{keys_str}" "dashdrm://{mpd_url}" best'
print(command)
if __name__ == "__main__":
main()

1075
plugin/dashdrm_mp4ff.py Normal file

File diff suppressed because it is too large Load Diff

9
pyproject.toml Normal file
View File

@@ -0,0 +1,9 @@
[project]
name = "dashdrm-mp4ff"
version = "0.1.0"
description = "Plugin for Streamlink to support DASH-DRM protected streams using mp4ff in option."
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"inquirerpy>=0.3.4",
]

57
uv.lock generated Normal file
View File

@@ -0,0 +1,57 @@
version = 1
revision = 3
requires-python = ">=3.11"
[[package]]
name = "dashdrm-mp4ff"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "inquirerpy" },
]
[package.metadata]
requires-dist = [{ name = "inquirerpy", specifier = ">=0.3.4" }]
[[package]]
name = "inquirerpy"
version = "0.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pfzy" },
{ name = "prompt-toolkit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/73/7570847b9da026e07053da3bbe2ac7ea6cde6bb2cbd3c7a5a950fa0ae40b/InquirerPy-0.3.4.tar.gz", hash = "sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e", size = 44431, upload-time = "2022-06-27T23:11:20.598Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/ff/3b59672c47c6284e8005b42e84ceba13864aa0f39f067c973d1af02f5d91/InquirerPy-0.3.4-py3-none-any.whl", hash = "sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4", size = 67677, upload-time = "2022-06-27T23:11:17.723Z" },
]
[[package]]
name = "pfzy"
version = "0.3.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d9/5a/32b50c077c86bfccc7bed4881c5a2b823518f5450a30e639db5d3711952e/pfzy-0.3.4.tar.gz", hash = "sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1", size = 8396, upload-time = "2022-01-28T02:26:17.946Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/d7/8ff98376b1acc4503253b685ea09981697385ce344d4e3935c2af49e044d/pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96", size = 8537, upload-time = "2022-01-28T02:26:16.047Z" },
]
[[package]]
name = "prompt-toolkit"
version = "3.0.52"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wcwidth" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
]
[[package]]
name = "wcwidth"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" },
]

24
vault.json.example Normal file
View File

@@ -0,0 +1,24 @@
{
"channels": [
{
"id": "example-channel-1",
"number": "1",
"name": "Example Channel 1",
"mpd_url": "https://example.com/playlist/v1/live/123/1/live.mpd",
"keys": [
"11111111111111111111111111111111:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"22222222222222222222222222222222:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
]
},
{
"id": "example-channel-2",
"number": "2",
"name": "Example Channel 2",
"mpd_url": "https://example.com/playlist/v1/live/456/1/live.mpd",
"keys": [
"33333333333333333333333333333333:cccccccccccccccccccccccccccccccc",
"44444444444444444444444444444444:dddddddddddddddddddddddddddddddd"
]
}
]
}