功能特色:
使用前端技术实现局域网可用的屏幕共享程序,仅用于学习研究,商业化还需要解决很多问题。
技术要点
- MediaRecorder(chrome)
- nodejs
- scoket.io
架构
传播者(transmitter)> 中转器(nodejs)> 接收者(receiver)
项目目录结构
- client
- transmitter.html
- receiver.html
index.js(中转器)
实现细节
传播者【transmitter.html】
// 总体思路:获取到录屏数据arrayBuffer之后发送给中转器
// 1、socket连接
var socket = io('ws://' + location.hostname + ':9009', {
query: {
role: 'servant', // 身份标记
}
})
var connected = false;
socket.on('connect', function () {
connected = true;
});
socket.on('disconnect', function () {
connected = false;
})
// 录屏授权【chrome】
navigator.mediaDevices.getDisplayMedia({
video: true, // 这里我只需要视频,不要音频
}).then(function (stream) {
var mediaRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm\;codecs=vp8', // 这个很关键 #01
});
// 媒体片段
mediaRecorder.ondataavailable = function (e) {
if (e.data.size && connected) {
e.data.arrayBuffer().then(function (arrayBuffer) {
socket.emit('media', arrayBuffer); // 发送给中转器
})
}
}
mediaRecorder.start(200); // 每200毫秒触发一次片段
})
中转器【index.js】(nodejs执行这个脚本)
const Koa = require('koa');
const { createServer } = require('http')
const { Server } = require('socket.io');
const koaStatic = require('koa-static')
const koaBody = require('koa-body');
const path = require('path');
const app = new Koa();
const httpServer = createServer(app.callback());
// 启动一个静态服务器
app.use(koaBody({ multipart: true }))
app.use(koaStatic(path.resolve(__dirname, './client')));
app.listen(9000, () => console.log('服务已启动: 9000'));
// 启动一个socket服务器
const io = new Server(httpServer, { cors: true, maxhttpbuffersize: 1e6 * 50 });
let firstData; // 记录开播的第一个数据块
io.on('connection', (socket) => {
const { role } = socket.handshake.query;
if (role === 'transmitter') firstData = null; // 【传播者】如果重新开播,要刷新一下
socket.join("default-room"); // 全部加入同一个房间
// 开播过程中【 接收者】加入,是需要优先接收第一个数据块不然会异常
if (firstData) io.sockets.to("default-room").emit('mediaData', firstData);
// 收到【传播者】发送的媒体数据arrayBuffer
socket.on('media', function (data) {
if (!firstData) firstData = data;
socket.to("default-room").emit('mediaData', data) // 发送媒体数据给【接收者】
})
})
httpServer.listen(9009);
接收者【receiver.html】
<body>
<video id="video" style="width: 1280px; height: 720px;"></video>
<button id="look">观看</button>
</body>
window.onload = function () {
var video = document.querySelector('#video');
var btnLook = document.querySelector('#look');
var mediaSource = new MediaSource();
var chunks = [];
var sourceBuffer;
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen)
function sourceOpen() {
console.log('sourceOpen');
URL.revokeObjectURL(video.src);
var mime = 'video/webm; codecs=vp8'; // 这个很关键 #01
sourceBuffer = mediaSource.addSourceBuffer(mime);
sourceBuffer.mode = 'sequence';
}
// 把媒体片段加入视频播放缓存区
function appendSourceBuffer() {
if (chunks.length && sourceBuffer && !sourceBuffer.updating && mediaSource.readyState === 'open') {
sourceBuffer.appendBuffer(chunks.shift());
if (video.paused) video.play();
}
window.requestAnimationFrame(appendSourceBuffer);
}
// 开始播放
btnLook.addEventListener('click', function () {
appendSourceBuffer();
btnLook.remove();
});
// 连接socket
var socket = io('ws://' + location.hostname + ':9009', {
query: {
role: 'receiver'
}
})
var connected = false;
socket.on('connect', function () {
connected = true;
});
socket.on('disconnect', function () {
connected = false;
})
// 收到【传播者】发来的数据arrayBuffer
socket.on('mediaData', function (data) {
chunks.push(data);
});
}
期间遇到的难点
- MediaRecorder与mediaSource.addSourceBuffer的miniType有匹配关系,与编码格式有关。
- 使用socket发送数据不易过大,不然容易断开
- 【接收者】需要优先接收【传播者】开播的第一个媒体片段,不然addSourceBuffer之后会触发mediaSource关闭
结语
查了很多国内外的论坛资料才把这个场景下的功能需求完成,还有很多关于媒体方面的知识欠缺,还望路过的友人不吝赐教。
网友评论