{"record":{"id":"0f826e6971eea4f8","repo":"heygen-com/hyperframes","slug":"ffmpeg-returned-no-analyzable-video-frames","errorCode":null,"errorMessage":"FFmpeg returned no analyzable video frames","messagePattern":"FFmpeg returned no analyzable video frames","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core/src/mediaGradeAnalyzer.ts","lineNumber":185,"sourceCode":"    if (!stat?.[1] || !stat[2]) continue;\n    current ??= {};\n    current[stat[1]] = Number(stat[2]);\n  }\n  if (current) frames.push(current);\n  return frames.filter(\n    (frame) =>\n      Number.isFinite(frame.YMIN) &&\n      Number.isFinite(frame.YLOW) &&\n      Number.isFinite(frame.YAVG) &&\n      Number.isFinite(frame.YHIGH) &&\n      Number.isFinite(frame.YMAX) &&\n      Number.isFinite(frame.UAVG) &&\n      Number.isFinite(frame.VAVG),\n  );\n}\n\nfunction summarizeFrames(frames: readonly GradeSignalFrame[]): NumericStats {\n  if (frames.length === 0) throw new Error(\"FFmpeg returned no analyzable video frames\");\n  const values = (key: string) => frames.map((frame) => Number(frame[key]));\n  return {\n    frames: frames.length,\n    yMin: Math.min(...values(\"YMIN\")),\n    yLow: average(values(\"YLOW\")),\n    yAvg: average(values(\"YAVG\")),\n    yHigh: average(values(\"YHIGH\")),\n    yMax: Math.max(...values(\"YMAX\")),\n    uAvg: average(values(\"UAVG\")),\n    vAvg: average(values(\"VAVG\")),\n    satAvg: average(frames.map((frame) => frame.SATAVG ?? 0)),\n    shadowClipRisk: average(frames.map((frame) => (Number(frame.YLOW) <= 16 ? 1 : 0))),\n    highlightClipRisk: average(frames.map((frame) => (Number(frame.YHIGH) >= 235 ? 1 : 0))),\n  };\n}\n\nfunction suggestedExposure(normalizedAverage: number, yLow: number, yHigh: number): number {\n  if (normalizedAverage < 0.28 && yHigh / 255 < 0.65) {","sourceCodeStart":167,"sourceCodeEnd":203,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/core/src/mediaGradeAnalyzer.ts#L167-L203","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["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.","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.","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.","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 })`.","Raise the cap with `HYPERFRAMES_ANALYZE_TIMEOUT_MS` if the run is timing out and producing partial output."],"exampleFix":"// before\nconst analysis = analyzeMediaGrade(\"assets/clip.m4a\"); // audio-only -> no frames\n\n// after\nimport { execFileSync } from \"node:child_process\";\nconst hasVideo = (() => {\n  try {\n    const out = execFileSync(\"ffprobe\", [\"-v\",\"error\",\"-select_streams\",\"v:0\",\"-show_entries\",\"stream=codec_name\",\"-of\",\"json\",\"--\",\"assets/clip.m4a\"], { encoding: \"utf8\" });\n    return JSON.parse(out).streams?.length > 0;\n  } catch { return false; }\n})();\nif (!hasVideo) throw new Error(\"refusing to grade a file with no video stream\");\nconst analysis = analyzeMediaGrade(\"assets/clip.m4a\");","handlingStrategy":"validation","validationCode":"import { execFileSync } from \"node:child_process\";\n\nfunction hasDecodableVideoStream(path: string, ffprobe = \"ffprobe\"): boolean {\n  try {\n    const out = execFileSync(\n      ffprobe,\n      [\"-v\",\"error\",\"-select_streams\",\"v:0\",\"-show_entries\",\"stream=codec_name,nb_frames\",\"-of\",\"json\",\"--\",path],\n      { encoding: \"utf8\", timeout: 5000 },\n    );\n    const streams = JSON.parse(out).streams ?? [];\n    return streams.length > 0;\n  } catch {\n    return false;\n  }\n}\n\nif (!hasDecodableVideoStream(mediaPath)) {\n  throw new Error(`cannot grade ${mediaPath}: no decodable video stream`);\n}","typeGuard":"import { execFileSync } from \"node:child_process\";\n\nfunction isGradableMedia(path: string): boolean {\n  try {\n    execFileSync(\"ffprobe\", [\"-v\",\"error\",\"-select_streams\",\"v:0\",\"-count_frames\",\"-show_entries\",\"stream=nb_read_frames\",\"-of\",\"json\",\"--\",path], { encoding: \"utf8\", timeout: 5000 });\n    return true;\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  return analyzeMediaGrade(mediaPath, { ffmpegPath });\n} catch (e) {\n  if (/no analyzable video frames/.test(String(e))) {\n    logger.warn(`grade analysis yielded no frames for ${mediaPath}; falling back to default grade`);\n    return defaultAnalysis();\n  }\n  throw e;\n}","preventionTips":["Only pass files confirmed by ffprobe to have a decodable video stream.","Pin a full-featured ffmpeg build across local/CI (same version, same codecs).","Treat grade analysis as advisory: catch and degrade gracefully rather than failing the render."],"tags":["ffmpeg","media-analysis","signalstats","video-decode"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}