Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/raisa conference room #57

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading