当前位置:   article > 正文

WebRTC入门指南 —— 实现一个完整的点对点视频通话(信令服务器+客户端)_webrtc客户端

webrtc客户端

WebRTC 架构

通常来说,WebRTC的架构如下图所示:

我们可以看到,一个简单的点对点通讯系统主要由四部分组成:

  • WebRTC客户端:负责生产/消费音视频数据,位于NAT之内,属于内网

  • NAT:Network Address Translation (NAT),网络地址转换协议, 能够将设备的内网地址映射到一个公网的地址。

  • 信令服务器:用于传输SDP、candidate等信令数据。

  • STUN/TURN服务器(中继服务器):

    • STUN:用于为位于NAT内的设备找到自己的公网地址。WebRTC客户端通过给处于公网的STUN服务器发送请求来获得自己的公网地址信息,以及是否能够被(穿过路由器)访问。
    • TURN:对于无法通过STUN服务器进行内网穿越的“对称型NAT”,我们可以借助TURN服务器作为中继服务器,通过TURN服务器对数据进行转发。

点对点的通信原理:

  1. 首先客户端需要信令服务器连接,后续双方需要通过信令服务器来了解对方的一些必要的信息,比如告诉对方自己的支持的音视频格式、自己外网IP地址和端口是多少等(此时还无法知道自己的公网地址)。

  2. 与STUN建立连接,获得自己的外网IP地址和端口,以及是否能够进行内网穿越。不支持内网穿越的情况下还需要连接TURN服务器进行中继通信。

  3. WebRTC客户端拿到自己的外网IP地址和端口后,通过信令服务器将自己的信息(candidate信息)交换给对方。当双方都获取到对方的地址后,它们就可以尝试NAT穿越,进行P2P连接了。

WebRTC实现点对点通信

想要实现点对点通信通信,我们需要经历以下的几个步骤:

  1. 检测本地音视频设备和进行采集音视频的采集;
  2. 通过信令服务器与对方建立连接;
  3. 创建RTCPeerConnection对象
    • 绑定音视频数据
    • 进行媒体协商
    • 交换candidate信息
  4. 音视频数据传输与渲染

接下来我们对各个步骤进行逐步介绍。

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

前置知识

在介绍实现点对点通信的步骤之前,我们先来了解一些前置的概念知识。

MediaStreamTrack

MediaStreamTrack是WebRTC中的基本媒体单位,一个MediaStreamTrack包含一种媒体源(媒体设备或录制内容)返回的单一类型的媒体(如音频,视频)。单个轨道可包含多个通道,如立体声源尽管由多个音频轨道构成,但也可以看作是一个轨道。

MediaStream

MediaStream是MediaStreamTrack的合集,可以包含 >=0 个 MediaStreamTrack。MediaStream能够确保它所包含的所有轨道都是是同时播放的,以及轨道的单一性。

source 与 sink

再MediaTrack的源码中,MediaTrack都是由对应的source和sink组成的。

  1. //src\pc\video_track.cc
  2. void VideoTrack::AddOrUpdateSink(rtc::VideoSinkInterface<VideoFrame>* sink, const rtc::VideoSinkWants& wants) {
  3. RTC_DCHECK(worker_thread_->IsCurrent());
  4. VideoSourceBase::AddOrUpdateSink(sink, wants);
  5. rtc::VideoSinkWants modified_wants = wants;
  6. modified_wants.black_frames = !enabled();
  7. video_source_->AddOrUpdateSink(sink, modified_wants);
  8. }
  9. void VideoTrack::RemoveSink(rtc::VideoSinkInterface<VideoFrame>* sink) {
  10. RTC_DCHECK(worker_thread_->IsCurrent());
  11. VideoSourceBase::RemoveSink(sink);
  12. video_source_->RemoveSink(sink);
  13. }

浏览器中存在从source到sink的媒体管道,其中source负责生产媒体资源,包括多媒体文件,web资源等静态资源以及麦克风采集的音频,摄像头采集的视频等动态资源。而sink则负责消费source生产媒体资源,也就是通过,,等媒体标签进行展示,或者是通过RTCPeerConnection将source通过网络传递到远端。RTCPeerConnection可同时扮演source与sink的角色,作为sink,可以将获取的source降低码率,缩放,调整帧率等,然后传递到远端,作为source,将获取的远端码流传递到本地渲染。

MediaTrackConstraints

MediaTrackConstraints描述MediaTrack的功能以及每个功能可以采用的一个或多个值,从而达到选择和控制源的目的。 MediaTrackConstraints 可作为参数传递给applyConstraints()以达到控制轨道属性的目的,同时可以通过调getConstraints()用来查看最近应用自定义约束。

  1. const constraints = {
  2. width: {min: 640, ideal: 1280},
  3. height: {min: 480, ideal: 720},
  4. advanced: [
  5. {width: 1920, height: 1280},
  6. {aspectRatio: 1.333}
  7. ]
  8. };
  9. //{ video: true }也是一个MediaTrackConstraints对象,用于指定请求的媒体类型和相对应的参数。
  10. navigator.mediaDevices.getUserMedia({ video: true })
  11. .then(mediaStream => {
  12. const track = mediaStream.getVideoTracks()[0];
  13. track.applyConstraints(constraints)
  14. .then(() => {
  15. // Do something with the track such as using the Image Capture API.
  16. })
  17. .catch(e => {
  18. // The constraints could not be satisfied by the available devices.
  19. });
  20. });
  1. //移动设备上面,优先使用前置摄像头
  2. { audio: true, video: { facingMode: "user" } }
  1. //移动设备上面,强制使用后置摄像头
  2. { audio: true, video: { facingMode: { exact: "environment" } } }

如何播放MediaStream

可将MediaStream对象直接赋值给HTMLMediaElement接口的 srcObject属性。

video.srcObject = stream;

检测本地音视频设备和进行采集音视频的采集;

检测本地音视频设备

通过MediaDevices.enumerateDevices()我们可以得到一个本机可用的媒体输入和输出设备的列表,例如麦克风,摄像机,耳机设备等。

  1. //获取媒体设备
  2. navigator.mediaDevices.enumerateDevices().then(res => {
  3. console.log(res);
  4. });

列表中的每个媒体输入都可作为MediaTrackConstraints中对应类型的值,如一个音频设备输入audioDeviceInput可设置为MediaTrackConstraints中audio属性的值

  1. cosnt constraints = { audio : audioDeviceInput }
  2. 复制代码

将该constraint值作为参数传入到MediaDevices.getUserMedia(constraints)中,便可获得该设备的MediaStream。

采集本地音视频数据

可通过调用MediaDevices.getUserMedia()来访问本地媒体,调用该方法后浏览器会提示用户给予使用媒体输入的许可,媒体输入会产生一个MediaStream,里面包含了请求的媒体类型的轨道。此流可以包含一个视频轨道(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、一个音频轨道(同样来自硬件或虚拟音频源,比如麦克风、A/D转换器等等),也可能是其它轨道类型。

  1. navigator.mediaDevices.getUserMedia(constraints)
  2. .then(function(stream) {
  3. /* 使用这个stream*/
  4. video.srcObject = stream;
  5. })
  6. .catch(function(err) {
  7. /* 处理error */
  8. });

通过信令服务器与对方建立连接

信令服务器主要用于帮我们进行业务逻辑的处理(如加入房间、离开房间)以及进行媒体协商和交换candidate。

信令服务器可以有很多种方案,在这里我们借助node.js和socket.io实现一个简单的信令服务器。

创建HTTP服务器

  1. let http = require('http'); // 提供HTTP 服务
  2. let express = require('express');
  3. let app = express();
  4. let http_server = http.createServer(app);
  5. http_server.listen(8081, '127.0.0.1');

引入 socket.io 实现两端的实时通信

  1. let http = require('http'); // 提供HTTP 服务
  2. const { Server } = require('socket.io');
  3. let express = require('express');
  4. let app = express();
  5. //HTTP 服务
  6. let http_server = http.createServer(app);
  7. http_server.listen(8081, '127.0.0.1');
  8. const io = new Server(http_server, {
  9. // 处理跨域配置
  10. cors: {
  11. origin: ['http://127.0.0.1:3000', 'http://localhost:3000'],
  12. credentials: true,
  13. },
  14. });

监听客户端的消息

socket.on('messageName', messageHandler)

客户端加入房间

socket.join(roomId);

向房间内的客户端发送消息

socket.to(roomId).emit('messageName', data);

转发消息

  1. // 用于转发sdp、candidate等消息
  2. socket.on('message', ({ roomId, data }) => {
  3. socket.to(roomId).emit('message', data);
  4. });

创建RTCPeerConnection对象

RTCPeerConnection 接口代表一个由本地计算机到远端的WebRTC连接。该接口提供了创建,保持,监控,关闭连接的方法的实现。

const pc = new RTCPeerConnection()

绑定音视频数据

我们可以通过 addTrack 方法和 addStream 方法(已过时,不推荐)将音视频数据和 RTCPeerConnection 对象进行绑定。

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

  1. mediaStream.getTracks().forEach(track => {
  2. peerConnection.addTrack(track, mediaStream);
  3. });

进行媒体协商 

所谓的媒体协商,就是交换双方SDP信息,SDP包含音视频的编解码(coder),源地址,和时间信息等信息。

呼叫端获取本地sdp(offer),调用pc.setLocalDescription(offer)保存本地的sdp信息后,通过信令服务器发送本地sdp到远端。

  1. // 呼叫端获取本地sdp(offer)
  2. pc.createOffer().then(offer => {
  3. console.log('------ 获取到了本地offer', offer);
  4. // 绑定本地sdp信息
  5. pc.setLocalDescription(offer);
  6. // 通过信令服务器发送本地sdp到远端
  7. signalServer.send({
  8. type: 'offer',
  9. value: offer,
  10. });
  11. });

被叫端接收到来自远端的offer后,调用 pc.setRemoteDescription(offer) 绑定远端sdp,然后调用pc.createAnswer() 创建本地sdp并使用 pc.setLocalDescription(answer) 进行保存,最后利用信令服务器将 answer sdp 发送给远端。

  1. const onGetRemoteOffer = offer => {
  2. console.log('------ 获取到了远端offer', offer);
  3. // 远端发起呼叫,开始建立连接
  4. // 绑定远端sdp
  5. pc.setRemoteDescription(offer);
  6. // 创建本地sdp
  7. pc.createAnswer().then(answer => {
  8. // 绑定本地sdp
  9. pc.setLocalDescription(answer);
  10. console.log('------ 获取到了本地answer', answer);
  11. // 发送本地sdp到远端
  12. signalServer.send({
  13. type: 'answer',
  14. value: answer,
  15. });
  16. });
  17. };

呼叫端接收到远端的answer后,调用 pc.setRemoteDescription(answer) 绑定远端sdp。

  1. const onGetRemoteAnswer = answer => {
  2. console.log('------ 获取到了远端answer', answer);
  3. // 绑定远端sdp
  4. pc.setRemoteDescription(answer);
  5. };

ICE

当媒体协商完成后,WebRTC就开始建立网络连接了。建立网络连接的前提是客户端需要知道对端的外网地址,这个获取并交换外网地址的过程,我们称为ICE。

收集

WebRTC内部集成了收集Candidate的功能。收集到Candidate后,为了通知上层,WebRTC还提供onicecandidate事件。

  1. // 监听 candidate 获取事件
  2. pc.addEventListener('icecandidate', event => {
  3. const candidate = event.candidate;
  4. if (candidate) {
  5. console.log('------ 获取到了本地 candidate:', candidate)
  6. //...
  7. }
  8. });

交换

收集到candidate后,可以通过信令系统将candidate信息发送给远端。

  1. // 发送candidate到远端
  2. signalServer.send({ type: 'candidate', value: candidate });

远端接收到对端的candidate后,会与本地的candidate形成CandidatePair(即连接候选者对)。有了CandidatePair,WebRTC就可以开始尝试建立连接了。 

  1. // 获取到远端的candidate
  2. const onGetRemoteCandidate = candidate => {
  3. console.log('------ 获取到了远端candidate', candidate);
  4. pc.addIceCandidate(candidate);
  5. };

远端音视频数据接收与渲染

当双方都获取到对端的candidate信息后,WebRTC内部就开始尝试建立连接了。连接一旦建成,音视频数据就开始源源不断地由发送端发送给接收端。

通过RTCPeerConnection对象的track事件,我们能接收到远端的音视频数据并进行渲染。

  1. // 监听到远端传过来的媒体数据
  2. pc.addEventListener('track', e => {
  3. console.log('------ 获取到了远端媒体数据:', e);
  4. if (remoteVideo.srcObject !== e.streams[0]) {
  5. remoteVideo.srcObject = e.streams[0];
  6. }
  7. });

完整代码

信令服务器代码

  1. 'use strict ';
  2. let http = require('http'); // 提供HTTP 服务
  3. const { Server } = require('socket.io');
  4. let express = require('express');
  5. const MaxUserNum = 2;
  6. let app = express();
  7. const roomsInfo = {};
  8. const userRoomInfo = {};
  9. //HTTP 服务
  10. let http_server = http.createServer(app);
  11. http_server.listen(8081, '127.0.0.1');
  12. const io = new Server(http_server, {
  13. cors: {
  14. origin: ['http://127.0.0.1:3000', 'http://localhost:3000'],
  15. credentials: true,
  16. },
  17. });
  18. // 处理连接事件
  19. io.sockets.on('connection', socket => {
  20. console.log('got a connection');
  21. // 用于转发sdp、candidate等消息
  22. socket.on('message', ({ roomId, data }) => {
  23. console.log('message , room: ' + roomId + ', data , type:' + data.type);
  24. socket.to(roomId).emit('message', data);
  25. });
  26. socket.on('join', ({ roomId }) => {
  27. if (!roomId) return;
  28. socket.join(roomId);
  29. console.log(`${socket.id} join ${roomId}`);
  30. // 登记房间用户
  31. if (!roomsInfo[roomId]) {
  32. roomsInfo[roomId] = {};
  33. }
  34. roomsInfo[roomId][socket.id] = socket;
  35. //登记用户房间
  36. if (!userRoomInfo[socket.id]) {
  37. userRoomInfo[socket.id] = [];
  38. }
  39. userRoomInfo[socket.id].push(roomId);
  40. let userNum = Object.keys(roomsInfo[roomId]).length;
  41. // 如果房间里人未满
  42. if (userNum <= MaxUserNum) {
  43. // 回复用户已经加入到房间里了
  44. socket.emit('joined', { roomId, userNum });
  45. // 通知另一个用户, 有人来了
  46. if (userNum > 1) {
  47. socket.to(roomId).emit('otherjoined', { roomId, userId: socket.id });
  48. }
  49. } else {
  50. // 如果房间里人满了
  51. socket.leave(roomId);
  52. // 回复用户房间满人了
  53. socket.emit('full', { roomId, userNum });
  54. }
  55. });
  56. const onLeave = ({ roomId }) => {
  57. if (!roomId) return;
  58. socket.leave(roomId);
  59. roomsInfo[roomId] && roomsInfo[roomId][socket.id] && delete roomsInfo[roomId][socket.id];
  60. userRoomInfo[socket.id] &&
  61. (userRoomInfo[socket.id] = userRoomInfo[socket.id].filter(id => id !== roomId));
  62. console.log(
  63. 'someone leaved the room, the user number of room is: ',
  64. roomsInfo[roomId] ? Object.keys(roomsInfo[roomId]).length : 0,
  65. );
  66. // 通知其他用户有人离开了
  67. socket.to(roomId).emit('bye', { roomId, userId: socket.id });
  68. // 回复用户你已经离开房间了
  69. socket.emit('leaved', { roomId });
  70. };
  71. // 用户离开房间
  72. socket.on('leave', onLeave);
  73. //disconnect
  74. socket.on('disconnect', () => {
  75. console.log(socket.id, 'disconnect, and clear user`s Room', userRoomInfo[socket.id]);
  76. if (userRoomInfo[socket.id]) {
  77. userRoomInfo[socket.id].forEach(roomId => {
  78. onLeave({ roomId });
  79. });
  80. delete userRoomInfo[socket.id];
  81. }
  82. });
  83. });

客户端的信令服务器处理对象

  1. import { io, Socket } from 'socket.io-client';
  2. interface Option {
  3. onJoined?: (message: { roomId: string; userNum: number }) => void;
  4. onOtherJoined?: (message: { roomId: string; userId: number }) => void;
  5. onMessage: (data: { type: string; value: any }) => void;
  6. onFull?: (message: { roomId: string }) => void;
  7. onBye?: (message: { roomId: string; userId: number }) => void;
  8. onLeaved?: (message: { roomId: string }) => void;
  9. serverUrl?: string;
  10. }
  11. export default class SignalServer {
  12. socket: Socket;
  13. roomId: string;
  14. constructor(option: Option) {
  15. this.init(option);
  16. }
  17. init(option) {
  18. this.socket = io(option.serverUrl || 'http://127.0.0.1:8081/');
  19. this.socket.connect();
  20. this.socket.on(
  21. 'joined',
  22. option.onJoined ||
  23. (({ roomId, usersNum }) => {
  24. console.log('i joined a room', roomId);
  25. console.log('current user number:', usersNum);
  26. }),
  27. );
  28. this.socket.on(
  29. 'otherjoined',
  30. option.onOtherJoined ||
  31. (({ roomId, userId }) => {
  32. console.log('other user joined, userId', userId);
  33. }),
  34. );
  35. this.socket.on('message', option.onMessage);
  36. this.socket.on(
  37. 'full',
  38. option.onFull ||
  39. (({ roomId }) => {
  40. console.log(roomId, 'is full');
  41. }),
  42. );
  43. this.socket.on(
  44. 'bye',
  45. option.onBye ||
  46. (({ roomId, userId }) => {
  47. console.log(userId, `leaved`, roomId);
  48. }),
  49. );
  50. this.socket.on('leaved', option.onLeaved || (({ roomId }) => {}));
  51. window.addEventListener('beforeunload', () => {
  52. this.leave();
  53. });
  54. }
  55. send(data) {
  56. if (!this.roomId) return;
  57. this.socket.emit('message', { roomId: this.roomId, data });
  58. }
  59. join(roomId) {
  60. this.roomId = roomId;
  61. this.socket.emit('join', { roomId });
  62. }
  63. leave() {
  64. this.roomId && this.socket.emit('leave', { roomId: this.roomId });
  65. this.roomId = '';
  66. }
  67. }

客户端代码 

  1. import React, { useEffect, useState, useRef, useMemo } from 'react';
  2. import { Button, Input, message } from 'antd';
  3. import SignalServer from '../components/SignalServer';
  4. import './index.less';
  5. const pcOption = {};
  6. type State = 'init' | 'disconnect' | 'waiting' | 'canCall' | 'connecting';
  7. const Simple1v1 = () => {
  8. // 远端传递过来的媒体数据
  9. const remoteMediaStream = useRef<MediaStream>(null);
  10. // 本地设备采集的媒体数据
  11. const localMediaStream = useRef<MediaStream>(null);
  12. const localVideo = useRef<HTMLVideoElement>(null);
  13. const remoteVideo = useRef<HTMLVideoElement>(null);
  14. // 信令服务器对象
  15. const signalServer = useRef<SignalServer>(null);
  16. const peerConnection = useRef<RTCPeerConnection>(null);
  17. const [roomId, setRoomId] = useState('');
  18. const [state, setState] = useState<State>('disconnect');
  19. const tip = useMemo(() => {
  20. switch (state) {
  21. case 'init':
  22. return '正在获取媒体数据...';
  23. case 'disconnect':
  24. return '请输入房间号并加入房间';
  25. case 'waiting':
  26. return '等待对方加入房间...';
  27. case 'canCall':
  28. return '可点击啊call进行呼叫';
  29. case 'connecting':
  30. return '通话中';
  31. default:
  32. return '';
  33. }
  34. }, [state]);
  35. useEffect(() => {
  36. // 初始化信令服务器
  37. signalServer.current = new SignalServer({ onMessage, onJoined, onOtherJoined });
  38. const initPeerConnection = () => {
  39. console.log('------ 初始化本地pc对象');
  40. // 创建pc实例
  41. peerConnection.current = new RTCPeerConnection(pcOption);
  42. const pc = peerConnection.current;
  43. // 监听 candidate 获取事件
  44. pc.addEventListener('icecandidate', event => {
  45. const candidate = event.candidate;
  46. if (candidate) {
  47. console.log('------ 获取到了本地 candidate:', candidate);
  48. // 发送candidate到远端
  49. signalServer.current.send({ type: 'candidate', value: candidate });
  50. }
  51. });
  52. // 监听到远端传过来的媒体数据
  53. pc.addEventListener('track', e => {
  54. console.log('------ 获取到了远端媒体数据:', e);
  55. if (remoteVideo.current.srcObject !== e.streams[0]) {
  56. remoteVideo.current.srcObject = e.streams[0];
  57. }
  58. });
  59. };
  60. //获取本地媒体数据
  61. const getLocalMediaStream = () => {
  62. navigator.mediaDevices.getUserMedia({ audio: false, video: true }).then(mediaStream => {
  63. console.log('------ 成功获取本地设备媒体数据:', mediaStream);
  64. if (mediaStream) {
  65. localVideo.current.srcObject = mediaStream;
  66. localMediaStream.current = mediaStream;
  67. // 绑定本地媒体数据到pc对象上
  68. if (localMediaStream.current) {
  69. console.log('------ 绑定本地媒体数据到pc对象上');
  70. localMediaStream.current.getTracks().forEach(track => {
  71. peerConnection.current.addTrack(track, localMediaStream.current);
  72. });
  73. }
  74. }
  75. });
  76. };
  77. initPeerConnection();
  78. getLocalMediaStream();
  79. return () => {
  80. // 离开页面前销毁mediaStream数据
  81. localMediaStream.current &&
  82. localMediaStream.current.getTracks().forEach(track => track.stop());
  83. remoteMediaStream.current &&
  84. remoteMediaStream.current.getTracks().forEach(track => track.stop());
  85. //销毁本地pc
  86. peerConnection.current && peerConnection.current.close();
  87. };
  88. }, []);
  89. const join = () => {
  90. if (!roomId || state !== 'disconnect') return;
  91. signalServer.current.join(roomId);
  92. setState('waiting');
  93. };
  94. const onJoined = ({ roomId, userNum }) => {
  95. message.success('成功加入房间,当前房间人数为:' + userNum);
  96. console.log('------ 成功加入房间,当前房间人数为:' + userNum);
  97. if (userNum === 1) {
  98. setState('waiting');
  99. } else {
  100. setState('canCall');
  101. }
  102. };
  103. const onOtherJoined = data => {
  104. console.log('------ 有人加入房间了');
  105. setState('canCall');
  106. };
  107. const call = () => {
  108. if (state !== 'canCall') return;
  109. // 开始建立连接
  110. setState('connecting');
  111. const pc = peerConnection.current;
  112. // 获取本地sdp(offer)
  113. pc.createOffer().then(offer => {
  114. console.log('------ 获取到了本地offer', offer);
  115. // 绑定本地sdp
  116. pc.setLocalDescription(offer);
  117. // 发送本地sdp到远端
  118. signalServer.current.send({
  119. type: 'offer',
  120. value: offer,
  121. });
  122. });
  123. };
  124. const onMessage = ({ type, value }) => {
  125. switch (type) {
  126. case 'offer':
  127. onGetRemoteOffer(value);
  128. break;
  129. case 'answer':
  130. onGetRemoteAnswer(value);
  131. break;
  132. case 'candidate':
  133. onGetRemoteCandidate(value);
  134. break;
  135. default:
  136. console.log('unknown message');
  137. }
  138. };
  139. const onGetRemoteAnswer = answer => {
  140. console.log('------ 获取到了远端answer', answer);
  141. const pc = peerConnection.current;
  142. // 绑定远端sdp
  143. pc.setRemoteDescription(answer);
  144. };
  145. const onGetRemoteOffer = offer => {
  146. console.log('------ 获取到了远端offer', offer);
  147. // 远端发起呼叫,开始建立连接
  148. setState('connecting');
  149. const pc = peerConnection.current;
  150. // 绑定远端sdp
  151. pc.setRemoteDescription(offer);
  152. // 创建本地sdp
  153. pc.createAnswer().then(answer => {
  154. // 绑定本地sdp
  155. pc.setLocalDescription(answer);
  156. console.log('------ 获取到了本地answer', answer);
  157. // 发送本地sdp到远端
  158. signalServer.current.send({
  159. type: 'answer',
  160. value: answer,
  161. });
  162. });
  163. };
  164. // 获取到远端的candidate
  165. const onGetRemoteCandidate = candidate => {
  166. console.log('------ 获取到了远端candidate', candidate);
  167. peerConnection.current.addIceCandidate(candidate);
  168. };
  169. return (
  170. <div className="one-on-one">
  171. <h1>Simple1v1{tip && `-${tip}`}</h1>
  172. <div className="one-on-one-container">
  173. <div className="one-on-one-operation">
  174. <div className="room-selector operation-item">
  175. <Input
  176. value={roomId || undefined}
  177. disabled={state !== 'disconnect'}
  178. onChange={e => setRoomId(e.target.value)}
  179. placeholder="请输入房间号"></Input>
  180. <Button disabled={state !== 'disconnect'} onClick={join} type="primary">
  181. 加入房间
  182. </Button>
  183. </div>
  184. <div className="call-btn operation-item">
  185. <Button disabled={state !== 'canCall'} onClick={call} type="primary">
  186. call
  187. </Button>
  188. </div>
  189. </div>
  190. <div className="videos">
  191. <div className="local-container">
  192. <h3>local-video</h3>
  193. <video autoPlay controls ref={localVideo}></video>
  194. </div>
  195. <div className="remote-container">
  196. <h3>remote-video</h3>
  197. <video autoPlay controls ref={remoteVideo}></video>
  198. </div>
  199. </div>
  200. </div>
  201. </div>
  202. );
  203. };
  204. export default Simple1v1;

实现效果 

原文地址:WebRTC入门指南 —— 实现一个完整的点对点视频通话(信令服务器+客户端) - 掘金

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓ 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/228271
推荐阅读
相关标签
  

闽ICP备14008679号