简述
本文是以FreeSwitch作为信令服务器,通过sipjs(基于webRtc) 进行媒体协商,网络协商后,进行P2P媒体传输。
参考知识:
效果图:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<title>视频通话demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="shortcut icon" th:href="@{/ico/logo.ico}" type="image/x-icon" />
<link rel="stylesheet" th:href="@{/layui/css/layui.css}">
<link rel="stylesheet" th:href="@{/css/baiban.css}">
<script th:inline="javascript">
const pub = [[${pub}]];
</script>
</head>
<body>
<div id="app">
<!--头部导航-->
<ul class="layui-nav layui-bg-blue" lay-bar="disabled">
<li class="layui-nav-item"><a target="_blank">视频通话</a></li>
</ul>
<!--音视频通话-->
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 20px;">
<legend>音视频通话</legend>
</fieldset>
<div class="layui-row" style="border: 1px solid #f0f0f0;margin-left: 10px;margin-right: 10px;padding-top: 10px">
<div class="layui-col-xs12 layui-col-md4" >
<form class="layui-form">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">我的号码:</label>
<div class="layui-input-inline">
<input id="myNumber" value="1000" autocomplete="off" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">信令地址:</label>
<div class="layui-input-inline">
<input id="sipAddr" th:value="${fs}" autocomplete="off" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">Websocket端口:</label>
<div class="layui-input-block">
<input type="radio" name="wsUrl" value="5066" title="http" checked>
<input type="radio" name="wsUrl" value="7443" title="https">
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">拨打号码</label>
<div class="layui-input-inline">
<input id="sip_phone_number" value="1001" autocomplete="off" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item" style="text-align: center">
<div class="layui-inline">
<button type="button" class="layui-btn layui-btn-primary" onclick="softPhone.start()"
id="register">注册
</button>
<button type="button" class="layui-btn layui-btn-normal operate layui-btn-disabled" disabled
onclick="PHONE.call()" id="call">拨打
</button>
<button type="button" class="layui-btn layui-btn-normal operate layui-btn-disabled" disabled
onclick="PHONE.hangUp()" id="hangup">挂断
</button>
</div>
</form>
</div>
<!--视频展示区-->
<div class="layui-col-xs12 layui-col-md4" style="border:5px solid #3385FF;width: 615px;height: 600px">
<div id="playVideo">
<video id="youVideo" style="padding-right:10px" width="600px" height="600px" muted autoplay
onmousedown="move(this)" th:poster="@{/img/webrtc.png}"
playsinline></video>
</div>
<div style="position:relative;top:-168px;left:1px">
<video id="meVideo" width="100px" height="100px" th:poster="@{/img/webrtc.png}" autoplay playsinline></video>
</div>
</div>
</div>
</div>
<canvas id="capture" style="display: none"></canvas>
<script th:src="@{/javascript/jquery-2.1.4.js}" type="application/javascript"></script>
<script th:src="@{/javascript/sip-0.15.10.js}" type="application/javascript"></script>
<script th:src="@{/layui/layui.js}" type="application/javascript"></script>
<!--操作-->
<script th:src="@{/javascript/sip-pc/operation.js}" type="application/javascript"></script>
let table = null;
let form = null;
let layer = null;
let laydate = null;
let upload = null;
// 拨打电话确认框
let confirmIndex = null;
layui.use(['table', 'form', 'layer', 'laydate', 'upload'], function () {
$(function () {
table = layui.table;
form = layui.form;
layer = layui.layer;
upload = layui.upload;
if (window.location.protocol === 'http:') {
$('input[name="wsUrl"][value="5066"]').attr("checked", true);
} else {
$('input[name="wsUrl"][value="7443"]').attr("checked", true);
}
});
});
// 本地视频
var localVideo = document.getElementById('meVideo');
// 远端视频
var remoteVideo = document.getElementById('youVideo');
var softPhoneUA = null;
var currentSession = null;
/**
* 注册模块
* @type {{logout(): void, start(): void, unregister(): void, UAEvent(*): void, register(): void, sessionEvent(*): void}}
*/
const softPhone = {
/**
* 登陆软电话
*/
start() {
// 获取我的号码
var myNumber = $.trim($('#myNumber').val());
// 获取sip地址
var sipAddr = $.trim($('#sipAddr').val());
// sip url 拼接
var sip_uri = myNumber + '@' + sipAddr
// 信令密码
var sip_password = $.trim($('#sipPassword').val());
// 获取WS连接端口
var wsPort = $.trim($('input[name="wsUrl"]:checked').val());
// var wsPort = "5060";
var ws_uri = wsPort == '5066' ? 'ws://' + sipAddr + ':' + wsPort : 'wss://' + sipAddr + ':' + wsPort
var config = {
uri: sip_uri,
transportOptions: {
wsServers: [ws_uri]
},
// 授权号
authorizationUser: myNumber,
// 登陆密码
password: '1234',
displayName: myNumber,
register: true
};
//v 就绪软电话、监听软电话连接状态、监听电话呼入、拨打电话、登出软电话系统
softPhoneUA = new SIP.UA(config);
softPhone.UAEvent(softPhoneUA);
// 有电话呼入
softPhoneUA.on('invite', function (session) {
currentSession = session;
softPhone.sessionEvent(session);
layer.confirm('有电话呼入 ... 请注意是否接听)', {
btn: ['取消', '接听', '拒绝'],
btn1: function () {
layer.close(index);
},
btn2: function () {
PHONE.answer();
},
btn3: function () {
softPhone.hangUp();
}
});
})
},
/** 就绪 */
register() {
softPhoneUA.register({ // 注册
register: true
});
},
/**
* 绑定ua事件
* @param {*} ua
*/
UAEvent(ua) {
// 开始尝试连接
ua.on('connecting', function (args) {
console.log('%c connecting - 开始尝试连接', 'color: #f00');
});
// 连接完毕
ua.on('connected', function () {
console.log('%c connected - 连接完毕', 'color: #f00');
});
// 主动取消注册或注册后定期重新注册失败
ua.on('unregistered', function (response, cause) {
$('#register').removeClass("layui-btn-disabled").removeAttr('disabled');
console.log('%c unregistered - 主动取消注册或注册后定期重新注册失败', 'color: #f00');
});
// 注册成功
ua.on('registered', function () {
layer.msg("注册成功", {icon: 1, time: 1500});
console.log('%c registered -- 注册成功', 'color: #f00');
btnHide(['register','shard'])
btnShow(['call'])
})
// websocket 连接失败
ua.on('disconnected', function () {
console.log('%c disconnected - 连接失败', 'color: #f00');
})
},
/**
* 绑定session事件
* @param {} session
*/
sessionEvent(session) {
session.on("rejected", function (response, cause) {
layer.close(confirmIndex);
});
session.on("bye", function (response, cause) {
// 不可用
btnHide(['hangup','mute','unmute','openVideo','closeVideo','shard'])
localVideo.srcObject = null;
remoteVideo.srcObject = null;
});
// 会话被接入
session.on("accepted", function (response, cause) {
layer.close(confirmIndex);
btnShow(['hangup','mute','unmute','openVideo','closeVideo','shard','capturePic'])
var pc = session.sessionDescriptionHandler.peerConnection;
var remoteStream = new MediaStream();
pc.getReceivers().forEach(function (receiver) {
remoteStream.addTrack(receiver.track);
});
remoteVideo.srcObject = remoteStream;
if (pc.getSenders()) {
var localStream = new MediaStream();
pc.getSenders().forEach(function (sender) {
localStream.addTrack(sender.track);
});
localVideo.srcObject = localStream;
}
});
session.on("cancel", function (response, cause) {
layer.close(confirmIndex);
});
}
}
/**
* 拨打、接听、挂断 模块
* @type {{call(): void, answer(): void, hangUp(): void}}
*/
const PHONE = {
/**
* 拨打
*/
call() {
const telNumber = $.trim($('#sip_phone_number').val());
var sipAddr = $.trim($('#sipAddr').val());
const inviteUrl = telNumber + '@' + sipAddr
currentSession = softPhoneUA.invite(inviteUrl, {
sessionDescriptionHandlerOptions: {
constraints: {
audio: {
autoGainControl: true,
// 噪音消除
noiseSuppression: true,
// 设置降噪
echoCancellation: true
},
video: true
},
alwaysAcquireMediaFirst: true // 此参数是sip.js官方修复在firefox遇到的bug所设置
})
confirmIndex = layer.confirm('呼叫中....', {
btn: ['取消'],
btn1: function (index) {
currentSession.cancel();
layer.close(index);
}
});
// 拨打后 监听
currentSession.on("rejected", function (response, cause) {
layer.msg("请求通话被拒绝", {icon: 1, time: 1500});
console.log(response)
console.log(cause)
});
// 本次通话结束
currentSession.on("bye", function (response, cause) {
layer.msg("本次通话结束", {icon: 1, time: 1500});
localVideo.srcObject = null;
remoteVideo.srcObject = null;
});
// 对方接听
currentSession.on("accepted", function (response, cause) {
layer.msg("对方接听", {icon: 1, time: 1500});
$('#call').addClass('layui-btn-disabled').attr('disabled', 'disabled');
$('#hangup').removeClass('layui-btn-disabled').removeAttr('disabled');
$('#mute').removeClass('layui-btn-disabled').removeAttr('disabled');
$('#unmute').removeClass('layui-btn-disabled').removeAttr('disabled');
var pc = currentSession.sessionDescriptionHandler.peerConnection;
var remoteStream = new MediaStream();
pc.getReceivers().forEach(function (receiver) {
remoteStream.addTrack(receiver.track);
});
remoteVideo.srcObject = remoteStream;
if (pc.getSenders()) {
var localStream = new MediaStream();
pc.getSenders().forEach(function (sender) {
localStream.addTrack(sender.track);
});
localVideo.srcObject = localStream;
}
});
// 取消通话
currentSession.on("cancel", function (response, cause) {
layer.msg("取消通话", {icon: 1, time: 1500});
});
},
/**
* 挂断
*/
hangUp() {
if (currentSession instanceof Object) {
if (currentSession.hasAnswer) {
currentSession.bye();
} else if (currentSession.isCanceled === false) {
currentSession.cancel();
} else {
currentSession.reject();
}
}
},
/**
* 接听
*/
answer() {
var option = {
sessionDescriptionHandlerOptions: {
constraints: {
audio: {
autoGainControl: true,
// 噪音消除
noiseSuppression: true,
// 设置降噪
echoCancellation: true
},
video: true
},
alwaysAcquireMediaFirst: true, // 此参数是sip.js官方修复在firefox遇到的bug所设置
rtcConfiguration: {
iceServers: [
{
url: "stun:124.222.83.153:3478",
username: "test",//可选
credential: "test123"//可选
},
{
url: "turn:124.222.83.153:3478",
"username": "test",//可选
"credential": "test123"//可选
}
]
}
}
}
currentSession.accept(option)
}
}




导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc
动漫制作技巧是很多新人想了解的问题,今天小编就来解答与大家分享一下动漫制作流程,为了帮助有兴趣的同学理解,大多数人会选择动漫培训机构,那么今天小编就带大家来看看动漫制作要掌握哪些技巧?一、动漫作品首先完成草图设计和原型制作。设计草图要有目的、有对象、有步骤、要形象、要简单、符合实际。设计图要一致性,以保证制作的顺利进行。二、原型制作是根据设计图纸和制作材料,可以是手绘也可以是3d软件创建。在此步骤中,要注意的问题是色彩和平面布局。三、动漫制作制作完成后,加工成型。完成不同的表现形式后,就要对设计稿进行加工处理,使加工的难易度降低,并得到一些基本准确的概念,以便于后续的大样、准确的尺寸制定。四、
2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p
Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图
我正在尝试用ruby编写一个简单的网络抓取代码。它一直工作到第29个url,然后我收到此错误消息:C:/Ruby193/lib/ruby/1.9.1/open-uri.rb:346:in`open_http':500InternalServerError(OpenURI::HTTPError)fromC:/Ruby193/lib/ruby/1.9.1/open-uri.rb:775:in`buffer_open'fromC:/Ruby193/lib/ruby/1.9.1/open-uri.rb:203:in`blockinopen_loop'fromC:/Ruby193/lib/r
我正在使用Ruby/Mechanize编写一个“自动填写表格”应用程序。它几乎可以工作。我可以使用精彩CharlesWeb代理以查看服务器和我的Firefox浏览器之间的交换。现在我想使用Charles查看服务器和我的应用程序之间的交换。Charles在端口8888上代理。假设服务器位于https://my.host.com。.一件不起作用的事情是:@agent||=Mechanize.newdo|agent|agent.set_proxy("my.host.com",8888)end这会导致Net::HTTP::Persistent::Error:...lib/net/http/pe
我正在寻找用于Rails的优质管理插件。似乎大多数现有的插件/gem(例如“restful_authentication”、“acts_as_authenticated”)都围绕着self注册等展开。但是,我正在寻找一种功能齐全的基于管理/管理角色的解决方案——但不是简单地附加到另一个非基于角色的解决方案。如果我找不到,我想我会自己动手......只是不想重新发明轮子。 最佳答案 RyanBates最近做了两个关于授权的railscast(注意身份验证和授权之间的区别;身份验证检查用户是否如她所说的那样,授权检查用户是否有权访问资源
目前我正在使用这个正则表达式从YoutubeURL中提取视频ID:url.match(/v=([^&]*)/)[1]我怎样才能改变它,以便它也可以从这个没有v参数的YoutubeURL获取视频ID:http://www.youtube.com/user/SHAYTARDS#p/u/9/Xc81AajGUMU感谢阅读。编辑:我正在使用ruby1.8.7 最佳答案 对于Ruby1.8.7,这就可以了。url_1='http://www.youtube.com/watch?v=8WVTOUh53QY&feature=feedf'url