No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

223 líneas
6.9KB

  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. rpc.onconnectionstatechange = () => {
  87. if(rpc.connectionState === 'failed') {
  88. console.log(target, 'failed, retry!')
  89. wire({kind: 'peerInfo', value: {type: 'request'}, target})
  90. }
  91. }
  92. }
  93. dom.listener = async ({detail: {source, value}}) => {
  94. if(source !== target) {
  95. return
  96. }
  97. console.log(source, value.type)
  98. if(value.type === 'request') {
  99. resetRpc()
  100. const localOffer = await rpc.createOffer()
  101. await rpc.setLocalDescription(localOffer)
  102. wire({kind: 'peerInfo', value: localOffer, target})
  103. }
  104. else if(value.type === 'offer') {
  105. resetRpc()
  106. const remoteOffer = new RTCSessionDescription(value)
  107. await rpc.setRemoteDescription(remoteOffer)
  108. const localAnswer = await rpc.createAnswer()
  109. await rpc.setLocalDescription(localAnswer)
  110. wire({kind: 'peerInfo', value: localAnswer, target})
  111. }
  112. else if(value.type === 'answer') {
  113. const remoteAnswer = new RTCSessionDescription(value)
  114. await rpc.setRemoteDescription(remoteAnswer)
  115. }
  116. else if(value.type === 'candidate') {
  117. const candidate = new RTCIceCandidate(value.candidate)
  118. await rpc.addIceCandidate(candidate)
  119. }
  120. else if(value.type === 'stop') {
  121. stopRpc()
  122. }
  123. }
  124. addEventListener('peerInfo', dom.listener)
  125. }
  126. const Video = {
  127. setUp: (username) => ({dom}) => {
  128. dom.username = username
  129. dom.srcObject = new MediaStream()
  130. if(username === State.username) {
  131. dom.classList.add('self')
  132. dom.muted = true
  133. updateSelfVideo()
  134. }
  135. else {
  136. updateOtherVideo(username, dom)
  137. }
  138. },
  139. tearDown: (username) => ({dom}) => {
  140. removeEventListener('peerInfo', dom.listener)
  141. dom.srcObject.getTracks().forEach(track => {
  142. track.stop()
  143. dom.srcObject.removeTrack(track)
  144. delete track
  145. })
  146. },
  147. view({attrs: {username}}) {
  148. const styleOuter = {
  149. position: 'relative',
  150. display: 'block',
  151. color: 'white',
  152. overflow: 'hidden',
  153. }
  154. const styleMeta = {
  155. position: 'absolute',
  156. display: 'flex',
  157. alignItems: 'center',
  158. justifyContent: 'center',
  159. height: '100%',
  160. width: '100%',
  161. fontFamily: 'monospace',
  162. fontSize: 'xxx-large',
  163. }
  164. const styleVideo = {
  165. objectFit: Settings.get('blackBars') ? 'contain' : 'cover',
  166. width: '100%',
  167. height: '100%',
  168. transform: username === State.username ? 'scaleX(-1)' : 'scaleX(1)',
  169. }
  170. return m('.video-container', {style: styleOuter},
  171. m('.video-info', {style: styleMeta},
  172. m('.username', username),
  173. ),
  174. m('video[playsinline][autoplay]', {
  175. style: styleVideo,
  176. oncreate: this.setUp(username),
  177. onremove: this.tearDown(username),
  178. }),
  179. )
  180. },
  181. }
  182. const StreamContainer = {
  183. videoOn: true,
  184. audioOn: true,
  185. view() {
  186. const dims = [
  187. Math.floor((1 + Math.sqrt(4 * State.online.length - 3)) / 2),
  188. Math.ceil(Math.sqrt(State.online.length)),
  189. ].map(n => Array(n).fill('1fr').join(' '))
  190. if(innerHeight > innerWidth) dims.reverse()
  191. const style = {
  192. backgroundColor: 'black',
  193. height: '100%',
  194. overflow: 'hidden',
  195. display: 'grid',
  196. gridTemplateRows: dims[0],
  197. gridTemplateColumns: dims[1],
  198. }
  199. return m('.videos', {style},
  200. State.online.map(username => m(Video, {key: username, username}))
  201. )
  202. },
  203. }
  204. const signalPeerStop = (username) => {
  205. signal({kind: 'peerInfo', value: {type: 'stop'}, source: username})
  206. }
  207. addEventListener('pagehide', () => State.online.forEach(signalPeerStop))
  208. addEventListener('logout', () => State.online.forEach(signalPeerStop))
  209. addEventListener('load', () => {
  210. Headers.push([VideoConfig])
  211. Apps.push([StreamContainer, {key: 'stream-container'}])
  212. })