Skip to content
Merged
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
23 changes: 20 additions & 3 deletions plugins/dispatchwrapparr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ Important notes about fragment options:

### 🧑‍💻 Using the 'clearkey' URL fragment for DRM decryption

To use a clearkey for a particular stream using a URL fragment, simply create a custom m3u8 file that places the #clearkey=<clearkey> fragment at the end of the stream URL.
Most DRM implementations apply consistent encryption across both audio and video streams by using the same key.

In this instance, you can simply just supply a clearkey. The HLSDRM and DASHDRM plugins will ignore any Key ID (KID) supplied to it.

To use a single clearkey for a particular stream using a URL fragment, simply create a custom m3u8 file that places the #clearkey=<clearkey> fragment at the end of the stream URL.

Below is an example that could be used for Channel 4 (UK):

Expand All @@ -86,12 +90,25 @@ Below is an example that could be used for Channel 4 (UK):
https://olsp.live.dash.c4assets.com/dash_iso_sp_tl/live/channel(c4)/manifest.mpd#clearkey=5ce85f1aa5771900b952f0ba58857d7a
```

You can also add the cleakey fragment to the end of a URL of a channel that you add manually into Dispatcharr.

More channels can be added to the same m3u8 file, and may also contain a mixture of DRM and non-DRM encrypted streams.

Simply upload your m3u8 file into Dispatcharr, select a Dispatchwrapparr stream profile, and it'll do the rest.

You can also add a cleakey fragment to the end of a URL of a stream being manually entered into Dispatcharr rather than administering a custom m3u8 file. This can be useful in testing.

For more complex streams that contain different clearkeys for video and audio, two keys can be provided in the order of 'video,audio'.

Streams which contain separate audio and video feeds, where only one is encrypted can specify 'none' as the key at the position.

The below table provides real world examples of how the DASHDRM and HLSDRM streamlink plugins process clearkeys.

| Example Fragment | Clearkey applied to VIDEO stream | Clearkey applied to AUDIO stream |
| :--- | :--- | :--- |
| #clearkey=a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0 | a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0 | a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0 |
| #clearkey=a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0,b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1 | a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0 | b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1 |
| #clearkey=a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0,none | a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0 | None (Unencrypted) |
| #clearkey=none,b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1 | None (Unencrypted) | b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1 |

### ▶️ Using the 'stream' URL fragment for manual stream variant/quality selection

The `#stream` fragment allows you to manually select a stream variant. Sometimes there may be occasions where you may want to manually select various stream variants depending on your preferences.
Expand Down
81 changes: 50 additions & 31 deletions plugins/dispatchwrapparr/dashdrm.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

log = logging.getLogger(__name__)

__version__ = "1.7.4"
__version__ = "1.7.5"

'''
DASHDRM plugin for Dispatchwrapparr & Streamlink
Expand Down Expand Up @@ -163,34 +163,53 @@ def _process_keys(self):
Based on work by Titus-AU: https://github.com/titus-au (Thank you!!)
'''
keys = self.get_option('decryption-key')
# if a colon separated key is given, assume its kid:key and take the
# last component after the colon
return_keys = []

for k in keys:
if k.lower() == "none":
return_keys.append(None)
continue

key = k.split(':')
key_len = len(key[-1])
log.debug("MPEGDASHDRM: Decryption Key %s has %s digits", key[-1], key_len)
if key_len in (21, 22, 23, 24):
# key len of 21-24 may mean a base64 key was provided, so we
# try and decode it
log.debug("MPEGDASHDRM: Decryption key length is too short to be hex and looks like it might be base64, so we'll try and decode it..")
b64_string = key[-1]
padding = 4 - (len(b64_string) % 4)
b64_string = b64_string + ("=" * padding)
b64_key = base64.urlsafe_b64decode(b64_string).hex()
if b64_key:
key = [b64_key]
key_len = len(b64_key)
log.debug("MPEGDASHDRM: Decryption Key (post base64 decode) is %s and has %s digits", key[-1], key_len)
key_val = key[-1]
key_len = len(key_val)
log.debug("MPEGDASHDRM: Decryption Key %s has %s digits", key_val, key_len)

is_valid_hex = False
if key_len == 32:
# sanity check that it's a valid hex string
try:
int(key[-1], 16)
except ValueError as err:
raise FatalPluginError(f"MPEGDASHDRM: Expecting 128bit key in 32 hex digits, but the key contains invalid hex.")
elif key_len != 32:
raise FatalPluginError(f"MPEGDASHDRM: Expecting 128bit key in 32 hex digits.")
return_keys.append(key[-1])
int(key_val, 16)
is_valid_hex = True
except ValueError:
pass

if not is_valid_hex:
try:
padding = 4 - (key_len % 4)
b64_string = key_val + ("=" * padding) if padding != 4 else key_val
decoded_bytes = base64.urlsafe_b64decode(b64_string)

if len(decoded_bytes) == 16:
# Handle base64 encoded raw bytes
key_val = decoded_bytes.hex()
elif len(decoded_bytes) == 32:
# Handle base64 encoded hex strings (e.g. YmQ3ZWVh...)
key_val = decoded_bytes.decode('utf-8')
int(key_val, 16) # Validate it's hex
else:
raise ValueError
except Exception:
raise FatalPluginError("MPEGDASHDRM: Expecting 128bit key in 32 hex digits, or base64 equivalent.")

if len(key_val) != 32:
raise FatalPluginError("MPEGDASHDRM: Expecting 128bit key in 32 hex digits.")

return_keys.append(key_val)

# Duplicate if only a single key is provided
if len(return_keys) == 1:
return_keys.append(return_keys[0])

return return_keys

class FFMPEGMuxerDRM(FFMPEGMuxer):
Expand All @@ -201,12 +220,8 @@ class FFMPEGMuxerDRM(FFMPEGMuxer):

@classmethod
def _get_keys(cls, session):
keys=[]
if session.options.get("decryption-key"):
keys = session.options.get("decryption-key")
# If only 1 key is given, then we use that also for all remaining streams
if len(keys) == 1:
keys.extend(keys)
keys = session.options.get("decryption-key") or []
if keys:
log.debug("FFMPEGMuxerDRM: Decryption Keys %s", keys)
return keys

Expand All @@ -220,6 +235,7 @@ def __init__(self, session, *streams, **options):
# begin building a new ffmpeg command list
old_cmd = self._cmd.copy()
self._cmd = []

while len(old_cmd) > 0:
cmd = old_cmd.pop(0)
if cmd == "-i":
Expand All @@ -228,15 +244,18 @@ def __init__(self, session, *streams, **options):
self._cmd.extend(['-thread_queue_size', '5120'])
# generate presentation timestamps from dts
self._cmd.extend(['-fflags', '+genpts'])

if keys:
self._cmd.extend(["-decryption_key", keys[key]])
if keys[key] is not None:
self._cmd.extend(["-decryption_key", keys[key]])
key += 1
# If we had more streams than keys, start with the first audio key again
if key == len(keys):
key = 1
self._cmd.extend([cmd, _])
else:
self._cmd.append(cmd)

# pop the last argument (the output pipe, e.g., "pipe:1")
output_pipe = self._cmd.pop()
# ffmpeg output options here if needed
Expand Down
Loading
Loading