我有一台 Windows 10 家庭版笔记本电脑,出于隐私原因,我想听到我的网络摄像头何时激活。我知道当网络摄像头激活时会亮起一个 LED 灯 + 我已启用 OSD 消息,但我希望有一个声音通知(因为 LED 和 OSD 都是静音的)。
有什么方法可以实现这一点吗?我甚至愿意用 VBScript 编写一些脚本,但我不知道如何连接到网络摄像头激活事件。
答案1
否定回答:要么不可能,要么“解决方案”比问题更糟糕。
关于事件查看器:我已激活并停用相机,但没有看到任何结果事件。所以这种方式没有解决方案。
关于写剧本它会定期检查相机状态,并在活动时显示警报:这里存在一个小技术问题。
PowerShell 支持使用以下命令对设备进行查询,但根据我的测试,它们并未表明网络摄像头是否已激活:
Get-CimInstance Win32_PnPEntity | where caption -match 'webcam'
Get-WmiObject Win32_PnPEntity | where {$_.caption -match 'webcam'}
网络摄像头 API 支持通过发出以下指令来获取网络摄像头的状态: WM_CAP_GET_STATUS 消息唯一的问题是,这个消息要发送到通过 API 创建的捕获窗口 capCreateCaptureWindowA 函数。 然而,创建此窗口的行为本身就会打开相机。
可以编写一个小脚本来定期检查网络摄像头的状态(网络摄像头代码示例)。但是,每次运行这样的脚本时,LED 都会亮起,并且会出现 OSD 消息。如果每秒运行一次此脚本,想象一下会发生什么。
我的结论是,即使你所要求的是可能的,解决方案也会比问题更糟糕。
答案2
两年后,我自己找到了一个解决方案。我注意到 Windows 10 通过“隐私”-->“应用权限”菜单跟踪摄像头状态:
每个键前缀代表一个应用程序(“桌面应用程序”,即非 Microsoft 商店应用程序,位于“NonPackaged”键前缀下方)。每个应用程序有 3 个子键:
- 值:如果允许使用相机,
- LastUsedTimeStart:应用程序上次启动相机访问的时间,
- LastUsedTimeStop:应用程序上次停止相机访问的时间。
因此,如果我们希望每次任何应用程序发生与相机相关的事件时都收到通知(即授予/撤销特权、访问开始/停止),那么我们可以使用 Windows 管理界面来获取整个子树的注册表更改:
#Requires -version 2.0
$voice = New-Object -ComObject Sapi.spvoice
$voice.rate = 0
$voice.volume = 100
$user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
Register-WmiEvent -Query "SELECT * FROM RegistryTreeChangeEvent WHERE Hive = 'HKEY_USERS' AND RootPath = '$user\\Software\\Microsoft\\Windows\\CurrentVersion\\CapabilityAccessManager\\ConsentStore\\webcam'" -SourceIdentifier WebKeyChanged
while($true) {
Wait-Event -SourceIdentifier WebKeyChanged
Remove-Event -SourceIdentifier WebKeyChanged
$voice.speak("Webcam!")
}
不幸的是,当相机停止时也会触发此操作,因此可能会有点嘈杂。基于https://stackoverflow.com/a/145934/7612556,我们无法真正轻松地缩小范围 - 所以如果有人想使用这种方法,您可能需要对音频进行去抖动或添加更复杂的注册表跟踪(例如,在每个事件上导出内存中的子树+比较发生了什么变化以查看是否值得触发音频通知)。
好处是这也适用于新应用程序(即注册表中的全新密钥)。
就我而言,我主要对特定应用感兴趣,并且只对“启动访问”事件感兴趣。对于这种狭窄的用例,以下 WQL 查询将其缩小:
SELECT * FROM RegistryValueChangeEvent WHERE Hive = 'HKEY_USERS' AND KeyPath = '$user\\Software\\Microsoft\\Windows\\CurrentVersion\\CapabilityAccessManager\\ConsentStore\\webcam\\NonPackaged\\C:#Program Files (x86)#SomeProgramName#SomeProgram.exe' AND ValueName='LastUsedTimeStart'
请注意,对于这个“目标”脚本,您所关注的应用程序必须至少请求过一次摄像头访问权限,然后该脚本才能成功运行(否则查询将失败,因为注册表项不存在)。
为了使脚本始终运行,我遵循了以下问题的答案:https://stackoverflow.com/questions/20575257/how-do-i-run-a-powershell-script-when-the-computer-starts
答案3
我建议你使用这个网站:
它解释了如何使用 PowerShell 脚本查找和禁用网络摄像头。我假设您愿意做任何事情来实现这一点。如果您对这个想法如此着迷,我想您不会介意稍微编辑一下脚本以播放声音或音调,而不是禁用摄像头,也可以在后台运行。这是一个先机。
[console]::beep(50,1000)
这应该在 powershell 中播放 50hz 的音调 1 秒(或 1000 毫秒)
这里还有一个快速链接解释这个命令。
答案4
我可以在此答案的基础上创建您想要的内容。下面的脚本添加了一个条件,仅当 LastUsedTimeStart 或 LastUsedTimeStop 自脚本启动以来更新时才触发操作,并且另外输出属于触发该操作的进程的 Key 值。当然,这也可以用作条件。
您可以将其实现为计划任务,以便它在后台自动运行,以下是一些提示。登录后会立即弹出一个 PowerShell 窗口,此窗口还可以使用 VBScript 隐藏(如讨论的那样)这里)。
- 计划任务常规,仅在用户登录时运行
- 计划任务触发器,“登录时”和“工作站解锁时”
- 计划任务操作,启动程序:
powershell
,参数:-ep bypass -windowstyle hidden -file <PathTo\CameraStateMonitor.ps1>
示例输出:
Camera was last started by MSTeams_8wekyb3d8bbwe at 11/09/2023 17:19:10
# CameraStateMonitor.ps1
# Start Transcript if running from script file
If ($MyInvocation.MyCommand.Path) {
$WorkingDir = (Split-Path $MyInvocation.MyCommand.Path -Parent)
$LogPath = "$WorkingDir\$((Get-Item $MyInvocation.MyCommand.Path).BaseName).log"
Start-Transcript $LogPath
}
$ElgatoSwitch = {
param($State='on')
$wr = Invoke-WebRequest -Method POST -Headers @{
"authorization"="Bearer <token>"
"Content-Type"="application/json"
} -Uri "http://192.168.1.140:8123/api/services/light/turn_$State" -Body '{"entity_id":"light.elgato_bw20k1a10412"}'
}
$CameraStartedAction = {
Write-Host "Camera was last started by $($latest.LastUsedStartName) at $($latest.LastUsedTimeStart)"
#.$ElgatoSwitch -State on
[console]::beep(1800,350); $voice.Speak("Webcam enabled") | Out-Null
}
$CameraStoppedAction = {
Write-Host "Camera was last stopped by $($latest.LastUsedStopName) at $($latest.LastUsedTimeStop)"
#.$ElgatoSwitch -State off
[console]::beep(400,500); $voice.Speak("Webcam disabled") | Out-Null
}
$WmiEventHandler = {
If (!$latest) { $latest = [pscustomobject]@{LastUsedStartName=$null;LastUsedTimeStart=Get-Date;LastUsedStopName=$null;LastUsedTimeStop=Get-Date} }
$props = Get-ChildItem "Registry::HKEY_USERS\$($user.User.Value)\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam"
$props | ForEach-Object {
$propUsedTimeStart = [datetime]::FromFileTime($_.GetValue('LastUsedTimeStart'))
$propUsedTimeStop = [datetime]::FromFileTime($_.GetValue('LastUsedTimeStop'))
If ($propUsedTimeStart -gt $latest.LastUsedTimeStart) {
$latest.LastUsedStartName = $_.Name -Replace '.*\\'
$latest.LastUsedTimeStart = $propUsedTimeStart
.$CameraStartedAction
}
If ($propUsedTimeStop -gt $latest.LastUsedTimeStop) {
$latest.LastUsedStopName = $_.Name -Replace '.*\\'
$latest.LastUsedTimeStop = $propUsedTimeStop
.$CameraStoppedAction
}
}
}
$voice = New-Object -ComObject Sapi.spvoice; $voice.rate = 0; $voice.volume = 100
$user = [System.Security.Principal.WindowsIdentity]::GetCurrent()
Write-Host -b black -f yellow "User:" $user.Name "`r`n SID:" $user.User.Value
$WmiEventQuery = @"
SELECT *
FROM RegistryTreeChangeEvent
WHERE Hive = 'HKEY_USERS'
AND RootPath = '$($user.User.Value)\\Software\\Microsoft\\Windows\\CurrentVersion\\CapabilityAccessManager\\ConsentStore\\webcam'
"@
Register-WmiEvent -SourceIdentifier RegistryTreeChangeEvent -Query $WmiEventQuery
try {
# Monitoring loop
while($true) {
Wait-Event -SourceIdentifier RegistryTreeChangeEvent | Out-Null
#Wait-Event -SourceIdentifier RegistryTreeChangeEvent -Timeout 60 | Out-Null # Return after 60 seconds in case of issues with ChangeEvent not triggering
Write-Host -b black -f yellow "$(Get-Date) Wait-Event returned"
Remove-Event -SourceIdentifier RegistryTreeChangeEvent -ea 0
.$WmiEventHandler
}
} catch {
Write-Host "Error occurred:`r`n" + $Error[0]
} finally {
Get-EventSubscriber -SourceIdentifier RegistryTreeChangeEvent | Unregister-Event
Stop-Transcript
}