关于react.js:WebRTC-跨端通信React-React-Native-双端视频聊天屏幕共享

3次阅读

共计 13347 个字符,预计需要花费 34 分钟才能阅读完成。

大家好,我是杨胜利。

之前介绍过 WebRTC,简略来说它是一个点对点的实时通信技术,次要基于浏览器来实现音视频通信。这项技术目前曾经被广泛应用于实时视频通话,多人会议等场景。

不过 WebRTC 因为其过于优良的体现,其利用范畴曾经不限于 Web 端,挪动 App 也根本实现了 WebRTC 的 API。在跨平台框架中,Flutter 和 React Native 都实现了对 WebRTC 的反对。

咱们以 App(React Native)为呼叫端,Web(React)为接收端,别离介绍两端如何进行视频通话。

接收端 React 实现

React 运行在浏览器中,无需援用任何模块,能够间接应用 WebRTC API。上面分几个步骤,逐渐介绍在 Web 端如何获取、发送、承受近程视频流。

1. 获取本地摄像头流,并保留。

const stream = null;
const getMedia = async () => {
  let ori_stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
  stream = ori_stream;
};

2. 创立 video 标签用于播放视频。

创立两个 video 标签,别离用于播放本地视频和近程视频。

const local_video = useRef();
const remote_video = useRef();
const Player = () => {
  return (
    <div>
      <video ref={local_video} autoPlay muted />;
      <video ref={remote_video} autoPlay muted />;
    </div>
  );
};
// stream 是上一步获取的本地视频流
if (local_video.current) {local_video.current.srcObject = stream;}

3. 创立 RTC 连贯实例。

每一个应用 WebRTC 通信的客户端,都要创立一个 RTCPeerConnection 连贯实例,该实例是真正负责通信的角色。WebRTC 通信的本质就是 RTCPeerConnection 实例之间的通信。

// 1. 创立实例
let peer = new RTCPeerConnection();
// 2. 将本地视频流增加到实例中
stream.getTracks().forEach((track) => {peer.addTrack(track, stream);
});
// 3. 接管近程视频流并播放
peer.ontrack = async (event) => {let [remoteStream] = event.streams;
  remote_video.current.srcObject = remoteStream;
};

实例创立之后,别忘记将上一步获取的摄像头流增加到实例中。

当两端连贯齐全建设之后,在 peer.ontrack 之内就能接管到对方的视频流了。

4. 连贯信令服务器,筹备与 App 端通信。

WebRTC 的通信过程须要两个客户端实时进行数据交换。替换内容分为两大部分:

  • 替换 SDP(媒体信息)。
  • 替换 ICE(网络信息)。

因而咱们须要一个 WebSocket 服务器来连贯两个客户端进行传输数据,该服务器在 WebRTC 中被称为 信令服务器

咱们曾经基于 socket.io 搭建了信令服务器,当初须要客户端连贯,形式如下。

(1)装置 socket.io-client:

$ yarn add socket.io-client

(2)连贯服务器,并监听音讯。

连贯服务器时带上验证信息(上面的用户 ID、用户名),不便在通信时能够找到对方。

import {io} from 'socket.io-client';
const socket = null;
const socketInit = () => {
  let sock = io(`https://xxxx/webrtc`, {
    auth: {
      userid: '111',
      username: '我是接收端',
      role: 'reader',
    },
  });
  sock.on('connect', () => {console.log('连贯胜利');
  });
  socket = sock;
};
useEffect(() => {socketInit();
}, []);

通过下面 4 个步骤,咱们的筹备工作曾经做好了。结下来能够进行正式的通信步骤了。

5. 接管 offer,替换 SDP。

监听 offer 事件(呼叫端发来的 offer 数据),而后创立 answer 并发回呼叫端。

// 接管 offer
socket.on('offer', (data) => {transMedia(data);
});
// 发送 answer
const transMedia = async (data: any) => {let offer = new RTCSessionDescription(data.offer);
  await peer.setRemoteDescription(offer);
  let answer = await peer.createAnswer();

  socket.emit('answer', {
    to: data.from, // 呼叫端 Socket ID
    answer,
  });
  await peer.setLocalDescription(answer);
};

6. 接管 candidate,替换 ICE。

监听 candid 事件(呼叫端发来的 candidate 数据)并增加到本地 peer 实例,而后监听本地的 candidate 数据并发回给呼叫端。

上一步执行 peer.setLocalDescription() 之后,就会触发 peer.onicecandidate 事件。

// 接管 candidate
socket.on('candid', (data) => {let candid = new RTCIceCandidate(data.candid);
  peer.addIceCandidate(candid);
});
// 发送 candidate
peer.onicecandidate = (event) => {if (event.candidate) {
    socket.emit('candid', {
      to: data.from, // 呼叫端 Socket ID
      candid: event.candidate,
    });
  }
};

至此,整个通信过程就实现了。如果没有意外,此时在第 3 步的 peer.ontrack 事件内拿到的对端视频流开始传输数据,咱们能够看到对端的视频画面了。

呼叫端 React Native 实现

在 React Native 端并不能间接应用 WebRTC API,咱们须要一个第三方模块 react-native-webrtc 来实现,它提供了和 Web 端简直统一的 API。

侥幸的是,React Native 能够复用 Web 端的大多数逻辑性资源,socket.io-client 能够间接装置应用,和 Web 端完全一致。

可怜的是,App 开发少不了原生的环境配置、权限配置,这些比拟繁琐,接下来介绍如何实现吧。

1. 创立 React Native 我的项目。

创立我的项目之前须要配置开发环境,咱们以 Android 为例,具体的配置能够看这篇文章。

配置实现之后,间接通过 npx 命令创立我的项目,命名为 RnWebRTC

$ npx react-native init RnWebRTC --template react-native-template-typescript

提醒:如果不想应用 TypeScript,将 –template 选项和前面的内容去掉即可。

我装置的最新版本如下:

  • react: “18.1.0”
  • react-native: “0.70.6”

创立之后,查看根目录的 index.jsApp.tsx 两个文件,别离是入口文件和页面组件,咱们就在 App.tsx 这个组件中编写代码。

咱们以安卓为例,间接用手机数据线连贯电脑,关上 USB 调试模式,而后运行以下命令:

$ yarn run android

执行该命令会装置 Gradle(安卓包治理)依赖,并编译打包 Android 利用。第一次运行耗时比拟久,急躁期待打包实现后,手机上会提醒装置该 App。

该命令在打包的同时,还会独自启动一个终端,运行 Metro 开发服务器。Metro 的作用是监 JS 代码批改,并打包成 js bundle 交给原生 App 去渲染。

独自启动 Metro 服务器,可运行 yarn run start

2. 装置 react-native-webrtc。

间接运行装置命令:

$ yarn add react-native-webrtc

装置之后并不能间接应用,须要在 android 文件夹中批改两处代码。

第一处:因为 react-native-webrtc 须要最低的安卓 SDK 版本为 24,而默认生成的最低版本是 21,所以批改 android/build.gradle 中的配置:

buildscript {
  ext {minSdkVersion = 24}
}

第二处:webrtc 须要摄像头、麦克风等权限,所以咱们把权限先配齐喽。在 android/app/src/main/AndroidManifest.xml 中的 <application> 标签前增加如下配置:

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.audio.output" />
<uses-feature android:name="android.hardware.microphone" />

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" />

接着执行命令从新打包安卓:

$ yarn run android

当初就能够在 App.tsx 中导入和应用 WebRTC 的相干 API 了:

import {
  ScreenCapturePickerView,
  RTCPeerConnection,
  RTCIceCandidate,
  RTCSessionDescription,
  RTCView,
  MediaStream,
  MediaStreamTrack,
  mediaDevices,
} from 'react-native-webrtc';

3. 连贯信令服务器,筹备与 Web 端通信。

因为 React Native 能够间接应用 socket.io-client 模块,所以这一步和 Web 端的代码统一。

$ yarn add socket.io-client

连贯信令服务器时,只是传递的验证信息不同:

import {io} from 'socket.io-client';
const socket = null;
const socketInit = () => {
  let sock = io(`https://xxxx/webrtc`, {
    auth: {
      userid: '222',
      username: '我是呼叫端',
      role: 'sender',
    },
  });
  sock.on('connect', () => {console.log('连贯胜利');
  });
  socket = sock;
};
useEffect(() => {socketInit();
}, []);

4. 应用 RTCView 组件播放视频。

创立两个 RTCView 组件,别离用于播放本地视频和近程视频。

import {mediaDevices, RTCView} from 'react-native-webrtc';
var local_stream = null;
var remote_stream = null;
// 获取本地摄像头
const getMedia = async () => {
  local_stream = await mediaDevices.getUserMedia({
    audio: true,
    video: true,
  });
};
// 播放视频组件
const Player = () => {
  return (
    <View>
      <RTCView style={{height: 500}} streamURL={local_stream.toURL()} />
      <RTCView style={{height: 500}} streamURL={remote_stream.toURL()} />
    </View>
  );
};

5. 创立 RTC 连贯实例。

这一步与 Web 端基本一致,接管到远端流间接赋值。

// 1. 创立实例
let peer = new RTCPeerConnection();
// 2. 将本地视频流增加到实例中
local_stream.getTracks().forEach((track) => {peer.addTrack(track, local_stream);
});
// 3. 接管近程视频流
peer.ontrack = async (event) => {let [remoteStream] = event.streams;
  remote_stream = remoteStream;
};

6. 创立 offer,开始替换 SDP。

App 端作为呼叫端,须要被动创立 offer 并发给接收端,并监听接收端发回的 answer:

// 发送 offer
const peerInit = async (socket_id) => {let offer = await peer.createOffer();
  peer.setLocalDescription(offer);
  socket.emit('offer', {
    to: socket_id, // 接收端 Socket ID
    offer: offer,
  });
};
// 接管 answer
socket.on('answer', (data) => {let answer = new RTCSessionDescription(data.answer);
  peer.setRemoteDescription(answer);
});

7. 监听 candidate,替换 ICE。

这一步仍然与 Web 端统一,别离是发送本地的 candidate 和接管近程的 candidate:

// 发送 candidate
peer.onicecandidate = (event) => {if (event.candidate) {
    socket.emit('candid', {
      to: socket_id, // 接收端 Socket ID
      candid: event.candidate,
    });
  }
};
// 接管 candidate
socket.on('candid', (data) => {let candid = new RTCIceCandidate(data.candid);
  peer.addIceCandidate(candid);
});

至此,App 端与 Web 端的视频通话流程曾经实现,当初两端能够相互看到对方的视频画面了。

搭建 socket.io 信令服务器

在 Web 和 App 两端视频通话时用到了信令服务器,该服务应用 socket.io 实现,代码并不简单,上面介绍下信令服务器如何编写:

1. 创立我的项目,装置须要的依赖。

创立 socket-server 文件夹,并执行以下命令生成 package.json:

$ npm init

接着装置必要的模块:

$ npm install koa socket.io

2. 创立入口文件,启动 socket 服务。

创立 app.js 文件,写入以下代码:

const Koa = require('koa');
const http = require('http');
const SocketIO = require('socket.io');
const SocketIoApi = require('./io.js');

const app = new Koa();

const server = http.createServer(app.callback());

const io = new SocketIO.Server(server, {cors: { origin: '*'},
  allowEIO3: true,
});
app.context.io = io; // 将 socket 实例存到全局

new SocketIoApi(io);

server.listen(9800, () => {console.log(`listen to http://localhost:9800`);
});

代码中应用 Koa 框架运行一个服务器,并且接入了 socket.io,一个根本的 WebSocket 服务器就写好了。接着将它运行起来:

$ node app.js

此时 Koa 运行的 HTTP 服务器和 socket.io 运行的 WebSocket 服务器共享 9800 端口。

3. 创立 io.js,编写信令服务器逻辑。

上一步在入口文件 app.js 中援用了 io.js,该文件导出一个类,咱们来编写具体的逻辑:

// io.js
class IoServer {constructor(io) {
    this.io = io;
    this.rtcio = io.of('/webrtc');
    this.rtcio.on('connection', (socket) => {this.rtcListen(socket);
    });
  }
  rtcListen(socket) {
    // 发送端|发送 offer
    socket.on('offer', (json) => {let { to, offer} = json;
      let target = this.rtcio.sockets.get(to);
      if (target) {
        target.emit('offer', {
          from: socket.id,
          offer,
        });
      } else {console.error('offer 接管方未找到');
      }
    });
    // 接收端|发送 answer
    socket.on('answer', (json) => {let { to, answer} = json;
      let target = this.rtcio.sockets.get(to);
      // console.log(to, socket)
      if (target) {
        target.emit('answer', {
          from: socket.id,
          answer,
        });
      } else {console.error('answer 接管方未找到');
      }
    });
    // 发送端|发送 candidate
    socket.on('candid', (json) => {let { to, candid} = json;
      let target = this.rtcio.sockets.get(to);
      // console.log(to, socket)
      if (target) {
        target.emit('candid', {
          from: socket.id,
          candid,
        });
      } else {console.error('candid 接管方未找到');
      }
    });
  }
}

module.exports = IoServer;

下面代码的逻辑中,当客户端连贯到服务器,就开始监听 offeranswercandid 三个事件。当有客户端发送音讯,这里负责将音讯转发给另一个客户端。

两个客户端通过惟一的 Socket ID 找到对方。在 socket.io 中,每一次连贯都会产生一个惟一的 Socket Id。

4. 获取已连贯的接收端列表。

因为每次刷新浏览器 WebSocket 都要从新连贯,因而每次的 Socket ID 都不雷同。为了在呼叫端精确找到在线的接收端,咱们写一个获取接收端列表的接口。

在入口文件中通过 app.context.io = io 将 SocketIO 实例全局存储到了 Koa 中,那么获取已连贯的接收端形式如下:

app.get('/io-clients', async (ctx, next) => {let { io} = ctx.app.context;
  try {let data = await io.of('/webrtc').fetchSockets();
    let resarr = data
      .map((row) => ({
        id: row.id,
        auth: row.handshake.auth,
        data: row.data,
      }))
      .filter((row) => row.auth.role == 'reader');
    ctx.body = {code: 200, data: resarr};
  } catch (error) {ctx.body = error.toString();
  }
});

而后在呼叫端应用 GET 申请 https://xxxx/io-clients 可拿到在线接收端的 Socket ID 和其余信息,而后抉择一个接收端发动连贯。

留神:在线上获取摄像头和屏幕时要求域名必须是 https,否则无奈获取。因而切记给 Web 端和信令服务器都配置好 https 的域名,能够防止通信时产生异样。

TURN 跨网络视频通信

后面咱们实现了 App 端和 Web 端双端通信,然而通信胜利有一个前提:两端必须连贯同一个 WIFI

这是因为 WebRTC 的通信是基于 IP 地址和端口来找到对方,如果两端连贯不同的 WIFI(不在同一个网段),或是 App 用流量 Web 端用 WIFI,那么两端的 IP 地址谁都不意识谁,天然无奈建设通信。

当两端不在一个局域网时,优先应用 STUN 服务器,将两端的本地 IP 转换为公网 IP 进行连贯。STUN 服务器咱们间接应用谷歌的就能够,然而因为防火墙等各种起因,理论测试 STUN 根本是连不通的。

当两端不能找到对方,无奈间接建设连贯时,那么咱们就要应用兜底计划 ——— 用一个中继服务器转发数据,将媒体流数据转发给对方。该中继服务器被称为 TURN 服务器。

TURN 服务器因为要转发数据流,因而对带宽耗费比拟大,须要咱们本人搭建。目前有很多开源的 TURN 服务器计划,通过性能测试,我抉择应用 Go 语言开发的 pion/turn 框架。

1. 装置 Golang:

应用 pion/turn 的第一步是在你的服务器上安装 Golang。我应用的是 Linux Centos7 零碎,应用 yum 命令装置最不便。

$ yum install -y epel-release # 增加 epel 源
$ yum install -y golang # 间接装置

装置之后应用如下命令查看版本,测试是否装置胜利:

$ go version
go version go1.17.7 linux/amd64

2. 运行 pion/turn:

间接将 pion/turn 的源码拉下来:

$ git clone https://github.com/pion/turn ./pion-turn

源码中提供了很多案例能够间接应用,咱们应用 examples/turn-server/simple 这个目录下的代码:

$ cd ./pion-turn/examples/turn-server/simple
$ go build

应用 go build 编译后,当前目录下会生成一个 simple 文件,该文件是可执行文件,应用该文件启动 TURN 服务器:

$ ./simple -public-ip 123.45.67.89 -users ruidoc=123456

下面命令中的 123.45.67.89 是你服务器的公网 IP,并配置一个用户名和明码别离为 ruidoc123456,这几项配置依据你的理论状况设置。

默认状况下该命令不会后盾运行,咱们用上面的形式让它后盾运行:

$ nohup ./simple -public-ip 123.45.67.89 -users ruidoc=123456 &

此时,该 TURN 服务曾经在后盾运行,并占用一个 3478 的 UDP 端口。咱们查看一下端口占用:

$ netstat -nplu

从上图能够看出,该服务曾经在运行中。

3. 配置平安组、检测 TURN 是否可用。

上一步曾经启动了 TURN 服务器,然而默认状况下从内部连贯不上(这里是个坑),因为阿里云须要在平安组的入方向增加一条 UPD 3478 端口(其余云服务商也差不多),示意该端口容许从内部拜访。

平安组增加后,咱们就能够测试 TURN 服务器的连通性了。

关上 Trickle ICE,增加咱们的 TURN 服务器,格局为 turn:123.45.67.89:3478,而后输出上一步配置的用户名和明码:

点击 Gather candidates 按钮,会列出以下信息。如果蕴含 relay 这一条,阐明咱们的 TURN 服务器搭建胜利,能够应用了。

4. 客户端增加 ICE 配置。

在 App 端和 Web 端创立 RTC 实例时,退出以下配置:

var turnConf = {
  iceServers: [
    {urls: 'stun:stun1.l.google.com:19302', // 收费的 STUN 服务器},
    {
      urls: 'turn:123.45.67.89:3478',
      username: 'ruidoc',
      credential: '123456',
    },
  ],
};
var peer = new RTCPeerConnection(turnConf);

当初敞开手机 WIFI,应用流量与 Web 端发动连贯,不出意外能够失常通信,然而提早如同高了一些(毕竟走直达必定没有直连快)。此时再关上手机 WIFI 从新连贯,发现提早又变低了。

这就是 WebRTC 智能的中央。它会优先尝试直连,如果直连不胜利,最初才会应用 TURN 转发。

App 端屏幕共享

视频通话个别都是共享摄像头,然而有的时候会有共享屏幕的需要。

在 Web 端共享屏幕很简略,将 getUserMedia 改成 getDisplayMedia 即可。然而在 App 端,可能因为隐衷和平安问题,实现屏幕共享比拟吃力。

安卓原生端应用 mediaProjection 实现共享屏幕。在 Android 10+ 之后,如果想正确共享屏幕,必须要有一个继续存在的“前台过程”来保障屏幕共享的过程不被零碎主动杀死。

如果没有配置前台过程,则屏幕流无奈传输。上面咱们来介绍下在如何 App 端共享屏幕。

1. 装置 Notifee。

Notifee 是 React Native 实现告诉栏音讯的第三方库。咱们能够启动一个继续存在的音讯告诉作为前台过程,从而使屏幕流失常推送。

应用命令装置 Notifee:

$ yarn add @notifee/react-native@5

留神这里装置 Notifee 5.x 的版本,因为最新版的 7.x 须要 Android SDK 的 compileSdkVersion 为 33,而咱们创立的我的项目默认为 31,应用 5.x 不须要批改 SDK 的版本号。

2. 注册前台服务。

在入口文件 index.js 中,咱们应用 Notifee 注册一个前台服务:

import notifee from '@notifee/react-native';
notifee.registerForegroundService((notification) => {return new Promise(() => {});
});

这里只须要注册一下,不须要其余操作,因而比较简单。接着还须要在 Android 代码中注册一个 service,否则前台服务不失效。

关上 android/app/src/main/AndroidManifest.xml 文件,在 <application> 标签内增加如下代码:

<service
   android:name="app.notifee.core.ForegroundService"
   android:foregroundServiceType="mediaProjection|camera|microphone" />

3. 创立前台告诉。

注册好前台服务之后,接着咱们在获取屏幕前创立前台告诉,代码如下:

import notifee from '@notifee/react-native';
const startNoti = async () => {
  try {
    let channelId = await notifee.createChannel({
      id: 'default',
      name: 'Default Channel',
    });
    await notifee.displayNotification({
      title: '屏幕录制中...',
      body: '在利用中手动敞开该告诉',
      android: {
        channelId,
        asForegroundService: true, // 告诉作为前台服务,必填
      },
    });
  } catch (err) {console.error('前台服务启动异样:', err);
  }
};

该办法会创立一个告诉音讯,具体 API 能够参考 Notifee 文档。接着在捕捉屏幕之前调用该办法即可:

const getMedia = async () => {await startNoti();
  let stream = await mediaDevices.getDisplayMedia();};

最终通过测试,能够看到 App 端和 Web 端的屏幕都实现了共享。App 端成果如下:

Web 端成果如下:

摄像头与屏幕视频混流

在一些直播教学的场景中,呼叫端会同时共享两路视频 ——— 屏幕画面和摄像头画面。在咱们的教训中,一个 RTC 连贯实例中只能增加一条视频流。

如果要同时共享屏幕和摄像头,咱们首先想到的计划可能是在一个客户端创立两个 peer 实例,每个实例中增加一路视频流,发动两次 RTC 连贯。

事实上这种计划是低效的,减少复杂度的同时也减少了资源的损耗。那么能不能在一个 peer 实例中增加两路视频呢?其实是能够的。

总体思路:在呼叫端将两条流合并为一条,在接收端再将一条流拆分为两条。

1. 呼叫端组合流。

组合流其实很简略。因为流是由多个媒体轨道组成,咱们只须要从屏幕和摄像头中拿到媒体轨道,再将它们塞到一个自定义的流中,一条蕴含两个视频轨道的流就组合好了。

var stream = new MediaStream();
const getMedia = async () => {let camera_stream = await mediaDevices.getUserMedia();
  let screen_stream = await mediaDevices.getDisplayMedia();
  screen_stream.getVideoTracks().map((row) => stream.addTrack(row));
  camera_stream.getVideoTracks().map((row) => stream.addTrack(row));
  camera_stream.getAudioTracks().map((row) => stream.addTrack(row));
};

代码中为一个自定义媒体流增加了三条媒体轨道,别离是屏幕视频、摄像头视频和摄像头音频。记住这个程序,在接收端依照该程序拆流。

接着将这条媒体流增加到 peer 实例中,前面走失常的通信逻辑即可:

stream.getTracks().forEach((track) => {peer.addTrack(track, stream);
});

2. 接收端拆解流。

接收端在 ontrack 事件中拿到组合流,进行拆解:

peer.ontrack = async (event: any) => {const [remoteStream] = event.streams;
  let screen_stream = new MediaStream();
  let camera_stream = new MediaStream();
  remoteStream.getTracks().forEach((track, ind) => {if (ind == 0) {screen_stream.addTrack(track);
    } else {camera_stream.addTrack(track);
    }
  });
  video1.srcObject = camera_stream; // 播放摄像头音视频
  video2.srcObject = screen_stream; // 播放屏幕视频
};

这一步中,定义两条媒体流,而后将接管到的混合流中的媒体轨道拆分,别离增加到两条流中,这样屏幕流和摄像头流就拆开了,别离在两个 video 中播放即可。

总结

本篇比拟零碎的介绍了 App 端和 Web 端应用 WebRTC 通信的全流程,能够看到整个流程还是比较复杂的。尤其是咱们前端不善于的中央,比方 TURN 搭建和连通,App 端的各种版本权限问题,坑还是很多的。

如果有小伙伴在这个流程中遇到了问题,欢送加我微信 ruidoc 拉你进入跨端与音视频探讨群发问,或者关注我的公众号 程序员胜利 查看更多文章。

再次感谢您的浏览~

正文完
 0