Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/flutter_cef.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export 'package:flutter_cef_platform_interface/flutter_cef_platform_interface.da
CefConsoleMessage,
CefFindResult,
CefJsDialogRequest,
CefLoadError;
CefLoadError,
CefSurfaceInfo;
export 'src/cef_web_controller.dart' show CefWebController;
export 'src/cef_web_view.dart' show CefWebView;
34 changes: 34 additions & 0 deletions lib/src/cef_web_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ class CefWebController {
/// you generally don't set this yourself.
void Function(Rect caretRect)? onImeCompositionBounds;

/// Called whenever the off-screen frame surface is (re)allocated — once at
/// session create and again on every resize (which frees the old IOSurface
/// and allocs a new one). Carries the new global IOSurface id and its
/// PHYSICAL (Retina) pixel dims. A consumer that mirrors the live page pixels
/// off-Flutter (e.g. a WebRTC capturer) resolves the surface by id and must
/// re-read on every fire — never cache the surface across frames. Pull the
/// current surface on demand with [getFrameSurface].
void Function(CefSurfaceInfo info)? onSurface;

/// Handle a page `alert(...)`. Show your UI, then return to dismiss it. If
/// unset, alerts are auto-dismissed.
Future<void> Function(CefJsDialogRequest request)? onJavaScriptAlertDialog;
Expand Down Expand Up @@ -240,6 +249,13 @@ class CefWebController {
(a['h'] as num? ?? 0).toDouble(),
));
break;
case 'onSurface':
onSurface?.call(CefSurfaceInfo(
surfaceId: a['surfaceId'] as int? ?? 0,
width: a['width'] as int? ?? 0,
height: a['height'] as int? ?? 0,
));
break;
case 'processGone':
// The native host dropped this session (crash, cache-lock loss, or a
// create that failed — reason 'createFailed'). The texture is dead. Fail
Expand Down Expand Up @@ -552,6 +568,24 @@ class CefWebController {
Future<void> disableAgentControl() =>
_channel.invokeMethod('disableAgentControl', {'sessionId': sessionId});

/// Read the live off-screen frame surface: the global IOSurface id this
/// session's CVPixelBuffer is backed by, plus its PHYSICAL (Retina) pixel
/// dims. The on-demand pull counterpart to the [onSurface] event (which fires
/// on each (re)alloc). Returns null if the session doesn't exist yet, or a
/// [CefSurfaceInfo] with `surfaceId: 0` before the buffer is allocated. A
/// consumer mirroring the live pixels off-Flutter resolves the surface by id
/// and must re-read after any resize — the surface is freed and reallocated.
Future<CefSurfaceInfo?> getFrameSurface() async {
final res = await _channel.invokeMapMethod<String, dynamic>(
'getFrameSurface', {'sessionId': sessionId});
if (res == null) return null;
return CefSurfaceInfo(
surfaceId: res['surfaceId'] as int? ?? 0,
width: res['width'] as int? ?? 0,
height: res['height'] as int? ?? 0,
);
}

/// Load host-trusted content, bypassing the navigation scheme allowlist.
/// Backs [loadHtmlString] (data:) and [loadFile] (file:): the host explicitly
/// chose this content, so it isn't subject to `allowedSchemes` the way page
Expand Down
49 changes: 47 additions & 2 deletions packages/flutter_cef_macos/macos/Classes/CefWebSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ final class CefWebSession: NSObject, FlutterTexture {
var onDownload: ((String) -> Void)? // suggested name
var onImeBounds: ((Int, Int, Int, Int) -> Void)? // caret rect x,y,w,h (DIP)
var onCookies: ((Int, String) -> Void)? // request id, json array
// Fired when the backing IOSurface is (re)allocated — at create and on every
// resize() (which reallocs). Args are the live global surface id and the
// PHYSICAL (Retina) pixel dims. A consumer that mirrors the live frame
// (e.g. an off-Flutter capturer) reads the surface by id and must re-read on
// each fire, since resize() frees the old surface and allocs a new one.
var onSurface: ((UInt32, Int, Int) -> Void)? // surfaceId, physW, physH

let sessionId: String
private(set) var textureId: Int64 = 0
Expand Down Expand Up @@ -263,9 +269,14 @@ final class CefWebSession: NSObject, FlutterTexture {
let active = resizeInFlight && gen == resizeGen
let givenUp = active && (nowNs() &- resizeSentAtNs) > 300_000_000
var promotedTid: Int64 = 0
var promotedSid: UInt32 = 0
var promotedW = 0, promotedH = 0
if givenUp {
if let pending = pendingBuffer {
pixelBuffer = pending
promotedSid = pendingSurfaceId // capture before clearing
promotedW = width
promotedH = height
pendingBuffer = nil
pendingSurfaceId = 0
promotedTid = textureId
Expand All @@ -275,6 +286,8 @@ final class CefWebSession: NSObject, FlutterTexture {
bufferLock.unlock()
if givenUp {
if promotedTid != 0 { registry?.textureFrameAvailable(promotedTid) }
// R2: force-promoted a resized surface — notify WebRTC consumers (same as opPresent).
if promotedSid != 0 { notifySurface(promotedSid, promotedW, promotedH) }
maybeSendNextResize()
return
}
Expand Down Expand Up @@ -487,12 +500,36 @@ final class CefWebSession: NSObject, FlutterTexture {
@discardableResult
private func publishBuffers(_ surf: IOSurfaceRef, _ buffer: CVPixelBuffer,
_ w: Int, _ h: Int) -> UInt32 {
bufferLock.lock(); defer { bufferLock.unlock() }
bufferLock.lock()
ioSurface = surf
pixelBuffer = buffer
width = w
height = h
return IOSurfaceGetID(surf)
let sid = IOSurfaceGetID(surf)
bufferLock.unlock()
notifySurface(sid, w, h)
return sid
}

/// WebRTC frame export: notify any consumer that the live surface (re)allocated, so it
/// can IOSurfaceLookup the new id and re-point its capture (R2). Call OUTSIDE bufferLock
/// so the callback can read session accessors without self-deadlock. Reports PHYSICAL
/// (Retina) pixel dims = logical * dpr.
private func notifySurface(_ sid: UInt32, _ logicalW: Int, _ logicalH: Int) {
guard sid != 0 else { return }
onSurface?(sid, Int((Double(logicalW) * Double(dpr)).rounded()),
Int((Double(logicalH) * Double(dpr)).rounded()))
}

/// Re-emit the current live surface to a just-attached onSurface consumer. The init
/// publish fires before the plugin wires onSurface, so the plugin calls this right
/// after assigning the callback to deliver the initial surface.
func emitCurrentSurface() {
bufferLock.lock()
let surf = ioSurface
let w = width, h = height
bufferLock.unlock()
if let surf = surf { notifySurface(IOSurfaceGetID(surf), w, h) }
}

/// H4: read (w, h, dpr, surfaceId) as ONE consistent tuple under a single bufferLock
Expand All @@ -518,6 +555,8 @@ final class CefWebSession: NSObject, FlutterTexture {
// (BE u32). If it's our pending (resized) surface, promote it to live now — we
// kept serving the old surface until this exact frame so Flutter never sampled the
// blank new one. A present for the old/current surface just advances the frame.
var promotedSid: UInt32 = 0
var promotedW = 0, promotedH = 0
if payload.count >= 4 {
let psid = (UInt32(payload[0]) << 24) | (UInt32(payload[1]) << 16)
| (UInt32(payload[2]) << 8) | UInt32(payload[3])
Expand All @@ -526,10 +565,16 @@ final class CefWebSession: NSObject, FlutterTexture {
pendingBuffer = nil
pendingSurfaceId = 0
resizeInFlight = false // its paint landed; free to send the next size
promotedSid = psid
promotedW = width
promotedH = height
}
}
let tid = textureId
bufferLock.unlock()
// R2: a resized surface just went live — tell WebRTC consumers to re-point their
// IOSurface capture at the new id (this is the "fires on each resize" half).
if promotedSid != 0 { notifySurface(promotedSid, promotedW, promotedH) }
if tid != 0 {
DispatchQueue.main.async { [weak self] in
// Re-read on main (serialized with dispose()): the texture may have
Expand Down
27 changes: 27 additions & 0 deletions packages/flutter_cef_macos/macos/Classes/FlutterCefPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin {
case "navigate": navigate(args, result)
case "loadTrusted": loadTrusted(args, result)
case "resize": resize(args, result)
case "getFrameSurface": getFrameSurface(args, result)
case "dispose": destroy(args, result)
case "pointer": pointer(args, result)
case "key": key(args, result)
Expand Down Expand Up @@ -403,6 +404,15 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin {
session.onCookies = { [weak self] id, json in
self?.emit("cookies", ["sessionId": sessionId, "id": id, "json": json])
}
session.onSurface = { [weak self] surfaceId, width, height in
self?.emit("onSurface", [
"sessionId": sessionId, "surfaceId": Int(surfaceId),
"width": width, "height": height,
])
}
// The session's init publish fired onSurface before the callback above existed, so
// deliver the current surface now (no-op until the first surface is allocated).
session.emitCurrentSurface()
// Allocate the wire browserId + (when ready) issue opCreateBrowser. The
// process arg --allowed-schemes is shared by every browser in the profile;
// it's taken from the first browser that triggered the spawn.
Expand Down Expand Up @@ -584,6 +594,23 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin {
}
}

/// Read the session's live frame surface synchronously: the global IOSurface
/// id its CVPixelBuffer is backed by, plus the PHYSICAL (Retina) pixel dims
/// (logical w/h * dpr, matching allocateBuffers' rounding). A frame-export
/// consumer resolves the surface by id; the `onSurface` event fires it on each
/// (re)alloc, this verb is the on-demand pull. Returns nil for an unknown
/// session, or `surfaceId: 0` before the buffer is allocated.
private func getFrameSurface(_ a: [String: Any], _ result: @escaping FlutterResult) {
guard let id = a["sessionId"] as? String, let s = sessions[id] else {
result(nil)
return
}
let dpr = Double(s.scale)
let pw = max(1, Int((Double(s.w) * dpr).rounded()))
let ph = max(1, Int((Double(s.h) * dpr).rounded()))
result(["surfaceId": Int(s.surfaceId), "width": pw, "height": ph])
}

private func destroy(_ a: [String: Any], _ result: @escaping FlutterResult) {
if let id = a["sessionId"] as? String { disposeSession(id) }
result(nil)
Expand Down
27 changes: 27 additions & 0 deletions packages/flutter_cef_platform_interface/lib/src/cef_events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,33 @@ class CefJsDialogRequest {
String toString() => 'CefJsDialogRequest($message)';
}

/// The live frame surface backing a session: the global IOSurface id its
/// off-screen CVPixelBuffer is wrapped over, plus the surface's PHYSICAL
/// (Retina) pixel dimensions. Delivered by [CefWebController.onSurface] on each
/// (re)allocation (create + every resize) and pullable on demand via
/// [CefWebController.getFrameSurface]. A consumer resolves the surface by id
/// (e.g. `IOSurfaceLookup`) to mirror the live page pixels off-Flutter; it must
/// re-read on every change, since a resize frees the old surface.
class CefSurfaceInfo {
const CefSurfaceInfo({
required this.surfaceId,
required this.width,
required this.height,
});

/// The global IOSurface id, resolvable cross-process. 0 before allocation.
final int surfaceId;

/// Physical (Retina) pixel width of the surface.
final int width;

/// Physical (Retina) pixel height of the surface.
final int height;

@override
String toString() => 'CefSurfaceInfo($surfaceId, ${width}x$height)';
}

/// A cookie returned by [CefWebController.getCookies].
class CefCookie {
const CefCookie({
Expand Down
Loading