diff --git a/src/connection-storage.js b/src/connection-storage.js
new file mode 100644
index 00000000..bd81d324
--- /dev/null
+++ b/src/connection-storage.js
@@ -0,0 +1,52 @@
+/* eslint-env browser */
+
+const CONNECTION_COOKIE_NAME = 'obsConnection'
+const CONNECTION_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 days
+
+function encodeConnection (data) {
+ try {
+ const json = JSON.stringify(data)
+ // Note: this is simple base64 encoding, not strong encryption.
+ // It prevents casual snooping of the cookie contents but does
+ // not protect against a determined attacker with access to this origin.
+ return btoa(json)
+ } catch (e) {
+ console.warn('Failed to encode connection cookie', e)
+ return ''
+ }
+}
+
+function decodeConnection (value) {
+ if (!value) return null
+ try {
+ const json = atob(value)
+ return JSON.parse(json)
+ } catch (e) {
+ console.warn('Failed to decode connection cookie', e)
+ return null
+ }
+}
+
+export function setConnectionCookie (connection) {
+ if (!connection) {
+ document.cookie = `${CONNECTION_COOKIE_NAME}=; Max-Age=0; path=/; SameSite=Lax`
+ return
+ }
+ const encoded = encodeConnection(connection)
+ if (!encoded) return
+ let cookie = `${CONNECTION_COOKIE_NAME}=${encoded}; Max-Age=${CONNECTION_COOKIE_MAX_AGE}; path=/; SameSite=Lax`
+ if (location.protocol === 'https:') {
+ cookie += '; Secure'
+ }
+ document.cookie = cookie
+}
+
+export function getConnectionCookie () {
+ const cookies = document.cookie ? document.cookie.split('; ') : []
+ const found = cookies.find((c) => c.startsWith(`${CONNECTION_COOKIE_NAME}=`))
+ if (!found) return null
+ const value = found.split('=').slice(1).join('=')
+ return decodeConnection(value)
+}
+
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index ed0ebbfd..78a70124 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -25,12 +25,14 @@
mdiMotionPlayOutline,
mdiMotionPlay,
mdiContentSaveMoveOutline,
- mdiContentSaveCheckOutline
+ mdiContentSaveCheckOutline,
+ mdiLogout
} from '@mdi/js'
import Icon from 'mdi-svelte'
import { compareVersions } from 'compare-versions'
import { obs, sendCommand } from '../obs.js'
+ import { getConnectionCookie, setConnectionCookie } from '../connection-storage.js'
import ProgramPreview from '../ProgramPreview.svelte'
import SceneSwitcher from '../SceneSwitcher.svelte'
import SourceSwitcher from '../SourceSwitcher.svelte'
@@ -38,62 +40,17 @@
import SceneCollectionSelect from '../SceneCollectionSelect.svelte'
onMount(async () => {
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.register('/service-worker.js')
- }
+ setupServiceWorker()
+ await setupWakeLock()
+ setupNavbarToggle()
+ setupFullscreenListeners()
- // Request screen wakelock
- if ('wakeLock' in navigator) {
- try {
- await navigator.wakeLock.request('screen')
- // Re-request when coming back
- document.addEventListener('visibilitychange', async () => {
- if (document.visibilityState === 'visible') {
- await navigator.wakeLock.request('screen')
- }
- })
- } catch (e) {}
- }
+ const { shouldAutoConnect } = restoreConnectionState()
- // Toggle the navigation hamburger menu on mobile
- const navbar = document.querySelector('.navbar-burger')
- navbar.addEventListener('click', () => {
- navbar.classList.toggle('is-active')
- document
- .getElementById(navbar.dataset.target)
- .classList.toggle('is-active')
- })
-
- // Listen for fullscreen changes
- document.addEventListener('fullscreenchange', () => {
- isFullScreen = document.fullscreenElement
- })
-
- document.addEventListener('webkitfullscreenchange', () => {
- isFullScreen = document.webkitFullscreenElement
- })
-
- document.addEventListener('msfullscreenchange', () => {
- isFullScreen = document.msFullscreenElement
- })
-
- if (document.location.hash !== '') {
- // Read address from hash
- address = document.location.hash.slice(1)
-
- // This allows you to add a password in the URL like this:
- // http://obs-web.niek.tv/#ws://localhost:4455#password
- if (address.includes('#')) {
- [address, password] = address.split('#')
- }
+ if (shouldAutoConnect && address) {
await connect()
}
- if (window.localStorage.getItem('obsAddress')) {
- // If we have a saved address, use that
- address = window.localStorage.getItem('obsAddress')
- }
-
// Export the sendCommand() function to the window object
window.sendCommand = sendCommand
})
@@ -111,6 +68,7 @@
let editable = false
let address
let password
+ let rememberConnection = false
let scenes = []
let replayError = ''
let errorMessage = ''
@@ -126,6 +84,146 @@
? window.localStorage.setItem('isIconMode', 'true')
: window.localStorage.removeItem('isIconMode')
+ // UI helpers
+ function setupServiceWorker () {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.register('/service-worker.js')
+ }
+ }
+
+ async function setupWakeLock () {
+ if (!('wakeLock' in navigator)) return
+ try {
+ const requestWakeLock = async () => {
+ try {
+ await navigator.wakeLock.request('screen')
+ } catch (e) {}
+ }
+
+ await requestWakeLock()
+
+ // Re-request when coming back
+ document.addEventListener('visibilitychange', async () => {
+ if (document.visibilityState === 'visible') {
+ await requestWakeLock()
+ }
+ })
+ } catch (e) {}
+ }
+
+ function setupNavbarToggle () {
+ // Toggle the navigation hamburger menu on mobile
+ const navbar = document.querySelector('.navbar-burger')
+ if (!navbar) return
+ navbar.addEventListener('click', () => {
+ navbar.classList.toggle('is-active')
+ document
+ .getElementById(navbar.dataset.target)
+ .classList.toggle('is-active')
+ })
+ }
+
+ function setupFullscreenListeners () {
+ // Listen for fullscreen changes
+ document.addEventListener('fullscreenchange', () => {
+ isFullScreen = document.fullscreenElement
+ })
+
+ document.addEventListener('webkitfullscreenchange', () => {
+ isFullScreen = document.webkitFullscreenElement
+ })
+
+ document.addEventListener('msfullscreenchange', () => {
+ isFullScreen = document.msFullscreenElement
+ })
+ }
+
+ function normalizeSavedPassword (rawPassword) {
+ if (typeof rawPassword === 'string') return rawPassword
+ if (rawPassword && typeof rawPassword.password === 'string') {
+ return rawPassword.password
+ }
+ return ''
+ }
+
+ // Centralised cookie / legacy-storage restore logic
+ function restoreConnectionFromCookie (hashHasPassword, shouldAutoConnect) {
+ const savedConnection = getConnectionCookie()
+ const hasCookie = !!(savedConnection && savedConnection.address)
+ const legacyAddress = window.localStorage.getItem('obsAddress')
+
+ if (!hasCookie) {
+ // 3. Fallback: legacy storage, address only (no password)
+ if (!address && legacyAddress) {
+ address = legacyAddress
+ }
+ return shouldAutoConnect
+ }
+
+ const cookieAddress = savedConnection.address
+ const cookiePassword = normalizeSavedPassword(savedConnection.password)
+ const hasCookiePassword = cookiePassword.length > 0
+
+ // If hash contained a password, always prefer that over cookie contents
+ if (hashHasPassword) {
+ // Still allow cookie to provide address when hash only set a password,
+ // but in our current format hash always carries both address and password.
+ if (!address) {
+ address = cookieAddress
+ }
+ return shouldAutoConnect
+ }
+
+ // Only override address from cookie when it wasn't explicitly provided via hash
+ if (!address) {
+ address = cookieAddress
+ }
+
+ if (!hasCookiePassword) {
+ rememberConnection = false
+ return shouldAutoConnect
+ }
+
+ // Only use cookie password when not explicitly provided via hash
+ if (!password) {
+ password = cookiePassword
+ }
+ rememberConnection = true
+ shouldAutoConnect = true
+
+ return shouldAutoConnect
+ }
+
+ function restoreConnectionState () {
+ let shouldAutoConnect = false
+ let hashHasPassword = false
+
+ // 1. Read address and optional password from URL hash
+ if (document.location.hash !== '') {
+ // This allows you to add a password in the URL like this:
+ // http://obs-web.niek.tv/#ws://localhost:4455#password
+ let hashValue = document.location.hash.slice(1)
+ if (hashValue.includes('#')) {
+ const [hashAddress, hashPassword] = hashValue.split('#')
+ address = hashAddress
+ password = hashPassword
+ hashHasPassword = true
+ shouldAutoConnect = true
+ } else {
+ address = hashValue
+ }
+ }
+
+ // 2. Restore from cookie / legacy storage
+ shouldAutoConnect = restoreConnectionFromCookie(
+ hashHasPassword,
+ shouldAutoConnect
+ )
+
+ // Final state for onMount caller
+ return { shouldAutoConnect }
+ }
+
function formatTime (secs) {
secs = Math.round(secs / 1000)
const hours = Math.floor(secs / 3600)
@@ -236,7 +334,13 @@
const secure = location.protocol === 'https:' || address.endsWith(':443')
address = secure ? 'wss://' : 'ws://' + address
}
- console.log('Connecting to:', address, '- using password:', password)
+ const hasPassword = typeof password === 'string' && password.length > 0
+ console.log(
+ 'Connecting to:',
+ address,
+ '- using password:',
+ hasPassword ? '[set]' : '[empty]'
+ )
await disconnect()
try {
const { obsWebSocketVersion, negotiatedRpcVersion } = await obs.connect(
@@ -247,6 +351,12 @@
`Connected to obs-websocket version ${obsWebSocketVersion} (using RPC ${negotiatedRpcVersion})`
)
window.localStorage.setItem('obsAddress', address) // Save address for next time
+ if (rememberConnection) {
+ const safePassword = typeof password === 'string' ? password : ''
+ setConnectionCookie({ address, password: safePassword })
+ } else {
+ setConnectionCookie(null)
+ }
} catch (e) {
console.log(e)
errorMessage = e.message
@@ -260,14 +370,23 @@
errorMessage = 'Disconnected'
}
+ async function logout () {
+ rememberConnection = false
+ address = ''
+ password = ''
+ setConnectionCookie(null)
+ window.localStorage.removeItem('obsAddress')
+ if (connected) {
+ await disconnect()
+ } else {
+ connected = false
+ errorMessage = 'Disconnected'
+ }
+ }
+
// OBS events
obs.on('ConnectionClosed', () => {
connected = false
- window.history.pushState(
- '',
- document.title,
- window.location.pathname + window.location.search
- ) // Remove the hash
console.log('Connection closed')
})
@@ -520,10 +639,24 @@
>