webrtc.service.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import useStore from '@/store'
  2. /**
  3. * WebRTC服务
  4. */
  5. export default class WebRtcService {
  6. private ICE = [
  7. { urls: [ 'stun:caner.top:3478' ] },
  8. {
  9. urls: 'turn:caner.top:3478',
  10. username: 'admin',
  11. credential: '123456'
  12. },
  13. { urls: 'stun:stun.l.google.com:19302' }
  14. ]
  15. public Peer: RTCPeerConnection | null = null
  16. private audioTack: MediaStreamTrack | null = null
  17. private store = useStore()
  18. private lastStats: RTCStatsReport | null = null
  19. private sleep = (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms) })
  20. /**
  21. * 初始化
  22. * @param DOM HTMLVideoElement
  23. */
  24. initRTC(DOM: HTMLVideoElement) {
  25. try {
  26. if (this.Peer) this.distory()
  27. if (!DOM) { this.store.setMqttMessage({ type: 'WebRtcIntError' }); return }
  28. console.log('start initRTC')
  29. // create Peer
  30. this.Peer = new RTCPeerConnection({
  31. iceServers: this.ICE,
  32. bundlePolicy: 'max-bundle'
  33. })
  34. // listen state
  35. this.Peer.onicegatheringstatechange = () => {
  36. console.log('GatheringState: ', this.Peer?.iceGatheringState)
  37. if (this.Peer?.iceGatheringState === 'complete') {
  38. const answer = this.Peer?.localDescription
  39. this.store.setMqttMessage({ type: 'answer', data: answer?.toJSON() })
  40. }
  41. }
  42. // listen track
  43. this.Peer.ontrack = (evt) => {
  44. console.log('track', evt)
  45. // TODO safari 不支持opus ogg 音频编码
  46. DOM.srcObject = evt.streams[0]
  47. }
  48. // listen changestate·
  49. this.Peer.oniceconnectionstatechange = () => {
  50. const state = this.Peer?.iceConnectionState
  51. console.log('StateChange', state)
  52. // P2P 连接失败
  53. if (state === 'failed' || state === 'closed') {
  54. console.log('P2P通信失败')
  55. this.store.setMqttMessage({ type: 'WebRtcFailed' })
  56. }
  57. // ICE连接成功
  58. if (state === 'connected') {
  59. this.store.setMqttMessage({ type: 'WebRtcConnected', data: null })
  60. }
  61. }
  62. // get stats
  63. this.getStats()
  64. console.log('RTC success')
  65. } catch (error) {
  66. console.log('RTC 初始化失败', error)
  67. this.store.setMqttMessage({ type: 'WebRtcIntError' })
  68. }
  69. }
  70. /**
  71. * 远程静音
  72. * @param mute boolean
  73. */
  74. muteRemoteAudio(mute: boolean) {
  75. console.log('远程静音', mute)
  76. if (!this.audioTack) return
  77. this.audioTack!.enabled = mute
  78. }
  79. /**
  80. * 比特率|丢包率
  81. * @returns
  82. */
  83. async getStats() {
  84. if (this.Peer) {
  85. const stats = await this.Peer.getStats()
  86. let packetLoss = 0
  87. let bitrate = 0
  88. if (this.lastStats) {
  89. stats.forEach((report) => {
  90. if (report.type === 'inbound-rtp' && report.kind === 'video') {
  91. if (this.lastStats && this.lastStats.has(report.id)) {
  92. const lastReport = this.lastStats.get(report.id)
  93. const duration = (report.timestamp - lastReport.timestamp) / 1000
  94. bitrate = (report.bytesReceived - lastReport.bytesReceived) / duration / 1000
  95. const lostPackets = (report.packetsLost - lastReport.packetsLost)
  96. if ((report.packetsReceived - lastReport.packetsReceived + lostPackets) === 0) {
  97. packetLoss = 0
  98. } else {
  99. packetLoss = (lostPackets / (report.packetsReceived - lastReport.packetsReceived + lostPackets)) * 100
  100. }
  101. }
  102. }
  103. })
  104. }
  105. this.lastStats = stats
  106. this.store.setMqttMessage({ type: 'WebRtcStats', data: { packetLoss: Math.floor(packetLoss * 100) / 100, bitrate: Math.floor(bitrate * 100) / 100 } })
  107. } else {
  108. this.store.setMqttMessage({ type: 'WebRtcStats', data: { packetLoss: 0, bitrate: 0 } })
  109. }
  110. await this.sleep(1000)
  111. this.getStats()
  112. }
  113. distory() {
  114. this.Peer?.close()
  115. this.Peer = null
  116. this.audioTack = null
  117. }
  118. }