1- import { type Catalog , Publish , Watch } from "@kixelated/hang" ;
1+ import { Publish , Watch } from "@kixelated/hang" ;
22import { Effect , Signal } from "@kixelated/signals" ;
3- import { Audio , type AudioProps } from "./audio" ;
3+ import { Audio } from "./audio" ;
44import { Canvas } from "./canvas" ;
55import { Captions } from "./captions" ;
66import { Chat } from "./chat" ;
@@ -19,12 +19,6 @@ export type ChatMessage = {
1919 expires : DOMHighResTimeStamp ;
2020} ;
2121
22- export type BroadcastProps = {
23- audio ?: AudioProps ;
24- position ?: Catalog . Position ;
25- visible ?: boolean ;
26- } ;
27-
2822// Catalog.Position but all fields are required.
2923type Position = {
3024 x : number ;
@@ -33,6 +27,13 @@ type Position = {
3327 s : number ;
3428} ;
3529
30+ export interface BroadcastProps < T extends BroadcastSource = BroadcastSource > {
31+ source : T ;
32+ canvas : Canvas ;
33+ sound : Sound ;
34+ scale : Signal < number > ;
35+ }
36+
3637export class Broadcast < T extends BroadcastSource = BroadcastSource > {
3738 source : T ;
3839 canvas : Canvas ;
@@ -46,7 +47,6 @@ export class Broadcast<T extends BroadcastSource = BroadcastSource> {
4647 message = new Signal < HTMLElement | undefined > ( undefined ) ;
4748
4849 bounds : Signal < Bounds > ; // 0 to canvas
49- scale = 1.0 ; // 1 is 100%
5050 velocity = Vector . create ( 0 , 0 ) ; // in pixels per ?
5151
5252 // Replaced by position
@@ -64,31 +64,35 @@ export class Broadcast<T extends BroadcastSource = BroadcastSource> {
6464 meme = new Signal < HTMLVideoElement | HTMLAudioElement | undefined > ( undefined ) ;
6565 memeName = new Signal < string | undefined > ( undefined ) ;
6666
67+ scale : Signal < number > ; // room scale, 1 is 100%
68+ zoom = new Signal < number > ( 1.0 ) ; // local zoom, 1 is 100%
69+
6770 // Show a locator arrow for 8 seconds to show our position on join.
6871 #locatorStart?: DOMHighResTimeStamp ;
6972
7073 signals = new Effect ( ) ;
7174
72- constructor ( source : T , canvas : Canvas , sound : Sound , props ?: BroadcastProps ) {
73- this . source = source ;
74- this . canvas = canvas ;
75- this . visible = new Signal ( props ?. visible ?? true ) ;
75+ constructor ( props : BroadcastProps < T > ) {
76+ this . source = props . source ;
77+ this . canvas = props . canvas ;
78+ this . visible = new Signal ( true ) ; // TODO
79+ this . scale = props . scale ;
7680
7781 // Unless provided, start them at the center of the screen with a tiiiiny bit of variance to break ties.
7882 const start = ( ) => ( Math . random ( ) - 0.5 ) / 100 ;
7983 const position = {
80- x : props ?. position ?. x ?? start ( ) ,
81- y : props ?. position ?. y ?? start ( ) ,
82- z : props ?. position ?. z ?? 0 ,
83- s : props ?. position ?. s ?? 1 ,
84+ x : start ( ) ,
85+ y : start ( ) ,
86+ z : 0 ,
87+ s : 1 ,
8488 } ;
8589
8690 this . position = new Signal ( position ) ;
8791
8892 this . video = new Video ( this ) ;
89- this . audio = new Audio ( this , sound , props ?. audio ) ;
90- this . chat = new Chat ( this , canvas ) ;
91- this . captions = new Captions ( this , canvas ) ;
93+ this . audio = new Audio ( this , props . sound ) ;
94+ this . chat = new Chat ( this , props . canvas ) ;
95+ this . captions = new Captions ( this , props . canvas ) ;
9296
9397 const viewport = this . canvas . viewport . peek ( ) ;
9498
@@ -101,36 +105,38 @@ export class Broadcast<T extends BroadcastSource = BroadcastSource> {
101105 // Normalize to find the closest edge of the screen.
102106 startPosition = startPosition . normalize ( ) . mult ( viewport . length ( ) ) . add ( viewport . div ( 2 ) ) ;
103107
104- this . bounds = new Signal ( new Bounds ( startPosition , this . video . targetSize ) ) ;
105-
106- // Load the broadcaster's position from the network.
107- this . signals . effect ( ( effect ) => {
108- if ( ! effect . get ( this . visible ) ) {
109- // Change the target position to somewhere outside the screen.
110- this . position . update ( ( prev ) => {
111- const offscreen = Vector . create ( prev . x , prev . y ) . normalize ( ) . mult ( 2 ) ;
112- return { ...prev , x : offscreen . x , y : offscreen . y } ;
113- } ) ;
114-
115- return ;
116- }
108+ this . bounds = new Signal ( new Bounds ( startPosition , this . video . targetSize . peek ( ) ) ) ;
117109
118- // Update the target position from the network.
119- const location = effect . get ( this . source . location . window . position ) ;
120- if ( ! location ) return ;
110+ this . signals . effect ( this . #runLocation. bind ( this ) ) ;
111+ this . signals . effect ( this . #runChat. bind ( this ) ) ;
112+ this . signals . effect ( this . #runTarget. bind ( this ) ) ;
113+ }
121114
115+ // Load the broadcaster's position from the network.
116+ #runLocation( effect : Effect ) {
117+ if ( ! effect . get ( this . visible ) ) {
118+ // Change the target position to somewhere outside the screen.
122119 this . position . update ( ( prev ) => {
123- return {
124- ...prev ,
125- x : location . x ?? prev . x ,
126- y : location . y ?? prev . y ,
127- z : location . z ?? prev . z ,
128- s : location . s ?? prev . s ,
129- } ;
120+ const offscreen = Vector . create ( prev . x , prev . y ) . normalize ( ) . mult ( 2 ) ;
121+ return { ...prev , x : offscreen . x , y : offscreen . y } ;
130122 } ) ;
131- } ) ;
132123
133- this . signals . effect ( this . #runChat. bind ( this ) ) ;
124+ return ;
125+ }
126+
127+ // Update the target position from the network.
128+ const location = effect . get ( this . source . location . window . position ) ;
129+ if ( ! location ) return ;
130+
131+ this . position . update ( ( prev ) => {
132+ return {
133+ ...prev ,
134+ x : location . x ?? prev . x ,
135+ y : location . y ?? prev . y ,
136+ z : location . z ?? prev . z ,
137+ s : location . s ?? prev . s ,
138+ } ;
139+ } ) ;
134140 }
135141
136142 #runChat( effect : Effect ) {
@@ -165,8 +171,29 @@ export class Broadcast<T extends BroadcastSource = BroadcastSource> {
165171 } ) ;
166172 }
167173
174+ // Decides the simulcast size to use based on the number of pixels.
175+ #runTarget( effect : Effect ) {
176+ if ( ! ( this . source instanceof Watch . Broadcast ) ) return ;
177+
178+ const catalog = effect . get ( this . source . video . catalog ) ;
179+ if ( ! catalog ) return ;
180+
181+ for ( const rendition of catalog ) {
182+ if ( ! rendition . config . displayAspectHeight || ! rendition . config . displayAspectWidth ) continue ;
183+
184+ const pixels = rendition . config . displayAspectHeight * rendition . config . displayAspectWidth ;
185+ const scale = effect . get ( this . scale ) ;
186+ const zoom = effect . get ( this . zoom ) ;
187+
188+ const scaled = pixels * scale * zoom ;
189+ effect . set ( this . source . video . target , { pixels : scaled } ) ;
190+
191+ return ;
192+ }
193+ }
194+
168195 // TODO Also make scale a signal
169- tick ( scale : number ) {
196+ tick ( ) {
170197 this . video . tick ( ) ;
171198
172199 const bounds = this . bounds . peek ( ) ;
@@ -213,8 +240,12 @@ export class Broadcast<T extends BroadcastSource = BroadcastSource> {
213240 }
214241
215242 // Apply everything now.
216- const targetSize = this . video . targetSize . mult ( this . scale * scale ) ;
217- this . scale += ( targetPosition . s - this . scale ) * 0.1 ;
243+ const targetSize = this . video . targetSize . peek ( ) . mult ( this . zoom . peek ( ) * this . scale . peek ( ) ) ;
244+
245+ const dz = ( targetPosition . s - this . zoom . peek ( ) ) * 0.1 ;
246+ if ( Math . abs ( dz ) >= 0.002 ) {
247+ this . zoom . update ( ( prev ) => prev + dz ) ;
248+ }
218249
219250 // Apply the velocity and size.
220251 const dx = this . velocity . x / 50 ;
@@ -275,9 +306,9 @@ export class Broadcast<T extends BroadcastSource = BroadcastSource> {
275306 ctx . globalAlpha *= alpha ;
276307
277308 // Calculate arrow position and animation
278- const arrowSize = 12 * this . scale ;
309+ const arrowSize = 12 * this . zoom . peek ( ) ;
279310 const pulseScale = 1 + Math . sin ( now / 500 ) * 0.1 ; // Subtle pulsing effect
280- const offset = 10 * this . scale ;
311+ const offset = 10 * this . zoom . peek ( ) ;
281312
282313 const gap = 2 * ( arrowSize + offset ) ;
283314
@@ -294,14 +325,14 @@ export class Broadcast<T extends BroadcastSource = BroadcastSource> {
294325 ctx . closePath ( ) ;
295326
296327 // Style the arrow
297- ctx . lineWidth = 4 * this . scale ;
328+ ctx . lineWidth = 4 * this . zoom . peek ( ) ;
298329 ctx . strokeStyle = "#000" ; // Gold color
299330 ctx . fillStyle = "#FFD700" ;
300331 ctx . stroke ( ) ;
301332 ctx . fill ( ) ;
302333
303334 // Draw "YOU" text
304- const fontSize = Math . round ( 32 * this . scale ) ; // round to avoid busting font caches
335+ const fontSize = Math . round ( 32 * this . zoom . peek ( ) ) ; // round to avoid busting font caches
305336 ctx . font = `bold ${ fontSize } px Arial` ;
306337 ctx . textAlign = "center" ;
307338 ctx . textBaseline = "middle" ;
0 commit comments