fix(electron): render video preview posters and document Linux compositing workaround

This commit is contained in:
Palash Debnath
2026-09-15 14:59:58 +05:30
parent d6ca24a6bb
commit 3569ac2d24
4 changed files with 29 additions and 3 deletions
+1
View File
@@ -27,6 +27,7 @@ the frozen-backend fallback mirror it for their toolchains.
### Fixed
- Video previews show their thumbnail before playback, including the source video in Dub (#2129)
- Linux and Windows workspace headers consistently expand and collapse the sidebar, with the app logo at the top of the collapsed rail (#2129)
- Stopping a process on macOS no longer fails with "Operation not permitted" when it was already exiting (#2032)
- A YouTube link blocked by its "not a bot" check now says how to attach signed-in cookies in Dub, instead of quoting yt-dlp's command-line flags (#2036, #2034)
+4
View File
@@ -77,3 +77,7 @@ Branding remains in a fixed native title row while onboarding status loads, inst
On Windows and Linux, the collapsed workspace sidebar shows the VoiceStudio icon at the top. Use the toggle beside the page title to expand the sidebar. macOS retains its existing sidebar control.
Every main workspace header exposes the same sidebar toggle, including pages that automatically collapse the voice library at narrow widths. The control reflects the visible sidebar state and explicitly expands it for the current workspace.
The shared video player renders Vidstack's poster before playback, including the Dub source thumbnail, and hides it once playback starts.
On Linux Wayland systems where Chromium logs `eglCreateImage failed` / `OzoneImageBacking` and video or window contents flicker, launch Electron with `--disable-gpu-compositing`. For source development, run `ELECTRON_CLI_ARGS='["--disable-gpu-compositing"]' bun run dev` from `electron/`. This opt-in uses software window compositing while leaving backend CUDA inference available; it does not disable acceleration for other installations. It requires a full Electron restart, not a renderer reload. A refused connection to port 3903 instead means the development proxy is stopped; restart the Electron development process to restore it.
@@ -11,7 +11,7 @@ import {
RotateCwIcon,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { MediaPlayerProps } from '@vidstack/react';
import { Poster, type MediaPlayerProps } from '@vidstack/react';
import {
StudioMediaPlayer,
MediaProvider,
@@ -75,7 +75,12 @@ export const VideoPlayer = memo(function VideoPlayer({
<MediaProvider
loaders={videoLoaders}
className="relative aspect-video [&_[data-remotion-canvas]]:h-full [&_[data-remotion-canvas]]:w-full [&_[data-remotion-container]]:h-full [&_[data-remotion-container]]:w-full [&_video]:h-full [&_video]:w-full [&_iframe]:h-full [&_iframe]:w-full"
/>
>
<Poster
alt=""
className="absolute inset-0 h-full w-full object-contain opacity-0 data-[visible]:opacity-100 data-[hidden]:hidden"
/>
</MediaProvider>
<VideoControls player={player} source={source} sourceIdentity={sourceIdentity} />
</StudioMediaPlayer>
);
+17 -1
View File
@@ -3,7 +3,9 @@ import { chromium } from 'playwright';
import assert from 'node:assert/strict';
import { wave } from './test-wave.mjs';
const browser = await chromium.launch({
channel: process.env.PLAYWRIGHT_CHANNEL || 'msedge',
...(process.env.PLAYWRIGHT_EXECUTABLE_PATH
? { executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH }
: { channel: process.env.PLAYWRIGHT_CHANNEL || 'msedge' }),
headless: true,
});
try {
@@ -176,6 +178,9 @@ try {
});
});
if (video) {
await page.route('**/api/dub/thumb/fixture', (route) =>
route.fulfill({ contentType: 'image/svg+xml', body: '<svg xmlns="http://www.w3.org/2000/svg" width="320" height="180"><rect width="320" height="180" fill="teal"/></svg>' }),
);
await page.route('**/api/dub/media/fixture', (route) => {
if (route.request().method() === 'HEAD') mediaHeadRequests++;
return route.fulfill({ contentType: 'video/mp4', body: video });
@@ -256,7 +261,17 @@ try {
assert.match(await translateWithAgent.getAttribute('title'), /Codex/);
let script = await editSegment(0);
if (video) {
const poster = page.locator('[data-media-player] img[src*="/dub/thumb/fixture"]');
await poster.waitFor({ state: 'visible', timeout: 5000 });
await page.waitForFunction(() => {
const image = document.querySelector('[data-media-player] img[src*="/dub/thumb/fixture"]');
return image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0;
});
await page.getByRole('button', { name: 'Play', exact: true }).click();
await page.waitForFunction(() => {
const image = document.querySelector('[data-media-player] img[src*="/dub/thumb/fixture"]');
return image && getComputedStyle(image).opacity === '0';
});
await page.waitForFunction(() => {
const video = document.querySelector('video');
return video && !video.paused && video.currentTime > 0.1;
@@ -274,6 +289,7 @@ try {
await page.getByRole('button', { name: 'Exit full screen', exact: true }).click();
await page.waitForFunction(() => !document.fullscreenElement);
}
script = await editSegment(0);
assert.equal(await script.inputValue(), 'Hello there');
await editSegment(1);
const timeline = page.getByRole('region', { name: 'Segment timeline', exact: true });