fix(recording): validate video outputs before open

This commit is contained in:
webadderall
2026-03-28 16:10:49 +11:00
parent 045850417f
commit 6fb7444c03
+62 -1
View File
@@ -284,6 +284,63 @@ async function getFileSizeIfPresent(filePath: string | null | undefined) {
}
}
function parseFfmpegDurationSeconds(stderr: string) {
const match = stderr.match(/Duration:\s+(\d+):(\d+):(\d+(?:\.\d+)?)/i)
if (!match) {
return null
}
const hours = Number(match[1])
const minutes = Number(match[2])
const seconds = Number(match[3])
if (![hours, minutes, seconds].every(Number.isFinite)) {
return null
}
return hours * 3600 + minutes * 60 + seconds
}
async function validateRecordedVideo(videoPath: string) {
const stat = await fs.stat(videoPath)
if (!stat.isFile()) {
throw new Error(`Recorded output is not a file: ${videoPath}`)
}
if (stat.size <= 0) {
throw new Error(`Recorded output is empty: ${videoPath}`)
}
const ffmpegPath = getFfmpegBinaryPath()
let stderr = ''
try {
const result = await execFileAsync(
ffmpegPath,
['-hide_banner', '-i', videoPath, '-map', '0:v:0', '-frames:v', '1', '-f', 'null', '-'],
{ timeout: 20000, maxBuffer: 10 * 1024 * 1024 },
)
stderr = result.stderr
} catch (error) {
const execError = error as NodeJS.ErrnoException & { stderr?: string }
const output = execError.stderr?.trim()
throw new Error(output || `Recorded output could not be decoded: ${videoPath}`)
}
if (!/Stream #.*Video:/i.test(stderr)) {
throw new Error(`Recorded output does not contain a readable video stream: ${videoPath}`)
}
const durationSeconds = parseFfmpegDurationSeconds(stderr)
if (durationSeconds !== null && durationSeconds <= 0) {
throw new Error(`Recorded output has an invalid duration: ${videoPath}`)
}
return {
fileSizeBytes: stat.size,
durationSeconds,
}
}
async function getProjectsDir() {
const projectsDir = path.join(await getRecordingsDir(), PROJECTS_DIRECTORY_NAME)
await fs.mkdir(projectsDir, { recursive: true })
@@ -2548,6 +2605,8 @@ function snapshotCursorTelemetryForPersistence() {
}
async function finalizeStoredVideo(videoPath: string) {
const validation = await validateRecordedVideo(videoPath)
snapshotCursorTelemetryForPersistence()
currentVideoPath = videoPath
currentProjectPath = null
@@ -2559,7 +2618,9 @@ async function finalizeStoredVideo(videoPath: string) {
return {
success: true,
path: videoPath,
message: 'Video stored successfully'
message: validation.durationSeconds !== null
? `Video stored successfully (${validation.fileSizeBytes} bytes, ${validation.durationSeconds.toFixed(2)}s)`
: `Video stored successfully (${validation.fileSizeBytes} bytes)`
}
}