Fix export sync and allow dev window capture

This commit is contained in:
webadderall
2026-03-11 17:36:34 +11:00
parent 96152ed24b
commit 26ea6d244c
3 changed files with 76 additions and 14 deletions
+17 -1
View File
@@ -17,6 +17,7 @@ const SHORTCUTS_FILE = path.join(app.getPath('userData'), 'shortcuts.json')
const AUTO_RECORDING_PREFIX = 'recording-'
const AUTO_RECORDING_RETENTION_COUNT = 20
const AUTO_RECORDING_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000
const ALLOW_RECORDLY_WINDOW_CAPTURE = Boolean(process.env['VITE_DEV_SERVER_URL'])
function getScreen() {
return nodeRequire('electron').screen as typeof import('electron').screen
@@ -1201,6 +1202,7 @@ export function registerIpcHandlers(
.map((name) => normalizeDesktopSourceName(name))
.filter(Boolean)
)
const ownAppName = normalizeDesktopSourceName(app.getName())
const screenSources = electronSources
.filter((source) => source.id.startsWith('screen:'))
@@ -1222,6 +1224,10 @@ export function registerIpcHandlers(
return true
}
if (ALLOW_RECORDLY_WINDOW_CAPTURE && normalizedName.includes('recordly')) {
return true
}
for (const ownName of ownWindowNames) {
if (!ownName) continue
if (normalizedName === ownName) {
@@ -1255,10 +1261,14 @@ export function registerIpcHandlers(
const normalizedWindowName = normalizeDesktopSourceName(source.windowTitle ?? source.name)
const normalizedAppName = normalizeDesktopSourceName(source.appName ?? '')
if (normalizedAppName && normalizedAppName === normalizeDesktopSourceName(app.getName())) {
if (!ALLOW_RECORDLY_WINDOW_CAPTURE && normalizedAppName && normalizedAppName === ownAppName) {
return false
}
if (ALLOW_RECORDLY_WINDOW_CAPTURE && (normalizedAppName === 'recordly' || normalizedWindowName?.includes('recordly'))) {
return true
}
if (!normalizedWindowName) {
return true
}
@@ -1299,6 +1309,10 @@ export function registerIpcHandlers(
return true
}
if (ALLOW_RECORDLY_WINDOW_CAPTURE && normalizedName.includes('recordly')) {
return true
}
for (const ownName of ownWindowNames) {
if (!ownName) continue
if (normalizedName === ownName || normalizedName.includes(ownName) || ownName.includes(normalizedName)) {
@@ -1375,6 +1389,8 @@ export function registerIpcHandlers(
const appName = normalizeDesktopSourceName(String(source?.appName ?? ''))
const ownAppName = normalizeDesktopSourceName(app.getName())
if (
!ALLOW_RECORDLY_WINDOW_CAPTURE
&&
source?.id?.startsWith('window:')
&& appName
&& (appName === ownAppName || appName === 'recordly')
+23 -1
View File
@@ -2,6 +2,7 @@ import { Application, Container, Sprite, Graphics, BlurFilter, Texture } from 'p
import { MotionBlurFilter } from 'pixi-filters/motion-blur';
import type { ZoomRegion, CropRegion, AnnotationRegion, SpeedRegion, CursorTelemetryPoint } from '@/components/video-editor/types';
import { ZOOM_DEPTH_SCALES } from '@/components/video-editor/types';
import { getRenderableAssetUrl } from '@/lib/assetPath';
import { findDominantRegion } from '@/components/video-editor/videoPlayback/zoomRegionUtils';
import { applyZoomTransform, computeFocusFromTransform, computeZoomTransform, createMotionBlurState, type MotionBlurState } from '@/components/video-editor/videoPlayback/zoomTransform';
import { DEFAULT_FOCUS, ZOOM_SCALE_DEADZONE, ZOOM_TRANSLATION_DEADZONE_PX } from '@/components/video-editor/videoPlayback/constants';
@@ -184,7 +185,7 @@ export class FrameRenderer {
}
private async setupBackground(): Promise<void> {
const wallpaper = this.config.wallpaper;
const wallpaper = await this.resolveWallpaperForExport(this.config.wallpaper);
// Create background canvas for separate rendering (not affected by zoom)
const bgCanvas = document.createElement('canvas');
@@ -300,6 +301,27 @@ export class FrameRenderer {
this.backgroundSprite = bgCanvas as any;
}
private async resolveWallpaperForExport(wallpaper: string): Promise<string> {
if (!wallpaper) {
return wallpaper;
}
if (wallpaper.startsWith('#') || wallpaper.startsWith('linear-gradient') || wallpaper.startsWith('radial-gradient')) {
return wallpaper;
}
const looksLikeAbsoluteFilePath = wallpaper.startsWith('/')
&& !wallpaper.startsWith('//')
&& !wallpaper.startsWith('/wallpapers/')
&& !wallpaper.startsWith('/app-icons/');
const wallpaperAsset = looksLikeAbsoluteFilePath
? `file://${encodeURI(wallpaper)}`
: wallpaper;
return getRenderableAssetUrl(wallpaperAsset);
}
async renderFrame(videoFrame: VideoFrame, timestamp: number): Promise<void> {
if (!this.app || !this.videoContainer || !this.cameraContainer) {
throw new Error('Renderer not initialized');
+36 -12
View File
@@ -291,9 +291,9 @@ export class StreamingVideoDecoder {
/**
* Resample buffered frames to fill the target frame count for this segment.
* Handles VFR sources by duplicating/decimating as needed.
* Uses interpolated source timestamps so animations advance smoothly
* even when multiple export frames map to the same source frame.
* Handles VFR sources by duplicating/decimating as needed.
* Choose the source frame by target source time so the picture and overlays
* are both driven from the same display timeline.
*/
private async deliverSegment(
frames: VideoFrame[],
@@ -312,23 +312,47 @@ export class StreamingVideoDecoder {
let exportFrameIndex = startExportFrameIndex;
for (let i = 0; i < segmentFrameCount && !this.cancelled; i++) {
const sourceIdx = Math.min(
Math.floor(i * frames.length / segmentFrameCount),
frames.length - 1
);
const progress = segmentFrameCount > 1 ? i / segmentFrameCount : 0;
const targetSourceTimeMs = (segment.startSec + progress * segmentDuration) * 1000;
const sourceIdx = this.findSourceFrameIndexForTime(frames, targetSourceTimeMs * 1000);
const sourceFrame = frames[sourceIdx];
// Compute a uniformly-spaced source timestamp so zoom/cursor animations
// progress smoothly instead of stalling on duplicate VFR timestamps
const t = segmentFrameCount > 1 ? i / segmentFrameCount : 0;
const interpolatedSourceTimeMs = (segment.startSec + t * segmentDuration) * 1000;
const clone = new VideoFrame(sourceFrame, { timestamp: sourceFrame.timestamp });
await onFrame(clone, exportFrameIndex * frameDurationUs, interpolatedSourceTimeMs);
await onFrame(clone, exportFrameIndex * frameDurationUs, targetSourceTimeMs);
exportFrameIndex++;
}
return exportFrameIndex;
}
private findSourceFrameIndexForTime(frames: VideoFrame[], targetTimestampUs: number): number {
if (frames.length <= 1) {
return 0;
}
if (targetTimestampUs <= frames[0].timestamp) {
return 0;
}
const lastIndex = frames.length - 1;
if (targetTimestampUs >= frames[lastIndex].timestamp) {
return lastIndex;
}
let lo = 0;
let hi = lastIndex;
while (lo < hi) {
const mid = Math.ceil((lo + hi) / 2);
if (frames[mid].timestamp <= targetTimestampUs) {
lo = mid;
} else {
hi = mid - 1;
}
}
return lo;
}
private computeSegments(
totalDuration: number,
trimRegions?: TrimRegion[]