Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

211 Zeilen
6.5KB

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