Compare commits

...

20 Commits

Author SHA1 Message Date
4a53a2fb55 3.0.5
All checks were successful
continuous-integration/drone/push Build is passing
2020-03-11 14:25:29 +01:00
8ebd92c53d Add input field to specify call name 2020-03-11 14:24:36 +01:00
5b431afbc2 3.0.4 2020-03-10 13:45:56 +01:00
e28af6740e Merge branch 'share-desktop' 2020-03-10 13:45:10 +01:00
478df8c8fd Fix streams.test.ts 2020-03-10 13:44:09 +01:00
e851181ffa Fix SocketActions.test.ts 2020-03-10 13:42:47 +01:00
0ae475f898 Fix media.test.ts 2020-03-10 13:38:27 +01:00
80ac818616 Fix Toolbar.test.tsx 2020-03-10 13:16:03 +01:00
720e26a5dd Fix CallActions.test.ts 2020-03-10 13:14:31 +01:00
9db349d4a5 Fix App.test.tsx 2020-03-10 13:14:22 +01:00
6effc10d9e Do not remove tracks when sharing desktop and camera 2020-03-10 13:10:00 +01:00
8c0377bdaf Make socket automatically do the handshake on each reconnect 2020-03-10 12:10:08 +01:00
46a0b1f7ea Remove each user stream individually 2020-03-10 11:58:15 +01:00
f056048d62 Use addTrack/removeTrack over addStream/removeStream
The addStream and removeStream are deprecated and the MDN docs
recommend using addStream/removeStream instead.

While we add tracks, we can also add event listeners to whether or not a
track has ended and then remove a stream once all tracks in the streams
have ended.
2020-03-10 11:21:35 +01:00
53ddcdfcbf Add catch when requesting desktop 2020-03-10 09:03:19 +01:00
2f582e66b9 Write error to log on promise rejected 2020-03-10 09:03:11 +01:00
18ff3947b8 Run npm audit fix 2020-03-10 08:32:37 +01:00
61fc53bcf9 Add experimental support for sharing desktop 2020-03-09 11:58:28 +01:00
ee209d7889 Add display icon 2020-03-09 08:29:06 +01:00
5ebd5c07c0 Run npm audit fix --force 2020-03-09 08:21:12 +01:00
36 changed files with 5103 additions and 2020 deletions

6331
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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.

View File

@ -7,19 +7,20 @@
<font-face units-per-em="1024" ascent="870.850439882698" descent="-153.14956011730206" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="0" d="" />
<glyph unicode="&#xe029;" 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="&#xe02b;" 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="&#xe04b;" 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="&#xe04c;" 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="&#xe0b1;" 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="&#xe5c8;" 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="&#xe5d0;" 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="&#xe5d1;" 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="&#xe5d4;" 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="&#xe813;" 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="&#xe87c;" 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="&#xe8af;" 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="&#xe8b4;" 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="&#xe8b5;" 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="&#xe926;" 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="&#xe029;" 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="&#xe02b;" 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="&#xe04b;" 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="&#xe04c;" 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="&#xe0b1;" 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="&#xe5c8;" 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="&#xe5d0;" 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="&#xe5d1;" 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="&#xe5d4;" 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="&#xe813;" 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="&#xe87c;" 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="&#xe8af;" 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="&#xe8b4;" 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="&#xe8b5;" 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="&#xe926;" 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="&#xe956;" 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.

View File

@ -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
});

View File

@ -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()

View File

@ -74,6 +74,8 @@ describe('CallActions', () => {
message: 'Connected to server socket',
type: 'warning',
},
}, {
type: constants.INIT,
}, {
type: constants.NOTIFY,
payload: {

View File

@ -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'))

View File

@ -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 =

View File

@ -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 }))

View File

@ -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),
}],
},
})
})

View File

@ -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)

View File

@ -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

View File

@ -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> |

View File

@ -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>

View File

@ -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,

View File

@ -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)
})
})
})

View File

@ -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

View File

@ -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,
},
})
})
})

View File

@ -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

View File

@ -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'

View File

@ -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' },
}]])
})
})

View File

@ -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,

View File

@ -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)
})
})
})

View File

@ -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:

View File

@ -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
}

View File

@ -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', () => {
})
})

View File

@ -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
View 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";
}

View File

@ -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;
}

View File

@ -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>', () => {

View File

@ -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')))

View File

@ -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) => {

View File

@ -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>