前言
- 有关WebRTC的一些概念可以参考另外一篇文章 WebRTC概念
- 我这里交换媒体信息、网络信息交换使用的是WebSocket,媒体信息是什么参考 WebRTC概念
- 以下的使用方法中,只有使用WebRTC传输通用数据跟音频流的,视频流要再自己配置一下
- 使用SFU结构,所以并没有用户与用户之间直接的信令交换,这些东西都给后台处理了,什么是SFU架构参考另外一篇文章 WebRTC中的SFU架构
usePeer.tsx
- 使用方法:userPeer导出一个localAudioRef,这个是本地音视频流的dom;还可以导出一个PeerRef,这是WebRTC要用的peer
- 其实localAudioRef好像不放这里面也是可以的,具体的情况实际使用的时候再决定吧
- 代码:
import{ useEffect, useContext, useRef }from"react";import{ AppContext }from"../App";/**
* peer socket 的初始化
* @returns socket
*/constusePeer=()=>{// 这是因为我的peerRef跟socketRef要在两个hook里面用到 所以就放全局了// 在实际使用的时候 可以return然后在调用usePeer的地方拿一下const{ peerRef, socketRef }=useContext(AppContext)!const remoteAudioRef =useRef<HTMLDivElement>(null);// 其他用户的音视频domconst localAudioRef =useRef<HTMLAudioElement>(null)// 自己的音视频domconstcreatePeer=()=>{// peer创建const peer =newRTCPeerConnection();
peer.onicecandidate=(event)=>{// 收到自己的candidate// 使用ws发送candidate 这里的ws自己写就好了}
peer.ontrack=(event)=>{// 收到对方的流轨道// 动态生成是为了一个房间有多个人 这里只用到音频 所以如果要视频的话可以在这里操作一下const audio = document.createElement('audio');
audio.srcObject = event.streams[0];
audio.autoplay =true;
audio.controls =false;
remoteAudioRef.current?.appendChild(audio);
event.track.onmute=()=>{// 静音
audio.play();}
event.streams[0].onremovetrack=()=>{// 对象移除if(audio.parentNode){
audio.parentNode.removeChild(audio);}}}return peer;}constgetLocalStream=async()=>{// 打开视频音频流const stream =await navigator.mediaDevices.getUserMedia({
audio:true,
video:false,// 如果要视频这里可以打开})return stream;}consthandleLocalStream=async()=>{// 获取 处理本地音频流const stream =awaitgetLocalStream();
stream.getTracks().forEach((track)=>{
peerRef.current?.addTrack(track, stream);})}// 我这里只初始化peer一次 具体使用的时候可以结合自己的需求进行peer的创建跟关闭处理useEffect(()=>{handleLocalStream()
peerRef.current =createPeer();// 这个是关闭peer的方法// peerRef.current.close();},[])return{ localAudioRef }}exportdefault usePeer;
useDatachannel.tsx
import{ useEffect, useRef, useContext }from"react";import{ AppContext }from"../App";/**
* 数据通道初始化
* return 想要的话dataChannel也可以传出去的 或者把有关处理都放这里面也行
*/constuseDataChannel=()=>{// 从全局拿到peerRef 如果不放全局的话 可以直接传进来const{ peerRef }=useContext(AppContext)!const dataChannel =useRef<RTCDataChannel>();constcreateDataChannel=()=>{// dataChannel创建// 创建数据通道const channel = peerRef.current!.createDataChannel("myDataChannel66666666_1395212519");
channel.onopen=()=>{console.log("[dataChannel open]");}
channel.onmessage=(event)=>{// 在这里接收通道数据}
channel.onclose=()=>{console.log("[dataChannel close]");}return channel
}useEffect(()=>{// 监听用户是否在房间中
dataChannel.current =createDataChannel();// 这个是关闭通道的方法// dataChannel.current.close();// 这个是发送数据的方法// dataChannel.current.send()},[])}exportdefault useDataChannel
useSocketHandle.tsx
- 因为不想useSocket太多代码了,所以分了一个这样的文件出来,主要是ws收到信息的函数
import{ useRef, useContext, useEffect }from"react";import{ AppContext }from"../../App";constuseHandleOffer=()=>{const{ peerRef, socketRef }=useContext(AppContext)!consthandleOffer=async(offer:any)=>{// 收到offer的处理const peer = peerRef.current
await peer?.setRemoteDescription(offer);// 设置远端描述信息const answer =await peer?.createAnswer();// 生成answerawait peer?.setLocalDescription(answer);// 设置本地描述信息
socketRef.current?.send()// 按照跟后台约定好的格式发送自己的answer}consthandleCandidate=(candidate:any)=>{// 收到candidate的处理
peerRef.current?.addIceCandidate(candidate);// 添加candidate}return{ handleOffer, handleCandidate }}exportdefault useHandleOffer;
useSocket.tsx
import{ useEffect, useContext }from"react";import{ AppContext }from"../../App";import useSocketHandle from"./useSocketHandle";constWS_URL='wss://xxx'// 服务地址constuseSocket=()=>{const{ handleOffer, handleCandidate }=useSocketHandle();// ws处理函数const{ socketRef }=useContext(AppContext)!// 用全局的let heartTimer =0;// 心跳定时器 IDconstheartCheck=(socket: WebSocket)=>{// 心跳检查clearInterval(heartTimer);// 先清除之前的定时器
heartTimer =setInterval(()=>{
socket.send('xxx');// 约定好的心跳},30000);}constcreateSocket=()=>{// socket创建if(socketRef.current)return;const socket =newWebSocket(`${WS_URL}`)// 信令服务器连接
socket.onopen=()=>{// 连接建立console.log("[ws open] 连接已建立");heartCheck(socket);// 心跳处理};
socket.onmessage=async(event)=>{// 接收到服务器的信息const msg =JSON.parse(event.data)// 这个主要看跟后台约定的格式switch(msg.event){case'offer':// 收到offerhandleOffer(JSON.parse(msg.data))break;case'candidate':// 收到candidatehandleCandidate(JSON.parse(msg.data))break;}};
socket.onclose=()=>{// 连接关闭console.log('[ws close] 连接中断');
socketRef.current =undefinedclearInterval(heartTimer);// 清除定时器};
socket.onerror=(error)=>{// 连接错误console.log(`[error] 连接错误 `, error);};return socket;}useEffect(()=>{// 监听房间
socketRef.current =createSocket();// 关闭socket的方法// socketRef.current.close();},[])}exportdefault useSocket
使用方法
- 要注意的是,我这里只是提供了一个大概的框架,具体的一些细节,比如说跟后台交换candidate、offer、answer这种,还是需要自己去填写的
- 另外一个点,如果按照这里搞出来,answer、offer什么的都完成了交换,视频也不一定有的,你要自己加上一些视频的配置,比如说获取音视频流的时候video变为true,以及动态生成元素的时候也把video给生成一下。
import usePeer from'../../hooks/usePeer';import useDataChannel from'../../hooks/useDataChannel';import useSocket from'../../hooks/socket/useSocket';exportdefaultfunctionHome(){const{ localAudioRef }=usePeer()useSocket()useDataChannel()return(<div className='Home'><div className="remoteAudioContainer"></div><audio src="" ref={localAudioRef}></audio></div>)}
本文转载自: https://blog.csdn.net/hello_helloworld/article/details/135207093
版权归原作者 hello_helloworld 所有, 如有侵权,请联系我们删除。
版权归原作者 hello_helloworld 所有, 如有侵权,请联系我们删除。