• 示例工程

【免费下载链接】WebRTC-Experiment

WebRTC, WebRTC and WebRTC. Everything here is all about WebRTC!!

项目地址: https://gitcode.com/gh_mirrors/we/WebRTC-Experiment
点击查看 免费下载

导读

本文围绕 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 隔离 + 全屏遮罩

从源码可见,通知条由两个顶层元素构成:

  1. 遮罩层 overlay:源码 navigator.customGetUserMediaBar.js#L34-L36 中通过 document.createElement('vide') 创建(源码原文如此,浏览器会按未知元素处理,样式仍可生效),应用 styles.overlay 样式后追加到 document.body。默认遮罩是半透明白色(rgba(255, 255, 255, 0.66)),position: fixed 覆盖整个视口,阻止用户在通知条出现期间与页面交互。

  2. 通知条 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: truewants to use your microphone and webcam.
audio: true, video: falsewants to use your microphone.
audio: false, video: truewants 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!!

项目地址: https://gitcode.com/gh_mirrors/we/WebRTC-Experiment
点击查看 免费下载
Logo

邀请您加入社区

更多推荐