webrtc远程控制系统源码学习笔记_客户端(一)
1、index.html
<br>
<div style="text-align:center;"><h2>WebRTC Remote Control</h2></div>
<hr style="background-color: #213F99;width: 100px;margin-left: auto;margin-right: auto;">
<div style="text-align:center;"><p class="lead">基于WebRTC的实时远程控制系统,支持视频监控、远程控制与SSH登录</p></div>
上面三行代码的作用是显示以下内容:
图1
<div class="container">
<div class="row">
<div class="col-md-4 offset-md-4">
<div class="row">
<div class="col-8 px-0">
<div class="dropdown">
<button class="btn btn-outline-secondary btn-block dropdown-toggle" id="dropdown_menu_link" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" style="overflow:hidden; text-overflow:ellipsis;">请选择你要连接的设备</button>
<div class="dropdown-menu" style="min-width: 100%" aria-labelledby="dropdown_menu_link" id="devices_list"></div>
</div>
</div>
<div class="col-1 px-0">
<button type="button" class="btn btn-outline-dark" onclick="getDevices()" id="device_refresh"><i class="fas fa-sync"></i></button>
</div>
<div class="col-3 pr-0">
<button type="button" class="btn btn-block btn-primary disabled" onclick="startSession()" id="connection_btn" disabled>连接</button>
</div>
</div>
<div class="row">
<div class="col-12 px-1">
<small id="loger"></small>
</div>
</div>
</div>
</div>
</div>
上面一段代码的作用是显示以下内容:
图2
<!-- Modal -->
<div class="modal fade" id="login_modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">登录</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<label for="device_id_show">Device ID:</label>
<input type="text" class="form-control" id="device_id_show" readonly>
<label for="password_show">Password</label>
<input type="password" class="form-control" id="password_input">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" onclick="submitPassword()">确定</button>
</div>
</div>
</div>
</div>
代码效果图:
图3
{{ template "cards" .}} //加载cards.html
<script>
var wsClient = null;
//页面关闭beforeunload事件,关闭 websocket 连接
$(window).on('beforeunload', function(){
wsClient.close();
});
//初始化websocket连接
function initWebsocket(){
wsClient = new WebSocket("ws://"+document.location.host+"/offer"); //设置
wsClient.onopen = function() {
//成功连接到服务器
console.log("connected to server"); //console.log() 方法用于在控制台输出信息
initWebRTC(); //初始化Webrtc连接
}
//连接关闭时触发是事件
wsClient.onclose = function(e) {
btnClose();
console.log("connection closed (" + e.code + ")");
}
//当接收到消息是事件触发
wsClient.onmessage = function(e) {
console.log("message received: " + e.data);
var obj = JSON.parse(e.data); //解析为json格式
//收到 answer 后配置 LocalDescription,建立webrtc连接
//收到 password 后显示 图3 界面
if(obj.type == "error"){
log(obj.msg);
stopSession();
}else if(obj.type == "answer"){
var remoteSessionDescription = obj.data;
if (remoteSessionDescription === '') {
alert('Session Description must not be empty');
}
try {
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(remoteSessionDescription))));
btnOpen(); //建立连接后触发的事件
} catch (e) {
alert(e);
}
}else if(obj.type == "password"){
$('#login_modal').modal('show');
$('#device_id_show').val(obj.device_id); //val()读取和修改一个表单元素的value字段值,此处为修改
$('#password_show').val("");
}
}
}
//当点击确认密码时触发的事件
function submitPassword(){
$('#login_modal').modal('hide');//将图3 界面隐藏
var content = new Object();
content["type"]="password";
content["data"]=$('#password_input').val(); //获取输入的密码,此处是读取
wsClient.send(JSON.stringify(content)); //将密码通过服务端转发到设备端
}
</script>
<script>
var pc;
var trackCache;
var trackFlag = true;
//初始化Webrtc连接
function initWebRTC(){
//参数配置
pc = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:118.89.111.54:3478'
}, {
url: "turn:118.89.111.54:3478",
username: "tyj",
credential: "123"
}
]
});
//在RTCPeerConnection接口上触发 track 事件时调用的方法
pc.ontrack = function (event) {
if(trackFlag){
trackCache = event.track;
trackFlag = false;
}else{
var el = document.getElementById('remote-video')
resStream = event.streams[0].clone() //复制一条全新的stream
resStream.addTrack(trackCache) //将一个MediaStream音频或视频的本地源,添加到WebRTC对等连接流对象中
el.srcObject = resStream //resStream赋值给video元素的srcobject ,这样媒体流就和页面中的video元素结合起来以便于呈现给用户
}
}
initSSH(); //ssh初始化
initControl(); //远程指令初始化
// 设置ICE连接状态处理函数
//当对等方已连接/断开连接时,将得到通知
pc.oniceconnectionstatechange = e => log(pc.iceConnectionState)
RTCPeerConnection 的属性 onicecandidate (是一个事件触发器 EventHandler) 能够让函数在事件icecandidate发生在实例 RTCPeerConnection 上时被调用。 只要本地代理ICE 需要通过信令服务器传递信息给其他对等端时就会触发。 这让本地代理与其
他对等体相协商而浏览器本身在使用时无需知道任何详细的有关信令技术的细节,只需要简单地应用这种方法就可使用您选择的任何消息
传递技术将ICE候选发送到远程对等方。
这应该设置为您提供的函数,该函数接受RTCPeerConnectionIceEvent表示icecandidate事件的对象作为输入。该功能应该通过信令服
务器将可以在事件属性中找到其SDP的ICE候选者传递candidate给远程对等体。
如果事件的candidate属性是null,ICE收集已经完成。不应将此消息发送到远程对等方。发生这种情况时,连接iceGatheringState也
已更改为complete。
关于webrtc中ICE的详细介绍参考:https://blog.csdn.net/u013692429/article/details/106529213/
pc.onicecandidate = event => {
if (event.candidate === null) {
//ICE收集已经完成则创建offer
var localSessionDescription = btoa(JSON.stringify(pc.localDescription));
var content = new Object();
content["type"]="offer";
content["device_id"]=$("#dropdown_menu_link").attr("value"); //attr() 方法设置或返回被选元素的属性值
content["data"]=localSessionDescription;
wsClient.send(JSON.stringify(content)); //交给服务器转发
//console.log("localDescription:",btoa(JSON.stringify(pc.localDescription)));
}
}
// Offer to receive 1 audio, and 1 video tracks
pc.addTransceiver('audio', {'direction': 'sendrecv'})
pc.addTransceiver('video', {'direction': 'sendrecv'})
pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log)
}
function log(msg){
$("#loger").html(msg);
}
</script>
<script>
var DevicesList = new Array();
//获取上线的设备
function getDevices(){
//先清空表单,并进行初始化
$("#devices_list").empty();
$("#dropdown_menu_link").text("请选择你要连接的设备");
$("#dropdown_menu_link").attr("value","");
var el = document.getElementById('remote-video');
el.srcObject = null;
//向服务器发送get请求,获取设备
//关于ajax的更多知识参考:https://www.cnblogs.com/tylerdonet/p/3520862.html
$.ajax({
type:"GET",
url:"/devices",
dataType:"json",
//请求成功后调用的回调函数
success:function(data){
//没有设备,则显示当前没有设备在线
//有则按格式显示设备
if(data.data == null){
$("#devices_list").prepend("<a class=\"dropdown-item disabled\" οnclick=\"dropdownShow($(this).text())\">当前没有设备在线</a>");
}else{
$.each(data.data, function (index, value) {
DevicesList[value.device_id] = value.using;
name = value.device_id +(value.using?"<font color=\"red\">[使用中]</font>":"<font color=\"green\">[可用]</font>");
//添加鼠标点击事件
$("#devices_list").prepend("<a class=\"dropdown-item\" οnclick=\"dropdownShow($(this))\" value=\""+value.device_id+"\">"+name+"</a>");
console.log(name);
});
}
},
//请求失败后调用的回调函数
error:function(jqXHR){
console.log("Error: "+jqXHR.status);
}
});
}
//页面启动时触发的事件
$(document).ready(function() {
getDevices();//获取上线设备
initTerm(); //初始化Term
});
//鼠标点击事件
function dropdownShow(a) {
$("#dropdown_menu_link").text(a.text()); //修改 dropdown_menu_link 显示的内容
$("#dropdown_menu_link").attr("value",a.attr("value")); //更新设备的value
//如果点击的设备在使用中,使得不能点击连接,否则可以
if(DevicesList[a.attr("value")] == true){
$("#connection_btn").addClass("disabled");
$("#connection_btn").attr("disabled", true);
}else{
$("#connection_btn").removeClass("disabled");
$("#connection_btn").attr("disabled", false);
}
}
//点击连接后触发的事件
function startSession(){
initWebsocket();
}
//点击断开后触发的事件
function stopSession(){
wsClient.close();
btnClose();
}
//建立webrtc连接后触发的事件
function btnOpen(){
$("#connection_btn").text("断开"); //连接按钮变为断开
$("#connection_btn").removeClass("btn_primary"); //移除btn_primary样式
$("#connection_btn").addClass("btn-danger"); //添加btn-danger样式
$("#connection_btn").attr("onclick","stopSession()");//添加鼠标点击事件
$("#dropdown_menu_link").addClass("disabled"); //使得不能点击dropdown_menu_link
$("#dropdown_menu_link").attr("disabled", true);
$("#device_refresh").addClass("disabled"); //使得不能点击device_refresh
$("#device_refresh").attr("disabled", true);
}
//断开webrtc连接后触发的事件,与上面同理
function btnClose(){
$("#connection_btn").text("连接");
$("#connection_btn").removeClass("btn-danger");
$("#connection_btn").addClass("btn_primary");
$("#connection_btn").attr("onclick","startSession()");
$("#connection_btn").addClass("disabled");
$("#connection_btn").attr("disabled", true);
$("#dropdown_menu_link").removeClass("disabled");
$("#dropdown_menu_link").attr("disabled", false);
$("#device_refresh").removeClass("disabled");
$("#device_refresh").attr("disabled", false);
$("#control-send").attr("disabled", true);
}
</script>
2、cards.html
<div class="container">
<div class="row">
<div class="col-md-6">
<br><video id="remote-video" controls="controls" autoplay class="m-0 p-0" style="width:100%; height:100%;object-fit:fill">等待初始化</video><br>
</div>
<div class="col-md-6">
<br>
<div class="card text-center">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" style="position: relative;">
<li class="nav-item">
<a class="nav-link active" onclick="sshShow()" id="ssh_nav">SSH</a>
</li>
<li class="nav-item">
<a class="nav-link" onclick="controlShow()" id="control_nav">Control</a>
</li>
<button type="button" class="btn btn-light m-1 py-0" onclick="fullscreenOpen()" id="fullscreen_btn" style="position:absolute;right:0px;"><i class="fas fa-expand-arrows-alt"></i></button>
</ul>
</div>
<div class="card-body" id="control_body" hidden>
<div class="input-group" style="width:100%;">
<textarea id="controlInput" placeholder='e.g. {"device_id":"led_0","operate":"on"}'rows="5" style="width:80%"></textarea>
<div class="input-group-append" style="float:left;
overflow:hidden;width:20%;"><button class="btn btn-outline-secondary" style="width:100%" type="button" id="control-send" onclick="controlSend()" disabled>Send</button></div>
</div>
<br>
<div class="input-group" style="width:100%">
<textarea id="control_output" style="width:80%" rows="5" readonly></textarea>
<div class="input-group-append" style="float:left;
overflow:hidden;width:20%;"><button class="btn btn-outline-secondary" style="width:100%" type="button" disabled>Receive</button></div>
</div>
</div>
<div class="card-body m-0 p-0" id="ssh_body">
<div class="container">
<div id="terminal" style="height:320px;width:100%"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="z-index: 9999; position: fixed ! important; right: 30px; top: 0px;" id="fullscreen_exit" hidden> <button type="button" class="btn btn-dark m-1" onclick="fullscreenClose()"><i class="fas fa-compress-arrows-alt"></i></button></div>
<br><br>
<script>
界面显示效果如图4:
var sshDC;
//初始化ssh
function initSSH(){
//创建一个label 为SSH 的DataChannel
sshDC = pc.createDataChannel("SSH");
term.clear(); //清空终端
term.writeln("welcome to use WebRTC Remote Control");//添加提示信息
//当SSH 的DataChannel连接时触发的事件
sshDC.onopen = function () {
userInput(); //输出提示信息以及接收user
console.log("datachannel open");
};
//当收到设备端的信息时触发的事件
sshDC.onmessage = function (event) {
//如果收到的不是success,重复
//如果成功,清空终端,分别向设备端、服务器发送提示信息
if(event.data != "success"){
userInput();
console.log(event.data);
}else{
term.clear();
sshDC.send("resize "+term.cols+" "+term.rows);
console.log("success resize "+term.cols+" "+term.rows);
//当接收到设备端消息时触发的事件,显示接收到的信息
sshDC.onmessage = function (event) {
inputCount = 0;
term.write('\r\n');
term.write(ab2str(event.data));
console.log(ab2str(event.data));
};
}
};
//连接关闭事件
sshDC.onclose = function () {
console.log("datachannel close");
};
}
//点击ssh标签事件
function sshShow(){
$("#ssh_body").attr("hidden",false);
$("#fullscreen_btn").attr("hidden",false)
$("#control_body").attr("hidden",true);
$("#ssh_nav").addClass("active");
$("#control_nav").removeClass("active");
}
</script>
<script>
var controlDC;
//初始化control
function initControl(){
controlDC = pc.createDataChannel("Control"); //创建label为control的datachannel
//接收到设备端信息时事件
controlDC.onmessage = function (event) {
console.log("received: " + event.data);
$("#control_output").val(event.data); //在control_output中显示接收到的信息
};
//datachannel建立时触发的事件
controlDC.onopen = function () {
$("#control-send").attr("disabled", false); //send 按钮不能按
console.log("datachannel open");
};
//连接关闭事件
controlDC.onclose = function () {
$("#control-send").attr("disabled", true);//send 按钮不能按
console.log("datachannel close");
};
}
//点击send事件
function controlSend(){
var msg = $("#controlInput").val();
controlDC.send(msg);
}
//点击control标签事件
function controlShow(){
$("#ssh_body").attr("hidden",true);
$("#fullscreen_btn").attr("hidden",true)
$("#control_body").attr("hidden",false);
$("#ssh_nav").removeClass("active");
$("#control_nav").addClass("active");
}
</script>
<script>
var term;
var input = '';
var pwTarget = false;
var userTarget = false;
var inputCount = 0;
//初始化模拟终端
//关于模拟终端的可以参考:https://blog.csdn.net/weixin_44685869/article/details/102420456
function initTerm(){
Terminal.applyAddon(fit); // Apply the `fit` addon
Terminal.applyAddon(fullscreen);//添加fullscreen功能
let terminalContainer = document.getElementById('terminal'); //获取html中id为terminal的元素
term = new Terminal({cursorBlink: true}) //初始化终端
term.open(terminalContainer); //将DIV绑定到初始化后的终端
term._initialized = true;
term.fit();
term.writeln("welcome to use WebRTC Remote Control");在终端中输出信息
//收到paste类型,触发是事件
term.on("paste", function(data) {
term.write(data);
});
//收到resize类型,触发的事件
term.on("resize", function(ev){
console.log("resize: cols:",term.cols," rows:",term.rows);
sshDC.send("resize "+term.cols+" "+term.rows);
});
//收到key类型,触发的事件
term.on("key", function(key,ev){
if(userTarget == true){
if(ev.keyCode === 13){
term.write('\r\n');
sshDC.send(input);
input = '';
passwordInput();
}else if (ev.keyCode === 8) {
if (input.length > 0) {
term.write('\b \b');
input = input.substr(0, input.length - 1);
}
}else{
input += key;
term.write(key);
}
}else if(pwTarget == true){
if(ev.keyCode === 13){
term.write('\r\n');
sshDC.send(input);
input = '';
pwTarget = false;
}else if (ev.keyCode === 8) {
if (input.length > 0) {
term.write('\b \b');
input = input.substr(0, input.length - 1);
}
}else{
input += key;
term.write("*");
}
}else {
sshDC.send(key);
if(ev.keyCode === 8){
if(inputCount > 0){
inputCount = inputCount - 1;
term.write('\b \b');
}
}else{
term.write(key);
inputCount = inputCount + 1;
}
}
})
}
//获取user
function userInput(){
term.writeln("~$ user:");
term.write("~$ ");
userTarget = true;
}
//获取密码
function passwordInput(){
userTarget = false;
term.writeln("~$ passward:");
term.write("~$ ");
pwTarget = true;
}
//转化为Uint8Array格式
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
//点击全屏时触发的事件
function fullscreenOpen(){
$("#fullscreen_exit").attr("hidden",false)
term.toggleFullScreen();
term.resize(parseInt($(window).width()/9),parseInt($(window).height()/18));
term.scrollToBottom();
}
//关闭全屏事件
function fullscreenClose(){
$("#fullscreen_exit").attr("hidden",true)
term.toggleFullScreen();
term.fit();
term.scrollToBottom();
}
</script>