您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

190 行
6.0KB

  1. const VideoConfig = Object.seal({
  2. videoOn: true,
  3. audioOn: true,
  4. get video() {
  5. return VideoConfig.videoOn
  6. && {width: {ideal: 320}, facingMode: 'user', frameRate: 26}
  7. },
  8. get audio() {
  9. return VideoConfig.audioOn
  10. },
  11. toggle: (property) => () => {
  12. VideoConfig[property] = !VideoConfig[property]
  13. updateSelfVideo()
  14. }
  15. })
  16. const updateSelfVideo = async () => {
  17. const video = document.querySelector('video.self')
  18. video.srcObject.getTracks().forEach(track => {
  19. track.stop()
  20. video.srcObject.removeTrack(track)
  21. delete track
  22. })
  23. if(VideoConfig.videoOn || VideoConfig.audioOn) {
  24. await navigator.mediaDevices
  25. .getUserMedia(VideoConfig)
  26. .then(s => s.getTracks().forEach(t => video.srcObject.addTrack(t)))
  27. .catch(e => console.error(e))
  28. }
  29. video.srcObject = video.srcObject // safari
  30. wire({kind: 'peerInfo', value: {type: 'request'}})
  31. }
  32. const updateOtherVideo = (target, dom) => {
  33. dom.srcObject = new MediaStream()
  34. let rpc = null
  35. const rpcConfig = {iceServers: [{urls: 'stun:stun.sipgate.net:3478'}]}
  36. const stopRpc = () => {
  37. rpc && rpc.close()
  38. dom.srcObject.getTracks().forEach(track => {
  39. track.stop()
  40. dom.srcObject.removeTrack(track)
  41. delete track
  42. })
  43. }
  44. const resetRpc = () => {
  45. stopRpc()
  46. rpc = new RTCPeerConnection(rpcConfig)
  47. document.querySelector('video.self').srcObject.getTracks()
  48. .forEach(t => rpc.addTrack(t))
  49. rpc.onicecandidate = ({candidate}) => {
  50. if(candidate && candidate.candidate) {
  51. const value = {type: 'candidate', candidate}
  52. wire({kind: 'peerInfo', value, target})
  53. }
  54. }
  55. rpc.ontrack = ({track}) => {
  56. dom.srcObject.addTrack(track)
  57. dom.srcObject = dom.srcObject
  58. }
  59. }
  60. dom.listener = async ({detail: {source, value}}) => {
  61. if(source !== target) {
  62. return
  63. }
  64. console.log(source, value.type)
  65. if(value.type === 'request') {
  66. resetRpc()
  67. const localOffer = await rpc.createOffer()
  68. await rpc.setLocalDescription(localOffer)
  69. wire({kind: 'peerInfo', value: localOffer, target})
  70. }
  71. else if(value.type === 'offer') {
  72. resetRpc()
  73. const remoteOffer = new RTCSessionDescription(value)
  74. await rpc.setRemoteDescription(remoteOffer)
  75. const localAnswer = await rpc.createAnswer()
  76. await rpc.setLocalDescription(localAnswer)
  77. wire({kind: 'peerInfo', value: localAnswer, target})
  78. }
  79. else if(value.type === 'answer') {
  80. const remoteAnswer = new RTCSessionDescription(value)
  81. await rpc.setRemoteDescription(remoteAnswer)
  82. }
  83. else if(value.type === 'candidate') {
  84. const candidate = new RTCIceCandidate(value.candidate)
  85. await rpc.addIceCandidate(candidate)
  86. }
  87. else if(value.type === 'stop') {
  88. stopRpc()
  89. }
  90. }
  91. addEventListener('peerInfo', dom.listener)
  92. }
  93. const Video = {
  94. setUp: (username) => ({dom}) => {
  95. dom.srcObject = new MediaStream()
  96. if(username === State.username) {
  97. dom.classList.add('self')
  98. dom.muted = true
  99. updateSelfVideo()
  100. }
  101. else {
  102. updateOtherVideo(username, dom)
  103. }
  104. },
  105. tearDown: (username) => ({dom}) => {
  106. removeEventListener('peerInfo', dom.listener)
  107. dom.srcObject.getTracks().forEach(track => {
  108. track.stop()
  109. dom.srcObject.removeTrack(track)
  110. delete track
  111. })
  112. },
  113. view({attrs: {username}}) {
  114. const styleOuter = {
  115. position: 'relative',
  116. display: 'block',
  117. backgroundColor: 'black',
  118. color: 'white',
  119. overflow: 'hidden',
  120. }
  121. const styleInner = {
  122. objectFit: 'cover',
  123. width: '100%',
  124. height: '100%',
  125. transform: 'scaleX(-1)',
  126. }
  127. return m('.video-container', {key: username, style: styleOuter},
  128. m('.video-info', {style: {position: 'absolute', zIndex: 999}},
  129. m('span', {style: {padding: '5px'}}, username),
  130. ),
  131. m('video[playsinline][autoplay]', {
  132. style: styleInner,
  133. oncreate: this.setUp(username),
  134. onremove: this.tearDown(username),
  135. }),
  136. )
  137. },
  138. }
  139. const StreamContainer = {
  140. // screen.width, screen.height
  141. getColumns() {
  142. const n = State.online.length
  143. if(n > 4) return '1fr 1fr 1fr'
  144. if(n > 1) return '1fr 1fr'
  145. return '1fr'
  146. },
  147. getRows() {
  148. const n = State.online.length
  149. if(n > 6) return '1fr 1fr 1fr'
  150. if(n > 2) return '1fr 1fr'
  151. return '1fr'
  152. },
  153. view() {
  154. const dims = [StreamContainer.getRows(), StreamContainer.getColumns()]
  155. if(screen.height > screen.width) dims.reverse()
  156. const style = {
  157. display: 'grid',
  158. padding: '3px',
  159. gridGap: '3px',
  160. height: '90vh',
  161. gridTemplateRows: dims[0],
  162. gridTemplateColumns: dims[1],
  163. }
  164. return [
  165. m('span.video-controls',
  166. m('button', {onclick: VideoConfig.toggle('videoOn')}, 'video'),
  167. m('button', {onclick: VideoConfig.toggle('audioOn')}, 'audio'),
  168. ),
  169. m('.videos', {style},
  170. m(Video, {username: State.username}),
  171. State.online.filter(username => username != State.username)
  172. .map(username => m(Video, {username}))
  173. ),
  174. ]
  175. },
  176. }
  177. const signalPeerStop = (username) => signal({kind: 'peerInfo', value: {type: 'stop'}, source: username})
  178. addEventListener('pagehide', () => State.online.forEach(signalPeerStop))
  179. addEventListener('logout', () => State.online.forEach(signalPeerStop))