mirror of
https://github.com/yt-dlp/yt-dlp
synced 2024-12-28 22:24:34 +01:00
[fd/dash, pp/ffmpeg] support DASH CENC decryption
This commit is contained in:
parent
a95757d3b7
commit
6b0ce31939
4 changed files with 79 additions and 4 deletions
|
@ -48,6 +48,7 @@ from .plugins import directories as plugin_directories
|
|||
from .postprocessor import _PLUGIN_CLASSES as plugin_pps
|
||||
from .postprocessor import (
|
||||
EmbedThumbnailPP,
|
||||
FFmpegCENCDecryptPP,
|
||||
FFmpegFixupDuplicateMoovPP,
|
||||
FFmpegFixupDurationPP,
|
||||
FFmpegFixupM3u8PP,
|
||||
|
@ -3384,6 +3385,8 @@ class YoutubeDL:
|
|||
self.report_error(f'{msg}. Aborting')
|
||||
return
|
||||
|
||||
decrypter = FFmpegCENCDecryptPP(self)
|
||||
info_dict.setdefault('__files_to_cenc_decrypt', [])
|
||||
if info_dict.get('requested_formats') is not None:
|
||||
old_ext = info_dict['ext']
|
||||
if self.params.get('merge_output_format') is None:
|
||||
|
@ -3464,8 +3467,12 @@ class YoutubeDL:
|
|||
downloaded.append(fname)
|
||||
partial_success, real_download = self.dl(fname, new_info)
|
||||
info_dict['__real_download'] = info_dict['__real_download'] or real_download
|
||||
if new_info.get('dash_cenc', {}).get('key'):
|
||||
info_dict['__files_to_cenc_decrypt'].append((fname, new_info['dash_cenc']['key']))
|
||||
success = success and partial_success
|
||||
|
||||
if downloaded and info_dict['__files_to_cenc_decrypt'] and decrypter.available:
|
||||
info_dict['__postprocessors'].append(decrypter)
|
||||
if downloaded and merger.available and not self.params.get('allow_unplayable_formats'):
|
||||
info_dict['__postprocessors'].append(merger)
|
||||
info_dict['__files_to_merge'] = downloaded
|
||||
|
@ -3482,6 +3489,9 @@ class YoutubeDL:
|
|||
# So we should try to resume the download
|
||||
success, real_download = self.dl(temp_filename, info_dict)
|
||||
info_dict['__real_download'] = real_download
|
||||
if info_dict.get('dash_cenc', {}).get('key') and decrypter.available:
|
||||
info_dict['__postprocessors'].append(decrypter)
|
||||
info_dict['__files_to_cenc_decrypt'] = [(temp_filename, info_dict['dash_cenc']['key'])]
|
||||
else:
|
||||
self.report_file_already_downloaded(dl_filename)
|
||||
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import base64
|
||||
import binascii
|
||||
import json
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
from . import get_suitable_downloader
|
||||
from .fragment import FragmentFD
|
||||
from ..networking import Request
|
||||
from ..networking.exceptions import RequestError
|
||||
from ..utils import update_url_query, urljoin
|
||||
|
||||
|
||||
|
@ -60,6 +65,9 @@ class DashSegmentsFD(FragmentFD):
|
|||
|
||||
args.append([ctx, fragments_to_download, fmt])
|
||||
|
||||
if 'dash_cenc' in info_dict and not info_dict['dash_cenc'].get('key'):
|
||||
self._get_clearkey_cenc(info_dict)
|
||||
|
||||
return self.download_and_append_fragments_multiple(*args, is_fatal=lambda idx: idx == 0)
|
||||
|
||||
def _resolve_fragments(self, fragments, ctx):
|
||||
|
@ -88,3 +96,41 @@ class DashSegmentsFD(FragmentFD):
|
|||
'index': i,
|
||||
'url': fragment_url,
|
||||
}
|
||||
|
||||
def _get_clearkey_cenc(self, info_dict):
|
||||
dash_cenc = info_dict.get('dash_cenc', {})
|
||||
laurl = dash_cenc.get('laurl')
|
||||
if not laurl:
|
||||
self.report_error('No Clear Key license server URL for encrypted DASH stream')
|
||||
return
|
||||
key_ids = dash_cenc.get('key_ids')
|
||||
if not key_ids:
|
||||
self.report_error('No requested CENC KIDs for encrypted DASH stream')
|
||||
return
|
||||
payload = json.dumps({
|
||||
'kids': [
|
||||
base64.urlsafe_b64encode(bytes.fromhex(k)).decode().rstrip('=')
|
||||
for k in key_ids
|
||||
],
|
||||
'type': 'temporary',
|
||||
}).encode()
|
||||
try:
|
||||
response = self.ydl.urlopen(Request(
|
||||
laurl, data=payload, headers={'Content-Type': 'application/json'}))
|
||||
data = json.loads(response.read())
|
||||
except (RequestError, json.JSONDecodeError) as err:
|
||||
self.report_error(f'Failed to retrieve key from Clear Key license server: {err}')
|
||||
return
|
||||
keys = data.get('keys', [])
|
||||
if len(keys) > 1:
|
||||
self.report_warning('Clear Key license server returned multiple keys but only single key CENC is supported')
|
||||
for key in keys:
|
||||
k = key.get('k')
|
||||
if k:
|
||||
try:
|
||||
dash_cenc['key'] = base64.urlsafe_b64decode(f'{k}==').hex()
|
||||
info_dict['dash_cenc'] = dash_cenc
|
||||
return
|
||||
except (ValueError, binascii.Error):
|
||||
pass
|
||||
self.report_error('Clear key license server did not return any valid CENC keys')
|
||||
|
|
|
@ -8,6 +8,7 @@ from .ffmpeg import (
|
|||
FFmpegCopyStreamPP,
|
||||
FFmpegEmbedSubtitlePP,
|
||||
FFmpegExtractAudioPP,
|
||||
FFmpegCENCDecryptPP,
|
||||
FFmpegFixupDuplicateMoovPP,
|
||||
FFmpegFixupDurationPP,
|
||||
FFmpegFixupM3u8PP,
|
||||
|
|
|
@ -331,7 +331,7 @@ class FFmpegPostProcessor(PostProcessor):
|
|||
[(path, []) for path in input_paths],
|
||||
[(out_path, opts)], **kwargs)
|
||||
|
||||
def real_run_ffmpeg(self, input_path_opts, output_path_opts, *, expected_retcodes=(0,)):
|
||||
def real_run_ffmpeg(self, input_path_opts, output_path_opts, *, prepend_opts=None, expected_retcodes=(0,)):
|
||||
self.check_version()
|
||||
|
||||
oldest_mtime = min(
|
||||
|
@ -342,6 +342,9 @@ class FFmpegPostProcessor(PostProcessor):
|
|||
if self.basename == 'ffmpeg':
|
||||
cmd += [encodeArgument('-loglevel'), encodeArgument('repeat+info')]
|
||||
|
||||
if prepend_opts:
|
||||
cmd += prepend_opts
|
||||
|
||||
def make_args(file, args, name, number):
|
||||
keys = [f'_{name}{number}', f'_{name}']
|
||||
if name == 'o':
|
||||
|
@ -857,12 +860,23 @@ class FFmpegMergerPP(FFmpegPostProcessor):
|
|||
return True
|
||||
|
||||
|
||||
class FFmpegCENCDecryptPP(FFmpegPostProcessor):
|
||||
@PostProcessor._restrict_to(images=False)
|
||||
def run(self, info):
|
||||
for filename, key in info.get('__files_to_cenc_decrypt', []):
|
||||
temp_filename = prepend_extension(filename, 'temp')
|
||||
self.to_screen(f'Decrypting "{filename}"')
|
||||
self.run_ffmpeg(filename, temp_filename, self.stream_copy_opts(), prepend_opts=['-decryption_key', key])
|
||||
os.replace(temp_filename, filename)
|
||||
return [], info
|
||||
|
||||
|
||||
class FFmpegFixupPostProcessor(FFmpegPostProcessor):
|
||||
def _fixup(self, msg, filename, options):
|
||||
def _fixup(self, msg, filename, options, prepend_opts=None):
|
||||
temp_filename = prepend_extension(filename, 'temp')
|
||||
|
||||
self.to_screen(f'{msg} of "{filename}"')
|
||||
self.run_ffmpeg(filename, temp_filename, options)
|
||||
self.run_ffmpeg(filename, temp_filename, options, prepend_opts=prepend_opts)
|
||||
|
||||
os.replace(temp_filename, filename)
|
||||
|
||||
|
@ -934,7 +948,11 @@ class FFmpegCopyStreamPP(FFmpegFixupPostProcessor):
|
|||
|
||||
@PostProcessor._restrict_to(images=False)
|
||||
def run(self, info):
|
||||
self._fixup(self.MESSAGE, info['filepath'], self.stream_copy_opts())
|
||||
self._fixup(
|
||||
self.MESSAGE,
|
||||
info['filepath'],
|
||||
self.stream_copy_opts(),
|
||||
)
|
||||
return [], info
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue