Quick answer: Re-encode your video as OGV with constant frame rate and embedded Vorbis audio using ffmpeg -c:v libtheora -r 30 -c:a libvorbis -q:a 6 output.ogv. Variable frame rate sources drift over time. Godot’s built-in player expects fixed-rate content.

You add an intro cutscene to your Godot game. The first ten seconds look perfect. By the thirty-second mark, the dialogue is a second behind the lip movements. By the end, the audio and video are clearly out of sync. Nothing in your code controls the timing — the VideoStreamPlayer is doing its best — but Godot’s built-in video decoder has strict requirements the file is not meeting.

The Symptom

Why Godot’s Video Player Is Strict

Godot ships with a simple Theora decoder for video and Vorbis for audio, packaged in an OGV container. The implementation is intentionally minimalist — it is not a full media player. It assumes the source file is well-behaved:

Most consumer video files violate at least one of these. Phone recordings use VFR to save battery. YouTube downloads are VBR for efficiency. Editing software leaves interleave gaps. Each violation causes the Godot player’s audio clock to drift away from the video clock.

The Fix: Re-Encode with ffmpeg

Convert the source to a strict OGV before importing:

# Basic conversion with fixed 30 fps and CBR audio
ffmpeg -i input.mp4 \
  -c:v libtheora -q:v 7 -r 30 \
  -c:a libvorbis -q:a 6 -ar 44100 \
  -vsync cfr \
  output.ogv

Key flags:

For a cinematic cutscene where quality matters more than file size:

ffmpeg -i input.mp4 \
  -c:v libtheora -b:v 4M -r 30 \
  -c:a libvorbis -b:a 192k -ar 44100 \
  -vsync cfr \
  output.ogv

4 Mbps video and 192 kbps audio give near-lossless quality for most cutscene content.

Verify the File

After encoding, check the file has both streams with proper parameters:

ffprobe output.ogv
# Look for:
#   Stream #0:0: Video: theora, ..., 30 fps, 30 tbr, ...
#   Stream #0:1: Audio: vorbis, 44100 Hz, stereo, ...

If fps and tbr are equal, the file is CFR. If they differ, you still have VFR and Godot will desync.

Import Into Godot

Drop the OGV into your project. Godot imports it automatically. Add a VideoStreamPlayer node, set its stream to the imported file, and play:

extends VideoStreamPlayer

func _ready():
    stream = load("res://cutscenes/intro.ogv")
    play()

    # Wait for finish
    await finished
    get_tree().change_scene_to_file("res://scenes/main_menu.tscn")

Web Export Caveats

Web exports run the decoder in WebAssembly, which is slower than native and has stricter timing. If sync holds on native but drifts on web, lower the video frame rate to 24 fps and bitrate to 2 Mbps. The browser decoder handles the lighter load more reliably.

When You Need MP4 or WebM

Godot’s built-in player does not support MP4, WebM, AV1, or HEVC. If your pipeline requires one of these formats, use a third-party plugin like Godot Video Decoder or embed a platform-native player through GDExtension. The tradeoff is setup complexity versus format flexibility — for most games, sticking with OGV is the path of least resistance.

Verifying the Fix

Add a debug overlay that shows the current video time and audio time simultaneously. With the bug, the two numbers diverge as the video plays. With the fix, they remain within a frame of each other for the full duration.

“Godot’s video player is intentionally simple. Give it a strictly-encoded file and it does the right thing. Give it anything else and audio drifts. Pick your battles and re-encode.”

Related Issues

For audio playback issues unrelated to video, see Godot AudioStreamPlayer not playing sound. For web export audio crackling, see Godot audio crackling in web export. For audio bus issues, see Godot audio bus effects not applying.

Always use the -vsync cfr flag with ffmpeg when encoding for Godot. It forces a constant frame rate even if the source is variable, which is the single most effective fix for desync.