webRTC.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import { getRandomString } from 'billd-utils';
  2. import browserTool from 'browser-tool';
  3. import { ICandidate, MediaTypeEnum } from '@/interface';
  4. import { AppRootState } from '@/store/app';
  5. import { useAppCacheStore } from '@/store/cache';
  6. import { useNetworkStore } from '@/store/network';
  7. import { WsMsgTypeEnum } from './webSocket';
  8. export const audioElArr: HTMLVideoElement[] = [];
  9. export class WebRTCClass {
  10. roomId = '-1';
  11. receiver = '';
  12. videoEl: HTMLVideoElement;
  13. peerConnection: RTCPeerConnection | null = null;
  14. /** 最大码率 */
  15. maxBitrate = -1;
  16. /** 最大帧率 */
  17. maxFramerate = -1;
  18. /** 分辨率 */
  19. resolutionRatio = -1;
  20. localStream?: MediaStream;
  21. isSRS: boolean;
  22. browser: {
  23. device: string;
  24. language: string;
  25. engine: string;
  26. browser: string;
  27. system: string;
  28. systemVersion: string;
  29. platform: string;
  30. isWebview: boolean;
  31. isBot: boolean;
  32. version: string;
  33. };
  34. constructor(data: {
  35. roomId: string;
  36. videoEl: HTMLVideoElement;
  37. maxBitrate?: number;
  38. maxFramerate?: number;
  39. resolutionRatio?: number;
  40. isSRS: boolean;
  41. receiver: string;
  42. }) {
  43. this.roomId = data.roomId;
  44. this.videoEl = data.videoEl;
  45. this.receiver = data.receiver;
  46. if (data.maxBitrate) {
  47. this.maxBitrate = data.maxBitrate;
  48. }
  49. if (data.resolutionRatio) {
  50. this.resolutionRatio = data.resolutionRatio;
  51. }
  52. if (data.maxFramerate) {
  53. this.maxFramerate = data.maxFramerate;
  54. }
  55. this.isSRS = data.isSRS;
  56. console.warn('new webrtc参数:', data);
  57. this.browser = browserTool();
  58. this.createPeerConnection();
  59. }
  60. prettierLog = (msg: string, type?: 'log' | 'warn' | 'error', ...args) => {
  61. console[type || 'log'](
  62. `${new Date().toLocaleString()},${this.roomId},${
  63. this.browser.browser
  64. }浏览器,${msg}`,
  65. ...args
  66. );
  67. };
  68. addTrack = (stream: MediaStream, isCb?: boolean) => {
  69. console.log('开始addTrack,是否是pc的track回调', isCb);
  70. console.log('收到新track', stream);
  71. console.log('收到新track的视频轨', stream.getVideoTracks());
  72. console.log('收到新track的音频轨', stream.getAudioTracks());
  73. console.log('原本旧track的视频轨', this.localStream?.getVideoTracks());
  74. console.log('原本旧track的音频轨', this.localStream?.getAudioTracks());
  75. const appCacheStore = useAppCacheStore();
  76. if (isCb) {
  77. stream.onremovetrack = (event) => {
  78. console.log('onremovetrack事件', event);
  79. const res = appCacheStore.allTrack.filter((info) => {
  80. if (info.track?.id === event.track.id) {
  81. return false;
  82. }
  83. return true;
  84. });
  85. appCacheStore.setAllTrack(res);
  86. };
  87. }
  88. const addTrack: AppRootState['allTrack'] = [];
  89. this.localStream?.getVideoTracks().forEach((track) => {
  90. if (!appCacheStore.allTrack.find((info) => info.track?.id === track.id)) {
  91. addTrack.push({
  92. id: getRandomString(8),
  93. track,
  94. stream,
  95. audio: 2,
  96. video: 1,
  97. type: MediaTypeEnum.screen,
  98. mediaName: '',
  99. streamid: stream.id,
  100. trackid: track.id,
  101. });
  102. }
  103. });
  104. this.localStream?.getAudioTracks().forEach((track) => {
  105. if (!appCacheStore.allTrack.find((info) => info.track?.id === track.id)) {
  106. addTrack.push({
  107. id: getRandomString(8),
  108. track,
  109. stream,
  110. audio: 1,
  111. video: 2,
  112. type: MediaTypeEnum.microphone,
  113. mediaName: '',
  114. streamid: stream.id,
  115. trackid: track.id,
  116. });
  117. }
  118. });
  119. stream.getVideoTracks().forEach((track) => {
  120. if (!appCacheStore.allTrack.find((info) => info.track?.id === track.id)) {
  121. addTrack.push({
  122. id: getRandomString(8),
  123. track,
  124. stream,
  125. audio: 2,
  126. video: 1,
  127. type: MediaTypeEnum.screen,
  128. mediaName: '',
  129. streamid: stream.id,
  130. trackid: track.id,
  131. });
  132. }
  133. });
  134. stream.getAudioTracks().forEach((track) => {
  135. if (!appCacheStore.allTrack.find((info) => info.track?.id === track.id)) {
  136. addTrack.push({
  137. id: getRandomString(8),
  138. track,
  139. stream,
  140. audio: 1,
  141. video: 2,
  142. type: MediaTypeEnum.microphone,
  143. mediaName: '',
  144. streamid: stream.id,
  145. trackid: track.id,
  146. });
  147. }
  148. });
  149. if (addTrack.length) {
  150. appCacheStore.setAllTrack([...appCacheStore.allTrack, ...addTrack]);
  151. }
  152. this.localStream = stream;
  153. // if (this.maxBitrate !== -1) {
  154. // this.setMaxBitrate(this.maxBitrate);
  155. // }
  156. // if (this.maxFramerate !== -1) {
  157. // this.setMaxFramerate(this.maxFramerate);
  158. // }
  159. // if (this.resolutionRatio !== -1) {
  160. // this.setResolutionRatio(this.resolutionRatio);
  161. // }
  162. };
  163. /** 设置分辨率 */
  164. setResolutionRatio = (height: number) => {
  165. console.log('开始设置分辨率', height);
  166. console.log('旧的分辨率', this.resolutionRatio);
  167. return new Promise((resolve) => {
  168. this.localStream?.getTracks().forEach((track) => {
  169. if (track.kind === 'video') {
  170. console.log('设置分辨率ing', track.id);
  171. track
  172. .applyConstraints({
  173. height,
  174. })
  175. .then(() => {
  176. console.log('设置分辨率成功');
  177. this.resolutionRatio = height;
  178. resolve(1);
  179. })
  180. .catch((error) => {
  181. console.error('设置分辨率失败', height, error);
  182. resolve(0);
  183. });
  184. }
  185. });
  186. });
  187. };
  188. /** 设置最大帧率 */
  189. setMaxFramerate = (maxFramerate: number) => {
  190. console.log('开始设置最大帧率', maxFramerate);
  191. console.log('旧的最大帧率', this.maxFramerate);
  192. return new Promise<number>((resolve) => {
  193. this.peerConnection?.getSenders().forEach((sender) => {
  194. if (sender.track?.kind === 'video') {
  195. console.log('设置最大帧率ing', sender.track.id);
  196. const parameters = { ...sender.getParameters() };
  197. if (parameters.encodings[0]) {
  198. if (parameters.encodings[0].maxFramerate === maxFramerate) {
  199. console.log('最大帧率不变,不设置');
  200. resolve(1);
  201. return;
  202. }
  203. parameters.encodings[0].maxFramerate = maxFramerate;
  204. sender
  205. .setParameters(parameters)
  206. .then(() => {
  207. console.log('设置最大帧率成功', maxFramerate);
  208. this.maxFramerate = maxFramerate;
  209. resolve(1);
  210. })
  211. .catch((error) => {
  212. console.error('设置最大帧率失败', maxFramerate, error);
  213. resolve(0);
  214. });
  215. }
  216. }
  217. });
  218. });
  219. };
  220. /** 设置最大码率 */
  221. setMaxBitrate = (maxBitrate: number) => {
  222. console.log('开始设置最大码率', maxBitrate);
  223. console.log('旧的最大码率', this.maxBitrate);
  224. return new Promise<number>((resolve) => {
  225. this.peerConnection?.getSenders().forEach((sender) => {
  226. if (sender.track?.kind === 'video') {
  227. console.log('设置最大码率ing', sender.track.id);
  228. const parameters = { ...sender.getParameters() };
  229. if (parameters.encodings[0]) {
  230. const val = 1000 * maxBitrate;
  231. console.log(parameters.encodings[0].maxBitrate, val, 23223);
  232. if (parameters.encodings[0].maxBitrate === val) {
  233. console.log('最大码率不变,不设置');
  234. resolve(1);
  235. return;
  236. }
  237. parameters.encodings[0].maxBitrate = val;
  238. sender
  239. .setParameters(parameters)
  240. .then(() => {
  241. console.log('设置最大码率成功', maxBitrate);
  242. this.maxBitrate = val;
  243. resolve(1);
  244. })
  245. .catch((error) => {
  246. console.error('设置最大码率失败', maxBitrate, error);
  247. resolve(0);
  248. });
  249. }
  250. }
  251. });
  252. });
  253. };
  254. // 创建offer
  255. createOffer = async () => {
  256. if (!this.peerConnection) return;
  257. this.prettierLog('createOffer开始', 'warn');
  258. try {
  259. const description = await this.peerConnection.createOffer({
  260. iceRestart: true,
  261. });
  262. this.prettierLog('createOffer成功', 'warn', description);
  263. // const sdpStr = description.sdp;
  264. // const sdpObj = SDPTransform.parse(sdpStr);
  265. // // Get all m-lines
  266. // const mlines = sdpObj.media;
  267. // // Map to store unique m-lines
  268. // const mLineMap = new Map();
  269. // mlines.forEach((mLine) => {
  270. // const key = `${mLine.type}_${mLine.port}`;
  271. // if (!mLineMap.has(key)) {
  272. // mLineMap.set(key, mLine);
  273. // }
  274. // });
  275. // // Clear media array and only keep m-lines from map
  276. // sdpObj.media = [];
  277. // mLineMap.forEach((mLine) => {
  278. // sdpObj.media.push(mLine);
  279. // });
  280. // // Write new SDP string
  281. // const newSdpStr = SDPTransform.write(sdpObj);
  282. // console.log('old', description.sdp);
  283. // console.log('newSdpStr', newSdpStr);
  284. // Use new SDP string ...
  285. // description.sdp = newSdpStr;
  286. return description;
  287. } catch (error) {
  288. this.prettierLog('createOffer失败', 'error');
  289. console.log(error);
  290. }
  291. };
  292. // 创建answer
  293. createAnswer = async () => {
  294. if (!this.peerConnection) return;
  295. this.prettierLog('createAnswer开始', 'warn');
  296. try {
  297. const description = await this.peerConnection.createAnswer();
  298. this.prettierLog('createAnswer成功', 'warn', description);
  299. return description;
  300. } catch (error) {
  301. this.prettierLog('createAnswer失败', 'error');
  302. console.log(error);
  303. }
  304. };
  305. // 设置本地描述
  306. setLocalDescription = async (desc: RTCLocalSessionDescriptionInit) => {
  307. if (!this.peerConnection) return;
  308. this.prettierLog('setLocalDescription开始', 'warn');
  309. try {
  310. await this.peerConnection.setLocalDescription(desc);
  311. this.prettierLog('setLocalDescription成功', 'warn', desc);
  312. } catch (error) {
  313. this.prettierLog('setLocalDescription失败', 'error');
  314. console.error('setLocalDescription', desc);
  315. console.error(error);
  316. }
  317. };
  318. // 设置远端描述
  319. setRemoteDescription = async (desc: RTCSessionDescriptionInit) => {
  320. if (!this.peerConnection) return;
  321. this.prettierLog(`setRemoteDescription开始`, 'warn');
  322. try {
  323. await this.peerConnection.setRemoteDescription(desc);
  324. this.prettierLog('setRemoteDescription成功', 'warn', desc);
  325. } catch (error) {
  326. this.prettierLog('setRemoteDescription失败', 'error');
  327. console.error('setRemoteDescription', desc);
  328. console.error(error);
  329. }
  330. };
  331. handleStreamEvent = () => {
  332. if (!this.peerConnection) return;
  333. // 废弃:https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream
  334. console.warn(`${this.roomId},开始监听pc的addstream`);
  335. this.peerConnection.addEventListener('addstream', (event: any) => {
  336. // console.warn(`${this.roomId},pc收到addstream事件`, event);
  337. // console.log('addstream事件的stream', event.stream);
  338. // console.log('addstream事件的视频轨', event.stream.getVideoTracks());
  339. // console.log('addstream事件的音频轨', event.stream.getAudioTracks());
  340. // this.addTrack(event.stream, true);
  341. });
  342. console.warn(`${this.roomId},开始监听pc的track`);
  343. this.peerConnection.addEventListener('track', (event) => {
  344. console.warn(`${this.roomId},pc收到track事件`, event);
  345. console.log('track事件的stream', event.streams[0]);
  346. console.log('track事件的视频轨', event.streams[0].getVideoTracks());
  347. console.log('track事件的音频轨', event.streams[0].getAudioTracks());
  348. this.addTrack(event.streams[0], true);
  349. });
  350. };
  351. handleConnectionEvent = () => {
  352. if (!this.peerConnection) return;
  353. console.warn(`${this.roomId},开始监听pc的icecandidate`);
  354. // icecandidate
  355. this.peerConnection.addEventListener('icecandidate', (event) => {
  356. this.prettierLog('pc收到icecandidate', 'warn');
  357. if (event.candidate) {
  358. const networkStore = useNetworkStore();
  359. console.log('准备发送candidate', event.candidate.candidate);
  360. const roomId = this.roomId.split('___')[0];
  361. const receiver = this.roomId.split('___')[1];
  362. const data: ICandidate['data'] = {
  363. candidate: event.candidate.candidate,
  364. sdpMid: event.candidate.sdpMid,
  365. sdpMLineIndex: event.candidate.sdpMLineIndex,
  366. sender: networkStore.wsMap.get(roomId)?.socketIo?.id || '',
  367. receiver,
  368. live_room_id: Number(roomId),
  369. };
  370. networkStore.wsMap
  371. .get(roomId)
  372. ?.send({ msgType: WsMsgTypeEnum.candidate, data });
  373. } else {
  374. console.log('没有候选者了');
  375. }
  376. });
  377. console.warn(`${this.roomId},开始监听pc的iceconnectionstatechange`);
  378. // iceconnectionstatechange
  379. this.peerConnection.addEventListener(
  380. 'iceconnectionstatechange',
  381. (event: any) => {
  382. // https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/connectionState
  383. const iceConnectionState = event.currentTarget.iceConnectionState;
  384. console.log(
  385. this.roomId,
  386. 'pc收到iceconnectionstatechange',
  387. // eslint-disable-next-line
  388. `iceConnectionState:${iceConnectionState}`,
  389. event
  390. );
  391. if (iceConnectionState === 'connected') {
  392. // ICE 代理至少对每个候选发现了一个可用的连接,此时仍然会继续测试远程候选以便发现更优的连接。同时可能在继续收集候选。
  393. console.warn(this.roomId, 'iceConnectionState:connected', event);
  394. }
  395. if (iceConnectionState === 'completed') {
  396. // ICE 代理已经发现了可用的连接,不再测试远程候选。
  397. console.warn(this.roomId, 'iceConnectionState:completed', event);
  398. }
  399. if (iceConnectionState === 'failed') {
  400. // ICE 候选测试了所有远程候选没有发现匹配的候选。也可能有些候选中发现了一些可用连接。
  401. console.error(this.roomId, 'iceConnectionState:failed', event);
  402. }
  403. if (iceConnectionState === 'disconnected') {
  404. // 测试不再活跃,这可能是一个暂时的状态,可以自我恢复。
  405. console.error(this.roomId, 'iceConnectionState:disconnected', event);
  406. }
  407. if (iceConnectionState === 'closed') {
  408. // ICE 代理关闭,不再应答任何请求。
  409. console.error(this.roomId, 'iceConnectionState:closed', event);
  410. }
  411. }
  412. );
  413. console.warn(`${this.roomId},开始监听pc的connectionstatechange`);
  414. // connectionstatechange
  415. this.peerConnection.addEventListener(
  416. 'connectionstatechange',
  417. (event: any) => {
  418. const connectionState = event.currentTarget.connectionState;
  419. console.log(
  420. this.roomId,
  421. 'pc收到connectionstatechange',
  422. // eslint-disable-next-line
  423. `connectionState:${connectionState}`,
  424. event
  425. );
  426. if (connectionState === 'connected') {
  427. // 表示每一个 ICE 连接要么正在使用(connected 或 completed 状态),要么已被关闭(closed 状态);并且,至少有一个连接处于 connected 或 completed 状态。
  428. console.warn(this.roomId, 'connectionState:connected');
  429. // if (this.maxBitrate !== -1) {
  430. // this.setMaxBitrate(this.maxBitrate);
  431. // }
  432. // if (this.maxFramerate !== -1) {
  433. // this.setMaxFramerate(this.maxFramerate);
  434. // }
  435. // if (this.resolutionRatio !== -1) {
  436. // this.setResolutionRatio(this.resolutionRatio);
  437. // }
  438. }
  439. if (connectionState === 'disconnected') {
  440. // 表示至少有一个 ICE 连接处于 disconnected 状态,并且没有连接处于 failed、connecting 或 checking 状态。
  441. console.error(this.roomId, 'connectionState:disconnected');
  442. }
  443. if (connectionState === 'closed') {
  444. // 表示 RTCPeerConnection 已关闭。
  445. console.error(this.roomId, 'connectionState:closed');
  446. }
  447. if (connectionState === 'failed') {
  448. // 表示至少有一个 ICE 连接处于 failed 的状态。
  449. console.error(this.roomId, 'connectionState:failed');
  450. }
  451. }
  452. );
  453. };
  454. // 创建对等连接
  455. createPeerConnection = () => {
  456. if (!window.RTCPeerConnection) {
  457. console.error('当前环境不支持RTCPeerConnection!');
  458. alert('当前环境不支持RTCPeerConnection!');
  459. return;
  460. }
  461. if (!this.peerConnection) {
  462. const iceServers = this.isSRS
  463. ? []
  464. : [
  465. // {
  466. // urls: 'stun:stun.l.google.com:19302',
  467. // },
  468. {
  469. urls: 'turn:hsslive.cn:3478',
  470. username: 'hss',
  471. credential: '123456',
  472. },
  473. ];
  474. this.peerConnection = new RTCPeerConnection({
  475. iceServers,
  476. });
  477. this.handleStreamEvent();
  478. this.handleConnectionEvent();
  479. this.update();
  480. }
  481. };
  482. // 手动关闭webrtc连接
  483. close = () => {
  484. console.warn(`${new Date().toLocaleString()},手动关闭webrtc连接`);
  485. this.peerConnection?.getSenders().forEach((sender) => {
  486. this.peerConnection?.removeTrack(sender);
  487. });
  488. this.peerConnection?.close();
  489. this.peerConnection = null;
  490. };
  491. // 更新store
  492. update = () => {
  493. const networkStore = useNetworkStore();
  494. networkStore.updateRtcMap(this.roomId, this);
  495. };
  496. }