3. Replace src/main.luau
luastra/media · live media_state events · about 20 minuteslaunch → queue → commands + events → decoded state
The host owns playback and progress; Luau only issues intent and renders validated snapshots.
When to use it
Replace the complete entry module. Do not add a timer or frame loop: the host emits bounded progress and lifecycle updates.
Focused example
--!strict
local Assets = require("luastra/assets")
local Media = require("luastra/media")
local UI = require("luastra/ui")
local media: Media.State = {
revision = 0,
status = "idle",
itemId = "",
title = "",
artist = "",
positionMs = 0,
durationMs = 0,
bufferedMs = 0,
queueIndex = -1,
queueCount = 0,
background = false,
interruption = "none",
route = "default",
error = nil,
}
local pending: {[number]: string} = {}
local message = "Waiting for launch"
local App = require("luastra/app")
local Root = {}
local queue: {Media.QueueItem} = {
{
id = "focus",
source = Assets.uri(Assets.audio("audio/focus")),
title = "Focus sample",
artist = "Luastra",
},
}
local function track(requestId: number, label: string)
pending[requestId] = label
message = label .. " requested"
end
local function applyState(payload: string, nextMessage: string?): boolean
local decoded = Media.decodeState(payload)
if not decoded.success then
message = "Rejected media state: " .. decoded.error
return false
end
media = decoded.state
if nextMessage ~= nil then message = nextMessage end
return true
end
function Root.render(): UI.Node
local elapsed = math.floor(media.positionMs / 1000)
local duration = math.floor(media.durationMs / 1000)
return UI.Screen {
id = "media-recipe",
width = "content",
padding = "responsive",
UI.Card {
id = "player/card",
gap = "md",
padding = "lg",
surface = "elevated",
UI.Text { id = "player/title", text = "Packaged audio", variant = "title" },
UI.Text {
id = "player/item",
text = if media.title == "" then "Nothing loaded" else media.title,
variant = "heading",
},
UI.Text {
id = "player/state",
text = "Status: " .. media.status,
role = "status",
tone = if media.status == "error" then "error" else nil,
},
UI.Text {
id = "player/progress",
text = "Progress: " .. tostring(elapsed) .. "s / " .. tostring(duration) .. "s",
},
UI.Text { id = "player/message", text = message, role = "status", tone = "muted" },
UI.Actions {
id = "player/actions",
gap = "sm",
responsive = true,
UI.Button {
id = "player/play",
text = "Play",
onTap = "media.play",
disabled = media.status ~= "ready" and media.status ~= "paused" and media.status ~= "ended",
},
UI.Button {
id = "player/pause",
text = "Pause",
onTap = "media.pause",
appearance = "secondary",
disabled = media.status ~= "playing" and media.status ~= "buffering",
},
},
},
}
end
function Root.handle(action: string, target: string, value: string)
if action == "lifecycle" and target == "app" and value == "launch" then
if media.queueCount == 0 then track(Media.setQueue(queue, 1), "Queue load") end
elseif action == "media_state" then
applyState(value, nil)
elseif action == "media.play" and target == "player/play" then
track(Media.play(), "Play")
elseif action == "media.pause" and target == "player/pause" then
track(Media.pause(), "Pause")
end
end
function Root.resolve(
requestId: number,
success: boolean,
payload: string,
errorCode: string,
errorMessage: string
)
local label = pending[requestId]
if label == nil then return end
pending[requestId] = nil
if not success then
message = label .. " failed: " .. errorCode .. " — " .. errorMessage
return
end
applyState(payload, label .. " completed")
end
function Root.snapshot()
return { status = media.status, title = media.title, message = message }
end
return App.compose {
render = function(_outputs) return Root.render() end,
handle = Root.handle,
resolve = Root.resolve,
snapshot = Root.snapshot,
}