From a2a2da65861914b23d046b67f3ebd9253e85b675 Mon Sep 17 00:00:00 2001 From: Cesar Mendivil Date: Wed, 26 Nov 2025 15:42:40 -0700 Subject: [PATCH] feat(deploy): add LiveKit and Caddy configuration files, init script, and Docker Compose setup for server deployment --- package-lock.json | 153 ++++++++++++++- packages/avanza-ui/README.md | 103 ++++++++++- packages/avanza-ui/rollup.config.js | 11 +- packages/avanza-ui/scripts/copy-css.js | 5 + .../{studio => prejoin}/PreJoin.module.css | 96 ++++++++++ .../features/{studio => prejoin}/PreJoin.tsx | 174 ++++++++++++++---- .../src/features/studio/StudioPortal.tsx | 2 +- packages/broadcast-panel/src/main.tsx | 2 +- .../src/stories/PreJoin.stories.tsx | 2 +- 9 files changed, 502 insertions(+), 46 deletions(-) create mode 100644 packages/avanza-ui/scripts/copy-css.js rename packages/broadcast-panel/src/features/{studio => prejoin}/PreJoin.module.css (79%) rename packages/broadcast-panel/src/features/{studio => prejoin}/PreJoin.tsx (53%) diff --git a/package-lock.json b/package-lock.json index ee5c8df..adb75ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6265,6 +6265,25 @@ "@types/send": "*" } }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -6379,6 +6398,12 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -15007,6 +15032,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -23510,6 +23544,122 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-copy": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz", + "integrity": "sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ==", + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.1", + "colorette": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "10.0.1", + "is-plain-object": "^3.0.0" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/rollup-plugin-copy/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rollup-plugin-copy/node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "license": "MIT" + }, + "node_modules/rollup-plugin-copy/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/rollup-plugin-copy/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup-plugin-copy/node_modules/globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-copy/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/rollup-plugin-copy/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup-plugin-copy/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/rollup-plugin-peer-deps-external": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.4.tgz", @@ -30597,7 +30747,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "clsx": "^2.1.1" + "clsx": "^2.1.1", + "rollup-plugin-copy": "^3.4.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", diff --git a/packages/avanza-ui/README.md b/packages/avanza-ui/README.md index dae1d55..4f45c2b 100644 --- a/packages/avanza-ui/README.md +++ b/packages/avanza-ui/README.md @@ -1,19 +1,102 @@ # avanza-ui -Librería de componentes reutilizables para AvanzaCast. +Paquete de componentes UI usado por AvanzaCast. -Componentes añadidos en esta entrega: +Este README explica el flujo de build y las instrucciones rápidas para desarrollar, compilar y depurar `avanza-ui` en este monorepo. -- `ControlButton` - botón redondo con icono y etiqueta opcional (tamaños: sm|md|lg) -- `IconButton` - botón icon-only para acciones rápidas -- `ControlGroup` - contenedor para agrupar controles -- `ControlBar` - barra de controles centrada que usa `ControlGroup` +## Propósito -Importar desde otros paquetes: +`avanza-ui` contiene componentes React reutilizables (Button, Modal, VideoTile, StudioHeader, etc.) y sus estilos. El artefacto compilado queda en `dist/` y es consumido por otras aplicaciones del monorepo (por ejemplo `broadcast-panel`). -```ts -import { ControlButton, IconButton, ControlGroup, ControlBar } from 'avanza-ui' +## Resumen del flujo de build + +- Usamos Rollup para compilar el código TS/TSX a `dist/index.cjs.js` y `dist/index.esm.js`. +- `postcss` genera CSS extracted (archivo global) y `rollup-plugin-copy` copia desde `src/components/**/*.module.css` a `dist/components/` y `src/styles/**/*.css` a `dist/styles/` para que las importaciones relativas en los archivos emitidos en `dist` (p. ej. `@import '../styles/globals.css'`) resuelvan correctamente en tiempo de ejecución. +- Se dejó un script `scripts/copy-css.js` como respaldo, pero está marcado como obsoleto; la copia la realiza `rollup-plugin-copy` en el build normal. + +## Requisitos + +- Node.js (recomendado >= 18) +- npm + +## Comandos útiles + +Instalar dependencias del paquete: + +```bash +cd packages/avanza-ui +npm install ``` -Los estilos se importan como efecto secundario al importar `avanza-ui` (archivo `controls.css`). +Construir (producción / CI): +```bash +cd packages/avanza-ui +npm run build +``` + +- `rollup -c` compila el paquete y `rollup-plugin-copy` copiará automáticamente los CSS modules y estilos globales a `dist`. + +Modo desarrollo (watch): + +```bash +cd packages/avanza-ui +npm run dev +``` + +Verificar que los CSS aparezcan en dist: + +```bash +ls -la packages/avanza-ui/dist/components | head -n 40 +ls -la packages/avanza-ui/dist/styles +``` + +Levantar el `broadcast-panel` para probar la UI integrada: + +```bash +cd packages/broadcast-panel +npm run dev +# abrir http://localhost:5176/ (o el puerto que Vite muestre) +``` + +## Notas y troubleshooting + +- Si ves warnings de Node como `MODULE_TYPELESS_PACKAGE_JSON` al ejecutar `npm run build`, puedes añadir `"type": "module"` a `packages/avanza-ui/package.json` para evitar que Node reanalice el `rollup.config.js` como CommonJS. + +- Si aparece un error tipo `ENOENT: no such file or directory, open '../styles/globals.css'` significa que `dist/styles/globals.css` no existe. Ejecuta `npm run build` en `packages/avanza-ui` y verifica que `dist/styles/globals.css` y `dist/components/*.module.css` estén presentes. + +- El script `packages/avanza-ui/scripts/copy-css.js` existe como respaldo histórico. Ya está marcado como "deprecated" y no se ejecuta en el flujo por defecto. Si prefieres eliminarlo, puedes borrarlo del repositorio. + +## Integración en CI + +Asegúrate de que el job que construye la aplicación principal (o que publica paquetes) ejecute: + +```bash +cd packages/avanza-ui +npm ci +npm run build +``` + +Esto garantiza que los artefactos `dist` contienen tanto los JS compilados como los CSS necesarios. + +## Limpieza / regeneración de dist + +Para forzar una reconstrucción limpia: + +```bash +cd packages/avanza-ui +rm -rf dist node_modules +npm ci +npm run build +``` + +## Contribuciones + +Si agregas nuevos componentes que usan `*.module.css` o nuevos archivos en `src/styles/`, asegúrate de que las rutas relativas usadas en los ficheros emitidos en `dist` coincidan con la estructura que `rollup-plugin-copy` genera (`dist/components` y `dist/styles`). + +--- +Si quieres, puedo: +- añadir una nota al `README.md` de la raíz del repo explicando el cambio, o +- abrir un commit/PR con estos cambios documentados. + +Dime qué prefieres y lo hago ahora. diff --git a/packages/avanza-ui/rollup.config.js b/packages/avanza-ui/rollup.config.js index 0d8dce5..314de40 100644 --- a/packages/avanza-ui/rollup.config.js +++ b/packages/avanza-ui/rollup.config.js @@ -3,6 +3,7 @@ import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import typescript from '@rollup/plugin-typescript'; import postcss from 'rollup-plugin-postcss'; +import copy from 'rollup-plugin-copy'; export default { input: 'src/index.ts', @@ -24,6 +25,14 @@ export default { commonjs(), typescript({ tsconfig: './tsconfig.json' }), postcss({ extract: true, minimize: true }), + // copy CSS module files and global styles to dist so relative @import paths resolve + copy({ + targets: [ + { src: 'src/components/**/*.module.css', dest: 'dist/components' }, + { src: 'src/styles/**/*.css', dest: 'dist/styles' } + ], + verbose: true, + flatten: false, + }), ], }; - diff --git a/packages/avanza-ui/scripts/copy-css.js b/packages/avanza-ui/scripts/copy-css.js new file mode 100644 index 0000000..1b6d587 --- /dev/null +++ b/packages/avanza-ui/scripts/copy-css.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node +// Deprecated: rollup-plugin-copy now handles copying CSS files into dist. +// Keeping this script for backward compatibility/history. It will not perform any action. +console.log('scripts/copy-css.js is deprecated; rollup-plugin-copy handles CSS copying now.'); +process.exit(0); diff --git a/packages/broadcast-panel/src/features/studio/PreJoin.module.css b/packages/broadcast-panel/src/features/prejoin/PreJoin.module.css similarity index 79% rename from packages/broadcast-panel/src/features/studio/PreJoin.module.css rename to packages/broadcast-panel/src/features/prejoin/PreJoin.module.css index 951239e..42ce96c 100644 --- a/packages/broadcast-panel/src/features/studio/PreJoin.module.css +++ b/packages/broadcast-panel/src/features/prejoin/PreJoin.module.css @@ -214,6 +214,57 @@ .control-btn.disabled { color: #dc2626; background-color: #fecaca; } .control-btn.disabled:hover { color: #b91c1c; background-color: #fca5a5; } +/* Device picker dropdown/dialog shown under control buttons when multiple devices exist */ +.device-picker, +.devicePicker { + position: absolute; + top: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 8px; + box-shadow: 0 6px 18px rgba(15, 23, 42, 0.08); + z-index: 60; + min-width: 180px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.device-item, +.deviceItem { + background: transparent; + border: none; + text-align: left; + padding: 8px 10px; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + color: #111827; +} +.device-item:hover, .deviceItem:hover { background: #f3f4f6; } + +.device-cancel, +.deviceCancel { + background: #fff; + border: 1px solid #e5e7eb; + padding: 8px 10px; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + color: #6b7280; +} + +.device-arrow, +.deviceArrow { + margin-left: 8px; + font-size: 12px; + color: #6b7280; + display: inline-block; +} + .control-icon { width: 36px; height: 36px; @@ -360,6 +411,51 @@ font-size: 14px; } +/* Modal for device settings */ +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 120; + padding: 24px; +} + +.modal-content { + background: #ffffff; + border-radius: 12px; + width: 720px; + max-width: 100%; + box-shadow: 0 12px 40px rgba(2,6,23,0.12); + overflow: hidden; +} + +.modal-header { padding: 20px 24px; border-bottom: 1px solid #eef2f7; } +.modal-header h3 { margin: 0; font-size: 18px; color: #111827; } + +.modal-body { padding: 18px 24px; display: flex; gap: 20px; flex-wrap: wrap; } +.modal-section { flex: 1 1 300px; min-width: 260px; } +.modal-section h4 { margin: 0 0 8px 0; font-size: 14px; color: #0f172a; } + +.modal-actions { margin-bottom: 8px; } +.btn-primary { background: #2563eb; color: white; border: none; padding: 8px 14px; border-radius: 8px; cursor: pointer; } +.btn-secondary { background: #fff; color: #374151; border: 1px solid #e5e7eb; padding: 8px 12px; border-radius: 8px; cursor: pointer; } + +.device-list { display: flex; flex-direction: column; gap: 8px; max-height: 180px; overflow: auto; padding-right: 6px; } +.device-row { display: flex; gap: 10px; align-items: center; font-size: 14px; color: #111827; } +.device-row input { accent-color: #2563eb; } +.muted { color: #6b7280; font-size: 13px; } + +.modal-footer { display:flex; gap: 12px; justify-content: flex-end; padding: 12px 20px; border-top: 1px solid #eef2f7; } + +/* small responsive tweaks */ +@media (max-width: 640px) { + .modal-body { flex-direction: column; padding: 12px; } + .modal-content { width: 100%; } +} + /* responsive */ @media (max-width: 800px) { .video-container { flex-direction: column; } diff --git a/packages/broadcast-panel/src/features/studio/PreJoin.tsx b/packages/broadcast-panel/src/features/prejoin/PreJoin.tsx similarity index 53% rename from packages/broadcast-panel/src/features/studio/PreJoin.tsx rename to packages/broadcast-panel/src/features/prejoin/PreJoin.tsx index e94b10e..585da6b 100644 --- a/packages/broadcast-panel/src/features/studio/PreJoin.tsx +++ b/packages/broadcast-panel/src/features/prejoin/PreJoin.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react' import styles from './PreJoin.module.css' // We'll dynamically import MockToggle inside the component when appropriate (DEV mode or VITE_MOCK_STUDIO). -import { isMacPlatform } from 'avanza-ui' +import { isMacPlatform } from 'avanza-ui/dist' import { FiMic, FiVideo, FiSettings } from 'react-icons/fi' // Fallback static list in case dynamic discovery fails. Match actual files present in @@ -32,6 +32,12 @@ export default function PreJoin({ roomName: _roomName, onProceed, onCancel: _onC const [name, setName] = useState(() => { try { return localStorage.getItem('avanzacast_user') || '' } catch { return '' } }) + // device-management state + const [audioDevices, setAudioDevices] = useState([]) + const [videoDevices, setVideoDevices] = useState([]) + const [selectedAudioDeviceId, setSelectedAudioDeviceId] = useState(null) + const [selectedVideoDeviceId, setSelectedVideoDeviceId] = useState(null) + const [showSettingsModal, setShowSettingsModal] = useState(false) const [micEnabled, setMicEnabled] = useState(true) const [camEnabled, setCamEnabled] = useState(true) const [isChecking, setIsChecking] = useState(false) @@ -72,11 +78,12 @@ export default function PreJoin({ roomName: _roomName, onProceed, onCancel: _onC ;(async () => { try { if (!navigator?.mediaDevices?.getUserMedia) return - localStream = await navigator.mediaDevices.getUserMedia({ audio: micEnabled, video: camEnabled }) + const audioConstraint: any = micEnabled ? (selectedAudioDeviceId ? { deviceId: { exact: selectedAudioDeviceId } } : true) : false + const videoConstraint: any = camEnabled ? (selectedVideoDeviceId ? { deviceId: { exact: selectedVideoDeviceId } } : true) : false + localStream = await navigator.mediaDevices.getUserMedia({ audio: audioConstraint, video: videoConstraint }) if (!mounted) { try { - const tracks = localStream?.getTracks?.() - if (tracks && tracks.length) tracks.forEach(t => t.stop()) + if (localStream && localStream.getTracks) localStream.getTracks().forEach(t => t.stop()) } catch (e) {} return } @@ -122,10 +129,7 @@ export default function PreJoin({ roomName: _roomName, onProceed, onCancel: _onC return () => { mounted = false if (localStream) { - try { - const tracks = localStream?.getTracks?.() - if (tracks && tracks.length) tracks.forEach(t => t.stop()) - } catch (e) {} + try { if (localStream.getTracks) localStream.getTracks().forEach(t => t.stop()) } catch (e) {} } // clear previewStream state setPreviewStream(null) @@ -149,14 +153,6 @@ export default function PreJoin({ roomName: _roomName, onProceed, onCancel: _onC } } - const toggleMic = async () => { - setMicEnabled(v => !v) - } - - const toggleCam = async () => { - setCamEnabled(v => !v) - } - // If analyzing the token, show the loading/analyzing UI instead of prejoin if (isAnalyzing) { return ( @@ -182,6 +178,64 @@ export default function PreJoin({ roomName: _roomName, onProceed, onCancel: _onC ) } + // When user selects a device from picker, apply and close picker + const applySelectedAudio = async (deviceId: string) => { + setSelectedAudioDeviceId(deviceId) + // recreate preview stream with selected device + try { + const s = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: { exact: deviceId } }, video: false }) + // merge with existing preview if needed: we let effect recreate full preview on next state change + if (s && s.getTracks) { + try { s.getTracks().forEach(t => t.stop()) } catch (e) {} + } + } catch (e) {} + } + + const applySelectedVideo = async (deviceId: string) => { + setSelectedVideoDeviceId(deviceId) + try { + const s = await navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: deviceId } }, audio: false }) + // merge with existing preview if needed: we let effect recreate full preview on next state change + if (s && s.getTracks) { + try { s.getTracks().forEach(t => t.stop()) } catch (e) {} + } + } catch (e) {} + } + + // Request audio permission and optionally show picker + const requestAndEnableMic = async () => { + try { + // ask for audio permission (without specifying device to allow labels) + const s = await navigator.mediaDevices.getUserMedia({ audio: true }) + // stop tracks from this quick probe (we'll re-create preview with selected device) + if (s && s.getTracks) { try { s.getTracks().forEach(t => t.stop()) } catch (e) {} } + // enumerate devices and store list (modal will show them) + const list = await navigator.mediaDevices.enumerateDevices() + const audioIn = list.filter(d => d.kind === 'audioinput') + setAudioDevices(audioIn) + if (!selectedAudioDeviceId && audioIn.length) setSelectedAudioDeviceId(audioIn[0].deviceId) + setMicEnabled(true) + } catch (e) { + // permission denied or error -> keep disabled + setMicEnabled(false) + } + } + + // Request camera permission and optionally show picker + const requestAndEnableCam = async () => { + try { + const s = await navigator.mediaDevices.getUserMedia({ video: true }) + if (s && s.getTracks) { try { s.getTracks().forEach(t => t.stop()) } catch (e) {} } + const list = await navigator.mediaDevices.enumerateDevices() + const videoIn = list.filter(d => d.kind === 'videoinput') + setVideoDevices(videoIn) + if (!selectedVideoDeviceId && videoIn.length) setSelectedVideoDeviceId(videoIn[0].deviceId) + setCamEnabled(true) + } catch (e) { + setCamEnabled(false) + } + } + return (
@@ -215,23 +269,35 @@ export default function PreJoin({ roomName: _roomName, onProceed, onCancel: _onC
- +
+ +
- +
+ +
- + + {/* Settings modal: device configuration moved here */} + {showSettingsModal ? ( +
+
+
+

Configuración de dispositivos

+
+
+
+

Micrófonos

+
+ +
+
+ {audioDevices.length ? audioDevices.map(d => ( + + )) :
Ningún dispositivo detectado
} +
+
+ +
+

Cámaras

+
+ +
+
+ {videoDevices.length ? videoDevices.map(d => ( + + )) :
Ningún dispositivo detectado
} +
+
+
+
+ + +
+
+
+ ) : null}
) } diff --git a/packages/broadcast-panel/src/features/studio/StudioPortal.tsx b/packages/broadcast-panel/src/features/studio/StudioPortal.tsx index 289e2eb..61fcf79 100644 --- a/packages/broadcast-panel/src/features/studio/StudioPortal.tsx +++ b/packages/broadcast-panel/src/features/studio/StudioPortal.tsx @@ -3,7 +3,7 @@ import StudioRoom from "./StudioRoom"; import "./StudioPortal.css"; import { Room } from "livekit-client"; import AutoRequestAndInject from './AutoRequestAndInject' -import PreJoin from './PreJoin' +import PreJoin from '../prejoin/PreJoin' export interface StudioPortalProps { serverUrl: string; diff --git a/packages/broadcast-panel/src/main.tsx b/packages/broadcast-panel/src/main.tsx index 9199636..aba9ef5 100644 --- a/packages/broadcast-panel/src/main.tsx +++ b/packages/broadcast-panel/src/main.tsx @@ -4,7 +4,7 @@ import PageContainer from "./components/PageContainer"; import "./styles.css"; import { ToastProvider } from "./hooks/useToast"; import StudioPortal from "./features/studio/StudioPortal"; -import PreJoin from "./features/studio/PreJoin"; +import PreJoin from "./features/prejoin/PreJoin"; function SessionLoader({ sessionId }: { sessionId: string }) { const [state, setState] = React.useState<{ diff --git a/packages/studio-panel/src/stories/PreJoin.stories.tsx b/packages/studio-panel/src/stories/PreJoin.stories.tsx index 41a0703..a7a89f5 100644 --- a/packages/studio-panel/src/stories/PreJoin.stories.tsx +++ b/packages/studio-panel/src/stories/PreJoin.stories.tsx @@ -1,5 +1,5 @@ // Simple Storybook story for PreJoin (keeps it robust across envs) -import PreJoin from '../../../../packages/broadcast-panel/src/features/studio/PreJoin' +import PreJoin from '../../../broadcast-panel/src/features/prejoin/PreJoin' export default { title: 'Broadcast/PreJoin',