Video
robot.media plays video on the same two lanes used for audio — foreground (FG) and background (BG) — from files or as a raw streamed RGBA signal. These examples assume you already have a connected robot — see Connection if you haven't set one up yet.
How the FG/BG video system works
Video uses the same two-lane idea as Audio, but the lanes aren't symmetric, and there's alpha (transparency) control instead of volume:
- BG is the opaque base layer — a plain image, no transparency.
- FG sits on top of BG and is alpha-blended over it.
robot.media.set_fg_video_alpha()controls how much of the FG layer shows through; there's no separate BG alpha, since BG is always the fully-opaque bottom layer.
Just like audio, each lane accepts a file and a stream input, and file playback takes priority — while a file is playing on a lane, frames arriving on that lane's stream are dropped, and the lane reverts to the stream once the file finishes.
An important difference from audio: there's no video equivalent of TTS using FG — it's the opposite. QTrobot's own face system uses the BG lane: emotions (robot.face.show_emotion(...)) play as a BG file, and a continuous live face rendering is pushed to the BG stream whenever no emotion file is playing. The FG lane is left free for you — anything you play or stream on FG is alpha-blended on top of whatever the face is currently showing on BG.
Creative possibilities
Because FG always overlays whatever's on BG, you can layer your own content on top of the robot's normal face/emotions without replacing them:
- Reactive overlays — show a small icon, sticker, or shape on FG (like the pulsing heart below) that fades in and out over an emotion or the idle face, using
set_fg_video_alpha()for the fade. - Captions or visual cues — overlay text or a simple graphic on FG while an emotion plays on BG underneath, for accessibility or for a teaching/story context.
- Speech-synced effects — combine FG video (a custom overlay) with FG audio (TTS) for a richer "speaking" effect, while BG continues showing the robot's current emotion or idle face.
- Smooth transitions — fade an FG overlay in just as a BG emotion finishes, to bridge between two visual states instead of an abrupt cut.
Setup
mkdir ~/example
cd ~/example
python -m venv .venv
# or: uv venv .venv
source .venv/bin/activate
pip install luxai-robot
# or: uv pip install luxai-robot
Connect
from luxai.robot.core import Robot
robot = Robot.connect_zmq(robot_id="QTRD000123")
print(f"connected to {robot.robot_id} ({robot.robot_type})")
See Connection for MQTT, WebRTC, and other connection options.
Foreground alpha (transparency)
The foreground video lane sits on top of the background lane — its alpha controls how transparent it is.
import time
# Fully opaque
robot.media.set_fg_video_alpha(1.0)
# Half-transparent
robot.media.set_fg_video_alpha(0.5)
time.sleep(2)
# Restore to fully opaque
robot.media.set_fg_video_alpha(1.0)
Play a background video file
A file path must exist on the robot itself (not on your laptop) for these examples to work.
import time
video_file_on_robot = "/home/qtrobot/robot/data/emotions/QT/kiss.avi"
# Play and wait for it to finish
ret = robot.media.play_bg_video_file(video_file_on_robot)
Logger.info(f"Done. Result: {ret}")
# Play non-blocking, then cancel it after 2 seconds
h = robot.media.play_bg_video_file_async(video_file_on_robot)
time.sleep(2)
h.cancel()
Pause and resume
import time
video_file_on_robot = "/home/qtrobot/robot/data/emotions/QT/kiss.avi"
play_handler = robot.media.play_bg_video_file_async(video_file_on_robot)
time.sleep(2)
robot.media.pause_bg_video_file()
Logger.info("Paused. Waiting 3 seconds...")
time.sleep(3)
robot.media.resume_bg_video_file()
play_handler.wait() # wait for playback to finish
Stream raw video frames
This example streams a pulsing heart shape to the foreground lane, fading its alpha in time with a heartbeat.
import time
import math
import numpy as np
from luxai.magpie.utils.common import get_uinque_id
from luxai.magpie.frames import ImageFrameRaw
def _make_heart_frame(width, height, scale=1.5):
image = np.zeros((height, width, 4), dtype=np.uint8) # transparent RGBA
y, x = np.ogrid[:height, :width]
x = (x - width / 2) / (width / 2)
y = -((y - height / 2) / (height / 2)) # flip vertically
x *= scale
y *= scale
heart_mask = (x**2 + y**2 - 1)**3 - x**2 * y**3 <= 0
image[heart_mask] = (255, 0, 0, 255) # red, full alpha
return ImageFrameRaw(
data=image.tobytes(), format="raw",
width=width, height=height, channels=4, pixel_format="RGBA",
)
stream_id = get_uinque_id()
fps = 30
duration_s = 5.0
total_frames = int(fps * duration_s)
frame_interval = 1.0 / fps
heart_frame = _make_heart_frame(width=400, height=280, scale=5.0)
heart_frame.gid = stream_id
bpm = 72.0
freq = bpm / 60.0 # beats per second
robot.media.set_fg_video_alpha(0.0)
writer = robot.media.stream.open_fg_video_stream_writer()
t0 = time.perf_counter()
for frame_id in range(1, total_frames + 1):
heart_frame.id = frame_id
writer.write(heart_frame)
t = time.perf_counter() - t0
alpha = 0.5 * (1 + math.sin(2 * math.pi * freq * t))
alpha = alpha ** 2 # makes it feel more like a "beat"
robot.media.set_fg_video_alpha(alpha)
time.sleep(frame_interval)
robot.media.set_fg_video_alpha(1.0)
robot.media.cancel_fg_video_stream()
Next steps
Continue with the Microphone tutorial, or see the full robot.media namespace in the Python API Reference.