Skip to content
Closed
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
5 changes: 5 additions & 0 deletions package/src/bun/core/BrowserView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export type BrowserViewOptions<T = undefined> = {
startTransparent: boolean;
// Set passthrough on the AbstractView at creation (before first paint)
startPassthrough: boolean;
// Allow background media playback (prevents suspension when app loses focus)
backgroundMedia: boolean;
// renderer:
};

Expand Down Expand Up @@ -106,6 +108,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
sandbox: boolean = false;
startTransparent: boolean = false;
startPassthrough: boolean = false;
backgroundMedia: boolean = false;
isRemoved: boolean = false;

constructor(options: Partial<BrowserViewOptions<T>> = defaultOptions) {
Expand Down Expand Up @@ -135,6 +138,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
this.sandbox = options.sandbox ?? false;
this.startTransparent = options.startTransparent ?? false;
this.startPassthrough = options.startPassthrough ?? false;
this.backgroundMedia = options.backgroundMedia ?? false;

BrowserViewMap[this.id] = this;
this.ptr = this.init() as Pointer;
Expand Down Expand Up @@ -176,6 +180,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
sandbox: this.sandbox,
startTransparent: this.startTransparent,
startPassthrough: this.startPassthrough,
backgroundMedia: this.backgroundMedia,
// transparent is looked up from parent window in native.ts
});
}
Expand Down
6 changes: 6 additions & 0 deletions package/src/bun/core/BrowserWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export type WindowOptionsType<T = undefined> = {
// Use for untrusted content (remote URLs) to prevent malicious sites from
// accessing internal APIs, creating OOPIFs, or communicating with Bun
sandbox: boolean;
// Allow background media playback (prevents suspension when app loses focus)
backgroundMedia: boolean;
};

const defaultOptions: WindowOptionsType = {
Expand All @@ -62,6 +64,7 @@ const defaultOptions: WindowOptionsType = {
hidden: false,
navigationRules: null,
sandbox: false,
backgroundMedia: false,
};

export const BrowserWindowMap: {
Expand Down Expand Up @@ -126,6 +129,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
navigationRules: string | null = null;
// Sandbox mode disables RPC and only allows event emission (for untrusted content)
sandbox: boolean = false;
backgroundMedia: boolean = false;
frame: {
x: number;
y: number;
Expand Down Expand Up @@ -155,6 +159,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
this.hidden = options.hidden ?? false;
this.navigationRules = options.navigationRules || null;
this.sandbox = options.sandbox ?? false;
this.backgroundMedia = options.backgroundMedia ?? false;

this.init(options);
}
Expand Down Expand Up @@ -239,6 +244,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
navigationRules: this.navigationRules,
sandbox: this.sandbox,
startPassthrough: this.passthrough,
backgroundMedia: this.backgroundMedia,
});

this.webviewId = webview.id;
Expand Down
5 changes: 4 additions & 1 deletion package/src/bun/proc/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export const native = (() => {
args: [
FFIType.bool, // startTransparent
FFIType.bool, // startPassthrough
FFIType.bool, // backgroundMedia
],
returns: FFIType.void,
},
Expand Down Expand Up @@ -1178,6 +1179,7 @@ const _ffiImpl = {
sandbox: boolean;
startTransparent: boolean;
startPassthrough: boolean;
backgroundMedia: boolean;
}): FFIType.ptr => {
const {
id,
Expand All @@ -1197,6 +1199,7 @@ const _ffiImpl = {
sandbox,
startTransparent,
startPassthrough,
backgroundMedia,
} = params;

const parentWindow = BrowserWindow.getById(windowId);
Expand Down Expand Up @@ -1250,7 +1253,7 @@ window.__electrobunBunBridge = window.__electrobunBunBridge || window.webkit?.me
const customPreload = preload;

// Pre-set flags before initWebview (workaround for FFI param count limits)
native_.symbols.setNextWebviewFlags(startTransparent, startPassthrough);
native_.symbols.setNextWebviewFlags(startTransparent, startPassthrough, backgroundMedia);
const webviewPtr = native_.symbols.initWebview(
id,
windowPtr,
Expand Down
10 changes: 7 additions & 3 deletions package/src/native/linux/nativeWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6452,7 +6452,8 @@ AbstractView* initCEFWebview(uint32_t webviewId,
static struct {
bool startTransparent;
bool startPassthrough;
} g_nextWebviewFlags = {false, false};
bool backgroundMedia;
} g_nextWebviewFlags = {false, false, false};

AbstractView* initGTKWebkitWebview(uint32_t webviewId,
void* window,
Expand Down Expand Up @@ -6519,9 +6520,10 @@ AbstractView* initGTKWebkitWebview(uint32_t webviewId,
return result;
}

ELECTROBUN_EXPORT void setNextWebviewFlags(bool startTransparent, bool startPassthrough) {
ELECTROBUN_EXPORT void setNextWebviewFlags(bool startTransparent, bool startPassthrough, bool backgroundMedia) {
g_nextWebviewFlags.startTransparent = startTransparent;
g_nextWebviewFlags.startPassthrough = startPassthrough;
g_nextWebviewFlags.backgroundMedia = backgroundMedia;
}

ELECTROBUN_EXPORT AbstractView* initWebview(uint32_t webviewId,
Expand All @@ -6545,7 +6547,9 @@ ELECTROBUN_EXPORT AbstractView* initWebview(uint32_t webviewId,
// Read and clear pre-set flags
bool startTransparent = g_nextWebviewFlags.startTransparent;
bool startPassthrough = g_nextWebviewFlags.startPassthrough;
g_nextWebviewFlags = {false, false};
bool backgroundMedia = g_nextWebviewFlags.backgroundMedia;
(void)backgroundMedia; // Not used on Linux
g_nextWebviewFlags = {false, false, false};

// TODO: Implement transparent handling for Linux

Expand Down
69 changes: 56 additions & 13 deletions package/src/native/macos/nativeWrapper.mm
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,8 @@ - (instancetype)initWithWebviewId:(uint32_t)webviewId
customPreloadScript:(const char *)customPreloadScript
viewsRoot:(const char *)viewsRoot
transparent:(bool)transparent
sandbox:(bool)sandbox;
sandbox:(bool)sandbox
backgroundMedia:(bool)backgroundMedia;
@end

@interface WGPUViewImpl : AbstractView
Expand Down Expand Up @@ -2421,6 +2422,7 @@ - (instancetype)initWithWebviewId:(uint32_t)webviewId
viewsRoot:(const char *)viewsRoot
transparent:(bool)transparent
sandbox:(bool)sandbox
backgroundMedia:(bool)backgroundMedia
{
self = [super init];
if (self) {
Expand All @@ -2434,16 +2436,33 @@ - (instancetype)initWithWebviewId:(uint32_t)webviewId
// TODO: rewrite this so we can return a reference to the AbstractRenderer and then call
// init from zig after the handle is added to the webviewMap then we don't need this async stuff
dispatch_async(dispatch_get_main_queue(), ^{

// configuration
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];

configuration.websiteDataStore = createDataStoreForPartition(partitionIdentifier);

[configuration.preferences setValue:@YES forKey:@"developerExtrasEnabled"];
[configuration.preferences setValue:@YES forKey:@"elementFullscreenEnabled"];
[configuration.preferences setValue:@YES forKey:@"allowsPictureInPictureMediaPlayback"];


[configuration.preferences setValue:@YES forKey:@"developerExtrasEnabled"];
[configuration.preferences setValue:@YES forKey:@"elementFullscreenEnabled"];
[configuration.preferences setValue:@YES forKey:@"allowsPictureInPictureMediaPlayback"];

if (backgroundMedia) {
// Allow media playback without requiring a user gesture (enables autoplay)
configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone;

// Keep WebView content process running at foreground priority even when
// the application loses focus, preventing audio/media suspension
[configuration setValue:@YES forKey:@"_alwaysRunsAtForegroundPriority"];

// Ensure media data loads automatically without waiting for user interaction
[configuration setValue:@YES forKey:@"_mediaDataLoadsAutomatically"];

// Disable page visibility-based process suppression so WebKit does not
// throttle or suspend the content process when the window is occluded
// (e.g., when the user is 2+ Spaces/desktops away)
[configuration.preferences setValue:@NO forKey:@"_pageVisibilityBasedProcessSuppressionEnabled"];
}

// Add scheme handler
MyURLSchemeHandler *assetSchemeHandler = [[MyURLSchemeHandler alloc] init];
// TODO: Consider storing views handler globally and not on each AbstractView
Expand Down Expand Up @@ -5475,7 +5494,8 @@ - (instancetype)initWithWebviewId:(uint32_t)webviewId
customPreloadScript:(const char *)customPreloadScript
viewsRoot:(const char *)viewsRoot
transparent:(bool)transparent
sandbox:(bool)sandbox;
sandbox:(bool)sandbox
backgroundMedia:(bool)backgroundMedia;

@end

Expand Down Expand Up @@ -5852,6 +5872,7 @@ - (instancetype)initWithWebviewId:(uint32_t)webviewId
viewsRoot:(const char *)viewsRoot
transparent:(bool)transparent
sandbox:(bool)sandbox
backgroundMedia:(bool)backgroundMedia
{
self = [super init];
if (self) {
Expand Down Expand Up @@ -6541,11 +6562,13 @@ - (void)windowDidResignKey:(NSNotification *)notification {
static struct {
bool startTransparent;
bool startPassthrough;
} g_nextWebviewFlags = {false, false};
bool backgroundMedia;
} g_nextWebviewFlags = {false, false, false};

extern "C" void setNextWebviewFlags(bool startTransparent, bool startPassthrough) {
extern "C" void setNextWebviewFlags(bool startTransparent, bool startPassthrough, bool backgroundMedia) {
g_nextWebviewFlags.startTransparent = startTransparent;
g_nextWebviewFlags.startPassthrough = startPassthrough;
g_nextWebviewFlags.backgroundMedia = backgroundMedia;
}

extern "C" AbstractView* initWebview(uint32_t webviewId,
Expand All @@ -6570,7 +6593,8 @@ - (void)windowDidResignKey:(NSNotification *)notification {
// Read and clear pre-set flags
bool startTransparent = g_nextWebviewFlags.startTransparent;
bool startPassthrough = g_nextWebviewFlags.startPassthrough;
g_nextWebviewFlags = {false, false};
bool backgroundMedia = g_nextWebviewFlags.backgroundMedia;
g_nextWebviewFlags = {false, false, false};

// Validate frame values - use defaults if NaN or invalid
if (isnan(x) || isinf(x)) {
Expand Down Expand Up @@ -6612,13 +6636,20 @@ - (void)windowDidResignKey:(NSNotification *)notification {
customPreloadScript:strdup(customPreloadScript)
viewsRoot:strdup(viewsRoot)
transparent:transparent
sandbox:sandbox];
sandbox:sandbox
backgroundMedia:backgroundMedia];

// Store initial state flags — applied later in each impl's deferred creation block
// (nsView is nil at this point because view creation is async)
impl.pendingStartTransparent = startTransparent;
impl.pendingStartPassthrough = startPassthrough;

// Enable occlusion state override on the window so WebKit doesn't
// suspend media when the window is on a non-adjacent Space
if (backgroundMedia && [window isKindOfClass:[ElectrobunWindow class]]) {
((ElectrobunWindow *)window).backgroundMediaEnabled = YES;
}

});

return impl;
Expand Down Expand Up @@ -7014,11 +7045,23 @@ - (void)windowDidResignKey:(NSNotification *)notification {


@interface ElectrobunWindow : NSWindow
@property (nonatomic, assign) BOOL backgroundMediaEnabled;
@end

@implementation ElectrobunWindow
- (BOOL)canBecomeKeyWindow { return YES; }
- (BOOL)canBecomeMainWindow { return YES; }

// When backgroundMedia is enabled, report the window as visible to prevent
// WebKit from suspending media playback when the window is occluded
// (e.g., on a non-adjacent Space). macOS clears NSWindowOcclusionStateVisible
// for fully occluded windows, which causes WKWebView to throttle/suspend audio.
- (NSWindowOcclusionState)occlusionState {
if (self.backgroundMediaEnabled) {
return [super occlusionState] | NSWindowOcclusionStateVisible;
}
return [super occlusionState];
}
@end

NSWindow *createNSWindowWithFrameAndStyle(uint32_t windowId,
Expand Down
10 changes: 7 additions & 3 deletions package/src/native/win/nativeWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7093,11 +7093,13 @@ ELECTROBUN_EXPORT void shutdownApplication() {
static struct {
bool startTransparent;
bool startPassthrough;
} g_nextWebviewFlags = {false, false};
bool backgroundMedia;
} g_nextWebviewFlags = {false, false, false};

ELECTROBUN_EXPORT void setNextWebviewFlags(bool startTransparent, bool startPassthrough) {
ELECTROBUN_EXPORT void setNextWebviewFlags(bool startTransparent, bool startPassthrough, bool backgroundMedia) {
g_nextWebviewFlags.startTransparent = startTransparent;
g_nextWebviewFlags.startPassthrough = startPassthrough;
g_nextWebviewFlags.backgroundMedia = backgroundMedia;
}

// Clean, elegant initWebview function - Windows version matching Mac pattern
Expand All @@ -7123,7 +7125,9 @@ ELECTROBUN_EXPORT AbstractView* initWebview(uint32_t webviewId,
// Read and clear pre-set flags
bool startTransparent = g_nextWebviewFlags.startTransparent;
bool startPassthrough = g_nextWebviewFlags.startPassthrough;
g_nextWebviewFlags = {false, false};
bool backgroundMedia = g_nextWebviewFlags.backgroundMedia;
(void)backgroundMedia; // Not used on Windows
g_nextWebviewFlags = {false, false, false};

// Serialize webview creation to avoid CEF/WebView2 conflicts
std::lock_guard<std::mutex> lock(g_webviewCreationMutex);
Expand Down