加载中...

初聊WebRTC实现点对点音视频通话

前言

音视频通信领域,本人也算是个老兵了。从早先的H.323到SIP、以及自定义协议,从G.729、MJEPG、MPEG-4到iLBC、GIPS、H.264、VP8,从点对点通话到多媒体会议,从PC客户端、专用硬件设备到移动端、网页端......在这二十多年的变迁中,我认为,WebRTC应该是最具影响力的技术变革——正是它的出现,极大地降低了音视频通信的入门门槛。稍不留神,差点要动手写成一篇历史考古文章了。

代码

言归正传,下面分享的是一段React项目中的WebRTC点对点音视频通话实现代码。

1)本地音视频输入设备

首先,需要获取到本地的音视频输入设备,即麦克风、摄像头。通过MediaTrackConstraints可以设置相关参数,包括优先值、最大值、最小值等。需要注意的是,不仅是不同的硬件,而且不同的操作系统、不同的浏览器所支持的参数值范围也是不相同的,所以兼容性测试是不可忽视的。

  1. async function getLocalStream(constraints: MediaStreamConstraints) {
  2. let c: MediaStreamConstraints = {
  3. audio: false,
  4. video: false,
  5. };
  6. const audio: MediaTrackConstraints = {
  7. echoCancellation: true,
  8. noiseSuppression: true,
  9. autoGainControl: true,
  10. };
  11. const video: MediaTrackConstraints = {
  12. aspectRatio: 4 / 3,
  13. width: { ideal: 640, max: 800 },
  14. frameRate: { ideal: 15, max: 20 },
  15. };
  16. // 检查音视频输入设备
  17. (await navigator.mediaDevices.enumerateDevices()).forEach((d) => {
  18. switch (d.kind) {
  19. case ''audioinput'':
  20. if (constraints.audio) {
  21. c.audio = audio;
  22. }
  23. break;
  24. case ''videoinput'':
  25. if (constraints.video) {
  26. c.video = video;
  27. }
  28. break;
  29. }
  30. });
  31. return await navigator.mediaDevices.getUserMedia(c);
  32. }

2)媒体流操作

包括本地媒体流与远程对方媒体流。

  1. /** 关闭媒体流 */
  2. function stopStream(stream: MediaStream | undefined | null) {
  3. stream?.getTracks().forEach((track) => track.stop());
  4. }

3)点对点连接

点对点连接的建立,最关键的就是搞清楚主叫方与被叫方的ICECandidate与SessionDescription交换过程,也就是一般所说的信令部分,以及媒体流Track建立。WebRTC没有提供统一的信令通道的实现,需要大家自己去实现,本文就不展开了。

  1. let peerConnection: RTCPeerConnection | null = null;
  2. let localStream: MediaStream | null = null;
  3. let remoteStream: MediaStream | null = null;
  4. async function initPeerConnection(
  5. constraints: { audio: boolean; video: boolean },
  6. ice: RTCIceServer,
  7. offer: RTCSessionDescription | undefined,
  8. onCandidate: (candidate: RTCIceCandidate) => void,
  9. ) {
  10. peerConnection = new RTCPeerConnection({ iceServers: [ice] });
  11. localStream = await getLocalStream(constraints);
  12. localMedia?.getTracks().forEach((t) => peerConnection!.addTrack(t));
  13. remoteStream = new MediaStream();
  14. peerConnection.onicecandidate = ({ candidate }) => {
  15. candidate && onCandidate(candidate);
  16. };
  17. peerConnection.ontrack = async ({ track }) => {
  18. track.onunmute = () => {
  19. remoteMedia.addTrack(track);
  20. };
  21. };
  22. if (offer) {
  23. // 被叫方
  24. await peerConnection.setRemoteDescription(offer);
  25. const answerInit = await peerConnection.createAnswer();
  26. await peerConnection.setLocalDescription(answerInit);
  27. } else {
  28. // 主叫方
  29. const offerInit = await peerConnection.createOffer({
  30. offerToReceiveAudio: constraints.audio,
  31. offerToReceiveVideo: constraints.video,
  32. });
  33. await peerConnection.setLocalDescription(offerInit);
  34. }
  35. }
  36. function getLocalDescription() {
  37. return peerConnection?.localDescription;
  38. }
  39. async function addRemoteDescription({ description }: { description: RTCSessionDescription }) {
  40. try {
  41. if (peerConnection) {
  42. if (description) {
  43. await peerConnection.setRemoteDescription(description);
  44. }
  45. }
  46. } catch (err: any) {}
  47. }
  48. async function addRemoteCandidate({ candidate }: { candidate: RTCIceCandidate }) {
  49. try {
  50. if (peerConnection) {
  51. if (candidate) {
  52. await peerConnection.addIceCandidate(candidate);
  53. }
  54. }
  55. } catch (err: any) {}
  56. }
  57. function closePeerConnection() {
  58. stopStream(remoteStream)
  59. stopStream(localStream)
  60. if (peerConnection) {
  61. peerConnection.close();
  62. peerConnection = null;
  63. }
  64. }

4)前端页面展示

这里以React项目为例,摘取前端页面中与点对点通话的音视频播放的相关实现代码。

  1. const localVideoRef = useRef<HTMLVideoElement | null>(null)
  2. const remoteVideoRef = useRef<HTMLVideoElement | null>(null)
  3. useEffect(() => {
  4. const init = async () => {
  5. try {
  6. await initPeerConnection(
  7. { audio: true, video: true },
  8. ice,
  9. offer, // 主叫方,为空;被叫方,为信令通道接收到的主叫方SessionDescription
  10. (candidate) => {
  11. // 信令通道发送ICECandidate
  12. },
  13. )
  14. if (!offer) { // 主叫方
  15. const sd = peerConnection.getLocalDescription()
  16. // 信令通道发送SessionDescription
  17. }
  18. localVideoRef.current!.srcObject = localStream
  19. remoteVideoRef.current!.srcObject = remoteStream
  20. } catch (err: any) {
  21. if (err === ''PermissionDeniedError'') {
  22. // 浏览器权限请求未被接受
  23. }
  24. }
  25. }
  26. init()
  27. return () => {
  28. closePeerConnection()
  29. }
  30. }, [])
  31. return (
  32. // ...
  33. <video
  34. ref={remoteVideoRef}
  35. width={640}
  36. height={480}
  37. muted={false}
  38. autoPlay
  39. playsInline
  40. />
  41. <video
  42. ref={localVideoRef}
  43. width={144}
  44. height={108}
  45. muted={true}
  46. autoPlay
  47. playsInline
  48. />
  49. // ...
  50. )

结束语

WebRTC确实使得多媒体通信领域的开发变得更简单了。这里通过点对点音视频通话,分享了一点点WebRTC的开发经验。实际上,在多媒体通信领域,还有很多的应用场景与需求,后续还会继续分享。