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

pico.js 7.4KB

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