heygen-com/hyperframes · error · Error

FFmpeg returned no analyzable video frames

Error message

FFmpeg returned no analyzable video frames

What it means

Thrown by summarizeFrames() when the array of ffmpeg signalstats frames is empty after parsing. analyzeMediaGrade() runs ffmpeg with the `signalstats`+`metadata=print` filter chain and parses its stdout into GradeSignalFrame entries; frames missing any of YMIN/YLOW/YAVG/YHIGH/YMAX/UAVG/VAVG are filtered out, so this fires when zero usable luma/chroma samples survived. It means the grade-analysis pipeline could not extract any measurable video data from the input.

Source

Thrown at packages/core/src/mediaGradeAnalyzer.ts:185

    if (!stat?.[1] || !stat[2]) continue;
    current ??= {};
    current[stat[1]] = Number(stat[2]);
  }
  if (current) frames.push(current);
  return frames.filter(
    (frame) =>
      Number.isFinite(frame.YMIN) &&
      Number.isFinite(frame.YLOW) &&
      Number.isFinite(frame.YAVG) &&
      Number.isFinite(frame.YHIGH) &&
      Number.isFinite(frame.YMAX) &&
      Number.isFinite(frame.UAVG) &&
      Number.isFinite(frame.VAVG),
  );
}

function summarizeFrames(frames: readonly GradeSignalFrame[]): NumericStats {
  if (frames.length === 0) throw new Error("FFmpeg returned no analyzable video frames");
  const values = (key: string) => frames.map((frame) => Number(frame[key]));
  return {
    frames: frames.length,
    yMin: Math.min(...values("YMIN")),
    yLow: average(values("YLOW")),
    yAvg: average(values("YAVG")),
    yHigh: average(values("YHIGH")),
    yMax: Math.max(...values("YMAX")),
    uAvg: average(values("UAVG")),
    vAvg: average(values("VAVG")),
    satAvg: average(frames.map((frame) => frame.SATAVG ?? 0)),
    shadowClipRisk: average(frames.map((frame) => (Number(frame.YLOW) <= 16 ? 1 : 0))),
    highlightClipRisk: average(frames.map((frame) => (Number(frame.YHIGH) >= 235 ? 1 : 0))),
  };
}

function suggestedExposure(normalizedAverage: number, yLow: number, yHigh: number): number {
  if (normalizedAverage < 0.28 && yHigh / 255 < 0.65) {

View on GitHub (pinned to c2996c8626)

Solutions

  1. Run `ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,nb_frames -of json -- <path>` to confirm the file has a decodable video stream with frames.
  2. Reproduce the exact ffmpeg invocation manually (`ffmpeg -hide_banner -nostdin -v error -i <path> -vf fps=...,format=yuv444p,signalstats,metadata=print:file=- -frames:v 5 -f null -`) and check whether any `lavfi.signalstats.YMIN=` lines print.
  3. If ffmpeg prints nothing, try a different source file or re-encode with `ffmpeg -i in -c:v libx264 -pix_fmt yuv420p out.mp4` and re-run.
  4. If the host ffmpeg is a stripped/old build, install a full build (e.g. `brew install ffmpeg` or a static johnvansickle build) and pass it via `analyzeMediaGrade(path, { ffmpegPath })`.
  5. Raise the cap with `HYPERFRAMES_ANALYZE_TIMEOUT_MS` if the run is timing out and producing partial output.

Example fix

// before
const analysis = analyzeMediaGrade("assets/clip.m4a"); // audio-only -> no frames

// after
import { execFileSync } from "node:child_process";
const hasVideo = (() => {
  try {
    const out = execFileSync("ffprobe", ["-v","error","-select_streams","v:0","-show_entries","stream=codec_name","-of","json","--","assets/clip.m4a"], { encoding: "utf8" });
    return JSON.parse(out).streams?.length > 0;
  } catch { return false; }
})();
if (!hasVideo) throw new Error("refusing to grade a file with no video stream");
const analysis = analyzeMediaGrade("assets/clip.m4a");
Defensive patterns

Strategy: validation

Validate before calling

import { execFileSync } from "node:child_process";

function hasDecodableVideoStream(path: string, ffprobe = "ffprobe"): boolean {
  try {
    const out = execFileSync(
      ffprobe,
      ["-v","error","-select_streams","v:0","-show_entries","stream=codec_name,nb_frames","-of","json","--",path],
      { encoding: "utf8", timeout: 5000 },
    );
    const streams = JSON.parse(out).streams ?? [];
    return streams.length > 0;
  } catch {
    return false;
  }
}

if (!hasDecodableVideoStream(mediaPath)) {
  throw new Error(`cannot grade ${mediaPath}: no decodable video stream`);
}

Type guard

import { execFileSync } from "node:child_process";

function isGradableMedia(path: string): boolean {
  try {
    execFileSync("ffprobe", ["-v","error","-select_streams","v:0","-count_frames","-show_entries","stream=nb_read_frames","-of","json","--",path], { encoding: "utf8", timeout: 5000 });
    return true;
  } catch { return false; }
}

Try / catch

try {
  return analyzeMediaGrade(mediaPath, { ffmpegPath });
} catch (e) {
  if (/no analyzable video frames/.test(String(e))) {
    logger.warn(`grade analysis yielded no frames for ${mediaPath}; falling back to default grade`);
    return defaultAnalysis();
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling analyzeMediaGrade(path) on a media file whose video stream yields no signalstats output: an audio-only container, a 0-frame or truncated video, a codec ffmpeg cannot decode with the linked build, or an ffmpeg version whose `metadata=print:file=-` formatting diverges from the parser regex. Also fires if ffmpeg exits 0 but writes nothing to stdout (e.g. `format=yuv444p` rejected for an exotic pix_fmt).

Common situations: Pointing the analyzer at a file path that is actually audio (podcast .m4a), a corrupted/empty .mp4 from a failed render, a .mov with a codec the static ffmpeg lacks (e.g. HEVC without build support), or after a host ffmpeg upgrade changed signalstats line formatting. Wrapped by error 341 so the user usually sees the `grade analysis failed for ...` message with this as the cause.

Related errors


AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12). Data as JSON: /api/errors/0f826e6971eea4f8. Report an issue: GitHub.