Skip to content

Commit ed6edcd

Browse files
player: support media decryption keys for mpv (#1050)
1 parent 3126c20 commit ed6edcd

5 files changed

Lines changed: 62 additions & 0 deletions

File tree

feeluown/library/provider_protocol.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def song_select_media(
9898
"""Select a media by the quality sorting policy
9999
100100
If the song has some valid medias, this method can always return one of them.
101+
Providers may return a Media object with decryption_key for DRM streams.
101102
"""
102103
raise NotImplementedError
103104

@@ -108,6 +109,7 @@ def song_get_media(
108109
"""Get song's media by a specified quality
109110
110111
:return: when quality is invalid, return None
112+
Providers may return a Media object with decryption_key for DRM streams.
111113
"""
112114
raise NotImplementedError
113115

@@ -244,10 +246,18 @@ def video_list_quality(self, video: BriefVideoModel) -> List[Quality.Video]:
244246
def video_select_media(
245247
self, video: BriefVideoModel, policy=None
246248
) -> Tuple[Media, Quality.Video]:
249+
"""Select a media by quality.
250+
251+
Providers may return a Media object with decryption_key for DRM streams.
252+
"""
247253
raise NotImplementedError
248254

249255
@abstractmethod
250256
def video_get_media(self, video: BriefVideoModel, quality) -> Optional[Media]:
257+
"""Get video's media by a specified quality.
258+
259+
Providers may return a Media object with decryption_key for DRM streams.
260+
"""
251261
raise NotImplementedError
252262

253263

feeluown/media.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ class Media:
226226
def __init__(self, obj, type_=MediaType.audio,
227227
http_headers=None,
228228
http_proxy=None,
229+
decryption_key=None,
229230
**kwargs):
230231
if isinstance(obj, Media):
231232
self._copy(obj)
@@ -244,10 +245,14 @@ def __init__(self, obj, type_=MediaType.audio,
244245
self.http_headers = http_headers or {}
245246
# Example: http://127.0.0.1:7890
246247
self.http_proxy = http_proxy or ''
248+
# Optional DRM decryption key forwarded to mpv as demuxer-lavf-o.
249+
self.decryption_key = decryption_key
247250

248251
def _copy(self, media):
249252
self.url = media.url
250253
self.http_headers = media.http_headers
254+
self.http_proxy = media.http_proxy
255+
self.decryption_key = media.decryption_key
251256
self.type_ = media.type_
252257
self._props = media._props
253258
self._manifest = media.manifest

feeluown/player/mpvplayer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def play(self, media, video=True, metadata=None):
129129
media = Media(media)
130130
self._set_http_headers(media.http_headers)
131131
self._set_http_proxy(media.http_proxy)
132+
self._set_decryption_key(media.decryption_key)
132133
self._stop_mpv()
133134
if media.manifest is None:
134135
url = media.url
@@ -365,6 +366,13 @@ def _set_http_proxy(self, http_proxy):
365366
self._mpv.handle, b'http-proxy', bytes(http_proxy, 'utf-8')
366367
)
367368

369+
def _set_decryption_key(self, decryption_key):
370+
if decryption_key is not None:
371+
value = bytes(f'decryption_key={decryption_key}', 'utf-8')
372+
else:
373+
value = b''
374+
_mpv_set_option_string(self._mpv.handle, b'demuxer-lavf-o', value)
375+
368376
def __log_handler(self, loglevel, component, message):
369377
print('[{}] {}: {}'.format(loglevel, component, message))
370378

tests/player/test_player.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,30 @@ def test_play_media_with_http_headers(self, mock_set_option_string):
110110
assert mock_set_option_string.called
111111
self.player.stop()
112112

113+
@mock.patch('feeluown.player.mpvplayer._mpv_set_option_string')
114+
def test_play_media_with_decryption_key(self, mock_set_option_string):
115+
media = Media('http://xxx', decryption_key='0123456789abcdef')
116+
self.player.play(media)
117+
118+
mock_set_option_string.assert_any_call(
119+
self.player._mpv.handle,
120+
b'demuxer-lavf-o',
121+
b'decryption_key=0123456789abcdef',
122+
)
123+
self.player.stop()
124+
125+
@mock.patch('feeluown.player.mpvplayer._mpv_set_option_string')
126+
def test_play_media_without_decryption_key_clears_mpv_option(
127+
self, mock_set_option_string
128+
):
129+
media = Media('http://xxx')
130+
self.player.play(media)
131+
132+
mock_set_option_string.assert_any_call(
133+
self.player._mpv.handle, b'demuxer-lavf-o', b''
134+
)
135+
self.player.stop()
136+
113137

114138
class TestPlaylist(TestCase):
115139
def setUp(self):

tests/test_media.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,22 @@ def media():
1212
def test_media_copy(media):
1313
media2 = Media(media)
1414
assert media2.http_headers == media.http_headers
15+
assert media2.http_proxy == media.http_proxy
16+
assert media2.decryption_key == media.decryption_key
1517

1618

1719
def test_media_http_headers(media):
1820
assert 'referer' in media.http_headers
21+
22+
23+
def test_media_decryption_key_default_is_none():
24+
media = Media('zzz://xxx.yyy')
25+
26+
assert media.decryption_key is None
27+
28+
29+
def test_media_decryption_key(media):
30+
media.decryption_key = '0123456789abcdef'
31+
media2 = Media(media)
32+
33+
assert media2.decryption_key == '0123456789abcdef'

0 commit comments

Comments
 (0)