You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

216 lines
6.7KB

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