mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-26 15:55:35 +00:00
Fix export sync and allow dev window capture
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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[]
|
||||
|
||||
Reference in New Issue
Block a user