選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

pico.js 7.0KB

5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
5年前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. const State = {
  2. websocket: null,
  3. online: [],
  4. posts: [],
  5. media: null,
  6. username: null,
  7. }
  8. /*
  9. *
  10. * SIGNALING
  11. *
  12. */
  13. const wire = (message) => State.websocket.send(JSON.stringify(message))
  14. const signal = (message) => dispatchEvent(new CustomEvent(message.kind, {detail: message}))
  15. const listen = (kind, handler) => addEventListener(kind, handler)
  16. listen('login', ({detail}) => State.username = detail.value)
  17. listen('post', ({detail}) => State.posts.push(detail))
  18. listen('peerInfo', (e) => onPeerInfo(e))
  19. const doNotLog = new Set(['login', 'post', 'peerInfo'])
  20. /*
  21. *
  22. * WEBRTC
  23. *
  24. */
  25. const rpcs = {}
  26. const streams = {}
  27. const getOrCreateRpc = (username) => {
  28. if(!rpcs[username] && State.media) {
  29. const rpc = new RTCPeerConnection({iceServers: [{urls: 'stun:stun.sipgate.net:3478'}]})
  30. State.media.getTracks().forEach(track => rpc.addTrack(track, State.media))
  31. rpc.onicecandidate = ({candidate}) => {
  32. if(candidate) {
  33. wire({kind: 'peerInfo', value: {type: 'candidate', candidate}})
  34. }
  35. }
  36. rpc.ontrack = (e) => {
  37. streams[username] = e.streams[0]
  38. m.redraw()
  39. }
  40. rpc.onclose = (e) => {
  41. console.log(username, e)
  42. }
  43. rpcs[username] = rpc
  44. }
  45. return rpcs[username]
  46. }
  47. const onPeerInfo = async ({detail: message}) => {
  48. if(State.username === message.source) {
  49. return
  50. }
  51. const rpc = getOrCreateRpc(message.source)
  52. if(rpc && message.value.type === 'request') {
  53. const localOffer = await rpc.createOffer()
  54. await rpc.setLocalDescription(localOffer)
  55. wire({kind: 'peerInfo', value: localOffer})
  56. }
  57. else if(rpc && message.value.type === 'offer') {
  58. const remoteOffer = new RTCSessionDescription(message.value)
  59. await rpc.setRemoteDescription(remoteOffer)
  60. const localAnswer = await rpc.createAnswer()
  61. await rpc.setLocalDescription(localAnswer)
  62. wire({kind: 'peerInfo', value: localAnswer})
  63. }
  64. else if(rpc && message.value.type === 'answer') {
  65. const remoteAnswer = new RTCSessionDescription(message.value)
  66. await rpc.setRemoteDescription(remoteAnswer)
  67. }
  68. else if(rpc && message.value.type === 'candidate') {
  69. const candidate = new RTCIceCandidate(message.value.candidate)
  70. rpc.addIceCandidate(candidate)
  71. }
  72. else if(message.value.type === 'stop') {
  73. if(streams[message.source]) {
  74. streams[message.source].getTracks().map(track => track.stop())
  75. delete streams[message.source]
  76. }
  77. if(rpcs[message.source]) {
  78. rpcs[message.source].close()
  79. delete rpcs[message.source]
  80. }
  81. }
  82. else {
  83. console.log('uncaught', message)
  84. }
  85. }
  86. /*
  87. *
  88. * GUI
  89. *
  90. */
  91. const autoFocus = (vnode) => {
  92. vnode.dom.focus()
  93. }
  94. const scrollIntoView = (vnode) => {
  95. vnode.dom.scrollIntoView()
  96. }
  97. const prettyTime = (ts) => {
  98. return ts.slice(11, 19)
  99. }
  100. const Video = {
  101. appendStream: (stream) => ({dom}) => {
  102. dom.autoplay = true
  103. dom.muted = true
  104. dom.srcObject = stream
  105. },
  106. view({attrs}) {
  107. return m('.video-container',
  108. m('.video-source', attrs.username),
  109. m('video', {playsinline: true, oncreate: Video.appendStream(attrs.stream)}),
  110. )
  111. },
  112. }
  113. const Media = {
  114. constraints: {audio: true, video: {width: {ideal: 320}, facingMode: 'user'}},
  115. turnOn: async () => {
  116. State.media = await navigator.mediaDevices.getUserMedia(Media.constraints)
  117. wire({kind: 'peerInfo', value: {type: 'request'}})
  118. m.redraw()
  119. },
  120. turnOff: () => {
  121. wire({kind: 'peerInfo', value: {type: 'stop'}})
  122. // signal internally
  123. Object.keys(rpcs).forEach(source => signal({kind: 'peerInfo', value: {type: 'stop'}, source}))
  124. State.media.getTracks().map(track => track.stop())
  125. delete State.media
  126. },
  127. view() {
  128. if(!State.media) {
  129. return m('.media',
  130. m('button', {onclick: Media.turnOn}, 'turn media on'),
  131. )
  132. }
  133. else {
  134. return m('.media',
  135. m('button', {onclick: Media.turnOff}, 'turn media off'),
  136. m('.videos',
  137. m(Video, {username: State.username, stream: State.media}),
  138. Object.entries(streams).map(([username, stream]) =>
  139. m(Video, {username, stream})
  140. ),
  141. ),
  142. )
  143. }
  144. }
  145. }
  146. const Login = {
  147. sendLogin: (e) => {
  148. e.preventDefault()
  149. const username = e.target.username.value
  150. localStorage.username = username
  151. connect(username)
  152. },
  153. sendLogout: (e) => {
  154. State.media && Media.turnOff()
  155. wire({kind: 'logout'})
  156. State.posts = []
  157. },
  158. view() {
  159. const attrs = {
  160. oncreate: autoFocus,
  161. name: 'username',
  162. autocomplete: 'off',
  163. value: localStorage.username,
  164. }
  165. return m('.login',
  166. m('form', {onsubmit: Login.sendLogin},
  167. m('input', attrs),
  168. m('button', 'Login'),
  169. ),
  170. State.kind === 'error' && m('.error', State.info),
  171. )
  172. },
  173. }
  174. const Chat = {
  175. sendPost: (e) => {
  176. e.preventDefault()
  177. const field = e.target.text
  178. if(field.value) {
  179. wire({kind: 'post', value: field.value})
  180. field.value = ''
  181. }
  182. },
  183. view() {
  184. return m('.chat',
  185. m('.posts',
  186. State.posts.map(post => m('.post', {oncreate: scrollIntoView},
  187. m('.ts', prettyTime(post.ts)),
  188. m('.source', post.source || '~'),
  189. m('.text', post.value),
  190. )),
  191. ),
  192. m('form.actions', {onsubmit: Chat.sendPost},
  193. m('input', {oncreate: autoFocus, name: 'text', autocomplete: 'off'}),
  194. m('button', 'Send'),
  195. ),
  196. m('.online',
  197. m('button', {onclick: Login.sendLogout}, 'Logout'),
  198. m('ul.user-list', State.online.map(username => m('li', username))),
  199. ),
  200. m(Media),
  201. )
  202. },
  203. }
  204. const Main = {
  205. view() {
  206. const connected = State.websocket && State.websocket.readyState === 1
  207. return connected ? m(Chat) : m(Login)
  208. },
  209. }
  210. m.mount(document.body, Main)
  211. /*
  212. *
  213. * WEBSOCKETS
  214. *
  215. */
  216. const connect = (username) => {
  217. const wsUrl = location.href.replace('http', 'ws')
  218. State.websocket = new WebSocket(wsUrl)
  219. State.websocket.onopen = (e) => {
  220. wire({kind: 'login', value: username})
  221. }
  222. State.websocket.onmessage = (e) => {
  223. const message = JSON.parse(e.data)
  224. if(message.online) {
  225. State.online = message.online
  226. }
  227. if(!doNotLog.has(message.kind)) {
  228. console.log(message)
  229. }
  230. signal(message)
  231. m.redraw()
  232. }
  233. State.websocket.onclose = (e) => {
  234. if(!e.wasClean) {
  235. setTimeout(connect, 1000, username)
  236. }
  237. m.redraw()
  238. }
  239. }
  240. if(localStorage.username) {
  241. connect(localStorage.username)
  242. }