webRTC.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. import { getRandomString } from 'billd-utils';
  2. import { LiveLineEnum, MediaTypeEnum } from '@/interface';
  3. import { prodDomain } from '@/spec-config';
  4. import { AppRootState, useAppStore } from '@/store/app';
  5. import { useNetworkStore } from '@/store/network';
  6. import { WsCandidateType, WsMsgTypeEnum } from '@/types/websocket';
  7. /** 设置分辨率 */
  8. export async function handleResolutionRatio(data: {
  9. frameRate: number;
  10. height: number;
  11. stream: MediaStream;
  12. }): Promise<number> {
  13. const { frameRate, height, stream } = data;
  14. const queue: Promise<any>[] = [];
  15. console.log('开始设置分辨率', height);
  16. stream.getTracks().forEach((track) => {
  17. if (track.kind === 'video') {
  18. queue.push(
  19. track.applyConstraints({
  20. height: { ideal: height },
  21. frameRate: { ideal: frameRate },
  22. })
  23. );
  24. }
  25. });
  26. try {
  27. await Promise.all(queue);
  28. console.log('设置分辨率成功');
  29. return 1;
  30. } catch (error) {
  31. console.error('设置分辨率失败', height, error);
  32. return 0;
  33. }
  34. }
  35. /** 设置帧率 */
  36. export async function handleMaxFramerate(data: {
  37. frameRate: number;
  38. height: number;
  39. stream: MediaStream;
  40. }): Promise<number> {
  41. const { frameRate, height, stream } = data;
  42. const queue: Promise<any>[] = [];
  43. console.log('开始设置帧率', frameRate);
  44. stream.getTracks().forEach((track) => {
  45. if (track.kind === 'video') {
  46. queue.push(
  47. track.applyConstraints({
  48. height: { ideal: height },
  49. frameRate: { ideal: frameRate },
  50. })
  51. );
  52. }
  53. });
  54. try {
  55. await Promise.all(queue);
  56. console.log('设置帧率成功');
  57. return 1;
  58. } catch (error) {
  59. console.error('设置帧率失败', frameRate, error);
  60. return 0;
  61. }
  62. }
  63. export class WebRTCClass {
  64. roomId = '-1';
  65. sender = '';
  66. receiver = '';
  67. videoEl: HTMLVideoElement;
  68. peerConnection: RTCPeerConnection | null = null;
  69. dataChannel: RTCDataChannel | null = null;
  70. cbDataChannel: RTCDataChannel | null = null;
  71. /** 最大码率 */
  72. maxBitrate = -1;
  73. /** 最大帧率 */
  74. maxFramerate = -1;
  75. /** 分辨率 */
  76. resolutionRatio = -1;
  77. localStream?: MediaStream | null;
  78. isSRS: boolean;
  79. constructor(data: {
  80. roomId: string;
  81. videoEl: HTMLVideoElement;
  82. maxBitrate?: number;
  83. maxFramerate?: number;
  84. resolutionRatio?: number;
  85. isSRS: boolean;
  86. sender: string;
  87. receiver: string;
  88. localStream?: MediaStream;
  89. }) {
  90. this.roomId = data.roomId;
  91. this.videoEl = data.videoEl;
  92. // document.body.appendChild(this.videoEl);
  93. this.sender = data.sender;
  94. this.receiver = data.receiver;
  95. this.localStream = data.localStream;
  96. if (data.maxBitrate) {
  97. this.maxBitrate = data.maxBitrate;
  98. }
  99. if (data.resolutionRatio) {
  100. this.resolutionRatio = data.resolutionRatio;
  101. }
  102. if (data.maxFramerate) {
  103. this.maxFramerate = data.maxFramerate;
  104. }
  105. this.isSRS = data.isSRS;
  106. console.warn('new webrtc参数:', data);
  107. this.createPeerConnection();
  108. }
  109. prettierLog = (data: {
  110. msg: string;
  111. type?: 'log' | 'warn' | 'error' | 'success';
  112. }) => {
  113. const { msg, type } = data;
  114. if (type === 'success') {
  115. console.log(
  116. `%c ` +
  117. `【WebRTCClass】${new Date().toLocaleString()},房间id:${
  118. this.roomId
  119. }` +
  120. ` %c ${msg} ` +
  121. `%c`,
  122. 'background:#35495e ; padding: 1px; border-radius: 3px 0 0 3px; color: #fff',
  123. 'background:#41b883 ; padding: 1px; border-radius: 0 3px 3px 0; color: #fff',
  124. 'background:transparent'
  125. );
  126. } else {
  127. console[type || 'log'](
  128. `【WebRTCClass】${new Date().toLocaleString()},房间id:${this.roomId}`,
  129. msg
  130. );
  131. }
  132. };
  133. /** 设置分辨率 */
  134. setResolutionRatio = async (height: number) => {
  135. if (this.localStream) {
  136. const res = await handleResolutionRatio({
  137. frameRate: this.maxFramerate,
  138. stream: this.localStream,
  139. height,
  140. });
  141. return res;
  142. }
  143. };
  144. /** 设置最大帧率 */
  145. setMaxFramerate = async (maxFramerate: number) => {
  146. if (this.localStream) {
  147. const res = await handleMaxFramerate({
  148. frameRate: maxFramerate,
  149. stream: this.localStream,
  150. height: this.resolutionRatio,
  151. });
  152. return res;
  153. }
  154. };
  155. /** 设置最大码率 */
  156. setMaxBitrate = (maxBitrate: number) => {
  157. console.log('开始设置最大码率', maxBitrate);
  158. return new Promise<number>((resolve) => {
  159. this.peerConnection?.getSenders().forEach((sender) => {
  160. if (sender.track?.kind === 'video') {
  161. const parameters = { ...sender.getParameters() };
  162. if (parameters.encodings[0]) {
  163. const val = 1000 * maxBitrate;
  164. if (parameters.encodings[0].maxBitrate === val) {
  165. console.log('最大码率不变,不设置');
  166. resolve(1);
  167. return;
  168. }
  169. parameters.encodings[0].maxBitrate = val;
  170. sender
  171. .setParameters(parameters)
  172. .then(() => {
  173. console.log('设置最大码率成功', maxBitrate);
  174. this.maxBitrate = val;
  175. resolve(1);
  176. })
  177. .catch((error) => {
  178. console.error('设置最大码率失败', maxBitrate, error);
  179. resolve(0);
  180. });
  181. }
  182. }
  183. });
  184. });
  185. };
  186. /** 创建offer */
  187. createOffer = async () => {
  188. if (!this.peerConnection) return;
  189. this.prettierLog({ msg: 'createOffer开始', type: 'warn' });
  190. try {
  191. const sdp = await this.peerConnection.createOffer();
  192. this.prettierLog({ msg: 'createOffer成功', type: 'warn' });
  193. return sdp;
  194. } catch (error) {
  195. this.prettierLog({ msg: 'createOffer失败', type: 'error' });
  196. console.error(error);
  197. }
  198. };
  199. /** 创建answer */
  200. createAnswer = async () => {
  201. if (!this.peerConnection) return;
  202. this.prettierLog({ msg: 'createAnswer开始', type: 'warn' });
  203. try {
  204. const sdp = await this.peerConnection.createAnswer();
  205. this.prettierLog({ msg: 'createAnswer成功', type: 'warn' });
  206. return sdp;
  207. } catch (error) {
  208. this.prettierLog({ msg: 'createAnswer失败', type: 'error' });
  209. console.error(error);
  210. }
  211. };
  212. /** 处理candidate */
  213. addIceCandidate = async (candidate: RTCIceCandidateInit) => {
  214. this.prettierLog({ msg: 'addIceCandidate开始', type: 'warn' });
  215. try {
  216. await this.peerConnection?.addIceCandidate(candidate);
  217. this.prettierLog({ msg: 'addIceCandidate成功', type: 'warn' });
  218. } catch (error) {
  219. this.prettierLog({ msg: 'addIceCandidate错误', type: 'error' });
  220. console.error(error);
  221. }
  222. };
  223. /** 设置本地描述 */
  224. setLocalDescription = async (sdp: RTCLocalSessionDescriptionInit) => {
  225. if (!this.peerConnection) return;
  226. this.prettierLog({ msg: 'setLocalDescription开始', type: 'warn' });
  227. try {
  228. await this.peerConnection.setLocalDescription(sdp);
  229. this.prettierLog({ msg: 'setLocalDescription成功', type: 'warn' });
  230. } catch (error) {
  231. this.prettierLog({ msg: 'setLocalDescription失败', type: 'error' });
  232. console.error(error);
  233. }
  234. };
  235. /** 设置远端描述 */
  236. setRemoteDescription = async (sdp: RTCSessionDescriptionInit) => {
  237. if (!this.peerConnection) return;
  238. this.prettierLog({ msg: 'setRemoteDescription开始', type: 'warn' });
  239. try {
  240. await this.peerConnection.setRemoteDescription(sdp);
  241. this.prettierLog({ msg: 'setRemoteDescription成功', type: 'warn' });
  242. } catch (error) {
  243. this.prettierLog({ msg: 'setRemoteDescription失败', type: 'error' });
  244. console.error(error);
  245. }
  246. };
  247. handleStreamEvent = () => {
  248. if (!this.peerConnection) return;
  249. // 废弃:https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream
  250. // this.prettierLog({ msg: '开始监听pc的addstream事件', type: 'warn' });
  251. // this.peerConnection.addEventListener('addstream', (event) => {
  252. // this.prettierLog({ msg: 'pc收到addstream事件', type: 'warn' });
  253. // console.log('addstream事件的event', event);
  254. // console.log('addstream事件的stream', event.stream);
  255. // console.log('addstream事件的视频轨', event.stream.getVideoTracks());
  256. // console.log('addstream事件的音频轨', event.stream.getAudioTracks());
  257. // });
  258. this.prettierLog({ msg: '开始监听pc的track事件', type: 'warn' });
  259. this.peerConnection.addEventListener('track', (event) => {
  260. this.prettierLog({ msg: 'pc收到track事件', type: 'warn' });
  261. console.log('track事件的event', event);
  262. console.log('track事件的stream', event.streams[0]);
  263. console.log('track事件的视频轨', event.streams[0].getVideoTracks());
  264. console.log('track事件的音频轨', event.streams[0].getAudioTracks());
  265. const stream = event.streams[0];
  266. this.localStream = stream;
  267. const appStore = useAppStore();
  268. stream.onremovetrack = () => {
  269. this.prettierLog({ msg: 'onremovetrack事件', type: 'warn' });
  270. // const res = appStore.allTrack.filter((info) => {
  271. // if (info.track?.id === event.track.id) {
  272. // return false;
  273. // }
  274. // return true;
  275. // });
  276. // appStore.setAllTrack(res);
  277. };
  278. const addTrack: AppRootState['allTrack'] = [];
  279. this.localStream?.getVideoTracks().forEach((track) => {
  280. if (!appStore.allTrack.find((info) => info.track?.id === track.id)) {
  281. addTrack.push({
  282. openEye: true,
  283. id: getRandomString(8),
  284. track,
  285. stream,
  286. audio: 2,
  287. video: 1,
  288. type: MediaTypeEnum.screen,
  289. mediaName: '',
  290. streamid: stream.id,
  291. trackid: track.id,
  292. scaleInfo: {},
  293. });
  294. }
  295. });
  296. this.localStream?.getAudioTracks().forEach((track) => {
  297. if (!appStore.allTrack.find((info) => info.track?.id === track.id)) {
  298. addTrack.push({
  299. openEye: true,
  300. id: getRandomString(8),
  301. track,
  302. stream,
  303. audio: 1,
  304. video: 2,
  305. type: MediaTypeEnum.microphone,
  306. mediaName: '',
  307. streamid: stream.id,
  308. trackid: track.id,
  309. scaleInfo: {},
  310. });
  311. }
  312. });
  313. stream.getVideoTracks().forEach((track) => {
  314. if (!appStore.allTrack.find((info) => info.track?.id === track.id)) {
  315. addTrack.push({
  316. openEye: true,
  317. id: getRandomString(8),
  318. track,
  319. stream,
  320. audio: 2,
  321. video: 1,
  322. type: MediaTypeEnum.screen,
  323. mediaName: '',
  324. streamid: stream.id,
  325. trackid: track.id,
  326. scaleInfo: {},
  327. });
  328. }
  329. });
  330. stream.getAudioTracks().forEach((track) => {
  331. if (!appStore.allTrack.find((info) => info.track?.id === track.id)) {
  332. addTrack.push({
  333. openEye: true,
  334. id: getRandomString(8),
  335. track,
  336. stream,
  337. audio: 1,
  338. video: 2,
  339. type: MediaTypeEnum.microphone,
  340. mediaName: '',
  341. streamid: stream.id,
  342. trackid: track.id,
  343. scaleInfo: {},
  344. });
  345. }
  346. });
  347. this.videoEl.srcObject = event.streams[0];
  348. });
  349. };
  350. handleConnectionEvent = () => {
  351. if (!this.peerConnection) return;
  352. const appStore = useAppStore();
  353. this.prettierLog({ msg: '开始监听pc的icecandidate事件', type: 'warn' });
  354. this.peerConnection.addEventListener('icecandidate', (event) => {
  355. this.prettierLog({ msg: 'pc收到icecandidate', type: 'warn' });
  356. if (event.candidate) {
  357. const networkStore = useNetworkStore();
  358. networkStore.wsMap.get(this.roomId)?.send<WsCandidateType['data']>({
  359. requestId: getRandomString(8),
  360. msgType: this.isSRS
  361. ? WsMsgTypeEnum.srsCandidate
  362. : WsMsgTypeEnum.nativeWebRtcCandidate,
  363. data: {
  364. candidate: event.candidate,
  365. sender: this.sender,
  366. receiver: this.receiver,
  367. live_room_id: Number(this.roomId),
  368. },
  369. });
  370. } else {
  371. console.log('没有候选者了');
  372. }
  373. });
  374. this.prettierLog({
  375. msg: '开始监听pc的iceconnectionstatechange事件',
  376. type: 'warn',
  377. });
  378. this.peerConnection.addEventListener(
  379. 'iceconnectionstatechange',
  380. (event: any) => {
  381. this.prettierLog({
  382. msg: 'pc收到iceconnectionstatechange:connected',
  383. type: 'warn',
  384. });
  385. // https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/connectionState
  386. const iceConnectionState = event.currentTarget.iceConnectionState;
  387. if (iceConnectionState === 'connected') {
  388. // ICE 代理至少对每个候选发现了一个可用的连接,此时仍然会继续测试远程候选以便发现更优的连接。同时可能在继续收集候选。
  389. this.prettierLog({
  390. msg: 'iceConnectionState:connected',
  391. type: 'warn',
  392. });
  393. this.prettierLog({
  394. msg: 'webrtc连接成功!',
  395. type: 'success',
  396. });
  397. appStore.remoteDesk.isRemoteing = true;
  398. console.log('sender', this.sender, 'receiver', this.receiver);
  399. this.update();
  400. }
  401. if (iceConnectionState === 'completed') {
  402. // ICE 代理已经发现了可用的连接,不再测试远程候选。
  403. this.prettierLog({
  404. msg: 'iceConnectionState:completed',
  405. type: 'warn',
  406. });
  407. }
  408. if (iceConnectionState === 'failed') {
  409. // ICE 候选测试了所有远程候选没有发现匹配的候选。也可能有些候选中发现了一些可用连接。
  410. this.prettierLog({
  411. msg: 'iceConnectionState:failed',
  412. type: 'error',
  413. });
  414. this.close();
  415. }
  416. if (iceConnectionState === 'disconnected') {
  417. // 测试不再活跃,这可能是一个暂时的状态,可以自我恢复。
  418. this.prettierLog({
  419. msg: 'iceConnectionState:disconnected',
  420. type: 'error',
  421. });
  422. this.close();
  423. }
  424. if (iceConnectionState === 'closed') {
  425. // ICE 代理关闭,不再应答任何请求。
  426. this.prettierLog({
  427. msg: 'iceConnectionState:closed',
  428. type: 'error',
  429. });
  430. }
  431. }
  432. );
  433. this.prettierLog({
  434. msg: '开始监听pc的connectionstatechange事件',
  435. type: 'warn',
  436. });
  437. this.peerConnection.addEventListener(
  438. 'connectionstatechange',
  439. (event: any) => {
  440. const connectionState = event.currentTarget.connectionState;
  441. this.prettierLog({
  442. msg: 'pc收到connectionstatechange:connected',
  443. type: 'warn',
  444. });
  445. if (connectionState === 'connected') {
  446. // 表示每一个 ICE 连接要么正在使用(connected 或 completed 状态),要么已被关闭(closed 状态);并且,至少有一个连接处于 connected 或 completed 状态。
  447. this.prettierLog({
  448. msg: 'connectionState:connected',
  449. type: 'warn',
  450. });
  451. appStore.setLiveLine(LiveLineEnum.rtc);
  452. if (this.maxBitrate !== -1) {
  453. this.setMaxBitrate(this.maxBitrate);
  454. }
  455. if (this.maxFramerate !== -1) {
  456. this.setMaxFramerate(this.maxFramerate);
  457. }
  458. if (this.resolutionRatio !== -1) {
  459. this.setResolutionRatio(this.resolutionRatio);
  460. }
  461. }
  462. if (connectionState === 'disconnected') {
  463. // 表示至少有一个 ICE 连接处于 disconnected 状态,并且没有连接处于 failed、connecting 或 checking 状态。
  464. this.prettierLog({
  465. msg: 'connectionState:disconnected',
  466. type: 'error',
  467. });
  468. this.close();
  469. }
  470. if (connectionState === 'closed') {
  471. // 表示 RTCPeerConnection 已关闭。
  472. this.prettierLog({
  473. msg: 'connectionState:closed',
  474. type: 'error',
  475. });
  476. }
  477. if (connectionState === 'failed') {
  478. // 表示至少有一个 ICE 连接处于 failed 的状态。
  479. this.prettierLog({
  480. msg: 'connectionState:failed',
  481. type: 'error',
  482. });
  483. this.close();
  484. }
  485. }
  486. );
  487. this.prettierLog({
  488. msg: '开始监听pc的negotiationneeded事件',
  489. type: 'warn',
  490. });
  491. this.peerConnection.addEventListener('negotiationneeded', () => {
  492. this.prettierLog({
  493. msg: 'pc收到negotiationneeded',
  494. type: 'warn',
  495. });
  496. });
  497. };
  498. dataChannelSend = <T extends unknown>({
  499. // 写成<T extends unknown>而不是<T>是为了避免eslint将箭头函数的<T>后面的内容识别成jsx语法
  500. msgType,
  501. requestId,
  502. data,
  503. }: {
  504. msgType: WsMsgTypeEnum;
  505. requestId: string;
  506. data?: T;
  507. }) => {
  508. if (this.dataChannel?.readyState !== 'open') {
  509. console.error('dataChannel未连接成功,不发送消息!', msgType, data);
  510. return;
  511. }
  512. this.dataChannel.send(
  513. JSON.stringify({
  514. msgType,
  515. requestId,
  516. data,
  517. })
  518. );
  519. };
  520. /** 创建对等连接 */
  521. createPeerConnection = () => {
  522. if (!window.RTCPeerConnection) {
  523. console.error('当前环境不支持RTCPeerConnection!');
  524. alert('当前环境不支持RTCPeerConnection!');
  525. return;
  526. }
  527. if (!this.peerConnection) {
  528. const iceServers = this.isSRS
  529. ? []
  530. : [
  531. // {
  532. // urls: 'stun:stun.l.google.com:19302',
  533. // },
  534. {
  535. urls: `turn:hk.${prodDomain}`,
  536. username: 'hss',
  537. credential: '123456',
  538. },
  539. ];
  540. this.peerConnection = new RTCPeerConnection({
  541. iceServers,
  542. });
  543. if (!this.isSRS) {
  544. this.handleDataChannel();
  545. }
  546. this.handleStreamEvent();
  547. this.handleConnectionEvent();
  548. this.update();
  549. }
  550. };
  551. handleDataChannel = () => {
  552. if (!this.peerConnection) return;
  553. this.peerConnection.ondatachannel = (event) => {
  554. this.cbDataChannel = event.channel;
  555. this.update();
  556. };
  557. this.dataChannel = this.peerConnection.createDataChannel('MessageChannel', {
  558. // maxRetransmits,用户代理应尝试重新传输在不可靠模式下第一次失败的消息的最大次数。虽然该值是 16 位无符号数,但每个用户代理都可以将其限制为它认为合适的任何最大值。
  559. maxRetransmits: 3,
  560. // ordered,表示通过 RTCDataChannel 的信息的到达顺序需要和发送顺序一致 (true), 或者到达顺序不需要和发送顺序一致 (false). 默认:true
  561. ordered: false,
  562. protocol: 'udp',
  563. });
  564. this.dataChannel.onopen = () => {
  565. this.prettierLog({
  566. msg: 'dataChannel连接成功!',
  567. type: 'success',
  568. });
  569. };
  570. this.dataChannel.onerror = () => {
  571. this.prettierLog({
  572. msg: 'dataChannel连接失败!',
  573. type: 'error',
  574. });
  575. };
  576. };
  577. /** 手动关闭webrtc连接 */
  578. close = () => {
  579. try {
  580. this.prettierLog({ msg: '手动关闭webrtc连接', type: 'warn' });
  581. this.localStream?.getTracks().forEach((track) => {
  582. track.stop();
  583. });
  584. this.localStream = null;
  585. this.peerConnection?.close();
  586. this.peerConnection = null;
  587. this.dataChannel = null;
  588. this.videoEl.remove();
  589. const appStore = useAppStore();
  590. appStore.remoteDesk.isClose = true;
  591. appStore.remoteDesk.isRemoteing = false;
  592. appStore.remoteDesk.startRemoteDesk = false;
  593. } catch (error) {
  594. this.prettierLog({ msg: '手动关闭webrtc连接失败', type: 'error' });
  595. console.error(error);
  596. }
  597. };
  598. /** 更新store */
  599. update = () => {
  600. const networkStore = useNetworkStore();
  601. networkStore.updateRtcMap(this.receiver, this);
  602. };
  603. }