Do not leave a ghost in the call, and use the window
Two things a real call found. Dropping out was silent. freeq counts a participant until an av-leave says otherwise, and when the media plane ended under us we cleared the local call and told nobody — so the room still counted this device, and pressing Join again added a second one beside it. That is how a channel came to report seven people in a call with two. Any exit we did not ask for now announces itself first, while the session id it needs is still there to read. A failure announces too: the reason stays on screen for the person, but the server is told either way, because we are out of the call whether or not they have read why yet. And the wall was too timid. A third of the height held one person to a 440-point tile on a 1920x1060 screen with fourteen hundred points of empty width beside them, which reads as video that will not scale, because in every way the eye can tell it does not. Half the height leaves the conversation legible underneath while letting a maximised window be worth maximising: three people go from 440 points to 620. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9cc16c7 parent: ddebf2a modified
src/frq/app.jolt +4 -0 | @@ -807,5 +807,9 @@ | ||
| 807 | 807 | ;; else that touches a node belongs to the loop thread, and this is glimmer's |
| 808 | 808 | ;; way of getting onto it. |
| 809 | 809 | (av/init-logging!) |
| 810 | + ;; Who to tell when a call ends under us rather than at our asking. Set here | |
| 811 | + ;; rather than in frq.av because sending a TAGMSG needs the connection, and | |
| 812 | + ;; that belongs to the state layer. | |
| 813 | + (reset! av/on-dropped s/announce-leave!) | |
| 810 | 814 | (vidya/after! 0 av/install-pump!) |
| 811 | 815 | (ui/run app :title "frq" :width 520 :height 860)) |
| @@ -807,5 +807,9 @@ | |||
| 807 | ;; else that touches a node belongs to the loop thread, and this is glimmer's | 807 | ;; else that touches a node belongs to the loop thread, and this is glimmer's |
| 808 | ;; way of getting onto it. | 808 | ;; way of getting onto it. |
| 809 | (av/init-logging!) | 809 | (av/init-logging!) |
| 810 | + ;; Who to tell when a call ends under us rather than at our asking. Set here | ||
| 811 | + ;; rather than in frq.av because sending a TAGMSG needs the connection, and | ||
| 812 | + ;; that belongs to the state layer. | ||
| 813 | + (reset! av/on-dropped s/announce-leave!) | ||
| 810 | (vidya/after! 0 av/install-pump!) | 814 | (vidya/after! 0 av/install-pump!) |
| 811 | (ui/run app :title "frq" :width 520 :height 860)) | 815 | (ui/run app :title "frq" :width 520 :height 860)) |
modified
src/frq/av.jolt +53 -15 | @@ -178,6 +178,17 @@ | ||
| 178 | 178 | ;; The last thing the media plane failed with, for the line under the controls. |
| 179 | 179 | (defonce media-error (atom nil)) |
| 180 | 180 | |
| 181 | +;; What to do when this device stops being in a call without having asked to. | |
| 182 | +;; | |
| 183 | +;; The media plane failing is not the server hearing about it: freeq counts a | |
| 184 | +;; participant until an `av-leave` says otherwise. Dropping out quietly leaves | |
| 185 | +;; a ghost in the room, and pressing Join again adds a second one — which is | |
| 186 | +;; how a channel ends up reporting seven people in a call with two. | |
| 187 | +;; | |
| 188 | +;; `frq.state` puts the TAGMSG here. This namespace cannot send one itself | |
| 189 | +;; without depending on the client that owns the connection. | |
| 190 | +(defonce on-dropped (atom nil)) | |
| 191 | + | |
| 181 | 192 | ;; Feeds pushed to Vidya, so the ones that stop can be dropped again. Without |
| 182 | 193 | ;; this the last frame of someone who left hangs on the wall for the rest of |
| 183 | 194 | ;; the call. |
| @@ -249,9 +260,11 @@ | ||
| 249 | 260 | (reset! feeds [])) |
| 250 | 261 | |
| 251 | 262 | (defn stop-media! |
| 252 | - "Leave the media plane and forget the call. Signaling is the caller's to send | |
| 253 | - — `leave!` does both, and this is what is left when the server ended it for | |
| 254 | - us." | |
| 263 | + "Leave the media plane and forget the call, telling nobody. | |
| 264 | + | |
| 265 | + For the two cases where the server already knows: it ended the call itself, | |
| 266 | + or the caller is about to send an `av-leave` of its own. Anything else wants | |
| 267 | + `dropped!`, or freeq goes on counting a participant who is not there." | |
| 255 | 268 | [] |
| 256 | 269 | (raw-stop) |
| 257 | 270 | (drop-feeds!) |
| @@ -259,6 +272,18 @@ | ||
| 259 | 272 | (reset! local-call nil) |
| 260 | 273 | (reset! media-error nil)) |
| 261 | 274 | |
| 275 | +(defn dropped! | |
| 276 | + "We are out of the call and did not ask to be — the media plane failed, or | |
| 277 | + the transport went away under it. | |
| 278 | + | |
| 279 | + Tells whoever registered `on-dropped` first, while the session id and | |
| 280 | + instance it needs are still here to be read, and only then forgets them." | |
| 281 | + [] | |
| 282 | + (when-let [announce @on-dropped] | |
| 283 | + (when-let [call @local-call] | |
| 284 | + (try (announce call) (catch Exception _ nil)))) | |
| 285 | + (stop-media!)) | |
| 286 | + | |
| 262 | 287 | (defn- start-media! |
| 263 | 288 | "Dial the SFU for the call we have already joined over IRC. |
| 264 | 289 | |
| @@ -374,12 +399,21 @@ | ||
| 374 | 399 | :has-camera? (not (zero? (raw-status-has-camera))) |
| 375 | 400 | :has-mic? (not (zero? (raw-status-has-mic)))))) |
| 376 | 401 | |
| 402 | + ;; Both of these are the call ending underneath us rather than at | |
| 403 | + ;; our request, so both have to be announced. A failure keeps the | |
| 404 | + ;; local call up afterwards so the reason stays on screen — but the | |
| 405 | + ;; server is told either way, because we are no longer in the call | |
| 406 | + ;; whether or not the person has read why yet. | |
| 377 | 407 | (= code status-ended) |
| 378 | - (stop-media!) | |
| 408 | + (dropped!) | |
| 379 | 409 | |
| 380 | 410 | (= code status-failed) |
| 381 | - (do (reset! media-error (status-text)) | |
| 382 | - (swap! local-call #(when % (assoc % :media :failed))))) | |
| 411 | + (let [why (status-text) | |
| 412 | + call @local-call] | |
| 413 | + (when-let [announce @on-dropped] | |
| 414 | + (when call (try (announce call) (catch Exception _ nil)))) | |
| 415 | + (reset! media-error why) | |
| 416 | + (swap! local-call #(when % (assoc % :media :failed))))) | |
| 383 | 417 | (recur))))) |
| 384 | 418 | |
| 385 | 419 | (defn- pump-frames! |
| @@ -498,20 +532,24 @@ | ||
| 498 | 532 | ;; A tile narrower than this is not a face, it is a thumbnail of one. |
| 499 | 533 | (def ^:private min-tile 96) |
| 500 | 534 | |
| 501 | -;; And an upper bound, so that one person alone does not become a wall-sized | |
| 502 | -;; portrait in a maximised window. Generous rather than tight — what actually | |
| 503 | -;; stops tiles growing is the height budget below, and this is only here so | |
| 504 | -;; there is an answer on a screen tall enough not to bind. | |
| 505 | -(def ^:private max-tile 480) | |
| 535 | +;; And an upper bound, so one person alone does not become a wall-sized | |
| 536 | +;; portrait. Generous rather than tight — what actually stops tiles growing is | |
| 537 | +;; the height budget below, and this is only here so there is an answer on a | |
| 538 | +;; screen tall enough that it never binds. | |
| 539 | +(def ^:private max-tile 720) | |
| 506 | 540 | |
| 507 | 541 | ;; What a tile costs in height beyond its picture: the name under it, and the |
| 508 | 542 | ;; gap to the row below. |
| 509 | 543 | (def ^:private tile-label 22) |
| 510 | 544 | |
| 511 | -;; The wall's share of the window. A call in a chat client is something you | |
| 512 | -;; have *while* reading the room — past about a third of the height it stops | |
| 513 | -;; being that and starts being a video app with a chat box attached. | |
| 514 | -(def ^:private wall-share 0.34) | |
| 545 | +;; The wall's share of the window height. | |
| 546 | +;; | |
| 547 | +;; A third was the first guess and it was wrong: on a 1920x1060 screen it held | |
| 548 | +;; one person to a 440-point tile with fourteen hundred points of empty width | |
| 549 | +;; beside them, which reads as video that will not scale — because in every way | |
| 550 | +;; the eye can tell, it does not. Half leaves the conversation legible under it | |
| 551 | +;; while letting a maximised window actually be worth maximising. | |
| 552 | +(def ^:private wall-share 0.5) | |
| 515 | 553 | |
| 516 | 554 | (defn- rows-for [n cols] (max 1 (quot (+ n (dec cols)) cols))) |
| 517 | 555 | |
| @@ -178,6 +178,17 @@ | |||
| 178 | ;; The last thing the media plane failed with, for the line under the controls. | 178 | ;; The last thing the media plane failed with, for the line under the controls. |
| 179 | (defonce media-error (atom nil)) | 179 | (defonce media-error (atom nil)) |
| 180 | 180 | ||
| 181 | +;; What to do when this device stops being in a call without having asked to. | ||
| 182 | +;; | ||
| 183 | +;; The media plane failing is not the server hearing about it: freeq counts a | ||
| 184 | +;; participant until an `av-leave` says otherwise. Dropping out quietly leaves | ||
| 185 | +;; a ghost in the room, and pressing Join again adds a second one — which is | ||
| 186 | +;; how a channel ends up reporting seven people in a call with two. | ||
| 187 | +;; | ||
| 188 | +;; `frq.state` puts the TAGMSG here. This namespace cannot send one itself | ||
| 189 | +;; without depending on the client that owns the connection. | ||
| 190 | +(defonce on-dropped (atom nil)) | ||
| 191 | + | ||
| 181 | ;; Feeds pushed to Vidya, so the ones that stop can be dropped again. Without | 192 | ;; Feeds pushed to Vidya, so the ones that stop can be dropped again. Without |
| 182 | ;; this the last frame of someone who left hangs on the wall for the rest of | 193 | ;; this the last frame of someone who left hangs on the wall for the rest of |
| 183 | ;; the call. | 194 | ;; the call. |
| @@ -249,9 +260,11 @@ | |||
| 249 | (reset! feeds [])) | 260 | (reset! feeds [])) |
| 250 | 261 | ||
| 251 | (defn stop-media! | 262 | (defn stop-media! |
| 252 | - "Leave the media plane and forget the call. Signaling is the caller's to send | 263 | + "Leave the media plane and forget the call, telling nobody. |
| 253 | - — `leave!` does both, and this is what is left when the server ended it for | 264 | + |
| 254 | - us." | 265 | + For the two cases where the server already knows: it ended the call itself, |
| 266 | + or the caller is about to send an `av-leave` of its own. Anything else wants | ||
| 267 | + `dropped!`, or freeq goes on counting a participant who is not there." | ||
| 255 | [] | 268 | [] |
| 256 | (raw-stop) | 269 | (raw-stop) |
| 257 | (drop-feeds!) | 270 | (drop-feeds!) |
| @@ -259,6 +272,18 @@ | |||
| 259 | (reset! local-call nil) | 272 | (reset! local-call nil) |
| 260 | (reset! media-error nil)) | 273 | (reset! media-error nil)) |
| 261 | 274 | ||
| 275 | +(defn dropped! | ||
| 276 | + "We are out of the call and did not ask to be — the media plane failed, or | ||
| 277 | + the transport went away under it. | ||
| 278 | + | ||
| 279 | + Tells whoever registered `on-dropped` first, while the session id and | ||
| 280 | + instance it needs are still here to be read, and only then forgets them." | ||
| 281 | + [] | ||
| 282 | + (when-let [announce @on-dropped] | ||
| 283 | + (when-let [call @local-call] | ||
| 284 | + (try (announce call) (catch Exception _ nil)))) | ||
| 285 | + (stop-media!)) | ||
| 286 | + | ||
| 262 | (defn- start-media! | 287 | (defn- start-media! |
| 263 | "Dial the SFU for the call we have already joined over IRC. | 288 | "Dial the SFU for the call we have already joined over IRC. |
| 264 | 289 | ||
| @@ -374,12 +399,21 @@ | |||
| 374 | :has-camera? (not (zero? (raw-status-has-camera))) | 399 | :has-camera? (not (zero? (raw-status-has-camera))) |
| 375 | :has-mic? (not (zero? (raw-status-has-mic)))))) | 400 | :has-mic? (not (zero? (raw-status-has-mic)))))) |
| 376 | 401 | ||
| 402 | + ;; Both of these are the call ending underneath us rather than at | ||
| 403 | + ;; our request, so both have to be announced. A failure keeps the | ||
| 404 | + ;; local call up afterwards so the reason stays on screen — but the | ||
| 405 | + ;; server is told either way, because we are no longer in the call | ||
| 406 | + ;; whether or not the person has read why yet. | ||
| 377 | (= code status-ended) | 407 | (= code status-ended) |
| 378 | - (stop-media!) | 408 | + (dropped!) |
| 379 | 409 | ||
| 380 | (= code status-failed) | 410 | (= code status-failed) |
| 381 | - (do (reset! media-error (status-text)) | 411 | + (let [why (status-text) |
| 382 | - (swap! local-call #(when % (assoc % :media :failed))))) | 412 | + call @local-call] |
| 413 | + (when-let [announce @on-dropped] | ||
| 414 | + (when call (try (announce call) (catch Exception _ nil)))) | ||
| 415 | + (reset! media-error why) | ||
| 416 | + (swap! local-call #(when % (assoc % :media :failed))))) | ||
| 383 | (recur))))) | 417 | (recur))))) |
| 384 | 418 | ||
| 385 | (defn- pump-frames! | 419 | (defn- pump-frames! |
| @@ -498,20 +532,24 @@ | |||
| 498 | ;; A tile narrower than this is not a face, it is a thumbnail of one. | 532 | ;; A tile narrower than this is not a face, it is a thumbnail of one. |
| 499 | (def ^:private min-tile 96) | 533 | (def ^:private min-tile 96) |
| 500 | 534 | ||
| 501 | -;; And an upper bound, so that one person alone does not become a wall-sized | 535 | +;; And an upper bound, so one person alone does not become a wall-sized |
| 502 | -;; portrait in a maximised window. Generous rather than tight — what actually | 536 | +;; portrait. Generous rather than tight — what actually stops tiles growing is |
| 503 | -;; stops tiles growing is the height budget below, and this is only here so | 537 | +;; the height budget below, and this is only here so there is an answer on a |
| 504 | -;; there is an answer on a screen tall enough not to bind. | 538 | +;; screen tall enough that it never binds. |
| 505 | -(def ^:private max-tile 480) | 539 | +(def ^:private max-tile 720) |
| 506 | 540 | ||
| 507 | ;; What a tile costs in height beyond its picture: the name under it, and the | 541 | ;; What a tile costs in height beyond its picture: the name under it, and the |
| 508 | ;; gap to the row below. | 542 | ;; gap to the row below. |
| 509 | (def ^:private tile-label 22) | 543 | (def ^:private tile-label 22) |
| 510 | 544 | ||
| 511 | -;; The wall's share of the window. A call in a chat client is something you | 545 | +;; The wall's share of the window height. |
| 512 | -;; have *while* reading the room — past about a third of the height it stops | 546 | +;; |
| 513 | -;; being that and starts being a video app with a chat box attached. | 547 | +;; A third was the first guess and it was wrong: on a 1920x1060 screen it held |
| 514 | -(def ^:private wall-share 0.34) | 548 | +;; one person to a 440-point tile with fourteen hundred points of empty width |
| 549 | +;; beside them, which reads as video that will not scale — because in every way | ||
| 550 | +;; the eye can tell, it does not. Half leaves the conversation legible under it | ||
| 551 | +;; while letting a maximised window actually be worth maximising. | ||
| 552 | +(def ^:private wall-share 0.5) | ||
| 515 | 553 | ||
| 516 | (defn- rows-for [n cols] (max 1 (quot (+ n (dec cols)) cols))) | 554 | (defn- rows-for [n cols] (max 1 (quot (+ n (dec cols)) cols))) |
| 517 | 555 | ||
modified
src/frq/state.jolt +11 -0 | @@ -788,6 +788,17 @@ | ||
| 788 | 788 | :camera? false})] |
| 789 | 789 | (irc/tagmsg! c channel (av/join-tags session-id instance))))) |
| 790 | 790 | |
| 791 | +(defn announce-leave! | |
| 792 | + "Tell the room this device is out of a call it did not choose to leave. | |
| 793 | + | |
| 794 | + freeq counts a participant until an `av-leave` says otherwise, so a media | |
| 795 | + plane that fails silently leaves a ghost behind — and the next Join adds | |
| 796 | + another beside it. Registered with `frq.av` at startup, because that | |
| 797 | + namespace has no connection to send on." | |
| 798 | + [{:keys [channel session-id instance]}] | |
| 799 | + (when (and @conn (seq (or session-id ""))) | |
| 800 | + (irc/tagmsg! @conn channel (av/leave-tags session-id instance)))) | |
| 801 | + | |
| 791 | 802 | (defn leave-call! |
| 792 | 803 | "Leave the call, telling the room and the SFU both. |
| 793 | 804 | |
| @@ -788,6 +788,17 @@ | |||
| 788 | :camera? false})] | 788 | :camera? false})] |
| 789 | (irc/tagmsg! c channel (av/join-tags session-id instance))))) | 789 | (irc/tagmsg! c channel (av/join-tags session-id instance))))) |
| 790 | 790 | ||
| 791 | +(defn announce-leave! | ||
| 792 | + "Tell the room this device is out of a call it did not choose to leave. | ||
| 793 | + | ||
| 794 | + freeq counts a participant until an `av-leave` says otherwise, so a media | ||
| 795 | + plane that fails silently leaves a ghost behind — and the next Join adds | ||
| 796 | + another beside it. Registered with `frq.av` at startup, because that | ||
| 797 | + namespace has no connection to send on." | ||
| 798 | + [{:keys [channel session-id instance]}] | ||
| 799 | + (when (and @conn (seq (or session-id ""))) | ||
| 800 | + (irc/tagmsg! @conn channel (av/leave-tags session-id instance)))) | ||
| 801 | + | ||
| 791 | (defn leave-call! | 802 | (defn leave-call! |
| 792 | "Leave the call, telling the room and the SFU both. | 803 | "Leave the call, telling the room and the SFU both. |
| 793 | 804 | ||