运行Web Demo

本文通过在Web浏览器中演示通信场景,介绍Web Demo的运行示例。

前提条件

已开通音视频通信服务。具体操作,请参见开通服务

环境准备

本文示例环境为Windows 10系统下Chrome 90版本浏览器。

创建产品和设备

为了演示实际通信场景,需要准备两台设备作为主叫方(Host)和被呼叫方(Guest)。

  1. 阿里云物联网平台控制台创建产品。具体操作,请参见创建产品

  2. 在阿里云物联网平台,创建两台设备作为主叫方和被呼叫方。

    DeviceName示例:web-host、web-guest。具体操作,请参见创建设备

    说明

    请在物联网平台获取以下参数,后续编译需要使用。

    • 设备证书:设备创建成功后,将生成设备证书。设备证书包含ProductKey、DeviceName和DeviceSecret。

    • 设备接入域名:在实例详情页面,单击查看开发配置,获取设备接入域名。

运行Demo

音视频通信服务控制台已提供Web Demo运行窗口,您可直接进行音视频通话调试。

  1. 登录物联网平台控制台,在左侧导航栏选择增值服务

  2. 增值服务页面,单击音视频通信服务

  3. 在音视频通信服务控制台左侧导航栏,选择实例,然后单击服务示例

  4. 在服务示例页面,输入呼叫方和被呼叫方的设备信息,选择通话类型。通话

  5. 单击连接,接入音视频通信服务器,然后单击呼叫,向对端设备发起通话。例如视频通话,界面如下:您可单击对应图标管理通话:通话界面

    • image:开启、关闭显示时间。

    • 麦克风:开启、关闭麦克风。

    • 摄像头:开启、关闭摄像头。

    • 结束通话:结束通话。

    支持使用当前操作系统自带的截图功能进行通话截图。

附录:示例代码

初始化设备证书信息

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>
  );
};
阿里云首页 物联网平台 相关技术圈