Drop an .mkv in, get a browser-playable .mp4 out — gr.Video is ffmpeg with a UI stapled to it.
Your handler never sees the .mkv the user uploaded — an ffmpeg subprocess already silently converted it to .mp4 before your first line ran.
gr.Video is a media player that doubles as a file input. Give it a filepath and it renders an HTML5 <video>; with sources=["upload", "webcam"] users drop files or record straight from the camera. Your function receives a plain str filepath, and whatever you return goes back through postprocess into a gr.FileData. Under the hood Gradio shells out to ffmpeg to keep clips browser-playable, convert to your chosen format, strip audio, and burn in subtitles and watermarks — so users can hand you .mkv, .avi or .mov and your player still actually plays them.
Video is where naive ML demos fall apart: browsers refuse half the codecs users throw at you, files are hundreds of MB, and webcam recording means MediaRecorder plumbing. gr.Video absorbs all of it. Because the conversion runs inside postprocess, the exact code path you demo locally is the one that runs on a Space — the same promise that made hundreds of video-processing Spaces work without anyone writing codec code. If your model consumes frames, audio tracks, or whole clips, this component is the front door.
import gradio as gr, os
def metadata(video): # video is a str filepath, e.g. '/tmp/clip.mkv'
if video is None:
return {"clips": 0}
return {
"clips": 1,
"orig_name": os.path.basename(video),
"size_mb": round(os.path.getsize(video) / 1e6, 2),
}
with gr.Blocks(title="Clip intake") as demo:
gr.Markdown("## Dataset clip QA — drop anything ffmpeg can read")
with gr.Row():
vid = gr.Video(label="clip", sources=["upload", "webcam"])
meta = gr.JSON(label="pipeline view")
vid.upload(metadata, vid, meta)
demo.launch(prevent_thread_lock=True)* Running on local URL: http://127.0.0.1:7901
* To create a public link, set `share=True` in `launch()`.
Renders: a 'Dataset clip QA' header; left = upload dropzone ('Drop video here - or - Click to Upload') with a webcam tab; right = the JSON panel labeled 'pipeline view'. Feeding a 27 KB test clip /tmp/kmailvid_intake.mkv through the live event (verified via gradio_client) returned {"clips": 1, "orig_name": "kmailvid_intake.mkv", "size_mb": 0.03} — the raw container, no conversion, original filename intact.
Run on gradio 6.18.0; the JSON is the real event payload. On construction with sources=['upload','webcam'], include_audio auto-defaults to True; sources=['webcam'] alone flips it to False (verified via get_config()).
import gradio as gr
def ingest(video): # the path your handler receives is ALREADY .mp4
return video
with gr.Blocks() as demo:
src = gr.Video(label="drop any container (.mkv, .avi, .mov)",
sources=["upload"], format="mp4")
dst = gr.Textbox(label="path passed to the handler")
src.upload(ingest, src, dst)
demo.launch(prevent_thread_lock=True)* Running on local URL: http://127.0.0.1:7902 * To create a public link, set `share=True` in `launch()`. Uploading the same .mkv through the live event returned '/home/kmail/.hermes/cache/scratch/gradio/40b0eec.../kmailvid_intake.mp4' — same stem, swapped extension, sitting in the Gradio cache; that exact str was handed to ingest(). UI: video player left, converted path echoed in the Textbox right.
format= applies at OUTPUT postprocess (and to webcam saves); at INPUT time preprocess re-encodes only when it must (verified: postprocess(gr.Video(format='mp4'), *.mkv) → FileData orig_name 'kmailvid_test2.mp4').
import gradio as gr
def brand(video): # plain path — subtitles/watermark live on the component
return video
with gr.Blocks() as demo:
vid = gr.Video(label="raw render")
branded = gr.Video(label="branded render",
subtitles="captions.srt",
watermark="logo.png", # or gr.WatermarkOptions(watermark=..., position='bottom-right')
format="mp4")
btn = gr.Button("Brand it")
btn.click(brand, vid, branded)
demo.launch(prevent_thread_lock=True)
# captions.srt: "1\n00:00:00,000 --> 00:00:02,000\nGradio burns me in with ffmpeg"* Running on local URL: http://127.0.0.1:7903 * To create a public link, set `share=True` in `launch()`. predict returned '/home/kmail/.hermes/cache/scratch/gradio/e72948.../kmailvid_clean_watermarked.mp4'. During postprocess ffmpeg ran an overlay filter chain (stderr captured during verification: 'Stream #1:0 (png) -> overlay') — the downloaded kmail.at logo composited onto every frame, output named 'kmailvid_clean_watermarked.mp4'. The output player shows video + burned-in watermark; the .srt was converted to a WebVTT .vtt the player overlays via <track>.
Gradio logs 'UserWarning: Video does not have browser-compatible container or codec. Converting to mp4.' when a re-encode is needed; watermark + format ride one ffmpeg pass.
| Flag | Meaning |
|---|---|
sources=["upload", "webcam"] | Which inputs to offer. In 6.x the default order is webcam first, upload second — flipped from the 3.x order in 4.14.0 (PR #6994). |
include_audio=False | Strip or keep the audio track. Auto-defaults: True when 'upload' is in sources, otherwise False — webcam-only demos record silent video unless you flip it back. |
format="mp4" | Extension to normalize to. Accepted silently even for nonsense like 'mp4x' (verified — it constructs without complaint and blows up later as a bad ffmpeg arg). Use a real container: mp4, webm, avi. |
subtitles=... | An .srt/.vtt/JSON path or URL, or a list of {"text", "timestamp": [start, end]} dicts — dict lists are written to a .vtt in the Gradio cache; other file extensions raise ValueError at construction. |
watermark=... | PNG/JPEG path or URL (position via gr.WatermarkOptions(..., position='bottom-right')) composited during postprocess through an ffmpeg overlay filter. |
autoplay + loop + streaming | Output-side playback: autoplay=True (browsers refuse it until the user has interacted with the page), loop replays from the end, streaming stitches yielded .mp4/.ts chunks into one live feed. |
webcam_options=gr.WebcamOptions(...) | mirror=True by default (flipped live preview, matching what the user sees), constraints={'width': 320, 'height': 180} to pin the capture resolution. |
The name first appears in gradio/__init__.py at commit 982ebe77 ('cleaned up imports', 2022-03-21), and 2.8.13 — uploaded to PyPI the same day — is the first release that provably carried it, three weeks before 2.9.0. It arrived as part of the late-2.x Blocks component rework, one month before Blocks itself shipped in 3.0.
Gradio started checking whether a video is playable in the browser and re-encoding to mp4 when it isn't (PR #2003). Then came include_audio (3.12.0), subtitles-as-input (3.26.0), webcam recording + autoplay (3.35.0), video watermarking (4.40.0), the subtitles= parameter (6.0.1), volume control (6.4.0).
preprocess maps the uploaded gr.FileData to a str path — no ffmpeg unless format= or webcam mirroring forces a re-encode (verified: with format='mp4', preprocess(.mkv) returned a sibling .mp4). postprocess is where conversion actually ships: _format_video checks the extension against your format=, asks ffmpeg whether the file is playable, re-encodes to mp4 when it isn't, runs the watermark overlay, and returns a FileData into the Gradio cache. Streaming inverts it: stream_output converts chunks with async_convert_mp4_to_ts and ships MediaStreamChunk payloads with computed durations. Every conversion is a subprocess — a long ffmpeg run burns a worker slot unless you pre-convert.