Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

206 lines
6.4KB

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