[Bug]: incorrect behavior of the WebContentsView when the electron app window is inactive
HyrepBall opened this issue · 0 comments
Preflight Checklist
- I have read the Contributing Guidelines for this project.
- I agree to follow the Code of Conduct that this project adheres to.
- I have searched the issue tracker for a bug report that matches the one I want to file, without success.
Electron Version
30.0.1
What operating system are you using?
Windows
Operating System Version
Windows 11 Pro version 23H2 Build 22631.3447
What arch are you using?
x64
Last Known Working Electron version
No response
Expected Behavior
the content in the WebContentsView should load in any state of the application, even when it is minimized
Actual Behavior
the content in the WebContentsView becomes visible only when the application becomes active (you can see this at 55 seconds in the video)
20240507132712.online-video-cutter.com.mp4
Testcase Gist URL
No response
Additional Information
In the video, Google Meet was used to reproduce the bug to clearly show the behavior of the application window in inactive mode
There was a bug with both BrowserView and WebContentsView
backgroundThrottling: false
doesn't solve my problem.
In my application there are 2 types of files: local ones which i download like video or pdf and remote (simple URLs) that renders with WebContentsView. Just in the video I compare the behavior of a regular vue.js component consisting of and the WebContentsView component in which the content begins to be displayed only if the application is made active (55 seconds in the video)
main.js
import { app, BrowserWindow, ipcMain } from 'electron'
import path from 'path'
import icon from '../assets/icon.png'
import { download } from './download'
import { createBrowserView } from './browserView'
import { createBrowserViewCustomEvents } from '../helpers/browserView'
import { getAppPaths } from './getAppPaths'
process.env.ROOT = path.join(__dirname, '..')
process.env.DIST = path.join(process.env.ROOT, 'dist-electron')
process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
? path.join(process.env.ROOT, 'public')
: path.join(process.env.ROOT, '.output/public')
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
const { VITE_DEV_SERVER_URL, VITE_PUBLIC, NODE_ENV } = process.env
let win: BrowserWindow
const preload = path.join(process.env.DIST, 'preload.js')
async function bootstrap() {
win = new BrowserWindow({
autoHideMenuBar: false,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
backgroundThrottling: false,
preload,
nodeIntegrationInWorker: true,
contextIsolation: false,
nodeIntegration: true,
webSecurity: false,
},
})
ipcMain.on('download', download(win))
ipcMain.on(createBrowserViewCustomEvents('').create, createBrowserView(win))
ipcMain.on('get_app_paths', getAppPaths(win))
win.maximize()
win.on('ready-to-show', () => {
win.show()
})
if (VITE_DEV_SERVER_URL) {
await win.loadURL(VITE_DEV_SERVER_URL)
win.webContents.openDevTools()
} else {
win.setFullScreen(true)
win.setMenu(null)
await win.loadFile(path.join(VITE_PUBLIC!, 'index.html'))
}
}
app.whenReady().then(bootstrap)
createBrowserView function
import {
BrowserView,
WebContentsView,
BrowserWindow,
ipcMain,
type IpcMainEvent,
type Rectangle,
} from 'electron'
import { BrowserViewEventsEnum } from '../types/browserView'
import { createBrowserViewCustomEvents } from '../helpers/browserView'
interface EventData {
url: string
id: string
bounds: Rectangle
}
export const createBrowserView =
(win: BrowserWindow) =>
async (event: IpcMainEvent, { url, id, bounds }: EventData) => {
const view = new WebContentsView({
webPreferences: {
backgroundThrottling: false,
},
})
Object.values(BrowserViewEventsEnum).forEach((name) =>
// @ts-ignore
view.webContents.on(name, () => win.webContents.send(`${name}_${id}`)),
)
const customEvents = createBrowserViewCustomEvents(id)
ipcMain.on(customEvents.reload, () => view.webContents.reload())
ipcMain.on(customEvents.pageBack, () => {
if (!view.webContents.canGoBack()) return
view.webContents.goBack()
})
ipcMain.on(customEvents.pageForward, () => {
if (!view.webContents.canGoForward()) return
view.webContents.goForward()
})
ipcMain.on(customEvents.zoomOut, () => {
const factor = view.webContents.getZoomFactor()
view.webContents.setZoomFactor(factor - 0.1)
})
ipcMain.on(customEvents.zoomIn, () => {
const factor = view.webContents.getZoomFactor()
view.webContents.setZoomFactor(factor + 0.1)
})
ipcMain.on(customEvents.close, () => {
view.webContents?.close()
win.contentView.removeChildView(view)
})
ipcMain.on(customEvents.execute, async (_, { script }) => {
try {
await view.webContents.executeJavaScript(script)
} catch (e) {
console.log(e)
}
})
win.contentView.addChildView(view)
view.setBounds(bounds)
await view.webContents.loadURL(url)
}
useBrowserView hook that uses in vue.js component for intercation with electron processes
import { useIpcRenderer } from '@vueuse/electron'
import { nanoid } from 'nanoid'
import type { Ref } from '@vue/reactivity'
import { z } from 'zod'
import { BrowserViewEventsEnum } from '~/types/browserView'
import { createBrowserViewCustomEvents } from '~/helpers/browserView'
import mitt from 'mitt'
import path from 'path'
const fs = window.require('fs-extra')
interface Options {
container: Ref<HTMLDivElement | undefined>
url: string
}
interface BrowserViewHandler {
action: BrowserViewEventsEnum
once: boolean
}
type ScriptFileName =
| 'zoomInExcel'
| 'zoomOutExcel'
| 'pageUpExcel'
| 'pageDownExcel'
| 'slideNextExcel'
| 'slidePrevExcel'
| 'pageLeftExcel'
| 'pageRightExcel'
| 'zoomInWord'
| 'zoomOutWord'
| 'listDownWord'
| 'listUpWord'
| 'pageUpMiro'
| 'pageDownMiro'
| 'slideNextMiro'
| 'slidePrevMiro'
| 'pageLeftMiro'
| 'pageRightMiro'
| 'slideNextFigma'
| 'slidePrevFigma'
| 'slideNextPptx'
| 'slidePrevPptx'
| 'removeTargetBlancAttribute'
const urlValidationScheme = z.string().trim().url()
export function useBrowserView({ url, container }: Options) {
console.log('on useBrowserView')
const ipcRenderer = useIpcRenderer()
const id = nanoid()
const customEvents = createBrowserViewCustomEvents(id)
const { GLOBAL_DIRS } = useAppPaths()
const bus = mitt<Record<BrowserViewEventsEnum, undefined>>()
const executeViewScript = (script: string) => {
ipcRenderer.send(customEvents.execute, { script })
}
const executeViewScriptFromFile = async (name: ScriptFileName) => {
const scriptPath = path.join(
GLOBAL_DIRS.PUBLIC_DIR,
'web-view-scripts',
`${name}.js`,
)
const script = await fs.readFile(scriptPath, 'utf8')
executeViewScript(script)
}
const reloadView = () => ipcRenderer.send(customEvents.reload)
const pageBackView = () => ipcRenderer.send(customEvents.pageBack)
const pageForwardView = () => ipcRenderer.send(customEvents.pageForward)
const zoomOutView = () => ipcRenderer.send(customEvents.zoomOut)
const zoomInView = () => ipcRenderer.send(customEvents.zoomIn)
const handlers: BrowserViewHandler[] = [
{
action: BrowserViewEventsEnum.DidFinishLoad,
once: false,
},
{
action: BrowserViewEventsEnum.DomReady,
once: false,
},
]
handlers.forEach((h) => {
const name = `${h.action}_${id}`
if (h.once) {
ipcRenderer.once(name, () => bus.emit(h.action))
} else {
ipcRenderer.on(name, () => bus.emit(h.action))
}
})
const onViewEvent = bus.on
onViewEvent(BrowserViewEventsEnum.DidFinishLoad, async () => {
await executeViewScriptFromFile('removeTargetBlancAttribute')
})
onActivated(() => {
try {
urlValidationScheme.parse(url)
const bounds = container.value?.getBoundingClientRect()
ipcRenderer.send(customEvents.create, {
url,
id,
bounds: JSON.parse(JSON.stringify(bounds)),
})
} catch (e) {
console.log(e)
}
})
onDeactivated(() => {
ipcRenderer.send(customEvents.close)
bus.all.clear()
})
return {
reloadView,
pageBackView,
pageForwardView,
zoomInView,
zoomOutView,
executeViewScript,
executeViewScriptFromFile,
onViewEvent,
}
}
vue.js component
<template>
<div ref="container" class="relative size-full" />
</template>
<script lang="ts" setup>
import { ActionEnum } from '~/types/actions'
import {
MaterialAuthTypeEnum,
type MaterialComponentProps,
MaterialSubTypeEnum,
MaterialType,
PictureMaterialState,
} from '~/types/material'
import { BrowserViewEventsEnum } from '~/types/browserView'
import { wait } from '~/utils'
const props = defineProps<MaterialComponentProps>()
const { areaId, statusContentArea, material } = toRefs(props)
const materialId = ref(material.value.id)
const container = ref<HTMLDivElement>()
const url = material.value.url || ''
console.log('browser.vue before useBrowserView')
const {
reloadView,
pageBackView,
pageForwardView,
zoomInView,
zoomOutView,
executeViewScript,
executeViewScriptFromFile,
onViewEvent,
} = useBrowserView({
container,
url,
})
const { updateState, state } = useMaterialState<PictureMaterialState>(
MaterialType.Picture,
{
material: material.value,
areaId: areaId.value,
statusContentId: statusContentArea.value.statusContentId,
},
)
onViewEvent(BrowserViewEventsEnum.DidFinishLoad, async () => {
const { authData } = material.value
if (authData?.authType !== MaterialAuthTypeEnum.Script) return
if (!authData?.authScript) return
let script = authData.authScript
if (authData.password) {
script = script.replaceAll('{{password}}', authData.password)
}
if (authData.username) {
script = script.replaceAll('{{username}}', authData.username)
}
await wait(3000)
executeViewScript(script)
})
</script>