From 8b429da830f9eec06f49e2052fe2d6a840d9ba1f Mon Sep 17 00:00:00 2001 From: TMHSDigital Date: Tue, 16 Jun 2026 18:56:50 -0400 Subject: [PATCH] fix: sample_frames (timestamps) returns only frames that exist (#35) A timestamp past the end of the video makes ffmpeg exit 0 without writing a file, but the tool still reported that nonexistent path. It now checks the file exists before reporting it and lists any out-of-range timestamps under skippedTimestamps with a note. A stale file at the target path (reused outputDir) is cleared first so existence reflects only this run. Mirrors the fps-mode fix in #20. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 11 +++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/tools/sampleFrames.ts | 19 +++++++++++++++++-- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb5048a..33849bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,17 @@ project adheres to [Semantic Versioning](https://semver.org/). - ESLint (flat config, typescript-eslint recommended) with a `lint` script, run in CI (#23). Tooling only; not part of the published package. +## [0.8.11] + +### Fixed + +- **`sample_frames` (timestamps) no longer returns paths for frames past the end + of the video** (#35). A timestamp beyond the video length makes ffmpeg exit 0 + without writing a file, but the tool still reported that (nonexistent) path. + It now returns only frames that actually exist on disk and lists any + out-of-range timestamps under `skippedTimestamps` with a note. Mirrors the + fps-mode fix in #20. + ## [0.8.10] ### Fixed diff --git a/package-lock.json b/package-lock.json index 9c3c491..e6df5d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tmhs/screencast-mcp", - "version": "0.8.10", + "version": "0.8.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tmhs/screencast-mcp", - "version": "0.8.10", + "version": "0.8.11", "license": "CC-BY-NC-ND-4.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", diff --git a/package.json b/package.json index 5746f5a..84e358b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tmhs/screencast-mcp", - "version": "0.8.10", + "version": "0.8.11", "description": "MCP server for Windows screen recording, frame sampling, and minimal ffmpeg edits", "type": "module", "main": "dist/index.js", diff --git a/src/tools/sampleFrames.ts b/src/tools/sampleFrames.ts index 9b4b67e..ead3858 100644 --- a/src/tools/sampleFrames.ts +++ b/src/tools/sampleFrames.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { join } from "node:path"; -import { mkdirSync, readdirSync, existsSync } from "node:fs"; +import { mkdirSync, readdirSync, existsSync, rmSync } from "node:fs"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { errorResponse, okResponse, ScreencastError } from "../utils/errors.js"; import { requireFfmpeg, runFfmpeg } from "../utils/ffmpeg.js"; @@ -52,6 +52,7 @@ export function register(server: McpServer): void { mkdirSync(dir, { recursive: true }); let frames: string[] = []; + const skipped: number[] = []; if (hasFps) { const isPng = (f: string) => f.endsWith(".png"); // Snapshot any pre-existing PNGs so a reused outputDir does not @@ -68,8 +69,14 @@ export function register(server: McpServer): void { const ts = args.timestamps!; for (let i = 0; i < ts.length; i++) { const out = join(dir, `frame_${String(i).padStart(3, "0")}_${ts[i]}s.png`); + // Clear any stale file at this path (reused outputDir) so existence + // after the run reflects only what this invocation wrote. + rmSync(out, { force: true }); await runFfmpeg(buildSampleAtTimestampArgs(args.input, ts[i], out), 60_000); - frames.push(out); + // A timestamp past the end of the video makes ffmpeg exit 0 without + // writing a file. Report only frames that actually exist (#35). + if (existsSync(out)) frames.push(out); + else skipped.push(ts[i]); } } @@ -78,6 +85,14 @@ export function register(server: McpServer): void { frameCount: frames.length, frames, mode: hasFps ? `fps=${args.fps}` : `timestamps=[${args.timestamps!.join(", ")}]`, + ...(skipped.length > 0 + ? { + skippedTimestamps: skipped, + note: + `No frame was written for ${skipped.length} timestamp(s) past the ` + + `end of the video: [${skipped.join(", ")}].`, + } + : {}), }); } catch (error) { return errorResponse(error);