Compare commits
20 Commits
1110cb0dfb
...
4a53a2fb55
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a53a2fb55 | |||
| 8ebd92c53d | |||
| 5b431afbc2 | |||
| e28af6740e | |||
| 478df8c8fd | |||
| e851181ffa | |||
| 0ae475f898 | |||
| 80ac818616 | |||
| 720e26a5dd | |||
| 9db349d4a5 | |||
| 6effc10d9e | |||
| 8c0377bdaf | |||
| 46a0b1f7ea | |||
| f056048d62 | |||
| 53ddcdfcbf | |||
| 2f582e66b9 | |||
| 18ff3947b8 | |||
| 61fc53bcf9 | |||
| ee209d7889 | |||
| 5ebd5c07c0 |
6331
package-lock.json
generated
6331
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "peer-calls",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.5",
|
||||
"description": "Group peer to peer video calls for anybody.",
|
||||
"repository": "https://github.com/jeremija/peer-calls",
|
||||
"main": "lib/index.js",
|
||||
@ -61,11 +61,12 @@
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/polyfill": "^7.7.0",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/ejs": "^2.6.3",
|
||||
"@types/express": "^4.17.2",
|
||||
"@types/jest": "^24.0.23",
|
||||
"@types/jest": "^25.1.0",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
"@types/lodash": "^4.14.148",
|
||||
"@types/node": "^12.12.8",
|
||||
@ -81,19 +82,19 @@
|
||||
"@types/uuid": "^3.4.6",
|
||||
"@typescript-eslint/eslint-plugin": "^2.7.0",
|
||||
"@typescript-eslint/parser": "^2.7.0",
|
||||
"acorn": "^7.1.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-minify": "^0.5.1",
|
||||
"babelify": "^10.0.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"chastifol": "^4.1.0",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.4.1",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-react": "^7.16.0",
|
||||
"jest": "^24.9.0",
|
||||
"jest": "^25.1.0",
|
||||
"loose-envify": "^1.4.0",
|
||||
"node-sass": "^4.13.0",
|
||||
"node-sass": "4.13.1",
|
||||
"nodemon": "^1.19.4",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
@ -107,7 +108,7 @@
|
||||
"simple-peer": "^9.6.2",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"supertest": "^4.0.2",
|
||||
"ts-jest": "^24.1.0",
|
||||
"ts-jest": "^25.1.0",
|
||||
"ts-node": "^8.5.2",
|
||||
"tsify": "^4.0.1",
|
||||
"typescript": "^3.7.2",
|
||||
|
||||
Binary file not shown.
@ -7,19 +7,20 @@
|
||||
<font-face units-per-em="1024" ascent="870.850439882698" descent="-153.14956011730206" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="0" d="" />
|
||||
<glyph unicode="" glyph-name="mic" d="M738.721 376.857h72.070c0-146.143-116.113-266.26-256.25-286.28v-140.137h-84.082v140.137c-140.137 20.020-256.25 140.137-256.25 286.28h72.070c0-128.125 108.106-216.211 226.221-216.211s226.221 88.086 226.221 216.211zM512.5 248.732c-70.068 0-128.125 58.057-128.125 128.125v256.25c0 70.068 58.057 128.125 128.125 128.125s128.125-58.057 128.125-128.125v-256.25c0-70.068-58.057-128.125-128.125-128.125z" />
|
||||
<glyph unicode="" glyph-name="mic_off" d="M182.178 719.192l714.698-714.698-54.053-54.053-178.174 178.174c-32.031-20.020-72.070-32.031-110.108-38.037v-140.137h-84.082v140.137c-140.137 20.020-256.25 140.137-256.25 286.28h72.070c0-128.125 108.106-216.211 226.221-216.211 34.033 0 68.066 8.008 98.096 22.022l-70.068 70.068c-8.008-2.002-18.018-4.004-28.027-4.004-70.068 0-128.125 58.057-128.125 128.125v32.031l-256.25 256.25zM640.626 370.851l-256.25 254.248v8.008c0 70.068 58.057 128.125 128.125 128.125s128.125-58.057 128.125-128.125v-262.256zM810.792 376.857c0-50.049-14.014-98.096-38.037-140.137l-52.051 54.053c12.012 26.025 18.018 54.053 18.018 86.084h72.070z" />
|
||||
<glyph unicode="" glyph-name="videocam" d="M726.71 398.879l170.166 170.166v-468.457l-170.166 170.166v-150.147c0-24.023-20.020-42.041-44.043-42.041h-512.5c-24.023 0-42.041 18.018-42.041 42.041v428.418c0 24.023 18.018 42.041 42.041 42.041h512.5c24.023 0 44.043-18.018 44.043-42.041v-150.147z" />
|
||||
<glyph unicode="" glyph-name="videocam_off" d="M140.137 761.233l756.739-756.739-54.053-54.053-136.133 136.133c-6.006-4.004-16.016-8.008-24.023-8.008h-512.5c-24.023 0-42.041 18.018-42.041 42.041v428.418c0 24.023 18.018 42.041 42.041 42.041h32.031l-116.113 116.113zM896.876 569.045v-456.446l-478.467 478.467h264.258c24.023 0 44.043-18.018 44.043-42.041v-150.147z" />
|
||||
<glyph unicode="" glyph-name="call_end" d="M512.5 462.941c-68.066 0-134.131-10.010-196.192-30.029v-132.129c0-16.016-10.010-34.033-24.023-40.039-42.041-20.020-80.078-46.045-114.111-78.076-8.008-8.008-18.018-12.012-30.029-12.012s-22.022 4.004-30.029 12.012l-106.104 106.104c-8.008 8.008-12.012 18.018-12.012 30.029s4.004 22.022 12.012 30.029c130.127 124.121 306.299 200.196 500.489 200.196s370.362-76.074 500.489-200.196c8.008-8.008 12.012-18.018 12.012-30.029s-4.004-22.022-12.012-30.029l-106.104-106.104c-8.008-8.008-18.018-12.012-30.029-12.012s-22.022 4.004-30.029 12.012c-34.033 32.031-72.070 58.057-114.111 78.076-14.014 6.006-24.023 20.020-24.023 38.037v132.129c-62.061 20.020-128.125 32.031-196.192 32.031z" />
|
||||
<glyph unicode="" glyph-name="arrow_forward" d="M512.5 677.151l342.334-342.334-342.334-342.334-60.059 60.059 238.233 240.235h-520.508v84.082h520.508l-238.233 240.235z" />
|
||||
<glyph unicode="" glyph-name="fullscreen" d="M598.585 633.108h212.207v-212.207h-84.082v128.125h-128.125v84.082zM726.71 120.607v128.125h84.082v-212.207h-212.207v84.082h128.125zM214.209 420.9v212.207h212.207v-84.082h-128.125v-128.125h-84.082zM298.291 248.732v-128.125h128.125v-84.082h-212.207v212.207h84.082z" />
|
||||
<glyph unicode="" glyph-name="fullscreen_exit" d="M682.667 504.982h128.125v-84.082h-212.207v212.207h84.082v-128.125zM598.585 36.525v212.207h212.207v-84.082h-128.125v-128.125h-84.082zM342.334 504.982v128.125h84.082v-212.207h-212.207v84.082h128.125zM214.209 164.65v84.082h212.207v-212.207h-84.082v128.125h-128.125z" />
|
||||
<glyph unicode="" glyph-name="more_vert" d="M512.5 164.65c46.045 0 86.084-40.039 86.084-86.084s-40.039-86.084-86.084-86.084-86.084 40.039-86.084 86.084 40.039 86.084 86.084 86.084zM512.5 420.9c46.045 0 86.084-40.039 86.084-86.084s-40.039-86.084-86.084-86.084-86.084 40.039-86.084 86.084 40.039 86.084 86.084 86.084zM512.5 504.982c-46.045 0-86.084 40.039-86.084 86.084s40.039 86.084 86.084 86.084 86.084-40.039 86.084-86.084-40.039-86.084-86.084-86.084z" />
|
||||
<glyph unicode="" glyph-name="sentiment_satisfied" d="M512.5 164.65c64.063 0 118.115 34.033 148.145 84.082h70.068c-34.033-88.086-118.115-148.145-218.213-148.145s-184.18 60.059-218.213 148.145h70.068c30.029-50.049 84.082-84.082 148.145-84.082zM512.5-7.518c188.184 0 342.334 154.151 342.334 342.334s-154.151 342.334-342.334 342.334-342.334-154.151-342.334-342.334 154.151-342.334 342.334-342.334zM512.5 761.233c236.231 0 426.416-190.186 426.416-426.416s-190.186-426.416-426.416-426.416-426.416 190.186-426.416 426.416 190.186 426.416 426.416 426.416zM298.291 440.92c0 36.035 28.027 64.063 64.063 64.063s64.063-28.027 64.063-64.063-28.027-64.063-64.063-64.063-64.063 28.027-64.063 64.063zM598.585 440.92c0 36.035 28.027 64.063 64.063 64.063s64.063-28.027 64.063-64.063-28.027-64.063-64.063-64.063-64.063 28.027-64.063 64.063z" />
|
||||
<glyph unicode="" glyph-name="face" d="M512.5-7.518c188.184 0 342.334 154.151 342.334 342.334 0 34.033-6.006 66.065-14.014 96.094-30.029-8.008-62.061-10.010-96.094-10.010-144.141 0-270.264 70.068-348.34 180.176-42.041-102.1-124.121-186.182-224.219-230.225-2.002-12.012-2.002-24.023-2.002-36.035 0-188.184 154.151-342.334 342.334-342.334zM512.5 761.233c236.231 0 426.416-190.186 426.416-426.416s-190.186-426.416-426.416-426.416-426.416 190.186-426.416 426.416 190.186 426.416 426.416 426.416zM640.626 344.826c30.029 0 54.053-22.022 54.053-52.051s-24.023-54.053-54.053-54.053-54.053 24.023-54.053 54.053 24.023 52.051 54.053 52.051zM384.375 344.826c30.029 0 54.053-22.022 54.053-52.051s-24.023-54.053-54.053-54.053-54.053 24.023-54.053 54.053 24.023 52.051 54.053 52.051z" />
|
||||
<glyph unicode="" glyph-name="question_answer" d="M726.71 334.816c0-24.023-20.020-42.041-44.043-42.041h-426.416l-170.166-172.168v598.585c0 24.023 18.018 42.041 42.041 42.041h554.542c24.023 0 44.043-18.018 44.043-42.041v-384.375zM896.876 591.066c24.023 0 42.041-18.018 42.041-42.041v-640.626l-170.166 170.166h-470.459c-24.023 0-42.041 18.018-42.041 42.041v86.084h554.542v384.375h86.084z" />
|
||||
<glyph unicode="" glyph-name="room" d="M512.5 356.838c58.057 0 106.104 48.047 106.104 106.104s-48.047 106.104-106.104 106.104-106.104-48.047-106.104-106.104 48.047-106.104 106.104-106.104zM512.5 761.233c166.162 0 298.291-132.129 298.291-298.291 0-224.219-298.291-554.542-298.291-554.542s-298.291 330.323-298.291 554.542c0 166.162 132.129 298.291 298.291 298.291z" />
|
||||
<glyph unicode="" glyph-name="schedule" d="M534.522 549.025v-224.219l192.188-114.111-32.031-54.053-224.219 136.133v256.25h64.063zM512.5-7.518c188.184 0 342.334 154.151 342.334 342.334s-154.151 342.334-342.334 342.334-342.334-154.151-342.334-342.334 154.151-342.334 342.334-342.334zM512.5 761.233c236.231 0 426.416-190.186 426.416-426.416s-190.186-426.416-426.416-426.416-426.416 190.186-426.416 426.416 190.186 426.416 426.416 426.416z" />
|
||||
<glyph unicode="" glyph-name="file-text2" d="M917.806 666.924c-22.212 30.292-53.174 65.7-87.178 99.704s-69.412 64.964-99.704 87.178c-51.574 37.82-76.592 42.194-90.924 42.194h-496c-44.112 0-80-35.888-80-80v-864c0-44.112 35.888-80 80-80h736c44.112 0 80 35.888 80 80v624c0 14.332-4.372 39.35-42.194 90.924zM785.374 721.374c30.7-30.7 54.8-58.398 72.58-81.374h-153.954v153.946c22.984-17.78 50.678-41.878 81.374-72.572zM896-48c0-8.672-7.328-16-16-16h-736c-8.672 0-16 7.328-16 16v864c0 8.672 7.328 16 16 16 0 0 495.956 0.002 496 0v-224c0-17.672 14.326-32 32-32h224v-624zM736 64h-448c-17.672 0-32 14.326-32 32s14.328 32 32 32h448c17.674 0 32-14.326 32-32s-14.326-32-32-32zM736 192h-448c-17.672 0-32 14.326-32 32s14.328 32 32 32h448c17.674 0 32-14.326 32-32s-14.326-32-32-32zM736 320h-448c-17.672 0-32 14.326-32 32s14.328 32 32 32h448c17.674 0 32-14.326 32-32s-14.326-32-32-32z" />
|
||||
<glyph unicode="" glyph-name="mic" d="M739.443 370.375h72.14c0-146.286-116.227-266.52-256.5-286.56v-140.274h-84.164v140.274c-140.274 20.040-256.5 140.274-256.5 286.56h72.14c0-128.25 108.212-216.422 226.442-216.422s226.442 88.172 226.442 216.422zM513.001 242.125c-70.136 0-128.25 58.114-128.25 128.25v256.5c0 70.136 58.114 128.25 128.25 128.25s128.25-58.114 128.25-128.25v-256.5c0-70.136-58.114-128.25-128.25-128.25z" />
|
||||
<glyph unicode="" glyph-name="mic_off" d="M182.356 713.045l715.397-715.397-54.106-54.106-178.348 178.348c-32.062-20.040-72.14-32.062-110.216-38.074v-140.274h-84.164v140.274c-140.274 20.040-256.5 140.274-256.5 286.56h72.14c0-128.25 108.212-216.422 226.442-216.422 34.066 0 68.133 8.016 98.192 22.044l-70.136 70.136c-8.016-2.004-18.036-4.008-28.054-4.008-70.136 0-128.25 58.114-128.25 128.25v32.062l-256.5 256.5zM641.252 364.363l-256.5 254.497v8.016c0 70.136 58.114 128.25 128.25 128.25s128.25-58.114 128.25-128.25v-262.512zM811.585 370.375c0-50.098-14.028-98.192-38.074-140.274l-52.102 54.106c12.024 26.050 18.036 54.106 18.036 86.168h72.14z" />
|
||||
<glyph unicode="" glyph-name="videocam" d="M727.42 392.418l170.332 170.332v-468.915l-170.332 170.332v-150.294c0-24.046-20.040-42.082-44.086-42.082h-513.001c-24.046 0-42.082 18.036-42.082 42.082v428.837c0 24.046 18.036 42.082 42.082 42.082h513.001c24.046 0 44.086-18.036 44.086-42.082v-150.294z" />
|
||||
<glyph unicode="" glyph-name="videocam_off" d="M140.274 755.127l757.479-757.479-54.106-54.106-136.266 136.266c-6.012-4.008-16.032-8.016-24.046-8.016h-513.001c-24.046 0-42.082 18.036-42.082 42.082v428.837c0 24.046 18.036 42.082 42.082 42.082h32.062l-116.227 116.227zM897.753 562.751v-456.892l-478.935 478.935h264.516c24.046 0 44.086-18.036 44.086-42.082v-150.294z" />
|
||||
<glyph unicode="" glyph-name="call_end" d="M513.001 456.543c-68.133 0-134.262-10.020-196.384-30.058v-132.258c0-16.032-10.020-34.066-24.046-40.078-42.082-20.040-80.156-46.090-114.223-78.152-8.016-8.016-18.036-12.024-30.058-12.024s-22.044 4.008-30.058 12.024l-106.208 106.208c-8.016 8.016-12.024 18.036-12.024 30.058s4.008 22.044 12.024 30.058c130.254 124.242 306.598 200.392 500.978 200.392s370.724-76.148 500.978-200.392c8.016-8.016 12.024-18.036 12.024-30.058s-4.008-22.044-12.024-30.058l-106.208-106.208c-8.016-8.016-18.036-12.024-30.058-12.024s-22.044 4.008-30.058 12.024c-34.066 32.062-72.14 58.114-114.223 78.152-14.028 6.012-24.046 20.040-24.046 38.074v132.258c-62.122 20.040-128.25 32.062-196.384 32.062z" />
|
||||
<glyph unicode="" glyph-name="arrow_forward" d="M513.001 670.962l342.669-342.669-342.669-342.669-60.118 60.118 238.466 240.47h-521.017v84.164h521.017l-238.466 240.47z" />
|
||||
<glyph unicode="" glyph-name="fullscreen" d="M599.17 626.876h212.414v-212.414h-84.164v128.25h-128.25v84.164zM727.42 113.874v128.25h84.164v-212.414h-212.414v84.164h128.25zM214.418 414.461v212.414h212.414v-84.164h-128.25v-128.25h-84.164zM298.583 242.125v-128.25h128.25v-84.164h-212.414v212.414h84.164z" />
|
||||
<glyph unicode="" glyph-name="fullscreen_exit" d="M683.334 498.625h128.25v-84.164h-212.414v212.414h84.164v-128.25zM599.17 29.71v212.414h212.414v-84.164h-128.25v-128.25h-84.164zM342.669 498.625v128.25h84.164v-212.414h-212.414v84.164h128.25zM214.418 157.961v84.164h212.414v-212.414h-84.164v128.25h-128.25z" />
|
||||
<glyph unicode="" glyph-name="more_vert" d="M513.001 157.961c46.090 0 86.168-40.078 86.168-86.168s-40.078-86.168-86.168-86.168-86.168 40.078-86.168 86.168 40.078 86.168 86.168 86.168zM513.001 414.461c46.090 0 86.168-40.078 86.168-86.168s-40.078-86.168-86.168-86.168-86.168 40.078-86.168 86.168 40.078 86.168 86.168 86.168zM513.001 498.625c-46.090 0-86.168 40.078-86.168 86.168s40.078 86.168 86.168 86.168 86.168-40.078 86.168-86.168-40.078-86.168-86.168-86.168z" />
|
||||
<glyph unicode="" glyph-name="sentiment_satisfied" d="M513.001 157.961c64.126 0 118.23 34.066 148.29 84.164h70.136c-34.066-88.172-118.23-148.29-218.426-148.29s-184.36 60.118-218.426 148.29h70.136c30.058-50.098 84.164-84.164 148.29-84.164zM513.001-14.376c188.368 0 342.669 154.302 342.669 342.669s-154.302 342.669-342.669 342.669-342.669-154.302-342.669-342.669 154.302-342.669 342.669-342.669zM513.001 755.127c236.462 0 426.833-190.372 426.833-426.833s-190.372-426.833-426.833-426.833-426.833 190.372-426.833 426.833 190.372 426.833 426.833 426.833zM298.583 434.501c0 36.070 28.054 64.126 64.126 64.126s64.126-28.054 64.126-64.126-28.054-64.126-64.126-64.126-64.126 28.054-64.126 64.126zM599.17 434.501c0 36.070 28.054 64.126 64.126 64.126s64.126-28.054 64.126-64.126-28.054-64.126-64.126-64.126-64.126 28.054-64.126 64.126z" />
|
||||
<glyph unicode="" glyph-name="face" d="M513.001-14.376c188.368 0 342.669 154.302 342.669 342.669 0 34.066-6.012 66.13-14.028 96.188-30.058-8.016-62.122-10.020-96.188-10.020-144.282 0-270.528 70.136-348.681 180.352-42.082-102.2-124.242-186.364-224.438-230.45-2.004-12.024-2.004-24.046-2.004-36.070 0-188.368 154.302-342.669 342.669-342.669zM513.001 755.127c236.462 0 426.833-190.372 426.833-426.833s-190.372-426.833-426.833-426.833-426.833 190.372-426.833 426.833 190.372 426.833 426.833 426.833zM641.252 338.313c30.058 0 54.106-22.044 54.106-52.102s-24.046-54.106-54.106-54.106-54.106 24.046-54.106 54.106 24.046 52.102 54.106 52.102zM384.751 338.313c30.058 0 54.106-22.044 54.106-52.102s-24.046-54.106-54.106-54.106-54.106 24.046-54.106 54.106 24.046 52.102 54.106 52.102z" />
|
||||
<glyph unicode="" glyph-name="question_answer" d="M727.42 328.293c0-24.046-20.040-42.082-44.086-42.082h-426.833l-170.332-172.336v599.17c0 24.046 18.036 42.082 42.082 42.082h555.084c24.046 0 44.086-18.036 44.086-42.082v-384.751zM897.753 584.793c24.046 0 42.082-18.036 42.082-42.082v-641.252l-170.332 170.332h-470.919c-24.046 0-42.082 18.036-42.082 42.082v86.168h555.084v384.751h86.168z" />
|
||||
<glyph unicode="" glyph-name="room" d="M513.001 350.336c58.114 0 106.208 48.094 106.208 106.208s-48.094 106.208-106.208 106.208-106.208-48.094-106.208-106.208 48.094-106.208 106.208-106.208zM513.001 755.127c166.324 0 298.583-132.258 298.583-298.583 0-224.438-298.583-555.084-298.583-555.084s-298.583 330.646-298.583 555.084c0 166.324 132.258 298.583 298.583 298.583z" />
|
||||
<glyph unicode="" glyph-name="schedule" d="M535.045 542.711v-224.438l192.376-114.223-32.062-54.106-224.438 136.266v256.5h64.126zM513.001-14.376c188.368 0 342.669 154.302 342.669 342.669s-154.302 342.669-342.669 342.669-342.669-154.302-342.669-342.669 154.302-342.669 342.669-342.669zM513.001 755.127c236.462 0 426.833-190.372 426.833-426.833s-190.372-426.833-426.833-426.833-426.833 190.372-426.833 426.833 190.372 426.833 426.833 426.833z" />
|
||||
<glyph unicode="" glyph-name="file-text2" d="M918.703 660.725c-22.234 30.322-53.226 65.764-87.263 99.801s-69.48 65.028-99.801 87.263c-51.624 37.857-76.667 42.235-91.013 42.235h-496.485c-44.155 0-80.078-35.923-80.078-80.078v-864.845c0-44.155 35.923-80.078 80.078-80.078h736.719c44.155 0 80.078 35.923 80.078 80.078v624.61c0 14.346-4.376 39.388-42.235 91.013zM786.142 715.229c30.73-30.73 54.854-58.455 72.651-81.454h-154.104v154.096c23.006-17.797 50.728-41.919 81.454-72.643zM896.876-54.897c0-8.68-7.335-16.016-16.016-16.016h-736.719c-8.68 0-16.016 7.335-16.016 16.016v864.845c0 8.68 7.335 16.016 16.016 16.016 0 0 496.441 0.002 496.485 0v-224.219c0-17.689 14.34-32.031 32.031-32.031h224.219v-624.61zM736.719 57.212h-448.438c-17.689 0-32.031 14.34-32.031 32.031s14.342 32.031 32.031 32.031h448.438c17.691 0 32.031-14.34 32.031-32.031s-14.34-32.031-32.031-32.031zM736.719 185.337h-448.438c-17.689 0-32.031 14.34-32.031 32.031s14.342 32.031 32.031 32.031h448.438c17.691 0 32.031-14.34 32.031-32.031s-14.34-32.031-32.031-32.031zM736.719 313.462h-448.438c-17.689 0-32.031 14.34-32.031 32.031s14.342 32.031 32.031 32.031h448.438c17.691 0 32.031-14.34 32.031-32.031s-14.34-32.031-32.031-32.031z" />
|
||||
<glyph unicode="" glyph-name="display" d="M0 832v-640h1024v640h-1024zM960 256h-896v512h896v-512zM672 128h-320l-32-128-64-64h512l-64 64z" />
|
||||
</font></defs></svg>
|
||||
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
Binary file not shown.
@ -6,6 +6,10 @@ const Peer = jest.fn().mockImplementation(() => {
|
||||
(peer as any).destroy = jest.fn();
|
||||
(peer as any).signal = jest.fn();
|
||||
(peer as any).send = jest.fn();
|
||||
(peer as any).addTrack = jest.fn();
|
||||
(peer as any).removeTrack = jest.fn();
|
||||
(peer as any).addStream = jest.fn();
|
||||
(peer as any).removeStream = jest.fn();
|
||||
(Peer as any).instances.push(peer)
|
||||
return peer
|
||||
});
|
||||
|
||||
@ -32,6 +32,9 @@ window.navigator.mediaDevices.enumerateDevices = async () => {
|
||||
window.navigator.mediaDevices.getUserMedia = async () => {
|
||||
return {} as any
|
||||
}
|
||||
(window.navigator.mediaDevices as any).getDisplayMedia = async () => {
|
||||
return {} as any
|
||||
}
|
||||
|
||||
export const play = jest.fn()
|
||||
|
||||
|
||||
@ -74,6 +74,8 @@ describe('CallActions', () => {
|
||||
message: 'Connected to server socket',
|
||||
type: 'warning',
|
||||
},
|
||||
}, {
|
||||
type: constants.INIT,
|
||||
}, {
|
||||
type: constants.NOTIFY,
|
||||
payload: {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import socket from '../socket'
|
||||
import { Dispatch, ThunkResult } from '../store'
|
||||
import { ThunkResult } from '../store'
|
||||
import { callId } from '../window'
|
||||
import { ClientSocket } from '../socket'
|
||||
import * as NotifyActions from './NotifyActions'
|
||||
import * as SocketActions from './SocketActions'
|
||||
|
||||
@ -20,23 +19,15 @@ const initialize = (): InitializeAction => ({
|
||||
|
||||
export const init = (): ThunkResult<Promise<void>> =>
|
||||
async (dispatch, getState) => {
|
||||
const socket = await dispatch(connect())
|
||||
|
||||
dispatch(SocketActions.handshake({
|
||||
socket,
|
||||
roomName: callId,
|
||||
}))
|
||||
|
||||
dispatch(initialize())
|
||||
}
|
||||
|
||||
export const connect = () => (dispatch: Dispatch) => {
|
||||
return new Promise<ClientSocket>(resolve => {
|
||||
socket.once('connect', () => {
|
||||
resolve(socket)
|
||||
})
|
||||
return new Promise(resolve => {
|
||||
socket.on('connect', () => {
|
||||
dispatch(NotifyActions.warning('Connected to server socket'))
|
||||
dispatch(SocketActions.handshake({
|
||||
socket,
|
||||
roomName: callId,
|
||||
}))
|
||||
dispatch(initialize())
|
||||
resolve()
|
||||
})
|
||||
socket.on('disconnect', () => {
|
||||
dispatch(NotifyActions.error('Server socket disconnected'))
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { makeAction, AsyncAction } from '../async'
|
||||
import { MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_ENUMERATE, MEDIA_STREAM } from '../constants'
|
||||
import { MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_ENUMERATE, MEDIA_STREAM, ME, STREAM_TYPE_CAMERA, STREAM_TYPE_DESKTOP } from '../constants'
|
||||
import _debug from 'debug'
|
||||
import { AddStreamPayload } from './StreamActions'
|
||||
|
||||
const debug = _debug('peercalls')
|
||||
|
||||
@ -66,6 +67,11 @@ async function getUserMedia(
|
||||
})
|
||||
}
|
||||
|
||||
async function getDisplayMedia(): Promise<MediaStream> {
|
||||
const mediaDevices = navigator.mediaDevices as any // eslint-disable-line
|
||||
return mediaDevices.getDisplayMedia({video: true, audio: false})
|
||||
}
|
||||
|
||||
export interface MediaVideoConstraintAction {
|
||||
type: 'MEDIA_VIDEO_CONSTRAINT_SET'
|
||||
payload: VideoConstraint
|
||||
@ -106,12 +112,30 @@ export const getMediaStream = makeAction(
|
||||
MEDIA_STREAM,
|
||||
async (constraints: GetMediaConstraints) => {
|
||||
debug('getMediaStream', constraints)
|
||||
return getUserMedia(constraints)
|
||||
const payload: AddStreamPayload = {
|
||||
stream: await getUserMedia(constraints),
|
||||
type: STREAM_TYPE_CAMERA,
|
||||
userId: ME,
|
||||
}
|
||||
return payload
|
||||
},
|
||||
)
|
||||
|
||||
export const getDesktopStream = makeAction(
|
||||
MEDIA_STREAM,
|
||||
async () => {
|
||||
debug('getDesktopStream')
|
||||
const payload: AddStreamPayload = {
|
||||
stream: await getDisplayMedia(),
|
||||
type: STREAM_TYPE_DESKTOP,
|
||||
userId: ME,
|
||||
}
|
||||
return payload
|
||||
},
|
||||
)
|
||||
|
||||
export type MediaEnumerateAction = AsyncAction<'MEDIA_ENUMERATE', MediaDevice[]>
|
||||
export type MediaStreamAction = AsyncAction<'MEDIA_STREAM', MediaStream>
|
||||
export type MediaStreamAction = AsyncAction<'MEDIA_STREAM', AddStreamPayload>
|
||||
export type MediaPlayAction = AsyncAction<'MEDIA_PLAY', void>
|
||||
|
||||
export type MediaAction =
|
||||
|
||||
@ -57,18 +57,31 @@ class PeerHandler {
|
||||
const state = getState()
|
||||
const peer = state.peers[user.id]
|
||||
const localStream = state.streams[constants.ME]
|
||||
if (localStream && localStream.stream) {
|
||||
localStream && localStream.streams.forEach(s => {
|
||||
// If the local user pressed join call before this peer has joined the
|
||||
// call, now is the time to share local media stream with the peer since
|
||||
// we no longer automatically send the stream to the peer.
|
||||
peer.addStream(localStream.stream)
|
||||
}
|
||||
s.stream.getTracks().forEach(track => {
|
||||
peer.addTrack(track, s.stream)
|
||||
})
|
||||
})
|
||||
}
|
||||
handleStream = (stream: MediaStream) => {
|
||||
handleTrack = (track: MediaStreamTrack, stream: MediaStream) => {
|
||||
const { user, dispatch } = this
|
||||
debug('peer: %s, stream', user.id)
|
||||
const userId = user.id
|
||||
debug('peer: %s, track', userId)
|
||||
// Listen to mute event to know when a track was removed
|
||||
// https://github.com/feross/simple-peer/issues/512
|
||||
track.onmute = () => {
|
||||
debug('peer: %s, track muted', userId)
|
||||
dispatch(StreamActions.removeTrack({
|
||||
userId,
|
||||
stream,
|
||||
track,
|
||||
}))
|
||||
}
|
||||
dispatch(StreamActions.addStream({
|
||||
userId: user.id,
|
||||
userId,
|
||||
stream,
|
||||
}))
|
||||
}
|
||||
@ -95,10 +108,13 @@ class PeerHandler {
|
||||
}
|
||||
}
|
||||
handleClose = () => {
|
||||
const { dispatch, user } = this
|
||||
debug('peer: %s, close', user.id)
|
||||
const { dispatch, user, getState } = this
|
||||
dispatch(NotifyActions.error('Peer connection closed'))
|
||||
dispatch(StreamActions.removeStream(user.id))
|
||||
const state = getState()
|
||||
const userStreams = state.streams[user.id]
|
||||
userStreams && userStreams.streams.forEach(s => {
|
||||
dispatch(StreamActions.removeStream(user.id, s.stream))
|
||||
})
|
||||
dispatch(removePeer(user.id))
|
||||
}
|
||||
}
|
||||
@ -156,7 +172,7 @@ export function createPeer (options: CreatePeerOptions) {
|
||||
peer.once(constants.PEER_EVENT_CONNECT, handler.handleConnect)
|
||||
peer.once(constants.PEER_EVENT_CLOSE, handler.handleClose)
|
||||
peer.on(constants.PEER_EVENT_SIGNAL, handler.handleSignal)
|
||||
peer.on(constants.PEER_EVENT_STREAM, handler.handleStream)
|
||||
peer.on(constants.PEER_EVENT_TRACK, handler.handleTrack)
|
||||
peer.on(constants.PEER_EVENT_DATA, handler.handleData)
|
||||
|
||||
dispatch(addPeer({ peer, userId }))
|
||||
|
||||
@ -136,13 +136,16 @@ describe('SocketActions', () => {
|
||||
}]
|
||||
},
|
||||
}
|
||||
peer.emit(constants.PEER_EVENT_STREAM, stream)
|
||||
peer.emit(constants.PEER_EVENT_TRACK, stream.getTracks()[0], stream)
|
||||
|
||||
expect(store.getState().streams).toEqual({
|
||||
b: {
|
||||
userId: 'b',
|
||||
stream,
|
||||
url: jasmine.any(String),
|
||||
streams: [{
|
||||
stream,
|
||||
type: undefined,
|
||||
url: jasmine.any(String),
|
||||
}],
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -151,12 +154,18 @@ describe('SocketActions', () => {
|
||||
describe('close', () => {
|
||||
beforeEach(() => {
|
||||
const stream = new MediaStream()
|
||||
peer.emit(constants.PEER_EVENT_STREAM, stream)
|
||||
const track = {} as unknown as MediaStreamTrack
|
||||
peer.emit(constants.PEER_EVENT_TRACK, track, stream)
|
||||
// test stream with two tracks
|
||||
peer.emit(constants.PEER_EVENT_TRACK, track, stream)
|
||||
expect(store.getState().streams).toEqual({
|
||||
b: {
|
||||
userId: 'b',
|
||||
stream,
|
||||
url: jasmine.any(String),
|
||||
streams: [{
|
||||
stream,
|
||||
type: undefined,
|
||||
url: jasmine.any(String),
|
||||
}],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -88,6 +88,10 @@ export function handshake (options: HandshakeOptions) {
|
||||
getState,
|
||||
})
|
||||
|
||||
// remove listeneres to make seocket reusable
|
||||
socket.removeListener(constants.SOCKET_EVENT_SIGNAL, handler.handleSignal)
|
||||
socket.removeListener(constants.SOCKET_EVENT_USERS, handler.handleUsers)
|
||||
|
||||
socket.on(constants.SOCKET_EVENT_SIGNAL, handler.handleSignal)
|
||||
socket.on(constants.SOCKET_EVENT_USERS, handler.handleUsers)
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import * as constants from '../constants'
|
||||
|
||||
export type StreamType = 'camera' | 'desktop'
|
||||
|
||||
export interface AddStreamPayload {
|
||||
userId: string
|
||||
type?: StreamType
|
||||
stream: MediaStream
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface AddStreamAction {
|
||||
@ -18,11 +20,16 @@ export interface RemoveStreamAction {
|
||||
|
||||
export interface RemoveStreamPayload {
|
||||
userId: string
|
||||
stream: MediaStream
|
||||
}
|
||||
|
||||
export interface SetActiveStreamPayload {
|
||||
userId: string
|
||||
}
|
||||
|
||||
export interface SetActiveStreamAction {
|
||||
type: 'ACTIVE_SET'
|
||||
payload: RemoveStreamPayload
|
||||
payload: SetActiveStreamPayload
|
||||
}
|
||||
|
||||
export interface ToggleActiveStreamAction {
|
||||
@ -30,6 +37,17 @@ export interface ToggleActiveStreamAction {
|
||||
payload: UserIdPayload
|
||||
}
|
||||
|
||||
export interface RemoveStreamTrackPayload {
|
||||
userId: string
|
||||
stream: MediaStream
|
||||
track: MediaStreamTrack
|
||||
}
|
||||
|
||||
export interface RemoveStreamTrackAction {
|
||||
type: 'PEER_STREAM_TRACK_REMOVE'
|
||||
payload: RemoveStreamTrackPayload
|
||||
}
|
||||
|
||||
export interface UserIdPayload {
|
||||
userId: string
|
||||
}
|
||||
@ -39,9 +57,19 @@ export const addStream = (payload: AddStreamPayload): AddStreamAction => ({
|
||||
payload,
|
||||
})
|
||||
|
||||
export const removeStream = (userId: string): RemoveStreamAction => ({
|
||||
export const removeStream = (
|
||||
userId: string,
|
||||
stream: MediaStream,
|
||||
): RemoveStreamAction => ({
|
||||
type: constants.STREAM_REMOVE,
|
||||
payload: { userId },
|
||||
payload: { userId, stream },
|
||||
})
|
||||
|
||||
export const removeTrack = (
|
||||
payload: RemoveStreamTrackPayload,
|
||||
): RemoveStreamTrackAction => ({
|
||||
type: constants.STREAM_TRACK_REMOVE,
|
||||
payload,
|
||||
})
|
||||
|
||||
export const setActive = (userId: string): SetActiveStreamAction => ({
|
||||
@ -58,4 +86,5 @@ export type StreamAction =
|
||||
AddStreamAction |
|
||||
RemoveStreamAction |
|
||||
SetActiveStreamAction |
|
||||
ToggleActiveStreamAction
|
||||
ToggleActiveStreamAction |
|
||||
RemoveStreamTrackAction
|
||||
|
||||
@ -14,6 +14,15 @@ export type RejectedAction<T extends string> = Action<T> & {
|
||||
status: 'rejected'
|
||||
}
|
||||
|
||||
export function isRejectedAction(
|
||||
value: unknown,
|
||||
): value is RejectedAction<string> {
|
||||
// eslint-disable-next-line
|
||||
const v = value as any
|
||||
return !!v && 'type' in v && typeof v.type === 'string' &&
|
||||
'status' in v && v.status === 'rejected'
|
||||
}
|
||||
|
||||
export type AsyncAction<T extends string, P> =
|
||||
PendingAction<T, P> |
|
||||
ResolvedAction<T, P> |
|
||||
|
||||
@ -5,7 +5,7 @@ import Peer from 'simple-peer'
|
||||
import { Message } from '../actions/ChatActions'
|
||||
import { dismissNotification, Notification } from '../actions/NotifyActions'
|
||||
import { TextMessage } from '../actions/PeerActions'
|
||||
import { AddStreamPayload, removeStream } from '../actions/StreamActions'
|
||||
import { removeStream } from '../actions/StreamActions'
|
||||
import * as constants from '../constants'
|
||||
import Chat from './Chat'
|
||||
import { Media } from './Media'
|
||||
@ -13,6 +13,8 @@ import Notifications from './Notifications'
|
||||
import { Side } from './Side'
|
||||
import Toolbar from './Toolbar'
|
||||
import Video from './Video'
|
||||
import { getDesktopStream } from '../actions/MediaActions'
|
||||
import { StreamsState } from '../reducers/streams'
|
||||
|
||||
export interface AppProps {
|
||||
active: string | null
|
||||
@ -24,7 +26,8 @@ export interface AppProps {
|
||||
peers: Record<string, Peer.Instance>
|
||||
play: () => void
|
||||
sendMessage: (message: TextMessage) => void
|
||||
streams: Record<string, AddStreamPayload>
|
||||
streams: StreamsState
|
||||
getDesktopStream: typeof getDesktopStream
|
||||
removeStream: typeof removeStream
|
||||
onSendFile: (file: File) => void
|
||||
toggleActive: (userId: string) => void
|
||||
@ -60,7 +63,16 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
||||
init()
|
||||
}
|
||||
onHangup = () => {
|
||||
this.props.removeStream(constants.ME)
|
||||
const localStreams = this.getLocalStreams()
|
||||
localStreams.streams.forEach(s => {
|
||||
this.props.removeStream(constants.ME, s.stream)
|
||||
})
|
||||
}
|
||||
getLocalStreams() {
|
||||
return this.props.streams[constants.ME] || {
|
||||
userId: constants.ME,
|
||||
streams: [],
|
||||
}
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
@ -83,6 +95,8 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
||||
'chat-visible': this.state.chatVisible,
|
||||
})
|
||||
|
||||
const localStreams = this.getLocalStreams()
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Side align='flex-end' left zIndex={2}>
|
||||
@ -92,7 +106,16 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
||||
onToggleChat={this.handleToggleChat}
|
||||
onSendFile={onSendFile}
|
||||
onHangup={this.onHangup}
|
||||
stream={streams[constants.ME]}
|
||||
stream={
|
||||
localStreams.streams
|
||||
.filter(s => s.type === constants.STREAM_TYPE_CAMERA)[0]
|
||||
}
|
||||
desktopStream={
|
||||
localStreams.streams
|
||||
.filter(s => s.type === constants.STREAM_TYPE_DESKTOP)[0]
|
||||
}
|
||||
onGetDesktopStream={this.props.getDesktopStream}
|
||||
onRemoveStream={this.props.removeStream}
|
||||
/>
|
||||
</Side>
|
||||
<Side className={chatVisibleClassName} top zIndex={1}>
|
||||
@ -109,33 +132,43 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
||||
visible={this.state.chatVisible}
|
||||
/>
|
||||
<div className={classnames('videos', chatVisibleClassName)}>
|
||||
{streams[constants.ME] && (
|
||||
<Video
|
||||
videos={videos}
|
||||
active={active === constants.ME}
|
||||
onClick={toggleActive}
|
||||
play={play}
|
||||
stream={streams[constants.ME]}
|
||||
userId={constants.ME}
|
||||
muted
|
||||
mirrored
|
||||
/>
|
||||
)}
|
||||
|
||||
{localStreams.streams.map((s, i) => {
|
||||
const key = localStreams.userId + '_' + i
|
||||
return (
|
||||
<Video
|
||||
videos={videos}
|
||||
key={key}
|
||||
active={active === key}
|
||||
onClick={toggleActive}
|
||||
play={play}
|
||||
stream={s}
|
||||
userId={key}
|
||||
muted
|
||||
mirrored={s.type === 'camera'}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{
|
||||
map(peers, (_, userId) => userId)
|
||||
.filter(stream => !!stream)
|
||||
.map(userId =>
|
||||
<Video
|
||||
active={userId === active}
|
||||
key={userId}
|
||||
onClick={toggleActive}
|
||||
play={play}
|
||||
stream={streams[userId]}
|
||||
userId={userId}
|
||||
videos={videos}
|
||||
/>,
|
||||
)
|
||||
.map(userId => streams[userId])
|
||||
.filter(userStreams => !!userStreams)
|
||||
.map(userStreams => {
|
||||
return userStreams.streams.map((s, i) => {
|
||||
const key = userStreams.userId + '_' + i
|
||||
return (
|
||||
<Video
|
||||
active={key === active}
|
||||
key={key}
|
||||
onClick={toggleActive}
|
||||
play={play}
|
||||
stream={s}
|
||||
userId={key}
|
||||
videos={videos}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,7 @@ import { MediaState } from '../reducers/media'
|
||||
import { State } from '../store'
|
||||
import { Alerts, Alert } from './Alerts'
|
||||
import { info, warning, error } from '../actions/NotifyActions'
|
||||
import { ME } from '../constants'
|
||||
import { ME, STREAM_TYPE_CAMERA } from '../constants'
|
||||
|
||||
export type MediaProps = MediaState & {
|
||||
visible: boolean
|
||||
@ -21,7 +21,9 @@ export type MediaProps = MediaState & {
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
const localStream = state.streams[ME]
|
||||
const visible = !localStream
|
||||
const hidden = !!localStream &&
|
||||
localStream.streams.filter(s => s.type === STREAM_TYPE_CAMERA).length > 0
|
||||
const visible = !hidden
|
||||
return {
|
||||
...state.media,
|
||||
visible,
|
||||
|
||||
@ -2,9 +2,12 @@ jest.mock('../window')
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import Toolbar, { ToolbarProps } from './Toolbar'
|
||||
import { getDesktopStream } from '../actions/MediaActions'
|
||||
import { AddStreamPayload, removeStream } from '../actions/StreamActions'
|
||||
import { STREAM_TYPE_CAMERA, STREAM_TYPE_DESKTOP } from '../constants'
|
||||
import { StreamWithURL } from '../reducers/streams'
|
||||
import { MediaStream } from '../window'
|
||||
import { AddStreamPayload } from '../actions/StreamActions'
|
||||
import Toolbar, { ToolbarProps } from './Toolbar'
|
||||
|
||||
describe('components/Toolbar', () => {
|
||||
|
||||
@ -15,15 +18,19 @@ describe('components/Toolbar', () => {
|
||||
class ToolbarWrapper extends React.PureComponent<ToolbarProps, StreamState> {
|
||||
state = {
|
||||
stream: null,
|
||||
desktopStream: null,
|
||||
}
|
||||
render () {
|
||||
return <Toolbar
|
||||
chatVisible={this.props.chatVisible}
|
||||
onToggleChat={this.props.onToggleChat}
|
||||
onHangup={this.props.onHangup}
|
||||
onGetDesktopStream={this.props.onGetDesktopStream}
|
||||
onRemoveStream={this.props.onRemoveStream}
|
||||
onSendFile={this.props.onSendFile}
|
||||
messagesCount={this.props.messagesCount}
|
||||
stream={this.state.stream || this.props.stream}
|
||||
desktopStream={this.state.desktopStream || this.props.desktopStream}
|
||||
/>
|
||||
}
|
||||
}
|
||||
@ -34,12 +41,22 @@ describe('components/Toolbar', () => {
|
||||
let onToggleChat: jest.Mock<() => void>
|
||||
let onSendFile: jest.Mock<(file: File) => void>
|
||||
let onHangup: jest.Mock<() => void>
|
||||
let onGetDesktopStream: jest.MockedFunction<typeof getDesktopStream>
|
||||
let onRemoveStream: jest.MockedFunction<typeof removeStream>
|
||||
let desktopStream: StreamWithURL | undefined
|
||||
async function render () {
|
||||
mediaStream = new MediaStream()
|
||||
onToggleChat = jest.fn()
|
||||
onSendFile = jest.fn()
|
||||
onHangup = jest.fn()
|
||||
onGetDesktopStream = jest.fn().mockImplementation(() => Promise.resolve())
|
||||
onRemoveStream = jest.fn()
|
||||
const div = document.createElement('div')
|
||||
const stream: StreamWithURL = {
|
||||
stream: mediaStream,
|
||||
type: STREAM_TYPE_CAMERA,
|
||||
url,
|
||||
}
|
||||
await new Promise<ToolbarWrapper>(resolve => {
|
||||
ReactDOM.render(
|
||||
<ToolbarWrapper
|
||||
@ -49,7 +66,10 @@ describe('components/Toolbar', () => {
|
||||
onToggleChat={onToggleChat}
|
||||
onSendFile={onSendFile}
|
||||
messagesCount={1}
|
||||
stream={{ userId: '', stream: mediaStream, url }}
|
||||
stream={stream}
|
||||
desktopStream={desktopStream}
|
||||
onGetDesktopStream={onGetDesktopStream}
|
||||
onRemoveStream={onRemoveStream}
|
||||
/>,
|
||||
div,
|
||||
)
|
||||
@ -136,4 +156,26 @@ describe('components/Toolbar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('desktop sharing', () => {
|
||||
it('starts desktop sharing', async () => {
|
||||
const shareDesktop = node.querySelector('.stream-desktop')!
|
||||
expect(shareDesktop).toBeDefined()
|
||||
TestUtils.Simulate.click(shareDesktop)
|
||||
await Promise.resolve()
|
||||
expect(onGetDesktopStream.mock.calls.length).toBe(1)
|
||||
})
|
||||
it('stops desktop sharing', async () => {
|
||||
desktopStream = {
|
||||
stream: new MediaStream(),
|
||||
type: STREAM_TYPE_DESKTOP,
|
||||
}
|
||||
await render()
|
||||
const shareDesktop = node.querySelector('.stream-desktop')!
|
||||
expect(shareDesktop).toBeDefined()
|
||||
TestUtils.Simulate.click(shareDesktop)
|
||||
await Promise.resolve()
|
||||
expect(onRemoveStream.mock.calls.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
import screenfull from 'screenfull'
|
||||
import { AddStreamPayload } from '../actions/StreamActions'
|
||||
import { removeStream } from '../actions/StreamActions'
|
||||
import { getDesktopStream } from '../actions/MediaActions'
|
||||
import { StreamWithURL } from '../reducers/streams'
|
||||
import { ME } from '../constants'
|
||||
|
||||
const hidden = {
|
||||
display: 'none',
|
||||
@ -9,8 +12,11 @@ const hidden = {
|
||||
|
||||
export interface ToolbarProps {
|
||||
messagesCount: number
|
||||
stream: AddStreamPayload
|
||||
stream: StreamWithURL
|
||||
desktopStream: StreamWithURL | undefined
|
||||
onToggleChat: () => void
|
||||
onGetDesktopStream: typeof getDesktopStream
|
||||
onRemoveStream: typeof removeStream
|
||||
onSendFile: (file: File) => void
|
||||
onHangup: () => void
|
||||
chatVisible: boolean
|
||||
@ -113,6 +119,13 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
||||
})
|
||||
this.props.onToggleChat()
|
||||
}
|
||||
handleToggleShareDesktop = () => {
|
||||
if (this.props.desktopStream) {
|
||||
this.props.onRemoveStream(ME, this.props.desktopStream.stream)
|
||||
} else {
|
||||
this.props.onGetDesktopStream().catch(() => {})
|
||||
}
|
||||
}
|
||||
render () {
|
||||
const { messagesCount, stream } = this.props
|
||||
const unreadCount = messagesCount - this.state.readMessages
|
||||
@ -145,6 +158,14 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
||||
title='Send File'
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
className='stream-desktop'
|
||||
icon='icon-display'
|
||||
onClick={this.handleToggleShareDesktop}
|
||||
on={!!this.props.desktopStream}
|
||||
title='Share Desktop'
|
||||
/>
|
||||
|
||||
{stream && (
|
||||
<React.Fragment>
|
||||
<ToolbarButton
|
||||
|
||||
@ -2,14 +2,15 @@ jest.mock('../window')
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import { AddStreamPayload } from '../actions/StreamActions'
|
||||
import Video, { VideoProps } from './Video'
|
||||
import { MediaStream } from '../window'
|
||||
import { STREAM_TYPE_CAMERA } from '../constants'
|
||||
import { StreamWithURL } from '../reducers/streams'
|
||||
|
||||
describe('components/Video', () => {
|
||||
|
||||
interface VideoState {
|
||||
stream: null | AddStreamPayload
|
||||
stream: null | StreamWithURL
|
||||
}
|
||||
|
||||
const play = jest.fn()
|
||||
@ -61,12 +62,17 @@ describe('components/Video', () => {
|
||||
mediaStream = new MediaStream()
|
||||
const div = document.createElement('div')
|
||||
component = await new Promise<VideoWrapper>(resolve => {
|
||||
const stream: StreamWithURL = {
|
||||
stream: mediaStream,
|
||||
url,
|
||||
type: STREAM_TYPE_CAMERA,
|
||||
}
|
||||
ReactDOM.render(
|
||||
<VideoWrapper
|
||||
ref={instance => resolve(instance!)}
|
||||
videos={videos}
|
||||
active={flags.active}
|
||||
stream={{ stream: mediaStream, url, userId: 'test' }}
|
||||
stream={stream}
|
||||
onClick={onClick}
|
||||
play={play}
|
||||
userId="test"
|
||||
@ -100,22 +106,38 @@ describe('components/Video', () => {
|
||||
it('updates src only when changed', () => {
|
||||
mediaStream = new MediaStream()
|
||||
component.setState({
|
||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
||||
stream: {
|
||||
url: 'test',
|
||||
stream: mediaStream,
|
||||
type: STREAM_TYPE_CAMERA,
|
||||
},
|
||||
})
|
||||
expect(video.videoRef.current!.src).toBe('http://localhost/test')
|
||||
component.setState({
|
||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
||||
stream: {
|
||||
url: 'test',
|
||||
stream: mediaStream,
|
||||
type: STREAM_TYPE_CAMERA,
|
||||
},
|
||||
})
|
||||
})
|
||||
it('updates srcObject only when changed', () => {
|
||||
video.videoRef.current!.srcObject = null
|
||||
mediaStream = new MediaStream()
|
||||
component.setState({
|
||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
||||
stream: {
|
||||
url: 'test',
|
||||
stream: mediaStream,
|
||||
type: STREAM_TYPE_CAMERA,
|
||||
},
|
||||
})
|
||||
expect(video.videoRef.current!.srcObject).toBe(mediaStream)
|
||||
component.setState({
|
||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
||||
stream: {
|
||||
url: 'test',
|
||||
stream: mediaStream,
|
||||
type: STREAM_TYPE_CAMERA,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import React, { ReactEventHandler } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import socket from '../socket'
|
||||
import { AddStreamPayload } from '../actions/StreamActions'
|
||||
import { StreamWithURL } from '../reducers/streams'
|
||||
|
||||
export interface VideoProps {
|
||||
videos: Record<string, unknown>
|
||||
onClick: (userId: string) => void
|
||||
active: boolean
|
||||
stream?: AddStreamPayload
|
||||
stream?: StreamWithURL
|
||||
userId: string
|
||||
muted: boolean
|
||||
mirrored: boolean
|
||||
|
||||
@ -29,7 +29,7 @@ export const PEER_EVENT_ERROR = 'error'
|
||||
export const PEER_EVENT_CONNECT = 'connect'
|
||||
export const PEER_EVENT_CLOSE = 'close'
|
||||
export const PEER_EVENT_SIGNAL = 'signal'
|
||||
export const PEER_EVENT_STREAM = 'stream'
|
||||
export const PEER_EVENT_TRACK = 'track'
|
||||
export const PEER_EVENT_DATA = 'data'
|
||||
|
||||
export const SOCKET_EVENT_SIGNAL = 'signal'
|
||||
@ -37,3 +37,7 @@ export const SOCKET_EVENT_USERS = 'users'
|
||||
|
||||
export const STREAM_ADD = 'PEER_STREAM_ADD'
|
||||
export const STREAM_REMOVE = 'PEER_STREAM_REMOVE'
|
||||
export const STREAM_TRACK_REMOVE = 'PEER_STREAM_TRACK_REMOVE'
|
||||
|
||||
export const STREAM_TYPE_CAMERA = 'camera'
|
||||
export const STREAM_TYPE_DESKTOP = 'desktop'
|
||||
|
||||
@ -79,13 +79,19 @@ describe('App', () => {
|
||||
state.streams = {
|
||||
[constants.ME]: {
|
||||
userId: constants.ME,
|
||||
stream: new MediaStream(),
|
||||
url: 'blob://',
|
||||
streams: [{
|
||||
stream: new MediaStream(),
|
||||
type: constants.STREAM_TYPE_CAMERA,
|
||||
url: 'blob://',
|
||||
}],
|
||||
},
|
||||
'other-user': {
|
||||
userId: 'other-user',
|
||||
stream: new MediaStream(),
|
||||
url: 'blob://',
|
||||
streams: [{
|
||||
stream: new MediaStream(),
|
||||
type: undefined,
|
||||
url: 'blob://',
|
||||
}],
|
||||
},
|
||||
}
|
||||
state.peers = {
|
||||
@ -109,7 +115,7 @@ describe('App', () => {
|
||||
expect(dispatchSpy.mock.calls[0][0].type).toBe(constants.MEDIA_PLAY)
|
||||
expect(dispatchSpy.mock.calls.slice(1)).toEqual([[{
|
||||
type: constants.ACTIVE_TOGGLE,
|
||||
payload: { userId: constants.ME },
|
||||
payload: { userId: constants.ME + '_0' },
|
||||
}]])
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { init } from '../actions/CallActions'
|
||||
import { play } from '../actions/MediaActions'
|
||||
import { getDesktopStream, play } from '../actions/MediaActions'
|
||||
import { dismissNotification } from '../actions/NotifyActions'
|
||||
import { sendFile, sendMessage } from '../actions/PeerActions'
|
||||
import { toggleActive, removeStream } from '../actions/StreamActions'
|
||||
@ -22,6 +22,7 @@ const mapDispatchToProps = {
|
||||
toggleActive,
|
||||
sendMessage,
|
||||
dismissNotification,
|
||||
getDesktopStream,
|
||||
removeStream,
|
||||
init,
|
||||
onSendFile: sendFile,
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
jest.mock('simple-peer')
|
||||
|
||||
import * as MediaActions from '../actions/MediaActions'
|
||||
import { MEDIA_ENUMERATE, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_STREAM, ME, PEERS_DESTROY, PEER_ADD } from '../constants'
|
||||
import { MEDIA_ENUMERATE, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_STREAM, ME, PEERS_DESTROY, PEER_ADD, STREAM_TYPE_CAMERA, STREAM_TYPE_DESKTOP } from '../constants'
|
||||
import { createStore, Store } from '../store'
|
||||
import SimplePeer from 'simple-peer'
|
||||
|
||||
@ -89,7 +91,12 @@ describe('media', () => {
|
||||
})
|
||||
|
||||
describe(MEDIA_STREAM, () => {
|
||||
const stream: MediaStream = {} as MediaStream
|
||||
const track: MediaStreamTrack = {} as unknown as MediaStreamTrack
|
||||
const stream: MediaStream = {
|
||||
getTracks() {
|
||||
return [track]
|
||||
},
|
||||
} as MediaStream
|
||||
describe('using navigator.mediaDevices.getUserMedia', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
@ -97,29 +104,30 @@ describe('media', () => {
|
||||
})
|
||||
|
||||
async function dispatch() {
|
||||
const promise = store.dispatch(MediaActions.getMediaStream({
|
||||
const result = await store.dispatch(MediaActions.getMediaStream({
|
||||
audio: true,
|
||||
video: true,
|
||||
}))
|
||||
expect(await promise).toBe(stream)
|
||||
expect(result.stream).toBe(stream)
|
||||
expect(result.type).toBe(STREAM_TYPE_CAMERA)
|
||||
expect(result.userId).toBe(ME)
|
||||
}
|
||||
|
||||
describe('reducers/streams', () => {
|
||||
it('adds the local stream to the map of videos', async () => {
|
||||
expect(store.getState().streams[ME]).toBeFalsy()
|
||||
await dispatch()
|
||||
expect(store.getState().streams[ME]).toBeTruthy()
|
||||
expect(store.getState().streams[ME].stream).toBe(stream)
|
||||
const localStreams = store.getState().streams[ME]
|
||||
expect(localStreams).toBeTruthy()
|
||||
expect(localStreams.streams.length).toBe(1)
|
||||
expect(localStreams.streams[0].type).toBe(STREAM_TYPE_CAMERA)
|
||||
expect(localStreams.streams[0].stream).toBe(stream)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reducers/peers', () => {
|
||||
const peer1 = new SimplePeer()
|
||||
peer1.addStream = jest.fn()
|
||||
peer1.removeStream = jest.fn()
|
||||
const peer2 = new SimplePeer()
|
||||
peer2.addStream = jest.fn()
|
||||
peer2.removeStream = jest.fn()
|
||||
const peers = [peer1, peer2]
|
||||
|
||||
beforeEach(() => {
|
||||
@ -148,19 +156,19 @@ describe('media', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('replaces local stream on all peers', async () => {
|
||||
it('adds local camera stream to all peers', async () => {
|
||||
await dispatch()
|
||||
peers.forEach(peer => {
|
||||
expect((peer.addStream as jest.Mock).mock.calls)
|
||||
.toEqual([[ stream ]])
|
||||
expect((peer.removeStream as jest.Mock).mock.calls).toEqual([])
|
||||
expect((peer.addTrack as jest.Mock).mock.calls)
|
||||
.toEqual([[ track, stream ]])
|
||||
expect((peer.removeTrack as any).mock.calls).toEqual([])
|
||||
})
|
||||
await dispatch()
|
||||
peers.forEach(peer => {
|
||||
expect((peer.addStream as jest.Mock).mock.calls)
|
||||
.toEqual([[ stream ], [ stream ]])
|
||||
expect((peer.removeStream as jest.Mock).mock.calls)
|
||||
.toEqual([[ stream ]])
|
||||
expect((peer.addTrack as jest.Mock).mock.calls)
|
||||
.toEqual([[ track, stream ], [ track, stream ]])
|
||||
expect((peer.removeTrack as jest.Mock).mock.calls)
|
||||
.toEqual([[ track, stream ]])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -183,10 +191,34 @@ describe('media', () => {
|
||||
})
|
||||
expect(promise.type).toBe('MEDIA_STREAM')
|
||||
expect(promise.status).toBe('pending')
|
||||
expect(await promise).toBe(stream)
|
||||
const result = await promise
|
||||
expect(result.stream).toBe(stream)
|
||||
expect(result.userId).toBe(ME)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDesktopStream (getDisplayMedia)', () => {
|
||||
const stream: MediaStream = {} as MediaStream
|
||||
beforeEach(() => {
|
||||
(navigator.mediaDevices as any).getDisplayMedia = async () => stream
|
||||
})
|
||||
async function dispatch() {
|
||||
const result = await store.dispatch(MediaActions.getDesktopStream())
|
||||
expect(result.stream).toBe(stream)
|
||||
expect(result.type).toBe(STREAM_TYPE_DESKTOP)
|
||||
expect(result.userId).toBe(ME)
|
||||
}
|
||||
it('adds the local stream to the map of videos', async () => {
|
||||
expect(store.getState().streams[ME]).toBeFalsy()
|
||||
await dispatch()
|
||||
const localStreams = store.getState().streams[ME]
|
||||
expect(localStreams).toBeTruthy()
|
||||
expect(localStreams.streams.length).toBe(1)
|
||||
expect(localStreams.streams[0].type).toBe(STREAM_TYPE_DESKTOP)
|
||||
expect(localStreams.streams[0].stream).toBe(stream)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import * as constants from '../constants'
|
||||
import { Notification, NotificationActionType } from '../actions/NotifyActions'
|
||||
import { error, Notification, NotificationActionType } from '../actions/NotifyActions'
|
||||
import { isRejectedAction } from '../async'
|
||||
import { AnyAction } from 'redux'
|
||||
|
||||
export type NotificationState = Record<string, Notification>
|
||||
|
||||
@ -7,7 +9,17 @@ const defaultState: NotificationState = {}
|
||||
|
||||
export default function notifications (
|
||||
state = defaultState,
|
||||
action: NotificationActionType,
|
||||
action: AnyAction,
|
||||
) {
|
||||
if (isRejectedAction(action)) {
|
||||
action = error('' + action.payload)
|
||||
}
|
||||
return handleNotifications(state, action)
|
||||
}
|
||||
|
||||
function handleNotifications (
|
||||
state = defaultState,
|
||||
action: NotificationActionType,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case constants.NOTIFY:
|
||||
|
||||
@ -4,16 +4,63 @@ import Peer from 'simple-peer'
|
||||
import { PeerAction } from '../actions/PeerActions'
|
||||
import * as constants from '../constants'
|
||||
import { MediaStreamAction } from '../actions/MediaActions'
|
||||
import { RemoveStreamAction, StreamType } from '../actions/StreamActions'
|
||||
|
||||
export type PeersState = Record<string, Peer.Instance>
|
||||
|
||||
const defaultState: PeersState = {}
|
||||
|
||||
let localStream: MediaStream | undefined
|
||||
let localStreams: Record<StreamType, MediaStream | undefined> = {
|
||||
camera: undefined,
|
||||
desktop: undefined,
|
||||
}
|
||||
|
||||
function handleRemoveStream(
|
||||
state: PeersState,
|
||||
action: RemoveStreamAction,
|
||||
): PeersState {
|
||||
const stream = action.payload.stream
|
||||
if (action.payload.userId === constants.ME) {
|
||||
forEach(state, peer => {
|
||||
stream.getTracks().forEach(track => {
|
||||
peer.removeTrack(track, stream)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
function handleMediaStream(
|
||||
state: PeersState,
|
||||
action: MediaStreamAction,
|
||||
): PeersState {
|
||||
if (action.status !== 'resolved') {
|
||||
return state
|
||||
}
|
||||
const streamType = action.payload.type
|
||||
if (
|
||||
action.payload.userId === constants.ME &&
|
||||
streamType
|
||||
) {
|
||||
forEach(state, peer => {
|
||||
const localStream = localStreams[streamType]
|
||||
localStream && localStream.getTracks().forEach(track => {
|
||||
peer.removeTrack(track, localStream)
|
||||
})
|
||||
const stream = action.payload.stream
|
||||
stream.getTracks().forEach(track => {
|
||||
peer.addTrack(track, stream)
|
||||
})
|
||||
})
|
||||
localStreams[streamType] = action.payload.stream
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
export default function peers(
|
||||
state = defaultState,
|
||||
action: PeerAction | MediaStreamAction,
|
||||
action: PeerAction | MediaStreamAction | RemoveStreamAction,
|
||||
): PeersState {
|
||||
switch (action.type) {
|
||||
case constants.PEER_ADD:
|
||||
@ -24,18 +71,16 @@ export default function peers(
|
||||
case constants.PEER_REMOVE:
|
||||
return omit(state, [action.payload.userId])
|
||||
case constants.PEERS_DESTROY:
|
||||
localStream = undefined
|
||||
localStreams = {
|
||||
camera: undefined,
|
||||
desktop: undefined,
|
||||
}
|
||||
forEach(state, peer => peer.destroy())
|
||||
return defaultState
|
||||
case constants.STREAM_REMOVE:
|
||||
return handleRemoveStream(state, action)
|
||||
case constants.MEDIA_STREAM:
|
||||
if (action.status === 'resolved') {
|
||||
forEach(state, peer => {
|
||||
localStream && peer.removeStream(localStream)
|
||||
peer.addStream(action.payload)
|
||||
})
|
||||
localStream = action.payload
|
||||
}
|
||||
return state
|
||||
return handleMediaStream(state, action)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
||||
@ -35,8 +35,11 @@ describe('reducers/alerts', () => {
|
||||
expect(store.getState().streams).toEqual({
|
||||
[userId]: {
|
||||
userId,
|
||||
stream: stream,
|
||||
url: jasmine.any(String),
|
||||
streams: [{
|
||||
stream,
|
||||
url: jasmine.any(String),
|
||||
type: undefined,
|
||||
}],
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -47,8 +50,11 @@ describe('reducers/alerts', () => {
|
||||
expect(store.getState().streams).toEqual({
|
||||
[userId]: {
|
||||
userId,
|
||||
stream: stream,
|
||||
url: undefined,
|
||||
streams: [{
|
||||
stream,
|
||||
type: undefined,
|
||||
url: undefined,
|
||||
}],
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -57,12 +63,16 @@ describe('reducers/alerts', () => {
|
||||
describe('removeStream', () => {
|
||||
it('removes a stream', () => {
|
||||
store.dispatch(StreamActions.addStream({ userId, stream }))
|
||||
store.dispatch(StreamActions.removeStream(userId))
|
||||
store.dispatch(StreamActions.removeStream(userId, stream))
|
||||
expect(store.getState().streams).toEqual({})
|
||||
})
|
||||
it('does not fail when no stream', () => {
|
||||
store.dispatch(StreamActions.removeStream(userId))
|
||||
store.dispatch(StreamActions.removeStream(userId, stream))
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeStreamTrack', () => {
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import _debug from 'debug'
|
||||
import omit from 'lodash/omit'
|
||||
import { AddStreamAction, AddStreamPayload, RemoveStreamAction, StreamAction } from '../actions/StreamActions'
|
||||
import { STREAM_ADD, STREAM_REMOVE, MEDIA_STREAM, ME } from '../constants'
|
||||
import { AddStreamAction, RemoveStreamAction, StreamAction, StreamType, RemoveStreamTrackAction } from '../actions/StreamActions'
|
||||
import { STREAM_ADD, STREAM_REMOVE, MEDIA_STREAM, STREAM_TRACK_REMOVE } from '../constants'
|
||||
import { createObjectURL, revokeObjectURL } from '../window'
|
||||
import { MediaStreamAction } from '../actions/MediaActions'
|
||||
|
||||
@ -17,8 +17,19 @@ function safeCreateObjectURL (stream: MediaStream) {
|
||||
}
|
||||
}
|
||||
|
||||
export interface StreamWithURL {
|
||||
stream: MediaStream
|
||||
type: StreamType | undefined
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface UserStreams {
|
||||
userId: string
|
||||
streams: StreamWithURL[]
|
||||
}
|
||||
|
||||
export interface StreamsState {
|
||||
[userId: string]: AddStreamPayload
|
||||
[userId: string]: UserStreams
|
||||
}
|
||||
|
||||
function addStream (
|
||||
@ -26,40 +37,87 @@ function addStream (
|
||||
): StreamsState {
|
||||
const { userId, stream } = payload
|
||||
|
||||
const userStream: AddStreamPayload = {
|
||||
const userStreams = state[userId] || {
|
||||
userId,
|
||||
streams: [],
|
||||
}
|
||||
|
||||
if (userStreams.streams.map(s => s.stream).indexOf(stream) >= 0) {
|
||||
return state
|
||||
}
|
||||
|
||||
const streamWithURL: StreamWithURL = {
|
||||
stream,
|
||||
type: payload.type,
|
||||
url: safeCreateObjectURL(stream),
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
[userId]: userStream,
|
||||
[userId]: {
|
||||
userId,
|
||||
streams: [...userStreams.streams, streamWithURL],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function removeStream (
|
||||
state: StreamsState, payload: RemoveStreamAction['payload'],
|
||||
): StreamsState {
|
||||
const { userId } = payload
|
||||
const stream = state[userId]
|
||||
if (stream && stream.stream) {
|
||||
stream.stream.getTracks().forEach(track => track.stop())
|
||||
const { userId, stream } = payload
|
||||
const userStreams = state[userId]
|
||||
if (!userStreams) {
|
||||
return state
|
||||
}
|
||||
if (stream && stream.url) {
|
||||
revokeObjectURL(stream.url)
|
||||
|
||||
if (stream) {
|
||||
const streams = userStreams.streams.filter(s => {
|
||||
const found = s.stream === stream
|
||||
if (found) {
|
||||
stream.getTracks().forEach(track => track.stop())
|
||||
s.url && revokeObjectURL(s.url)
|
||||
}
|
||||
return !found
|
||||
})
|
||||
if (streams.length > 0) {
|
||||
return {
|
||||
...state,
|
||||
[userId]: {
|
||||
userId,
|
||||
streams,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
omit(state, [userId])
|
||||
}
|
||||
}
|
||||
|
||||
userStreams && userStreams.streams.forEach(s => {
|
||||
s.stream.getTracks().forEach(track => track.stop())
|
||||
s.url && revokeObjectURL(s.url)
|
||||
})
|
||||
return omit(state, [userId])
|
||||
}
|
||||
|
||||
function replaceStream(state: StreamsState, stream: MediaStream): StreamsState {
|
||||
state = removeStream(state, {
|
||||
userId: ME,
|
||||
})
|
||||
return addStream(state, {
|
||||
userId: ME,
|
||||
stream,
|
||||
})
|
||||
function removeStreamTrack(
|
||||
state: StreamsState, payload: RemoveStreamTrackAction['payload'],
|
||||
): StreamsState {
|
||||
const { userId, stream, track } = payload
|
||||
const userStreams = state[userId]
|
||||
if (!userStreams) {
|
||||
return state
|
||||
}
|
||||
const index = userStreams.streams.map(s => s.stream).indexOf(stream)
|
||||
if (index < 0) {
|
||||
return state
|
||||
}
|
||||
stream.removeTrack(track)
|
||||
if (stream.getTracks().length === 0) {
|
||||
return removeStream(state, {userId, stream})
|
||||
}
|
||||
// UI does not update when a stream track is removed so there is no need to
|
||||
// update the state object
|
||||
return state
|
||||
}
|
||||
|
||||
export default function streams(
|
||||
@ -71,9 +129,11 @@ export default function streams(
|
||||
return addStream(state, action.payload)
|
||||
case STREAM_REMOVE:
|
||||
return removeStream(state, action.payload)
|
||||
case STREAM_TRACK_REMOVE:
|
||||
return removeStreamTrack(state, action.payload)
|
||||
case MEDIA_STREAM:
|
||||
if (action.status === 'resolved') {
|
||||
return replaceStream(state, action.payload)
|
||||
return addStream(state, action.payload)
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
|
||||
13
src/scss/_fonts.scss
Executable file → Normal file
13
src/scss/_fonts.scss
Executable file → Normal file
@ -1,10 +1,10 @@
|
||||
@font-face {
|
||||
font-family: 'icons';
|
||||
src: url('../res/fonts/icons.eot?tcgv6b');
|
||||
src: url('../res/fonts/icons.eot?tcgv6b#iefix') format('embedded-opentype'),
|
||||
url('../res/fonts/icons.woff?tcgv6b') format('woff'),
|
||||
url('../res/fonts/icons.ttf?tcgv6b') format('truetype'),
|
||||
url('../res/fonts/icons.svg?tcgv6b#icons') format('svg');
|
||||
src: url('../res/fonts/icons.eot?ny6drs');
|
||||
src: url('../res/fonts/icons.eot?ny6drs#iefix') format('embedded-opentype'),
|
||||
url('../res/fonts/icons.ttf?ny6drs') format('truetype'),
|
||||
url('../res/fonts/icons.woff?ny6drs') format('woff'),
|
||||
url('../res/fonts/icons.svg?ny6drs#icons') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@ -92,3 +92,6 @@
|
||||
.icon-file-text2:before {
|
||||
content: "\e926";
|
||||
}
|
||||
.icon-display:before {
|
||||
content: "\e956";
|
||||
}
|
||||
|
||||
@ -69,6 +69,11 @@ body.call {
|
||||
@include button-style($color-fg, darken($color-bg, 5%));
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 1rem rgba(black, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
outline: none;
|
||||
transform: translate(0px, 1px);
|
||||
@ -95,10 +100,29 @@ body.call {
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
input {
|
||||
font-family: $font-monospace;
|
||||
input[type="text"] {
|
||||
font-size: 1rem;
|
||||
padding: 1rem 1rem 0.75rem;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid rgba($color-warning, 0);
|
||||
transition: border-bottom 200ms ease-in;
|
||||
text-align: center;
|
||||
color: white;
|
||||
position: relative;
|
||||
|
||||
&:focus {
|
||||
border-bottom: 2px solid $color-warning;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
// font-family: $font-monospace;
|
||||
@include button($color-primary, $color-warning);
|
||||
font-size: 1.1rem;
|
||||
font-size: 1rem;
|
||||
padding: 1rem 1rem;
|
||||
}
|
||||
|
||||
|
||||
@ -27,15 +27,23 @@ describe('server/app', () => {
|
||||
|
||||
})
|
||||
|
||||
describe('GET /call', () => {
|
||||
describe('POST /call', () => {
|
||||
|
||||
it('redirects to a new call', () => {
|
||||
return request(app)
|
||||
.get('/call')
|
||||
.post('/call')
|
||||
.expect(302)
|
||||
.expect('location', new RegExp(`^${BASE_URL}/call/[0-9a-f-]{36}$`))
|
||||
})
|
||||
|
||||
it('redirects to specific call', () => {
|
||||
return request(app)
|
||||
.post('/call')
|
||||
.send('call=test%20id')
|
||||
.expect(302)
|
||||
.expect('location', `${BASE_URL}/call/test%20id`)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('GET /call/<uuid>', () => {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { config } from './config'
|
||||
import _debug from 'debug'
|
||||
import bodyParser from 'body-parser'
|
||||
import express from 'express'
|
||||
import handleSocket from './socket'
|
||||
import path from 'path'
|
||||
@ -38,6 +39,7 @@ app.use((req, res, next) => {
|
||||
})
|
||||
next()
|
||||
})
|
||||
app.use(bodyParser.urlencoded({ extended: false }))
|
||||
|
||||
const router = express.Router()
|
||||
router.use('/res', express.static(path.join(__dirname, '../../res')))
|
||||
|
||||
@ -8,8 +8,9 @@ const router = Router()
|
||||
const BASE_URL: string = config.baseUrl
|
||||
const cfgIceServers = config.iceServers
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.redirect(`${BASE_URL}/call/${v4()}`)
|
||||
router.post('/', (req, res) => {
|
||||
const callId = req.body.call ? encodeURIComponent(req.body.call) : v4()
|
||||
res.redirect(`${BASE_URL}/call/${callId}`)
|
||||
})
|
||||
|
||||
router.get('/:callId', (req, res) => {
|
||||
|
||||
@ -10,12 +10,13 @@
|
||||
</a>
|
||||
|
||||
<div id="container">
|
||||
<form id="form" method="get" action="<%= baseUrl + '/call' %>">
|
||||
<form id="form" method="post" action="<%= baseUrl + '/call' %>" autocomplete="off">
|
||||
<h1>
|
||||
<img src="<%= baseUrl + '/res/peer-calls.svg' %>" width="100%" alt="Peer Calls">
|
||||
</h1>
|
||||
<p>Group peer-to-peer calls for everyone. Create a private room. Share the link.</p>
|
||||
<input type="submit" value="New Session">
|
||||
<input type="text" value="" name="call" placeholder="Room ID (Leave empty for random)" autofocus>
|
||||
<input type="submit" value="Start Session">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user