运行Web Demo
本文通过在Web浏览器中演示通信场景,介绍Web Demo的运行示例。
前提条件
已开通音视频通信服务。具体操作,请参见开通服务。
环境准备
本文示例环境为Windows 10系统下Chrome 90版本浏览器。
创建产品和设备
为了演示实际通信场景,需要准备两台设备作为主叫方(Host)和被呼叫方(Guest)。
在阿里云物联网平台控制台创建产品。具体操作,请参见创建产品。
在阿里云物联网平台,创建两台设备作为主叫方和被呼叫方。
DeviceName示例:web-host、web-guest。具体操作,请参见创建设备。
说明请在物联网平台获取以下参数,后续编译需要使用。
设备证书:设备创建成功后,将生成设备证书。设备证书包含ProductKey、DeviceName和DeviceSecret。
设备接入域名:在实例详情页面,单击查看开发配置,获取设备接入域名。
运行Demo
音视频通信服务控制台已提供Web Demo运行窗口,您可直接进行音视频通话调试。
登录物联网平台控制台,在左侧导航栏选择增值服务。
在增值服务页面,单击音视频通信服务。
在音视频通信服务控制台左侧导航栏,选择实例,然后单击服务示例。
在服务示例页面,输入呼叫方和被呼叫方的设备信息,选择通话类型。
单击连接,接入音视频通信服务器,然后单击呼叫,向对端设备发起通话。例如视频通话,界面如下:您可单击对应图标管理通话:
:开启、关闭显示时间。
:开启、关闭麦克风。
:开启、关闭摄像头。
:结束通话。
支持使用当前操作系统自带的截图功能进行通话截图。
附录:示例代码
初始化设备证书信息
const { Engine } = window.AliDeviceRtcEngine;
const engine = new Engine({
instanceId: '***'
productKey: '****',
deviceName: '****',
deviceSecret: '******',
});
// 捕获异常信息。
engine.onError(error => {
const { message } = error;
LogStore.add(`【异常信息】${message || '未知异常'}`);
});
engine.init();
管理主叫方设备通话
const HostClient = props => {
const { engine, calleeInfo, onClose } = props;
const [visible, setVisible] = useState(false);
const [counter, setCounter] = useState(0); // 通话时间。
const [micMuted, setMicMuted] = useState(false); // 禁用麦克风。
const [cameraMuted, setCameraMuted] = useState(false); // 摄像头禁用。
const [networkStatus, setNetworkStatus] = useState(0); // 网络质量。
const channelRef = useRef({}); // channelId。
const guestInfo = useRef({}); // 接听方信息。
const timer = useRef(); // 通话时间定时器。
const responseTimer = useRef(); // 等待响应时间。
const { channelType } = calleeInfo || {};
function handleMuteMic() {
engine.disableLocalAudioPublish(!micMuted);
setMicMuted(v => !v);
LogStore.add(`${micMuted ? '开启' : '关闭'}麦克风`)
}
function handleMuteCamera() {
engine.disableLocalVideoPublish(!cameraMuted);
setCameraMuted(v => !v);
LogStore.add(`${cameraMuted ? '开启' : '关闭'}摄像头`)
}
function handleTerminate() {
clearTimeout(responseTimer.current);
const { channelId } = channelRef.current;
if (channelId) {
// 结束通话。
engine.cancelChannel(channelId);
setCounter(0);
clearInterval(timer.current);
LogStore.add('主叫设备结束通话')
}
setVisible(false);
onClose();
}
// 对方接听电话。
function onAccept(res) {
guestInfo.current = res;
setCounter(0);
timer.current = setInterval(() => {
setCounter(v => v + 1);
}, 1000);
clearTimeout(responseTimer.current);
LogStore.add('被叫设备接听通话')
}
// 对方拒听电话。
function onReject() {
setCounter(0);
setVisible(false);
clearTimeout(responseTimer.current);
onClose();
LogStore.add('被叫设备拒听通话')
}
// 对方忙线。
function onBusy() {
setCounter(0);
setVisible(false);
clearTimeout(responseTimer.current);
onClose();
LogStore.add('被叫设备忙线')
}
// 对方挂断。
function onLeave() {
setCounter(0);
setVisible(false);
clearInterval(timer.current);
clearTimeout(responseTimer.current);
onClose();
LogStore.add('被叫设备挂断')
}
// 主叫时,被呼叫直接返回忙线状态。
function onCalling(response) {
const { callerIotId, channelId } = response;
engine.guestResponse('busy', callerIotId, channelId);
LogStore.add('主叫设备忙线')
}
// 网络质量。
function onNetworkQuality(data) {
const { downlinkNetworkQuality } = data;
setNetworkStatus(downlinkNetworkQuality);
}
// 对方断线。
function onUserLeave(data) {
const { userId } = data;
const { iotId } = guestInfo.current;
if (userId === iotId) {
handleTerminate();
LogStore.add('被叫设备断线')
}
}
// 加入频道。
function onJoin(info) {
const { appid, channel } = info;
LogStore.add(`主叫设备加入频道:${channel}, 应用id: ${appid}`);
}
useEffect(() => {
if (engine) {
engine.on('accept', onAccept);
engine.on('reject', onReject);
engine.on('busy', onBusy);
engine.on('leave', onLeave);
engine.on('calling', onCalling);
engine.on('networkQuality', onNetworkQuality);
engine.on('userLeave', onUserLeave);
engine.on('join', onJoin);
}
return () => {
if (engine) {
engine.off('accept', onAccept);
engine.off('reject', onReject);
engine.off('busy', onBusy);
engine.off('leave', onLeave);
engine.off('calling', onCalling);
engine.off('networkQuality', onNetworkQuality);
engine.off('userLeave', onUserLeave);
engine.off('join', onJoin);
}
}
}, [engine]);
useEffect(() => {
if (calleeInfo) {
(async function () {
const { productKey, deviceName, channelType } = calleeInfo;
// 呼叫对方。
const channelId = await engine.launchChannel(
productKey,
deviceName,
channelType,
true,
'target_video',
);
channelRef.current.channelId = channelId;
setVisible(true);
LogStore.add(`呼叫设备:${deviceName}`)
// 一分钟未响应自动挂断。
responseTimer.current = setTimeout(() => {
handleTerminate();
}, 1000 * 60);
})();
}
}, [calleeInfo]);
useEffect(() => {
if (visible) {
// 开启本地预览。
engine && engine.startPreview('preview_video');
} else {
// 关闭本地预览。
engine && engine.stopPreview();
}
}, [visible]);
return (
<Dialog className={styles.dialog} visible={visible} footer={false}>
<div>
<div className={styles.view} style={{ display: channelType === 'video' ? 'flex' : 'none' }}>
<div className={styles.card}>
<div className={styles.title}>{'本地画面'}</div>
<video id="preview_video" playsInline autoPlay />
</div>
<div className={styles.card}>
<div className={styles.title}>{'对方画面'}</div>
<video id="target_video" playsInline autoPlay />
</div>
</div>
<div className={styles.content}>
<div className={styles.message}>
{counter ? `通话时长 ${formatTimer(counter)}` : '呼叫中...'}
<div className={styles.sub}>{`网络质量:${NETWORK_QUALITY[networkStatus]}`}</div>
</div>
<div className={styles.controls}>
<Button text onClick={handleMuteMic}>
{micMuted ? (
<img src="https://img.alicdn.com/imgextra/i4/O1CN01RFU4IX1D8MTnFpE3T_!!6000000000171-55-tps-20-20.svg" />
) : (
<img src="https://img.alicdn.com/imgextra/i4/O1CN01c6D0ZD1wRHYIbG4qd_!!6000000006304-55-tps-20-20.svg" />
)}
</Button>
{channelType === 'video' && (
<Button text onClick={handleMuteCamera}>
{cameraMuted ? (
<img src="https://img.alicdn.com/imgextra/i1/O1CN01xl7k8S1MTH6rK2hwV_!!6000000001435-55-tps-20-20.svg" />
) : (
<img src="https://img.alicdn.com/imgextra/i2/O1CN01gT9gL91jX9cykjy4r_!!6000000004557-55-tps-20-20.svg" />
)}
</Button>
)}
<Button text onClick={handleTerminate}>
<img src="https://img.alicdn.com/imgextra/i3/O1CN01MPqmGR1l8zy3W8G8h_!!6000000004775-55-tps-20-20.svg" />
</Button>
</div>
</div>
</div>
</Dialog>
);
};
管理被呼叫方设备通话
const GuestClient = props => {
const { engine } = props;
const [visible, setVisible] = useState(false);
const [counter, setCounter] = useState(0); // 通话时间。
const [micMuted, setMicMuted] = useState(false); // 禁用麦克风。
const [cameraMuted, setCameraMuted] = useState(false); // 摄像头禁用。
const [networkStatus, setNetworkStatus] = useState(0); // 网络质量。
const timer = useRef(); // 定时器。
const callerInfo = useRef({}); // 主叫信息。
const { channelType } = callerInfo.current;
function handleAccept() {
const { callerIotId, channelId } = callerInfo.current;
// 接听通话。
engine.guestResponse(
'accept',
callerIotId,
channelId,
true,
'target_video',
);
Notification.destroy();
setVisible(true);
setCounter(0);
timer.current = setInterval(() => {
setCounter(v => v + 1);
}, 1000);
LogStore.add(`接听设备:${callerIotId}`)
}
function handleReject() {
const { callerIotId, channelId } = callerInfo.current;
engine.guestResponse('reject', callerIotId, channelId);
Notification.destroy();
callerInfo.current = {};
LogStore.add(`拒听设备:${callerIotId}`)
}
function handleLeave() {
const { channelId } = callerInfo.current;
// 结束通话。
engine.guestLeaveChannel(channelId);
callerInfo.current = {};
setCounter(0);
setVisible(false);
clearInterval(timer.current);
LogStore.add(`被叫设备结束通话`)
}
function handleMuteMic() {
engine.disableLocalAudioPublish(!micMuted);
setMicMuted(v => !v);
LogStore.add(`${micMuted ? '开启' : '关闭'}麦克风`)
}
function handleMuteCamera() {
engine.disableLocalVideoPublish(!cameraMuted);
setCameraMuted(v => !v);
LogStore.add(`${cameraMuted ? '开启' : '关闭'}摄像头`)
}
function onCalling(response) {
if (isEmpty(callerInfo.current)) {
callerInfo.current = response;
Notification.open({
className: styles.dialog,
duration: 0,
content: (
<div className={styles.content}>
<i className={classnames('iconfont icon-zhanghaoquanxianguanli1', styles.avatar)} />
<div className={styles.message}>
<div className={styles.name}>{response.callerIotId}</div>
{counter ? `通话时长 ${formatTimer(counter)}` : '呼叫中...'}
</div>
<div className={styles.controls}>
<Button text onClick={handleAccept}>
<i className={classnames('iconfont icon-zhengqueshixin1', styles.accept)} />
</Button>
<Button text onClick={handleReject}>
<i className={classnames('iconfont icon-cuowushixin', styles.reject)} />
</Button>
</div>
</div>
),
});
LogStore.add(`被叫设备收到来电`)
} else {
const { callerIotId, channelId } = response;
engine.guestResponse('busy', callerIotId, channelId);
LogStore.add('被叫设备忙线')
}
}
function onTerminate() {
Notification.destroy();
callerInfo.current = {};
setCounter(0);
setVisible(false);
clearInterval(timer.current);
LogStore.add('主叫设备挂断')
}
function onNetworkQuality(data) {
const { downlinkNetworkQuality } = data;
setNetworkStatus(downlinkNetworkQuality);
}
// 对方断线。
function onUserLeave(data) {
const { userId } = data;
const { callerIotId } = callerInfo.current;
if (userId === callerIotId) {
handleLeave();
LogStore.add('主叫设备断线')
}
}
// 加入频道。
function onJoin(info) {
const { appid, channel } = info;
LogStore.add(`被叫设备加入频道:${channel}, 应用id: ${appid}`);
}
useEffect(() => {
if (engine) {
engine.on('calling', onCalling);
engine.on('terminate', onTerminate);
engine.on('networkQuality', onNetworkQuality);
engine.on('userLeave', onUserLeave);
engine.on('join', onJoin);
}
return () => {
if (engine) {
engine.off('calling', onCalling);
engine.off('terminate', onTerminate);
engine.off('networkQuality', onNetworkQuality);
engine.off('userLeave', onUserLeave);
engine.off('join', onJoin);
}
}
}, [engine]);
useEffect(() => {
if (visible) {
// 开启本地预览。
engine && engine.startPreview('preview_video');
} else {
// 关闭本地预览。
engine && engine.stopPreview();
}
}, [visible]);
return (
<Dialog className={styles.dialog} visible={visible} footer={false}>
<div>
<div className={styles.view} style={{ display: channelType === 'video' ? 'flex' : 'none' }}>
<div className={styles.card}>
<div className={styles.title}>{'本地画面'}</div>
<video id="preview_video" playsInline autoPlay />
</div>
<div className={styles.card}>
<div className={styles.title}>{'对方画面'}</div>
<video id="target_video" playsInline autoPlay />
</div>
</div>
<div className={styles.content}>
<div className={styles.message}>
{counter ? `通话时长 ${formatTimer(counter)}` : '呼叫中...'}
<div className={styles.sub}>{`网络质量:${NETWORK_QUALITY[networkStatus]}`}</div>
</div>
<div className={styles.controls}>
<Button text onClick={handleMuteMic}>
{micMuted ? (
<img src="https://img.alicdn.com/imgextra/i4/O1CN01RFU4IX1D8MTnFpE3T_!!6000000000171-55-tps-20-20.svg" />
) : (
<img src="https://img.alicdn.com/imgextra/i4/O1CN01c6D0ZD1wRHYIbG4qd_!!6000000006304-55-tps-20-20.svg" />
)}
</Button>
{channelType === 'video' && (
<Button text onClick={handleMuteCamera}>
{cameraMuted ? (
<img src="https://img.alicdn.com/imgextra/i1/O1CN01xl7k8S1MTH6rK2hwV_!!6000000001435-55-tps-20-20.svg" />
) : (
<img src="https://img.alicdn.com/imgextra/i2/O1CN01gT9gL91jX9cykjy4r_!!6000000004557-55-tps-20-20.svg" />
)}
</Button>
)}
<Button text onClick={handleLeave}>
<img src="https://img.alicdn.com/imgextra/i3/O1CN01MPqmGR1l8zy3W8G8h_!!6000000004775-55-tps-20-20.svg" />
</Button>
</div>
</div>
</div>
</Dialog>
);
};