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('stream_server')
attributeUPLOAD_PART_TTL_SECONDSint
= 3600
attributeUPLOAD_STAGING_PREFIXstr
= '.selkies-upload-'
attributeUPLOAD_READ_DELAY_BUDGETfloat
= 0.0125
attributeUPLOAD_READ_MIN_BYTESint
= 8 * 1024
attributeUPLOAD_READ_MAX_BYTESint
= 1 << 20
attributeUPLOAD_MIN_GAUGED_BYTESint
= 4 * 1024 * 1024
attributeAUTH_REALMstr
= 'Selkies Restricted'
attributeWEBSOCKET_ROUTESTuple[str, ...]
= ('/api/websockets', '/api/webrtc/signaling', '/api/ws')
attributeSESSION_TOKEN_COOKIEstr
= 'selkies_token'
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 <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 This page carries no theme toggle of its own, so it follows the\n system preference; the tokens are otherwise the ones in the\n dashboard stylesheet. */\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 {\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 * {\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 colour 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>&copy; <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 listing is mounted at /api/files/ (the\n // scheme the dashboards iframe) or the legacy /files/, optionally\n // behind a deployment subfolder or fronting proxy — derive the\n // mount from the pathname rather than assuming a fixed prefix.\n const path = window.location.pathname;\n let webPathPrefix = \'/api/files/\';\n let idx = path.indexOf(webPathPrefix);\n if (idx === -1) {\n webPathPrefix = \'/files/\';\n idx = path.indexOf(webPathPrefix);\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 // The listing exists to download files: force file links to save\n // (directories keep navigating and the thead sort links keep\n // sorting). nginx-served listings send no Content-Disposition\n // header, so without this a click inside the dashboard\'s\n // file-browser iframe renders the file inline instead.\n // A secure-mode session token in the query (the dashboards open\n // the listing with the page\'s token) rides every link, so the\n // listing stays 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 if (!href.endsWith(\'/\')) {\n a.setAttribute(\'download\', \'\');\n }\n if (sessionToken) {\n a.setAttribute(\'href\', href + \'?token=\' + encodeURIComponent(sessionToken));\n }\n });\n\n const isAtRoot = idx !== -1 && path.endsWith(webPathPrefix);\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.style.display = \'none\';\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