landing 页面核心代码 meeting-simple/src/landing/index.js
import { LoadingOutlined, WarningOutlined } from "@ant-design/icons"; import React, { useEffect, useState } from "react"; import * as utils from "../utils"; // import * as api from './meeting/api' export default function Landing(props) { // 检测 RTC 支持 return !utils.isSupportRTC() ? ( <NotSupport /> ) : ( <NotReady setReady={props.setReady} /> ); } // 不支持时的显示 function NotSupport() { // ... } // 支持 RTC 时的显示 function NotReady(props) { const [permissionState, setPermissionState] = useState("prompt"); const [timeCount, setTimeCount] = useState(0); const [loadingState, setLoadingState] = useState("init"); const retry = () => { setTimeCount(timeCount + 1); }; // 不同状态时的提示信息,prompt、granted、denied const permissionStr = { prompt: ( <p> Please allow camera and microphone access to continue, you can turn off camera or microphone later in meeting </p> ), denied: ( <p> You should granted camera microphone permissions,{" "} <a onClick={retry}>click to retry</a> </p> ), granted: <p>Loading meeting info...</p>, }; useEffect(() => { (async () => { // 检测权限 const status = await utils.checkMediaPermission(); // 设置授权信息 setPermissionState(status ? "granted" : "denied"); if (!status) return; try { // 从浏览器参数拿到会话信息 const sessID = location.hash.slice(1); // if (sessID) { // await api.getSessionInfo(sessID) // } props.setReady("landing"); } catch (error) { console.warn("failed to get session info", error); setLoadingState("Failed to get meeting info: " + JSON.stringify(error)); } })(); }, [timeCount]); const tip = permissionStr[permissionState] || (loadingState === "init" ? "loading..." : loadingState); return <div className="landing-mask"><!--loading 信息--></div>; }增加 Video-window 页, 用于支持视频画面显示
Video-window 核心代码 meeting-simple/src/meeting/video-window/index.js
import React, { useRef, useEffect } from "react"; import * as utils from "../../utils"; export default function VideoWindow(props) { const videoRef = useRef(null); useEffect(() => { const updateStream = (stream) => { // video 对象对应的dom const dom = videoRef.current; if (!dom) return; // 自己则 mute 静音 dom.muted = !props.peer; if ("srcObject" in dom) { dom.srcObject = stream; dom.onloadedmetadata = function () { dom.play(); }; return; } // 设置实时视频的 stream 地址 dom.src = URL.createObjectURL(stream); dom.play(); }; if (props.peer) { props.peer.on("stream", updateStream); return; } // 获得 mediaStream utils.getMediaStream().then(updateStream); return () => { if (!props.peer) return; props.peer.off("stream", updateStream); }; }, [props.peer]); return ( <video ref={videoRef} controls={!!props.peer} ></video> ); }工具方法的核心实现meeting-simple/src/utils.js,检测是否支持 WebRTC、
/** 检查是否支持 WebRTC */ export function isSupportRTC() { return !!navigator.mediaDevices; } // 检测是否有media权限 export async function checkMediaPermission() { // 请求获得媒体流输入(包含声音和视频) const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true, }); // 判断是否有视频和声音轨道输入 const result = stream.getAudioTracks().length && stream.getVideoTracks().length; // 终止媒体流输入 revokeMediaStream(stream); return result; } // 终止媒体流 export function revokeMediaStream(stream) { if (!stream) return; const tracks = stream.getTracks(); tracks.forEach(function (track) { track.stop(); }); } let cachedMediaStream = null; export async function getMediaStream() { if (cachedMediaStream) { return Promise.resolve(cachedMediaStream); } // 请求媒体流输入 const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true, }); revokeMediaStream(cachedMediaStream); cachedMediaStream = stream; return cachedMediaStream; } 代码提交记录本步骤对应的 git commit
第 2 步 支持创建会议 注意要点:浏览器端调用云开发能力需要借助官方 npm 包 tcb-js-sdk, 官方文档
因为视频会议应用无需注册, 即需要匿名使用云开发能力, 调用能力前, 需要在云开发 登录授权 中开启 「匿名登录」
使用云开发能力(不论是在浏览器端、Node 端或其他端)调用数据库时, 操作端 collection 必须存在, 否则会报错. 所以在本步骤应当提前进入云开发数据库控制台 创建视频会议使用的 collection meeting-simple
使用 JS sdk 调用云开发能力时, 需保证调用的域名已加入云开发WEB 安全域名中, 以避免调用时出现跨域问题. 即本地开发使用的域名应增加进 WEB 安全域名 中.
操作步骤增加 「创建会议」界面
增加云开发能力调用模块 「api.js」, 添加 创建会议方法(通过云开发 js sdk 连接数据库创建记录)