fix: use destination context when wrapping VideoFrame in contextBridge (#50021)

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 switch to ScriptState::From with an explicit context argument to
make the intent clearer.

Adds spec coverage for VideoFrame crossing the bridge in both
directions and adds VideoFrame to the existing prototype checks.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Samuel Attard <sattard@anthropic.com>
This commit is contained in:
trop[bot]
2026-03-02 23:30:46 -08:00
committed by GitHub
parent 277164fc20
commit 4544b97e28
2 changed files with 50 additions and 3 deletions

View File

@@ -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));

View File

@@ -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)