Repository: electron/electron
Pull request: fix: use destination context when wrapping VideoFrame in contextBridge

Enter the destination context scope before creating the VideoFrame V8 wrapper, matching the sibling Element and Blob branches. Without this, `ScriptState::ForCurrentRealm` resolved to the calling context instead of the target context, producing an incorrect wrapper.

Also switches to `ScriptState::From` with an explicit context argument to make the intent clearer.

Notes: Fixed an issue where `VideoFrame` objects returned through `contextBridge` had an incorrect prototype.

Changed files:
filename: shell/renderer/api/electron_api_context_bridge.cc
patch: @@ -423,8 +423,9 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContextInner(
     blink::VideoFrame* video_frame =
         blink::V8VideoFrame::ToWrappable(source_isolate, value);
     if (video_frame != nullptr) {
+      v8::Context::Scope destination_context_scope(destination_context);
       blink::ScriptState* script_state =
-          blink::ScriptState::ForCurrentRealm(destination_isolate);
+          blink::ScriptState::From(destination_isolate, destination_context);
       return v8::MaybeLocal<v8::Value>(
           blink::ToV8Traits<blink::VideoFrame>::ToV8(script_state,
                                                      video_frame));

filename: spec/api-context-bridge-spec.ts
patch: @@ -666,6 +666,46 @@ describe('contextBridge', () => {
         expect(result).to.deep.equal(['1245']);
       });
 
+      it('should handle VideoFrames', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            getVideoFrame: () => {
+              const canvas = new OffscreenCanvas(16, 16);
+              canvas.getContext('2d')!.fillRect(0, 0, 16, 16);
+              return new VideoFrame(canvas, { timestamp: 0 });
+            }
+          });
+        });
+        const result = await callWithBindings((root: any) => {
+          const frame = root.example.getVideoFrame();
+          const info = [frame.constructor.name, frame.codedWidth, frame.codedHeight, frame.timestamp];
+          frame.close();
+          return info;
+        });
+        expect(result).to.deep.equal(['VideoFrame', 16, 16, 0]);
+      });
+
+      it('should handle VideoFrames going backwards over the bridge', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            getVideoFrameInfo: (fn: Function) => {
+              const frame = fn();
+              const info = [frame.constructor.name, frame.codedWidth, frame.codedHeight, frame.timestamp];
+              frame.close();
+              return info;
+            }
+          });
+        });
+        const result = await callWithBindings((root: any) => {
+          return root.example.getVideoFrameInfo(() => {
+            const canvas = new OffscreenCanvas(32, 32);
+            canvas.getContext('2d')!.fillRect(0, 0, 32, 32);
+            return new VideoFrame(canvas, { timestamp: 100 });
+          });
+        });
+        expect(result).to.deep.equal(['VideoFrame', 32, 32, 100]);
+      });
+
       // Can only run tests which use the GCRunner in non-sandboxed environments
       if (!useSandbox) {
         it('should release the global hold on methods sent across contexts', async () => {
@@ -904,7 +944,12 @@ describe('contextBridge', () => {
               [Symbol('foo')]: 123
             },
             getBody: () => document.body,
-            getBlob: () => new Blob(['ab', 'cd'])
+            getBlob: () => new Blob(['ab', 'cd']),
+            getVideoFrame: () => {
+              const canvas = new OffscreenCanvas(16, 16);
+              canvas.getContext('2d')!.fillRect(0, 0, 16, 16);
+              return new VideoFrame(canvas, { timestamp: 0 });
+            }
           });
         });
         const result = await callWithBindings(async (root: any) => {
@@ -978,7 +1023,8 @@ describe('contextBridge', () => {
             [arg, Object],
             [arg.key, String],
             [example.getBody(), HTMLBodyElement],
-            [example.getBlob(), Blob]
+            [example.getBlob(), Blob],
+            [example.getVideoFrame(), VideoFrame]
           ];
           return {
             protoMatches: protoChecks.map(([a, Constructor]) => Object.getPrototypeOf(a) === Constructor.prototype)