Compare commits

...

7 Commits

Author SHA1 Message Date
chen jing
d06db5f30c test: skip webFrameMain.copyVideoFrameAt on win32 CI due Chromium DCHECK 2026-03-19 14:02:07 +08:00
dodola
67eec6ceb3 Merge branch 'electron:main' into videoframe 2026-03-17 19:27:30 +08:00
David Sanders
36350d78d0 chore: add missing timers-shim.ts to filenames.auto.gni (#50311) 2026-03-17 09:46:15 +01:00
dependabot[bot]
9b80324d7f build(deps): bump github/codeql-action from 4.32.6 to 4.33.0 (#50308)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.6 to 4.33.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](0d579ffd05...b1bff81932)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 22:15:03 -07:00
dependabot[bot]
a549c56faa build(deps): bump slackapi/slack-github-action from 2.1.1 to 3.0.1 (#50307)
Bumps [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action) from 2.1.1 to 3.0.1.
- [Release notes](https://github.com/slackapi/slack-github-action/releases)
- [Commits](91efab103c...af78098f53)

---
updated-dependencies:
- dependency-name: slackapi/slack-github-action
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 22:14:43 -07:00
dodola
ea6da976eb Merge branch 'electron:main' into videoframe 2026-02-24 10:57:39 +08:00
chenjing
b568504d8d feat: add copyVideoFrameAt and saveVideoFrameAs Method on Webcontent
chore: change the description of savevideoframe api

chore: add the description of the restrictive elements for using the APIs.

move to webframemain

fixed mediaPlayerAction to kSaveVideoFrameAs

Update spec/api-web-frame-main-spec.ts

Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>

Update spec/api-web-frame-main-spec.ts

Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>

fixed clipboard tests for video frame copying

fixed test for copying video frame to clipboard. check video loaded before copy video frame in test.

chore: try non-proprietary video format

Revert "chore: try non-proprietary video format"

This reverts commit ef085f88a1af53b6408a7af695cc60b8681398cf.

fix: format video as file url
2025-12-10 18:25:52 +08:00
10 changed files with 154 additions and 3 deletions

View File

@@ -155,7 +155,7 @@ jobs:
await core.summary.write();
- name: Send Slack message if errors
if: ${{ always() && steps.audit-errors.outputs.errorsFound && github.ref == 'refs/heads/main' }}
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
with:
payload: |
link: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"

View File

@@ -14,7 +14,7 @@ jobs:
permissions: {}
steps:
- name: Trigger Slack workflow
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
with:
webhook: ${{ secrets.BACKPORT_REQUESTED_SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger

View File

@@ -51,6 +51,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v3.29.5
uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v3.29.5
with:
sarif_file: results.sarif

View File

@@ -1585,6 +1585,20 @@ Centers the current text selection in web page.
Copy the image at the given position to the clipboard.
#### `contents.copyVideoFrameAt(x, y)`
* `x` Integer
* `y` Integer
When executed on a video media element, copies the frame at (x, y) to the clipboard.
#### `contents.saveVideoFrameAs(x, y)`
* `x` Integer
* `y` Integer
When executed on a video media element, shows a save dialog and saves the frame at (x, y) to disk.
#### `contents.paste()`
Executes the editing command `paste` in web page.

View File

@@ -175,6 +175,20 @@ app.on('web-contents-created', (_, webContents) => {
})
```
#### `frame.copyVideoFrameAt(x, y)`
* `x` Integer
* `y` Integer
When executed on a video media element, copies the frame at (x, y) to the clipboard.
#### `frame.saveVideoFrameAs(x, y)`
* `x` Integer
* `y` Integer
When executed on a video media element, shows a save dialog and saves the frame at (x, y) to disk.
### Instance Properties
#### `frame.ipc` _Readonly_

View File

@@ -179,6 +179,7 @@ auto_filenames = {
"lib/common/define-properties.ts",
"lib/common/deprecate.ts",
"lib/common/ipc-messages.ts",
"lib/common/timers-shim.ts",
"lib/common/web-view-methods.ts",
"lib/common/webpack-globals-provider.ts",
"lib/renderer/api/context-bridge.ts",

View File

@@ -437,6 +437,14 @@ WebContents.prototype.loadURL = function (url, options) {
return p;
};
WebContents.prototype.copyVideoFrameAt = function (x: number, y: number) {
this.mainFrame.copyVideoFrameAt(x, y);
};
WebContents.prototype.saveVideoFrameAs = function (x: number, y: number) {
this.mainFrame.saveVideoFrameAs(x, y);
};
WebContents.prototype.setWindowOpenHandler = function (handler: (details: Electron.HandlerDetails) => Electron.WindowOpenHandlerResponse) {
this._windowOpenHandler = handler;
};

View File

@@ -36,6 +36,8 @@
#include "shell/common/node_includes.h"
#include "shell/common/v8_util.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
#include "third_party/blink/public/mojom/frame/media_player_action.mojom.h"
#include "ui/gfx/geometry/point.h"
namespace {
@@ -260,6 +262,28 @@ v8::Local<v8::Promise> WebFrameMain::ExecuteJavaScript(
return handle;
}
void WebFrameMain::CopyVideoFrameAt(int x, int y) {
if (!CheckRenderFrame())
return;
auto location = gfx::Point(x, y);
auto action = blink::mojom::MediaPlayerAction(
blink::mojom::MediaPlayerActionType::kCopyVideoFrame,
/*enable=*/true);
return render_frame_host()->ExecuteMediaPlayerActionAtLocation(location,
action);
}
void WebFrameMain::SaveVideoFrameAs(int x, int y) {
if (!CheckRenderFrame())
return;
auto location = gfx::Point(x, y);
auto action = blink::mojom::MediaPlayerAction(
blink::mojom::MediaPlayerActionType::kSaveVideoFrameAs,
/*enable=*/true);
return render_frame_host()->ExecuteMediaPlayerActionAtLocation(location,
action);
}
bool WebFrameMain::Reload() {
if (!CheckRenderFrame())
return false;
@@ -593,6 +617,8 @@ void WebFrameMain::FillObjectTemplate(v8::Isolate* isolate,
.SetMethod("executeJavaScript", &WebFrameMain::ExecuteJavaScript)
.SetMethod("collectJavaScriptCallStack",
&WebFrameMain::CollectDocumentJSCallStack)
.SetMethod("copyVideoFrameAt", &WebFrameMain::CopyVideoFrameAt)
.SetMethod("saveVideoFrameAs", &WebFrameMain::SaveVideoFrameAs)
.SetMethod("reload", &WebFrameMain::Reload)
.SetMethod("isDestroyed", &WebFrameMain::IsDestroyed)
.SetMethod("_send", &WebFrameMain::Send)

View File

@@ -118,6 +118,8 @@ class WebFrameMain final : public gin_helper::DeprecatedWrappable<WebFrameMain>,
v8::Local<v8::Promise> ExecuteJavaScript(gin::Arguments* args,
const std::u16string& code);
void CopyVideoFrameAt(int x, int y);
void SaveVideoFrameAs(int x, int y);
bool Reload();
bool IsDestroyed() const;
void Send(v8::Isolate* isolate,

View File

@@ -1,3 +1,4 @@
import { clipboard } from 'electron/common';
import { BrowserWindow, WebFrameMain, webFrameMain, ipcMain, app, WebContents } from 'electron/main';
import { expect } from 'chai';
@@ -534,6 +535,91 @@ describe('webFrameMain module', () => {
});
});
describe('webFrameMain.copyVideoFrameAt', () => {
const insertVideoInFrame = async (frame: WebFrameMain) => {
const videoFilePath = url.pathToFileURL(path.join(fixtures, 'cat-spin.mp4')).href;
await frame.executeJavaScript(`
const video = document.createElement('video');
video.src = '${videoFilePath}';
video.muted = true;
video.loop = true;
video.play();
document.body.appendChild(video);
`);
};
const getFramePosition = async (frame: WebFrameMain) => {
const point = await frame.executeJavaScript(`(${() => {
const iframe = document.querySelector('iframe');
if (!iframe) return;
const rect = iframe.getBoundingClientRect();
return { x: Math.floor(rect.x), y: Math.floor(rect.y) };
}})()`) as Electron.Point;
expect(point).to.be.an('object');
return point;
};
const copyVideoFrameInFrame = async (frame: WebFrameMain) => {
const point = await frame.executeJavaScript(`(${() => {
const video = document.querySelector('video');
if (!video) return;
const rect = video.getBoundingClientRect();
return {
x: Math.floor(rect.x + rect.width / 2),
y: Math.floor(rect.y + rect.height / 2)
};
}})()`) as Electron.Point;
expect(point).to.be.an('object');
// Translate coordinate to be relative of main frame
if (frame.parent) {
const framePosition = await getFramePosition(frame.parent);
point.x += framePosition.x;
point.y += framePosition.y;
}
expect(clipboard.readImage().isEmpty()).to.be.true();
// wait for video to load
await frame.executeJavaScript(`(${() => {
const video = document.querySelector('video');
if (!video) return;
return new Promise(resolve => {
if (video.readyState >= 4) resolve(null);
else video.addEventListener('canplaythrough', resolve, { once: true });
});
}})()`);
frame.copyVideoFrameAt(point.x, point.y);
await waitUntil(() => clipboard.availableFormats().includes('image/png'));
expect(clipboard.readImage().isEmpty()).to.be.false();
};
beforeEach(() => {
clipboard.clear();
});
// TODO: Re-enable on Windows CI once Chromium fixes the intermittent
// backwards-time DCHECK hit while copying video frames:
// DCHECK failed: !delta.is_negative().
ifit(!(process.platform === 'win32' && process.env.CI))('copies video frame in main frame', async () => {
const w = new BrowserWindow({ show: false });
await w.webContents.loadFile(path.join(fixtures, 'blank.html'));
await insertVideoInFrame(w.webContents.mainFrame);
await copyVideoFrameInFrame(w.webContents.mainFrame);
await waitUntil(() => clipboard.availableFormats().includes('image/png'));
});
ifit(!(process.platform === 'win32' && process.env.CI))('copies video frame in subframe', async () => {
const w = new BrowserWindow({ show: false });
await w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html'));
const subframe = w.webContents.mainFrame.frames[0];
expect(subframe).to.exist();
await insertVideoInFrame(subframe);
await copyVideoFrameInFrame(subframe);
await waitUntil(() => clipboard.availableFormats().includes('image/png'));
});
});
describe('"frame-created" event', () => {
it('emits when the main frame is created', async () => {
const w = new BrowserWindow({ show: false });