openvibers/openvibe.live Commits
No Font Awesome: every icon is an OpenVibe glyph or brand (#500)
Changes
@@@ -385,10 +385,14 @@.bc-screen-advanced summary::-webkit-details-marker { display: none; }.bc-screen-advanced summary::before {-- content: '\f054';-- font-family: 'Font Awesome 6 Free';-- font-weight: 900;-- font-size: 0.7rem;++ /* The OpenVibe chevron (openvibe.design/icons/chevron), painted in the text's colour. */++ content: '';++ width: 0.75rem;++ height: 0.75rem;++ flex: none;++ background-color: currentColor;++ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;++ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;transition: transform 0.2s ease;}.bc-screen-advanced[open] summary::before {@@@ -358,7 +358,7 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }}.home-cta-feature:hover { transform: translateY(-3px); background: rgba(255,255,255,0.06); border-color: color-mix(in srgb, var(--accent) 35%, transparent); box-shadow: 0 12px 28px rgba(0,0,0,0.35); }.home-cta-feature.feat-star { border-color: color-mix(in srgb, var(--accent) 30%, transparent); background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 8%, transparent), rgba(255,255,255,0.02)); }--.home-cta-feature.feat-star::before { content: '\f005'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; top: 12px; right: 13px; font-size: 0.6rem; color: var(--accent); opacity: 0.8; }++.home-cta-feature.feat-star::before { content: ''; position: absolute; top: 12px; right: 13px; width: 0.7rem; height: 0.7rem; background-color: var(--accent); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M12 3.2l2.6 5.4 5.9.7-4.4 4 1.2 5.9L12 16.3l-5.3 2.9 1.2-5.9-4.4-4 5.9-.7Z'/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M12 3.2l2.6 5.4 5.9.7-4.4 4 1.2 5.9L12 16.3l-5.3 2.9 1.2-5.9-4.4-4 5.9-.7Z'/%3E%3C/svg%3E") center / contain no-repeat; opacity: 0.8; } /* the OpenVibe star, solid at this size */.home-cta-buttons { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }.cta-btn-glow::after {/* No spread and no outset: with either, the halo's edge reads as a hard ring around the button@@@ -8932,13 +8932,16 @@ body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.facrossed-out-speaker marker after the username. */.chat-msg-tts-off { opacity: 0.62; }.chat-msg-tts-off .chat-user::after {-- content: "\f6a9"; /* fa-volume-xmark */-- font-family: "Font Awesome 6 Free";-- font-weight: 900;-- font-size: 0.68em;++ /* The OpenVibe mute glyph, its solid drawing at this size (openvibe.design/icons/mute). */++ content: "";++ display: inline-block;++ width: 0.8em;++ height: 0.8em;margin-left: 5px;-- vertical-align: baseline;-- color: var(--text-muted);++ vertical-align: -0.05em;++ background-color: var(--text-muted);++ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M3.4 8.9h3.9l5.3-4.1v14.4l-5.3-4.1H3.4Z'/%3E%3Cpath fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round' d='m15.7 9.7 4.6 4.6M20.3 9.7l-4.6 4.6'/%3E%3C/svg%3E") center / contain no-repeat;++ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M3.4 8.9h3.9l5.3-4.1v14.4l-5.3-4.1H3.4Z'/%3E%3Cpath fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round' d='m15.7 9.7 4.6 4.6M20.3 9.7l-4.6 4.6'/%3E%3C/svg%3E") center / contain no-repeat;opacity: 0.85;}@@@ -14,7 +14,6 @@<meta property="og:image" content="https://openvibe.live/shared/social/live.jpg"><meta name="twitter:card" content="summary_large_image"><link rel="icon" type="image/svg+xml" href="/assets/logo.svg">-- <link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css"><style>*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }@@@ -203,6 +202,8 @@.page-footer a:hover { color: var(--accent-l); }.page-footer .sep { color: var(--border); }</style>++<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->++<script src="/shared/ov-glyphs.js" defer></script></head><body>@@@ -117,7 +117,6 @@their rules sit in the cascade exactly where they sat inside style.css. --><meta name="ov-css-anchor"><link rel="stylesheet" href="/css/i18n-star.css">-- <link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css"><link rel="stylesheet" href="/css/chat-shared.css" media="print" onload="this.media='all';this.onload=null"><link rel="stylesheet" href="/css/broadcast.css" media="print" onload="this.media='all';this.onload=null"><link rel="stylesheet" href="/css/cosmetics.css" media="print" onload="this.media='all';this.onload=null">@@@ -466,7 +466,7 @@ function renderOpenVibeAppMeta(data) {const ctaSub = document.getElementById('openvibeapp-cta-sub');if (ctaSub) {-- ctaSub.innerHTML = `<i class="fa-solid fa-code-branch"></i> Latest push ${esc(repo.pushedAt ? timeAgo(repo.pushedAt) : 'unknown')} · <i class="fa-solid fa-code-commit"></i> ${esc(latestCommit.shortSha || 'n/a')} · <i class="fa-brands fa-windows"></i> <i class="fa-brands fa-linux"></i> <i class="fa-brands fa-apple"></i> Windows, Linux & macOS`;++ ctaSub.innerHTML = `<i class="fa-solid fa-code-branch"></i> Latest push ${esc(repo.pushedAt ? timeAgo(repo.pushedAt) : 'unknown')} · <i class="fa-solid fa-code-commit"></i> ${esc(latestCommit.shortSha || 'n/a')} · <i class="fa-solid fa-desktop"></i> <i class="fa-brands fa-linux"></i> <i class="fa-brands fa-apple"></i> Windows, Linux & macOS`;}}@@@ -2424,7 +2424,7 @@ async function onManagedStreamCreated(newManagedStreamId) {const _wsRestreamPlatforms = {twitch: { name: 'Twitch', icon: 'fa-brands fa-twitch', color: '#9146ff', defaultUrl: 'rtmps://live.twitch.tv/app' },youtube: { name: 'YouTube', icon: 'fa-brands fa-youtube', color: '#ff0000', defaultUrl: 'rtmp://a.rtmp.youtube.com/live2' },-- kick: { name: 'Kick', icon: 'fa-solid fa-k', color: '#53fc18', defaultUrl: '' },++ kick: { name: 'Kick', icon: 'fa-brands fa-kick', color: '#53fc18', defaultUrl: '' },custom: { name: 'Custom RTMP', icon: 'fa-solid fa-globe', color: '#888', defaultUrl: '' },robotstreamer: { name: 'RobotStreamer', icon: 'fa-solid fa-robot', color: '#4a9eff', defaultUrl: '', isRobotStreamer: true },};@@@ -2990,11 +2990,19 @@ async function startMediaCapture(streamId, opts = {}) {const ctx = canvas.getContext('2d');ctx.fillStyle = '#1a1a2e';ctx.fillRect(0, 0, 640, 360);-- ctx.fillStyle = 'rgba(255,255,255,0.15)';-- ctx.font = '48px FontAwesome, sans-serif';++ // The OpenVibe mic glyph (openvibe.design/icons/mic), drawn on its 24-unit grid at 2.5x.++ ctx.save();++ ctx.translate(320 - 30, 160 - 30);++ ctx.scale(2.5, 2.5);++ ctx.strokeStyle = 'rgba(255,255,255,0.15)';++ ctx.lineWidth = 2;++ ctx.lineCap = 'round';++ ctx.lineJoin = 'round';++ ctx.stroke(new Path2D('M12 3a3 3 0 0 1 3 3v5a3 3 0 0 1-6 0V6a3 3 0 0 1 3-3Z'));++ ctx.stroke(new Path2D('M5 11a7 7 0 0 0 14 0M12 18v3'));++ ctx.restore();ctx.textAlign = 'center';ctx.textBaseline = 'middle';-- ctx.fillText('\uF130', 320, 160); // microphone iconctx.font = '18px sans-serif';ctx.fillStyle = 'rgba(255,255,255,0.4)';ctx.fillText('Audio Only', 320, 220);@@@ -3160,7 +3168,7 @@ function applyManualGain(streamId) {const RESTREAM_PLATFORM_META = {youtube: { name: 'YouTube', icon: 'fa-brands fa-youtube', color: '#ff0000' },twitch: { name: 'Twitch', icon: 'fa-brands fa-twitch', color: '#9146ff' },-- kick: { name: 'Kick', icon: 'fa-solid fa-k', color: '#53fc18' },++ kick: { name: 'Kick', icon: 'fa-brands fa-kick', color: '#53fc18' },custom: { name: 'Custom', icon: 'fa-solid fa-globe', color: '#888' },// RobotStreamer uses WebSocket (not RTMP) so it is NOT a standard RTMP destination.// It is listed here so the UI can show its status card and link to its own settings panel.@@@ -7,7 +7,6 @@<meta name="robots" content="noindex"><title>OpenVibe // New Tab</title><link rel="icon" href="/favicon.ico">--<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css"><link rel="stylesheet" href="/css/chat-shared.css"><style>:root{@@@ -514,6 +513,8 @@ button{font:inherit}}.omni-hint b{color:#97a2ad;font-weight:800}</style>++<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->++<script src="/shared/ov-glyphs.js" defer></script></head><body><div class="page">@@@ -1037,7 +1038,7 @@ button{font:inherit}const PROVIDERS={google:{name:"Google",url:"https://www.google.com/search?q=%s",icon:"fa-brands fa-google"},ddg:{name:"DuckDuckGo",url:"https://duckduckgo.com/?q=%s",icon:"fa-solid fa-shield-halved"},-- bing:{name:"Bing",url:"https://www.bing.com/search?q=%s",icon:"fa-brands fa-microsoft"},++ bing:{name:"Bing",url:"https://www.bing.com/search?q=%s",icon:"fa-solid fa-magnifying-glass"},brave:{name:"Brave",url:"https://search.brave.com/search?q=%s",icon:"fa-brands fa-brave"},startpage:{name:"Startpage",url:"https://www.startpage.com/sp/search?query=%s",icon:"fa-solid fa-user-secret"},ecosia:{name:"Ecosia",url:"https://www.ecosia.org/search?q=%s",icon:"fa-solid fa-leaf"}@@@ -4,8 +4,9 @@<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>OpenVibe.Live Media Queue</title>-- <link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css"><link rel="stylesheet" href="/css/media-player.css">++<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->++<script src="/shared/ov-glyphs.js" defer></script></head><body><div class="mp-shell">@@@ -45,8 +45,9 @@ body[data-pos=bottom] .al { animation-name: alInUp; }.al-status:empty { display: none; }@media (prefers-reduced-motion: reduce) { .al, .al.is-out { animation: none; } .al::after { display: none; } }</style>--<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css"><script src="/shared/theme-loader.js"></script>++<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->++<script src="/shared/ov-glyphs.js" defer></script></head><body><div class="al-stage" id="al-stage" aria-live="polite"></div>@@@ -4,7 +4,6 @@<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Chat Overlay</title>--<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css"><style>/* ── OBS Browser Source: transparent background ─────────────── */*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }@@@ -461,6 +460,8 @@ body.no-shadow .msg { text-shadow: none; }.spoiler { background: rgba(255, 255, 255, 0.35); color: transparent; border-radius: 4px; padding: 0 2px; }.spoiler * { visibility: hidden; }</style>++<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->++<script src="/shared/ov-glyphs.js" defer></script></head><body>@@@ -11,7 +11,6 @@<link rel="stylesheet" href="/css/chat-shared.css"><link rel="stylesheet" href="/css/i18n-star.css"><link rel="stylesheet" href="/css/cosmetics.css">-- <link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css"><style>html, body { height: 100%; margin: 0; overflow: hidden; }body { display: flex; flex-direction: column; background: var(--bg-primary, #0d0d1a); }@@@ -148,6 +147,8 @@.pc-messages .chat-deploy-t { display: none; }}</style>++<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->++<script src="/shared/ov-glyphs.js" defer></script></head><body><div class="pc-header">@@@ -14,7 +14,6 @@<meta property="og:image" content="https://openvibe.live/shared/social/live.jpg"><meta name="twitter:card" content="summary_large_image"><link rel="icon" type="image/svg+xml" href="/assets/logo.svg">-- <link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css"><style>*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }@@@ -199,6 +198,8 @@.page-footer a:hover { color: var(--accent-l); }.page-footer .sep { color: var(--border); }</style>++<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->++<script src="/shared/ov-glyphs.js" defer></script></head><body>@@@ -14,7 +14,6 @@<meta property="og:image" content="https://openvibe.live/shared/social/live.jpg"><meta name="twitter:card" content="summary_large_image"><link rel="icon" type="image/svg+xml" href="/assets/logo.svg">-- <link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css"><style>*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }@@@ -199,6 +198,8 @@.page-footer a:hover { color: var(--accent-l); }.page-footer .sep { color: var(--border); }</style>++<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->++<script src="/shared/ov-glyphs.js" defer></script></head><body>@@@ -31,7 +31,7 @@ const PLATFORMS = {patreon: { name: 'Patreon', icon: 'fa-brands fa-patreon', color: '#ff424d', hosts: ['patreon.com'], url: (h) => `https://www.patreon.com/${h}`, handle: /^\/([A-Za-z0-9_-]{2,64})\/?$/ },spotify: { name: 'Spotify', icon: 'fa-brands fa-spotify', color: '#1db954', hosts: ['open.spotify.com'], url: null, handle: null },soundcloud:{ name: 'SoundCloud',icon: 'fa-brands fa-soundcloud',color: '#ff5500', hosts: ['soundcloud.com'], url: (h) => `https://soundcloud.com/${h}`, handle: /^\/([A-Za-z0-9_-]{2,64})\/?$/ },-- linkedin: { name: 'LinkedIn', icon: 'fa-brands fa-linkedin', color: '#0a66c2', hosts: ['linkedin.com'], url: (h) => `https://www.linkedin.com/in/${h}`, handle: /^\/in\/([A-Za-z0-9-]{3,100})\/?$/ },++ linkedin: { name: 'LinkedIn', icon: 'fa-solid fa-briefcase', color: '#0a66c2', hosts: ['linkedin.com'], url: (h) => `https://www.linkedin.com/in/${h}`, handle: /^\/in\/([A-Za-z0-9-]{3,100})\/?$/ },mastodon: { name: 'Mastodon', icon: 'fa-brands fa-mastodon', color: '#6364ff', hosts: null, url: null, handle: /^\/@([A-Za-z0-9_]{1,30})\/?$/ },openvibe: { name: 'OpenVibe', icon: 'fa-solid fa-id-card', color: '#3b82f6', hosts: ['openvibe.network'], url: (h) => `https://openvibe.network/@${h}`, handle: /^\/@([A-Za-z0-9_]{3,24})\/?$/ },website: { name: 'Website', icon: 'fa-solid fa-globe', color: '#60a5fa', hosts: null, url: null, handle: null },@@@ -71,7 +71,7 @@ function validateIngestUrl(raw) {const PLATFORM_PRESETS = {youtube: { name: 'YouTube', defaultServerUrl: 'rtmp://a.rtmp.youtube.com/live2', icon: 'fa-brands fa-youtube', color: '#ff0000' },twitch: { name: 'Twitch', defaultServerUrl: 'rtmps://live.twitch.tv/app', icon: 'fa-brands fa-twitch', color: '#9146ff' },-- kick: { name: 'Kick', defaultServerUrl: '', icon: 'fa-solid fa-k', color: '#53fc18' },++ kick: { name: 'Kick', defaultServerUrl: '', icon: 'fa-brands fa-kick', color: '#53fc18' },custom: { name: 'Custom RTMP', defaultServerUrl: '', icon: 'fa-solid fa-globe', color: '#888' },};nnew file mode 100644@@@ -0,0 +1,58 @@++'use strict';++// No Font Awesome (plan L54 K4; the owner, 2026-11-01: "get rid of Font Awesome completely and just use our own++// openvibe.design system"). Live's icons are the OpenVibe glyphs (openvibe-shared/glyphs, drawn on the server by++// server/web/glyphs-html.js and in the browser by /shared/ov-glyphs.js) and brand logos from openvibe-shared/brands.++// This holds it there:++// - no page links Font Awesome's stylesheet, and every page without the navbar loads the glyph drop-in;++// - no stylesheet draws with Font Awesome's font;++// - every fa- name Live uses is one the glyphs or the brands draw (a name nobody draws would show nothing).++// node test/no-font-awesome.test.js++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const glyphs = require('openvibe-shared/glyphs');++const brands = require('openvibe-shared/brands');++++const ROOT = path.join(__dirname, '..');++const walk = (dir, ok, out = []) => {++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {++ if (e.name === 'node_modules' || e.name === 'vendor' || e.name.startsWith('.')) continue;++ const p = path.join(dir, e.name);++ if (e.isDirectory()) walk(p, ok, out); else if (ok.test(e.name)) out.push(p);++ }++ return out;++};++const rel = (p) => path.relative(ROOT, p);++++const pages = walk(path.join(ROOT, 'public'), /\.html$/);++for (const p of pages) {++ const html = fs.readFileSync(p, 'utf8');++ assert.ok(!/fontawesome|font-awesome/i.test(html), `${rel(p)} links Font Awesome`);++ // A whole page (with a <head>; fragments are parts of index.html) that names an icon loads what draws it.++ if (/<head[\s>]/i.test(html) && /\bfa-[a-z]/.test(html) && !/\/shared\/navbar|ov-glyphs\.js/.test(html)) assert.fail(`${rel(p)} has fa- icons but loads neither the navbar nor /shared/ov-glyphs.js`);++}++++for (const p of walk(path.join(ROOT, 'public'), /\.css$/)) {++ const css = fs.readFileSync(p, 'utf8');++ assert.ok(!/font-family:\s*["']?Font ?Awesome/i.test(css), `${rel(p)} draws with Font Awesome's font`);++}++for (const p of walk(path.join(ROOT, 'public'), /\.js$/)) {++ assert.ok(!/['"]\d+px FontAwesome|Font Awesome 6 Free/.test(fs.readFileSync(p, 'utf8')), `${rel(p)} draws with Font Awesome's font`);++}++++// Every fa- name in Live's pages, scripts and server views is drawn by a glyph or a brand.++const MOD = /^(solid|regular|light|thin|duotone|brands|sharp|classic|fw|lg|xl|xs|sm|2xs|2xl|\d+x|li|ul|border|inverse|pull-left|pull-right|rotate-\d+|rotate-by|flip-horizontal|flip-vertical|flip-both|width-auto|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|swap-opacity|stack-text)$/;++// Names that only look like icons: built from parts at run time (fa-chevron-${dir} is fa-chevron-left/right).++const PARTS = new Set(['chevron']);++const files = [...walk(path.join(ROOT, 'public'), /\.(html|js)$/), ...walk(path.join(ROOT, 'server'), /\.js$/)];++const missing = new Map();++for (const f of files) {++ for (const m of fs.readFileSync(f, 'utf8').matchAll(/\bfa-([a-z0-9]+(?:-[a-z0-9]+)*)\b/g)) {++ const n = m[1];++ if (MOD.test(n) || PARTS.has(n) || glyphs.fromFa(n) || brands.fromFa(n)) continue;++ if (!missing.has(n)) missing.set(n, rel(f));++ }++}++assert.deepStrictEqual([...missing].map(([n, f]) => `fa-${n} (${f})`), [], 'every icon Live names is drawn by the OpenVibe glyphs or brands');++++console.log(`no font awesome: ${pages.length} pages, ${files.length} files, every fa- name drawn`);
@@ -385,10 +385,14 @@
@@ -385,10 +385,14 @@
.bc-screen-advanced summary::-webkit-details-marker { display: none; }
.bc-screen-advanced summary::-webkit-details-marker { display: none; }
.bc-screen-advanced summary::before {
.bc-screen-advanced summary::before {
- content: '\f054';
- font-family: 'Font Awesome 6 Free';
- font-weight: 900;
- font-size: 0.7rem;
+ /* The OpenVibe chevron (openvibe.design/icons/chevron), painted in the text's colour. */
+ content: '';
+ width: 0.75rem;
+ height: 0.75rem;
+ flex: none;
+ background-color: currentColor;
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
transition: transform 0.2s ease;
transition: transform 0.2s ease;
}
}
.bc-screen-advanced[open] summary::before {
.bc-screen-advanced[open] summary::before {
@@ -358,7 +358,7 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
@@ -358,7 +358,7 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
}
}
.home-cta-feature:hover { transform: translateY(-3px); background: rgba(255,255,255,0.06); border-color: color-mix(in srgb, var(--accent) 35%, transparent); box-shadow: 0 12px 28px rgba(0,0,0,0.35); }
.home-cta-feature:hover { transform: translateY(-3px); background: rgba(255,255,255,0.06); border-color: color-mix(in srgb, var(--accent) 35%, transparent); box-shadow: 0 12px 28px rgba(0,0,0,0.35); }
.home-cta-feature.feat-star { border-color: color-mix(in srgb, var(--accent) 30%, transparent); background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 8%, transparent), rgba(255,255,255,0.02)); }
.home-cta-feature.feat-star { border-color: color-mix(in srgb, var(--accent) 30%, transparent); background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 8%, transparent), rgba(255,255,255,0.02)); }
-.home-cta-feature.feat-star::before { content: '\f005'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; top: 12px; right: 13px; font-size: 0.6rem; color: var(--accent); opacity: 0.8; }
+.home-cta-feature.feat-star::before { content: ''; position: absolute; top: 12px; right: 13px; width: 0.7rem; height: 0.7rem; background-color: var(--accent); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M12 3.2l2.6 5.4 5.9.7-4.4 4 1.2 5.9L12 16.3l-5.3 2.9 1.2-5.9-4.4-4 5.9-.7Z'/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M12 3.2l2.6 5.4 5.9.7-4.4 4 1.2 5.9L12 16.3l-5.3 2.9 1.2-5.9-4.4-4 5.9-.7Z'/%3E%3C/svg%3E") center / contain no-repeat; opacity: 0.8; } /* the OpenVibe star, solid at this size */
.home-cta-buttons { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.home-cta-buttons { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.cta-btn-glow::after {
.cta-btn-glow::after {
/* No spread and no outset: with either, the halo's edge reads as a hard ring around the button
/* No spread and no outset: with either, the halo's edge reads as a hard ring around the button
@@ -8932,13 +8932,16 @@ body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.fa
@@ -8932,13 +8932,16 @@ body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.fa
crossed-out-speaker marker after the username. */
crossed-out-speaker marker after the username. */
.chat-msg-tts-off { opacity: 0.62; }
.chat-msg-tts-off { opacity: 0.62; }
.chat-msg-tts-off .chat-user::after {
.chat-msg-tts-off .chat-user::after {
- content: "\f6a9"; /* fa-volume-xmark */
- font-family: "Font Awesome 6 Free";
- font-weight: 900;
- font-size: 0.68em;
+ /* The OpenVibe mute glyph, its solid drawing at this size (openvibe.design/icons/mute). */
+ content: "";
+ display: inline-block;
+ width: 0.8em;
+ height: 0.8em;
margin-left: 5px;
margin-left: 5px;
- vertical-align: baseline;
- color: var(--text-muted);
+ vertical-align: -0.05em;
+ background-color: var(--text-muted);
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M3.4 8.9h3.9l5.3-4.1v14.4l-5.3-4.1H3.4Z'/%3E%3Cpath fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round' d='m15.7 9.7 4.6 4.6M20.3 9.7l-4.6 4.6'/%3E%3C/svg%3E") center / contain no-repeat;
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M3.4 8.9h3.9l5.3-4.1v14.4l-5.3-4.1H3.4Z'/%3E%3Cpath fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round' d='m15.7 9.7 4.6 4.6M20.3 9.7l-4.6 4.6'/%3E%3C/svg%3E") center / contain no-repeat;
opacity: 0.85;
opacity: 0.85;
}
}
@@ -14,7 +14,6 @@
@@ -14,7 +14,6 @@
<meta property="og:image" content="https://openvibe.live/shared/social/live.jpg">
<meta property="og:image" content="https://openvibe.live/shared/social/live.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
- <link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<style>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -203,6 +202,8 @@
@@ -203,6 +202,8 @@
.page-footer a:hover { color: var(--accent-l); }
.page-footer a:hover { color: var(--accent-l); }
.page-footer .sep { color: var(--border); }
.page-footer .sep { color: var(--border); }
</style>
</style>
+<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
+<script src="/shared/ov-glyphs.js" defer></script>
</head>
</head>
<body>
<body>
@@ -117,7 +117,6 @@
@@ -117,7 +117,6 @@
their rules sit in the cascade exactly where they sat inside style.css. -->
their rules sit in the cascade exactly where they sat inside style.css. -->
<meta name="ov-css-anchor">
<meta name="ov-css-anchor">
<link rel="stylesheet" href="/css/i18n-star.css">
<link rel="stylesheet" href="/css/i18n-star.css">
- <link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<link rel="stylesheet" href="/css/chat-shared.css" media="print" onload="this.media='all';this.onload=null">
<link rel="stylesheet" href="/css/chat-shared.css" media="print" onload="this.media='all';this.onload=null">
<link rel="stylesheet" href="/css/broadcast.css" media="print" onload="this.media='all';this.onload=null">
<link rel="stylesheet" href="/css/broadcast.css" media="print" onload="this.media='all';this.onload=null">
<link rel="stylesheet" href="/css/cosmetics.css" media="print" onload="this.media='all';this.onload=null">
<link rel="stylesheet" href="/css/cosmetics.css" media="print" onload="this.media='all';this.onload=null">
@@ -466,7 +466,7 @@ function renderOpenVibeAppMeta(data) {
@@ -466,7 +466,7 @@ function renderOpenVibeAppMeta(data) {
const ctaSub = document.getElementById('openvibeapp-cta-sub');
const ctaSub = document.getElementById('openvibeapp-cta-sub');
if (ctaSub) {
if (ctaSub) {
- ctaSub.innerHTML = `<i class="fa-solid fa-code-branch"></i> Latest push ${esc(repo.pushedAt ? timeAgo(repo.pushedAt) : 'unknown')} · <i class="fa-solid fa-code-commit"></i> ${esc(latestCommit.shortSha || 'n/a')} · <i class="fa-brands fa-windows"></i> <i class="fa-brands fa-linux"></i> <i class="fa-brands fa-apple"></i> Windows, Linux & macOS`;
+ ctaSub.innerHTML = `<i class="fa-solid fa-code-branch"></i> Latest push ${esc(repo.pushedAt ? timeAgo(repo.pushedAt) : 'unknown')} · <i class="fa-solid fa-code-commit"></i> ${esc(latestCommit.shortSha || 'n/a')} · <i class="fa-solid fa-desktop"></i> <i class="fa-brands fa-linux"></i> <i class="fa-brands fa-apple"></i> Windows, Linux & macOS`;
}
}
}
}
@@ -2424,7 +2424,7 @@ async function onManagedStreamCreated(newManagedStreamId) {
@@ -2424,7 +2424,7 @@ async function onManagedStreamCreated(newManagedStreamId) {
const _wsRestreamPlatforms = {
const _wsRestreamPlatforms = {
twitch: { name: 'Twitch', icon: 'fa-brands fa-twitch', color: '#9146ff', defaultUrl: 'rtmps://live.twitch.tv/app' },
twitch: { name: 'Twitch', icon: 'fa-brands fa-twitch', color: '#9146ff', defaultUrl: 'rtmps://live.twitch.tv/app' },
youtube: { name: 'YouTube', icon: 'fa-brands fa-youtube', color: '#ff0000', defaultUrl: 'rtmp://a.rtmp.youtube.com/live2' },
youtube: { name: 'YouTube', icon: 'fa-brands fa-youtube', color: '#ff0000', defaultUrl: 'rtmp://a.rtmp.youtube.com/live2' },
- kick: { name: 'Kick', icon: 'fa-solid fa-k', color: '#53fc18', defaultUrl: '' },
+ kick: { name: 'Kick', icon: 'fa-brands fa-kick', color: '#53fc18', defaultUrl: '' },
custom: { name: 'Custom RTMP', icon: 'fa-solid fa-globe', color: '#888', defaultUrl: '' },
custom: { name: 'Custom RTMP', icon: 'fa-solid fa-globe', color: '#888', defaultUrl: '' },
robotstreamer: { name: 'RobotStreamer', icon: 'fa-solid fa-robot', color: '#4a9eff', defaultUrl: '', isRobotStreamer: true },
robotstreamer: { name: 'RobotStreamer', icon: 'fa-solid fa-robot', color: '#4a9eff', defaultUrl: '', isRobotStreamer: true },
};
};
@@ -2990,11 +2990,19 @@ async function startMediaCapture(streamId, opts = {}) {
@@ -2990,11 +2990,19 @@ async function startMediaCapture(streamId, opts = {}) {
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#1a1a2e';
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, 640, 360);
ctx.fillRect(0, 0, 640, 360);
- ctx.fillStyle = 'rgba(255,255,255,0.15)';
- ctx.font = '48px FontAwesome, sans-serif';
+ // The OpenVibe mic glyph (openvibe.design/icons/mic), drawn on its 24-unit grid at 2.5x.
+ ctx.save();
+ ctx.translate(320 - 30, 160 - 30);
+ ctx.scale(2.5, 2.5);
+ ctx.strokeStyle = 'rgba(255,255,255,0.15)';
+ ctx.lineWidth = 2;
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
+ ctx.stroke(new Path2D('M12 3a3 3 0 0 1 3 3v5a3 3 0 0 1-6 0V6a3 3 0 0 1 3-3Z'));
+ ctx.stroke(new Path2D('M5 11a7 7 0 0 0 14 0M12 18v3'));
+ ctx.restore();
ctx.textAlign = 'center';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.textBaseline = 'middle';
- ctx.fillText('\uF130', 320, 160); // microphone icon
ctx.font = '18px sans-serif';
ctx.font = '18px sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.fillText('Audio Only', 320, 220);
ctx.fillText('Audio Only', 320, 220);
@@ -3160,7 +3168,7 @@ function applyManualGain(streamId) {
@@ -3160,7 +3168,7 @@ function applyManualGain(streamId) {
const RESTREAM_PLATFORM_META = {
const RESTREAM_PLATFORM_META = {
youtube: { name: 'YouTube', icon: 'fa-brands fa-youtube', color: '#ff0000' },
youtube: { name: 'YouTube', icon: 'fa-brands fa-youtube', color: '#ff0000' },
twitch: { name: 'Twitch', icon: 'fa-brands fa-twitch', color: '#9146ff' },
twitch: { name: 'Twitch', icon: 'fa-brands fa-twitch', color: '#9146ff' },
- kick: { name: 'Kick', icon: 'fa-solid fa-k', color: '#53fc18' },
+ kick: { name: 'Kick', icon: 'fa-brands fa-kick', color: '#53fc18' },
custom: { name: 'Custom', icon: 'fa-solid fa-globe', color: '#888' },
custom: { name: 'Custom', icon: 'fa-solid fa-globe', color: '#888' },
// RobotStreamer uses WebSocket (not RTMP) so it is NOT a standard RTMP destination.
// RobotStreamer uses WebSocket (not RTMP) so it is NOT a standard RTMP destination.
// It is listed here so the UI can show its status card and link to its own settings panel.
// It is listed here so the UI can show its status card and link to its own settings panel.
@@ -7,7 +7,6 @@
@@ -7,7 +7,6 @@
<meta name="robots" content="noindex">
<meta name="robots" content="noindex">
<title>OpenVibe // New Tab</title>
<title>OpenVibe // New Tab</title>
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/favicon.ico">
-<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<link rel="stylesheet" href="/css/chat-shared.css">
<link rel="stylesheet" href="/css/chat-shared.css">
<style>
<style>
:root{
:root{
@@ -514,6 +513,8 @@ button{font:inherit}
@@ -514,6 +513,8 @@ button{font:inherit}
}
}
.omni-hint b{color:#97a2ad;font-weight:800}
.omni-hint b{color:#97a2ad;font-weight:800}
</style>
</style>
+<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
+<script src="/shared/ov-glyphs.js" defer></script>
</head>
</head>
<body>
<body>
<div class="page">
<div class="page">
@@ -1037,7 +1038,7 @@ button{font:inherit}
@@ -1037,7 +1038,7 @@ button{font:inherit}
const PROVIDERS={
const PROVIDERS={
google:{name:"Google",url:"https://www.google.com/search?q=%s",icon:"fa-brands fa-google"},
google:{name:"Google",url:"https://www.google.com/search?q=%s",icon:"fa-brands fa-google"},
ddg:{name:"DuckDuckGo",url:"https://duckduckgo.com/?q=%s",icon:"fa-solid fa-shield-halved"},
ddg:{name:"DuckDuckGo",url:"https://duckduckgo.com/?q=%s",icon:"fa-solid fa-shield-halved"},
- bing:{name:"Bing",url:"https://www.bing.com/search?q=%s",icon:"fa-brands fa-microsoft"},
+ bing:{name:"Bing",url:"https://www.bing.com/search?q=%s",icon:"fa-solid fa-magnifying-glass"},
brave:{name:"Brave",url:"https://search.brave.com/search?q=%s",icon:"fa-brands fa-brave"},
brave:{name:"Brave",url:"https://search.brave.com/search?q=%s",icon:"fa-brands fa-brave"},
startpage:{name:"Startpage",url:"https://www.startpage.com/sp/search?query=%s",icon:"fa-solid fa-user-secret"},
startpage:{name:"Startpage",url:"https://www.startpage.com/sp/search?query=%s",icon:"fa-solid fa-user-secret"},
ecosia:{name:"Ecosia",url:"https://www.ecosia.org/search?q=%s",icon:"fa-solid fa-leaf"}
ecosia:{name:"Ecosia",url:"https://www.ecosia.org/search?q=%s",icon:"fa-solid fa-leaf"}
@@ -4,8 +4,9 @@
@@ -4,8 +4,9 @@
<meta charset="UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenVibe.Live Media Queue</title>
<title>OpenVibe.Live Media Queue</title>
- <link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<link rel="stylesheet" href="/css/media-player.css">
<link rel="stylesheet" href="/css/media-player.css">
+<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
+<script src="/shared/ov-glyphs.js" defer></script>
</head>
</head>
<body>
<body>
<div class="mp-shell">
<div class="mp-shell">
@@ -45,8 +45,9 @@ body[data-pos=bottom] .al { animation-name: alInUp; }
@@ -45,8 +45,9 @@ body[data-pos=bottom] .al { animation-name: alInUp; }
.al-status:empty { display: none; }
.al-status:empty { display: none; }
@media (prefers-reduced-motion: reduce) { .al, .al.is-out { animation: none; } .al::after { display: none; } }
@media (prefers-reduced-motion: reduce) { .al, .al.is-out { animation: none; } .al::after { display: none; } }
</style>
</style>
-<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<script src="/shared/theme-loader.js"></script>
<script src="/shared/theme-loader.js"></script>
+<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
+<script src="/shared/ov-glyphs.js" defer></script>
</head>
</head>
<body>
<body>
<div class="al-stage" id="al-stage" aria-live="polite"></div>
<div class="al-stage" id="al-stage" aria-live="polite"></div>
@@ -4,7 +4,6 @@
@@ -4,7 +4,6 @@
<meta charset="UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Overlay</title>
<title>Chat Overlay</title>
-<link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<style>
<style>
/* ── OBS Browser Source: transparent background ─────────────── */
/* ── OBS Browser Source: transparent background ─────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -461,6 +460,8 @@ body.no-shadow .msg { text-shadow: none; }
@@ -461,6 +460,8 @@ body.no-shadow .msg { text-shadow: none; }
.spoiler { background: rgba(255, 255, 255, 0.35); color: transparent; border-radius: 4px; padding: 0 2px; }
.spoiler { background: rgba(255, 255, 255, 0.35); color: transparent; border-radius: 4px; padding: 0 2px; }
.spoiler * { visibility: hidden; }
.spoiler * { visibility: hidden; }
</style>
</style>
+<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
+<script src="/shared/ov-glyphs.js" defer></script>
</head>
</head>
<body>
<body>
@@ -11,7 +11,6 @@
@@ -11,7 +11,6 @@
<link rel="stylesheet" href="/css/chat-shared.css">
<link rel="stylesheet" href="/css/chat-shared.css">
<link rel="stylesheet" href="/css/i18n-star.css">
<link rel="stylesheet" href="/css/i18n-star.css">
<link rel="stylesheet" href="/css/cosmetics.css">
<link rel="stylesheet" href="/css/cosmetics.css">
- <link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<style>
<style>
html, body { height: 100%; margin: 0; overflow: hidden; }
html, body { height: 100%; margin: 0; overflow: hidden; }
body { display: flex; flex-direction: column; background: var(--bg-primary, #0d0d1a); }
body { display: flex; flex-direction: column; background: var(--bg-primary, #0d0d1a); }
@@ -148,6 +147,8 @@
@@ -148,6 +147,8 @@
.pc-messages .chat-deploy-t { display: none; }
.pc-messages .chat-deploy-t { display: none; }
}
}
</style>
</style>
+<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
+<script src="/shared/ov-glyphs.js" defer></script>
</head>
</head>
<body>
<body>
<div class="pc-header">
<div class="pc-header">
@@ -14,7 +14,6 @@
@@ -14,7 +14,6 @@
<meta property="og:image" content="https://openvibe.live/shared/social/live.jpg">
<meta property="og:image" content="https://openvibe.live/shared/social/live.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
- <link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<style>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -199,6 +198,8 @@
@@ -199,6 +198,8 @@
.page-footer a:hover { color: var(--accent-l); }
.page-footer a:hover { color: var(--accent-l); }
.page-footer .sep { color: var(--border); }
.page-footer .sep { color: var(--border); }
</style>
</style>
+<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
+<script src="/shared/ov-glyphs.js" defer></script>
</head>
</head>
<body>
<body>
@@ -14,7 +14,6 @@
@@ -14,7 +14,6 @@
<meta property="og:image" content="https://openvibe.live/shared/social/live.jpg">
<meta property="og:image" content="https://openvibe.live/shared/social/live.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
- <link rel="stylesheet" href="/css/vendor/fontawesome-6.5.1.min.css">
<style>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -199,6 +198,8 @@
@@ -199,6 +198,8 @@
.page-footer a:hover { color: var(--accent-l); }
.page-footer a:hover { color: var(--accent-l); }
.page-footer .sep { color: var(--border); }
.page-footer .sep { color: var(--border); }
</style>
</style>
+<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
+<script src="/shared/ov-glyphs.js" defer></script>
</head>
</head>
<body>
<body>
@@ -31,7 +31,7 @@ const PLATFORMS = {
@@ -31,7 +31,7 @@ const PLATFORMS = {
patreon: { name: 'Patreon', icon: 'fa-brands fa-patreon', color: '#ff424d', hosts: ['patreon.com'], url: (h) => `https://www.patreon.com/${h}`, handle: /^\/([A-Za-z0-9_-]{2,64})\/?$/ },
patreon: { name: 'Patreon', icon: 'fa-brands fa-patreon', color: '#ff424d', hosts: ['patreon.com'], url: (h) => `https://www.patreon.com/${h}`, handle: /^\/([A-Za-z0-9_-]{2,64})\/?$/ },
spotify: { name: 'Spotify', icon: 'fa-brands fa-spotify', color: '#1db954', hosts: ['open.spotify.com'], url: null, handle: null },
spotify: { name: 'Spotify', icon: 'fa-brands fa-spotify', color: '#1db954', hosts: ['open.spotify.com'], url: null, handle: null },
soundcloud:{ name: 'SoundCloud',icon: 'fa-brands fa-soundcloud',color: '#ff5500', hosts: ['soundcloud.com'], url: (h) => `https://soundcloud.com/${h}`, handle: /^\/([A-Za-z0-9_-]{2,64})\/?$/ },
soundcloud:{ name: 'SoundCloud',icon: 'fa-brands fa-soundcloud',color: '#ff5500', hosts: ['soundcloud.com'], url: (h) => `https://soundcloud.com/${h}`, handle: /^\/([A-Za-z0-9_-]{2,64})\/?$/ },
- linkedin: { name: 'LinkedIn', icon: 'fa-brands fa-linkedin', color: '#0a66c2', hosts: ['linkedin.com'], url: (h) => `https://www.linkedin.com/in/${h}`, handle: /^\/in\/([A-Za-z0-9-]{3,100})\/?$/ },
+ linkedin: { name: 'LinkedIn', icon: 'fa-solid fa-briefcase', color: '#0a66c2', hosts: ['linkedin.com'], url: (h) => `https://www.linkedin.com/in/${h}`, handle: /^\/in\/([A-Za-z0-9-]{3,100})\/?$/ },
mastodon: { name: 'Mastodon', icon: 'fa-brands fa-mastodon', color: '#6364ff', hosts: null, url: null, handle: /^\/@([A-Za-z0-9_]{1,30})\/?$/ },
mastodon: { name: 'Mastodon', icon: 'fa-brands fa-mastodon', color: '#6364ff', hosts: null, url: null, handle: /^\/@([A-Za-z0-9_]{1,30})\/?$/ },
openvibe: { name: 'OpenVibe', icon: 'fa-solid fa-id-card', color: '#3b82f6', hosts: ['openvibe.network'], url: (h) => `https://openvibe.network/@${h}`, handle: /^\/@([A-Za-z0-9_]{3,24})\/?$/ },
openvibe: { name: 'OpenVibe', icon: 'fa-solid fa-id-card', color: '#3b82f6', hosts: ['openvibe.network'], url: (h) => `https://openvibe.network/@${h}`, handle: /^\/@([A-Za-z0-9_]{3,24})\/?$/ },
website: { name: 'Website', icon: 'fa-solid fa-globe', color: '#60a5fa', hosts: null, url: null, handle: null },
website: { name: 'Website', icon: 'fa-solid fa-globe', color: '#60a5fa', hosts: null, url: null, handle: null },
@@ -71,7 +71,7 @@ function validateIngestUrl(raw) {
@@ -71,7 +71,7 @@ function validateIngestUrl(raw) {
const PLATFORM_PRESETS = {
const PLATFORM_PRESETS = {
youtube: { name: 'YouTube', defaultServerUrl: 'rtmp://a.rtmp.youtube.com/live2', icon: 'fa-brands fa-youtube', color: '#ff0000' },
youtube: { name: 'YouTube', defaultServerUrl: 'rtmp://a.rtmp.youtube.com/live2', icon: 'fa-brands fa-youtube', color: '#ff0000' },
twitch: { name: 'Twitch', defaultServerUrl: 'rtmps://live.twitch.tv/app', icon: 'fa-brands fa-twitch', color: '#9146ff' },
twitch: { name: 'Twitch', defaultServerUrl: 'rtmps://live.twitch.tv/app', icon: 'fa-brands fa-twitch', color: '#9146ff' },
- kick: { name: 'Kick', defaultServerUrl: '', icon: 'fa-solid fa-k', color: '#53fc18' },
+ kick: { name: 'Kick', defaultServerUrl: '', icon: 'fa-brands fa-kick', color: '#53fc18' },
custom: { name: 'Custom RTMP', defaultServerUrl: '', icon: 'fa-solid fa-globe', color: '#888' },
custom: { name: 'Custom RTMP', defaultServerUrl: '', icon: 'fa-solid fa-globe', color: '#888' },
};
};
new file mode 100644
new file mode 100644
@@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
+'use strict';
+// No Font Awesome (plan L54 K4; the owner, 2026-11-01: "get rid of Font Awesome completely and just use our own
+// openvibe.design system"). Live's icons are the OpenVibe glyphs (openvibe-shared/glyphs, drawn on the server by
+// server/web/glyphs-html.js and in the browser by /shared/ov-glyphs.js) and brand logos from openvibe-shared/brands.
+// This holds it there:
+// - no page links Font Awesome's stylesheet, and every page without the navbar loads the glyph drop-in;
+// - no stylesheet draws with Font Awesome's font;
+// - every fa- name Live uses is one the glyphs or the brands draw (a name nobody draws would show nothing).
+// node test/no-font-awesome.test.js
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const glyphs = require('openvibe-shared/glyphs');
+const brands = require('openvibe-shared/brands');
+
+const ROOT = path.join(__dirname, '..');
+const walk = (dir, ok, out = []) => {
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
+ if (e.name === 'node_modules' || e.name === 'vendor' || e.name.startsWith('.')) continue;
+ const p = path.join(dir, e.name);
+ if (e.isDirectory()) walk(p, ok, out); else if (ok.test(e.name)) out.push(p);
+ }
+ return out;
+};
+const rel = (p) => path.relative(ROOT, p);
+
+const pages = walk(path.join(ROOT, 'public'), /\.html$/);
+for (const p of pages) {
+ const html = fs.readFileSync(p, 'utf8');
+ assert.ok(!/fontawesome|font-awesome/i.test(html), `${rel(p)} links Font Awesome`);
+ // A whole page (with a <head>; fragments are parts of index.html) that names an icon loads what draws it.
+ if (/<head[\s>]/i.test(html) && /\bfa-[a-z]/.test(html) && !/\/shared\/navbar|ov-glyphs\.js/.test(html)) assert.fail(`${rel(p)} has fa- icons but loads neither the navbar nor /shared/ov-glyphs.js`);
+}
+
+for (const p of walk(path.join(ROOT, 'public'), /\.css$/)) {
+ const css = fs.readFileSync(p, 'utf8');
+ assert.ok(!/font-family:\s*["']?Font ?Awesome/i.test(css), `${rel(p)} draws with Font Awesome's font`);
+}
+for (const p of walk(path.join(ROOT, 'public'), /\.js$/)) {
+ assert.ok(!/['"]\d+px FontAwesome|Font Awesome 6 Free/.test(fs.readFileSync(p, 'utf8')), `${rel(p)} draws with Font Awesome's font`);
+}
+
+// Every fa- name in Live's pages, scripts and server views is drawn by a glyph or a brand.
+const MOD = /^(solid|regular|light|thin|duotone|brands|sharp|classic|fw|lg|xl|xs|sm|2xs|2xl|\d+x|li|ul|border|inverse|pull-left|pull-right|rotate-\d+|rotate-by|flip-horizontal|flip-vertical|flip-both|width-auto|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|swap-opacity|stack-text)$/;
+// Names that only look like icons: built from parts at run time (fa-chevron-${dir} is fa-chevron-left/right).
+const PARTS = new Set(['chevron']);
+const files = [...walk(path.join(ROOT, 'public'), /\.(html|js)$/), ...walk(path.join(ROOT, 'server'), /\.js$/)];
+const missing = new Map();
+for (const f of files) {
+ for (const m of fs.readFileSync(f, 'utf8').matchAll(/\bfa-([a-z0-9]+(?:-[a-z0-9]+)*)\b/g)) {
+ const n = m[1];
+ if (MOD.test(n) || PARTS.has(n) || glyphs.fromFa(n) || brands.fromFa(n)) continue;
+ if (!missing.has(n)) missing.set(n, rel(f));
+ }
+}
+assert.deepStrictEqual([...missing].map(([n, f]) => `fa-${n} (${f})`), [], 'every icon Live names is drawn by the OpenVibe glyphs or brands');
+
+console.log(`no font awesome: ${pages.length} pages, ${files.length} files, every fa- name drawn`);