Skip to content

Quick Deck: play a deck's video and audio - #518

Merged
ymote merged 6 commits into
mainfrom
feat/quickdeck-playback
Oct 19, 2026
Merged

ymote merged 6 commits into
mainfrom
feat/quickdeck-playback

Conversation

@ymote

@ymote ymote commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Why

The maintainer approved playback on 13 Oct 2026 ("Add playback"). #511 lets decks with audio and video open (posters only), and #515 added deck.media, which hands the app a clip's bytes as a file in its storage. This PR plays those clips with the runtime's Video widget, which uses the system's own player. The contract is main's SERVICE.md "Playback", including its "As built in #515" bullets; this PR doesn't change SERVICE.md. The branch is rebased on main ffd5e48 (#515, #516, #517).

What

Editor

  • A chosen video or audio object shows a play control on the canvas: Play calls deck.media (with the shape's own id, never media.id), then overlays a Video on the clip's box at the canvas zoom and plays it.
  • The control also has pause/play on, stop, the time and mute. A sound gets a small speaker control. The inspector's new clip section says how the clip plays in a show and has Play, Pause and Stop.
  • While a clip plays, its frame has no handles and changes to it wait ("Stop the clip first"). Another slide, choice, tab or view stops it.

Present

  • A clip set to play by itself starts when its slide shows, looping if set. Clips from a .pptx never autoplay: deckcraft d0e57d7e doesn't read autoplay from a .pptx.
  • A tap on a clip plays or pauses it.
  • Another slide, the end of the show, or Exit stops it. A build step on the same slide doesn't.

States

  • "Preparing the clip…".
  • storage_full: in the contract's words.
  • unsupported: in plain words: the clip is linked to a file outside the deck, is empty, or isn't MP4, MOV, M4V, MP3, M4A, AAC or WAV.
  • "Playback isn't available on this system" when the widget can't play and nothing has played yet in this session. Once a clip has played, a failure reads "This clip couldn't be played.". The inspector adds the player's own words.

No widget called bar

Dev fixture and tests

  • make_fixture.py --media adds Launch Film, and the stand-in engine answers deck.media for it. The sample clip is dev-fixture/media/clip.mp4: the repository's synthetic H.264 clip with no sound, from crates/video-api-smoke, nothing installed.
  • tests/ui.py gets a playback run in card-host and one in the shell with the real engine. The shell run uses tests/media/launch-film.{deckcraft,pptx}, made by tests/make_media_decks.py with deckcraft-cli at d0e57d7e. After every step it checks that the shell removed no client and contained no module fault.
  • Seven model tests: clips, the error words, clip_step, the time display, where the control sits, and the widget names.

READMEs (both languages) and tests/evidence/playback-20261013/.

How the Video widget is used

Citations are to .sources/makepad (makepad d653bc44 with the runtime patches on main):

  • Source: VideoDataSource.Filesystem {path} (widgets/src/video.rs:2573-2579, registered at :71-74), resolved inside the app's storage for a policed isolate (video.rs:1582-1590 → splash_policy.rs:193-201 → splash_storage.rs:246, :290). App Hub gives every admitted app its storage jail and a policy (crates/app-policy/src/splash_adapter.rs:26-37 at 38635915).
  • Properties: autoplay, is_looping, mute (video.rs:582-588), show_controls (:525).
  • Script calls: state() (:1047), error() (:1059), current_position_ms() (:1066), total_duration_ms() (:1071), seek_to() (:1077), and begin_playback, pause_playback, stop_and_cleanup_resources, mute_playback, unmute_playback through script_control (:1888-1956). These come from tools/runtime-patches/makepad-video-controls-lifecycle.patch.
  • Events: the widget has VideoActions for prepared, began, completed and reset (:1021-1036, sent at :1199-1347), but no script callbacks. The app therefore asks state() and error() four times a second.
  • Errors: an error comes in as Event::VideoDecodingError. The widget keeps it as last_error, drops its native player and returns to "unprepared" (:1871-1883, :1961-1981).
  • No player: this shows through error():
    • macOS: when AVFoundation fails, the runtime falls back to its software decoder (platform/src/os/apple/apple_video_player.rs:106-135). With no media plugin installed, that fallback reports "playback session unavailable: no media plugin installed" (video_decode/software_video.rs:37; nothing registers one, media_plugin.rs:307).
    • Linux desktop: GStreamer when it loads (os/linux/gstreamer_sys.rs:309, linux_video_player.rs:119). On our host it fails with "Your GStreamer installation is missing a plug-in.".
    • Vulkan builds report "…not implemented" (linux_wayland.rs:1063).
    • Windows: Media Foundation (windows_video_playback.rs:1), or "D3D11 device unavailable…" without D3D11 (windows.rs:1242).
  • No end event on the Mac: the Apple backend never sends VideoPlaybackCompleted, so a clip whose position stops at its end counts as ended.
  • Hidden until playing: a hidden view still gets video events (view.rs:989, event.rs:684). The widget is made hidden and shown once it plays, so the poster stays until then.
  • Autoplay at creation doesn't work: a Video made with autoplay: true stayed "preparing" in card-host (three tries, 5 to 9 s each). The likely cause: the apply asks for the player before on_after_new gives the widget its native id (:654-684, :1487), and the prepared event is taken only for the current id (:1199-1203). The app makes the widget with autoplay off and calls begin_playback() once it exists.

The demo's vanished window

During a visible demo of this branch at d953aa3, Quick Deck's window went away and the shell stayed up. This is a close, not a crash.

  • The log: after two Present renders of the video slide, the log has exactly wm: removing client 1 then wm: module instance card.1 torn down; isolate SplashVmId(1) freed, and nothing else.
  • That pair is the shell's ordinary close (close_client → remove_client → module_host.rs teardown).
  • A fault leaves different lines:
    • a panic logs wm: contained a module panic… (module_host.rs:114) and leaves a failed tile (:1219);
    • a script error logs [E] splash lines;
    • a Video error logs Error decoding video….
  • Reproduced: in a hidden macOS shell on a copy of the demo profile, ⌘W printed exactly those two lines. ⌘W and ⌘Q are both "Close window" (binds.rs:151-152).
  • Not reproduced: playing the deck's video, pausing, tapping it, changing slides during playback, Present with taps, the arrow keys, Space, Escape and Return, and browsing every slide closed nothing. The deck, its frames and logs stayed on that Mac.
  • Likely input: ⌘W or ⌘Q, the window's close button, or a right or middle click on the bar's window title. The demo's remote port was open, so input injected by another local process can't be ruled out.
  • For the shell, outside this PR:
    • log which input closed a client;
    • a Splash app can't ask before it is closed (App Hub's card runner answers close_requested with Allow), so a close also drops unsaved edits;
    • Escape can't leave Present, because script apps get no key events.

Tests

  • card-host on the Mac (macOS 26.6.2, App Hub 38635915 built against this checkout's runtime, hidden, --remote, 64 ms budget): missing, fixture, states and playback all passed, light and dark, with no script error or overrun in the final run.
    • The clip played over its box: two grabs a second apart differ by 13,850 pixels, and the time read 0:01 then 0:03.
    • In Present, the autoplay clip's two grabs differ by 33,418 pixels. A tap paused it ("Paused · 0:04 / 0:05") and another tap resumed it.
    • Two earlier rounds on this tree each logged one budget overrun while other builds loaded this Mac (load averages 63 and 74).
    • Frames: card-host/p2-playing-pair.png and p8-present-pair.png, plus the d variants.
  • macOS shell with the real engine (hidden, built from this branch on main ffd5e48): playback passed, light then dark.
    • The editor's video played: 6,825 pixels changed in a second (dark 7,020). Pause, Play, a tap and Stop all worked.
    • Present's video started by itself: 12,814 pixels changed. Four taps paused and resumed it on the same slide.
    • The .pptx copy's clip didn't start by itself, and played on a tap (12,814).
    • deck.media wrote .cache/media/<sha256>.mp4 (the clip's own bytes), and closing the deck removed it. The sound was never played.
    • Frames are in macos/.
  • Linux shell with the real engine (headless weston, the same tree): shell and restart passed unchanged. playback passed, light then dark:
    • Every clip, in the editor and in Present, said "Playback isn't available on this system".
    • deck.media wrote the .mp4 and .wav, and closing the deck removed them.
  • Without shell: find the bar's strip as the body's own child, not by name (fixes Quick Deck crash after #516) #519: both shells ran main with A window's full screen, as macOS's green button makes it #516 but not shell: find the bar's strip as the body's own child, not by name (fixes Quick Deck crash after #516) #519 and didn't panic, because of the qd_toolbar rename.
  • Model tests: cargo test --locked -p octosense-shell --lib quickdeck_model passed 35 on Linux, and 36 with --features app-hub.
  • App Hub gate: hub check --system-app on a stamped copy refuses only for the missing listing.json, as for every system app.
  • ci-local was not run.

Not verified

  • Sound with audio out on any system: these runs open no audio device.
  • Windows.
  • A looping clip actually playing.
  • A light/dark switch while a clip plays.
  • Clips inside a group: deck.slide lists top-level shapes only.

🤖 Generated with Claude Code

ymote and others added 4 commits October 13, 2026 14:04
A chosen video or audio object shows a play control on the canvas (and
Play, Pause and Stop in the inspector's new clip section): Play calls
deck.media for the clip's file in the storage, then overlays the runtime's
Video widget on the clip's box at the canvas zoom, with that file as its
storage source (VideoDataSource.Filesystem). Pause, play on, stop and mute;
while it plays the clip's frame has no handles and changes to it wait;
another slide, choice, tab or view stops it. A sound gets a small speaker
control. In Present, a clip set to play by itself starts when its slide
shows (looping if set), a tap on a clip plays or pauses it, and another
slide or Exit stops it.

The widget calls no script, so the app asks it four times a second
(state(), error(), the position and the duration) and moves the clip on
with clip_step, a pure function. It is made with autoplay off and started
with begin_playback() once it exists (one made with autoplay on stayed
"preparing"), hidden until it plays so the poster shows, and its end found
by a stalled position (the Mac's player reports none). States: preparing;
storage_full: and unsupported: in plain words; "Playback isn't available on
this system" when the widget can't play, or "This clip couldn't be
played." once a clip has played here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dev fixture answers deck.media: make_fixture.py --media adds Launch
Film (a video set to play by itself with the repository's synthetic sample
clip, now dev-fixture/media/clip.mp4; a linked sound; a clip whose file
isn't there; one too big for the storage). tests/ui.py gains a playback run
in card-host (the clip plays over its box: two grabs a second apart must
differ there; pause, stop, a slide change, the refusals, Present's
autoplay and taps, and a fresh session's "Playback isn't available") and
one in the shell with the real engine on tests/media's decks, made by
tests/make_media_decks.py with deckcraft-cli at d0e57d7e (a Mac must play
each video, Present's taps must pause and play it on the same slide, Linux
must say it can't; deck.media's cache is checked against the clip's own
bytes; after every step the shell's log must show no client removed and no
module fault). Model tests cover which shapes are clips, the refusals' and
failures' words, clip_step, the time and the control's place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #516 put the shell's bar after the scene, the shell's
self.ui.widget(cx, ids!(bar)) found Quick Deck's toolbar, also called
`bar`, first, and panicked applying its chrome to it with the shell's
script VM (#519 fixes the shell). The toolbar region is now `qd_toolbar`,
and a model test reads the names the shell looks its own widgets up by in
its window (lib.rs, desktop_app.rs, mobile_app.rs), plus `bar`, and fails
if Quick Deck names a widget after any of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
How a clip plays and where (the runtime's backends: AVFoundation on macOS,
GStreamer on a Linux desktop when its plugins are there, Media Foundation
on Windows), the states and words, what closing the app does, the runs and
the test decks, and what is verified: card-host on a Mac and the real
engine in hidden shells on macOS and Linux, light and dark, on main
ffd5e48, with tests/evidence/playback-20261013.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote marked this pull request as ready for review October 16, 2026 15:59
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote

ymote commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Local CI passed on 2e9f7aa (tools/ci-local.sh --only all --linux-host --offload, 16m18s, Darwin arm64; the ubuntu jobs and the macOS jobs' portable steps (their macOS steps ran here) on the Linux build host, Linux x86_64).

GitHub would run for this PR: desktop.yml, phone.yml, apps.yml. Merged on this local pass (macOS runner queue); GitHub CI runs on the merge commit on main.

Job Step Result Time
ci-local / drift The local mapping fits the workflows PASS -
desktop.yml / desktop Prepare the pinned framework sources PASS 2s
desktop.yml / desktop Compile the desktop with the shared runtime PASS 6m37s
desktop.yml / desktop Desktop tools and scripts PASS 38s
desktop.yml / desktop (linux) Prepare the pinned framework sources PASS 1s
desktop.yml / desktop (linux) One copy of every shell source PASS -
desktop.yml / desktop (linux) In-app updater and installer handoff PASS 1m42s
desktop.yml / desktop (linux) One Makepad, App Hub, octos and Rinx in the locked graph PASS 2s
desktop.yml / desktop (linux) The native apps' generated places match native-apps.json PASS 0s
desktop.yml / native-host-api Prepare the pinned framework sources PASS 2s
desktop.yml / native-host-api Build the native acceptance host and pinned Hub CLI PASS 1m55s
desktop.yml / native-host-api Signed app tool calls a real OS permission API PASS 5s
phone.yml / home Prepare pinned sources and the reviewed runtime patch PASS 2s
phone.yml / home Validate launcher icons and Apple asset catalogs PASS 11s
phone.yml / home (linux) Prepare pinned sources and the reviewed runtime patch PASS 0s
phone.yml / home (linux) Verify the single runtime graph PASS 2s
phone.yml / home (linux) The native apps' generated places match native-apps.json PASS 0s
phone.yml / home (linux) The pinned octos revision PASS 0s
phone.yml / home (linux) Build octos (unless cached) PASS -
phone.yml / home (linux) Build Home, App Hub admission and runtime policy tests PASS 3m25s
phone.yml / home (linux) Test Home, App Hub admission and runtime policy PASS 41s
phone.yml / home (linux) Two-lane scenario (real kernel, scripted model) PASS 27s
phone.yml / home (linux) Build opt-in AppCard tests PASS 2m45s
phone.yml / home (linux) Test opt-in AppCard PASS 1s
phone.yml / home (linux) Build craft-engine relay and system-chat tests PASS 2m47s
phone.yml / home (linux) Test craft-engine relay and system chat PASS 13s
phone.yml / home (linux) Build linked Terminal tests PASS 2m28s
phone.yml / home (linux) Test linked Terminal PASS 1s
phone.yml / home (linux) Build Wasm and runtime policy tests PASS 1m16s
phone.yml / home (linux) Test Wasm and runtime policy PASS 11s
phone.yml / home Compile Home and its bundled modules PASS 6m47s
apps.yml / services (linux) python3 tools/setup.py PASS 0s
apps.yml / services (linux) Shared system app interface PASS 0s
apps.yml / services (linux) Kernel service PASS 19s
apps.yml / services (linux) OctoSense release updater PASS 6s
apps.yml / services (linux) App peers PASS 30s
apps.yml / services (linux) AI providers config PASS 5s
apps.yml / services (linux) Connected provider protocols and Linux host adapter PASS 46s
apps.yml / services (linux) L0 in-card chat (sys.chat, host side) PASS 9s
apps.yml / services (linux) System toolbox (workflow templates) PASS 1m30s
apps.yml / kernel-security (linux) python3 tools/setup.py PASS 0s
apps.yml / kernel-security (linux) The pinned octos revision PASS 0s
apps.yml / kernel-security (linux) Build octos (unless cached) PASS -
apps.yml / kernel-security (linux) Real-kernel tests PASS 41s
apps.yml / apps (linux) python3 tools/setup.py PASS 0s
apps.yml / apps (linux) Host services (Mail, AI providers, News) PASS 2m05s
apps.yml / apps (linux) Connected accounts and Rinx editor PASS 1m31s
apps.yml / apps (linux) Shell AI services (crates/ai-host) PASS 2m58s
apps.yml / apps (linux) AppCard runtime setup safeguards PASS 0s
apps.yml / apps (linux) AppCard clippy (own crates; shared runtime compiles) PASS 26s
apps.yml / apps (linux) AppCard transport and store PASS 7s
apps.yml / apps (linux) One Makepad, App Hub, octos and Rinx in the locked graph PASS 3s
rom.yml / product (linux) python3 -m unittest discover -s tests -v SKIPPED (!) -
rom.yml / product (linux) Check generated Agent Binder client SKIPPED (!) -
rom.yml / product (linux) bash -n scripts/stage-forks.sh scripts/publish-release.sh scripts/apply-to-tree.sh scripts/build-rom.sh PASS -
rom.yml / web-installer (linux) npm ci --ignore-scripts --no-audit --no-fund SKIPPED (!) -
rom.yml / web-installer (linux) npm test SKIPPED (!) -
rom.yml / web-installer (linux) npx playwright install --with-deps chromium SKIPPED (!) -
rom.yml / web-installer (linux) npm run test:browser SKIPPED (!) -
rom.yml / web-installer (linux) python3 -m unittest discover -s tests -p test_installer_manifest.py -v SKIPPED (!) -
rom.yml / web-installer (linux) bash -n scripts/release.sh SKIPPED (!) -
linux-host / sandbox (linux) python3 tools/setup.py PASS 0s
linux-host / sandbox (linux) Process sandbox on Linux (Landlock, seccomp) PASS 1m47s

Skipped (these did NOT pass; (!) marks a skip that blocks tools/ci-local-merge.sh):

  • rom.yml / product (linux): python3 -m unittest discover -s tests -v: javac -version does not work here: install a JDK (17 or newer) and put its bin/ on PATH or set JAVA_HOME
  • rom.yml / product (linux): Check generated Agent Binder client: needs build-tools/35.0.0/aidl, platforms/android-35/framework.aidl in the Android SDK (no Android SDK found): install the SDK packages 'build-tools;35.0.0' and 'platforms;android-35' (sdkmanager) and set ANDROID_HOME
  • rom.yml / web-installer (linux): npm ci --ignore-scripts --no-audit --no-fund: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npm test: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npx playwright install --with-deps chromium: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npm run test:browser: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): python3 -m unittest discover -s tests -p test_installer_manifest.py -v: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): bash -n scripts/release.sh: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH

Notes:

  • GitHub runs Python 3.12; this run used Python 3.9.6
  • GitHub runs Python 3.12; the Linux host used Python 3.14.4

@ymote
ymote merged commit 3976b18 into main Oct 19, 2026
1 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant