封装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
最后编辑:jiangyichen 更新时间:2025-03-25 14:11