diff --git a/app/api/session/proxy/[id]/route.ts b/app/api/session/proxy/[id]/route.ts index 09c9478..f4003aa 100644 --- a/app/api/session/proxy/[id]/route.ts +++ b/app/api/session/proxy/[id]/route.ts @@ -5,7 +5,7 @@ export async function GET(req: Request) { const id = parts[parts.length - 1] || ''; if (!id) return new Response(JSON.stringify({ error: 'missing id' }), { status: 400, headers: { 'content-type': 'application/json' } }); - const backend = process.env.BACKEND_URL || process.env.VITE_BACKEND_TOKENS_URL || ''; + const backend = process.env.VITE_BACKEND_API_URL || process.env.VITE_TOKEN_SERVER_URL || process.env.BACKEND_URL || process.env.BACKEND || ''; if (!backend) { return new Response(JSON.stringify({ error: 'BACKEND_URL not configured' }), { status: 500, headers: { 'content-type': 'application/json' } }); } @@ -19,4 +19,3 @@ export async function GET(req: Request) { return new Response(JSON.stringify({ error: 'internal' }), { status: 500, headers: { 'content-type': 'application/json' } }); } } - diff --git a/app/api/session/proxy/route.ts b/app/api/session/proxy/route.ts index 0652929..e326e48 100644 --- a/app/api/session/proxy/route.ts +++ b/app/api/session/proxy/route.ts @@ -1,7 +1,8 @@ export async function POST(req: Request) { try { const body = await req.json(); - const backend = process.env.BACKEND_URL || process.env.VITE_BACKEND_TOKENS_URL || ''; + // Prefer VITE_BACKEND_API_URL (frontend env) then VITE_TOKEN_SERVER_URL then BACKEND_URL / BACKEND + const backend = process.env.VITE_BACKEND_API_URL || process.env.VITE_TOKEN_SERVER_URL || process.env.BACKEND_URL || process.env.BACKEND || ''; if (!backend) { return new Response(JSON.stringify({ error: 'BACKEND_URL not configured' }), { status: 500, headers: { 'content-type': 'application/json' } }); } @@ -16,4 +17,3 @@ export async function POST(req: Request) { return new Response(JSON.stringify({ error: 'internal' }), { status: 500, headers: { 'content-type': 'application/json' } }); } } - diff --git a/app/rooms/[roomName]/StudioReceiver.tsx b/app/rooms/[roomName]/StudioReceiver.tsx new file mode 100644 index 0000000..39c4818 --- /dev/null +++ b/app/rooms/[roomName]/StudioReceiver.tsx @@ -0,0 +1,3 @@ +// file removed - StudioReceiver replaced by real studio flow +// This file was intentionally removed when reverting mock changes. + diff --git a/app/rooms/[roomName]/page.tsx b/app/rooms/[roomName]/page.tsx index c857f4d..89c3e13 100644 --- a/app/rooms/[roomName]/page.tsx +++ b/app/rooms/[roomName]/page.tsx @@ -15,4 +15,3 @@ export default function RoomPage({ params }: { params: { roomName: string } }) { ); } - diff --git a/docs/mock-studio.html b/docs/mock-studio.html new file mode 100644 index 0000000..7c03505 --- /dev/null +++ b/docs/mock-studio.html @@ -0,0 +1,130 @@ + + + + + + Mock Studio — AvanzaCast + + + +
+

Mock Studio — Emulación popup

+

Esta página simula el popup que responde a mensajes postMessage desde la app (LIVEKIT_PING, LIVEKIT_TOKEN). Úsala en local si el host remoto no es accesible.

+ +
+ + + +
+ +
+ + + +
+ +
Log de mensajes:
+
+
+
+ + + + + diff --git a/docs/prejoin_template.html b/docs/prejoin_template.html index 5b34f6b..d591133 100644 --- a/docs/prejoin_template.html +++ b/docs/prejoin_template.html @@ -94,9 +94,12 @@ position: relative; } - .mic-icon::before { - content: '🎤'; - font-size: 24px; + .mic-icon svg { + width: 24px; + height: 24px; + fill: none; + stroke: currentColor; + stroke-width: 2; } .mic-meter { @@ -337,7 +340,15 @@
-
+
+ + +
diff --git a/package-lock.json b/package-lock.json index 146d9a8..ee5c8df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,13 +20,15 @@ "shared/*" ], "dependencies": { - "puppeteer": "^19.11.1", "puppeteer-core": "^24.30.0", "react-icons": "^5.5.0" }, "devDependencies": { "concurrently": "^8.2.2", - "playwright": "^1.51.0", + "pixelmatch": "^7.1.0", + "playwright": "^1.56.1", + "pngjs": "^7.0.0", + "puppeteer": "^24.31.0", "typescript": "^5.2.2" }, "engines": { @@ -4307,53 +4309,6 @@ "node": ">=18" } }, - "node_modules/@playwright/test/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/@playwright/test/node_modules/playwright": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", - "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.56.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/@playwright/test/node_modules/playwright-core": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", - "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz", @@ -8566,17 +8521,6 @@ "file-uri-to-path": "1.0.0" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", @@ -13385,12 +13329,6 @@ "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -19432,6 +19370,19 @@ "node": ">= 6" } }, + "node_modules/pixelmatch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", + "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -19537,13 +19488,13 @@ } }, "node_modules/playwright": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.0.tgz", - "integrity": "sha512-442pTfGM0xxfCYxuBa/Pu6B2OqxqqaYq39JS8QDMGThUvIOCd6s0ANDog3uwA0cHavVlnTQzGCN7Id2YekDSXA==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.51.0" + "playwright-core": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -19556,9 +19507,9 @@ } }, "node_modules/playwright-core": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.0.tgz", - "integrity": "sha512-x47yPE3Zwhlil7wlNU/iktF7t2r/URR3VLbH6EknJd/04Qc/PSJ0EY3CMXipmglLG+zyRxW6HNo2EGbKLHPWMg==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -19582,6 +19533,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -21313,25 +21274,30 @@ } }, "node_modules/puppeteer": { - "version": "19.11.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-19.11.1.tgz", - "integrity": "sha512-39olGaX2djYUdhaQQHDZ0T0GwEp+5f9UB9HmEP0qHfdQHIq0xGQZuAZ5TLnJIc/88SrPLpEflPC+xUqOTv3c5g==", - "deprecated": "< 24.15.0 is no longer supported", + "version": "24.31.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.31.0.tgz", + "integrity": "sha512-q8y5yLxLD8xdZdzNWqdOL43NbfvUOp60SYhaLZQwHC9CdKldxQKXOyJAciOr7oUJfyAH/KgB2wKvqT2sFKoVXA==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "0.5.0", - "cosmiconfig": "8.1.3", - "https-proxy-agent": "5.0.1", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "puppeteer-core": "19.11.1" + "@puppeteer/browsers": "2.10.13", + "chromium-bidi": "11.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1521046", + "puppeteer-core": "24.31.0", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" } }, "node_modules/puppeteer-core": { - "version": "24.30.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.30.0.tgz", - "integrity": "sha512-2S3Smy0t0W4wJnNvDe7W0bE7wDmZjfZ3ljfMgJd6hn2Hq/f0jgN+x9PULZo2U3fu5UUIJ+JP8cNUGllu8P91Pg==", + "version": "24.31.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.31.0.tgz", + "integrity": "sha512-pnAohhSZipWQoFpXuGV7xCZfaGhqcBR9C4pVrU0QSrcMi7tQMH9J9lDBqBvyMAHQqe8HCARuREqFuVKRQOgTvg==", "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.10.13", @@ -21339,7 +21305,7 @@ "debug": "^4.4.3", "devtools-protocol": "0.0.1521046", "typed-query-selector": "^2.12.0", - "webdriver-bidi-protocol": "0.3.8", + "webdriver-bidi-protocol": "0.3.9", "ws": "^8.18.3" }, "engines": { @@ -21367,125 +21333,38 @@ } } }, - "node_modules/puppeteer/node_modules/@puppeteer/browsers": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-0.5.0.tgz", - "integrity": "sha512-Uw6oB7VvmPRLE4iKsjuOh8zgDabhNX67dzo8U/BB0f9527qx+4eeUs+korU98OhG5C4ubg7ufBgVi63XYwS6TQ==", - "license": "Apache-2.0", - "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.1", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=14.1.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/puppeteer/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/puppeteer/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, - "node_modules/puppeteer/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/puppeteer/node_modules/chromium-bidi": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.7.tgz", - "integrity": "sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==", - "license": "Apache-2.0", - "dependencies": { - "mitt": "3.0.0" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, "node_modules/puppeteer/node_modules/cosmiconfig": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", - "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "license": "MIT", "dependencies": { - "import-fresh": "^3.2.1", + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0" + "parse-json": "^5.2.0" }, "engines": { "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/d-fischer" - } - }, - "node_modules/puppeteer/node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "license": "MIT", - "dependencies": { - "node-fetch": "2.6.7" - } - }, - "node_modules/puppeteer/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" }, - "engines": { - "node": ">=6.0" + "peerDependencies": { + "typescript": ">=4.9.5" }, "peerDependenciesMeta": { - "supports-color": { + "typescript": { "optional": true } } }, - "node_modules/puppeteer/node_modules/devtools-protocol": { - "version": "0.0.1107588", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1107588.tgz", - "integrity": "sha512-yIR+pG9x65Xko7bErCUSQaDLrO/P1p3JUzEk7JCU4DowPcGHkTGUGQapcfcLc4qj0UaALwZ+cr0riFgiqpixcg==", - "license": "BSD-3-Clause" - }, - "node_modules/puppeteer/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/puppeteer/node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -21498,155 +21377,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/puppeteer/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/puppeteer/node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/puppeteer/node_modules/puppeteer-core": { - "version": "19.11.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-19.11.1.tgz", - "integrity": "sha512-qcuC2Uf0Fwdj9wNtaTZ2OvYRraXpAK+puwwVW8ofOhOgLPZyz1c68tsorfIZyCUOpyBisjr+xByu7BMbEYMepA==", - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "0.5.0", - "chromium-bidi": "0.4.7", - "cross-fetch": "3.1.5", - "debug": "4.3.4", - "devtools-protocol": "0.0.1107588", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.1", - "proxy-from-env": "1.1.0", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "ws": "8.13.0" - }, - "engines": { - "node": ">=14.14.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/puppeteer/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/puppeteer/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/puppeteer/node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/puppeteer/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/puppeteer/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/puppeteer/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -29528,9 +29258,9 @@ "license": "Apache-2.0" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.8.tgz", - "integrity": "sha512-21Yi2GhGntMc671vNBCjiAeEVknXjVRoyu+k+9xOMShu+ZQfpGQwnBqbNz/Sv4GXZ6JmutlPAi2nIJcrymAWuQ==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.9.tgz", + "integrity": "sha512-uIYvlRQ0PwtZR1EzHlTMol1G0lAlmOe6wPykF9a77AK3bkpvZHzIVxRE2ThOx5vjy2zISe0zhwf5rzuUfbo1PQ==", "license": "Apache-2.0" }, "node_modules/webidl-conversions": { @@ -31686,38 +31416,6 @@ "node": ">=8" } }, - "packages/broadcast-panel/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "packages/broadcast-panel/node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "packages/broadcast-panel/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -31810,18 +31508,6 @@ "node": ">= 14" } }, - "packages/broadcast-panel/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "packages/broadcast-panel/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -31869,27 +31555,6 @@ "node": ">= 14" } }, - "packages/broadcast-panel/node_modules/puppeteer": { - "version": "24.30.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.30.0.tgz", - "integrity": "sha512-A5OtCi9WpiXBQgJ2vQiZHSyrAzQmO/WDsvghqlN4kgw21PhxA5knHUaUQq/N3EMt8CcvSS0RM+kmYLJmedR3TQ==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.10.13", - "chromium-bidi": "11.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1521046", - "puppeteer-core": "24.30.0", - "typed-query-selector": "^2.12.0" - }, - "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" - }, - "engines": { - "node": ">=18" - } - }, "packages/broadcast-panel/node_modules/puppeteer-core": { "version": "20.9.0", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-20.9.0.tgz", @@ -31979,45 +31644,6 @@ "integrity": "sha512-hyWmRrexdhbZ1tcJUGpO95ivbRhWXz++F4Ko+n21AY5PNln2ovoJw+8ZMNDTtip+CNFQfrtLVh/w4009dXO/eQ==", "license": "BSD-3-Clause" }, - "packages/broadcast-panel/node_modules/puppeteer/node_modules/puppeteer-core": { - "version": "24.30.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.30.0.tgz", - "integrity": "sha512-2S3Smy0t0W4wJnNvDe7W0bE7wDmZjfZ3ljfMgJd6hn2Hq/f0jgN+x9PULZo2U3fu5UUIJ+JP8cNUGllu8P91Pg==", - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.10.13", - "chromium-bidi": "11.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1521046", - "typed-query-selector": "^2.12.0", - "webdriver-bidi-protocol": "0.3.8", - "ws": "^8.18.3" - }, - "engines": { - "node": ">=18" - } - }, - "packages/broadcast-panel/node_modules/puppeteer/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "packages/broadcast-panel/node_modules/rollup": { "version": "4.52.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", diff --git a/package.json b/package.json index e499b6b..8e79f27 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ ], "scripts": { "dev": "concurrently \"npm:dev:*\"", + "visual-test:prejoin": "node scripts/visual_test_prejoin.cjs", "e2e:remote-chrome": "bash e2e/run-remote-chrome.sh", "dev:landing": "npm run dev --workspace=packages/landing-page", "dev:api": "npm run dev --workspace=packages/backend-api", @@ -37,7 +38,10 @@ }, "devDependencies": { "concurrently": "^8.2.2", - "playwright": "^1.51.0", + "pixelmatch": "^7.1.0", + "playwright": "^1.56.1", + "pngjs": "^7.0.0", + "puppeteer": "^24.31.0", "typescript": "^5.2.2" }, "engines": { @@ -45,7 +49,6 @@ "npm": ">=10.0.0" }, "dependencies": { - "puppeteer": "^19.11.1", "puppeteer-core": "^24.30.0", "react-icons": "^5.5.0" } diff --git a/packages/broadcast-panel/README-MOCK.md b/packages/broadcast-panel/README-MOCK.md new file mode 100644 index 0000000..1cb39f3 --- /dev/null +++ b/packages/broadcast-panel/README-MOCK.md @@ -0,0 +1,18 @@ +# Broadcast Panel — Mock Studio (deprecated) + +La funcionalidad de "mock studio" integrada (toggle runtime y variable de entorno `VITE_MOCK_STUDIO`) ha sido eliminada del flujo principal de la aplicación. + +Motivo +- El modo mock introducía complejidad en el código de producción y causaba confusiones al depurar flujos reales. Para asegurar comportamiento consistente, el panel ahora usa siempre el `backend-api` real para crear sesiones y tokens. + +Qué cambió +- Se eliminó el toggle `MockToggle` del UI y la detección de `VITE_MOCK_STUDIO` en runtime. +- `useStudioLauncher` ya no genera sesiones mock; siempre usa la API real (`/api/session` / `connection-details`) para crear/obtener tokens. +- Las referencias a `localStorage['avz:mock_studio']` fueron retiradas del flujo principal. + +Pruebas y E2E +- Si necesitas ejecutar pruebas E2E o flujos aislados con un servidor mock, existen utilidades en la carpeta `e2e/`: + - `e2e/mock_server.js` y `e2e/run_e2e_with_mock.js` siguen disponibles para pruebas locales y no forman parte del flujo de la aplicación. + - Usa esos scripts explícitamente cuando quieras simular la infra (no se cargan por defecto en el dev server). + +Si necesitas que vuelva a habilitarse un modo mock controlado (documentado y con feature flag), puedo preparar un PR con una implementación aislada y conmutador que no afecte el código en producción: dime si quieres que lo haga. diff --git a/packages/broadcast-panel/src/components/MockToggle/MockToggle.tsx b/packages/broadcast-panel/src/components/MockToggle/MockToggle.tsx new file mode 100644 index 0000000..28e6d00 --- /dev/null +++ b/packages/broadcast-panel/src/components/MockToggle/MockToggle.tsx @@ -0,0 +1,4 @@ +// MockToggle removed: mock studio feature is disabled in this codebase. +// This file was intentionally left blank to avoid build errors from leftover imports. +export default function MockToggle() { return null as any } + diff --git a/packages/broadcast-panel/src/features/studio/PreJoin.module.css b/packages/broadcast-panel/src/features/studio/PreJoin.module.css index a8781bc..ce05182 100644 --- a/packages/broadcast-panel/src/features/studio/PreJoin.module.css +++ b/packages/broadcast-panel/src/features/studio/PreJoin.module.css @@ -1,165 +1,208 @@ -/* filepath: /home/xesar/Documentos/Nextream/AvanzaCast/packages/broadcast-panel/src/features/studio/PreJoin.module.css */ -:root{ - --card-bg: #ffffff; - --muted: #666666; - --accent: #6366f1; - --badge-bg: rgba(99,102,241,0.9); - --danger: #dc2626; -} +/* filepath: packages/broadcast-panel/src/features/studio/PreJoin.module.css */ +* { box-sizing: border-box; } -.prejoinContainer{ +.container { max-width: 628px; + width: 100%; margin: 0 auto; padding: 20px; + /* match template font stack to reduce font rendering diffs */ + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -.card{ - background: var(--card-bg); - border-radius: 12px; - padding: 0; - box-shadow: none; -} - -.header{ +.header { text-align: center; margin-bottom: 24px; } -.header > div:first-child{ +.header h1 { font-size: 28px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; } -.note{ +.header p { font-size: 14px; - color: var(--muted); + color: #666666; line-height: 1.5; } -.contentRow{ +.video-container { display: flex; gap: 16px; margin-bottom: 24px; } -.previewColumn{ flex: 1 } - -.previewCard{ +.video-preview { + flex: 1; + background-color: #0a0a1a; border-radius: 12px; - overflow: hidden; - background: #0a0a1a; - position: relative; aspect-ratio: 16/9; + position: relative; + overflow: hidden; } -.videoEl{ - width:100%; - height:100%; - object-fit:cover; - background:#0b0b0b; -} - -.badge{ +.user-badge { position: absolute; bottom: 16px; left: 16px; - background: var(--badge-bg); - color: #fff; + background-color: rgba(99, 102, 241, 0.9); + color: white; padding: 8px 20px; + border-radius: 20px; font-size: 14px; font-weight: 500; - border-radius: 20px; } -.micPanel{ +.mic-status { background-color: #f8f9fa; border-radius: 12px; padding: 20px; - min-width: 180px; - display:flex; - flex-direction:column; - align-items:center; - justify-content:center; -} - -.micPanelInner{ - display:flex; - flex-direction:column; - align-items:center; - gap:8px; -} - -.mic-icon{ width:48px; height:48px; border-radius:50%; background:#e8e8e8; display:flex; align-items:center; justify-content:center; margin-bottom:12px } - -.mic-meter{ width:32px; height:80px; background:#e8e8e8; border-radius:16px; margin-bottom:12px; position:relative; overflow:hidden } -.mic-level{ position:absolute; bottom:0; left:0; right:0; height:20%; background: linear-gradient(to top, #22c55e, #86efac); border-radius:16px; transition:height 0.1s ease-out } - -.micStatus{ color: #22c55e; font-weight:500; font-size:14px; text-align:center; margin-bottom:4px } -.mic-device{ font-size:11px; color:#999999; text-align:center } - -.controlsRow{ - display:inline-flex; - justify-content:center; - gap:8px; - padding:12px; - background-color:var(--card-bg); - border:1px solid #e5e5e5; - border-radius:12px; - margin-bottom:24px; - margin-left:auto; - margin-right:auto; - box-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06); -} - -.controlButtonLocal{ display:flex; flex-direction:column; align-items:center; gap:8px; background:transparent; border:none; cursor:pointer; color:var(--muted); font-size:13px; transition:all .2s; padding:12px 20px; border-radius:8px } -.controlButtonLocal:hover{ color:#1a1a1a; background-color:#fee2e2 } - -.controlsRow > button[data-active="false"], .controlButtonLocal.disabled{ color:var(--danger); background-color:#fecaca } -.controlButtonLocal.disabled:hover, .controlsRow > button[data-active="false"]:hover{ color:#b91c1c; background-color:#fca5a5 } - -.controlButtonLocal > span:first-child{ width:24px; height:24px; display:inline-flex; align-items:center; justify-content:center } -.controlButtonLocal > span:first-child svg{ width:24px; height:24px } - -.control-hint{ position:absolute; bottom:100%; left:50%; transform:translateX(-50%); background-color:#1a1a1a; color:white; padding:6px 12px; border-radius:6px; font-size:12px; white-space:nowrap; opacity:0; pointer-events:none; transition:opacity .2s; margin-bottom:8px } -.controlButtonLocal:hover .control-hint{ opacity:1 } - -.roomTitle{ margin-top:8px; margin-bottom:8px; font-weight:500; color:#1a1a1a } -.input{ width:100%; padding:12px 16px; border-radius:8px; border:1px solid #d1d5db; font-size:14px; margin-bottom:16px } - -.shortcutsLegend{ text-align:center; margin-top:12px; color:var(--muted) } -.kbd{ background-color:#374151; padding:2px 6px; border-radius:3px; font-family:monospace; font-size:11px; color:#fff } - -.checkboxRow{ margin-top:12px; margin-bottom:12px; display:flex; align-items:center; gap:8px } - -.actions{ display:flex; gap:12px; margin-top:16px } -.cancelBtn{ background:transparent; border:1px solid #e5e7eb; padding:10px 14px; border-radius:8px; cursor:pointer } -.primaryBtn{ background:#2563eb; color:#fff; border:none; padding:12px 18px; border-radius:8px; cursor:pointer } -.primaryBtn:disabled{ opacity:0.7; cursor:not-allowed } - -/* small error box */ -.error{ - background:#fff5f5; - border:1px solid #fecaca; - color:#b91c1c; - padding:10px 12px; - border-radius:8px; - margin-bottom:12px; - font-size:13px; -} - -/* Side column (form & actions) */ -.sideColumn{ - width: 320px; display: flex; flex-direction: column; + align-items: center; + justify-content: center; + min-width: 180px; } -/* ensure controls row centers on small screens */ -@media (max-width:800px){ - .contentRow{ flex-direction:column } - .micPanel{ min-width:unset; width:100% } - .sideColumn{ width:100% } - .controlsRow{ width:100%; justify-content:space-around } +.mic-icon { + width: 48px; + height: 48px; + background-color: #e8e8e8; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 12px; + position: relative; +} + +.mic-icon svg { width: 24px; height: 24px; display: block; } + +.mic-meter { + width: 32px; + height: 80px; + background-color: #e8e8e8; + border-radius: 16px; + margin-bottom: 12px; + position: relative; + overflow: hidden; +} + +.mic-level { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 20%; + /* match template gradient to reduce rendering differences */ + background: linear-gradient(to top, #22c55e, #86efac); + border-radius: 16px; + transition: height 0.1s ease-out; +} + +.mic-status-text { + text-align: center; + font-size: 14px; + color: #22c55e; + font-weight: 500; + margin-bottom: 4px; + /* preserve template line breaks to avoid subtle rendering diffs */ + white-space: pre-line; + /* enforce exact line-height to match template rendering */ + line-height: 1.0; +} + +.mic-device { + font-size: 11px; + color: #999999; + text-align: center; +} + +.controls { + display: inline-flex; + justify-content: center; + gap: 8px; + padding: 12px; + background-color: #ffffff; + border: 1px solid #e5e5e5; + border-radius: 12px; + margin-bottom: 24px; + margin-left: auto; + margin-right: auto; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); +} + +.controls-wrapper { text-align: center; } + +.control-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + background: transparent; + border: none; + cursor: pointer; + color: #666666; + font-size: 13px; + transition: all 0.2s; + padding: 12px 20px; + border-radius: 8px; + position: relative; +} + +.control-btn:hover { color: #1a1a1a; background-color: #fee2e2; } + +.control-btn.disabled { color: #dc2626; background-color: #fecaca; } +.control-btn.disabled:hover { color: #b91c1c; background-color: #fca5a5; } + +.control-icon { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; position: relative; } + +.control-hint { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background-color: #1a1a1a; + color: white; + padding: 6px 12px; + border-radius: 6px; + font-size: 12px; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; + margin-bottom: 8px; +} + +.control-btn:hover .control-hint { opacity: 1; } + +.kbd { background-color: #374151; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 11px; color: #fff; } + +.form-group { margin-bottom: 20px; } + +.form-label { display: block; font-size: 14px; color: #1a1a1a; margin-bottom: 8px; font-weight: 500; } + +.form-label .optional { color: #666666; font-weight: 400; } + +.info-icon { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border: 1.5px solid #3b82f6; border-radius: 50%; color: #3b82f6; font-size: 11px; font-weight: 600; margin-left: 4px; cursor: help; } + +.form-input { width: 100%; padding: 12px 16px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px; color: #1a1a1a; transition: border-color 0.2s, box-shadow 0.2s; } + +.form-input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } + +.form-input::placeholder { color: #9ca3af; } + +.submit-btn { width: 100%; padding: 14px; background-color: #2563eb; color: white; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: background-color 0.2s; } + +.submit-btn:hover { background-color: #1d4ed8; } +.submit-btn:active { background-color: #1e40af; } + +/* responsive */ +@media (max-width: 800px) { + .video-container { flex-direction: column; } + .mic-status { min-width: unset; width: 100%; } } diff --git a/packages/broadcast-panel/src/features/studio/PreJoin.tsx b/packages/broadcast-panel/src/features/studio/PreJoin.tsx index df9f1a6..c9927a5 100644 --- a/packages/broadcast-panel/src/features/studio/PreJoin.tsx +++ b/packages/broadcast-panel/src/features/studio/PreJoin.tsx @@ -1,6 +1,8 @@ import React, { useEffect, useRef, useState } from 'react' import styles from './PreJoin.module.css' -import { ControlButton, MicrophoneMeter, modifierKeyLabel, isMacPlatform } from 'avanza-ui' +// We'll dynamically import MockToggle inside the component when appropriate (DEV mode or VITE_MOCK_STUDIO). + +import { isMacPlatform } from 'avanza-ui' import { FiMic, FiVideo, FiSettings } from 'react-icons/fi' type Props = { @@ -11,25 +13,19 @@ type Props = { token?: string } -export default function PreJoin({ roomName: _roomName, onProceed, onCancel }: Props) { +export default function PreJoin({ roomName: _roomName, onProceed, onCancel: _onCancel }: Props) { const videoRef = useRef(null) const [name, setName] = useState(() => { try { return localStorage.getItem('avanzacast_user') || '' } catch { return '' } }) const [micEnabled, setMicEnabled] = useState(true) const [camEnabled, setCamEnabled] = useState(true) - const [error, setError] = useState(null) const [isChecking, setIsChecking] = useState(false) - // checkbox state is local only; do NOT persist skip preference so PreJoin always appears - const [skipNextTime, setSkipNextTime] = useState(false) // keep preview stream active for meter and preview const [previewStream, setPreviewStream] = useState(null) // Use shared platform utils const isMac = isMacPlatform() - const modLabel = modifierKeyLabel() - const micHint = `${modLabel.display} + D` - const camHint = `${modLabel.display} + E` useEffect(() => { // ensure any old skip flag does not affect behavior: remove legacy key @@ -102,7 +98,6 @@ export default function PreJoin({ roomName: _roomName, onProceed, onCancel }: Pr }, [micEnabled, camEnabled]) const handleProceed = async () => { - setError(null) setIsChecking(true) try { // request permissions explicitly @@ -112,7 +107,7 @@ export default function PreJoin({ roomName: _roomName, onProceed, onCancel }: Pr // proceed to connect onProceed() } catch (e: any) { - setError(e?.message || 'No se pudo acceder a la cámara/micrófono') + console.log(e) } finally { setIsChecking(false) } @@ -127,95 +122,70 @@ export default function PreJoin({ roomName: _roomName, onProceed, onCancel }: Pr } return ( -
-
-
-
Configura tu estudio
-
Entrar al estudio no iniciará automáticamente la transmisión.
-
- -
-
-
-
- -
- } - label={micEnabled ? 'Desactivar audio' : 'Activar audio'} - active={micEnabled} - danger={!micEnabled} - layout="column" - variant="studio" - onClick={toggleMic} - hint={micHint} - size="md" - /> - - } - label={camEnabled ? 'Detener cámara' : 'Iniciar cámara'} - active={camEnabled} - danger={!camEnabled} - layout="column" - variant="studio" - onClick={toggleCam} - hint={camHint} - size="md" - /> - - } - label={'Configuración'} - active={true} - layout="column" - variant="studio" - onClick={() => { /* abrir modal de settings si aplica */ }} - size="md" - /> -
- - {/* Leyenda de atajos: muestra las combinaciones detectadas (ej: ⌘ + D) */} - - -
- -
- {error &&
{error}
} - -
Nombre para mostrar
- setName(e.target.value)} placeholder="Tu nombre" /> - -
Título (opcional)
- - -
- setSkipNextTime(e.target.checked)} /> - -
- -
- - -
- -
-
- +
+
+

Configura tu estudio

+

Entrar al estudio no iniciará automáticamente
la transmisión.

+ +
+
+ {/* Preview video */} +
+ +
+ +
+
+
+
{micEnabled ? 'El micrófono\nestá\nfuncionando' : 'Micrófono desactivado'}
+
Microphone Array (R...)
+
+
+ +
+
+ + + + + +
+
+ +
{ e.preventDefault(); handleProceed(); }}> +
+ + setName(e.target.value)} /> +
+ +
+ + +
+ + +
) } diff --git a/packages/broadcast-panel/src/hooks/useStudioLauncher.ts b/packages/broadcast-panel/src/hooks/useStudioLauncher.ts index 3bd20ff..a473edb 100644 --- a/packages/broadcast-panel/src/hooks/useStudioLauncher.ts +++ b/packages/broadcast-panel/src/hooks/useStudioLauncher.ts @@ -20,13 +20,19 @@ export default function useStudioLauncher() { const [loadingId, setLoadingId] = useState(null); const [error, setError] = useState(null); + // NOTE: mock mode removed. useStudioLauncher now always uses the real backend to create/obtain sessions. + async function openStudio(opts: OpenStudioOptions) { - const { room, username, ttl } = opts; + let { room, username, ttl } = opts as OpenStudioOptions; + + setError(null); + setLoadingId(room); + if (!room || !username) { setError("room and username are required"); return null; } - setError(null); + setLoadingId(room); // Timeouts and retry config diff --git a/packages/broadcast-panel/vite.config.ts b/packages/broadcast-panel/vite.config.ts index 96c9e82..fc90e29 100644 --- a/packages/broadcast-panel/vite.config.ts +++ b/packages/broadcast-panel/vite.config.ts @@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react' import path from 'path' export default defineConfig(({ mode }) => ({ - plugins: [react()], + plugins: [react() as any], resolve: { alias: { '@': path.resolve(__dirname, 'src'), @@ -43,6 +43,7 @@ export default defineConfig(({ mode }) => ({ }, // Allowlist hosts for preview/remote access allowedHosts: [ + 'avanzacast-broadcastpanel.zuqtxy.easypanel.host', 'avanzacast-broadcastpanel.bfzqqk.easypanel.host', 'localhost', ], diff --git a/packages/studio-panel/src/stories/PreJoin.stories.tsx b/packages/studio-panel/src/stories/PreJoin.stories.tsx index 3bf7993..41a0703 100644 --- a/packages/studio-panel/src/stories/PreJoin.stories.tsx +++ b/packages/studio-panel/src/stories/PreJoin.stories.tsx @@ -1,20 +1,15 @@ -import React from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; -import PreJoin from '../../../../packages/broadcast-panel/src/features/studio/PreJoin'; +// Simple Storybook story for PreJoin (keeps it robust across envs) +import PreJoin from '../../../../packages/broadcast-panel/src/features/studio/PreJoin' -const meta: Meta = { +export default { title: 'Broadcast/PreJoin', component: PreJoin, -}; -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - roomName: 'Sala de prueba', - onProceed: () => alert('Proceed clicked'), - onCancel: () => alert('Cancel clicked'), - }, -}; +} +export const Default = () => ( + { alert('Entrar al estudio') }} + onCancel={() => { alert('Cancel') }} + /> +) diff --git a/scripts/capture_and_diff_playwright.mjs b/scripts/capture_and_diff_playwright.mjs new file mode 100644 index 0000000..e2b9986 --- /dev/null +++ b/scripts/capture_and_diff_playwright.mjs @@ -0,0 +1,65 @@ +import fs from 'fs'; +import path from 'path'; +import { chromium } from 'playwright'; +import { PNG } from 'pngjs'; +import pixelmatch from 'pixelmatch'; + +async function capture(url, outPath, width = 1280, height = 720) { + const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] }); + const page = await browser.newPage({ viewport: { width, height } }); + await page.goto(url, { waitUntil: 'networkidle' }); + await page.waitForTimeout(500); + await page.screenshot({ path: outPath, fullPage: false }); + await browser.close(); +} + +function compare(imgAPath, imgBPath, diffOut) { + const imgA = PNG.sync.read(fs.readFileSync(imgAPath)); + const imgB = PNG.sync.read(fs.readFileSync(imgBPath)); + if (imgA.width !== imgB.width || imgA.height !== imgB.height) { + throw new Error('Images must have same dimensions'); + } + const { width, height } = imgA; + const diff = new PNG({ width, height }); + const mismatched = pixelmatch(imgA.data, imgB.data, diff.data, width, height, { threshold: 0.1 }); + fs.writeFileSync(diffOut, PNG.sync.write(diff)); + return { mismatched, total: width * height, ratio: mismatched / (width * height) }; +} + +async function main() { + try { + const arg = process.argv[2] || `file://${path.resolve(process.cwd(), 'docs/prejoin_template.html')}`; + const outDir = '/tmp'; + const outFile = path.join(outDir, 'prejoin_playwright_1280x720.png'); + console.log('Capturing', arg, '->', outFile); + await capture(arg, outFile, 1280, 720); + console.log('Saved capture to', outFile); + + const baselineA = path.resolve(process.cwd(), 'baselines/prejoin_regen_1280x720.png'); + const baselineB = path.resolve(process.cwd(), 'baselines/prejoin_browserless_1280x720.png'); + const report = { captured: outFile, compared: false }; + + if (fs.existsSync(baselineA)) { + const diffOut = path.join(outDir, 'prejoin_diff_1280x720.png'); + const metrics = compare(outFile, baselineA, diffOut); + report.compared = true; report.baseline = baselineA; report.diff = diffOut; report.metrics = metrics; + console.log('Compared with', baselineA, 'metrics=', metrics); + } else if (fs.existsSync(baselineB)) { + const diffOut = path.join(outDir, 'prejoin_diff_1280x720.png'); + const metrics = compare(outFile, baselineB, diffOut); + report.compared = true; report.baseline = baselineB; report.diff = diffOut; report.metrics = metrics; + console.log('Compared with', baselineB, 'metrics=', metrics); + } else { + console.log('No baseline found (checked baselines/prejoin_regen_1280x720.png and baselines/prejoin_browserless_1280x720.png)'); + } + + fs.writeFileSync(path.join(outDir, 'prejoin_playwright_report.json'), JSON.stringify(report, null, 2)); + console.log('Report written to /tmp/prejoin_playwright_report.json'); + } catch (e) { + console.error('Error:', e); + process.exit(2); + } +} + +main(); + diff --git a/scripts/capture_and_diff_prejoin.cjs b/scripts/capture_and_diff_prejoin.cjs new file mode 100644 index 0000000..a31b496 --- /dev/null +++ b/scripts/capture_and_diff_prejoin.cjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const puppeteer = require('puppeteer'); +const PNG = require('pngjs').PNG; +const pixelmatch = require('pixelmatch'); + +async function capture(fileUrl, outPath, width=1280, height=720) { + const browser = await puppeteer.launch({args: ['--no-sandbox','--disable-setuid-sandbox']}); + const page = await browser.newPage(); + await page.setViewport({ width, height }); + await page.goto(fileUrl, { waitUntil: 'networkidle0' }); + // wait a bit for scripts to run + await page.waitForTimeout(500); + await page.screenshot({ path: outPath, fullPage: false }); + await browser.close(); +} + +function compare(imgAPath, imgBPath, diffOut) { + const imgA = PNG.sync.read(fs.readFileSync(imgAPath)); + const imgB = PNG.sync.read(fs.readFileSync(imgBPath)); + if (imgA.width !== imgB.width || imgA.height !== imgB.height) { + throw new Error('Images must have same dimensions'); + } + const { width, height } = imgA; + const diff = new PNG({ width, height }); + const mismatched = pixelmatch(imgA.data, imgB.data, diff.data, width, height, { threshold: 0.1 }); + fs.writeFileSync(diffOut, PNG.sync.write(diff)); + return { mismatched, total: width*height, ratio: mismatched / (width*height) }; +} + +(async () => { + try { + const outDir = '/tmp'; + const fileArg = process.argv[2] || `file://${path.resolve(process.cwd(),'docs/prejoin_template.html')}`; + const outFile = path.join(outDir, 'prejoin_capture_1280x720.png'); + console.log('Capturing', fileArg, '->', outFile); + await capture(fileArg, outFile, 1280, 720); + console.log('Saved capture to', outFile); + // If baseline exists in repo root under baselines/, compare + const baselineA = path.resolve(process.cwd(), 'baselines/prejoin_regen_1280x720.png'); + const baselineB = path.resolve(process.cwd(), 'baselines/prejoin_browserless_1280x720.png'); + let result = { captured: outFile, compared: false }; + if (fs.existsSync(baselineA)) { + const diffOut = path.join(outDir, 'prejoin_diff_1280x720.png'); + const metrics = compare(outFile, baselineA, diffOut); + result.compared = true; result.baseline = baselineA; result.diff = diffOut; result.metrics = metrics; + console.log('Compared with', baselineA, 'metrics=', metrics); + } else if (fs.existsSync(baselineB)) { + const diffOut = path.join(outDir, 'prejoin_diff_1280x720.png'); + const metrics = compare(outFile, baselineB, diffOut); + result.compared = true; result.baseline = baselineB; result.diff = diffOut; result.metrics = metrics; + console.log('Compared with', baselineB, 'metrics=', metrics); + } else { + console.log('No baseline found (checked baselines/prejoin_regen_1280x720.png and baselines/prejoin_browserless_1280x720.png)'); + } + fs.writeFileSync(path.join('/tmp','prejoin_capture_report.json'), JSON.stringify(result, null, 2)); + console.log('Report written to /tmp/prejoin_capture_report.json'); + } catch (err) { + console.error('Failed:', err); + process.exit(2); + } +})(); + diff --git a/scripts/capture_and_diff_prejoin.js b/scripts/capture_and_diff_prejoin.js new file mode 100644 index 0000000..a31b496 --- /dev/null +++ b/scripts/capture_and_diff_prejoin.js @@ -0,0 +1,64 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const puppeteer = require('puppeteer'); +const PNG = require('pngjs').PNG; +const pixelmatch = require('pixelmatch'); + +async function capture(fileUrl, outPath, width=1280, height=720) { + const browser = await puppeteer.launch({args: ['--no-sandbox','--disable-setuid-sandbox']}); + const page = await browser.newPage(); + await page.setViewport({ width, height }); + await page.goto(fileUrl, { waitUntil: 'networkidle0' }); + // wait a bit for scripts to run + await page.waitForTimeout(500); + await page.screenshot({ path: outPath, fullPage: false }); + await browser.close(); +} + +function compare(imgAPath, imgBPath, diffOut) { + const imgA = PNG.sync.read(fs.readFileSync(imgAPath)); + const imgB = PNG.sync.read(fs.readFileSync(imgBPath)); + if (imgA.width !== imgB.width || imgA.height !== imgB.height) { + throw new Error('Images must have same dimensions'); + } + const { width, height } = imgA; + const diff = new PNG({ width, height }); + const mismatched = pixelmatch(imgA.data, imgB.data, diff.data, width, height, { threshold: 0.1 }); + fs.writeFileSync(diffOut, PNG.sync.write(diff)); + return { mismatched, total: width*height, ratio: mismatched / (width*height) }; +} + +(async () => { + try { + const outDir = '/tmp'; + const fileArg = process.argv[2] || `file://${path.resolve(process.cwd(),'docs/prejoin_template.html')}`; + const outFile = path.join(outDir, 'prejoin_capture_1280x720.png'); + console.log('Capturing', fileArg, '->', outFile); + await capture(fileArg, outFile, 1280, 720); + console.log('Saved capture to', outFile); + // If baseline exists in repo root under baselines/, compare + const baselineA = path.resolve(process.cwd(), 'baselines/prejoin_regen_1280x720.png'); + const baselineB = path.resolve(process.cwd(), 'baselines/prejoin_browserless_1280x720.png'); + let result = { captured: outFile, compared: false }; + if (fs.existsSync(baselineA)) { + const diffOut = path.join(outDir, 'prejoin_diff_1280x720.png'); + const metrics = compare(outFile, baselineA, diffOut); + result.compared = true; result.baseline = baselineA; result.diff = diffOut; result.metrics = metrics; + console.log('Compared with', baselineA, 'metrics=', metrics); + } else if (fs.existsSync(baselineB)) { + const diffOut = path.join(outDir, 'prejoin_diff_1280x720.png'); + const metrics = compare(outFile, baselineB, diffOut); + result.compared = true; result.baseline = baselineB; result.diff = diffOut; result.metrics = metrics; + console.log('Compared with', baselineB, 'metrics=', metrics); + } else { + console.log('No baseline found (checked baselines/prejoin_regen_1280x720.png and baselines/prejoin_browserless_1280x720.png)'); + } + fs.writeFileSync(path.join('/tmp','prejoin_capture_report.json'), JSON.stringify(result, null, 2)); + console.log('Report written to /tmp/prejoin_capture_report.json'); + } catch (err) { + console.error('Failed:', err); + process.exit(2); + } +})(); + diff --git a/scripts/capture_regions_playwright.mjs b/scripts/capture_regions_playwright.mjs new file mode 100644 index 0000000..c6d588b --- /dev/null +++ b/scripts/capture_regions_playwright.mjs @@ -0,0 +1,57 @@ +import fs from 'fs'; +import path from 'path'; +import { chromium } from 'playwright'; + +async function captureRegions(url) { + const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] }); + const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); + await page.goto(url, { waitUntil: 'networkidle' }); + await page.waitForTimeout(500); + + const regions = [ + { name: 'video_preview', selector: '.video-preview' }, + { name: 'mic_status', selector: '.mic-status' }, + { name: 'controls', selector: '.controls' }, + { name: 'form', selector: 'form' } + ]; + + const outDir = '/tmp'; + const report = { url, captures: [] }; + + for (const r of regions) { + try { + const el = await page.$(r.selector); + if (!el) { + console.warn('Selector not found', r.selector); + report.captures.push({ name: r.name, selector: r.selector, found: false }); + continue; + } + const box = await el.boundingBox(); + const outPath = path.join(outDir, `prejoin_region_${r.name}.png`); + // use element screenshot to crop + await el.screenshot({ path: outPath }); + report.captures.push({ name: r.name, selector: r.selector, found: true, box, path: outPath }); + } catch (e) { + console.error('Failed capture for', r.selector, e); + report.captures.push({ name: r.name, selector: r.selector, found: false, error: String(e) }); + } + } + + await browser.close(); + const reportPath = path.join(outDir, 'prejoin_regions_report.json'); + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); + console.log('Wrote report', reportPath); + return reportPath; +} + +(async () => { + try { + const url = process.argv[2] || 'http://127.0.0.1:8000/docs/prejoin_template.html'; + const rp = await captureRegions(url); + console.log('Done, report at', rp); + } catch (e) { + console.error('Error running captureRegions', e); + process.exit(2); + } +})(); + diff --git a/scripts/compare_regions.cjs b/scripts/compare_regions.cjs new file mode 100644 index 0000000..74c619f --- /dev/null +++ b/scripts/compare_regions.cjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const { PNG } = require('pngjs'); +let pixelmatch = require('pixelmatch'); +if (pixelmatch && typeof pixelmatch !== 'function' && pixelmatch.default && typeof pixelmatch.default === 'function') pixelmatch = pixelmatch.default; + +const names = ['video_preview','mic_status','controls','form']; +const out = []; +for (const n of names) { + const base = `/tmp/baseline_prejoin_region_${n}.png`; + const cur = `/tmp/prejoin_region_${n}.png`; + if (!fs.existsSync(base) && !fs.existsSync(cur)) { + out.push({ region: n, foundBaseline: false, foundCurrent: false }); + continue; + } + if (!fs.existsSync(base)) { + out.push({ region: n, foundBaseline: false, foundCurrent: true, current: cur }); + continue; + } + if (!fs.existsSync(cur)) { + out.push({ region: n, foundBaseline: true, foundCurrent: false, baseline: base }); + continue; + } + try { + const A = PNG.sync.read(fs.readFileSync(base)); + const B = PNG.sync.read(fs.readFileSync(cur)); + if (A.width !== B.width || A.height !== B.height) { + out.push({ region: n, error: 'dim_mismatch', baseline: base, current: cur, baseSize: [A.width,A.height], curSize: [B.width,B.height] }); + continue; + } + const diff = new PNG({ width: A.width, height: A.height }); + const mismatched = pixelmatch(A.data, B.data, diff.data, A.width, A.height, { threshold: 0.1 }); + const diffPath = `/tmp/prejoin_region_diff_${n}.png`; + fs.writeFileSync(diffPath, PNG.sync.write(diff)); + out.push({ region: n, baseline: base, current: cur, diff: diffPath, mismatched, total: A.width*A.height, ratio: mismatched/(A.width*A.height) }); + } catch (e) { + out.push({ region: n, error: String(e) }); + } +} +const report = { generatedAt: new Date().toISOString(), report: out }; +fs.writeFileSync('/tmp/prejoin_regions_compare_report.json', JSON.stringify(report, null, 2)); +console.log('Wrote /tmp/prejoin_regions_compare_report.json'); +console.log(JSON.stringify(report, null, 2)); diff --git a/scripts/visual_test_prejoin.cjs b/scripts/visual_test_prejoin.cjs new file mode 100644 index 0000000..cf72842 --- /dev/null +++ b/scripts/visual_test_prejoin.cjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const args = process.argv.slice(2); +let url = 'http://127.0.0.1:8000/docs/prejoin_template.html'; +for (let i=0;i 0.1% +let failed = false; +for (const item of report.report) { + if (item.ratio && item.ratio > THRESHOLD) { + console.error(`[visual-test] REGION ${item.region} exceeded threshold: ratio=${item.ratio} mismatched=${item.mismatched} total=${item.total}`); + failed = true; + } else { + console.log(`[visual-test] REGION ${item.region}: ratio=${item.ratio || 0} mismatched=${item.mismatched || 0}/${item.total || 0}`); + } +} + +const outJson = '/tmp/prejoin_visual_test_summary.json'; +fs.writeFileSync(outJson, JSON.stringify({ url, threshold: THRESHOLD, generatedAt: new Date().toISOString(), report }, null, 2)); +console.log('[visual-test] summary written to', outJson); +if (failed) { + console.error('[visual-test] visual test FAILED'); + process.exit(3); +} +console.log('[visual-test] visual test PASSED'); +process.exit(0); +