client.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. var pc = null;
  2. function negotiate() {
  3. pc.addTransceiver('video', { direction: 'recvonly' });
  4. pc.addTransceiver('audio', { direction: 'recvonly' });
  5. return pc.createOffer().then((offer) => {
  6. console.log('Created offer: type=' + offer.type + ', sdp_length=' + offer.sdp.length);
  7. return pc.setLocalDescription(offer).then(() => {
  8. console.log('Set local description complete, iceGatheringState=' + pc.iceGatheringState);
  9. });
  10. }).then(() => {
  11. // wait for ICE gathering to complete
  12. return new Promise((resolve, reject) => {
  13. if (pc.iceGatheringState === 'complete') {
  14. console.log('ICE gathering already complete');
  15. resolve();
  16. } else {
  17. const checkState = () => {
  18. if (pc.iceGatheringState === 'complete') {
  19. pc.removeEventListener('icegatheringstatechange', checkState);
  20. console.log('ICE gathering complete');
  21. resolve();
  22. }
  23. };
  24. pc.addEventListener('icegatheringstatechange', checkState);
  25. // 5秒超时
  26. setTimeout(() => {
  27. pc.removeEventListener('icegatheringstatechange', checkState);
  28. resolve(); // 继续执行,即使 ICE 汇集未完成
  29. }, 5000);
  30. }
  31. });
  32. }).then(() => {
  33. var offer = pc.localDescription;
  34. console.log('Sending offer: type=' + offer.type + ', sdp_length=' + offer.sdp.length);
  35. return fetch('/offer', {
  36. body: JSON.stringify({
  37. sdp: offer.sdp,
  38. type: offer.type,
  39. }),
  40. headers: {
  41. 'Content-Type': 'application/json'
  42. },
  43. method: 'POST'
  44. }).then((response) => {
  45. console.log('Received response status:', response.status);
  46. return response;
  47. });
  48. }).then((response) => {
  49. if (!response.ok) {
  50. throw new Error('服务器返回错误: ' + response.status);
  51. }
  52. return response.json();
  53. }).then((answer) => {
  54. console.log('Received answer: type=' + answer.type + ', sdp_length=' + answer.sdp.length);
  55. document.getElementById('sessionid').value = answer.sessionid
  56. return pc.setRemoteDescription(answer).then(() => {
  57. console.log('Successfully set remote description');
  58. });
  59. }).catch((e) => {
  60. console.error('Error in negotiate:', e);
  61. let errorMsg = '连接失败: ';
  62. if (e.name === 'InvalidStateError') {
  63. errorMsg += '连接状态异常,请刷新页面后重试';
  64. } else if (e.name === 'SyntaxError') {
  65. errorMsg += '服务器响应格式错误,请检查服务器状态';
  66. } else if (e.message && e.message.includes('state')) {
  67. errorMsg += '连接状态错误,请刷新页面后重试';
  68. } else if (e.message && e.message.includes('服务器返回错误')) {
  69. errorMsg += e.message + ',请检查后端日志';
  70. } else {
  71. errorMsg += e.message || e.toString();
  72. }
  73. alert(errorMsg);
  74. });
  75. }
  76. function start() {
  77. var config = {
  78. sdpSemantics: 'unified-plan'
  79. };
  80. // 使用多个 STUN 服务器(提高连接成功率)
  81. config.iceServers = [
  82. { urls: ['stun:stun.l.google.com:19302'] },
  83. { urls: ['stun:stun1.l.google.com:19302'] },
  84. { urls: ['stun:stun.services.mozilla.com'] },
  85. { urls: ['stun:stun.voip.blackberry.com:3478'] }
  86. ];
  87. pc = new RTCPeerConnection(config);
  88. // 添加连接状态日志
  89. pc.addEventListener('connectionstatechange', function() {
  90. console.log('PeerConnection state:', pc.connectionState);
  91. // 将日志发送到后端
  92. fetch('/log', {
  93. method: 'POST',
  94. headers: { 'Content-Type': 'application/json' },
  95. body: JSON.stringify({ type: 'webrtc', message: 'PeerConnection state: ' + pc.connectionState })
  96. }).catch(() => {});
  97. // 连接成功时的提示
  98. if (pc.connectionState === 'connected') {
  99. console.log('✅ WebRTC connection established successfully!');
  100. document.getElementById('startBtn').style.display = 'none';
  101. document.getElementById('stopBtn').style.display = 'inline-block';
  102. } else if (pc.connectionState === 'failed') {
  103. console.error('❌ WebRTC connection failed!');
  104. alert('WebRTC 连接失败,请检查网络连接或刷新页面重试');
  105. }
  106. });
  107. pc.addEventListener('iceconnectionstatechange', function() {
  108. console.log('ICE connection state:', pc.iceConnectionState);
  109. fetch('/log', {
  110. method: 'POST',
  111. headers: { 'Content-Type': 'application/json' },
  112. body: JSON.stringify({ type: 'webrtc', message: 'ICE connection state: ' + pc.iceConnectionState })
  113. }).catch(() => {});
  114. // ICE 连接超时处理
  115. if (pc.iceConnectionState === 'disconnected') {
  116. console.warn('⚠️ ICE disconnected, attempting to reconnect...');
  117. } else if (pc.iceConnectionState === 'failed') {
  118. console.error('❌ ICE connection failed!');
  119. alert('ICE 连接失败,可能是网络防火墙阻止了 WebRTC 连接');
  120. }
  121. });
  122. pc.addEventListener('icegatheringstatechange', function() {
  123. console.log('ICE gathering state:', pc.iceGatheringState);
  124. fetch('/log', {
  125. method: 'POST',
  126. headers: { 'Content-Type': 'application/json' },
  127. body: JSON.stringify({ type: 'webrtc', message: 'ICE gathering state: ' + pc.iceGatheringState })
  128. }).catch(() => {});
  129. });
  130. // 收集 ICE 候选
  131. pc.addEventListener('icecandidate', function(event) {
  132. if (event.candidate) {
  133. console.log('ICE candidate:', event.candidate.candidate);
  134. fetch('/log', {
  135. method: 'POST',
  136. headers: { 'Content-Type': 'application/json' },
  137. body: JSON.stringify({ type: 'webrtc', message: 'ICE candidate: ' + event.candidate.candidate })
  138. }).catch(() => {});
  139. } else {
  140. console.log('ICE gathering complete');
  141. fetch('/log', {
  142. method: 'POST',
  143. headers: { 'Content-Type': 'application/json' },
  144. body: JSON.stringify({ type: 'webrtc', message: 'ICE gathering complete' })
  145. }).catch(() => {});
  146. }
  147. });
  148. // connect audio / video
  149. pc.addEventListener('track', (evt) => {
  150. if (evt.track.kind == 'video') {
  151. const video = document.getElementById('video');
  152. if (video) {
  153. video.srcObject = evt.streams[0];
  154. console.log('Video track attached successfully');
  155. } else {
  156. console.error('Video element not found in DOM');
  157. }
  158. } else if (evt.track.kind == 'audio') {
  159. const audio = document.getElementById('audio');
  160. if (audio) {
  161. audio.srcObject = evt.streams[0];
  162. console.log('Audio track attached successfully');
  163. // 尝试播放音频(处理浏览器自动播放策略)
  164. audio.play().catch(e => {
  165. console.warn('Auto-play was prevented, user interaction needed:', e);
  166. });
  167. } else {
  168. console.warn('Audio element not found in DOM, but continuing with video only');
  169. }
  170. }
  171. });
  172. document.getElementById('startBtn').style.display = 'none';
  173. negotiate();
  174. document.getElementById('stopBtn').style.display = 'inline-block';
  175. }
  176. function stop() {
  177. document.getElementById('stopBtn').style.display = 'none';
  178. // close peer connection
  179. setTimeout(() => {
  180. pc.close();
  181. }, 500);
  182. }
  183. window.onunload = function(event) {
  184. // 在这里执行你想要的操作
  185. setTimeout(() => {
  186. pc.close();
  187. }, 500);
  188. };
  189. window.onbeforeunload = function (e) {
  190. setTimeout(() => {
  191. pc.close();
  192. }, 500);
  193. e = e || window.event
  194. // 兼容IE8和Firefox 4之前的版本
  195. if (e) {
  196. e.returnValue = '关闭提示'
  197. }
  198. // Chrome, Safari, Firefox 4+, Opera 12+ , IE 9+
  199. return '关闭提示'
  200. }