封装WebRTC通信组件:
自定义组件名,假设叫做:WebRTCTalk

import React, { useState, useEffect, useRef } from 'react';
import { _getTalkUrl } from '@/service';
import { AudioOutlined } from '@ant-design/icons';
import { Spin } from 'antd';

//检查IP是否为本地IP的功能(用于ICE候选过滤)
function isLocalIP(ip) {
  if (
    /^10\./.test(ip) ||
    /^192\.168\./.test(ip) ||
    /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ip) ||
    /^127\./.test(ip) ||
    /^169\.254\./.test(ip)
  ) {
    return true;
  }
  if (ip.length > 15) {
    return true;
  }
  return false;
}

const WebRTCComponent = (props) => {
  const { serialNumber } = props;
  const [talkUrlInput, setTalkUrlInput] = useState(''); //音频通话url
  const [localSDP, setLocalSDP] = useState(null);
  const [candidateSDP, setCandidateSDP] = useState('');
  const [hasLocalIP, setHasLocalIP] = useState(false);
  const [iceCandidateCount, setIceCandidateCount] = useState(0);
  const [iceCandidateCountOK, setIceCandidateCountOK] = useState(0);
  const [startTalkButtonDisabled, setStartTalkButtonDisabled] = useState(false); //控制开始按钮是否可用
  const [loading, setLoading] = useState(false);

  const remoteAudioRef = useRef(null); // For audio element
  const pcTalk = useRef(null);
  const pc = useRef(
    new RTCPeerConnection({
      iceServers: [{ urls: ['stun:stun.voipbuster.com:3478'] }],
    }),
  ).current;
  // 初始化WebRTC连接
  useEffect(() => {
    if (talkUrlInput && talkUrlInput !== '') {
      initWebRtc();
    }
  }, [talkUrlInput]);

  // 处理ICE候选和SDP更新
  useEffect(() => {
    if (iceCandidateCountOK === 0) return;
    let tmpLocalSDP = localSDP;
    if (iceCandidateCountOK === 1) {
      if (localSDP) {
        let searchStringAudio = 'm=audio';
        let indexAudio = localSDP.indexOf(searchStringAudio);
        let searchStringVideo = 'm=video';
        let indexVideo = localSDP.indexOf(searchStringVideo);
        let indexPos = 0;
        if (indexVideo > indexAudio) {
          indexPos = indexVideo;
        } else {
          indexPos = indexAudio;
        }
        tmpLocalSDP =
          localSDP.substring(0, indexPos) +
          candidateSDP +
          localSDP.substring(indexPos);
        setLocalSDP(tmpLocalSDP);
      }
    }
    if (iceCandidateCountOK >= 2) {
      let searchString = 'a=ice-options:trickle';
      if (localSDP) {
        setLocalSDP(
          (prevLocalSDP) =>
            prevLocalSDP.replace(searchString, '') + candidateSDP,
        );
        tmpLocalSDP = localSDP.replace(searchString, '') + candidateSDP;
      }
      doCall(tmpLocalSDP, talkUrlInput);
      setCandidateSDP('');
      setIceCandidateCountOK(0);
      setIceCandidateCount(0);
    }
  }, [iceCandidateCountOK]);

  //初始化WebRTC连接的回调函数
  const initWebRtc = () => {
    // pc.onconnectionstatechange = (event) =>
    //   console.log('Connection state:', event.currentTarget.connectionState);

    pcTalk.current.onicecandidate = (event) =>
      handleCandidate(event, talkUrlInput);
    pcTalk.current.ontrack = (event) =>
      handleTrack(event, remoteAudioRef.current);
    // pcTalk.current.onconnectionstatechange = (event) =>
    //   console.log(
    //     'Talk connection state:',
    //     event.currentTarget.connectionState,
    //   );
  };

  // 处理ICE候选
  const handleCandidate = (event, url) => {
    if (event.candidate) {
      setIceCandidateCount(iceCandidateCount + 1);
      const candidateIP = event.candidate.candidate.split(' ')[4];
      setHasLocalIP(isLocalIP(candidateIP));

      if (!hasLocalIP) {
        setCandidateSDP(
          (prevCandidateSDP) =>
            prevCandidateSDP + 'a=' + event.candidate.candidate + '\r\n',
        );
        setIceCandidateCountOK(
          (iceCandidateCountOK) => iceCandidateCountOK + 1,
        );
      }
    }
  };
  // 组件卸载时禁用通话
  useEffect(() => {
    return () => {
      setTalkDisable();
    };
  }, []);
  //处理媒体轨道
  const handleTrack = (event, videoElement) => {
    const stream = event.streams[0];
    if (stream) {
      videoElement.srcObject = stream;
    }
  };
  // 执行呼叫
  const doCall = (param, url) => {
    if (url === talkUrlInput) {
      negotiateTalkSDP(param, url);
    } else {
      negotiateSDP(param, url);
    }
  };

  // 协商SDP
  const negotiateSDP = (param, url) => {
    const json = { action: 'offer', sdp: param };
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 304)) {
        const jsonResponse = JSON.parse(xhr.responseText);
        const answer = { sdp: jsonResponse.sdp, type: 'answer' };
        pc.setRemoteDescription(answer)
          .then(() => {
            // console.log('SDP negotiation complete');
          })
          .catch(console.error);
      }
    };
    xhr.open('POST', url, true);
    xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
    xhr.send(JSON.stringify(json));
  };

  // 协商通话SDP
  const negotiateTalkSDP = (param, url) => {
    const json = { action: 'offer', sdp: param };
    let xhrTalk = new XMLHttpRequest();
    xhrTalk.onreadystatechange = () => {
      if (
        xhrTalk.readyState === 4 &&
        (xhrTalk.status === 200 || xhrTalk.status === 304)
      ) {
        const jsonResponse = JSON.parse(xhrTalk.responseText);
        const answer = { sdp: jsonResponse.sdp, type: 'answer' };
        pcTalk.current
          .setRemoteDescription(answer)
          .then(() => {
            console.log('Talk SDP negotiation complete');
          })
          .catch(console.error);
      }
    };
    xhrTalk.open('POST', url, true);
    xhrTalk.setRequestHeader('Content-type', 'application/json; charset=utf-8');
    xhrTalk.send(JSON.stringify(json));
  };

  // 启用通话功能
  const setTalkEnable = () => {
    // 重新初始化 pcTalk 连接
    pcTalk.current = new RTCPeerConnection({
      iceServers: [{ urls: ['stun:stun.voipbuster.com:3478'] }],
    });
    navigator.mediaDevices
      .getUserMedia({ audio: true })
      .then((stream) => {
        pcTalk.current.addTransceiver('video', { direction: 'sendrecv' }); //sendrecv
        pcTalk.current.addTransceiver(stream.getTracks()[0], {
          direction: 'sendrecv',
        });
        setTalkLocalOffer();
      })
      .catch(console.error);
  };

  // 设置本地通话提议
  const setTalkLocalOffer = () => {
    pcTalk.current
      .createOffer()
      .then((desc) => {
        pcTalk.current.setLocalDescription(desc).then(() => {
          setLocalSDP(desc.sdp);
        });
      })
      .catch(console.error);
    setStartTalkButtonDisabled(true);
  };

  // 禁用通话功能
  const setTalkDisable = () => {
    if (!pcTalk.current) return;
    // 停止音频流
    pcTalk?.current?.getSenders().forEach((sender) => {
      if (sender.track) {
        // 检查 sender.track 是否存在
        sender.track.stop(); // 停止音频轨道
      }
    });

    // 关闭 pcTalk 连接
    pcTalk.current.close();

    // 禁用“开始喊话”按钮
    setStartTalkButtonDisabled(false);
  };

  return (
    <>
      <div style={{ color: '#fff' }}>
        {!startTalkButtonDisabled ? (
          <div
            style={{ cursor: 'pointer' }}
            onClick={() => {
              setLoading(true);
              _getTalkUrl({
                serialNumber,//getDeviceInfo 中返回
                mediaType: 'webrtc',
              }).then((res) => {
                setTalkUrlInput(res.data);
                setTalkEnable();
                setLoading(false);
              });
            }}
          >
            {loading ? <Spin size="small" /> : <AudioOutlined />}
            <span
              style={
                loading
                  ? { color: 'rgba(255, 255, 255, 0.5)' }
                  : { color: 'fff' }
              }
            >
              开始喊话
            </span>
          </div>
        ) : (
          <div
            style={{ cursor: 'pointer' }}
            onClick={() => {
              setTalkDisable();
            }}
          >
            结束喊话
          </div>
        )}
      </div>

      <div style={{ display: 'none' }}>
        <audio ref={remoteAudioRef} autoPlay></audio>
      </div>
    </>
  );
};

export default WebRTCComponent;


获取对讲url

接口地址:/openapi/video/getJFTalkBackUrl

请求方式:GET

请求数据类型:application/x-www-form-urlencoded

响应数据类型:*/*

接口描述:

请求参数:

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
serialNumber serialNumber query false string
deviceId deviceId query false integer(int64)
mediaType mediaType query false string

响应状态:

状态码 说明 schema
200 OK R«string»
401 Unauthorized
403 Forbidden
404 Not Found

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data string
message string
successful boolean

响应示例:

{
    "code": 0,
    "data": "",
    "message": "",
    "successful": true
}

获取对讲url请求示例:

http://hostname:port/openapi/video/getJFTalkBackUrl?serialNumber=123456789&mediaType=webrtc

获取设备信息:

http://hostname:port/openapi/ai/getDeviceInfo?deviceId=123456789

调用WebRTC通信组件:

import WebRTCComponent from '@/components/WebRTCTalk';
<div>
    {
        // 自定义函数,判断设备平台是否以 'JF_' 开头
        //getDeviceInfo中返回的deviceType
        startsWithExcludedString(deviceType) ? (
            //以 'JF_' 开头,是新设备,渲染封装的组件
            <WebRTCComponent serialNumber={'设备号传过去'}/>
        ) : (
            <div>普通设备的喊话代码</div>
        )
    }
</div>
作者:jiangyichen  创建时间:2025-03-24 10:48
最后编辑:jiangyichen  更新时间:2025-03-25 14:11