diff --git a/.github/workflows/rtconnect-test.yml b/.github/workflows/rtconnect-test.yml index 938505b..4c5fd54 100644 --- a/.github/workflows/rtconnect-test.yml +++ b/.github/workflows/rtconnect-test.yml @@ -1,4 +1,4 @@ -# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node and various OS (Windows, Ubuntu) # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: RTConnect CI @@ -29,21 +29,4 @@ jobs: - run: npm run lint - run: npm test # env: - # CI: true - - - # slackNotification: - # name: Slack CI status - notify on failure - # runs-on: ubuntu-latest - # steps: - # - name: Slack Notify on Failure - # if: ${{ failure() }} - # id: slack - # uses: slackapi/slack-github-action@v1.24.0 - # with: - # channel-id: 'C067F896WG5' - # slack-message: "Github CI Result: ${{ job.status }}\nGithub PR/Commit URL: ${{ github.event.pull_request.html_url || github.event.head_commit.url }}" - # env: - # SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - # # SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - # # SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + # CI: true \ No newline at end of file diff --git a/.github/workflows/slack-notify.yml b/.github/workflows/slack-notify.yml index fcf6bc6..51cf5e7 100644 --- a/.github/workflows/slack-notify.yml +++ b/.github/workflows/slack-notify.yml @@ -1,26 +1,24 @@ name: Slack Notification of CI Status on: - push: - branches: ["main", "feature/raisa-cicd"] - pull_request: - branches: ["main"] - + push: + branches: ["main", "feature/raisa-cicd"] + pull_request: + branches: ["main"] +env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} # This works BUT it shows up as problem for some unknown reason ("Context access might be invalid: NPM_TOKEN") and there should not be any errors + # SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} jobs: - slackNotification: - name: Slack CI status - notify on failure - runs-on: ubuntu-latest - steps: - - name: Slack Notify on Failure - if: ${{ failure() }} - id: slack - uses: slackapi/slack-github-action@v1.24.0 - with: - channel-id: 'C067F896WG5' - slack-message: "Github CI Result: ${{ job.status }}\nGithub PR/Commit URL: ${{ github.event.pull_request.html_url || github.event.head_commit.url }}" - env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} # This works BUT it shows up as problem for some unknown reason ("Context access might be invalid: NPM_TOKEN") and there should not be any errors - # SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - + slackNotification: + runs-on: ubuntu-latest + name: Slack CI status - notify on failure + steps: + - name: Slack Notify on Failure + if: ${{ failure() }} + id: slack + uses: slackapi/slack-github-action@v1.24.0 + with: + channel-id: 'C067F896WG5' + slack-message: "Github CI Result: ${{ job.status }}\nGithub PR/Commit URL: ${{ github.event.pull_request.html_url || github.event.head_commit.url }}" # https://github.com/slackapi/slack-github-action \ No newline at end of file diff --git a/lib/__tests__/unit/server.unit.test.ts b/lib/__tests__/unit/server.unit.test.ts index be45417..c946ce0 100644 --- a/lib/__tests__/unit/server.unit.test.ts +++ b/lib/__tests__/unit/server.unit.test.ts @@ -22,7 +22,7 @@ describe('Testing the SignalingChannel class', () => { }); it('Empty hashmap of users is initialized', () => { - expect(sc.users.size).toBe(0); + expect(sc.peers.size).toBe(0); }); }); diff --git a/lib/server/server.ts b/lib/server/server.ts index 73ab4b0..04aa114 100644 --- a/lib/server/server.ts +++ b/lib/server/server.ts @@ -8,12 +8,11 @@ const { OFFER, ANSWER, ICECANDIDATE, LOGIN, LEAVE } = actions; * @class * @classdesc The SignalingChannel class, which utilizes WebSockets in order to facillitate communication between clients connected to the WebSocket server. * @prop { WebsocketServer } websocketServer - a simple WebSocket server - * @prop { Map } users - object containing key-value pairs consisting of users' names and their corresponding WebSocket in the following fashion { username1: socket1, username2: socket2, ... , usernameN: socketN } + * @prop { Map } peers - object containing key-value pairs consisting of peers' names and their corresponding WebSocket in the following fashion { username1: socket1, username2: socket2, ... , usernameN: socketN } */ - class SignalingChannel { webSocketServer: WebSocketServer; - users: Map; + peers: Map; /** * @constructor constructing a websocket server with an http/https object or port passed in upon instantiating SignalingChannel @@ -21,7 +20,7 @@ class SignalingChannel { */ constructor(server: Server | httpsServer | number) { this.webSocketServer = typeof server === 'number' ? new WebSocket.Server({ port: server }) : new WebSocket.Server({ server: server }); - this.users = new Map(); + this.peers = new Map(); // this.rooms = new Map(); //focus on later when constructing 2+ video conferencing functionality, SFU topology } @@ -36,14 +35,14 @@ class SignalingChannel { this.webSocketServer.on('connection', (socket) => { console.log('A user has connected to the websocket server.'); - // when a client closes their browser or connection to the websocket server (onclose), their socket gets terminated and they are removed from the map of users + // when a client closes their browser or connection to the websocket server (onclose), their socket gets terminated and they are removed from the map of peers // lastly a new user list is sent out to all clients connected to the websocket server. socket.on('close', () => { - const userToDelete = this.getByValue(this.users, socket); - this.users.delete(userToDelete); + const userToDelete = this.getByValue(this.peers, socket); + this.peers.delete(userToDelete); socket.terminate(); - const userList = { ACTION_TYPE: LOGIN, payload: Array.from(this.users.keys()) }; + const userList = { ACTION_TYPE: LOGIN, payload: Array.from(this.peers.keys()) }; this.webSocketServer.clients.forEach(client => client.send(JSON.stringify(userList))); }); @@ -67,11 +66,11 @@ class SignalingChannel { this.transmit(data); break; case LOGIN: - this.users.set(data.payload, socket); + this.peers.set(data.payload, socket); this.webSocketServer.clients.forEach(client => client.send(JSON.stringify( { ACTION_TYPE: LOGIN, - payload: Array.from(this.users.keys()) + payload: Array.from(this.peers.keys()) }))); break; case LEAVE: @@ -90,7 +89,7 @@ class SignalingChannel { * @param {object} data */ transmit(data: { ACTION_TYPE: string, receiver: string }): void { - this.users.get(data.receiver)?.send(JSON.stringify(data)); + this.peers.get(data.receiver)?.send(JSON.stringify(data)); } /** diff --git a/lib/src/components/VideoCall.tsx b/lib/src/components/VideoCall.tsx index 3fc66eb..c3464c8 100644 --- a/lib/src/components/VideoCall.tsx +++ b/lib/src/components/VideoCall.tsx @@ -38,7 +38,7 @@ interface icePayObj extends payloadObj { * @desc Wrapper component containing the logic necessary for peer connections using WebRTC APIs (RTCPeerConnect API + MediaSession API) and WebSockets. * - * ws, localVideo, remoteVideo, peerRef, localStreamRef, otherUser, senders are all mutable ref objects that are created using the useRef hook. The useRef hook allows you to persist values between renders and it is used to store a mutable value that does NOT cause a re-render when updated. + * ws, localVideoRef, remoteVideo, peerRef, localStreamRef, otherUser, senders are all mutable ref objects that are created using the useRef hook. The useRef hook allows you to persist values between renders and it is used to store a mutable value that does NOT cause a re-render when updated. * * The WebSocket connection (ws.current) is established using the useEffect hook and once the component mounts, the Socket component is rendered. The Socket component adds event listeners that handle the offer-answer model and the exchange of SDP objects between peers and the socket. * @@ -49,6 +49,32 @@ interface icePayObj extends payloadObj { * * @returns A component that renders two VideoComponents, */ + +/** + * A diagram of the WebRTC Connection logic + * Peer A Stun Signaling Channel(WebSockets) Peer B Step + * |------>| | | Who Am I? + RTCPeerConnection(configuration) this contains methods to connect to a remote Peer + * |<------| | | Symmetric NAT (your ip that you can be connected to) + * |-------------------------->|------------------>| Calling Peer B, Offer SDP is generated and sent over WebSocket + * |-------------------------->|------------------>| ICE Candidates are also being trickled in, where and what IP:PORT can Peer B connect to Peer A + * | |<------------------|-------------------| Who Am I? PeerB this time! + * | |-------------------|------------------>| Peer B's NAT + * |<--------------------------|-------------------| Accepting Peer A's call, sending Answer SDP + * |<--------------------------|-------------------| Peer B's ICE Candidates are now being trickled in to peer A for connectivity. + * |-------------------------->|------------------>| ICE Candidates from Peer A, these steps repeat and are only necessary if Peer B can't connect to the + * | | | | earlier candidates sent. + * |<--------------------------|-------------------| ICE Candidate trickling from Peer B, could also take a second if there's a firewall to be + * | | | | circumvented. + * | | | | Connected! Peer to Peer connection is made and now both users are streaming data to eachother! + * + * If Peer A starts a call their order of functions being invoked is... handleOffer --> callUser --> createPeer --> peerRef.current.negotiationNeeded event (handleNegotiationNeededEvent) --> ^send Offer SDP^ --> start ICE trickle, handleIceCandidateEvent --> ^receive Answer^ SDP --> handleIceCandidateMsg --> once connected, handleTrackEvent + * If Peer B receives a call then we invoke... ^Receive Offer SDP^ --> handleReceiveCall --> createPeer --> ^send Answer SDP^ --> handleIceCandidateMsg --> handleIceCandidateEvent --> once connected, handleTrackEvent + * + * Note: Media is attached to the Peer Connection and sent along with the offers/answers to describe what media each client has. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTrack +*/ + const VideoCall = ({ URL, mediaOptions }: { URL: string, mediaOptions: { controls: boolean, style: { width: string, height: string }}}): JSX.Element => { const [username, setUsername] = useState(''); @@ -65,10 +91,10 @@ const VideoCall = ({ URL, mediaOptions }: { URL: string, mediaOptions: { control const ws = useRef(null!); /** - * @type {mutable ref object} localVideo - video element of the local user. It will not be null or undefined. - * @property {HTMLVideoElement} localVideo.current + * @type {mutable ref object} localVideoRef - video element of the local user. It will not be null or undefined. + * @property {HTMLVideoElement} localVideoRef.current */ - const localVideo = useRef(null!); + const localVideoRef = useRef(null!); /** * @type {mutable ref object} remoteVideo - video stream of the remote user. It cannot be null or undefined. @@ -110,30 +136,27 @@ const VideoCall = ({ URL, mediaOptions }: { URL: string, mediaOptions: { control openUserMedia(); },[]); + /** - * A diagram of the WebRTC Connection logic - * Peer A Stun Signaling Channel(WebSockets) Peer B Step - * |------>| | | Who Am I? + RTCPeerConnection(configuration) this contains methods to connect to a remote Peer - * |<------| | | Symmetric NAT (your ip that you can be connected to) - * |-------------------------->|------------------>| Calling Peer B, Offer SDP is generated and sent over WebSocket - * |-------------------------->|------------------>| ICE Candidates are also being trickled in, where and what IP:PORT can Peer B connect to Peer A - * | |<------------------|-------------------| Who Am I? PeerB this time! - * | |-------------------|------------------>| Peer B's NAT - * |<--------------------------|-------------------| Accepting Peer A's call, sending Answer SDP - * |<--------------------------|-------------------| Peer B's ICE Candidates are now being trickled in to peer A for connectivity. - * |-------------------------->|------------------>| ICE Candidates from Peer A, these steps repeat and are only necessary if Peer B can't connect to the - * | | | | earlier candidates sent. - * |<--------------------------|-------------------| ICE Candidate trickling from Peer B, could also take a second if there's a firewall to be - * | | | | circumvented. - * | | | | Connected! Peer to Peer connection is made and now both users are streaming data to eachother! - * - * If Peer A starts a call their order of functions being invoked is... handleOffer --> callUser --> createPeer --> peerRef.current.negotiationNeeded event (handleNegotiationNeededEvent) --> ^send Offer SDP^ --> start ICE trickle, handleIceCandidateEvent --> ^receive Answer^ SDP --> handleIceCandidateMsg --> once connected, handleTrackEvent - * If Peer B receives a call then we invoke... ^Receive Offer SDP^ --> handleReceiveCall --> createPeer --> ^send Answer SDP^ --> handleIceCandidateMsg --> handleIceCandidateEvent --> once connected, handleTrackEvent + * @async + * @function openUserMedia is invoked in the useEffect Hook after WebSocket connection is established. + * @desc If the localVideoRef.current property exists, openUserMedia invokes the MediaDevices interface getUserMedia() method to prompt the clients for audio and video permission. * - * Note: Media is attached to the Peer Connection and sent along with the offers/answers to describe what media each client has. + * If clients grant permissions, getUserMedia() uses the video and audio constraints to assign the local MediaStream from the clients' cameras/microphones to the local