{"record":{"id":"35ce37b3741c26b8","repo":"invoke-ai/InvokeAI","slug":"video-must-use-a-browser-compatible-h-264-avc-code","errorCode":null,"errorMessage":"Video must use a browser-compatible H.264/AVC codec","messagePattern":"Video must use a browser-compatible H\\.264/AVC codec","errorType":"http","errorClass":"ValueError","httpStatus":415,"severity":"error","filePath":"invokeai/app/api/routers/videos.py","lineNumber":228,"sourceCode":"                    return len(major_brand) == 4 and major_brand != b\"qt  \"\n                position += box_size\n    except OSError:\n        return False\n    return False\n\n\ndef _probe_decodable_video(path: Path) -> tuple[tuple[int, int, float, Optional[float]], Optional[PILImage.Image]]:\n    \"\"\"Probes metadata and proves the video has a decodable first frame.\n\n    Returns the metadata plus the decoded frame so the save path can reuse it as the\n    thumbnail source instead of spawning another decode worker. A decode timeout is\n    contention on a loaded server, not evidence the video is bad — probe_video already\n    succeeded — so it yields (metadata, None) and the upload proceeds, with save-time\n    thumbnail extraction as the backstop.\n    \"\"\"\n    width, height, duration, fps, codec = probe_video_with_codec(path)\n    if codec is None or codec.lower() not in {\"h264\", \"avc\", \"avc1\", \"libx264\"}:\n        raise ValueError(\"Video must use a browser-compatible H.264/AVC codec\")\n    metadata = (width, height, duration, fps)\n    try:\n        first_frame = extract_video_frame(path, frame_index=0, raise_on_timeout=True)\n    except VideoDecodeTimeoutError:\n        return metadata, None\n    if first_frame is None:\n        raise ValueError(\"Video has no decodable frame\")\n    return metadata, first_frame\n\n\n@videos_router.post(\n    \"/upload\",\n    operation_id=\"upload_video\",\n    responses={\n        201: {\"description\": \"The video was uploaded successfully\"},\n        415: {\"description\": \"Video upload failed\"},\n    },\n    status_code=201,","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/invoke-ai/InvokeAI/blob/0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06/invokeai/app/api/routers/videos.py#L210-L246","documentation":"Raised by _probe_decodable_video in invokeai/app/api/routers/videos.py:227-228 after ffmpeg (probe_video_with_codec) reports the video's codec. InvokeAI only accepts videos whose codec is browser-compatible H.264/AVC (h264, avc, avc1, libx264); any other codec (HEVC/H.265, VP9, AV1, MPEG-4 Part 2) would be stored but fail to play in browsers, so the upload is rejected. The ValueError is caught by upload_video and surfaced as HTTP 415 'Failed to read video'.","triggerScenarios":"POST /v1/videos/upload with an MP4 whose video track is not H.264/AVC — e.g. HEVC (iPhone default 'High Efficiency'), VP9/AV1 webm-renamed-to-mp4, or MPEG-4 Part 2 (Xvid/DivX). Also fires when ffprobe returns no codec at all (corrupt video stream).","commonSituations":"Recording on iOS/Android with HEVC enabled, exporting from screen recorders that default to H.265, converting containers without re-encoding (remux of a .mov HEVC file to .mp4), or uploading a file with a damaged/no video stream so ffprobe cannot read a codec name.","solutions":["Re-encode the video to H.264: ffmpeg -i input.mp4 -c:v libx264 -pix_fmt yuv420p -c:a aac output.mp4","Disable HEVC/'High Efficiency' recording in the capture device (iPhone: Settings > Camera > Formats > Most Compatible) and re-record","Verify the codec before uploading with ffprobe -v error -select_streams v:0 -show_entries stream=codec_name input.mp4 and confirm it is h264/avc/avc1/libx264","Check ffprobe is installed and functioning on the server — a missing/broken ffmpeg makes probe_video_with_codec return codec=None, triggering the same error"],"exampleFix":"// before: remux only (keeps HEVC codec)\nffmpeg -i hevc.mov -c copy out.mp4\n// after: transcode to browser-compatible H.264\nffmpeg -i hevc.mov -c:v libx264 -pix_fmt yuv420p -c:a aac out.mp4","handlingStrategy":"validation","validationCode":"import subprocess, json\ndef codec_is_h264(path):\n    out = subprocess.run([\"ffprobe\", \"-v\", \"error\", \"-select_streams\", \"v:0\",\n                          \"-show_entries\", \"stream=codec_name\", \"-of\", \"json\", path],\n                         capture_output=True, text=True, check=True)\n    codec = (json.loads(out.stdout).get(\"streams\") or [{}])[0].get(\"codec_name\", \"\")\n    return codec.lower() in {\"h264\", \"avc\", \"avc1\", \"libx264\"}\n# upload only if codec_is_h264(\"video.mp4\")","typeGuard":"def is_h264_codec(codec: str | None) -> bool:\n    return codec is not None and codec.lower() in {\"h264\", \"avc\", \"avc1\", \"libx264\"}","tryCatchPattern":"try:\n    await client.upload_video(file)\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 415 and \"Failed to read video\" in e.response.text:\n        transcode_to_h264(path)  # ffmpeg -c:v libx264 -pix_fmt yuv420p\n        await client.upload_video(open(path, \"rb\"))\n    else:\n        raise","preventionTips":["Always encode to H.264 with yuv420p pixel format for browser delivery","Never remux HEVC .mov to .mp4 with -c copy; transcode instead","Pre-flight every file with ffprobe codec_name before upload","Set capture devices to 'Most Compatible' (H.264) rather than 'High Efficiency' (HEVC)"],"tags":["video","codec","ffmpeg","upload","h264"],"backgroundTag":"unsupported-video-codec","analyzedSha":"0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06","analyzedAt":"2026-08-29T04:46:49.967Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}