webRtc.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. import browserTool from 'browser-tool';
  2. import { useNetworkStore } from '@/store/network';
  3. import { wsMsgType } from './webSocket';
  4. function prettierInfo(
  5. str: string,
  6. data: {
  7. browser: string;
  8. },
  9. type?: 'log' | 'warn' | 'error',
  10. ...args
  11. ) {
  12. console[type || 'log'](
  13. `${new Date().toLocaleString()},${data.browser}浏览器,${str}`,
  14. ...args
  15. );
  16. }
  17. export const frontendErrorCode = {
  18. rtcStatusErr: {
  19. // rtcStatus没有100%
  20. code: 1001,
  21. msg: '连接错误,是否尝试刷新页面(不影响云手机内应用运行)',
  22. refresh: true,
  23. },
  24. streamStop: {
  25. // 视频流卡主
  26. code: 1002,
  27. msg: '网络似乎不稳定,建议更换网络或刷新页面(不影响云手机内应用运行)',
  28. refresh: true,
  29. },
  30. blackScreen: {
  31. // 视频流黑屏
  32. code: 1003,
  33. msg: '当前浏览器似乎不兼容,建议使用Google Chrome浏览器',
  34. refresh: false,
  35. },
  36. connectionStateFailed: {
  37. code: 1004,
  38. msg: '连接错误,是否尝试刷新页面(不影响云手机内应用运行)',
  39. refresh: true,
  40. },
  41. iceConnectionStateDisconnected: {
  42. code: 1005,
  43. msg: '连接错误,是否尝试刷新页面(不影响云手机内应用运行)',
  44. refresh: true,
  45. },
  46. getStatsErr: {
  47. code: 1006,
  48. msg: '连接错误,是否尝试刷新页面(不影响云手机内应用运行)',
  49. refresh: true,
  50. },
  51. connectLongTime: {
  52. // 5秒内没有收到loadedmetadata回调
  53. code: 1007,
  54. msg: '等待太久,是否刷新页面重试?',
  55. refresh: true,
  56. },
  57. };
  58. export class WebRTCClass {
  59. roomId = '-1';
  60. peerConnection: RTCPeerConnection | null = null;
  61. dataChannel: RTCDataChannel | null = null;
  62. candidateFlag = false;
  63. getStatsSetIntervalDelay = 1000;
  64. getStatsSetIntervalTimer;
  65. // getStatsSetIntervalDelay是1秒的话,forceINumsMax是3,就代表一直卡了3秒。
  66. forceINums = 0; // 发送forceI次数
  67. forceINumsMax = 3; // 最多发送几次forceI
  68. preFramesDecoded = -1; // 上一帧
  69. browser: {
  70. device: string;
  71. language: string;
  72. engine: string;
  73. browser: string;
  74. system: string;
  75. systemVersion: string;
  76. platform: string;
  77. isWebview: boolean;
  78. isBot: boolean;
  79. version: string;
  80. };
  81. rtcStatus = {
  82. joined: false, // true代表成功,false代表失败
  83. icecandidate: false, // true代表成功,false代表失败
  84. createOffer: false, // true代表成功,false代表失败
  85. setLocalDescription: false, // true代表成功,false代表失败
  86. answer: false, // true代表成功,false代表失败
  87. setRemoteDescription: false, // true代表成功,false代表失败
  88. addStream: false, // true代表成功,false代表失败
  89. loadstart: false, // true代表成功,false代表失败
  90. loadedmetadata: false, // true代表成功,false代表失败
  91. };
  92. localDescription: any;
  93. stream: any;
  94. constructor({ roomId }) {
  95. this.roomId = roomId;
  96. this.browser = browserTool();
  97. this.createPeerConnection();
  98. this.update();
  99. // this.handleWebRtcError();
  100. }
  101. myAddTrack = (track, stream) => {
  102. console.warn('myAddTrackmyAddTrack', track, stream);
  103. this.peerConnection?.addTrack(track, stream);
  104. };
  105. handleWebRtcError = () => {
  106. this.getStatsSetIntervalTimer = setInterval(() => {
  107. this.peerConnection
  108. ?.getStats()
  109. .then((res) => {
  110. let isBlack = false;
  111. let currFramesDecoded = -1;
  112. res.forEach((report: RTCInboundRtpStreamStats) => {
  113. // 不能结构report的值,不然如果卡主之后,report的值就一直都是
  114. // const { type, kind, framesDecoded } = report;
  115. const data = {
  116. type: report.type,
  117. kind: report.kind,
  118. framesDecoded: report.framesDecoded,
  119. decoderImplementation: report.decoderImplementation,
  120. isChrome: false,
  121. isSafari: false,
  122. other: '',
  123. };
  124. if (this.browser.browser === 'safari') {
  125. data.isSafari = true;
  126. } else if (this.browser.browser === 'chrome') {
  127. data.isChrome = true;
  128. } else {
  129. data.other = this.browser.browser;
  130. }
  131. const isStopFlag = this.getCurrentFramesDecoded(data);
  132. const isBlackFlag = this.isBlackScreen(data);
  133. if (isStopFlag !== false) {
  134. currFramesDecoded = isStopFlag;
  135. }
  136. if (isBlackFlag !== false) {
  137. isBlack = isBlackFlag;
  138. }
  139. });
  140. /** 处理视频流卡主 */
  141. const handleStreamStop = () => {
  142. // console.error(
  143. // `上一帧:${this.preFramesDecoded},当前帧:${currFramesDecoded},forceINums:${this.forceINums}`
  144. // );
  145. if (this.preFramesDecoded === currFramesDecoded) {
  146. if (this.forceINums >= this.forceINumsMax) {
  147. prettierInfo(
  148. '超过forceI次数,提示更换网络',
  149. { browser: this.browser.browser },
  150. 'warn'
  151. );
  152. this.forceINums = 0;
  153. } else {
  154. this.forceINums += 1;
  155. prettierInfo(
  156. `当前视频流卡主了,主动刷新云手机(${this.forceINums}/${this.forceINumsMax})`,
  157. { browser: this.browser.browser },
  158. 'warn'
  159. );
  160. }
  161. } else {
  162. this.forceINums = 0;
  163. // console.warn('视频流没有卡主');
  164. }
  165. this.preFramesDecoded = currFramesDecoded;
  166. };
  167. /** 处理黑屏 */
  168. const handleBlackScreen = () => {
  169. if (isBlack) {
  170. prettierInfo(
  171. '黑屏了',
  172. { browser: this.browser.browser },
  173. 'error'
  174. );
  175. }
  176. // else {
  177. // console.warn('没有黑屏');
  178. // }
  179. };
  180. /** 处理rtcStatus */
  181. const handleRtcStatus = () => {
  182. const res = this.rtcStatusIsOk();
  183. const length = Object.keys(res).length;
  184. if (length) {
  185. prettierInfo(
  186. `rtcStatus错误:${Object.keys(res).join()}`,
  187. { browser: this.browser.browser },
  188. 'error'
  189. );
  190. }
  191. // else {
  192. // console.warn('rtcStatus正常');
  193. // }
  194. };
  195. handleStreamStop();
  196. handleBlackScreen();
  197. handleRtcStatus();
  198. // if (!networkStore.errorCode.length) {
  199. // networkStore.setShowErrorModal(false);
  200. // }
  201. })
  202. .catch((err) => {
  203. console.error(new Date().toLocaleString(), 'getStatsErr', err);
  204. // networkStore.setErrorCode(frontendErrorCode.getStatsErr);
  205. // networkStore.setShowErrorModal(true);
  206. });
  207. }, this.getStatsSetIntervalDelay);
  208. };
  209. /** rtcStatus是否都是true了 */
  210. rtcStatusIsOk = () => {
  211. const res = {};
  212. const status = this.rtcStatus;
  213. Object.keys(status).forEach((key) => {
  214. if (!status[key]) {
  215. res[key] = false;
  216. }
  217. });
  218. return res;
  219. };
  220. /** 当前是否黑屏,true代表黑屏了,false代表没有黑屏 */
  221. isBlackScreen = ({
  222. type,
  223. kind,
  224. framesDecoded,
  225. decoderImplementation,
  226. isSafari = false,
  227. isChrome = false,
  228. }) => {
  229. // https://blog.csdn.net/weixin_44523653/article/details/127414387
  230. // console.warn(
  231. // // eslint-disable-next-line
  232. // `type:${type},kind:${kind},framesDecoded:${framesDecoded},decoderImplementation:${decoderImplementation}`
  233. // );
  234. if (isSafari) {
  235. if (
  236. type === 'inbound-rtp' &&
  237. kind === 'video' &&
  238. // framesDecoded等于0代表黑屏
  239. framesDecoded === 0
  240. ) {
  241. return true;
  242. }
  243. } else if (isChrome) {
  244. if (
  245. (type === 'track' || type === 'inbound-rtp') &&
  246. kind === 'video' &&
  247. // framesDecoded等于0代表黑屏
  248. framesDecoded === 0
  249. ) {
  250. return true;
  251. }
  252. } else {
  253. if (
  254. (type === 'track' || type === 'inbound-rtp') &&
  255. kind === 'video' &&
  256. // framesDecoded等于0代表黑屏
  257. framesDecoded === 0
  258. ) {
  259. // 安卓的qq浏览器适用这个黑屏判断
  260. return true;
  261. }
  262. }
  263. return false;
  264. };
  265. /** 获取当前帧 */
  266. getCurrentFramesDecoded = ({
  267. type,
  268. kind,
  269. framesDecoded = this.preFramesDecoded,
  270. isSafari = false,
  271. isChrome = false,
  272. }) => {
  273. if (isSafari) {
  274. if (type === 'inbound-rtp' && kind === 'video') {
  275. return framesDecoded;
  276. }
  277. } else if (isChrome) {
  278. if ((type === 'track' || type === 'inbound-rtp') && kind === 'video') {
  279. return framesDecoded;
  280. }
  281. } else {
  282. if ((type === 'track' || type === 'inbound-rtp') && kind === 'video') {
  283. // 安卓的qq浏览器适用这个卡屏判断
  284. return framesDecoded;
  285. }
  286. }
  287. return false;
  288. };
  289. // 创建offer
  290. createOffer = async () => {
  291. console.log('开始createOffer');
  292. if (!this.peerConnection) return;
  293. if (this.rtcStatus.createOffer) return this.localDescription;
  294. try {
  295. const description = await this.peerConnection.createOffer({
  296. iceRestart: true,
  297. offerToReceiveAudio: true,
  298. offerToReceiveVideo: true,
  299. });
  300. this.rtcStatus.createOffer = true;
  301. this.update();
  302. prettierInfo(
  303. 'createOffer成功',
  304. { browser: this.browser.browser },
  305. 'warn'
  306. );
  307. console.log('开始设置本地描述', description);
  308. await this.peerConnection.setLocalDescription(description);
  309. this.localDescription = description;
  310. this.rtcStatus.setLocalDescription = true;
  311. this.update();
  312. prettierInfo(
  313. 'setLocalDescription成功',
  314. { browser: this.browser.browser },
  315. 'warn'
  316. );
  317. return description;
  318. } catch (error) {
  319. prettierInfo(
  320. 'createOffer失败',
  321. { browser: this.browser.browser },
  322. 'error'
  323. );
  324. console.log(error);
  325. }
  326. };
  327. // 创建answer
  328. createAnswer = async () => {
  329. console.log('开始createAnswer');
  330. if (!this.peerConnection) return;
  331. try {
  332. const description = await this.peerConnection.createAnswer();
  333. this.update();
  334. prettierInfo(
  335. 'createAnswer成功',
  336. { browser: this.browser.browser },
  337. 'warn'
  338. );
  339. console.log('开始设置本地描述', description);
  340. await this.peerConnection.setLocalDescription(description);
  341. this.localDescription = description;
  342. this.rtcStatus.setLocalDescription = true;
  343. this.update();
  344. prettierInfo(
  345. 'setLocalDescription成功',
  346. { browser: this.browser.browser },
  347. 'warn'
  348. );
  349. return description;
  350. } catch (error) {
  351. prettierInfo(
  352. 'createAnswer失败',
  353. { browser: this.browser.browser },
  354. 'error'
  355. );
  356. console.log(error);
  357. }
  358. };
  359. // 设置远端描述
  360. setRemoteDescription = async (description) => {
  361. console.log('开始设置远端描述', description);
  362. if (!this.peerConnection) return;
  363. if (this.rtcStatus.setRemoteDescription) return;
  364. try {
  365. await this.peerConnection.setRemoteDescription(
  366. new RTCSessionDescription(description)
  367. );
  368. this.rtcStatus.setRemoteDescription = true;
  369. this.update();
  370. prettierInfo(
  371. 'setRemoteDescription成功',
  372. { browser: this.browser.browser },
  373. 'warn'
  374. );
  375. } catch (error) {
  376. console.error('设置远端描述错误', error);
  377. }
  378. };
  379. addStream = (stream) => {
  380. if (!this.peerConnection || this.rtcStatus.addStream) return;
  381. this.rtcStatus.addStream = true;
  382. this.stream = stream;
  383. console.log(stream, 22222);
  384. document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject = stream;
  385. prettierInfo('addStream成功', { browser: this.browser.browser }, 'warn');
  386. this.update();
  387. };
  388. // 创建连接
  389. createConnect = () => {
  390. if (!this.peerConnection) return;
  391. console.warn('createConnect');
  392. this.peerConnection.addEventListener('icecandidate', (event) => {
  393. this.rtcStatus.icecandidate = true;
  394. this.update();
  395. prettierInfo(
  396. 'pc收到icecandidate',
  397. { browser: this.browser.browser },
  398. 'warn'
  399. );
  400. if (event.candidate) {
  401. // if (this.candidateFlag) return;
  402. const networkStore = useNetworkStore();
  403. this.candidateFlag = true;
  404. console.log('准备发送candidate', event.candidate.candidate);
  405. const data = {
  406. socketId: networkStore.wsMap.get(this.roomId)?.socketIo?.id,
  407. roomId: this.roomId,
  408. candidate: event.candidate.candidate,
  409. sdpMid: event.candidate.sdpMid,
  410. sdpMLineIndex: event.candidate.sdpMLineIndex,
  411. };
  412. networkStore.wsMap
  413. .get(this.roomId)
  414. ?.socketIo?.emit(wsMsgType.candidate, data);
  415. this.update();
  416. }
  417. });
  418. console.warn('开始监听addstream');
  419. this.peerConnection.addEventListener('addstream', (event: any) => {
  420. console.log('pc收到addstream事件', event.stream);
  421. // document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject =
  422. // event.stream;
  423. // this.addStream(event.stream);
  424. });
  425. console.warn('开始监听ontrack');
  426. this.peerConnection.addEventListener('ontrack', (event: any) => {
  427. console.log('pc收到ontrack事件', event.stream);
  428. });
  429. console.warn('开始监听addtrack');
  430. this.peerConnection.addEventListener('addtrack', (event: any) => {
  431. console.log('pc收到addtrack事件', event.stream);
  432. });
  433. console.warn('开始监听track');
  434. this.peerConnection.addEventListener('track', (event: any) => {
  435. console.log('pc收到track事件', event);
  436. document.querySelector<HTMLVideoElement>('#localVideo')!.srcObject =
  437. event.streams[0];
  438. });
  439. // connectionstatechange
  440. this.peerConnection.addEventListener(
  441. 'connectionstatechange',
  442. (event: any) => {
  443. console.log('connectionstatechange', event);
  444. const connectionState = event.currentTarget.connectionState;
  445. const iceConnectionState = event.currentTarget.iceConnectionState;
  446. console.log(
  447. // eslint-disable-next-line
  448. `connectionState:${connectionState}, iceConnectionState:${iceConnectionState}`
  449. );
  450. if (connectionState === 'connected') {
  451. console.warn('connectionState:connected');
  452. }
  453. if (connectionState === 'failed') {
  454. // 失败
  455. console.error('connectionState:failed', event);
  456. }
  457. if (iceConnectionState === 'disconnected') {
  458. // 已断开,请重新连接
  459. console.error('iceConnectionState:disconnected', event);
  460. }
  461. }
  462. );
  463. };
  464. // 创建对等连接
  465. createPeerConnection() {
  466. if (!window.RTCPeerConnection) {
  467. console.error('当前环境不支持RTCPeerConnection!');
  468. alert('当前环境不支持RTCPeerConnection!');
  469. return;
  470. }
  471. if (!this.peerConnection) {
  472. this.peerConnection = new RTCPeerConnection({
  473. iceServers: [
  474. // {
  475. // urls: 'stun:stun.l.google.com:19302',
  476. // },
  477. {
  478. urls: 'turn:hsslive.cn:3478',
  479. username: 'hss',
  480. credential: '123456',
  481. },
  482. ],
  483. });
  484. // this.dataChannel =
  485. // this.peerConnection.createDataChannel('MessageChannel');
  486. // this.dataChannel.onopen = (event) => {
  487. // console.warn('dataChannel---onopen', event);
  488. // };
  489. // this.dataChannel.onerror = (event) => {
  490. // console.warn('dataChannel---onerror', event);
  491. // };
  492. // this.dataChannel.onmessage = (event) => {
  493. // console.log('dataChannel---onmessage', event);
  494. // };
  495. // this.peerConnection.addTransceiver('video', { direction: 'recvonly' });
  496. // this.peerConnection.addTransceiver('audio', { direction: 'recvonly' });
  497. this.createConnect();
  498. this.update();
  499. }
  500. }
  501. // 手动关闭webrtc连接
  502. close() {
  503. console.warn(`${new Date().toLocaleString()},手动关闭webrtc连接`);
  504. this.peerConnection?.close();
  505. this.dataChannel?.close();
  506. this.peerConnection = null;
  507. this.dataChannel = null;
  508. this.update();
  509. }
  510. // 更新store
  511. update = () => {
  512. const networkStore = useNetworkStore();
  513. networkStore.updateRtcMap(this.roomId, this);
  514. };
  515. }