React函数式和类组件实现相同功能,函数式组件执行错误?

同一个功能,类组件改为函数式组件后执行错误了,帮忙看看是哪里语法错误了吗?

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

阅读 1.4k
2 个回答

代码太长,我猜测是init捕获的 pc 和回调函数捕获的pc不一致导致的问题,因为按照这种写法,组件每渲染一次,都会生成全新的 pc 。除非是单例模式,不然这些 pc 之间彼此可能是没有关系的,浪费计算和信道资源不说,能不能构成连续的会话都是个问题。
所以第一点要改的,是 pc 要跨渲染周期,也就是确保每一次渲染前后访问到的都是同一个 RTCPeerConnection 实例,这个目标可以使用 useRef 来达成:

const CreateRTC = () => {
  const pc = useRef<RTCPeerConnection|null>(null);
  useEffect(() => {
    pc.current = new RTCPeerConnection();
    // 别的地方也记住用 pc.current 访问 RTCPeerConnection 实例:
    // const peer =  pc.current as RTCPeerConnection;

    return () => {
      // 使用 Effect 最好在返回的函数里负责地将其销毁
      pc.current.close();
    }
  }, [])
}

把const pc = new RTCPeerConnection()放到CeratRtc方法上面就可以了,不知道为什么

推荐问题
logo
Microsoft
子站问答
访问
宣传栏