From c283aa49cca26cea60f8d0a6985ddfaab3cc4eb8 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 11 Aug 2025 17:05:56 -0700 Subject: [PATCH 1/6] mobile touch controls apparently. --- app/src/room/space.ts | 216 ++++++++++++++++++++++++++++++++++++++++++ moq | 2 +- 2 files changed, 217 insertions(+), 1 deletion(-) diff --git a/app/src/room/space.ts b/app/src/room/space.ts index 4dcfbac5..52b51b0c 100644 --- a/app/src/room/space.ts +++ b/app/src/room/space.ts @@ -24,6 +24,11 @@ export class Space { #maxZ = 0; + // Touch handling for mobile + #touches = new Map(); + #pinchStartDistance = 0; + #pinchStartScale = 1; + #signals = new Effect(); constructor(canvas: Canvas, sound?: Sound) { @@ -37,6 +42,12 @@ export class Space { this.#signals.eventListener(window, "mouseleave", this.#onMouseLeave.bind(this)); this.#signals.eventListener(window, "wheel", this.#onMouseWheel.bind(this), { passive: false }); + // Touch event listeners for mobile + this.#signals.eventListener(window, "touchstart", this.#onTouchStart.bind(this), { passive: false }); + this.#signals.eventListener(window, "touchmove", this.#onTouchMove.bind(this), { passive: false }); + this.#signals.eventListener(window, "touchend", this.#onTouchEnd.bind(this), { passive: false }); + this.#signals.eventListener(window, "touchcancel", this.#onTouchCancel.bind(this), { passive: false }); + // This is a bit of a hack, but register our render method. this.canvas.onRender = this.#tick.bind(this); this.#signals.cleanup(() => { @@ -165,6 +176,211 @@ export class Space { broadcast.publishPosition(); } + #onTouchStart(e: TouchEvent) { + const rect = this.canvas.element.getBoundingClientRect(); + + // Store all active touches + this.#touches.clear(); + for (const touch of e.touches) { + const isOverCanvas = + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom; + + if (isOverCanvas) { + this.#touches.set(touch.identifier, { x: touch.clientX, y: touch.clientY }); + } + } + + if (this.#touches.size === 0) return; + + e.preventDefault(); + + // Single touch - start dragging + if (this.#touches.size === 1) { + const touch = e.touches[0]; + const mouse = this.canvas.relative(touch.clientX, touch.clientY); + + this.#dragging = undefined; + + const broadcast = this.#at(mouse); + if (!broadcast) return; + + if (broadcast.locked()) return; + + const viewport = this.canvas.viewport.peek(); + + // Bump the z-index unless we're already at the top. + broadcast.targetPosition.set((prev) => ({ + ...prev, + x: mouse.x / viewport.x - 0.5, + y: mouse.y / viewport.y - 0.5, + z: prev.z === this.#maxZ ? this.#maxZ : ++this.#maxZ, + })); + + this.#dragging = broadcast; + } + // Two touches - start pinch zoom + else if (this.#touches.size === 2) { + const touches = Array.from(e.touches); + const touch1 = touches[0]; + const touch2 = touches[1]; + + // Calculate the center point between the two touches + const centerX = (touch1.clientX + touch2.clientX) / 2; + const centerY = (touch1.clientY + touch2.clientY) / 2; + const center = this.canvas.relative(centerX, centerY); + + // Find the broadcast at the center point + const broadcast = this.#at(center); + if (!broadcast) return; + + if (broadcast.locked()) return; + + // Store the initial distance for pinch zoom + const dx = touch2.clientX - touch1.clientX; + const dy = touch2.clientY - touch1.clientY; + this.#pinchStartDistance = Math.sqrt(dx * dx + dy * dy); + this.#pinchStartScale = broadcast.targetPosition.peek().scale ?? 1; + + // Set as dragging to track which broadcast we're zooming + this.#dragging = broadcast; + + // Bump the z-index + broadcast.targetPosition.set((prev) => ({ + ...prev, + z: prev.z === this.#maxZ ? this.#maxZ : ++this.#maxZ, + })); + } + } + + #onTouchMove(e: TouchEvent) { + if (this.#touches.size === 0) return; + + const rect = this.canvas.element.getBoundingClientRect(); + + // Update touch positions + for (const touch of e.touches) { + if (this.#touches.has(touch.identifier)) { + const isOverCanvas = + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom; + + if (isOverCanvas) { + this.#touches.set(touch.identifier, { x: touch.clientX, y: touch.clientY }); + } + } + } + + e.preventDefault(); + + // Single touch - drag + if (e.touches.length === 1 && this.#dragging) { + const touch = e.touches[0]; + const mouse = this.canvas.relative(touch.clientX, touch.clientY); + const viewport = this.canvas.viewport.peek(); + + // Update the position but don't publish it yet. + this.#dragging.targetPosition.set((prev) => ({ + ...prev, + x: mouse.x / viewport.x - 0.5, + y: mouse.y / viewport.y - 0.5, + })); + } + // Two touches - pinch zoom + else if (e.touches.length === 2 && this.#dragging) { + const touches = Array.from(e.touches); + const touch1 = touches[0]; + const touch2 = touches[1]; + + // Calculate current distance + const dx = touch2.clientX - touch1.clientX; + const dy = touch2.clientY - touch1.clientY; + const currentDistance = Math.sqrt(dx * dx + dy * dy); + + // Calculate scale factor + if (this.#pinchStartDistance > 0) { + const scaleFactor = currentDistance / this.#pinchStartDistance; + const newScale = this.#pinchStartScale * scaleFactor; + + // Update the scale + this.#dragging.targetPosition.set((prev) => ({ + ...prev, + scale: Math.max(Math.min(newScale, 4), 0.25), + })); + } + + // Also update position to the center of the pinch + const centerX = (touch1.clientX + touch2.clientX) / 2; + const centerY = (touch1.clientY + touch2.clientY) / 2; + const center = this.canvas.relative(centerX, centerY); + const viewport = this.canvas.viewport.peek(); + + this.#dragging.targetPosition.set((prev) => ({ + ...prev, + x: center.x / viewport.x - 0.5, + y: center.y / viewport.y - 0.5, + })); + } + } + + #onTouchEnd(e: TouchEvent) { + // Remove ended touches + for (const touch of e.changedTouches) { + this.#touches.delete(touch.identifier); + } + + // If all touches ended, publish the final position + if (this.#touches.size === 0 && this.#dragging) { + this.#dragging.publishPosition(); + this.#dragging = undefined; + this.#hovering = undefined; + this.#pinchStartDistance = 0; + this.#pinchStartScale = 1; + } + // If we go from 2 touches to 1, switch from pinch to drag + else if (this.#touches.size === 1 && e.touches.length === 1) { + // Reset pinch state + this.#pinchStartDistance = 0; + this.#pinchStartScale = 1; + + // Check if we should start dragging a different broadcast + const touch = e.touches[0]; + const mouse = this.canvas.relative(touch.clientX, touch.clientY); + const broadcast = this.#at(mouse); + + if (broadcast && !broadcast.locked()) { + if (this.#dragging && this.#dragging !== broadcast) { + // Publish the old broadcast's position + this.#dragging.publishPosition(); + } + this.#dragging = broadcast; + } + } + + if (e.touches.length === 0) { + e.preventDefault(); + } + } + + #onTouchCancel(e: TouchEvent) { + // Clear all touches and reset state + this.#touches.clear(); + + if (this.#dragging) { + this.#dragging.publishPosition(); + this.#dragging = undefined; + this.#hovering = undefined; + this.#pinchStartDistance = 0; + this.#pinchStartScale = 1; + } + + e.preventDefault(); + } + #at(point: Vector): Broadcast | undefined { // Loop in reverse order to respect the z-index. const broadcasts = this.ordered.peek(); diff --git a/moq b/moq index be332d1d..f283d57b 160000 --- a/moq +++ b/moq @@ -1 +1 @@ -Subproject commit be332d1d79eee5c1d04e9c8a7718a79a59cad643 +Subproject commit f283d57bd946a5f5f27ec1c69f9d433b2a2f7fad From b411fac381722ca4366a73356f22c2e0494b43d1 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 12 Aug 2025 10:50:54 -0700 Subject: [PATCH 2/6] Looks good. --- api/src/fave.ts | 4 +- api/src/room.ts | 17 +++++- app/src/fave.tsx | 65 ++++++++++++++--------- app/src/preview.tsx | 125 +++++++++++++++++++++++++++++++++++++++----- app/src/sup.tsx | 2 +- moq | 2 +- 6 files changed, 173 insertions(+), 42 deletions(-) diff --git a/api/src/fave.ts b/api/src/fave.ts index e47c922d..0b65e4c4 100644 --- a/api/src/fave.ts +++ b/api/src/fave.ts @@ -57,7 +57,9 @@ export const router = rpc created_at: row.created_at, })); - return c.json({ favorites }); + // Generate a token that allows subscribing to all favorited rooms + const token = await ctx.room.signPreview(favorites.map((f) => f.room)); + return c.json({ favorites, token }); }) // Check if a room is favorited .get("/:room", rpc.withParam(z.object({ room: Room.nameSchema })), Auth.required, async (c) => { diff --git a/api/src/room.ts b/api/src/room.ts index 1de01bf5..735757f6 100644 --- a/api/src/room.ts +++ b/api/src/room.ts @@ -26,7 +26,22 @@ export class Context { return new URL(root, this.#env.RELAY_URL); } - const token = await Token.sign(this.#key, { root, sub: "", pub: account }); + const token = await Token.sign(this.#key, { root, put: account }); + return new URL(`${root}/?jwt=${token}`, this.#env.RELAY_URL); + } + + // Returns a URL to preview a list of rooms + async signPreview(rooms: Name[]): Promise { + const root = this.#env.RELAY_PREFIX; + if (!this.#key) { + // NOTE: On local dev with no key configured, this will be a firehose of all announcements + return new URL(root, this.#env.RELAY_URL); + } + + const token = await Token.sign(this.#key, { + root, + get: rooms, + }); return new URL(`${root}/?jwt=${token}`, this.#env.RELAY_URL); } } diff --git a/app/src/fave.tsx b/app/src/fave.tsx index b4e50f3b..c22fb31c 100644 --- a/app/src/fave.tsx +++ b/app/src/fave.tsx @@ -1,6 +1,7 @@ import * as Api from "@hang/api/client"; +import { Connection } from "@kixelated/hang"; import { useNavigate } from "@solidjs/router"; -import { createMemo, createResource, createSignal, For, Match, onMount, Show, Switch } from "solid-js"; +import { createMemo, createResource, createSignal, For, Match, onCleanup, onMount, Show, Switch } from "solid-js"; import type { JSX } from "solid-js/jsx-runtime"; import IconDelete from "~icons/mdi/delete"; import IconHeart from "~icons/mdi/heart"; @@ -12,6 +13,7 @@ import Dialog from "./components/dialog"; import Gradient from "./components/gradient"; import Login from "./components/login"; import Layout from "./layout/web"; +import { PreviewRoomCompact } from "./preview"; import * as Random from "./util/random"; export function Fave(props: { api: Api.Client }): JSX.Element { @@ -30,6 +32,10 @@ export function Fave(props: { api: Api.Client }): JSX.Element { return () => clearInterval(interval); }); + // Connection for live previews + const connection = new Connection(); + onCleanup(() => connection.close()); + const [favorites, { refetch }] = createResource(async () => { if (!props.api.authenticated()) return null; @@ -37,6 +43,7 @@ export function Fave(props: { api: Api.Client }): JSX.Element { const response = await props.api.routes.fave.all.$get(); if (response.ok) { const data = await response.json(); + connection.url.set(new URL(data.token)); return data.favorites; } } catch (error) { @@ -72,15 +79,6 @@ export function Fave(props: { api: Api.Client }): JSX.Element { } }; - // Determine how many favorites to show initially - const visibleFavorites = createMemo(() => { - const favs = favorites() || []; - if (showMore() || favs.length <= 6) { - return favs; - } - return favs.slice(0, 6); - }); - return (
@@ -133,12 +131,14 @@ export function Fave(props: { api: Api.Client }): JSX.Element { {(favs) => ( <>
- + {(favorite) => ( )} @@ -217,8 +217,15 @@ export function Fave(props: { api: Api.Client }): JSX.Element { ); } -function FavoriteRoom(props: { room: string; createdAt: number; onRemove: (room: string) => void }): JSX.Element { +function FavoriteRoom(props: { + room: string; + createdAt: number; + onRemove: (room: string) => void; + connection: Connection; + api: Api.Client; +}): JSX.Element { const [removing, setRemoving] = createSignal(false); + const [memberCount, setMemberCount] = createSignal(0); const handleRemove = async (e: MouseEvent) => { e.preventDefault(); @@ -229,22 +236,32 @@ function FavoriteRoom(props: { room: string; createdAt: number; onRemove: (room: return (
- -

{props.room}

-
- +
+ +

{props.room}

+
+
+ 0}> + + {memberCount()} + + + +
+
+
); } diff --git a/app/src/preview.tsx b/app/src/preview.tsx index 8704e5de..558a5ddb 100644 --- a/app/src/preview.tsx +++ b/app/src/preview.tsx @@ -2,7 +2,7 @@ import * as Api from "@hang/api/client"; import { Connection, Preview } from "@kixelated/hang"; import { Path } from "@kixelated/moq"; import solid from "@kixelated/signals/solid"; -import { For, onCleanup, Show } from "solid-js"; +import { createEffect, For, onCleanup, Show } from "solid-js"; import type { JSX } from "solid-js/jsx-runtime"; import { createStore } from "solid-js/store"; import IconChat from "~icons/mdi/message-text"; @@ -10,8 +10,100 @@ import IconMicrophone from "~icons/mdi/microphone"; import IconVideo from "~icons/mdi/video"; import IconVolumeHigh from "~icons/mdi/volume-high"; -export function PreviewRoom(props: { connection: Connection; room: string; api: Api.Client }): JSX.Element { - const room = new Preview.Room(props.connection, { enabled: true }); +export function PreviewRoomCompact(props: { + connection: Connection; + path?: string; + api: Api.Client; + onMemberCountChange?: (count: number) => void; +}): JSX.Element { + const room = new Preview.Room(props.connection, { + name: props.path ? Path.from(props.path) : undefined, + enabled: true, + }); + onCleanup(() => room.close()); + + const [members, setMembers] = createStore<{ [name: Path.Valid]: Preview.Member | undefined }>({}); + + room.onMember((name, member) => { + setMembers(name, member ?? undefined); + }); + + // Track member count changes + createEffect(() => { + const count = Object.values(members).filter(Boolean).length; + props.onMemberCountChange?.(count); + }); + + const memberList = () => Object.values(members).filter(Boolean); + + // Only show if there are members + return ( + 0}> +
+ + {(member) => {(member) => }} + +
+
+ ); +} + +function PreviewMemberCompact(props: { member: Preview.Member }): JSX.Element { + const info = solid(props.member.info); + return ( + + {(info) => ( +
+
+ {info().name} +
+ + +
+ +
+
+
+
{info().name}
+
+ + + + + + + + + + + + +
+
+
+ )} + + ); +} + +export function PreviewRoom(props: { connection: Connection; path?: string; api: Api.Client }): JSX.Element { + const room = new Preview.Room(props.connection, { + name: props.path ? Path.from(props.path) : undefined, + enabled: true, + }); onCleanup(() => room.close()); const [members, setMembers] = createStore<{ [name: Path.Valid]: Preview.Member | undefined }>({}); @@ -37,7 +129,7 @@ export function PreviewRoom(props: { connection: Connection; room: string; api: >
- {(member) => {(member) => }} + {(member) => {(member) => }}
@@ -45,7 +137,7 @@ export function PreviewRoom(props: { connection: Connection; room: string; api: ); } -function RoomMember(props: { member: Preview.Member }): JSX.Element { +function PreviewMember(props: { member: Preview.Member }): JSX.Element { const info = solid(props.member.info); return ( {(info) => (
@@ -88,12 +179,18 @@ function RoomMember(props: { member: Preview.Member }): JSX.Element { class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110" />
- -
- +
+ +
-
-
+ +
diff --git a/app/src/sup.tsx b/app/src/sup.tsx index bed26dda..0f49db11 100644 --- a/app/src/sup.tsx +++ b/app/src/sup.tsx @@ -142,7 +142,7 @@ function Preview(props: {
{/* Left Column: Participants List */}
- +
{/* Right Column: Avatar/Name Preview */} diff --git a/moq b/moq index f283d57b..73934315 160000 --- a/moq +++ b/moq @@ -1 +1 @@ -Subproject commit f283d57bd946a5f5f27ec1c69f9d433b2a2f7fad +Subproject commit 73934315db7f95b2fa29b96268017d8e1c15cdf0 From 6b2f5bf255e31e6a7712f41a23fccc6fdc303462 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 12 Aug 2025 14:38:40 -0700 Subject: [PATCH 3/6] Works I think. --- api/.dev.vars | 3 +++ api/justfile | 2 +- api/src/room.ts | 17 +++++------------ api/worker-configuration.d.ts | 4 ++-- api/wrangler.jsonc | 4 ++-- justfile | 5 ++++- moq | 2 +- 7 files changed, 18 insertions(+), 19 deletions(-) diff --git a/api/.dev.vars b/api/.dev.vars index 19f87d54..067770b5 100644 --- a/api/.dev.vars +++ b/api/.dev.vars @@ -3,6 +3,9 @@ AUTH_SECRET="dev-secret-change-in-production-must-be-32-chars-min" GOOGLE_CLIENT_SECRET="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" DISCORD_CLIENT_SECRET="uJRHl-pmOFYHvrViHJdcs2slyTCnoJog" + # PEM but without newlines or the header/trailer APPLE_CLIENT_SECRET="MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgRn72O3lOuG5Op8/2uGxkx3XlRcONRYpggQMGwCQd30KgCgYIKoZIzj0DAQehRANCAASw/36zB3txwM6YM70NwxSTqDVF1kRhFDIlAaI0Q7nKQNzx6kJ4Cgl7+iOApnCw7gbNuLQ4ULynk5QIB3lGUhGQ" + +# Set by just after it has been generated. RELAY_SECRET="" diff --git a/api/justfile b/api/justfile index 38995e70..c737341d 100644 --- a/api/justfile +++ b/api/justfile @@ -13,7 +13,7 @@ dev: # Generate SSL certificates for API development mkdir -p dev mkcert -cert-file dev/api-cert.pem -key-file dev/api-key.pem localhost api.hang.dev - pnpm wrangler dev --host api.hang.dev --local-protocol https --https-key-path dev/api-key.pem --https-cert-path dev/api-cert.pem + pnpm wrangler dev --var "RELAY_SECRET:$(cat ../moq/rs/dev/root.jwk)" --host api.hang.dev --local-protocol https --https-key-path dev/api-key.pem --https-cert-path dev/api-cert.pem build: pnpm i diff --git a/api/src/room.ts b/api/src/room.ts index 735757f6..278f243f 100644 --- a/api/src/room.ts +++ b/api/src/room.ts @@ -11,36 +11,29 @@ export type Name = z.infer; // TODO: Add proper type for Env.MOQ_JWK in worker-configuration.d.ts if needed export class Context { - #key?: Token.Key; + #key: Token.Key; #env: Env; constructor(env: Env) { - this.#key = env.RELAY_SECRET ? Token.load(env.RELAY_SECRET) : undefined; + this.#key = Token.load(env.RELAY_SECRET); this.#env = env; } // Returns the URL to join the room async sign(room: Name, account: Account.Id): Promise { const root = `${this.#env.RELAY_PREFIX}/${room}`; - if (!this.#key) { - return new URL(root, this.#env.RELAY_URL); - } - - const token = await Token.sign(this.#key, { root, put: account }); + // TODO add a field to force publishing, preventing someone from lurking. + const token = await Token.sign(this.#key, { root, get: "", put: account }); return new URL(`${root}/?jwt=${token}`, this.#env.RELAY_URL); } // Returns a URL to preview a list of rooms async signPreview(rooms: Name[]): Promise { const root = this.#env.RELAY_PREFIX; - if (!this.#key) { - // NOTE: On local dev with no key configured, this will be a firehose of all announcements - return new URL(root, this.#env.RELAY_URL); - } - const token = await Token.sign(this.#key, { root, get: rooms, + // no put permission }); return new URL(`${root}/?jwt=${token}`, this.#env.RELAY_URL); } diff --git a/api/worker-configuration.d.ts b/api/worker-configuration.d.ts index a8ac98b6..f483a019 100644 --- a/api/worker-configuration.d.ts +++ b/api/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 0fe5bdade492aa84f191db1d2d21b3ec) +// Generated by Wrangler by running `wrangler types` (hash: c429e33dff07f896846f4a5a45b21840) // Runtime types generated with workerd@1.20250709.0 2025-07-09 declare namespace Cloudflare { interface Env { @@ -12,7 +12,7 @@ declare namespace Cloudflare { APPLE_KEY_ID: "964W676BWT" | "7BQ2ZQY943" | "Q7CN38JH2Z"; R2_PUBLIC_URL: "https://api.hang.dev:3000/public" | "https://public.hang.now" | "https://public.hang.live"; RELAY_URL: "http://localhost:4443" | "https://relay.quic.video"; - RELAY_PREFIX: "anon" | "staging" | "live"; + RELAY_PREFIX: "demo" | "staging" | "live"; AUTH_SECRET: string; GOOGLE_CLIENT_SECRET: string; DISCORD_CLIENT_SECRET: string; diff --git a/api/wrangler.jsonc b/api/wrangler.jsonc index f6903c11..2eef673c 100644 --- a/api/wrangler.jsonc +++ b/api/wrangler.jsonc @@ -60,8 +60,8 @@ "R2_PUBLIC_URL": "https://api.hang.dev:3000/public", "RELAY_URL": "http://localhost:4443", - "RELAY_PREFIX": "anon" - // "RELAY_SECRET": + "RELAY_PREFIX": "demo" + // "RELAY_SECRET": }, // Staging environment overrides diff --git a/justfile b/justfile index 38159ab8..f7088b0d 100644 --- a/justfile +++ b/justfile @@ -69,8 +69,11 @@ deploy env="staging": dev: pnpm -r i + # Generate auth tokens if needed + @cd moq/rs && just auth-token + pnpm concurrently --kill-others --names api,app,native,relay --prefix-colors auto \ "just --justfile api/justfile dev" \ "just --justfile app/justfile dev" \ "just --justfile native/justfile dev" \ - "just --justfile moq/justfile relay" + "just --justfile moq/justfile root" diff --git a/moq b/moq index 73934315..1483517b 160000 --- a/moq +++ b/moq @@ -1 +1 @@ -Subproject commit 73934315db7f95b2fa29b96268017d8e1c15cdf0 +Subproject commit 1483517ba771214e13bc21cbaf8bb816fcd05691 From 20ed367d5b1b80a786f3e288d8f35a8579b712be Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 12 Aug 2025 16:34:54 -0700 Subject: [PATCH 4/6] wider. --- app/src/layout/web.tsx | 2 +- moq | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/layout/web.tsx b/app/src/layout/web.tsx index 168e352b..781206d5 100644 --- a/app/src/layout/web.tsx +++ b/app/src/layout/web.tsx @@ -10,7 +10,7 @@ import { Logo } from "./logo"; export default function Web(props: { children: JSX.Element }) { return ( -
+
diff --git a/moq b/moq index 1483517b..5c51ae16 160000 --- a/moq +++ b/moq @@ -1 +1 @@ -Subproject commit 1483517ba771214e13bc21cbaf8bb816fcd05691 +Subproject commit 5c51ae16f10b754fe19e402901fe97f2d0f30a67 From 995c70a0f9b7d7ad593e60d9ba050229486809b3 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 12 Aug 2025 16:49:36 -0700 Subject: [PATCH 5/6] update moq --- moq | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moq b/moq index 5c51ae16..bee49c05 160000 --- a/moq +++ b/moq @@ -1 +1 @@ -Subproject commit 5c51ae16f10b754fe19e402901fe97f2d0f30a67 +Subproject commit bee49c05d621d91f306d7f361d2f83de85aef5a1 From 165d05d32c48d31312927191294937fc9e443681 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 14 Aug 2025 10:49:15 -0700 Subject: [PATCH 6/6] Tweak the scale a bit. --- api/worker-configuration.d.ts | 4 ++-- api/wrangler.jsonc | 4 ++-- app/justfile | 2 +- app/src/about.tsx | 2 +- app/src/room/index.ts | 2 ++ app/src/room/space.ts | 12 +++++------- moq | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/api/worker-configuration.d.ts b/api/worker-configuration.d.ts index f483a019..7f7340e3 100644 --- a/api/worker-configuration.d.ts +++ b/api/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: c429e33dff07f896846f4a5a45b21840) +// Generated by Wrangler by running `wrangler types` (hash: a31fbac4d97e8636af353491409d82e0) // Runtime types generated with workerd@1.20250709.0 2025-07-09 declare namespace Cloudflare { interface Env { @@ -11,7 +11,7 @@ declare namespace Cloudflare { APPLE_TEAM_ID: "D7D5SDDB5Z"; APPLE_KEY_ID: "964W676BWT" | "7BQ2ZQY943" | "Q7CN38JH2Z"; R2_PUBLIC_URL: "https://api.hang.dev:3000/public" | "https://public.hang.now" | "https://public.hang.live"; - RELAY_URL: "http://localhost:4443" | "https://relay.quic.video"; + RELAY_URL: "http://localhost:4443" | "https://relay.moq.dev"; RELAY_PREFIX: "demo" | "staging" | "live"; AUTH_SECRET: string; GOOGLE_CLIENT_SECRET: string; diff --git a/api/wrangler.jsonc b/api/wrangler.jsonc index 2eef673c..e87fe07e 100644 --- a/api/wrangler.jsonc +++ b/api/wrangler.jsonc @@ -104,7 +104,7 @@ "R2_PUBLIC_URL": "https://public.hang.now", - "RELAY_URL": "https://relay.quic.video", + "RELAY_URL": "https://relay.moq.dev", "RELAY_PREFIX": "staging" // "RELAY_SECRET": npx wrangler secret put RELAY_SECRET --env staging } @@ -148,7 +148,7 @@ "R2_PUBLIC_URL": "https://public.hang.live", - "RELAY_URL": "https://relay.quic.video", + "RELAY_URL": "https://relay.moq.dev", "RELAY_PREFIX": "live" // "RELAY_SECRET": npx wrangler secret put RELAY_SECRET --env live } diff --git a/app/justfile b/app/justfile index f4d0574f..65ace114 100644 --- a/app/justfile +++ b/app/justfile @@ -34,7 +34,7 @@ fix: pnpm exec eslint . --fix # Make sure the JS packages are not vulnerable - pnpm exec pnpm audit --fix + # pnpm exec pnpm audit --fix # Run any CI tests test: diff --git a/app/src/about.tsx b/app/src/about.tsx index 81946e9f..94743252 100644 --- a/app/src/about.tsx +++ b/app/src/about.tsx @@ -174,7 +174,7 @@ export function About(): JSX.Element {
{canvas}

For the nerds in the audience, this site uses bleeding edge web technologies. We're using{" "} - Media over QUIC which is an{" "} + Media over QUIC which is an{" "} open source WebRTC alternative. This ain't your usual Zoom clone.

diff --git a/app/src/room/index.ts b/app/src/room/index.ts index 0c8e9f11..61f7ec3f 100644 --- a/app/src/room/index.ts +++ b/app/src/room/index.ts @@ -147,6 +147,8 @@ export class Room { constraints: { frameRate: { ideal: 60 }, resizeMode: "none", + width: { max: 1920 }, + height: { max: 1080 }, }, }, user: { diff --git a/app/src/room/space.ts b/app/src/room/space.ts index 52b51b0c..70d518fd 100644 --- a/app/src/room/space.ts +++ b/app/src/room/space.ts @@ -627,17 +627,15 @@ export class Space { return; } - const canvasArea = this.canvas.viewport.peek().area(); + const canvas = this.canvas.viewport.peek(); + const total = (canvas.x + canvas.y) / 2; - let broadcastArea = 0; + let covered = 0; for (const broadcast of broadcasts) { - broadcastArea += broadcast.video.targetSize.x * broadcast.video.targetSize.y; + covered += (broadcast.video.targetSize.x + broadcast.video.targetSize.y) / 2; } - const fillRatio = broadcastArea / canvasArea; - const targetFill = 0.25; - - this.#scale = Math.min(Math.sqrt(targetFill / fillRatio), 1.5); + this.#scale = Math.min(total / covered / 2, window.devicePixelRatio); } close() { diff --git a/moq b/moq index bee49c05..0c243506 160000 --- a/moq +++ b/moq @@ -1 +1 @@ -Subproject commit bee49c05d621d91f306d7f361d2f83de85aef5a1 +Subproject commit 0c243506cdbda9218c63c36831cd88d1c2b41ff2