instbyte 1.9.4 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,11 +21,13 @@
21
21
  npx instbyte
22
22
  ```
23
23
 
24
- **Instbyte** is a high-speed, real-time, short-lived LAN sharing utility built for teams and developers who need to move snippets, links, files, and structured notes across devices instantly — without cloud accounts, logins, or external services.
24
+ You're three feet from your teammate. *A quick share* still leaves your building before it reaches them.
25
25
 
26
- It operates entirely on your local network, acting as a lightweight "digital dead-drop" for frictionless collaboration.
26
+ Your chat app is for conversation. Your cloud storage is for files. Your email is for async.
27
27
 
28
- Instbyte can also be fully white-labelled the difference between ***a tool you use*** and ***a tool you own***.
28
+ None of them are for *right now, on this network, gone tomorrow.*
29
+
30
+ That's what **Instbyte** is for.
29
31
 
30
32
  ---
31
33
 
@@ -102,147 +104,19 @@ For teams who want auth, custom retention, or branding — create `instbyte.conf
102
104
 
103
105
  Then run `npx instbyte` in the same directory. The config is picked up automatically.
104
106
 
105
- ### Keeping It Running
106
- For persistent team use, run it as a background process:
107
-
108
- ```bash
109
- # using pm2
110
- npm install -g pm2
111
- pm2 start "npx instbyte" --name instbyte
112
- pm2 save
113
- ```
114
-
115
- Or use any process manager you already have — systemd, screen, tmux.
107
+ For persistent deployment options including pm2, systemd, and Docker, see the [Deployment Guide](docs/deployment.md).
116
108
 
117
109
  ---
118
110
 
119
111
  ## Docker
120
112
 
121
- The fastest way to run Instbyte with Docker:
122
- ```bash
123
- docker compose up -d
124
- ```
125
-
126
- Or with plain Docker:
127
- ```bash
128
- docker run -d \
129
- -p 3000:3000 \
130
- -v $(pwd)/instbyte-data:/data \
131
- -e INSTBYTE_DATA=/data \
132
- -e INSTBYTE_UPLOADS=/data/uploads \
133
- --name instbyte \
134
- mohitgauniyal/instbyte
135
- ```
136
-
137
- Data persists in `./instbyte-data` on your host. The same folder used by `npx instbyte` — so switching between the two preserves all your data.
138
-
139
- ### With a config file
140
-
141
- Mount your config file into the container:
142
- ```yaml
143
- services:
144
- instbyte:
145
- image: mohitgauniyal/instbyte
146
- ports:
147
- - "3000:3000"
148
- volumes:
149
- - ./instbyte-data:/data
150
- - ./instbyte.config.json:/app/instbyte.config.json
151
- environment:
152
- - INSTBYTE_DATA=/data
153
- - INSTBYTE_UPLOADS=/data/uploads
154
- restart: unless-stopped
155
- ```
156
-
157
- ### Changing the port
158
-
159
- Edit the host port in `docker-compose.yml`:
160
- ```yaml
161
- ports:
162
- - "8080:3000" # now runs on port 8080
163
- ```
164
-
165
- > **Note:** File uploads may not work correctly on Windows Docker Desktop due to network limitations. For Windows, use `npx instbyte` directly or deploy on a Linux server.
113
+ For Docker setup, persistent data, and config file mounting, see [Deployment Guide](docs/deployment.md).
166
114
 
167
115
  ---
168
116
 
169
117
  ## Reverse Proxy
170
118
 
171
- For teams who want to access Instbyte over HTTPS or from outside their local network, running it behind a reverse proxy is the standard approach.
172
-
173
- > **Important:** Instbyte uses WebSockets for real-time sync. Your proxy must be configured to forward WebSocket connections — otherwise the app will load but live updates will stop working.
174
-
175
- ### Nginx
176
- ```nginx
177
- server {
178
- listen 80;
179
- server_name instbyte.yourdomain.com;
180
-
181
- # Redirect HTTP to HTTPS
182
- return 301 https://$host$request_uri;
183
- }
184
-
185
- server {
186
- listen 443 ssl;
187
- server_name instbyte.yourdomain.com;
188
-
189
- ssl_certificate /etc/letsencrypt/live/instbyte.yourdomain.com/fullchain.pem;
190
- ssl_certificate_key /etc/letsencrypt/live/instbyte.yourdomain.com/privkey.pem;
191
-
192
- # Increase max upload size to match Instbyte's limit
193
- client_max_body_size 2G;
194
-
195
- location / {
196
- proxy_pass http://localhost:3000;
197
- proxy_http_version 1.1;
198
-
199
- # Required for WebSocket support
200
- proxy_set_header Upgrade $http_upgrade;
201
- proxy_set_header Connection "upgrade";
202
-
203
- proxy_set_header Host $host;
204
- proxy_set_header X-Real-IP $remote_addr;
205
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
206
- proxy_set_header X-Forwarded-Proto $scheme;
207
-
208
- # Prevent proxy timeouts on large uploads
209
- proxy_read_timeout 300s;
210
- proxy_send_timeout 300s;
211
- }
212
- }
213
- ```
214
-
215
- Get a free SSL certificate with Certbot:
216
- ```bash
217
- certbot --nginx -d instbyte.yourdomain.com
218
- ```
219
-
220
- ### Caddy
221
-
222
- Caddy automatically handles HTTPS certificates — no Certbot needed.
223
- ```caddy
224
- instbyte.yourdomain.com {
225
- reverse_proxy localhost:3000
226
- }
227
- ```
228
-
229
- Caddy handles WebSocket forwarding and HTTPS automatically. That's all you need.
230
-
231
- ### With Docker
232
-
233
- If running Instbyte via Docker, proxy to the mapped host port:
234
- ```nginx
235
- proxy_pass http://localhost:3000;
236
- ```
237
-
238
- Or use Docker's internal network — replace `localhost` with the container name:
239
- ```nginx
240
- proxy_pass http://instbyte:3000;
241
- ```
242
-
243
- ### Keeping it LAN-only
244
-
245
- If you don't want external access but still want HTTPS on your local network, tools like [Tailscale](https://tailscale.com) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) are good options that require no open ports.
119
+ For Nginx, Caddy, and HTTPS setup, see [Deployment Guide](docs/deployment.md).
246
120
 
247
121
  ---
248
122
 
@@ -305,6 +179,8 @@ The difference between *a tool you use* and *a tool you own.*
305
179
 
306
180
  **Presence awareness** — see how many people are currently connected in real time.
307
181
 
182
+ **Live broadcast** — share your screen in real time with everyone on the network. Viewers join instantly from their browser, no plugins or installs needed. Built on WebRTC for smooth, low-latency video. Includes mic audio, viewer mute/unmute, raise hand, and screen capture to channel.
183
+
308
184
  **Read receipts** — see how many devices have viewed each shared item. Updates live as teammates open the page.
309
185
 
310
186
  **Item management** — add optional titles to label any item for future reference. Edit text items inline without deleting and re-pasting. Pinned items are protected from both manual deletion and auto-cleanup.
@@ -315,8 +191,19 @@ The difference between *a tool you use* and *a tool you own.*
315
191
 
316
192
  ---
317
193
 
194
+ ## Broadcasting
195
+
196
+ One person shares their screen — everyone else on the network watches live in their browser. No plugins, no accounts, no external services. Built on WebRTC.
197
+
198
+ Viewers can save the current frame to a channel, raise a hand to notify the broadcaster, and toggle audio from the panel.
199
+
200
+ For HTTPS setup and advanced network configuration, see the [Deployment Guide](docs/deployment.md#broadcasting).
201
+
202
+ ---
203
+
318
204
  ## Keyboard Shortcuts
319
205
 
206
+ ```
320
207
  | Key | Action |
321
208
  |---|---|
322
209
  | `/` | Focus search |
@@ -346,40 +233,13 @@ node server/server.js
346
233
  - Home lab file sharing without setting up NAS or cloud sync
347
234
  - Piping build logs or stack traces from CI or terminal directly into a shared channel
348
235
  - Sharing sensitive credentials or config files over LAN without leaving a cloud trail
236
+ - Live screen sharing during standups, design reviews, or debugging sessions — no Zoom link needed
349
237
 
350
238
  ---
351
239
 
352
240
  ## Terminal Usage
353
241
 
354
- Since Instbyte exposes a simple HTTP API, you can push content directly from your terminal using `curl` — no browser needed.
355
-
356
- **Send a log file:**
357
- ```bash
358
- curl -X POST http://192.168.x.x:3000/text \
359
- -H "Content-Type: application/json" \
360
- -d "{\"content\": \"$(cat error.log)\", \"channel\": \"general\", \"uploader\": \"terminal\"}"
361
- ```
362
-
363
- **Pipe command output directly:**
364
- ```bash
365
- npm run build 2>&1 | curl -X POST http://192.168.x.x:3000/text \
366
- -H "Content-Type: application/json" \
367
- --data-binary @- \
368
- -H "X-Channel: general" \
369
- -H "X-Uploader: CI"
370
- ```
371
-
372
- **Upload a file from the terminal:**
373
- ```bash
374
- curl -X POST http://192.168.x.x:3000/upload \
375
- -F "file=@./build.log" \
376
- -F "channel=general" \
377
- -F "uploader=terminal"
378
- ```
379
-
380
- Replace `192.168.x.x:3000` with the URL shown when Instbyte starts. If auth is enabled, add `-b "instbyte_auth=your-token"` to each request.
381
-
382
- Useful for piping stack traces, build logs, or environment dumps straight into a channel your whole team can see instantly.
242
+ Push content from your terminal using curl — no browser needed. See [Terminal Usage Guide](docs/terminal-usage.md).
383
243
 
384
244
  ---
385
245
 
@@ -393,7 +253,7 @@ Instbyte follows [Semantic Versioning](https://semver.org). See [Releases](https
393
253
 
394
254
  Instbyte is intentionally lightweight and LAN-first. If you want to extend it — CLI tools, themes, integrations — open an issue or submit a pull request.
395
255
 
396
- The codebase has a full test suite (184 tests across unit and integration). Run `npm test` before submitting anything. Issues tagged **good first issue** are a good starting point.
256
+ The codebase has a full test suite (195 tests across unit and integration). Run `npm test` before submitting anything. Issues tagged **good first issue** are a good starting point.
397
257
 
398
258
  ---
399
259
 
package/bin/instbyte.js CHANGED
@@ -11,18 +11,27 @@ const fs = require("fs");
11
11
  // When run via npx or global install, we want data to live
12
12
  // in the user's current working directory, not inside the
13
13
  // npm cache or global node_modules.
14
+ //
15
+ // When run via Docker, INSTBYTE_DATA and INSTBYTE_UPLOADS may
16
+ // already be set via environment variables — respect those and
17
+ // don't override them.
14
18
 
15
- const dataDir = path.join(process.cwd(), "instbyte-data");
16
- const uploadsDir = path.join(dataDir, "uploads");
19
+ if (!process.env.INSTBYTE_DATA) {
20
+ process.env.INSTBYTE_DATA = path.join(process.cwd(), "instbyte-data");
21
+ }
22
+
23
+ if (!process.env.INSTBYTE_UPLOADS) {
24
+ process.env.INSTBYTE_UPLOADS = path.join(process.env.INSTBYTE_DATA, "uploads");
25
+ }
26
+
27
+ const dataDir = process.env.INSTBYTE_DATA;
28
+ const uploadsDir = process.env.INSTBYTE_UPLOADS;
17
29
 
18
30
  if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
19
31
  if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });
20
32
 
21
- // Pass locations to the rest of the app via env vars
22
- process.env.INSTBYTE_DATA = dataDir;
23
- process.env.INSTBYTE_UPLOADS = uploadsDir;
24
-
25
33
  // ========================
26
34
  // BOOT
27
35
  // ========================
36
+ process.env.INSTBYTE_BOOT = '1';
28
37
  require("../server/server.js");
@@ -1079,6 +1079,264 @@ input[type=text]:focus {
1079
1079
  transition: background 1.5s ease, border-color 1.5s ease;
1080
1080
  }
1081
1081
 
1082
+ /* ============================================================
1083
+ BROADCAST
1084
+ ============================================================ */
1085
+
1086
+ /* Bar — sits below header, visible to all when live */
1087
+ .broadcast-bar {
1088
+ background: #111827;
1089
+ color: #fff;
1090
+ padding: 8px 20px;
1091
+ position: sticky;
1092
+ top: 0;
1093
+ z-index: 100;
1094
+ border-bottom: 1px solid #1f2937;
1095
+ }
1096
+
1097
+ .broadcast-bar-inner {
1098
+ max-width: 900px;
1099
+ margin: 0 auto;
1100
+ display: flex;
1101
+ align-items: center;
1102
+ gap: 10px;
1103
+ }
1104
+
1105
+ .broadcast-pulse {
1106
+ display: inline-block;
1107
+ width: 8px;
1108
+ height: 8px;
1109
+ background: #ef4444;
1110
+ border-radius: 50%;
1111
+ flex-shrink: 0;
1112
+ animation: broadcastPulse 1.5s ease-in-out infinite;
1113
+ }
1114
+
1115
+ @keyframes broadcastPulse {
1116
+
1117
+ 0%,
1118
+ 100% {
1119
+ opacity: 1;
1120
+ transform: scale(1);
1121
+ }
1122
+
1123
+ 50% {
1124
+ opacity: 0.4;
1125
+ transform: scale(0.85);
1126
+ }
1127
+ }
1128
+
1129
+ #broadcastLabel {
1130
+ font-size: 13px;
1131
+ font-weight: 500;
1132
+ flex: 1;
1133
+ }
1134
+
1135
+ .broadcast-viewers {
1136
+ font-size: 12px;
1137
+ color: #9ca3af;
1138
+ }
1139
+
1140
+ .broadcast-bar-actions {
1141
+ display: flex;
1142
+ gap: 8px;
1143
+ }
1144
+
1145
+ .broadcast-join-btn {
1146
+ background: #2563eb;
1147
+ color: #fff;
1148
+ border: none;
1149
+ border-radius: 6px;
1150
+ padding: 5px 14px;
1151
+ font-size: 13px;
1152
+ cursor: pointer;
1153
+ }
1154
+
1155
+ .broadcast-join-btn:hover {
1156
+ background: #1d4ed8;
1157
+ }
1158
+
1159
+ .broadcast-end-btn {
1160
+ background: #ef4444;
1161
+ color: #fff;
1162
+ border: none;
1163
+ border-radius: 6px;
1164
+ padding: 5px 14px;
1165
+ font-size: 13px;
1166
+ cursor: pointer;
1167
+ }
1168
+
1169
+ .broadcast-end-btn:hover {
1170
+ background: #dc2626;
1171
+ }
1172
+
1173
+ /* Start button in composer */
1174
+ .broadcast-start-btn {
1175
+ background: #374151;
1176
+ color: #fff;
1177
+ border: none;
1178
+ border-radius: 6px;
1179
+ padding: 10px 14px;
1180
+ font-size: 13px;
1181
+ cursor: pointer;
1182
+ white-space: nowrap;
1183
+ }
1184
+
1185
+ .broadcast-start-btn:hover {
1186
+ background: #4b5563;
1187
+ }
1188
+
1189
+ .broadcast-start-btn.is-live {
1190
+ background: #ef4444;
1191
+ }
1192
+
1193
+ .broadcast-start-btn.is-live:hover {
1194
+ background: #dc2626;
1195
+ }
1196
+
1197
+ /* ─── VIEWER PANEL ─────────────────────────────────────────── */
1198
+
1199
+ .viewer-panel {
1200
+ position: fixed;
1201
+ top: 60px;
1202
+ right: 20px;
1203
+ width: 480px;
1204
+ max-width: calc(100vw - 40px);
1205
+ background: #fff;
1206
+ border: 1px solid #e5e7eb;
1207
+ border-radius: 12px;
1208
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
1209
+ z-index: 500;
1210
+ display: flex;
1211
+ flex-direction: column;
1212
+ overflow: auto;
1213
+ resize: both;
1214
+ min-width: 280px;
1215
+ min-height: 200px;
1216
+ }
1217
+
1218
+ .viewer-panel-header {
1219
+ display: flex;
1220
+ align-items: center;
1221
+ justify-content: space-between;
1222
+ padding: 10px 14px;
1223
+ background: #f9fafb;
1224
+ border-bottom: 1px solid #e5e7eb;
1225
+ flex-shrink: 0;
1226
+ cursor: grab;
1227
+ user-select: none;
1228
+ }
1229
+
1230
+ .viewer-panel-header:active {
1231
+ cursor: grabbing;
1232
+ }
1233
+
1234
+ .viewer-minimize-btn {
1235
+ background: #f3f4f6;
1236
+ border: 1px solid #e5e7eb;
1237
+ border-radius: 6px;
1238
+ padding: 4px 10px;
1239
+ font-size: 12px;
1240
+ cursor: pointer;
1241
+ color: #374151;
1242
+ }
1243
+
1244
+ .viewer-minimize-btn:hover {
1245
+ background: #fef9c3;
1246
+ border-color: #fde68a;
1247
+ color: #92400e;
1248
+ }
1249
+
1250
+ .viewer-panel.minimized .viewer-frame,
1251
+ .viewer-panel.minimized .viewer-panel-actions .viewer-capture-btn,
1252
+ .viewer-panel.minimized .viewer-panel-actions .viewer-raise-btn,
1253
+ .viewer-panel.minimized .viewer-panel-actions .viewer-close-btn {
1254
+ display: none;
1255
+ }
1256
+
1257
+ .viewer-panel.minimized {
1258
+ min-height: unset;
1259
+ resize: none;
1260
+ }
1261
+
1262
+ #viewerLabel {
1263
+ font-size: 13px;
1264
+ font-weight: 600;
1265
+ color: #111827;
1266
+ }
1267
+
1268
+ .viewer-panel-actions {
1269
+ display: flex;
1270
+ gap: 6px;
1271
+ }
1272
+
1273
+ .viewer-capture-btn,
1274
+ .viewer-raise-btn,
1275
+ .viewer-close-btn {
1276
+ background: #f3f4f6;
1277
+ border: 1px solid #e5e7eb;
1278
+ border-radius: 6px;
1279
+ padding: 4px 10px;
1280
+ font-size: 12px;
1281
+ cursor: pointer;
1282
+ color: #374151;
1283
+ }
1284
+
1285
+ .viewer-capture-btn:hover {
1286
+ background: #dbeafe;
1287
+ border-color: #bfdbfe;
1288
+ color: #1d4ed8;
1289
+ }
1290
+
1291
+ .viewer-raise-btn:hover {
1292
+ background: #fef9c3;
1293
+ border-color: #fde68a;
1294
+ color: #92400e;
1295
+ }
1296
+
1297
+ .viewer-close-btn:hover {
1298
+ background: #fee2e2;
1299
+ border-color: #fecaca;
1300
+ color: #b91c1c;
1301
+ }
1302
+
1303
+ .viewer-frame {
1304
+ width: 100%;
1305
+ display: block;
1306
+ background: #0f1117;
1307
+ min-height: 180px;
1308
+ object-fit: contain;
1309
+ }
1310
+
1311
+ /* ─── RAISE HAND TOAST ─────────────────────────────────────── */
1312
+
1313
+ .raise-hand-toast {
1314
+ position: fixed;
1315
+ bottom: 80px;
1316
+ left: 50%;
1317
+ transform: translateX(-50%);
1318
+ background: #111827;
1319
+ color: #fff;
1320
+ padding: 8px 16px;
1321
+ border-radius: 20px;
1322
+ font-size: 13px;
1323
+ z-index: 9999;
1324
+ pointer-events: none;
1325
+ animation: raiseHandIn 0.2s ease;
1326
+ }
1327
+
1328
+ @keyframes raiseHandIn {
1329
+ from {
1330
+ opacity: 0;
1331
+ transform: translateX(-50%) translateY(8px);
1332
+ }
1333
+
1334
+ to {
1335
+ opacity: 1;
1336
+ transform: translateX(-50%) translateY(0);
1337
+ }
1338
+ }
1339
+
1082
1340
  /* ============================================================
1083
1341
  MOBILE
1084
1342
  ============================================================ */
@@ -1322,6 +1580,36 @@ input[type=text]:focus {
1322
1580
  transform: translateY(0);
1323
1581
  }
1324
1582
  }
1583
+
1584
+ .broadcast-bar {
1585
+ padding: 8px 12px;
1586
+ }
1587
+
1588
+ .broadcast-bar-inner {
1589
+ gap: 8px;
1590
+ }
1591
+
1592
+ #broadcastLabel {
1593
+ font-size: 12px;
1594
+ }
1595
+
1596
+ .broadcast-start-btn {
1597
+ display: none;
1598
+ }
1599
+
1600
+ .viewer-panel {
1601
+ position: fixed;
1602
+ top: auto;
1603
+ bottom: 0;
1604
+ left: 0;
1605
+ right: 0;
1606
+ width: 100%;
1607
+ max-width: 100%;
1608
+ border-radius: 16px 16px 0 0;
1609
+ resize: none;
1610
+ max-height: 85vh;
1611
+ }
1612
+
1325
1613
  }
1326
1614
 
1327
1615
 
@@ -1728,6 +2016,45 @@ input[type=text]:focus {
1728
2016
  background: #1a1d27 !important;
1729
2017
  color: #e5e7eb !important;
1730
2018
  }
2019
+
2020
+ .broadcast-bar {
2021
+ background: #0f1117;
2022
+ border-bottom-color: #1f2937;
2023
+ }
2024
+
2025
+ .broadcast-start-btn {
2026
+ background: #1f2937;
2027
+ }
2028
+
2029
+ .broadcast-start-btn:hover {
2030
+ background: #374151;
2031
+ }
2032
+
2033
+ .viewer-panel {
2034
+ background: #1e2433;
2035
+ border-color: #2d3748;
2036
+ }
2037
+
2038
+ .viewer-panel-header {
2039
+ background: #161b27;
2040
+ border-bottom-color: #2d3748;
2041
+ }
2042
+
2043
+ #viewerLabel {
2044
+ color: #f9fafb;
2045
+ }
2046
+
2047
+ .viewer-capture-btn,
2048
+ .viewer-raise-btn,
2049
+ .viewer-close-btn {
2050
+ background: #2d3748;
2051
+ border-color: #374151;
2052
+ color: #d1d5db;
2053
+ }
2054
+
2055
+ .raise-hand-toast {
2056
+ background: #1f2937;
2057
+ }
1731
2058
  }
1732
2059
  }
1733
2060
 
@@ -2126,4 +2453,43 @@ input[type=text]:focus {
2126
2453
  background: #1a1d27 !important;
2127
2454
  color: #e5e7eb !important;
2128
2455
  }
2456
+
2457
+ .broadcast-bar {
2458
+ background: #0f1117;
2459
+ border-bottom-color: #1f2937;
2460
+ }
2461
+
2462
+ .broadcast-start-btn {
2463
+ background: #1f2937;
2464
+ }
2465
+
2466
+ .broadcast-start-btn:hover {
2467
+ background: #374151;
2468
+ }
2469
+
2470
+ .viewer-panel {
2471
+ background: #1e2433;
2472
+ border-color: #2d3748;
2473
+ }
2474
+
2475
+ .viewer-panel-header {
2476
+ background: #161b27;
2477
+ border-bottom-color: #2d3748;
2478
+ }
2479
+
2480
+ #viewerLabel {
2481
+ color: #f9fafb;
2482
+ }
2483
+
2484
+ .viewer-capture-btn,
2485
+ .viewer-raise-btn,
2486
+ .viewer-close-btn {
2487
+ background: #2d3748;
2488
+ border-color: #374151;
2489
+ color: #d1d5db;
2490
+ }
2491
+
2492
+ .raise-hand-toast {
2493
+ background: #1f2937;
2494
+ }
2129
2495
  }
package/client/index.html CHANGED
@@ -37,6 +37,34 @@
37
37
  </div>
38
38
  </header>
39
39
 
40
+ <div id="broadcastBar" class="broadcast-bar" style="display:none">
41
+ <div class="broadcast-bar-inner">
42
+ <span class="broadcast-pulse"></span>
43
+ <span id="broadcastLabel">Someone is broadcasting</span>
44
+ <span id="broadcastViewers" class="broadcast-viewers"></span>
45
+ <div class="broadcast-bar-actions">
46
+ <button id="broadcastJoinBtn" class="broadcast-join-btn" onclick="joinBroadcast()">Join</button>
47
+ <button id="broadcastEndBtn" class="broadcast-end-btn" onclick="stopBroadcast()" style="display:none">End
48
+ broadcast</button>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <div id="viewerPanel" class="viewer-panel" style="display:none">
54
+ <div class="viewer-panel-header">
55
+ <span id="viewerLabel">Broadcast</span>
56
+ <div class="viewer-panel-actions">
57
+ <button onclick="captureToChannel()" class="viewer-capture-btn" title="Save frame to channel">📸
58
+ Capture</button>
59
+ <button onclick="raiseHand()" class="viewer-raise-btn" title="Raise hand">✋</button>
60
+ <button onclick="toggleAudio()" class="viewer-audio-btn" id="audioToggleBtn" title="Unmute audio">🔇</button>
61
+ <button onclick="toggleMinimize()" class="viewer-minimize-btn" title="Minimize">─</button>
62
+ <button onclick="leaveBroadcast()" class="viewer-close-btn" title="Leave broadcast">✕</button>
63
+ </div>
64
+ </div>
65
+ <video id="viewerFrame" class="viewer-frame" autoplay playsinline muted></video>
66
+ </div>
67
+
40
68
  <div class="container">
41
69
  <div class="channel-bar">
42
70
  <div class="channels" id="channels"></div>
@@ -49,6 +77,9 @@
49
77
  <button onclick="sendText()">Send</button>
50
78
  <input type="file" id="fileInput" hidden multiple>
51
79
  <button onclick="fileInput.click()">Upload</button>
80
+ <button id="startBroadcastBtn" onclick="startBroadcast()" class="broadcast-start-btn"
81
+ title="Start broadcasting your screen">📡 Broadcast</button>
82
+
52
83
  </div>
53
84
  <div class="drop" id="drop">Drag files anywhere to upload</div>
54
85
  <div id="uploadStatus"