-
Notifications
You must be signed in to change notification settings - Fork 0
API Video
PicOS supports MJPEG video playback from AVI files stored on the SD card. Video is decoded on Core 0 using JPEGDEC and displayed via the standard display framebuffer. Audio tracks (MP3) are decoded on Core 1 via a shared ring buffer.
Add "video" (no explicit requirement needed) or "audio" to your app.json if your video has an audio track:
{
"requirements": ["audio"]
}local player = picocalc.video.player()Creates a new video player instance.
-
Returns: (userdata) Video player object, or
nil, errstron failure
player:load("/apps/myvideo/clip.avi") -- path on SD card
player:play()
player:stop()
-- no destroy(): the player is freed automatically by Lua garbage collectionplayer:setLoop(true) -- loop continuously (default: false)
player:seek(frame_number) -- jump to a specific frame indexPauses video playback. Audio is also paused.
Resumes video playback after a pause.
Returns the actual playback frame rate.
- Returns: (number) Frames per second
local fps = player:getFPS()
picocalc.sys.log("FPS: " .. string.format("%.1f", fps))Returns the video frame dimensions.
- Returns: (number, number) Width and height in pixels
local w, h = player:getSize()Returns the number of frames dropped during playback due to decode not keeping up with the frame rate.
- Returns: (number) Number of dropped frames
Resets playback statistics (dropped frame counter).
Controls whether the player automatically calls display.flush() after each decoded frame. When disabled, the app must call display.flush() manually, allowing HUD overlays to be drawn between decode and flush.
-
Parameters:
-
enabled(boolean):truefor automatic flush (default),falsefor manual flush
-
player:setAutoFlush(false) -- manual flush for HUD overlayplayer:hasAudio() -- true if the AVI file contains an MP3 audio track
player:setMuted(true) -- mute audio (video still plays)
player:isMuted() -- returns current mute state
player:setVolume(vol) -- 0–255
player:getVolume() -- returns current volumeplayer:isPlaying() -- true while playing
player:isPaused() -- true while paused
local info = player:getInfo()
-- info.width, info.height — frame dimensions
-- info.frames — total frames in file
-- info.current_frame — current playback position
-- info.dropped_frames — frames dropped during playback
-- info.has_audio — whether an audio track was foundCall player:update() each frame to advance playback:
local player = picocalc.video.player()
player:load("/apps/demo/video.avi")
player:play()
while true do
player:update()
-- draw HUD or overlays here
picocalc.display.flush()
picocalc.input.update()
local pressed = picocalc.input.getButtonsPressed()
if pressed & picocalc.input.BTN_ESC ~= 0 then break end
end
player:stop()PicOS plays MJPEG AVI files only. Each video frame is an independent JPEG; the player does not support inter-frame codecs (H.264, VP9, etc.).
| Property | Value |
|---|---|
| Container | AVI (RIFF) |
| Video codec | MJPEG (Motion JPEG) |
| Audio codec | MP3 (MPEG-1 Audio Layer III) |
| Resolution | Up to 320 × 320 (native display size) |
| Frame rate | 25 fps recommended |
| Audio sample rate | 22050 Hz or 44100 Hz |
| Audio channels | Mono or stereo |
The tools/video_converter.sh script converts any video file (or YouTube URL) to the correct AVI format using ffmpeg.
# Install ffmpeg (required)
sudo apt install ffmpeg # Debian/Ubuntu
brew install ffmpeg # macOS
sudo dnf install ffmpeg # Fedora
# Install yt-dlp (only needed for URL downloads)
pip install yt-dlptools/video_converter.sh [options] <file_or_url>Output is written to ~/Videos/PicOS/ (the directory must exist).
| Flag | Description | Default |
|---|---|---|
--crop |
Scale to 320×320, cropping to fill (square crop) | off (letterbox) |
--quality N |
MJPEG quality, 1 (best) – 31 (worst) | 8 |
--mute |
Strip the audio track entirely | off (include audio) |
# Basic conversion — letterbox to 320×height, include audio
tools/video_converter.sh myclip.mp4
# Square crop for portrait/square content
tools/video_converter.sh --crop myclip.mp4
# High quality, muted (video-only)
tools/video_converter.sh --quality 3 --mute myclip.mp4
# Download and convert directly from YouTube
tools/video_converter.sh "https://www.youtube.com/watch?v=..."
# Download, crop, strip audio
tools/video_converter.sh --crop --mute "https://youtu.be/..."Without --crop (letterbox):
ffmpeg -i <input>
-vf "fps=25,scale=320:-1"
-vcodec mjpeg -q:v <quality> -huffman optimal
-acodec libmp3lame -ab 128k -ar 22050
<output>.avi
With --crop (square fill):
ffmpeg -i <input>
-vf "fps=25,scale=320:320:force_original_aspect_ratio=increase,crop=320:320"
-vcodec mjpeg -q:v <quality> -huffman optimal
-acodec libmp3lame -ab 128k -ar 22050
<output>.avi
With --mute: replaces the -acodec libmp3lame ... line with -an (no audio).
Audio is decoded using the MP3 fed mode in mp3_player.c. Rather than reading from an SD file directly, Core 0 (video player) pre-indexes audio chunks from the AVI file and feeds compressed MP3 data into a 64 KB ring buffer in QMI PSRAM. Core 1 reads from this ring and decodes in the normal MP3 DMA path. This avoids PIO PSRAM bus contention (PIO1 SPI is not safe across cores) and lets video and audio share the existing MP3 playback pipeline.
Audio playback is automatically stopped and the ring freed when player:stop() is called or the player is garbage-collected.
When player:play() is called, the system clock is raised from 200 MHz to 300 MHz to fit MJPEG decode within the 40 ms frame budget. The clock is restored when player:stop() is called.
Because the CYW43 WiFi PIO divider cannot be updated at runtime, WiFi is disconnected before the overclock and reconnected (using saved credentials) after restore. Apps that require persistent WiFi connections should avoid video playback, or mute the audio and skip the overclock by using a custom playback loop.
- Crash Logging and Watchdog — fault recovery and reboot behavior
- API — Audio and Sound — MP3 player, sound samples, tone generation
- API — Display and Graphics — display primitives used alongside video