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.

127 line
4.0KB

  1. addEventListener('rpc-new', ({detail}) => {
  2. const {uid, kind, target} = detail.value
  3. if(kind === 'video') {
  4. const sender = VideoShare.streams[State.username]
  5. const receiver = VideoShare.resetStream(target)
  6. signal({kind: 'rpc-setup', value: {uid, sender, receiver}})
  7. }
  8. })
  9. const Video = {
  10. view({attrs: {username}}) {
  11. const styleOuter = {
  12. position: 'relative',
  13. display: 'block',
  14. color: 'white',
  15. overflow: 'hidden',
  16. }
  17. const styleMeta = {
  18. position: 'absolute',
  19. display: 'flex',
  20. alignItems: 'center',
  21. justifyContent: 'center',
  22. height: '100%',
  23. width: '100%',
  24. fontFamily: 'monospace',
  25. fontSize: 'xxx-large',
  26. }
  27. const styleVideo = {
  28. objectFit: Settings.get('blackBars') ? 'contain' : 'cover',
  29. width: '100%',
  30. height: '100%',
  31. transform: username === State.username ? 'scaleX(-1)' : 'scaleX(1)',
  32. }
  33. return m('.video-container', {style: styleOuter},
  34. m('.video-info', {style: styleMeta},
  35. m('.username', username),
  36. ),
  37. m('video[playsinline][autoplay]', {
  38. style: styleVideo,
  39. srcObject: VideoShare.streams[username],
  40. oncreate: ({dom}) => {dom.muted = username === State.username},
  41. onremove: () => VideoShare.resetStream(username),
  42. }),
  43. )
  44. },
  45. }
  46. const Toggle = {
  47. view({attrs: {key, label}}) {
  48. const onclick = async () => {
  49. VideoShare[key] = !VideoShare[key]
  50. await VideoShare.getStream()
  51. State.others.forEach(target =>
  52. signal({kind: 'rpc-needed', value: {kind: 'video', target}})
  53. )
  54. }
  55. const checked = VideoShare[key]
  56. return m('label.styled',
  57. m('input[type=checkbox]', {onclick, checked}),
  58. label,
  59. )
  60. },
  61. }
  62. const VideoShareConfig = {
  63. get video() {
  64. return VideoShare.videoOn
  65. && State.online.length < 10
  66. && params.get('v') !== '0'
  67. && {width: {ideal: 320}, facingMode: 'user', frameRate: 26}
  68. },
  69. get audio() {
  70. return VideoShare.audioOn
  71. && params.get('a') !== '0'
  72. },
  73. view() {
  74. return [
  75. m(Toggle, {key: 'videoOn', label: 'video'}),
  76. m(Toggle, {key: 'audioOn', label: 'audio'}),
  77. ]
  78. },
  79. }
  80. const VideoShare = {
  81. videoOn: true,
  82. audioOn: true,
  83. streams: {},
  84. oncreate(e) {
  85. VideoShare.getStream()
  86. },
  87. resetStream(target) {
  88. if(VideoShare.streams[target]) {
  89. VideoShare.streams[target].getTracks().forEach(tr => tr.stop())
  90. }
  91. VideoShare.streams[target] = new MediaStream()
  92. return VideoShare.streams[target]
  93. },
  94. async getStream() {
  95. VideoShare.resetStream(State.username)
  96. await navigator.mediaDevices.getUserMedia(VideoShareConfig)
  97. .catch(error => console.error(error))
  98. .then(stream => {VideoShare.streams[State.username] = stream})
  99. m.redraw()
  100. State.others.forEach(target => {
  101. signal({kind: 'rpc-needed', value: {kind: 'video', target}})
  102. })
  103. },
  104. view() {
  105. const dims = [
  106. Math.floor((1 + Math.sqrt(4 * State.online.length - 3)) / 2),
  107. Math.ceil(Math.sqrt(State.online.length)),
  108. ].map(n => Array(n || 1).fill('1fr').join(' '))
  109. if(innerHeight > innerWidth) dims.reverse()
  110. const style = {
  111. backgroundColor: 'black',
  112. overflow: 'hidden',
  113. display: 'grid',
  114. gridTemplateRows: dims[0],
  115. gridTemplateColumns: dims[1],
  116. }
  117. return m('.videos', {style},
  118. State.online.map(username => m(Video, {key: username, username}))
  119. )
  120. },
  121. }
  122. addEventListener('load', () => {
  123. Headers.push([VideoShareConfig])
  124. Apps.push([VideoShare, {key: 'stream-container'}])
  125. })