同一个功能,类组件改为函数式组件后执行错误了,帮忙看看是哪里语法错误了吗?
import React, { useState } from "react"
import { Button,Input, Tooltip } from 'antd'
import { CopyOutlined } from "@ant-design/icons"
import './index.less'
class CeratRtc extends React.Component {
// 创建本地/远程 SDP 描述, 用于描述本地/远程的媒体流
state = {
offerSdp: '',
offerSdp2: '',
answerSdp: '',
answerSdp2: ''
}
// 内网中使用
pc = new RTCPeerConnection()
componentDidMount() {
this.init()
}
async init () {
// 获取本地端视频标签
const localVideo = document.getElementById('local') as HTMLVideoElement
// 获取远程端视频标签
const remoteVideo = document.getElementById('remote') as HTMLVideoElement
// 采集本地媒体流
const localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
})
// 设置本地视频流
localVideo.srcObject = localStream
// 添加本地媒体流的轨道都添加到 RTCPeerConnection 中
localStream.getTracks().forEach((track) => {
this.pc.addTrack(track, localStream)
})
// 监听远程流,方法一:
this.pc.ontrack = (event) => {
remoteVideo.srcObject = event.streams[0]
}
// 方法二:你也可以使用 addStream API,来更加详细的控制流的添加
// const remoteStream: MediaStream = new MediaStream()
// pc.ontrack = (event) => {
// event.streams[0].getTracks().forEach((track) => {
// remoteStream.addTrack(track)
// })
// // 设置远程视频流
// remoteVideo.srcObject = remoteStream
// }
}
/**
*建立连接的主要过程,就是通过 RTCPeerConnection 对象的 createOffer 方法来创建本地的 SDP 描述
然后通过 RTCPeerConnection 对象的 setLocalDescription 方法来设置本地的 SDP 描述
最后通过 RTCPeerConnection 对象的 setRemoteDescription 方法来设置远程的 SDP 描述
*
* @memberof CeratRtc
*/
createOffer = async () => {
// 创建offer
const offer = await this.pc.createOffer()
// 设置本地描述
await this.pc.setLocalDescription(offer)
// 到这里,我们本地的 offer 就创建好了,一般在这里通过信令服务器发送 offerSdp 给远端
// 监听 RTCPeerConnection 的 onicecandidate 事件,当 ICE 服务器返回一个新的候选地址时,就会触发该事件
this.pc.onicecandidate = async (event) => {
if (event.candidate) {
this.setState({
offerSdp: JSON.stringify(this.pc.localDescription)
})
}
}
}
/**
*作为接收方,在拿到 offer 后
就可以创建 answer 并设置到本地描述中
然后通过信令服务器发送 answer 给对端。
*
* @memberof CeratRtc
*/
createAnswer = async () => {
// 解析字符串
const offer = JSON.parse(this.state.offerSdp2)
this.pc.onicecandidate = async (evet) => {
if (evet.candidate) {
this.setState({
answerSdp: JSON.stringify(this.pc.localDescription)
})
}
}
await this.pc.setRemoteDescription(offer)
const answer = await this.pc.createAnswer()
await this.pc.setLocalDescription(answer)
}
/**
*添加answer的应答
接收方拿到 answer 后,就可以设置到远程端的描述中。
*
* @memberof CeratRtc
*/
addAnswer = async () => {
const answer = JSON.parse(this.state.answerSdp2)
if (!this.pc.currentRemoteDescription) {
this.pc.setRemoteDescription(answer)
}
}
copyToClipboard = async (str: string) => {
navigator.clipboard.writeText(str)
}
handleChange2 = (e: { target: { value: string } }) => {
this.setState({
offerSdp2: e.target.value
})
}
handleChange3 = (e: { target: { value: string } }) => {
this.setState({
answerSdp2: e.target.value
})
}
render(): React.ReactNode {
return (
<div className="page-container">
<div className="video-container">
<div className="video-box">
<video id="local" autoPlay playsInline muted></video>
<div className="video-title">我</div>
</div>
<div className="video-box">
<video id="remote" autoPlay playsInline></video>
<div className="video-title">远程视频</div>
</div>
</div>
<div className="operation">
{/* step1 */}
<div className="step">
<div className="user">
用户1的操作区域
</div>
<p className="desc">
点击 Create Offer,生成 SDP offer,把下面生成的offer 复制给用户 2
<Button id="create-offer" type="primary" onClick={this.createOffer}>
创建 Offer
</Button>
</p>
<p>SDP offer:</p>
<Input.Group compact >
<Input
style={{ width: 'calc(100% - 200px)' }}
defaultValue={this.state.offerSdp}
value={this.state.offerSdp}
/>
<Tooltip title="copy address">
<Button id="create-offer" type="primary" onClick={this.copyToClipboard.bind(this, this.state.offerSdp)} icon={<CopyOutlined />} />
</Tooltip>
</Input.Group>
</div>
{/* step2 */}
<div className="step">
<div className="user">
用户2的操作区域
</div>
<p className="desc">
用户 2将用户1 刚才生成的SDP offer 粘贴到下方,点击 "创建答案
"来生成SDP答案,然后将 SDP Answer 复制给用户 1。
</p>
<Input.Group compact>
<Input style={{ width: 'calc(100% - 200px)' }} defaultValue={this.state.offerSdp2} onChange={this.handleChange2} />
<Button type="primary" onClick={this.createAnswer}>创建Answer</Button>
</Input.Group>
<p>SDP Answer:</p>
<Input.Group compact >
<Input
style={{ width: 'calc(100% - 200px)' }}
defaultValue={this.state.answerSdp}
value={this.state.answerSdp}
/>
<Tooltip title="copy address">
<Button id="create-offer" type="primary" onClick={this.copyToClipboard.bind(this, this.state.answerSdp)} icon={<CopyOutlined />} />
</Tooltip>
</Input.Group>
</div>
{/* <!-- step3 --> */}
<div className="step">
<div className="user">用户 1 的操作区域</div>
<p>将用户 2 创建的 Answer 粘贴到下方,然后点击 Add Answer。</p>
<p>SDP Answer:</p>
<Input.Group compact>
<Input style={{ width: 'calc(100% - 200px)' }} defaultValue={this.state.answerSdp2} onChange={this.handleChange3} />
<Button type="primary" onClick={this.addAnswer}>添加Answer</Button>
</Input.Group>
</div>
</div>
</div>
)
}
}
export default CeratRtc
import React, { useEffect, useState } from "react"
import { Button,Input, Tooltip } from 'antd'
import { CopyOutlined } from "@ant-design/icons"
import './index.less'
const CeratRtc = () => {
useEffect(() => {
init()
}, [])
// 创建本地/远程 SDP 描述, 用于描述本地/远程的媒体流
const [offerSdp, setOffer] = useState('')
const [offerSdp2, setOfferSdp2] = useState('')
const [answerSdp, setAnswerSdp] = useState('')
const [answerSdp2, setAnswerSdp2] = useState('')
// 内网中使用
const pc = new RTCPeerConnection()
const init = async () => {
// 获取本地端视频标签
const localVideo = document.getElementById('local') as HTMLVideoElement
// 获取远程端视频标签
const remoteVideo = document.getElementById('remote') as HTMLVideoElement
// 采集本地媒体流
const localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
})
// 设置本地视频流
localVideo.srcObject = localStream
// 添加本地媒体流的轨道都添加到 RTCPeerConnection 中
localStream.getTracks().forEach((track) => {
pc.addTrack(track, localStream)
})
// 监听远程流,方法一:
pc.ontrack = (event) => {
remoteVideo.srcObject = event.streams[0]
}
// 方法二:你也可以使用 addStream API,来更加详细的控制流的添加
// const remoteStream: MediaStream = new MediaStream()
// pc.ontrack = (event) => {
// event.streams[0].getTracks().forEach((track) => {
// remoteStream.addTrack(track)
// })
// // 设置远程视频流
// remoteVideo.srcObject = remoteStream
// }
}
/**
*建立连接的主要过程,就是通过 RTCPeerConnection 对象的 createOffer 方法来创建本地的 SDP 描述
然后通过 RTCPeerConnection 对象的 setLocalDescription 方法来设置本地的 SDP 描述
最后通过 RTCPeerConnection 对象的 setRemoteDescription 方法来设置远程的 SDP 描述
*
* @memberof CeratRtc
*/
const createOffer = async () => {
// 创建offer
const offer = await pc.createOffer()
// 设置本地描述
await pc.setLocalDescription(offer)
// 到这里,我们本地的 offer 就创建好了,一般在这里通过信令服务器发送 offerSdp 给远端
// 监听 RTCPeerConnection 的 onicecandidate 事件,当 ICE 服务器返回一个新的候选地址时,就会触发该事件
pc.onicecandidate = async (event) => {
if (event.candidate) {
setOffer(JSON.stringify(pc.localDescription))
}
}
}
/**
*作为接收方,在拿到 offer 后
就可以创建 answer 并设置到本地描述中
然后通过信令服务器发送 answer 给对端。
*
* @memberof CeratRtc
*/
const createAnswer = async () => {
// 解析字符串
const offer = JSON.parse(offerSdp2)
pc.onicecandidate = async (evet) => {
if (evet.candidate) {
setAnswerSdp(JSON.stringify(pc.localDescription))
}
}
await pc.setRemoteDescription(offer)
const answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
}
/**
*添加answer的应答
接收方拿到 answer 后,就可以设置到远程端的描述中。
*
* @memberof CeratRtc
*/
const addAnswer = async () => {
const answer = JSON.parse(answerSdp2)
if (!pc.currentRemoteDescription) {
pc.setRemoteDescription(answer)
}
}
const copyToClipboard = async (str: string) => {
navigator.clipboard.writeText(str)
}
const handleChange2 = (e: { target: { value: string } }) => {
setOfferSdp2(e.target.value)
}
const handleChange3 = (e: { target: { value: string } }) => {
setAnswerSdp2(e.target.value)
}
return (
<div className="page-container">
<div className="video-container">
<div className="video-box">
<video id="local" autoPlay playsInline muted></video>
<div className="video-title">我</div>
</div>
<div className="video-box">
<video id="remote" autoPlay playsInline></video>
<div className="video-title">远程视频</div>
</div>
</div>
<div className="operation">
{/* step1 */}
<div className="step">
<div className="user">
用户1的操作区域
</div>
<p className="desc">
点击 Create Offer,生成 SDP offer,把下面生成的offer 复制给用户 2
<Button id="create-offer" type="primary" onClick={createOffer}>
创建 Offer
</Button>
</p>
<p>SDP offer:</p>
<Input.Group compact >
<Input
style={{ width: 'calc(100% - 200px)' }}
defaultValue={offerSdp}
value={offerSdp}
/>
<Tooltip title="copy address">
<Button id="create-offer" type="primary" onClick={copyToClipboard.bind(this, offerSdp)} icon={<CopyOutlined />} />
</Tooltip>
</Input.Group>
</div>
{/* step2 */}
<div className="step">
<div className="user">
用户2的操作区域
</div>
<p className="desc">
用户 2将用户1 刚才生成的SDP offer 粘贴到下方,点击 "创建答案
"来生成SDP答案,然后将 SDP Answer 复制给用户 1。
</p>
<Input.Group compact>
<Input style={{ width: 'calc(100% - 200px)' }} defaultValue={offerSdp2} onChange={handleChange2} />
<Button type="primary" onClick={createAnswer}>创建Answer</Button>
</Input.Group>
<p>SDP Answer:</p>
<Input.Group compact >
<Input
style={{ width: 'calc(100% - 200px)' }}
defaultValue={answerSdp}
value={answerSdp}
/>
<Tooltip title="copy address">
<Button id="create-offer" type="primary" onClick={copyToClipboard.bind(this, answerSdp)} icon={<CopyOutlined />} />
</Tooltip>
</Input.Group>
</div>
{/* <!-- step3 --> */}
<div className="step">
<div className="user">用户 1 的操作区域</div>
<p>将用户 2 创建的 Answer 粘贴到下方,然后点击 Add Answer。</p>
<p>SDP Answer:</p>
<Input.Group compact>
<Input style={{ width: 'calc(100% - 200px)' }} defaultValue={answerSdp2} onChange={handleChange3} />
<Button type="primary" onClick={addAnswer}>添加Answer</Button>
</Input.Group>
</div>
</div>
</div>
)
}
export default CeratRtc
函数式组件点击addAnswer后报错:
// Uncaught (in promise) DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: stable
代码太长,我猜测是
init
捕获的pc
和回调函数捕获的pc
不一致导致的问题,因为按照这种写法,组件每渲染一次,都会生成全新的pc
。除非是单例模式,不然这些pc
之间彼此可能是没有关系的,浪费计算和信道资源不说,能不能构成连续的会话都是个问题。所以第一点要改的,是
pc
要跨渲染周期,也就是确保每一次渲染前后访问到的都是同一个RTCPeerConnection
实例,这个目标可以使用useRef
来达成: