Skip to the content.

Quickstart

This guide will help you get started with the Godot-LiveKit plugin by connecting to a LiveKit room.

Connecting to a Room

You can access LiveKit classes directly from GDScript once the plugin is installed and enabled.

The core class you will interact with is LiveKitRoom.

extends Node

var room: LiveKitRoom

func _ready():
    # 1. Instantiate the room
    room = LiveKitRoom.new()

    # 2. Connect to signals to handle room events
    room.connected.connect(_on_room_connected)
    room.disconnected.connect(_on_room_disconnected)
    room.connection_failed.connect(_on_connection_failed)
    room.participant_connected.connect(_on_participant_connected)

    # 3. Connect to the LiveKit server
    var url = "wss://your-livekit-server.url"
    var token = "your-access-token"

    room.connect_to_room(url, token, {})

# No _process() needed — auto_poll is true by default, so poll_events()
# is called automatically every frame by the built-in poller.

func _on_room_connected():
    print("Successfully connected to the room!")
    print("Local participant: ", room.get_local_participant().get_identity())

func _on_room_disconnected():
    print("Disconnected from the room.")

func _on_connection_failed(error):
    print("Connection failed: ", error)

func _on_participant_connected(participant):
    print("Participant joined: ", participant.get_identity())

Receiving Video

To display a remote participant’s video, use LiveKitVideoStream:

extends Node

var room: LiveKitRoom
var video_stream: LiveKitVideoStream

@onready var texture_rect: TextureRect = $TextureRect

func _ready():
    room = LiveKitRoom.new()
    room.track_subscribed.connect(_on_track_subscribed)
    room.connect_to_room("wss://your-livekit-server.url", "your-access-token", {})

func _on_track_subscribed(track, publication, participant):
    if track.get_kind() == LiveKitTrack.KIND_VIDEO:
        video_stream = LiveKitVideoStream.from_track(track)
        texture_rect.texture = video_stream.get_texture()

# No _process() needed — video streams are auto-polled by default.

Publishing Audio

To publish audio from Godot to the room:

var audio_source: LiveKitAudioSource
var audio_track: LiveKitLocalAudioTrack

func publish_microphone():
    audio_source = LiveKitAudioSource.create(48000, 1, 200)
    audio_track = LiveKitLocalAudioTrack.create("microphone", audio_source)
    room.get_local_participant().publish_track(audio_track, {})

Sending Data

To send arbitrary data messages:

func send_message(text: String):
    var data = text.to_utf8_buffer()
    room.get_local_participant().publish_data(data, true, [], "chat")

Remote Procedure Calls (RPC)

To call methods on other participants and handle incoming calls:

func setup_rpc():
    # Register a method to receive RPC calls
    room.get_local_participant().register_rpc_method("greet")
    room.get_local_participant().rpc_method_invoked.connect(_on_rpc_invoked)

    # Connect signals for outgoing RPC results
    room.get_local_participant().rpc_response_received.connect(_on_rpc_response)
    room.get_local_participant().rpc_error.connect(_on_rpc_error)

func _on_rpc_invoked(method, request_id, caller_identity, payload, response_timeout):
    print("RPC from ", caller_identity, ": ", method, " -> ", payload)

func call_remote_greet(target_identity: String):
    # perform_rpc is async — the result arrives via rpc_response_received signal
    room.get_local_participant().perform_rpc(target_identity, "greet", "Hello!", 10.0)

func _on_rpc_response(method, result):
    print("RPC response for ", method, ": ", result)

func _on_rpc_error(method, error_message):
    print("RPC error for ", method, ": ", error_message)

Screen Capture

To capture a screen or window and publish it as a video track:

var screen_capture: LiveKitScreenCapture
var video_source: LiveKitVideoSource
var video_track: LiveKitLocalVideoTrack

func start_screen_share():
    # Check permissions first (important on macOS)
    var perms = LiveKitScreenCapture.check_permissions()
    if perms["status"] == LiveKitScreenCapture.PERMISSION_ERROR:
        print("Screen capture not permitted: ", perms["summary"])
        return

    # Create capture for the primary monitor
    screen_capture = LiveKitScreenCapture.create()
    screen_capture.start()

    # Create a video source and track to publish the captured frames
    video_source = LiveKitVideoSource.create(1920, 1080)
    video_track = LiveKitLocalVideoTrack.create("screen", video_source)
    room.get_local_participant().publish_track(video_track, {})

func _process(_delta):
    if screen_capture and screen_capture.poll():
        var image = screen_capture.get_image()
        if image:
            video_source.capture_frame(image, Time.get_ticks_usec(), 0)

Avoid using frame_received for per-frame work. The frame_received signal fires on every captured frame, which can be 30-60+ fps. Connecting heavy operations like image processing or capture_frame() to this signal adds overhead from signal dispatch and blocks the main thread at capture rate. Instead, poll manually in _process() as shown above — this gives you full control over when frames are consumed and avoids unnecessary signal overhead. Note: set screen_capture.auto_poll = false when calling poll() yourself to avoid double-polling.

You can also capture a specific window:

func share_specific_window():
    var windows = LiveKitScreenCapture.get_windows()
    for w in windows:
        if "Firefox" in w["name"]:
            screen_capture = LiveKitScreenCapture.create_for_window(w)
            screen_capture.start()
            break

Or take a one-shot screenshot without starting continuous capture:

func take_screenshot():
    var capture = LiveKitScreenCapture.create()
    var image = capture.screenshot()
    if image:
        image.save_png("res://screenshot.png")
    capture.close()

End-to-End Encryption (E2EE)

To enable E2EE when connecting to a room:

func connect_with_e2ee():
    var e2ee_options = LiveKitE2eeOptions.new()
    e2ee_options.set_encryption_type(LiveKitE2eeOptions.ENCRYPTION_GCM)
    e2ee_options.set_shared_key("my-secret-key".to_utf8_buffer())

    room = LiveKitRoom.new()
    room.e2ee_state_changed.connect(_on_e2ee_state_changed)
    room.connect_to_room("wss://your-server.url", "your-token", {"e2ee": e2ee_options})

func _on_e2ee_state_changed(participant, state):
    print("E2EE state changed for ", participant.get_identity(), ": ", state)

func rotate_encryption_key():
    # Ratchet (rotate) the shared key
    var manager = room.get_e2ee_manager()
    var new_key = manager.get_key_provider().ratchet_shared_key()
    print("Key rotated")

Track Statistics

To monitor WebRTC connection quality:

func _on_track_subscribed(track, publication, participant):
    # Connect the signal to receive stats asynchronously
    track.stats_received.connect(_on_stats_received)
    # Request stats (non-blocking)
    track.request_stats()

func _on_stats_received(stats):
    for stat in stats:
        print(stat)

Connection Options

The connect_to_room() method accepts an options dictionary with the following keys:

Key Type Default Description
auto_subscribe bool true Automatically subscribe to tracks published by remote participants.
dynacast bool false Enable dynacast for adaptive simulcast.
auto_reconnect bool true Allow the SDK to automatically reconnect on network disruption. When false, a disconnected signal is emitted instead, letting your application handle reconnection.
connect_timeout float 15.0 Maximum time in seconds to wait for the connection to succeed. If exceeded, a connection_failed signal is emitted with a timeout error. Set to 0 to disable.
e2ee LiveKitE2eeOptions End-to-end encryption options.
# Example: disable auto-reconnect so you can handle reconnection yourself
room.connect_to_room(url, token, {"auto_reconnect": false, "dynacast": true})

Auto-Polling and poll_events()

Godot-LiveKit uses a background-thread event queue pattern. The LiveKit C++ SDK fires callbacks on its own internal threads, but Godot is not thread-safe — calling any Godot API (signals, Dictionary access, Ref creation) from a non-main thread will crash the engine.

To bridge this gap, all SDK callbacks capture lightweight C++ data and push a lambda onto a thread-safe queue. Every frame, poll_events() / poll() drains the queue and emits signals safely on the main thread.

Auto-polling (default): All LiveKitRoom, LiveKitVideoStream, and LiveKitScreenCapture objects have auto_poll = true by default. A built-in frame callback automatically calls their poll methods every frame — no _process() code required.

Manual polling: Set auto_poll = false on any object to take control of when polling happens. This is useful if you want to poll at a different rate or in a specific order.

Audio streams are the exception — LiveKitAudioStream.poll() requires an AudioStreamGeneratorPlayback argument, so it cannot be auto-polled. You must call it manually:

func _process(_delta):
    if audio_stream and playback:
        audio_stream.poll(playback)

Key points:

Troubleshooting

No signals are being emitted

By default, auto_poll is true and signals are delivered automatically each frame. If you set auto_poll = false, make sure you are calling room.poll_events() in _process() — signals are queued internally and only delivered when poll_events() is called.

Connection hangs or times out

macOS screen capture permissions

On macOS, screen capture requires explicit user permission. Call LiveKitScreenCapture.check_permissions() before creating a capture to check the current status. If the status is PERMISSION_ERROR, the user needs to grant Screen Recording permission in System Settings > Privacy & Security > Screen Recording.

Video frames appear to drop

If video_stream.poll() frequently returns false even when frames should be arriving, it may be due to lock contention between the reader thread and the main thread. A warning will be logged to the Godot console if this happens repeatedly. This is usually harmless at low rates but may indicate the main thread _process() is too slow.

Audio playback is choppy

Next Steps

Explore the full API Reference for details on all available classes and methods.