Skip to content

[GST] Dynamic insertion of decryptor element #1490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: wpe-2.46
Choose a base branch
from

Conversation

asurdej-comcast
Copy link

@asurdej-comcast asurdej-comcast commented Apr 11, 2025

We have a use-case in one of our web apps where MSE encrypted playback is mixed with a clear one in single SourceBuffer. This is because of add-insertion where clear add is appended to the same SourceBuffer during encrypted video playback. As a result there is CAPS change in GST pipeline between encrypted and clear ones.

There are two main cases:

  1. Main content (encrypted) starts first and add starts later in time. In such case initial encrypted CAPS make parsebin to set up a pipeline with a decryptor element (thunder decrypt in our case). Switching to clear CAPS is not a big deal as the decryptor will enter passthrough mode and the playback will continue. Going back to encrypted makes CAPS switch again and the decryptor operates in regular mode.
  2. Add playback (clear) starts first and the pipeline is prepared for clear playback only. Switching to encrypted in such case causes buffer push failure from source (webkit media src) with "not-negotiated" error. CAPS doesn't match. No decrypter is in the pipeline and parsebin/decodebin/playbin don't auto adjust to new CAPS.

We have a change to handle that, created when playback pipeline was using appsource element instead of webkit media src (2.22 or earlier) where the decrypter is inserted dynamically into the playback pipeline based on caps from source element.
It's not the perfect solution but good enough to start discussion on how we can handle that, and if that's the valid use-case at all.
There are two parts there:

  1. Insert a decryptor when encrypted caps are detected
  2. Always insert svppay payloader element to force secure video path on Amlogic device. Without that the pipeline will be set in regular memory so on encrypted caps we will have to replace more elements, for sure a parser element

Please let me know what you think on that?
202bd4e

Build-Tests Layout-Tests
✅ 🛠 wpe-246-amd64-build ❌ 🧪 wpe-246-amd64-layout
✅ 🛠 wpe-246-arm32-build ❌ 🧪 wpe-246-arm32-layout

Create and insert a GST decryptor element on the fly
once encrypted CAPS arrives
@asurdej-comcast asurdej-comcast requested a review from philn as a code owner April 11, 2025 12:07
@asurdej-comcast
Copy link
Author

@philn
Copy link

philn commented Apr 11, 2025

I had the same use-case in another project, I solved it by adding a new decryptor, a bin that would contain the real decryptor and a decodebin... That bin had a higher rank, so was auto-plugged. In there I had a pad probe, a bit similar to what you did, to know if I should plug decodebin or not (iirc).

I don't think modifying the MSE code is a good solution.

@asurdej-comcast
Copy link
Author

Could you please share more details on that? Considering playbin3, I think the only element that is autoplugging a pipeline is a parsebin and it should be possible to provide a high ranked bin element that will accept all caps - we need to always have it, even for clear caps. And do the dynamic decryptor insertion there. I'm not entirely sure if parsebin will autoplug such element really, will need to double check

Whatever the final solution will be, the other question is, if this is something to be included in downstream repo or we should go with custom impl?

@philn
Copy link

philn commented May 19, 2025

Well with the current architecture something is already auto-plugging decryptors, either decodebin3 or parsebin, based on their rank.

So if you have a bin acting as decryptor but also doing parsing, with a higher rank than the actual real decryptor, then it will be autoplugged.

I can try to salvage that previous work if you want. And yes, it could be maintained in WebKit upstream.

@asurdej-comcast
Copy link
Author

asurdej-comcast commented May 22, 2025

@philn Thanks,

I can try to salvage that previous work if you want

That would be great if you still have it somewhere.

Additionally, some media formats, create an extra parser inside AppendPipeline (createOptionalParserForFormat()) (eg h264). This prevents switching switching to encrypted caps as those are not supported by the parser. Currently we have that parser disabled but it would be better to try recreating it on caps change. however, that can probably be handled separately

@philn
Copy link

philn commented May 24, 2025

Additionally, some media formats, create an extra parser inside AppendPipeline (createOptionalParserForFormat()) (eg h264). This prevents switching switching to encrypted caps as those are not supported by the parser. Currently we have that parser disabled but it would be better to try recreating it on caps change. however, that can probably be handled separately

Yes, that can be handled separately in the AppendPipeline, probably by reacting on caps events from the appsrc, not sure...

@philn
Copy link

philn commented May 28, 2025

I think this patch might fix some of the issues reported here, can you check WebKit/WebKit#45912?

@asurdej-comcast
Copy link
Author

Thank you @philn
I brought the changes from WebKit/WebKit#45912 to 2.46 in https://github.com/asurdej-comcast/WPEWebKit/commits/wpe-2.46/thunderparser/ adding our modifications.
Smoke testing looks good, no problems spot so far. Will run more testing this week

The AppendPipeline change to remove parser element when stream becomes encrypted only works for streams with DRM init data present in the content file. We have some tests with pssh/init data injected from JavaScript and not part of the init segment that doesn't trigger protection/encrypted events.

@philn
Copy link

philn commented Jun 16, 2025

If you can make a standalone test and have it fail, I can check it and it might be worth upstreaming as well.

Also noteworthy for backporting, WebKit/WebKit@990c523 would be needed downstream too.

@asurdej-comcast
Copy link
Author

Here is a test page:
https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?drm=widevine&start=clear&codec=h264
It contains sample video in both clear and encrypted versions. The video will start clear and will switch encrypted/clear every 8 sec, ie. at 8, 16 and 24sec. It should reach 32sec of playback in total.

With current AppendPipeline version it reports an error and playback stops at 8sec so the moment of clear -> encrypted transition. Parser element is still present, not accepting encrypted caps

0:12:44.156476126    20 0xa2a66b28 INFO                 qtdemux qtdemux.c:4004:qtdemux_parse_sgpd:<qtdemux5> properties for group 'seig' at index 0: application/x-cenc, iv_size=(uint)8, encrypted=(boolean)true, kid=(buffer)9eb4050de44b4802932e27d75083e266, cipher-mode=(string)cenc;
0:12:44.157350860    20 0xa2a66b28 INFO                 qtdemux qtdemux.c:3714:qtdemux_parse_tfdt:<qtdemux5> Track fragment decode time: 192
0:12:44.157427942    20 0xa2a66b28 INFO                 qtdemux qtdemux.c:3457:qtdemux_parse_trun:<qtdemux5:video_0> first sample ts 0:00:08.000000000
0:12:44.157649688    20 0xa2a66b28 INFO               GST_EVENT gstevent.c:847:gst_event_new_caps: creating caps event application/x-cenc, stream-format=(string)avc, alignment=(string)au, level=(string)5, profile=(string)high, codec_data=(buffer)01640032ffe1001b67640032ac7204405005bb011000000300100000030308f183184601000668e843b2c8b0fdf8f800, width=(int)1280, height=(int)720, pixel-aspect-ratio=(fraction)1/1, colorimetry=(string)2:0:0:0, original-media-type=(string)video/x-h264, cipher-mode=(string)cenc, framerate=(fraction)24/1
0:12:44.157801018    20 0xa2a66b28 WARN                GST_CAPS gstpad.c:3235:gst_pad_query_accept_caps_default:<100_parser:sink> caps: application/x-cenc, stream-format=(string)avc, alignment=(string)au, level=(string)5, profile=(string)high, codec_data=(buffer)01640032ffe1001b67640032ac7204405005bb011000000300100000030308f183184601000668e843b2c8b0fdf8f800, width=(int)1280, height=(int)720, pixel-aspect-ratio=(fraction)1/1, colorimetry=(string)2:0:0:0, original-media-type=(string)video/x-h264, cipher-mode=(string)cenc, framerate=(fraction)24/1 were not compatible with: video/x-h264
0:12:44.157958182    20 0xa2a66b28 WARN                GST_CAPS gstpad.c:5714:pre_eventfunc_check:<100_parser:sink> caps application/x-cenc, stream-format=(string)avc, alignment=(string)au, level=(string)5, profile=(string)high, codec_data=(buffer)01640032ffe1001b67640032ac7204405005bb011000000300100000030308f183184601000668e843b2c8b0fdf8f800, width=(int)1280, height=(int)720, pixel-aspect-ratio=(fraction)1/1, colorimetry=(string)2:0:0:0, original-media-type=(string)video/x-h264, cipher-mode=(string)cenc, framerate=(fraction)24/1 not accepted
0:12:44.158226010    20 0xa2a66b28 WARN                GST_CAPS gstpad.c:3235:gst_pad_query_accept_caps_default:<100_parser:sink> caps: application/x-cenc, stream-format=(string)avc, alignment=(string)au, level=(string)5, profile=(string)high, codec_data=(buffer)01640032ffe1001b67640032ac7204405005bb011000000300100000030308f183184601000668e843b2c8b0fdf8f800, width=(int)1280, height=(int)720, pixel-aspect-ratio=(fraction)1/1, colorimetry=(string)2:0:0:0, original-media-type=(string)video/x-h264, cipher-mode=(string)cenc, framerate=(fraction)24/1 were not compatible with: video/x-h264
0:12:44.158367508    20 0xa2a66b28 WARN                GST_CAPS gstpad.c:5714:pre_eventfunc_check:<100_parser:sink> caps application/x-cenc, stream-format=(string)avc, alignment=(string)au, level=(string)5, profile=(string)high, codec_data=(buffer)01640032ffe1001b67640032ac7204405005bb011000000300100000030308f183184601000668e843b2c8b0fdf8f800, width=(int)1280, height=(int)720, pixel-aspect-ratio=(fraction)1/1, colorimetry=(string)2:0:0:0, original-media-type=(string)video/x-h264, cipher-mode=(string)cenc, framerate=(fraction)24/1 not accepted
0:12:44.158489297    20 0xa2a66b28 WARN                 basesrc gstbasesrc.c:3127:gst_base_src_loop:<appsrc5> error: Internal data stream error.
0:12:44.158519255    20 0xa2a66b28 WARN                 basesrc gstbasesrc.c:3127:gst_base_src_loop:<appsrc5> error: streaming stopped, reason not-negotiated (-4)

@philn
Copy link

philn commented Jun 17, 2025

Is it supposed to work in Chrome? Here the test fails after ~7 seconds, but I'm not sure if I have widevine...

Would it be possible to adapt the test to use ClearKey instead of Widevine?

@asurdej-comcast
Copy link
Author

asurdej-comcast commented Jun 17, 2025

It works for me with Chromium browser but fails at the same place.
Starting with encrypted works well so that is not a problem with Widevine, ie: https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?drm=widevine&start=encrypted&codec=h264

Both cases work well with Firefox. Looks like chrome has clear to encrypted transition not working/supported

Automatic video playback is blocked so need to call video.play() from web inspector or click the "Play" button I added.

@philn
Copy link

philn commented Jun 17, 2025

It would be easier for me if the test relies on ClearKey and we could upstream that as well 🙏🏻

@asurdej-comcast
Copy link
Author

Ok then, I will make tc with clear key

@asurdej-comcast
Copy link
Author

Here is an example with clearkey: https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?start=clear&codec=h264&drm=clearkey
It works fine for me with Firefox.
Fails with chrome at ~8sec with following error message:

Got an error: 3, PIPELINE_ERROR_DECODE: video decoder reinitialization failed

EME setup looks good, no DRM related errors. Starting with encrypted also plays fine so there is a problem with initial clear->encrypted transition

@philn
Copy link

philn commented Jun 18, 2025

Playback doesn't even start here

https://asurdej-comcast.github.io/tr/lib/utils.js:9:16: CONSOLE LOG _params[start] = clear
https://asurdej-comcast.github.io/tr/lib/utils.js:9:16: CONSOLE LOG _params[codec] = h264
https://asurdej-comcast.github.io/tr/lib/utils.js:9:16: CONSOLE LOG _params[drm] = clearkey
https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?start=clear&codec=h264&drm=clearkey:112:20: CONSOLE LOG DRM:  [object Object]
https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?start=clear&codec=h264&drm=clearkey:136:20: CONSOLE LOG switch: 2
https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?start=clear&codec=h264&drm=clearkey:137:20: CONSOLE LOG stop: 8
https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?start=clear&codec=h264&drm=clearkey:138:20: CONSOLE LOG buffer: 2
https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?start=clear&codec=h264&drm=clearkey:139:20: CONSOLE LOG loop: 0
https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?start=clear&codec=h264&drm=clearkey:163:20: CONSOLE LOG Audio type: audio/mp4; codecs="mp4a.40.29", video type: video/mp4; codecs="avc1.640032"
https://asurdej-comcast.github.io/tr/lib/mse.js:26:12: CONSOLE LOG MediaSource created, readyState: closed
https://asurdej-comcast.github.io/tr/lib/mse.js:28:12: CONSOLE LOG MediaSource attached, readyState: closed
https://asurdej-comcast.github.io/tr/lib/mse.js:29:12: CONSOLE LOG waiting for MediaSource 'sourceopen' event...
https://asurdej-comcast.github.io/tr/lib/mse.js:31:12: CONSOLE LOG MediaSource readyState: open
https://asurdej-comcast.github.io/tr/lib/mse.js:42:12: CONSOLE LOG MediaSource creating SourceBuffer for 'audio/mp4; codecs="mp4a.40.29"' (supported: true)
https://asurdej-comcast.github.io/tr/lib/mse.js:44:12: CONSOLE LOG MediaSource added SourceBuffer, readyState: open
https://asurdej-comcast.github.io/tr/lib/mse.js:42:12: CONSOLE LOG MediaSource creating SourceBuffer for 'video/mp4; codecs="avc1.640032"' (supported: true)
https://asurdej-comcast.github.io/tr/lib/mse.js:44:12: CONSOLE LOG MediaSource added SourceBuffer, readyState: open
https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?start=clear&codec=h264&drm=clearkey:224:32: CONSOLE LOG appendWindowEnd set to : 32
https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?start=clear&codec=h264&drm=clearkey:224:32: CONSOLE LOG appendWindowEnd set to : 32
https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?start=clear&codec=h264&drm=clearkey:304:24: CONSOLE LOG Video time: 0Need next audio segment: 1
https://asurdej-comcast.github.io/tr/lib/mse.js:119:12: CONSOLE LOG fetching 'https://media.axprod.net/TestVectors/v7-Clear/15/init.mp4' using XHR...
https://asurdej-comcast.github.io/tr/lib/mse.js:124:16: CONSOLE LOG fetching 'https://media.axprod.net/TestVectors/v7-Clear/15/init.mp4' done - received buffer of size: 1670 bytes
https://asurdej-comcast.github.io/tr/lib/mse.js:63:12: CONSOLE LOG Appending 1670 bytes of data to the SourceBuffer
https://asurdej-comcast.github.io/tr/lib/mse.js:69:12: CONSOLE LOG waiting for SourceBuffer 'updateend' event...
https://asurdej-comcast.github.io/tr/lib/mse.js:66:16: CONSOLE LOG received SourceBuffer 'updatestart' event
https://asurdej-comcast.github.io/tr/lib/mse.js:94:8: CONSOLE LOG MediaSource duration: 734
https://asurdej-comcast.github.io/tr/lib/mse.js:103:8: CONSOLE LOG SourceBuffer ranges: 0 ()
https://asurdej-comcast.github.io/tr/lib/mse.js:119:12: CONSOLE LOG fetching 'https://media.axprod.net/TestVectors/v7-Clear/15/0001.m4s' using XHR...
https://asurdej-comcast.github.io/tr/lib/mse.js:124:16: CONSOLE LOG fetching 'https://media.axprod.net/TestVectors/v7-Clear/15/0001.m4s' done - received buffer of size: 32284 bytes
https://asurdej-comcast.github.io/tr/lib/mse.js:63:12: CONSOLE LOG Appending 32284 bytes of data to the SourceBuffer
https://asurdej-comcast.github.io/tr/lib/mse.js:69:12: CONSOLE LOG waiting for SourceBuffer 'updateend' event...
https://asurdej-comcast.github.io/tr/lib/mse.js:66:16: CONSOLE LOG received SourceBuffer 'updatestart' event
https://asurdej-comcast.github.io/tr/lib/mse.js:94:8: CONSOLE LOG MediaSource duration: 734
https://asurdej-comcast.github.io/tr/lib/mse.js:103:8: CONSOLE LOG SourceBuffer ranges: 1 (#0: [0, 3.850999], )
https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?start=clear&codec=h264&drm=clearkey:322:24: CONSOLE LOG Video time: 0Need next video segment: 1, total: 1
https://asurdej-comcast.github.io/tr/lib/mse.js:119:12: CONSOLE LOG fetching 'https://media.axprod.net/TestVectors/v7-Clear/4/init.mp4' using XHR...
https://asurdej-comcast.github.io/tr/lib/mse.js:124:16: CONSOLE LOG fetching 'https://media.axprod.net/TestVectors/v7-Clear/4/init.mp4' done - received buffer of size: 910 bytes
https://asurdej-comcast.github.io/tr/lib/mse.js:63:12: CONSOLE LOG Appending 910 bytes of data to the SourceBuffer
https://asurdej-comcast.github.io/tr/lib/mse.js:69:12: CONSOLE LOG waiting for SourceBuffer 'updateend' event...
https://asurdej-comcast.github.io/tr/lib/mse.js:66:16: CONSOLE LOG received SourceBuffer 'updatestart' event
https://asurdej-comcast.github.io/tr/lib/mse.js:94:8: CONSOLE LOG MediaSource duration: 734
https://asurdej-comcast.github.io/tr/lib/mse.js:103:8: CONSOLE LOG SourceBuffer ranges: 0 ()
https://asurdej-comcast.github.io/tr/lib/mse.js:119:12: CONSOLE LOG fetching 'https://media.axprod.net/TestVectors/v7-Clear/4/0001.m4s' using XHR...
https://asurdej-comcast.github.io/tr/lib/mse.js:124:16: CONSOLE LOG fetching 'https://media.axprod.net/TestVectors/v7-Clear/4/0001.m4s' done - received buffer of size: 92133 bytes
https://asurdej-comcast.github.io/tr/lib/mse.js:63:12: CONSOLE LOG Appending 92133 bytes of data to the SourceBuffer
https://asurdej-comcast.github.io/tr/lib/mse.js:69:12: CONSOLE LOG waiting for SourceBuffer 'updateend' event...
https://asurdej-comcast.github.io/tr/lib/mse.js:66:16: CONSOLE LOG received SourceBuffer 'updatestart' event

(WebKitWebProcess:322261): GStreamer-WARNING **: 11:42:53.620: ../../Tools/flatpak/local-projects/subprojects/gstreamer-full/subprojects/gstreamer/gst/gstpad.c:4773:gst_pad_push_data:<sink_0:proxypad20> Got dataflow before stream-start event

(WebKitWebProcess:322261): GStreamer-WARNING **: 11:42:53.620: ../../Tools/flatpak/local-projects/subprojects/gstreamer-full/subprojects/gstreamer/gst/gstpad.c:4778:gst_pad_push_data:<sink_0:proxypad20> Got dataflow before segment event
https://asurdej-comcast.github.io/tr/lib/mse.js:94:8: CONSOLE LOG MediaSource duration: 734
https://asurdej-comcast.github.io/tr/lib/mse.js:103:8: CONSOLE LOG SourceBuffer ranges: 1 (#0: [0, 4.083332], )

(WebKitWebProcess:322261): GStreamer-WARNING **: 11:42:53.630: ../../Tools/flatpak/local-projects/subprojects/gstreamer-full/subprojects/gstreamer/gst/gstpad.c:4773:gst_pad_push_data:<sink_1:proxypad23> Got dataflow before stream-start event

(WebKitWebProcess:322261): GStreamer-WARNING **: 11:42:53.631: ../../Tools/flatpak/local-projects/subprojects/gstreamer-full/subprojects/gstreamer/gst/gstpad.c:4778:gst_pad_push_data:<sink_1:proxypad23> Got dataflow before segment event
https://asurdej-comcast.github.io/tr/lib/utils.js:35:14: CONSOLE LOG TEST FAILED : Test failed. Got an error: 4, Failed to push buffer
CONSOLE JS ERROR Unhandled Promise Rejection: NotSupportedError: The operation is not supported.

@philn
Copy link

philn commented Jun 22, 2025

I had to revert 295500@main because it broke auto-plugging of 3rd-party decryptors, I'll work on a new version.

@asurdej-comcast
Copy link
Author

Regarding playback failure with x86, try disabling audio by adding "audio=0" url param:
https://asurdej-comcast.github.io/tr/tests/DELIA-65629_mse_eme_clear_encrypted_transition.html?start=clear&codec=h264&drm=clearkey&audio=0

That way I'm able to start clear video at least with x86 PC build

@philn
Copy link

philn commented Jun 26, 2025

Your initial idea is actually better than mine I think... Wrapping the decryptor in a new bin with parsebin seemed cool but it's also quite risky :(

Draft PR opened, WebKit/WebKit#47230 (without svppay handling)

@asurdej-comcast
Copy link
Author

Wrapping the decryptor in a new bin with parsebin seemed cool but it's also quite risky :(

Could you please provide more details? What kind of issues are you encountering?
We did run into one issue with the AAMP player unnecessarily connecting the thunderparser element, even though it has its own decryptors. However, I believe we should be able to work around that by increasing the rank of thunderparser in the parsebin autoplug signals, rather than setting it globally

Otherwise, it seemed like a much cleaner solution to me

@philn
Copy link

philn commented Jun 30, 2025

I'm giving the magic parser another chance...

@philn
Copy link

philn commented Jul 4, 2025

The PR landed, hopefully all cases are covered now, can you give it a try?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

Successfully merging this pull request may close these issues.

3 participants