janus-manager
An adapter manager for Janus client library.
Janus
原文档地址可参考
基于 janus-manager Vue实现的 janus-connector 组件库可用于快速构建通讯应用。
Usage
- install
npm install janus-manager --save
- step 1:引入类库,初始化
import JanusManager from "janus-manager";
const manager = new JanusManager();
// 查看当前版本
const version = JanusManager.version;
- step 2:服务器初始化,建立服务器连接
// 传入服务器地址,进行服务连接初始化,返回连接上下文对象
const context = await manager.init({
server: this.server,
// 设置连接超时时间,默认6000,60s
longPollTimeout: 60000,
// iceServers config
/* iceServers: [{
"urls": ["turn:xxxxx:xxxx"],
"username": "username",
"credential": "credential"
}]*/
}).catch((err) => {
// 可处理兼容性及连接异常错误
throw err
});
- step 3:传入
handle
插件配置,通过已建立的连接通道,将插件与通过建立关联,建立插件状态Session
// 创建一个可用于拨打电话的插件处理器,将插件与服务器建立关联,返回插件处理器对象,并进行消息监听
const handler = await manager.createCallHandle({
// plugin options
});
handler.on("message", (msg, jsep) => {
// handle events.
// eg: registered / hangup / accepted / progress / hangup | incomingcall
// 处理接通事件
const event = msg?.result?.event;
if (event === "progress" || event === "accepted") {
jsep && handler.handleRemoteJsep(jsep);
}
}
// 处理音频流播放事件
handler.on("remotestream", (stream) => {
manager.attachMediaStream(remoteMediaDom, stream);
});
- step 4:发送注册消息,建立
webrtc
通讯能力
handler.sendMessage("register", {
authuser: this.authuser,
proxy: this.sipServer,
secret: this.password,
username: this.username,
display_name: this.displayname,
});
- step 5:拨打/接听/挂断/拒接 电话
// 拨打电话,通话地址 格式: sip:{phone}@ip:port
handler.makeCall(uri);
// 发送数字键盘命令信息
// 参数: tones - DTMF tones
handler.sendDtmf({ tones: '1'});
// 挂断电话
handler.hangup();
// 接听电话 - 监听 incomingcall 事件
handler.on("message", (msg, jsep) => {
const event = msg?.result?.event;
if (event === "incomingcall") {
handler.answerCall(jsep, { audio: true });
}
}
// 拒接电话
handler.declineCall();
// 呼出端禁用音频输入
handler.muteAudio();
// 呼出端重新启用音频输入
handler.unmuteAudio();
// 判断呼出端是否启用音频输入,返回boolean
handler.isAudioMuted(); // true/false
- step 6:页面退出前,销毁管理器实例
// 等同于 manager.destory();
context.destory();
其他说明
音视频环境调用检测
const isSupported = manager.isSupportedUserMedia();
可用于在通话前判断当前系统兼容性,是否支持音视频调用
异步方法使用
在如下方法调用时,注意为异步操作,同时根据逻辑决定是否需要捕获异常,若不catch,执行错误时,会默认抛出异常
- 连接上下文创建:
await manager.init({ }).catch()
- 通话对象创建:
await manager.createCallHandle({ }).catch()
- 发送消息:
await handler.sendMessage({ }).catch()
- 通话相关操作
// 拨打电话
await handler.makeCall({ }).catch()
// 挂断通话
await handler.hangup({ }).catch()
// 拒绝来电
await handler.declineCall({ }).catch()
// 接听来电
await handler.answerCall({ }).catch()
异常处理
- 常规异常:所有
Promise
类型的方法调用异常,都可使用catch
来捕获 - 特殊异常:处理janus运行时的异常,如超时异常
const context = await manager.init({
server: this.server,
// 设置连接超时时间,默认6000,60s
longPollTimeout: 60000,
})
.catch(error => {
// 可处理兼容性及连接异常错误
});
context.error = (err: Error) => {
// 一般网络连接超时,可在此处处理
}
调用主流程
- 初始化管理器对象
manager
- 通过管理器对象创建服务连接上下文对象
context
- 使用
manager
创建业务操作对象handler
- 使用操作对象
handler
进行通话操作(可多次调用相关方法) - 流程结束,释放相关连接资源
context.destory
或manager.destory
以上操作除对
handler
对象进行相关操作外,其他流程只需要执行一次。
兼容性问题
-
麦克风权限
- Android中创建webview时,需先获取
android.permission.RECORD_AUDIO
和android.permission.MODIFY_AUDIO_SETTINGS
权限 - 同样IOS中也需获取麦克风权限,可通过
AVAudioSession.sharedInstance().recordPermission
获取麦克风权限
- Android中创建webview时,需先获取
-
音视频自动播放
- Android中可禁用自动播放需人工操作
webView.getSettings().setMediaPlaybackRequiresUserGesture(false)
- IOS中也可进行相关配置,
WKWebViewConfiguration().mediaTypesRequiringUserActionForPlayback
根据客户端使用语言或版本设置为[]
或WKAudiovisualMediaTypeNon
或false
可参考
- Android中可禁用自动播放需人工操作
-
因系统支持问题,在Android系统版本
>=7.0
及 IOS系统版本>=14.3
才支持在webview中获取系统mediaDevices,才可正常使用库基础功能, 可通过实例方法isSupportedUserMedia
来检测。
Demo
see call-demo
<template>
<div class="page">
<div class="home-container">
<!-- 第一部分 启动服务 -->
<div class="start-btn">
<button type="primary" @click="start">启动服务</button>
</div>
<!-- 第二部分 注册服务配置 -->
<div v-if="running" class="register-infor">
<div>
<input
v-model="sipServer"
type="text"
placeholder="SIP服务器(例如, sip:10.10.10.10:5688)"
/>
</div>
<div>
<input
v-model="username"
type="text"
placeholder="SIP身份(例如,sip:0000@10.10.10.10:5688)"
/>
</div>
<div>
<input
v-model="authuser"
type="text"
placeholder="SIP号(例如, 1001)"
/>
</div>
<div>
<input
v-model="password"
type="password"
placeholder="SIP密码"
/>
</div>
<div>
<input
v-model="displayname"
type="text"
placeholder="显示名称 (例如, 806100)"
/>
</div>
<div class="start-btn">
<button type="primary" @click="connect">建立连接</button>
</div>
</div>
<!-- 第三部分 通话 -->
<div v-if="connected" class="register-infor">
<div>
<input
placeholder="请输入手机号 如: 13688886666"
v-model="phoneNumber"
/>
</div>
<div class="start-btn">
<button
:class="{ primary: !calling, danger: calling }"
type="primary"
@click="callHandler"
>
{{ calling ? "挂断" : "拨打" }}
</button>
</div>
</div>
<!-- 远程音频,接听电话时音频输入,需在 remotestream 事件中处理 -->
<video ref="remoteMedia" class="hide" autoplay playsinline />
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import JanusManager from "janus-manager";
const manager = new JanusManager();
export default Vue.extend({
data() {
return {
running: false,
server: [
"https://servername.com/janus",
"wss://servername.com/janus",
],
sipServer: "sip:server_ip:port", // SIP服务器
username: "sip:user_name@server_ip:port", // SIP身份
authuser: "user", // SIP号
password: "password",
displayname: "", // 显示名称
connected: false,
calling: false,
phoneNumber: "",
};
},
computed: {
callNumber() {
return `sip:${this.phoneNumber}@server_ip:port`;
},
},
methods: {
async start() {
const context = await manager
.init({ server: this.server })
.catch((err) => {
console.error(err);
this.running = false;
throw err;
});
this.context = context;
this.running = true;
},
async connect() {
const handler = await manager.createCallHandle({
mediaState: function (medium, on) {
const status = on ? "started" : "stopped";
const msg = `Janus ${status} receiving our ${medium}`;
console.info("Janus mediaState:", msg);
},
webrtcState: function (on) {
const status = on ? "up" : "down";
const msg = `Janus says our WebRTC PeerConnection is ${status}`;
console.info("Janus webrtcState:", msg);
},
iceState: function (state) {
// state: 'connected' | 'failed'
console.info("iceState", state);
},
});
handler.on("message", (msg, jsep) => {
console.info("message event:", msg?.result?.event, jsep);
const event = msg && msg.result && msg.result.event;
// 注册成功
if (event === "registered") {
this.connected = true;
} else if (event === "hangup") {
// 通话挂断
console.warn("通话已挂断:", msg, jsep);
this.calling = false;
handler.hangup();
} else if (event === "progress" || event === "accepted") {
// 接听
jsep && handler.handleRemoteJsep(jsep);
}
});
handler.on("remotestream", (stream) => {
manager.attachMediaStream(this.$refs.remoteMedia, stream);
});
console.log("connect success....");
this.handler = handler;
handler.sendMessage("register", {
authuser: this.authuser,
proxy: this.sipServer,
secret: this.password,
username: this.username,
display_name: this.displayname,
});
},
callHandler() {
if (this.calling) {
this.handler.hangup();
this.calling = false;
return;
}
if (this.phoneNumber === "") return;
this.handler.makeCall(this.callNumber).catch(error => {
throw error;
});
},
},
beforeDestroy() {
this.context && this.context.destroy();
},
});
</script>
<style lang="less" scoped>
.page {
text-align: center;
padding: 20px;
display: flex;
justify-content: center;
.text-center {
text-align: center;
}
.home-container {
text-align: left;
width: 300px;
.hide {
display: none;
}
input {
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #dcdfe6;
padding: 0 15px;
box-sizing: border-box;
width: 100%;
}
button {
border: none;
color: #fff;
padding: 10px 20px;
border-radius: 4px;
background-color: #357bff;
cursor: pointer;
&:focus {
outline: none;
}
&:active {
background-color: #2671fc;
}
}
.primary {
background-color: #357bff;
}
.danger {
background-color: #f8483b;
}
.register-infor {
margin-top: 10px;
> div {
margin-bottom: 10px;
}
}
}
}
</style>