功能特色:
使用前端技術(shù)實現(xiàn)局域網(wǎng)可用的屏幕共享程序龄砰,僅用于學(xué)習(xí)研究,商業(yè)化還需要解決很多問題暮芭。
技術(shù)要點
- MediaRecorder(chrome)
- nodejs
- scoket.io
架構(gòu)
傳播者(transmitter)> 中轉(zhuǎn)器(nodejs)> 接收者(receiver)
項目目錄結(jié)構(gòu)
- client
- transmitter.html
- receiver.html
index.js(中轉(zhuǎn)器)
實現(xiàn)細(xì)節(jié)
傳播者【transmitter.html】
// 總體思路:獲取到錄屏數(shù)據(jù)arrayBuffer之后發(fā)送給中轉(zhuǎn)器
// 1烈疚、socket連接
var socket = io('ws://' + location.hostname + ':9009', {
query: {
role: 'servant', // 身份標(biāo)記
}
})
var connected = false;
socket.on('connect', function () {
connected = true;
});
socket.on('disconnect', function () {
connected = false;
})
// 錄屏授權(quán)【chrome】
navigator.mediaDevices.getDisplayMedia({
video: true, // 這里我只需要視頻,不要音頻
}).then(function (stream) {
var mediaRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm\;codecs=vp8', // 這個很關(guān)鍵 #01
});
// 媒體片段
mediaRecorder.ondataavailable = function (e) {
if (e.data.size && connected) {
e.data.arrayBuffer().then(function (arrayBuffer) {
socket.emit('media', arrayBuffer); // 發(fā)送給中轉(zhuǎn)器
})
}
}
mediaRecorder.start(200); // 每200毫秒觸發(fā)一次片段
})
中轉(zhuǎn)器【index.js】(nodejs執(zhí)行這個腳本)
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());
// 啟動一個靜態(tài)服務(wù)器
app.use(koaBody({ multipart: true }))
app.use(koaStatic(path.resolve(__dirname, './client')));
app.listen(9000, () => console.log('服務(wù)已啟動: 9000'));
// 啟動一個socket服務(wù)器
const io = new Server(httpServer, { cors: true, maxhttpbuffersize: 1e6 * 50 });
let firstData; // 記錄開播的第一個數(shù)據(jù)塊
io.on('connection', (socket) => {
const { role } = socket.handshake.query;
if (role === 'transmitter') firstData = null; // 【傳播者】如果重新開播斗蒋,要刷新一下
socket.join("default-room"); // 全部加入同一個房間
// 開播過程中【 接收者】加入捌斧,是需要優(yōu)先接收第一個數(shù)據(jù)塊不然會異常
if (firstData) io.sockets.to("default-room").emit('mediaData', firstData);
// 收到【傳播者】發(fā)送的媒體數(shù)據(jù)arrayBuffer
socket.on('media', function (data) {
if (!firstData) firstData = data;
socket.to("default-room").emit('mediaData', data) // 發(fā)送媒體數(shù)據(jù)給【接收者】
})
})
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'; // 這個很關(guān)鍵 #01
sourceBuffer = mediaSource.addSourceBuffer(mime);
sourceBuffer.mode = 'sequence';
}
// 把媒體片段加入視頻播放緩存區(qū)
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;
})
// 收到【傳播者】發(fā)來的數(shù)據(jù)arrayBuffer
socket.on('mediaData', function (data) {
chunks.push(data);
});
}
期間遇到的難點
- MediaRecorder與mediaSource.addSourceBuffer的miniType有匹配關(guān)系,與編碼格式有關(guān)泉沾。
- 使用socket發(fā)送數(shù)據(jù)不易過大捞蚂,不然容易斷開
- 【接收者】需要優(yōu)先接收【傳播者】開播的第一個媒體片段,不然addSourceBuffer之后會觸發(fā)mediaSource關(guān)閉
結(jié)語
查了很多國內(nèi)外的論壇資料才把這個場景下的功能需求完成跷究,還有很多關(guān)于媒體方面的知識欠缺姓迅,還望路過的友人不吝賜教。