Skip to content

Commit

Permalink
Merge pull request #58 from oslabs-beta/feature/raisa-conference-room
Browse files Browse the repository at this point in the history
Feature/raisa conference room
  • Loading branch information
fraisai authored Dec 29, 2023
2 parents e8088e1 + 2587513 commit 2ce5a3b
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 98 deletions.
21 changes: 2 additions & 19 deletions .github/workflows/rtconnect-test.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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/[email protected]
# 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
38 changes: 18 additions & 20 deletions .github/workflows/slack-notify.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
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/[email protected]
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
2 changes: 1 addition & 1 deletion lib/__tests__/unit/server.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand Down
21 changes: 10 additions & 11 deletions lib/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,19 @@ 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<string, WebSocket>;
peers: Map<string, WebSocket>;

/**
* @constructor constructing a websocket server with an http/https object or port passed in upon instantiating SignalingChannel
* @param {Server} server - pass in a server (http or https) or pass in a port (this port cannot be the same as the application port and it has to listen on the same port)
*/
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
}

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

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

/**
Expand Down
97 changes: 50 additions & 47 deletions lib/src/components/VideoCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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<string>('');
Expand All @@ -65,10 +91,10 @@ const VideoCall = ({ URL, mediaOptions }: { URL: string, mediaOptions: { control
const ws = useRef<WebSocket>(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<HTMLVideoElement>(null!);
const localVideoRef = useRef<HTMLVideoElement>(null!);

/**
* @type {mutable ref object} remoteVideo - video stream of the remote user. It cannot be null or undefined.
Expand Down Expand Up @@ -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 <video> element.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTrack
* @param {void}
* @see https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
*/
const openUserMedia = async (): Promise<void> => {
try {
if (localVideoRef.current){
localStreamRef.current = localVideoRef.current.srcObject = await navigator.mediaDevices.getUserMedia(constraints);
}
} catch (error) {
console.log('Error in openUserMedia: ', error);
}
};


/**
* @func handleUsername
Expand Down Expand Up @@ -182,26 +205,6 @@ const VideoCall = ({ URL, mediaOptions }: { URL: string, mediaOptions: { control
setUsers(userList);
};

/**
* @async
* @function openUserMedia is invoked in the useEffect Hook after WebSocket connection is established.
* @desc If the localVideo.current property exists, openUserMedia invokes the MediaDevices interface getUserMedia() method to prompt the clients for audio and video permission.
*
* If clients grant permissions, getUserMedia() uses the video and audio constraints to assign the local MediaStream from the clients' cameras/microphones to the local <video> element.
*
* @param {void}
* @see https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
*/
const openUserMedia = async (): Promise<void> => {
try {
if (localVideo.current){
localStreamRef.current = localVideo.current.srcObject = await navigator.mediaDevices.getUserMedia(constraints);
}
} catch (error) {
console.log('Error in openUserMedia: ', error);
}
};

/**
* @function callUser - Constructs a new RTCPeerConnection object using the createPeer function and then adds the local client's (Peer A/caller) media tracks to peer connection ref object.
* @param {string} userID the remote client's (Peer B/callee) username
Expand Down Expand Up @@ -413,13 +416,13 @@ const VideoCall = ({ URL, mediaOptions }: { URL: string, mediaOptions: { control
senders.current
?.find(sender => sender.track?.kind === 'video')
?.replaceTrack(screenTrack);
localVideo.current.srcObject = stream; // changing local video to display what is being screen shared to the other peer
localVideoRef.current.srcObject = stream; // changing local video to display what is being screen shared to the other peer

screenTrack.onended = function() { // ended event is fired when playback or streaming has stopped because the end of the media was reached or because no further data is available
senders.current
?.find(sender => sender.track?.kind === 'video')
?.replaceTrack(localStreamRef.current.getTracks()[1]); //
localVideo.current.srcObject = localStreamRef.current; // changing local video displayed back to webcam
localVideoRef.current.srcObject = localStreamRef.current; // changing local video displayed back to webcam
};
});
}
Expand Down Expand Up @@ -573,7 +576,7 @@ const VideoCall = ({ URL, mediaOptions }: { URL: string, mediaOptions: { control
>

<VideoComponent
video={localVideo}
video={localVideoRef}
mediaOptions={mediaOptions}
/>

Expand Down

0 comments on commit 2ce5a3b

Please sign in to comment.