Selkies
Developer Reference

stream_server

HTTP/HTTPS control-plane and static-content server.

Hosts the single aiohttp application every deployment fronts: the /api control-plane endpoints (status, health, streaming-mode switch, file upload/browse, metrics), the packaged or user-supplied web frontend, and the routes each streaming service registers for itself. CentralizedStreamServer supervises the active streaming service (websockets or webrtc) so only one owns capture at a time, hot-reloads TLS certificates without dropping the listener, and serves on either a TCP address/port or a Unix domain socket. On Wayland deployments it also brings the pixelflux compositor socket up before any session app starts so early-launched apps find WAYLAND_DISPLAY.

All request handlers run on the event loop; disk-bound work (upload writes, static-content extraction, metrics generation) is pushed to executor threads so a slow disk never stalls streaming.

attributelogger
= logging.getLogger('server')
attributeUPLOAD_PART_TTL_SECONDSint
= 3600
attributeUPLOAD_STAGING_PREFIXstr
= '.selkies-upload-'
attributeTRANSFER_CHUNK_DELAY_BUDGETfloat
= 0.0125
attributeTRANSFER_CHUNK_MIN_BYTESint
= 8 * 1024
attributeTRANSFER_CHUNK_MAX_BYTESint
= 1 << 20
attributeTRANSFER_MIN_GAUGED_BYTESint
= 4 * 1024 * 1024
attributeEXIT_CONFIG_ERRORint
= 78
attributeAUTH_REALMstr
= 'Selkies Restricted'
attributeWEBSOCKET_ROUTESTuple[str, ...]
= ('/api/websockets', '/api/webrtc/signaling', '/api/ws')
attributeSESSION_TOKEN_COOKIEstr
= 'selkies_token'
attributeMASTER_TOKEN_HEADERstr
= 'Selkies-Authorization'
attributeFILE_INDEX_HEADERstr
= '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="utf-8">\n <title>Desktop Files</title>\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <script>\n /* Mirror the dashboard\'s stored theme (both front ends are same\n origin, classic under "theme", wish under "vite-ui-theme"); wish\'s\n "system" and denied storage leave the pick to the media query. */\n (function () {\n var apply = function () {\n var v = null;\n try {\n v = localStorage.getItem(\'theme\') || localStorage.getItem(\'vite-ui-theme\');\n } catch (e) { return; }\n if (v === \'system\') { delete document.documentElement.dataset.theme; }\n else { document.documentElement.dataset.theme = v || \'dark\'; }\n };\n apply();\n window.addEventListener(\'storage\', apply);\n })();\n </script>\n <style>\n /* The dashboards\' palette and type scale, so the file browser reads as\n part of the same session rather than a page from somewhere else.\n The script above stamps the dashboard\'s stored theme choice; with\n nothing stored it stamps the dark both dashboards default to. */\n :root {\n --accent: #a82a69;\n --accent-text: #a82a69;\n --accent-soft: rgba(168, 42, 105, 0.1);\n --bg: #f4f5f8;\n --surface: #ffffff;\n --surface-alt: #f6f7f9;\n --border-soft: #dfe2ea;\n --text: #23272f;\n --text-muted: #5b6478;\n --shadow: 0 1px 2px rgba(16, 20, 28, 0.06), 0 8px 24px rgba(16, 20, 28, 0.08);\n\n --container-max-width: 960px;\n --font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;\n --border-radius: 10px;\n }\n @media (prefers-color-scheme: dark) {\n :root:not([data-theme="light"]) {\n --accent: #ec4899;\n --accent-text: #f9a8d4;\n --accent-soft: rgba(236, 72, 153, 0.14);\n --bg: #12161d;\n --surface: #181d26;\n --surface-alt: #1f2530;\n --border-soft: #2b3441;\n --text: #e6ebf3;\n --text-muted: #aeb9c9;\n --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);\n }\n }\n :root[data-theme="dark"] {\n --accent: #ec4899;\n --accent-text: #f9a8d4;\n --accent-soft: rgba(236, 72, 153, 0.14);\n --bg: #12161d;\n --surface: #181d26;\n --surface-alt: #1f2530;\n --border-soft: #2b3441;\n --text: #e6ebf3;\n --text-muted: #aeb9c9;\n --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);\n }\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n body {\n font-family: var(--font-family);\n background-color: var(--bg);\n color: var(--text);\n line-height: 1.55;\n padding: 24px 0 60px;\n }\n .page-container {\n max-width: var(--container-max-width);\n margin: 0 auto;\n padding: 0 20px;\n position: relative;\n }\n h1 {\n color: var(--text);\n font-size: 1.4rem;\n font-weight: 600;\n letter-spacing: -0.01em;\n margin-bottom: 16px;\n padding: 0 52px 16px 0;\n border-bottom: 1px solid var(--border-soft);\n word-break: break-all;\n }\n h1 a {\n color: var(--accent-text);\n text-decoration: none;\n }\n h1 a:hover {\n text-decoration: underline;\n }\n hr {\n display: none;\n }\n table {\n width: 100%;\n border-collapse: separate;\n border-spacing: 0;\n margin-top: 20px;\n font-size: 0.9rem;\n background-color: var(--surface);\n border: 1px solid var(--border-soft);\n border-radius: var(--border-radius);\n overflow: hidden;\n box-shadow: var(--shadow);\n }\n thead {\n background-color: var(--surface-alt);\n }\n th {\n color: var(--text-muted);\n font-weight: 600;\n font-size: 0.75rem;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n }\n th, td {\n padding: 11px 16px;\n text-align: left;\n border-bottom: 1px solid var(--border-soft);\n }\n tbody tr {\n transition: background-color 0.15s ease-in-out;\n }\n tbody tr:hover {\n background-color: var(--accent-soft);\n }\n tbody tr:last-child td {\n border-bottom: none;\n }\n td {\n color: var(--text-muted);\n }\n td a {\n color: var(--text);\n text-decoration: none;\n display: inline-flex;\n align-items: center;\n font-weight: 500;\n }\n td a:hover {\n color: var(--accent-text);\n text-decoration: underline;\n }\n th a, th a:visited {\n color: var(--text-muted);\n text-decoration: none;\n }\n th a:hover {\n color: var(--accent-text);\n text-decoration: underline;\n }\n #reload-page-button {\n position: absolute;\n top: -2px;\n right: 20px;\n background-color: var(--surface);\n color: var(--text-muted);\n border: 1px solid var(--border-soft);\n border-radius: var(--border-radius);\n width: 34px;\n height: 34px;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n transition: color 0.15s ease-in-out, border-color 0.15s ease-in-out;\n z-index: 10;\n }\n #reload-page-button:hover {\n color: var(--accent-text);\n border-color: var(--accent);\n }\n #reload-page-button:focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--accent-soft);\n }\n #reload-page-button svg {\n width: 18px;\n height: 18px;\n fill: currentColor;\n }\n td:nth-child(1) {\n word-break: break-all;\n }\n td:nth-child(2), th:nth-child(2) {\n white-space: nowrap;\n width: 180px;\n }\n td:nth-child(3), th:nth-child(3) {\n text-align: right;\n white-space: nowrap;\n width: 100px;\n }\n /* Masked rather than painted, so one glyph follows the theme\'s text\n color instead of needing a copy per palette. */\n td a::before {\n display: inline-block;\n content: \'\';\n width: 1.05em;\n height: 1.05em;\n margin-right: 0.7em;\n flex-shrink: 0;\n background-color: var(--text-muted);\n mask-repeat: no-repeat;\n mask-size: contain;\n mask-position: center;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-size: contain;\n -webkit-mask-position: center;\n }\n td a:hover::before {\n background-color: var(--accent-text);\n }\n td a[href="../"]::before {\n mask-image: url(\'data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>\');\n -webkit-mask-image: url(\'data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>\');\n }\n td a[href$="/"]:not([href="../"])::before {\n mask-image: url(\'data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>\');\n -webkit-mask-image: url(\'data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>\');\n }\n td a:not([href$="/"]):not([href="../"])::before {\n mask-image: url(\'data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 2H6c-1.11 0-2 .9-2 2v16c0 1.1.89 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>\');\n -webkit-mask-image: url(\'data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 2H6c-1.11 0-2 .9-2 2v16c0 1.1.89 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>\');\n }\n footer {\n text-align: center;\n margin-top: 32px;\n padding-top: 16px;\n border-top: 1px solid var(--border-soft);\n font-size: 0.8rem;\n color: var(--text-muted);\n }\n footer p {\n margin: 0;\n }\n @media (max-width: 768px) {\n body {\n font-size: 14px;\n padding: 12px 0 40px;\n }\n .page-container {\n padding: 0 12px;\n }\n h1 {\n font-size: 1.15rem;\n padding-right: 46px;\n }\n #reload-page-button {\n right: 12px;\n }\n table {\n display: block;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n th, td {\n padding: 10px 12px;\n white-space: nowrap;\n }\n td:nth-child(1) {\n min-width: 200px;\n }\n td:nth-child(2), th:nth-child(2) {\n min-width: 150px;\n width: auto;\n }\n td:nth-child(3), th:nth-child(3) {\n min-width: 80px;\n width: auto;\n }\n }\n </style>\n</head>\n<body>\n <div class="page-container">\n <button id="reload-page-button" title="Reload Page">\n <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">\n <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>\n </svg>\n </button>\n\n <h1>\n'
attributeFILE_INDEX_FOOTERstr
= ' </div> <!-- closes .page-container -->\n <footer>\n <p>© <script>document.write(new Date().getFullYear())</script> Selkies.</p>\n </footer>\n\n <script>\n const reloadButton = document.getElementById(\'reload-page-button\');\n if (reloadButton) {\n reloadButton.addEventListener(\'click\', function() {\n window.location.reload();\n });\n }\n\n function processDirectoryListing() {\n // Shared with the nginx fancyindex footer (addons/selkies-web-core/\n // nginx/footer.html). The mount is derived from the pathname rather\n // than assumed: /api/files/ or the legacy /files/, either of them\n // possibly behind a deployment subfolder or a fronting proxy.\n const path = window.location.pathname;\n // Normalized before searching: behind a fronting proxy the root\n // can surface without its trailing slash, and both needles end\n // in one, so the raw path would simply never match.\n const normPath = path.endsWith(\'/\') ? path : path + \'/\';\n // The OUTERMOST match wins, not a fixed preference: preferring\n // /api/files/ unconditionally mistook /files/api/files/ (a\n // subdirectory literally named api/files under the legacy mount)\n // for the /api/files/ root.\n const idxApi = normPath.indexOf(\'/api/files/\');\n const idxLegacy = normPath.indexOf(\'/files/\');\n let webPathPrefix = \'/api/files/\';\n let idx = idxApi;\n if (idxLegacy !== -1 && (idxApi === -1 || idxLegacy < idxApi)) {\n webPathPrefix = \'/files/\';\n idx = idxLegacy;\n }\n const injectedPathPrefix = window.__SELKIES_INJECTED_PATH_PREFIX__ || \'\';\n const diskPathPrefix = injectedPathPrefix || \'\';\n const h1 = document.querySelector(\'h1\');\n\n if (h1 && idx !== -1) {\n const text = h1.textContent;\n const j = text.indexOf(webPathPrefix);\n if (j !== -1) {\n const tail = text.slice(j + webPathPrefix.length);\n h1.textContent = diskPathPrefix.replace(/\\/+$/, \'\') + \'/\' + tail;\n }\n }\n\n // File links are forced to save (directories and sort links keep\n // their behavior): nginx-served listings send no Content-Disposition,\n // so a click in the dashboard\'s iframe would render inline. A\n // secure-mode token in the query rides every link, keeping the\n // listing navigable where the token cookie cannot follow.\n const sessionToken = new URLSearchParams(window.location.search).get(\'token\');\n document.querySelectorAll(\'table#list td a\').forEach(function(a) {\n const href = a.getAttribute(\'href\') || \'\';\n if (!href || href.startsWith(\'?\')) return;\n // A sorted listing carries its sort in every link\'s query,\n // the parent\'s included, so a directory is told by the path\n // ahead of it, and the token joins a query already there.\n if (!href.split(\'?\')[0].endsWith(\'/\')) {\n a.setAttribute(\'download\', \'\');\n }\n if (sessionToken) {\n a.setAttribute(\'href\', href + (href.includes(\'?\') ? \'&\' : \'?\')\n + \'token=\' + encodeURIComponent(sessionToken));\n }\n });\n\n // At the root the parent row is an escape hatch out of the tree.\n // On the nginx /files/ mount its ../ resolves to /, the desktop\n // page, whose load registers a fresh primary client and kills\n // the running session; on /api/files/ it merely 404s at /api/.\n // Wrong either way, so remove the row outright, leaving no way\n // for a later style or script to bring it back.\n const isAtRoot = idx !== -1 && idx + webPathPrefix.length === normPath.length;\n if (isAtRoot) {\n const parentLink = document.querySelector(\'table#list a[href^="../"]\');\n if (parentLink) {\n const parentRow = parentLink.closest(\'tr\');\n if (parentRow) {\n parentRow.remove();\n }\n }\n }\n }\n\n let attempts = 0;\n const maxAttempts = 20;\n const intervalId = setInterval(function() {\n attempts++;\n const h1 = document.querySelector(\'h1\');\n const table = document.getElementById(\'list\');\n\n if (h1 && table) {\n clearInterval(intervalId);\n processDirectoryListing();\n } else if (attempts >= maxAttempts) {\n clearInterval(intervalId);\n processDirectoryListing();\n }\n }, 100);\n\n </script>\n</body>\n</html>\n'

On this page

No Headings
Edit on GitHub