实现网页内容投屏到第二显示屏并不一定需要依赖 HDMI 采集或专用投屏协议。Chromium 内核浏览器内置的 Presentation API 允许页面发现外接屏幕并请求在该屏幕打开一个展示页面,配合主页面与展示页之间的消息通道,就能把数据、视频地址或交互状态同步过去。本文以 jQuery 插件的形式封装这一过程,让调用方通过几行代码完成连接、发送与关闭。

Presentation API 的定位与浏览器支持
Presentation API 是 W3C 提出的第二屏呈现规范,核心接口包括 PresentationRequest、PresentationConnection、PresentationAvailability 以及接收端的 PresentationReceiver。它和屏幕共享不同:屏幕共享把当前页面镜像到另一个屏幕,而 Presentation API 会在第二屏打开一个独立 URL,这个 URL 与主页面之间建立双向消息通道。主页面可以只承担控制台角色,真正渲染内容的是第二屏页面。
目前该 API 在 Chromium 内核浏览器中支持较完整,例如 Chrome、Edge、Opera 的桌面版本。Firefox 和 Safari 尚未实现主要接口,因此在生产环境中需要做能力检测并给出降级提示。即使在同一浏览器中,部分接口也可能需要用户手势触发,例如 request.start() 通常必须放在点击事件回调里,否则会被浏览器拦截。
直接使用原生 API 涉及不少重复代码:创建请求、监听可用性、处理连接状态、序列化消息等。用 jQuery 封装可以把这些步骤收敛到插件中,业务代码只需传入接收页地址和处理回调即可。下面展示一个轻量插件的实现方式。
用 jQuery 封装投屏插件
插件的核心逻辑包括三步:能力检测与请求对象创建、启动投屏并保存连接、对外暴露发送和关闭方法。以下是一个基础封装:
(function($) {
$.fn.presentation = function(options) {
var settings = $.extend({
receiverUrl: 'receiver.html',
onConnect: null,
onMessage: null,
onClose: null
}, options);
var request = null;
var connection = null;
function initRequest() {
var target = new URL(settings.receiverUrl, window.location.href).href;
if (window.PresentationRequest) {
request = new PresentationRequest([target]);
} else if (navigator.presentation && navigator.presentation.defaultRequest) {
request = navigator.presentation.defaultRequest;
}
}
function bindConnection(conn) {
connection = conn;
conn.onconnect = function() {
if (settings.onConnect) {
settings.onConnect.apply(this, arguments);
}
};
conn.onmessage = function(event) {
var data = event.data;
try {
data = JSON.parse(event.data);
} catch (e) {}
if (settings.onMessage) {
settings.onMessage(data, event);
}
};
conn.onclose = function() {
connection = null;
if (settings.onClose) {
settings.onClose.apply(this, arguments);
}
};
conn.onterminate = function() {
connection = null;
};
}
function start() {
initRequest();
if (!request) {
return $.Deferred().reject(new Error('Presentation API not supported')).promise();
}
return request.start().then(function(conn) {
bindConnection(conn);
return conn;
});
}
function send(data) {
if (!connection || connection.state !== 'connected') {
return false;
}
var payload = typeof data === 'string' ? data : JSON.stringify(data);
connection.send(payload);
return true;
}
function close() {
if (connection) {
connection.close();
}
}
return {
start: start,
send: send,
close: close,
getConnection: function() {
return connection;
},
getAvailability: function() {
if (!request) {
initRequest();
}
if (request && request.getAvailability) {
return request.getAvailability();
}
return $.Deferred().reject(new Error('getAvailability not available')).promise();
}
};
};
})(jQuery);
调用时先为控制按钮绑定事件,在点击回调中启动投屏。这样既能满足用户手势要求,也能把连接对象保存下来方便后续发送消息。示例:
$('#present-btn').on('click', function() {
var sender = $('#present-btn').presentation({
receiverUrl: 'receiver.html',
onConnect: function() {
console.log('第二屏已连接');
},
onMessage: function(data) {
console.log('收到消息', data);
if (data && data.type === 'ready') {
sender.send({ type: 'slide', index: 2 });
}
}
});
sender.start().then(function(conn) {
console.log('连接状态', conn.state);
}).catch(function(err) {
alert('无法启动投屏:' + err.message);
});
});
这里有一个容易忽略的问题:插件返回对象后,需要把该对象保存在闭包或全局变量中,否则后续调用 send 时可能拿不到同一个连接上下文。更稳妥的做法是把连接状态挂在按钮元素的数据对象上,或者把插件设计成单例状态。
投屏接收页的消息处理
第二屏打开的 receiver.html 需要通过 navigator.presentation.receiver 获取连接列表。接收端代码通常在页面加载后立即运行:
if (navigator.presentation && navigator.presentation.receiver) {
navigator.presentation.receiver.connectionList.then(function(list) {
list.connections.forEach(function(connection) {
connection.onmessage = function(event) {
var data = JSON.parse(event.data);
if (data.type === 'slide') {
renderSlide(data.index);
}
};
connection.onclose = function() {
document.body.classList.add('closed');
};
connection.send(JSON.stringify({ type: 'ready', ts: Date.now() }));
});
});
}
function renderSlide(index) {
var items = document.querySelectorAll('.slide');
items.forEach(function(item, i) {
item.classList.toggle('active', i === index);
});
}
接收页的 HTML 可以包含视频、图片、图表容器等元素。注意投屏页面通常运行在独立窗口,它不会自动继承主页面样式,需要引入自己的 CSS 和脚本。内容渲染可以直接操作 DOM,比如切换轮播项、替换 <video> 的 src、更新表格数据等。
消息协议建议统一为 JSON 对象,并用 type 字段区分操作类型。这样可以避免后续业务复杂后出现字符串解析混乱。主页面发送动作指令,接收页执行并回报状态;如果需要持续同步,也可以使用定时器批量发送,但要注意 PresentationConnection 的消息频率不宜过高,建议控制在每秒 10 条以内。
常见问题与调试建议
投屏失败的最常见原因是页面没有运行在安全上下文。Presentation API 要求 HTTPS 或 localhost 环境,普通 HTTP 页面在多数 Chromium 版本中无法调用 request.start()。另一个坑是相对路径:PresentationRequest 构造函数期望接收绝对 URL,如果传入 receiver.html 可能被解析为不支持的形式,因此插件内部用 new URL(...) 转成完整地址更稳妥。
用户手势是硬性要求。按钮点击、键盘事件、触摸事件都可以触发 start(),但异步回调中直接调用会被认为丢失手势上下文。遇到 NotAllowedError 时,可以检查是否在 setTimeout 或 Ajax 回调中启动了投屏。
调试第二屏时,可以在 Chrome 地址栏输入 chrome://media-engagement 查看媒体相关状态,但更实际的做法是让接收页保持 DevTools 打开。外接显示器上的窗口无法直接 F12 时,可以先在本地浏览器打开接收页并连接 DevTools,再利用 Presentation API 在测试环境启动。也可以将接收页做成支持 URL 参数的模式,在本机直接访问模拟连接。
jQueryPresentation API网页投屏修改时间:2026-10-04 20:38:35