WebRTC-Experiment 的 navigator.customGetUserMediaBar.js 实战指南:在 HTTPS 下用自定义权限提示条保护用户隐私
【免费下载链接】WebRTC-Experiment
WebRTC, WebRTC and WebRTC. Everything here is all about WebRTC!!
导读
本文围绕 WebRTC-Experiment 仓库中的 navigator.customGetUserMediaBar 模块展开:当网站在 HTTPS 域名下运行时,Chrome 会对 getUserMedia 权限做"自动放行",用户甚至看不到任何授权提示;本模块通过在真实调用 getUserMedia 之前弹出一条可完全自定义的通知条,让用户先明确选择"允许 / 拒绝",从而把隐私选择权交还给用户。读完本文,你将掌握该 API 的三参数调用方式、通知条的内部实现原理,以及通过 navigator.customGetUserMediaBar.styles 定制外观的完整方法。
一、模块定位:为什么需要一条"自定义"的权限提示条
该模块的核心动机在源码头注释中写得很直白:
/*
*. https => displaying custom prompt-bar for HTTPs domains!
*. Keep users privacy as much as possible!
*/
在 HTTPS 域名下,Chrome 的媒体权限机制与 HTTP 不同:对于 HTTP 页面,Chrome 会弹出原生的"允许/阻止"询问条;而对于 HTTPS 页面,浏览器倾向于直接授予摄像头/麦克风权限(即"SSL auto allowed"),用户往往在毫无感知的情况下就被采集了音视频。navigator.customGetUserMediaBar 正是针对这一行为设计:在真正调用 getUserMedia 之前,先用一条属于你网站自己的通知条征求用户同意,最大程度保护用户隐私。
模块主体是一个单文件 navigator.customGetUserMediaBar.js(约 189 行、无任何第三方依赖),并配套有可直接运行的 演示页面。
二、快速上手:引入脚本并调用 API
2.1 引入脚本
在页面 <head> 或 <body> 中引入脚本文件(仓库中的源码文件即 navigator.customGetUserMediaBar/navigator.customGetUserMediaBar.js):
<script src="navigator.customGetUserMediaBar.js"></script>
脚本加载后即向 navigator 对象挂载 customGetUserMediaBar 方法,并同时初始化默认样式 navigator.customGetUserMediaBar.styles。
2.2 API 签名
navigator.customGetUserMediaBar(mediaConstraints, success_callback, failure_callback)
三个参数的含义(源码与 README 中均有明确注释):
| 参数 | 含义 | 触发时机 |
|---|---|---|
mediaConstraints | 媒体约束对象,如 {audio: true, video: true} | 用于决定通知条上展示的文案(见下文"动态文案") |
success_callback | 用户点击了 "Allow"(允许) | 允许之后,通常在这里继续调用真正的 getUserMedia |
failure_callback | 用户点击了 "Deny" 或右上角 "x" 关闭按钮 | 表示用户拒绝授权 |
2.3 最小可运行示例(来自 README)
var mediaConstraints = {
audio: true,
video: true
};
navigator.customGetUserMediaBar(mediaConstraints, function () {
// 用户允许后,再无缝调用真正的 getUserMedia 采集媒体
navigator.webkitGetUserMedia(mediaConstraints, success_callback, failure_callback);
}, function () {
// 用户点击了 "Deny" 或 "x" 按钮
throw new Error('PermissionDeniedError');
});
注意调用顺序的语义:customGetUserMediaBar 本身不会帮你采集媒体流,它只负责"先征求同意";真正的采集仍要由你自己在 success_callback 里通过 getUserMedia 完成。这样即使用户拒绝,浏览器也不会触发真实的权限弹窗或自动授权,隐私得到保护。
三、源码级解析:通知条内部是如何工作的
结合 navigator.customGetUserMediaBar.js 的实现,可以梳理出完整的调用链:
3.1 前置快速通道:什么情况会"直接放行"
函数入口 navigator.customGetUserMediaBar.js#L21-L28 有两条快速放行(直接调用 success_callback、不弹通知条)的分支:
navigator.customGetUserMediaBar = function (session, success_callback, failure_callback) {
// 仅对 HTTPS 下的 Chrome 有意义
if (!!navigator.mozGetUserMedia || location.protocol !== 'https:') return success_callback();
// 屏幕捕获请求:直接跳过自定义通知条
if (session.video && session.video.mandatory && session.video.mandatory.chromeMediaSource) {
return success_callback();
}
...
两条分支的用途:
- 非 HTTPS 或 Firefox:
location.protocol !== 'https:'说明页面不是 HTTPS,浏览器本来就会弹出原生权限询问;navigator.mozGetUserMedia存在说明是 Firefox,其权限模型不同。这两种情况下直接放行,避免重复询问。 - 屏幕捕获请求:如果
session.video.mandatory.chromeMediaSource存在(典型的屏幕分享约束,如{video: {mandatory: {chromeMediaSource: 'desktop'}}}),说明这是一次桌面/屏幕捕获而非摄像头采集,同样直接放行,不显示通知条。
3.2 通知条的 DOM 结构:iframe 隔离 + 全屏遮罩
从源码可见,通知条由两个顶层元素构成:
-
遮罩层 overlay:源码 navigator.customGetUserMediaBar.js#L34-L36 中通过
document.createElement('vide')创建(源码原文如此,浏览器会按未知元素处理,样式仍可生效),应用styles.overlay样式后追加到document.body。默认遮罩是半透明白色(rgba(255, 255, 255, 0.66)),position: fixed覆盖整个视口,阻止用户在通知条出现期间与页面交互。 -
通知条 iframe:源码 navigator.customGetUserMediaBar.js#L38-L46 创建一个
<iframe>并追加到 body,随后通过contentDocument写入内部文档。用 iframe 承载通知条内容的收益是:页面本身的 CSS/JS 不会污染通知条内部样式,同时通知条内容也与主页面隔离。iframe 内部 body 会继承主页面计算出的font-family(源码通过getComputedStyle获取,见 navigator.customGetUserMediaBar.js#L32 与getCSSPropertyValue辅助函数 navigator.customGetUserMediaBar.js#L114-L116),保证视觉上协调一致。
3.3 通知条内部内容与动态文案
iframe 内部依次写入(源码 navigator.customGetUserMediaBar.js#L48-L65):
- 右上角区域:一个指向 Chrome 官方"摄像头/麦克风权限"帮助页的 Learn more 链接 + 关闭按钮 x(
#close-notification-bar); - 当前站点域名(带
styles.icon锁形图标);当协议为https:时,域名中的https:前缀会被渲染为绿色(color: rgb(0, 206, 74)); - 根据
session组合动态生成的提示文案:
session 取值 | 通知条文案 |
|---|---|
audio: true, video: true | wants to use your microphone and webcam. |
audio: true, video: false | wants to use your microphone. |
audio: false, video: true | wants to use your webcam. |
- 两个操作按钮:Allow(
#allow-notification-bar)与 Deny(#deny-notification-bar)。
3.4 按钮回调与收起动画
三个交互元素的回调逻辑(源码 navigator.customGetUserMediaBar.js#L81-L97):
- 点击 x:等价于点击 Deny,先禁用自身再调用 Deny 的处理函数;
- 点击 Allow:禁用自身 → 调用
success_callback()→ 收起通知条;通知条加载完成后会立即focus()到 Allow 按钮,方便键盘操作; - 点击 Deny:禁用自身 → 调用
failure_callback()→ 收起通知条。
收起动画由 hideBar() 完成(源码 navigator.customGetUserMediaBar.js#L70-L79):先让 iframe 的 opacity 归零、height 归零,同时把 document.documentElement.style.marginTop 复位为 0,100ms 后再从 DOM 中移除 iframe 和遮罩层。而通知条展开时,页面顶部会被下推——源码 navigator.customGetUserMediaBar.js#L99-L101 将 iframe 的 opacity 置为 1、height 置为 32px,并同步设置 document.documentElement.style.marginTop = '32px',避免通知条遮挡页面内容。
四、带完整回调的实战示例(来自演示页面)
仓库的 index.html 提供了一个完整的交互示例:点击按钮触发通知条,允许后将采集到的媒体流直接挂到一个 <video> 元素上播放。核心逻辑如下:
document.querySelector('#invoke-customGetUserMediaBar').onclick = function() {
var button = this;
button.disabled = true;
var mediaConstraints = {
audio: true,
video: true
};
navigator.customGetUserMediaBar(mediaConstraints, function () {
// 用户允许后:优先使用 webkitGetUserMedia,否则退回 mozGetUserMedia
if (!!navigator.webkitGetUserMedia) {
navigator.webkitGetUserMedia(mediaConstraints, success_callback, failure_callback);
}
else navigator.mozGetUserMedia(mediaConstraints, success_callback, failure_callback);
}, function () {
// 用户点击 "Deny" 或 "x" 按钮
alert('PermissionDeniedError: User denied permission.');
button.disabled = false;
});
function success_callback(stream) {
var video = document.createElement('video');
video.srcObject = stream;
video.controls = true;
video.style.maxWidth = '100%';
button.parentNode.appendChild(video);
video.play();
}
function failure_callback(error) {
alert(JSON.stringify(error, null, '\t'));
}
};
这个示例补充了两个 README 没有展开的细节:一是对旧版 webkitGetUserMedia / mozGetUserMedia 的兼容分支写法;二是拒绝路径中把按钮重新置为可用,允许用户再次触发询问——这些模式可以直接迁移到自己的项目中。
五、完全自定义样式:styles 对象全解析
模块的核心可定制点是 navigator.customGetUserMediaBar.styles 对象,它定义在源码 navigator.customGetUserMediaBar.js#L122-L189。你可以在调用前整体替换它,也可以只覆盖其中的个别键。getStyles(property) 辅助函数(源码 navigator.customGetUserMediaBar.js#L104-L112)会把每个键下的 CSS 声明拼接成内联样式字符串,所以这里的键名即 CSS 属性名。
5.1 六个可定制键的职责(README 官方说明)
| 样式键 | 作用 | 默认值要点 |
|---|---|---|
styles.body | 通知条本体的样式 | position: fixed; left: 0; top: 0; z-index: 10000; width: 100%,默认透明隐藏(height: 0; opacity: 0; overflow: hidden),带渐变背景与底部边框 |
styles.overlay | 覆盖整个 <body> 的半透明白色遮罩,阻止通知条显示期间用户与页面交互 | rgba(255, 255, 255, 0.66)、position: fixed 铺满全屏 |
styles.icon | 通知条左侧的图标,支持 png/jpeg 等任意图片,甚至 Data-URL;官方建议可换成视频图标而非锁形图标 | 一个 20px 量级的锁形 PNG 的 Data-URL(完整 base64 见源码 navigator.customGetUserMediaBar.js#L156) |
styles.x | 右上角的关闭 "x" 按钮 | 无背景、细黑边框、圆角 2px、cursor: pointer |
styles['button-allow'] | "Allow" 允许按钮;官方提示可改为绿色背景 | 灰色渐变按钮(默认由 buttonStyles 统一赋值) |
styles['button-deny'] | "Deny" 拒绝按钮;官方提示可改为红色背景 | 同 button-allow,共用 buttonStyles |
5.2 自定义 body / overlay / icon / x 的完整示例(来自 README)
navigator.customGetUserMediaBar.styles = {
body: {
border: 0,
position: 'fixed',
left: 0,
top: 0,
'z-index': 10000,
'font-size': '16px',
'font-family': fontFamily,
'font-weight': 'normal',
padding: '4px 18px',
'text-decoration': 'none',
background: '-webkit-gradient( linear, left top, left bottom, color-stop(5%, #ededed), color-stop(100%, #dfdfdf) )',
color: '#290D0D',
display: 'inline-block',
'text-shadow': '1px 1px 0px #ffffff',
'box-shadow': 'inset 1px 1px 0px 0px #ffffff',
width: '100%',
'border-bottom': '1px solid rgb(175, 172, 172)',
'-webkit-user-select': 'none',
cursor: 'default',
transition: 'all .2s ease',
height: 0,
opacity: 0,
overflow: 'hidden'
},
overlay: {
background: 'rgba(255, 255, 255, 0.66)',
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%'
},
icon: 'data:image/png;base64,...', // 可换成你自己的图片 URL 或 Data-URL
x: {
background: 'none',
padding: '4px',
height: 'auto',
margin: 0,
'line-height': '.5',
color: 'black',
'text-shadow': 'none',
'box-shadow': 'none',
border: '1px solid black',
'border-radius': '2px',
cursor: 'pointer'
}
};
其中 fontFamily 在源码中这样取得(navigator.customGetUserMediaBar.js#L120):
var fontFamily = getCSSPropertyValue(document.body || document.documentElement, 'font-family') || 'Arial';
5.3 自定义 Allow / Deny 按钮样式(来自 README)
var buttonStyles = {
'font-size': '14px',
'font-family': fontFamily,
'font-weight': 'normal',
'border-radius': '3px',
border: '1px solid #7C7777',
padding: '4px 12px',
'text-decoration': 'none',
background: '-webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0.05, rgb(241, 241, 241)), to(rgb(230, 230, 230)))',
'background-color': '#ededed',
color: '#1B1A1A',
display: 'inline-block',
'box-shadow': 'inset 1px 1px 0px 0px #ffffff',
'text-shadow': 'none'
};
navigator.customGetUserMediaBar.styles['button-allow'] =
navigator.customGetUserMediaBar.styles['button-deny'] = buttonStyles;
按 README 的建议,实际项目中通常会把 button-allow 改成绿色背景、button-deny 改成红色背景,以强化视觉语义。注意:自定义 styles 必须在调用 navigator.customGetUserMediaBar(...) 之前设置,因为函数运行时才读取对应样式键。
六、与 RTCMultiConnection 的集成:preventSSLAutoAllowed
navigator.customGetUserMediaBar 并非孤立组件,它被仓库内的 RTCMultiConnection 集成以关闭 Chrome 在 HTTPS 下的"自动放行"行为。在 WebRTC-Scalable-Broadcast/RTCMultiConnection.js#L494-L514 中可以找到集成逻辑:
if (connection.preventSSLAutoAllowed && !dontPreventSSLAutoAllowed && isChrome) {
// if navigator.customGetUserMediaBar.js is missing
if (!navigator.customGetUserMediaBar) {
loadScript(connection.resources.customGetUserMediaBar, function() {
_captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks, dontPreventSSLAutoAllowed);
});
return;
}
navigator.customGetUserMediaBar(forcedConstraints, function() {
_captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks, true);
}, function() {
connection.onMediaError({
name: 'PermissionDeniedError',
message: 'User denied permission.',
constraintName: forcedConstraints,
session: session
});
});
return;
}
关键信息:
connection.preventSSLAutoAllowed默认值为false(见 WebRTC-Scalable-Broadcast/RTCMultiConnection.js#L4996),开启后配合本模块使用;- 当
navigator.customGetUserMediaBar尚未加载时,RTCMultiConnection 会通过loadScript动态加载资源(默认 CDN 地址配置在connection.resources.customGetUserMediaBar,见 WebRTC-Scalable-Broadcast/RTCMultiConnection.js#L5021),加载完成后再递归重试; - 用户拒绝时,错误被统一包装成
name: 'PermissionDeniedError'交给onMediaError处理——这与本模块 README 中失败回调抛PermissionDeniedError的约定一致; - 同样的集成代码也出现在 MultiRTC/MultiRTC-socketio/public/RTCMultiConnection.js 等仓库内其它 RTCMultiConnection 副本中(如该文件第 515-529 行区域及第 5564 行的
resources.customGetUserMediaBar配置),说明它是 RTCMultiConnection 系列的标准权限前置流程。
七、适用前提与限制
综合源码与 README,使用本模块前需要明确以下几点边界:
- 生效范围:仅在 HTTPS 域名下的 Chrome 中展示自定义通知条;HTTP 页面与 Firefox 会直接走原生权限流程(源码 navigator.customGetUserMediaBar.js#L23)。
- 不采集媒体:它只是权限征求层,真正的
getUserMedia需要自己在success_callback里调用。 - 屏幕捕获跳过:带
chromeMediaSource的约束会直接放行,不会弹出通知条。 - 依赖说明:该库编写于旧版 WebRTC API 时代,示例中大量使用
webkitGetUserMedia/mozGetUserMedia前缀写法;在现代浏览器中,success_callback内部应使用标准的navigator.mediaDevices.getUserMedia(Promise 形式),本模块自身只承担"征求同意"的职责,两者可无缝配合。 - 许可证:按 README 声明,本模块以 MIT 许可发布。
八、小结
navigator.customGetUserMediaBar 用不到两百行代码解决了一个真实痛点:让 HTTPS 下的 Chrome 用户重新获得"是否允许采集音视频"的知情权与选择权。其设计可圈可点——iframe 隔离的提示条、基于 mediaConstraints 的动态文案、六键式可定制样式体系,以及作为 RTCMultiConnection 的 preventSSLAutoAllowed 前置流程被深度集成。无论你是想为现有 WebRTC 应用加一层隐私保护,还是想学习如何优雅地在真实 getUserMedia 前插入一个自定义权限门,都可以直接参考本模块的 源码 与 演示页。
【免费下载链接】WebRTC-Experiment
WebRTC, WebRTC and WebRTC. Everything here is all about WebRTC!!
更多推荐
所有评论(0)