Skip to content

Instantly share code, notes, and snippets.

@rksm
Created August 22, 2025 21:12
Show Gist options
  • Save rksm/737b167d43b6fdd36126acebe9efc7d2 to your computer and use it in GitHub Desktop.
Save rksm/737b167d43b6fdd36126acebe9efc7d2 to your computer and use it in GitHub Desktop.
Debug getUserMedia and device permission requests with Tampermonkey
// ==UserScript==
// @name Navigator Permissions Debug Logger
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Debug navigator.permissions.query and navigator.mediaDevices.getUserMedia calls
// @author You
// @match http://localhost:*/*
// @match https://localhost:*/*
// @match http://127.0.0.1:*/*
// @match https://127.0.0.1:*/*
// @match https://*.hyper.video/*
// @match https://latest.dev.hyper.video/*
// @match https://hyper-lite.dev/*
// @grant GM_log
// @run-at document-start
// ==/UserScript==
;(function () {
'use strict'
let callSequence = 0
const logPrefix = '[Navigator Debug]'
const logStyle = 'color: #007ACC; font-weight: bold;'
// Store original functions
const originalPermissionsQuery = navigator.permissions?.query?.bind(
navigator.permissions
)
const originalGetUserMedia = navigator.mediaDevices?.getUserMedia?.bind(
navigator.mediaDevices
)
function formatTimestamp() {
const now = new Date()
return `${now.toISOString().split('T')[1]}`
}
function getCallStack() {
const stack = new Error().stack
// Remove the first few lines which are from this wrapper
const lines = stack.split('\n').slice(3, 8)
return lines.join('\n')
}
// Wrap navigator.permissions.query
if (navigator.permissions && navigator.permissions.query) {
navigator.permissions.query = async function (permissionDesc) {
const sequence = ++callSequence
const timestamp = formatTimestamp()
console.group(
`%c${logPrefix} [${sequence}] permissions.query CALLED @ ${timestamp}`,
logStyle
)
console.log('Permission:', permissionDesc)
console.log('Call Stack:\n', getCallStack())
console.groupEnd()
try {
const result = await originalPermissionsQuery(permissionDesc)
console.group(
`%c${logPrefix} [${sequence}] permissions.query RESOLVED @ ${formatTimestamp()}`,
'color: #28a745; font-weight: bold;'
)
console.log('Permission:', permissionDesc)
console.log('State:', result.state)
console.log('Full Result:', result)
console.groupEnd()
return result
} catch (error) {
console.group(
`%c${logPrefix} [${sequence}] permissions.query REJECTED @ ${formatTimestamp()}`,
'color: #dc3545; font-weight: bold;'
)
console.log('Permission:', permissionDesc)
console.error('Error:', error)
console.groupEnd()
throw error
}
}
}
// Wrap navigator.mediaDevices.getUserMedia
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia = async function (constraints) {
const sequence = ++callSequence
const timestamp = formatTimestamp()
console.group(
`%c${logPrefix} [${sequence}] getUserMedia CALLED @ ${timestamp}`,
logStyle
)
console.log('Constraints:', JSON.parse(JSON.stringify(constraints || {})))
console.log('Call Stack:\n', getCallStack())
console.groupEnd()
try {
const stream = await originalGetUserMedia(constraints)
const audioTracks = stream.getAudioTracks().map(t => ({
id: t.id,
label: t.label,
kind: t.kind,
enabled: t.enabled,
muted: t.muted,
readyState: t.readyState,
settings: t.getSettings ? t.getSettings() : {},
}))
const videoTracks = stream.getVideoTracks().map(t => ({
id: t.id,
label: t.label,
kind: t.kind,
enabled: t.enabled,
muted: t.muted,
readyState: t.readyState,
settings: t.getSettings ? t.getSettings() : {},
}))
console.group(
`%c${logPrefix} [${sequence}] getUserMedia RESOLVED @ ${formatTimestamp()}`,
'color: #28a745; font-weight: bold;'
)
console.log(
'Constraints:',
JSON.parse(JSON.stringify(constraints || {}))
)
console.log('Stream ID:', stream.id)
console.log('Audio Tracks:', audioTracks)
console.log('Video Tracks:', videoTracks)
console.groupEnd()
return stream
} catch (error) {
console.group(
`%c${logPrefix} [${sequence}] getUserMedia REJECTED @ ${formatTimestamp()}`,
'color: #dc3545; font-weight: bold;'
)
console.log(
'Constraints:',
JSON.parse(JSON.stringify(constraints || {}))
)
console.error('Error:', {
name: error.name,
message: error.message,
constraintName: error.constraintName || null,
})
console.groupEnd()
throw error
}
}
}
// Also wrap enumerateDevices for completeness
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
const originalEnumerateDevices =
navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices)
navigator.mediaDevices.enumerateDevices = async function () {
const sequence = ++callSequence
const timestamp = formatTimestamp()
console.group(
`%c${logPrefix} [${sequence}] enumerateDevices CALLED @ ${timestamp}`,
logStyle
)
console.log('Call Stack:\n', getCallStack())
console.groupEnd()
try {
const devices = await originalEnumerateDevices()
const deviceSummary = devices.map(d => ({
kind: d.kind,
label: d.label || '(no label)',
deviceId: d.deviceId ? `${d.deviceId.substring(0, 8)}...` : '(no id)',
groupId: d.groupId ? `${d.groupId.substring(0, 8)}...` : '(no group)',
}))
console.group(
`%c${logPrefix} [${sequence}] enumerateDevices RESOLVED @ ${formatTimestamp()}`,
'color: #28a745; font-weight: bold;'
)
console.table(deviceSummary)
console.groupEnd()
return devices
} catch (error) {
console.group(
`%c${logPrefix} [${sequence}] enumerateDevices REJECTED @ ${formatTimestamp()}`,
'color: #dc3545; font-weight: bold;'
)
console.error('Error:', error)
console.groupEnd()
throw error
}
}
}
console.log(
`%c${logPrefix} Script loaded! Monitoring navigator.permissions.query and navigator.mediaDevices.getUserMedia`,
'color: #6f42c1; font-weight: bold;'
)
// Add a global function to check status
window.navigatorDebugStatus = function () {
console.log(`%c${logPrefix} Status:`, 'color: #6f42c1; font-weight: bold;')
console.log(`- Total calls intercepted: ${callSequence}`)
console.log('- Wrapped APIs:')
console.log(
' • navigator.permissions.query:',
!!navigator.permissions?.query
)
console.log(
' • navigator.mediaDevices.getUserMedia:',
!!navigator.mediaDevices?.getUserMedia
)
console.log(
' • navigator.mediaDevices.enumerateDevices:',
!!navigator.mediaDevices?.enumerateDevices
)
}
// Add helper to reset counter
window.navigatorDebugReset = function () {
callSequence = 0
console.log(
`%c${logPrefix} Call sequence counter reset`,
'color: #6f42c1; font-weight: bold;'
)
}
console.log(`%c${logPrefix} Helper functions available:`, 'color: #6f42c1;')
console.log(' - window.navigatorDebugStatus() : Check wrapper status')
console.log(' - window.navigatorDebugReset() : Reset call counter')
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment