Show the video that was already arriving
The call wall asked the media plane who had a picture, at render time. glimmer re-renders a component when a ratom it read has changed, and that question read none — so the wall rendered once, with nobody on screen, and was never told that anyone had appeared. Frames arrived, were handed to Vidya, and painted into a node that had never been created. So the pump publishes the feed list into a cell and the wall reads that. Written only when the set actually changes: writing every frame would rebuild every :image node sixty times a second around textures that were fine where they were. The row is always present now, so a tile appearing adds a child instead of changing the shape of the tree around it — and its children are a seq rather than a vector, because children splice and a vector is read as one more element, which an empty one is not. Also call joltmoq_init_logging, which was bound and never called. RUST_LOG is the only thing that can see MoQ subscription and device open; the plane was silent about all of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ea3c833 parent: cd7e9d2 modified
src/frq/app.jolt +14 -6 | @@ -536,13 +536,20 @@ | ||
| 536 | 536 | "Everyone with a camera on, or nothing at all. |
| 537 | 537 | |
| 538 | 538 | A call with no video is the normal case and should look like one — a row of |
| 539 | - empty frames would suggest something had failed to load." | |
| 539 | + empty frames would suggest something had failed to load. The row itself is | |
| 540 | + always here even when empty, so a tile appearing adds a child rather than | |
| 541 | + changing the shape of the tree around it. | |
| 542 | + | |
| 543 | + `av/tiles` reads a cell the pump writes, which is what subscribes this | |
| 544 | + component: without that this would render once, with nobody on screen, and | |
| 545 | + never hear that someone had turned a camera on." | |
| 540 | 546 | [] |
| 541 | - (let [tiles (av/tiles)] | |
| 542 | - [:vbox {:key :call-wall :spacing 6} | |
| 543 | - (when (seq tiles) | |
| 544 | - [:hbox {:spacing 8} | |
| 545 | - (mapv call-tile tiles)])])) | |
| 547 | + [:vbox {:key :call-wall :spacing 6} | |
| 548 | + [:hbox {:key :tiles :spacing 8} | |
| 549 | + ;; A seq, not a vector: children splice, and a vector would be read as one | |
| 550 | + ;; more hiccup element — which an empty one is not. | |
| 551 | + (for [key (av/tiles)] | |
| 552 | + [call-tile key])]]) | |
| 546 | 553 | |
| 547 | 554 | (defn- call-controls |
| 548 | 555 | "What the person in a call can do about it. |
| @@ -787,5 +794,6 @@ | ||
| 787 | 794 | ;; read when it is not. It has to be a timer: `frame-rgba!` and everything |
| 788 | 795 | ;; else that touches a node belongs to the loop thread, and this is glimmer's |
| 789 | 796 | ;; way of getting onto it. |
| 797 | + (av/init-logging!) | |
| 790 | 798 | (vidya/after! 0 av/install-pump!) |
| 791 | 799 | (ui/run app :title "frq" :width 520 :height 860)) |
| @@ -536,13 +536,20 @@ | |||
| 536 | "Everyone with a camera on, or nothing at all. | 536 | "Everyone with a camera on, or nothing at all. |
| 537 | 537 | ||
| 538 | A call with no video is the normal case and should look like one — a row of | 538 | A call with no video is the normal case and should look like one — a row of |
| 539 | - empty frames would suggest something had failed to load." | 539 | + empty frames would suggest something had failed to load. The row itself is |
| 540 | + always here even when empty, so a tile appearing adds a child rather than | ||
| 541 | + changing the shape of the tree around it. | ||
| 542 | + | ||
| 543 | + `av/tiles` reads a cell the pump writes, which is what subscribes this | ||
| 544 | + component: without that this would render once, with nobody on screen, and | ||
| 545 | + never hear that someone had turned a camera on." | ||
| 540 | [] | 546 | [] |
| 541 | - (let [tiles (av/tiles)] | 547 | + [:vbox {:key :call-wall :spacing 6} |
| 542 | - [:vbox {:key :call-wall :spacing 6} | 548 | + [:hbox {:key :tiles :spacing 8} |
| 543 | - (when (seq tiles) | 549 | + ;; A seq, not a vector: children splice, and a vector would be read as one |
| 544 | - [:hbox {:spacing 8} | 550 | + ;; more hiccup element — which an empty one is not. |
| 545 | - (mapv call-tile tiles)])])) | 551 | + (for [key (av/tiles)] |
| 552 | + [call-tile key])]]) | ||
| 546 | 553 | ||
| 547 | (defn- call-controls | 554 | (defn- call-controls |
| 548 | "What the person in a call can do about it. | 555 | "What the person in a call can do about it. |
| @@ -787,5 +794,6 @@ | |||
| 787 | ;; read when it is not. It has to be a timer: `frame-rgba!` and everything | 794 | ;; read when it is not. It has to be a timer: `frame-rgba!` and everything |
| 788 | ;; else that touches a node belongs to the loop thread, and this is glimmer's | 795 | ;; else that touches a node belongs to the loop thread, and this is glimmer's |
| 789 | ;; way of getting onto it. | 796 | ;; way of getting onto it. |
| 797 | + (av/init-logging!) | ||
| 790 | (vidya/after! 0 av/install-pump!) | 798 | (vidya/after! 0 av/install-pump!) |
| 791 | (ui/run app :title "frq" :width 520 :height 860)) | 799 | (ui/run app :title "frq" :width 520 :height 860)) |
modified
src/frq/av.jolt +50 -14 | @@ -183,6 +183,15 @@ | ||
| 183 | 183 | ;; the call. |
| 184 | 184 | (defonce ^:private painted-feeds (atom #{})) |
| 185 | 185 | |
| 186 | +;; Who has a picture, in the order they should be drawn. | |
| 187 | +;; | |
| 188 | +;; A cell rather than a question asked at render time, and that is the whole | |
| 189 | +;; point: glimmer re-renders a component when a ratom it read has changed, so a | |
| 190 | +;; view that asked the media plane directly would render once with nobody on | |
| 191 | +;; screen and never be told that someone had appeared. The frames would arrive, | |
| 192 | +;; be handed to Vidya, and paint into no node at all. | |
| 193 | +(defonce feeds (atom [])) | |
| 194 | + | |
| 186 | 195 | ;; The self-view is keyed this way by the media plane; the UI wants to know |
| 187 | 196 | ;; which tile is its own, to label it and to mirror nothing else. |
| 188 | 197 | (def local-feed "__local__") |
| @@ -229,7 +238,8 @@ | ||
| 229 | 238 | "Stop painting every feed we have been pushing." |
| 230 | 239 | [] |
| 231 | 240 | (doseq [k @painted-feeds] (vidya/frame-drop! k)) |
| 232 | - (reset! painted-feeds #{})) | |
| 241 | + (reset! painted-feeds #{}) | |
| 242 | + (reset! feeds [])) | |
| 233 | 243 | |
| 234 | 244 | (defn stop-media! |
| 235 | 245 | "Leave the media plane and forget the call. Signaling is the caller's to send |
| @@ -238,6 +248,7 @@ | ||
| 238 | 248 | [] |
| 239 | 249 | (raw-stop) |
| 240 | 250 | (drop-feeds!) |
| 251 | + (reset! feeds []) | |
| 241 | 252 | (reset! local-call nil) |
| 242 | 253 | (reset! media-error nil)) |
| 243 | 254 | |
| @@ -383,14 +394,32 @@ | ||
| 383 | 394 | (swap! painted-feeds conj key))) |
| 384 | 395 | (recur)))) |
| 385 | 396 | |
| 386 | -(defn- forget-gone-feeds! | |
| 387 | - "Drop the tiles of everyone who has stopped publishing." | |
| 397 | +(defn- order-feeds | |
| 398 | + "Everyone with a picture, the self-view last. | |
| 399 | + | |
| 400 | + Last because it is the one whose subject the person can already see, so it | |
| 401 | + belongs where it will not push a face they are talking to off the row." | |
| 402 | + [keys] | |
| 403 | + (let [mine (filter #(= local-feed %) keys) | |
| 404 | + others (sort (remove #(= local-feed %) keys))] | |
| 405 | + (vec (concat others mine)))) | |
| 406 | + | |
| 407 | +(defn- pump-feeds! | |
| 408 | + "Reconcile who has a picture: drop the tiles of anyone who has stopped, and | |
| 409 | + publish the list for the view to render from. | |
| 410 | + | |
| 411 | + The cell is only written when the set has actually changed. Writing it every | |
| 412 | + frame would re-render the call wall sixty times a second to say the same | |
| 413 | + thing, and every `:image` node under it would be rebuilt around a texture | |
| 414 | + that was fine where it was." | |
| 388 | 415 | [] |
| 389 | - (let [live (set (remove str/blank? (str/split-lines (or (video-keys) "")))) | |
| 390 | - gone (remove live @painted-feeds)] | |
| 391 | - (doseq [k gone] | |
| 416 | + (let [live (set (remove str/blank? (str/split-lines (or (video-keys) ""))))] | |
| 417 | + (doseq [k (remove live @painted-feeds)] | |
| 392 | 418 | (vidya/frame-drop! k) |
| 393 | - (swap! painted-feeds disj k)))) | |
| 419 | + (swap! painted-feeds disj k)) | |
| 420 | + (let [ordered (order-feeds live)] | |
| 421 | + (when-not (= ordered @feeds) | |
| 422 | + (reset! feeds ordered))))) | |
| 394 | 423 | |
| 395 | 424 | (defn pump! |
| 396 | 425 | "One frame's worth of the media plane. Cheap when no call is up. |
| @@ -401,7 +430,17 @@ | ||
| 401 | 430 | (when (live?) |
| 402 | 431 | (pump-status!) |
| 403 | 432 | (pump-frames!) |
| 404 | - (forget-gone-feeds!))) | |
| 433 | + (pump-feeds!))) | |
| 434 | + | |
| 435 | +(defn init-logging! | |
| 436 | + "Let the media plane talk to stderr, honouring `RUST_LOG`. | |
| 437 | + | |
| 438 | + Worth doing unconditionally: it says nothing at all without a `RUST_LOG`, | |
| 439 | + and when a call misbehaves it is the only thing that knows why — MoQ | |
| 440 | + subscription, codec negotiation and device open all happen on the far side of | |
| 441 | + the boundary, where no jolt-level trace can see them." | |
| 442 | + [] | |
| 443 | + (raw-init-logging)) | |
| 405 | 444 | |
| 406 | 445 | (defn install-pump! |
| 407 | 446 | "Start pumping the media plane every frame. Returns a timer id. |
| @@ -414,10 +453,7 @@ | ||
| 414 | 453 | (defn tiles |
| 415 | 454 | "Everyone with a picture in the current call, the self-view last. |
| 416 | 455 | |
| 417 | - Last because it is the one the person can already see the subject of, so it | |
| 418 | - belongs where it will not push a face they are talking to off the row." | |
| 456 | + Reads the cell the pump writes, so a component calling this re-renders when | |
| 457 | + someone turns their camera on." | |
| 419 | 458 | [] |
| 420 | - (let [ks (remove str/blank? (str/split-lines (or (video-keys) ""))) | |
| 421 | - mine (filter #(= local-feed %) ks) | |
| 422 | - others (sort (remove #(= local-feed %) ks))] | |
| 423 | - (vec (concat others mine)))) | |
| 459 | + @feeds) | |
| @@ -183,6 +183,15 @@ | |||
| 183 | ;; the call. | 183 | ;; the call. |
| 184 | (defonce ^:private painted-feeds (atom #{})) | 184 | (defonce ^:private painted-feeds (atom #{})) |
| 185 | 185 | ||
| 186 | +;; Who has a picture, in the order they should be drawn. | ||
| 187 | +;; | ||
| 188 | +;; A cell rather than a question asked at render time, and that is the whole | ||
| 189 | +;; point: glimmer re-renders a component when a ratom it read has changed, so a | ||
| 190 | +;; view that asked the media plane directly would render once with nobody on | ||
| 191 | +;; screen and never be told that someone had appeared. The frames would arrive, | ||
| 192 | +;; be handed to Vidya, and paint into no node at all. | ||
| 193 | +(defonce feeds (atom [])) | ||
| 194 | + | ||
| 186 | ;; The self-view is keyed this way by the media plane; the UI wants to know | 195 | ;; The self-view is keyed this way by the media plane; the UI wants to know |
| 187 | ;; which tile is its own, to label it and to mirror nothing else. | 196 | ;; which tile is its own, to label it and to mirror nothing else. |
| 188 | (def local-feed "__local__") | 197 | (def local-feed "__local__") |
| @@ -229,7 +238,8 @@ | |||
| 229 | "Stop painting every feed we have been pushing." | 238 | "Stop painting every feed we have been pushing." |
| 230 | [] | 239 | [] |
| 231 | (doseq [k @painted-feeds] (vidya/frame-drop! k)) | 240 | (doseq [k @painted-feeds] (vidya/frame-drop! k)) |
| 232 | - (reset! painted-feeds #{})) | 241 | + (reset! painted-feeds #{}) |
| 242 | + (reset! feeds [])) | ||
| 233 | 243 | ||
| 234 | (defn stop-media! | 244 | (defn stop-media! |
| 235 | "Leave the media plane and forget the call. Signaling is the caller's to send | 245 | "Leave the media plane and forget the call. Signaling is the caller's to send |
| @@ -238,6 +248,7 @@ | |||
| 238 | [] | 248 | [] |
| 239 | (raw-stop) | 249 | (raw-stop) |
| 240 | (drop-feeds!) | 250 | (drop-feeds!) |
| 251 | + (reset! feeds []) | ||
| 241 | (reset! local-call nil) | 252 | (reset! local-call nil) |
| 242 | (reset! media-error nil)) | 253 | (reset! media-error nil)) |
| 243 | 254 | ||
| @@ -383,14 +394,32 @@ | |||
| 383 | (swap! painted-feeds conj key))) | 394 | (swap! painted-feeds conj key))) |
| 384 | (recur)))) | 395 | (recur)))) |
| 385 | 396 | ||
| 386 | -(defn- forget-gone-feeds! | 397 | +(defn- order-feeds |
| 387 | - "Drop the tiles of everyone who has stopped publishing." | 398 | + "Everyone with a picture, the self-view last. |
| 399 | + | ||
| 400 | + Last because it is the one whose subject the person can already see, so it | ||
| 401 | + belongs where it will not push a face they are talking to off the row." | ||
| 402 | + [keys] | ||
| 403 | + (let [mine (filter #(= local-feed %) keys) | ||
| 404 | + others (sort (remove #(= local-feed %) keys))] | ||
| 405 | + (vec (concat others mine)))) | ||
| 406 | + | ||
| 407 | +(defn- pump-feeds! | ||
| 408 | + "Reconcile who has a picture: drop the tiles of anyone who has stopped, and | ||
| 409 | + publish the list for the view to render from. | ||
| 410 | + | ||
| 411 | + The cell is only written when the set has actually changed. Writing it every | ||
| 412 | + frame would re-render the call wall sixty times a second to say the same | ||
| 413 | + thing, and every `:image` node under it would be rebuilt around a texture | ||
| 414 | + that was fine where it was." | ||
| 388 | [] | 415 | [] |
| 389 | - (let [live (set (remove str/blank? (str/split-lines (or (video-keys) "")))) | 416 | + (let [live (set (remove str/blank? (str/split-lines (or (video-keys) ""))))] |
| 390 | - gone (remove live @painted-feeds)] | 417 | + (doseq [k (remove live @painted-feeds)] |
| 391 | - (doseq [k gone] | ||
| 392 | (vidya/frame-drop! k) | 418 | (vidya/frame-drop! k) |
| 393 | - (swap! painted-feeds disj k)))) | 419 | + (swap! painted-feeds disj k)) |
| 420 | + (let [ordered (order-feeds live)] | ||
| 421 | + (when-not (= ordered @feeds) | ||
| 422 | + (reset! feeds ordered))))) | ||
| 394 | 423 | ||
| 395 | (defn pump! | 424 | (defn pump! |
| 396 | "One frame's worth of the media plane. Cheap when no call is up. | 425 | "One frame's worth of the media plane. Cheap when no call is up. |
| @@ -401,7 +430,17 @@ | |||
| 401 | (when (live?) | 430 | (when (live?) |
| 402 | (pump-status!) | 431 | (pump-status!) |
| 403 | (pump-frames!) | 432 | (pump-frames!) |
| 404 | - (forget-gone-feeds!))) | 433 | + (pump-feeds!))) |
| 434 | + | ||
| 435 | +(defn init-logging! | ||
| 436 | + "Let the media plane talk to stderr, honouring `RUST_LOG`. | ||
| 437 | + | ||
| 438 | + Worth doing unconditionally: it says nothing at all without a `RUST_LOG`, | ||
| 439 | + and when a call misbehaves it is the only thing that knows why — MoQ | ||
| 440 | + subscription, codec negotiation and device open all happen on the far side of | ||
| 441 | + the boundary, where no jolt-level trace can see them." | ||
| 442 | + [] | ||
| 443 | + (raw-init-logging)) | ||
| 405 | 444 | ||
| 406 | (defn install-pump! | 445 | (defn install-pump! |
| 407 | "Start pumping the media plane every frame. Returns a timer id. | 446 | "Start pumping the media plane every frame. Returns a timer id. |
| @@ -414,10 +453,7 @@ | |||
| 414 | (defn tiles | 453 | (defn tiles |
| 415 | "Everyone with a picture in the current call, the self-view last. | 454 | "Everyone with a picture in the current call, the self-view last. |
| 416 | 455 | ||
| 417 | - Last because it is the one the person can already see the subject of, so it | 456 | + Reads the cell the pump writes, so a component calling this re-renders when |
| 418 | - belongs where it will not push a face they are talking to off the row." | 457 | + someone turns their camera on." |
| 419 | [] | 458 | [] |
| 420 | - (let [ks (remove str/blank? (str/split-lines (or (video-keys) ""))) | 459 | + @feeds) |
| 421 | - mine (filter #(= local-feed %) ks) | ||
| 422 | - others (sort (remove #(= local-feed %) ks))] | ||
| 423 | - (vec (concat others mine)))) | ||