proctoring 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +245 -0
  4. data/Rakefile +32 -0
  5. data/app/assets/config/100ms_manifest.js +1 -0
  6. data/app/assets/config/knights_watch_manifest.js +4 -0
  7. data/app/assets/config/kurento_manifest.js +1 -0
  8. data/app/assets/config/proctoring_manifest.js +3 -0
  9. data/app/assets/config/videojs_manifest.js +2 -0
  10. data/app/assets/images/proctoring/poster.png +0 -0
  11. data/app/assets/javascripts/100ms/examine.js +27 -0
  12. data/app/assets/javascripts/100ms/hundred_ms.js +143 -0
  13. data/app/assets/javascripts/100ms/join_proctor_room.js +17 -0
  14. data/app/assets/javascripts/100ms/proctor.js +152 -0
  15. data/app/assets/javascripts/kurento/LiveVideoUsingSignalingServer.js +344 -0
  16. data/app/assets/javascripts/kurento/VideoPlayer.js +63 -0
  17. data/app/assets/javascripts/kurento/VideoRecording.js +286 -0
  18. data/app/assets/javascripts/kurento/VideoRecordingUsingSignalingServer.js +224 -0
  19. data/app/assets/javascripts/kurento/co.js +299 -0
  20. data/app/assets/javascripts/kurento/kurento-utils.js +4418 -0
  21. data/app/assets/javascripts/proctoring/stream_channel.js +66 -0
  22. data/app/assets/javascripts/proctoring/stream_room.js +131 -0
  23. data/app/assets/javascripts/proctoring/video_recorder.js +172 -0
  24. data/app/assets/javascripts/videojs/videojs-playlist-ui.js +516 -0
  25. data/app/assets/javascripts/videojs/videojs-playlist.js +909 -0
  26. data/app/assets/stylesheets/proctoring/application.css +15 -0
  27. data/app/assets/stylesheets/proctoring/video_player_100ms.css +34 -0
  28. data/app/assets/stylesheets/proctoring/video_streamings.css +49 -0
  29. data/app/assets/stylesheets/scaffold.css +80 -0
  30. data/app/assets/stylesheets/videojs/videojs-playlist-ui.css +1 -0
  31. data/app/controllers/proctoring/api/v1/authentication_controller.rb +31 -0
  32. data/app/controllers/proctoring/api/v1/hundred_ms/services_controller.rb +24 -0
  33. data/app/controllers/proctoring/application_controller.rb +23 -0
  34. data/app/controllers/proctoring/video_streamings_controller.rb +108 -0
  35. data/app/helpers/proctoring/application_helper.rb +4 -0
  36. data/app/helpers/proctoring/hundred_ms_service_helper.rb +90 -0
  37. data/app/helpers/proctoring/tokens_helper.rb +55 -0
  38. data/app/helpers/proctoring/video_streamings_helper.rb +4 -0
  39. data/app/jobs/proctoring/application_job.rb +4 -0
  40. data/app/mailers/proctoring/application_mailer.rb +6 -0
  41. data/app/models/proctoring/application_record.rb +5 -0
  42. data/app/models/proctoring/video_streaming.rb +54 -0
  43. data/app/models/proctoring/video_streaming_room.rb +29 -0
  44. data/app/views/layouts/proctoring/application.html.erb +15 -0
  45. data/app/views/proctoring/video_player/live_video_proctoring.html.erb +84 -0
  46. data/app/views/proctoring/video_player/live_video_proctoring_100ms.html.erb +48 -0
  47. data/app/views/proctoring/video_player/video_player.html.erb +40 -0
  48. data/app/views/proctoring/video_streamings/_form.html.erb +27 -0
  49. data/app/views/proctoring/video_streamings/_list.html.erb +27 -0
  50. data/app/views/proctoring/video_streamings/_record_video_from_client.html.erb +8 -0
  51. data/app/views/proctoring/video_streamings/_socket_rtc_scripts.html.erb +5 -0
  52. data/app/views/proctoring/video_streamings/_stream_video.html.erb +8 -0
  53. data/app/views/proctoring/video_streamings/_video_recording.html.erb +3 -0
  54. data/app/views/proctoring/video_streamings/_video_recording100ms.html.erb +4 -0
  55. data/app/views/proctoring/video_streamings/distribute_channel_to_rooms.html.erb +39 -0
  56. data/app/views/proctoring/video_streamings/edit.html.erb +6 -0
  57. data/app/views/proctoring/video_streamings/event.html.erb +9 -0
  58. data/app/views/proctoring/video_streamings/index.html.erb +1 -0
  59. data/app/views/proctoring/video_streamings/new.html.erb +5 -0
  60. data/app/views/proctoring/video_streamings/show.html.erb +19 -0
  61. data/app/views/proctoring/video_streamings/stream_channel.html.erb +1 -0
  62. data/app/views/proctoring/video_streamings/stream_room.html.erb +1 -0
  63. data/config/routes.rb +24 -0
  64. data/db/migrate/20200526061313_create_proctoring_video_streamings.rb +15 -0
  65. data/db/migrate/20200527045158_create_proctoring_video_streaming_rooms.rb +13 -0
  66. data/lib/proctoring/engine.rb +41 -0
  67. data/lib/proctoring/version.rb +3 -0
  68. data/lib/proctoring.rb +5 -0
  69. data/lib/tasks/proctoring_tasks.rake +4 -0
  70. metadata +158 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1538bea9746b16c243ab12d9b56e2abc8c795fa67b8af462f0a4149a5d227cf3
4
+ data.tar.gz: d95a3fee4ba698352726450c0717f457b9fab77ea9555e10e08b0716cbad1986
5
+ SHA512:
6
+ metadata.gz: a920b708cd41965880803a0b27590b538a61b9099b1ffc52e795522202b141a5cd335e053ad80487780a510ec6a942562ec9d8a001e4104fbf376fe90831415a
7
+ data.tar.gz: 4955dd9368b5ad72e304cecb28639a8f372bd75d487e294fb37ea24666c2beabfa708d0f15bbe73c61257bee2ec87a218ae2567da456217dd39267f2e58cf3ec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2025 Shireesh Jayashetty
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,245 @@
1
+ ## About Knights Watch
2
+
3
+ ![Open issues](https://img.shields.io/github/issues/elitmus/knights-watch)
4
+ ![Commit activity](https://img.shields.io/github/commit-activity/m/elitmus/knights-watch)
5
+
6
+ ### Introduction
7
+
8
+ :wave: Welcome to the eLitmus Knights Watch. Knights Watch is a open source project providing a platform suitable for creating modular applications with live video proctoring capabillites.
9
+
10
+ It provides the following feature:
11
+
12
+ * Video Proctoring Client side integration for services:
13
+
14
+ 1. [Kurento Media Server](https://www.kurento.org)
15
+ 2. [100ms](https://www.100ms.live/)
16
+
17
+ * Option to configure the video recording.
18
+
19
+ Future plans:
20
+
21
+ * Face detection
22
+ * AI video proctoring
23
+ * AI image proctoring
24
+ * Proctor can see candidate live screen
25
+ * Proctor can chat with candidate
26
+ * Auto Proctoring
27
+
28
+ ***This Gem is currently under development and API can change in future***
29
+
30
+ ### Usage
31
+
32
+ This section demonstrates how to use this engine with the RubyOnRails application to build web-based video proctoring application.
33
+
34
+ **100ms Example**
35
+
36
+ #### Set Up Variables
37
+
38
+ 1. Add this line to your application's Gemfile:
39
+
40
+ ```ruby
41
+ gem 'proctoring', '=1.0.0', git: 'https://github.com/elitmus/knights-watch.git'
42
+ ```
43
+
44
+ 2. And then execute:
45
+ ```bash
46
+ $ bundle install
47
+ ```
48
+
49
+ 3. Mount in application
50
+
51
+ ```ruby
52
+ mount Proctoring::Engine => '/proctoring'
53
+ ```
54
+
55
+ 3. Create initializer file `proctoring.rb`
56
+
57
+ ```ruby
58
+ Proctoring.setup do |config|
59
+ config.app_name = 'app_name'
60
+ config.hundred_ms_app_access_key = '' #Generate the app accccess key from the 100ms dashboard
61
+ config.hundred_ms_app_secret = '' #Generae the app secret from the 100ms dashboard
62
+ config.hundred_ms_template_id = '' #Generate template id from 100ms dashboard
63
+ config.hundred_ms_s3_bucket = '' #S3 bucket to store the recording fo candidates
64
+ config.hundred_ms_s3_access_key = '' #S3 access key
65
+ config.hundred_ms_s3_access_secret = '' #S3 access secret
66
+ config.hundred_ms_s3_region = '' #S3 region
67
+ config.hundred_ms_auth_secret = '' #32 bit random string
68
+ end
69
+ ```
70
+
71
+ #### Available API's for Candidate Side:
72
+
73
+ HTML for showing the candidate video element
74
+
75
+ ```ruby
76
+ <div id="videoreplay">
77
+ <video id="vid" autoplay loop ></video>
78
+ </div>
79
+ <%= render template: 'proctoring/video_streamings/_video_recording100ms.html.erb'%>
80
+ <%= content_tag :div, '', id: 'proctoring-user-data', data: { event_id: 'event_code', user: 'user_id', hundred_ms_proctoring_service: true, max_candidate_allowed: '10' } %>
81
+ ```
82
+
83
+ Javascript for the application
84
+
85
+ ```Javascript
86
+ const proctoringData = document.getElementById('proctoring-user-data');
87
+ const {
88
+ hundredMsProctoringService, eventId, user, maxCandidateAllowed,
89
+ } = proctoringData.dataset;
90
+ if (proctoringData && hundredMsProctoringService === 'true') {
91
+ const options = {
92
+ userId: user,
93
+ eventId,
94
+ maxPeopleAllowed: maxCandidateAllowed,
95
+ userRole: 'candidates',
96
+ };
97
+ connectToCandidateRoom(options);
98
+ }
99
+ ```
100
+
101
+ #### Avaliable API's for the proctor side
102
+
103
+ HTML for proctor side
104
+
105
+ ```ruby
106
+ <%= render template: 'proctoring/video_player/live_video_proctoring_100ms.html.erb'%>
107
+ <%= content_tag :div, '', id: 'proctoring-user-data', data: { 'user-id': 'user_id', 'event-id': 'event_code', 'max-proctor-allowed': '2' } %>
108
+ ```
109
+
110
+ Javascript is written in engine itself for proctor. So need to add javascript on the main application.
111
+
112
+ ---
113
+
114
+ **Kurento Media Server Example**
115
+
116
+ #### Set Up Variables
117
+
118
+ 1. Execute the first three steps mentioned in the 100ms Example.
119
+
120
+ 2. Create initializer file `proctoring.rb`
121
+
122
+ ```ruby
123
+ Proctoring.setup do |config|
124
+ config.app_name = 'your_name'
125
+ config.media_server_url = 'server-url without protocol and no extra forward slashes'
126
+ config.turn_secret = 'turn_server_secret'
127
+ end
128
+ ```
129
+
130
+ ##### Available API's for Candidate Side:
131
+
132
+ HTML if want to show video to user ids can be passed to Object.
133
+ ```ruby
134
+ <div
135
+ id="vid-btn"
136
+ title="Monitoring Video"
137
+ data-signaling-server-url = "<%= signaling_server_url %>"
138
+ data-event-id = "<%= event_id %>"
139
+ data-user-id = "<%= user_id %>"
140
+ data-video-proctoring="<%= video_proctoring %>"
141
+ ></div>
142
+
143
+ <div id="videoreplay">
144
+ <video id="vid" autoplay loop ></video>
145
+ </div>
146
+
147
+ <%= render template: 'proctoring/video_streamings/_video_recording.html.erb'%>
148
+ ```
149
+
150
+ Integrate Javascript into the main application.
151
+ ```Javascript
152
+ const connectToScocket = (url, evnt, app, user) => {
153
+ var socket = io(url);
154
+
155
+ socket.on('authorization',function(data){
156
+ socket.emit('authorization', {"event_id": evnt, "app_name": app, "user_id": user});
157
+ });
158
+ socket.on('disconnect', function(){
159
+ });
160
+ return socket;
161
+ }
162
+
163
+ const {
164
+ signalingServerUrl,
165
+ eventId,
166
+ userId
167
+ } = document.getElementById('vid-btn').dataset;
168
+
169
+ const socket = connectToScocket(
170
+ signalingServerUrl,
171
+ eventId,
172
+ 'app_name',
173
+ userId,
174
+ );
175
+
176
+ const options = {
177
+ event: eventId,
178
+ user: userId,
179
+ socket: socket,
180
+ };
181
+
182
+ videoRecordingUsingSignalingServer(options);
183
+ ```
184
+
185
+ #### Available API's for Proctor Side:
186
+
187
+ HTML to integrate proctor side screen.
188
+
189
+ ```ruby
190
+ <%= render template: 'proctoring/video_player/live_video_proctoring.html.erb'%>
191
+ <%= content_tag :div, '', id: 'proctoring-user-data', data: { 'signaling-server-url': 'singaling_server_url', 'event': 'event_id', 'user': 'user_id', 'assigned_user_ids': [] }%>
192
+ ```
193
+
194
+ Javascript for proctor side:
195
+
196
+ ```Javascript
197
+ const connectToScocketAsAdmin = (url, app) => {
198
+ var socket = io(url);
199
+
200
+ socket.on('authorization',function(data){
201
+ socket.emit('admin-authorization', {"app_name": app});
202
+ });
203
+ socket.on('disconnect', function(){
204
+ });
205
+ return socket;
206
+ }
207
+
208
+ document.addEventListener('DOMContentLoaded', () => {
209
+ const userData = document.getElementById('proctoring-user-data');
210
+ const { signalingServerUrl, event, user, assignedUserIds } = userData.dataset;
211
+ const socket = connectToScocketAsAdmin(signalingServerUrl, 'app_name');
212
+ liveVideoUsingSignalingServer({ socket, event, user, assignedUserIds });
213
+ });
214
+ ```
215
+
216
+ ### Documentation
217
+
218
+ #### Architecture
219
+
220
+ 1. Check the Knights Watch [100ms Architecture](https://docs.google.com/presentation/d/1_CebvXEStUtx8m4Hw9DLQPK6AD8gxBKU/edit?usp=sharing&ouid=100590295233713204603&rtpof=true&sd=true) to get better understanding.
221
+
222
+
223
+ ### Prerequisites
224
+
225
+ Check our prerequisites to get started.
226
+
227
+ 1. [100ms Prerequisites](./docs/100ms/prerequisites.md)
228
+
229
+ ### Installation
230
+
231
+ Use our installation guidelines to setup the project on your local
232
+
233
+ 1. [100ms Installation Guidelines](./docs/100ms/installation.md)
234
+
235
+ ### Useful Links
236
+
237
+ - [Development Guidelines](./docs/development.md)
238
+ - [Code of Conduct](./docs/code_of_conduct.md)
239
+
240
+ ### New Contributor?
241
+
242
+ If you are a new contributor, visit our [Contributing Guidelines](./docs/contributing.md) to get started.
243
+
244
+ ## License
245
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Proctoring'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+ task default: :test
@@ -0,0 +1 @@
1
+ //= link_directory ../javascripts/100ms .js
@@ -0,0 +1,4 @@
1
+ //= link proctoring_manifest.js
2
+ //= link 100ms_manifest.js
3
+ //= link kurento_manifest.js
4
+ //= link videojs_manifest.js
@@ -0,0 +1 @@
1
+ //= link_directory ../javascripts/kurento .js
@@ -0,0 +1,3 @@
1
+ //= link_directory ../stylesheets/proctoring .css
2
+ //= link_directory ../javascripts/proctoring .js
3
+ //= link_directory ../images/proctoring .png
@@ -0,0 +1,2 @@
1
+ //= link_directory ../stylesheets/videojs .css
2
+ //= link_directory ../javascripts/videojs .js
@@ -0,0 +1,27 @@
1
+ //= require 100ms/hundred_ms
2
+
3
+ import { HMSReactiveStore } from "https://cdn.skypack.dev/@100mslive/hms-video-store";
4
+
5
+ const connectToCandidateRoom = (props) => {
6
+ const hms = new HMSReactiveStore();
7
+ const hmsStore = hms.getStore();
8
+ const hmsActions = hms.getHMSActions();
9
+ const hmsNotifications = hms.getNotifications();
10
+
11
+ const renderCandidateStream = (peers) => {
12
+ const video = document.getElementById('vid');
13
+ peers.forEach((peer) => {
14
+ if((peer.isLocal || peer.name === props.userId) && peer.videoTrack){
15
+ hmsActions.attachVideo(peer.videoTrack, video);
16
+ }
17
+ })
18
+ }
19
+
20
+ const onConnectionCandidate = (isConnected) => {
21
+ console.log(isConnected);
22
+ }
23
+
24
+ connectToRoom(props, renderCandidateStream, onConnectionCandidate, hmsStore, hmsActions, hmsNotifications);
25
+ }
26
+
27
+ window.connectToCandidateRoom = connectToCandidateRoom;
@@ -0,0 +1,143 @@
1
+ import { selectIsConnectedToRoom, selectPeers } from "https://cdn.skypack.dev/@100mslive/hms-video-store";
2
+
3
+ const errorCodes = {
4
+ CAPTURE_DEVICE: '3002',
5
+ ICE_CONNECTION_FAILED: '4005'
6
+ }
7
+
8
+ const connectToRoom = (props, renderPeers, onConnection, hmsStore, hmsActions, hmsNotifications) => {
9
+ const setLocalStorageWithExpiry = (key, value, expiry) => {
10
+ const data = {
11
+ authToken: value,
12
+ ttl: Date.now() + (expiry * 1000),
13
+ }
14
+
15
+ localStorage.setItem(key, JSON.stringify(data));
16
+ }
17
+
18
+ const getLocalStorageItem = (key) => {
19
+ const data = localStorage.getItem(key);
20
+
21
+ if(!data) {
22
+ return null;
23
+ }
24
+
25
+ const item = JSON.parse(data);
26
+
27
+ if(Date.now() > item.ttl){
28
+ localStorage.removeItem(key);
29
+ return null;
30
+ }
31
+
32
+ return item.authToken;
33
+ }
34
+
35
+ const isAudioMuted = (userRole) => {
36
+ switch(userRole) {
37
+ case 'candidates':
38
+ return false;
39
+ default:
40
+ return true;
41
+ }
42
+ }
43
+
44
+ const isVideoMuted = (userRole) => {
45
+ switch(userRole) {
46
+ case 'candidates':
47
+ return false;
48
+ default:
49
+ return true;
50
+ }
51
+ }
52
+
53
+ hmsNotifications.onNotification((notification) => {
54
+ switch(notification.data.code){
55
+ case errorCodes.CAPTURE_DEVICE:
56
+ case errorCodes.ICE_CONNECTION_FAILED:
57
+ joinVideoProctoring(props);
58
+ break;
59
+ default:
60
+ return null;
61
+ }
62
+ });
63
+
64
+ const joinRoom = async (props) => {
65
+ await hmsActions.join(
66
+ {
67
+ userName: props.userId,
68
+ authToken: props.authToken,
69
+ settings: {
70
+ isAudioMuted: isAudioMuted(props.userRole),
71
+ isVideoMuted: isVideoMuted(props.userRole)
72
+ },
73
+ metadata: JSON.stringify({
74
+ user_id: props.userId,
75
+ }),
76
+ rememberDeviceSelection: true
77
+ }
78
+ )
79
+ }
80
+
81
+ const fetchAuthenticationToken = (props) => {
82
+ const url = '/proctoring/api/v1/authentication';
83
+ const csrfMeta = document.getElementsByName('csrf-token');
84
+ const token = csrfMeta[0] ? csrfMeta[0].content : '';
85
+ const data = {
86
+ user_id: props.userId,
87
+ role: props.userRole,
88
+ event_id: props.eventId,
89
+ max_people_allowed: props.maxPeopleAllowed
90
+ }
91
+ const applicationToken = document.getElementById('application-token');
92
+
93
+ return fetch(url, {
94
+ method: 'POST',
95
+ headers: {
96
+ 'X-CSRF-TOKEN': token,
97
+ 'Token': applicationToken.dataset.token,
98
+ Accept: 'application/json',
99
+ 'Content-Type': 'application/json'
100
+ },
101
+ body: JSON.stringify(data)
102
+ })
103
+ .then((response) => response.json())
104
+ .then((result) => result)
105
+ .catch((error) => console.log(error))
106
+ }
107
+
108
+ const setEventId = (props) => {
109
+ const connectedEvent = document.getElementById('connected-event');
110
+ connectedEvent.innerHTML = props.eventId;
111
+ }
112
+
113
+ const joinVideoProctoring = (props) => {
114
+ const authTokenKey = `authToken-${props.eventId}-${props.userId}`;
115
+ const authToken = getLocalStorageItem(authTokenKey);
116
+
117
+ if(props.userRole === 'proctor') setEventId(props);
118
+
119
+ if(authToken){
120
+ joinRoom({ userId: props.userId, authToken: authToken, userRole: props.userRole });
121
+ } else {
122
+ fetchAuthenticationToken(props).then((result) => {
123
+ if(result.success){
124
+ setLocalStorageWithExpiry(authTokenKey, result.authentication_token, 2 * 60 * 60);
125
+ joinRoom({ userId: props.userId, authToken: result.authentication_token, userRole: props.userRole });
126
+ }
127
+ })
128
+ }
129
+ }
130
+
131
+ joinVideoProctoring(props);
132
+
133
+ const leaveRoom = () => {
134
+ hmsActions.leave();
135
+ }
136
+
137
+ window.onunload = leaveRoom;
138
+
139
+ hmsStore.subscribe(renderPeers, selectPeers);
140
+ hmsStore.subscribe(onConnection, selectIsConnectedToRoom);
141
+ }
142
+
143
+ window.connectToRoom = connectToRoom;
@@ -0,0 +1,17 @@
1
+ /* eslint-disable no-undef */
2
+ document.getElementById('join-proctoring-room-btn').addEventListener(
3
+ 'click', () => {
4
+ document.getElementById('join-proctoring-room-btn').classList.add('d-none');
5
+ document.getElementById('loader').classList.remove('d-none');
6
+
7
+ const userData = document.getElementById('proctoring-user-data');
8
+ const {
9
+ userId,
10
+ eventId,
11
+ maxProctorAllowed,
12
+ } = userData.dataset;
13
+ connectToVideoProctoringRoom({
14
+ userId, eventId, maxPeopleAllowed: maxProctorAllowed, userRole: 'proctor'
15
+ });
16
+ },
17
+ );
@@ -0,0 +1,152 @@
1
+ //= require 100ms/join_proctor_room
2
+ //= require 100ms/hundred_ms
3
+
4
+ import { HMSReactiveStore, selectPeerByID, selectAudioTrackVolume } from "https://cdn.skypack.dev/@100mslive/hms-video-store";
5
+
6
+ const connectToVideoProctoringRoom = (props) => {
7
+ const hms = new HMSReactiveStore();
8
+ const hmsStore = hms.getStore();
9
+ const hmsActions = hms.getHMSActions();
10
+ const hmsNotifications = hms.getNotifications();
11
+
12
+ const htmlElement = (tag, attrs = {}, ...children) => {
13
+ const newElement = document.createElement(tag);
14
+
15
+ Object.keys(attrs).forEach((key) => {
16
+ newElement.setAttribute(key, attrs[key]);
17
+ })
18
+
19
+ children.forEach((child) => {
20
+ newElement.append(child);
21
+ })
22
+
23
+ return newElement;
24
+ }
25
+
26
+ const toggleCandidateSound = (candidateId) => {
27
+ const buttonEle = document.getElementById(`sound-btn-${candidateId}`);
28
+ const peer = hmsStore.getState(selectPeerByID(candidateId));
29
+
30
+ if(peer && peer.audioTrack) {
31
+ const presentVolume = hmsStore.getState(selectAudioTrackVolume(peer.audioTrack));
32
+ if(presentVolume === 0) {
33
+ buttonEle.innerHTML = 'Stop Sound';
34
+ hmsActions.setVolume(100, peer.audioTrack);
35
+ } else {
36
+ buttonEle.innerHTML = 'Start Sound';
37
+ hmsActions.setVolume(0, peer.audioTrack);
38
+ }
39
+ }
40
+ }
41
+
42
+ const renderPeers = (peers) => {
43
+ const divMeetingRoom = document.getElementById("proctoring-videos");
44
+ divMeetingRoom.innerHTML = '';
45
+
46
+ const proctorSet = new Set();
47
+ const candidateSet = new Set();
48
+
49
+ peers.forEach((peer) => {
50
+ if(peer.roleName === 'candidates'){
51
+ const participantVideoPeer = document.getElementById(`participant-video-${peer.name}`);
52
+
53
+ candidateSet.add(peer.name);
54
+
55
+ if(participantVideoPeer) {
56
+ participantVideoPeer.remove();
57
+ }
58
+
59
+ const video = htmlElement('video', {
60
+ autoplay: true,
61
+ muted: true,
62
+ playsinline: true,
63
+ controls: true,
64
+ id: `video-element-${peer.name}`
65
+ });
66
+
67
+ if(peer.videoTrack) {
68
+ hmsActions.attachVideo(peer.videoTrack, video);
69
+ }
70
+
71
+ if (peer && peer.audioTrack) {
72
+ hmsActions.setVolume(0, peer.audioTrack); //initial keep all the peer volume to the 0 not auidble.
73
+ }
74
+
75
+ const peerContainer = htmlElement(
76
+ 'div',
77
+ {
78
+ class: 'video-container rounded-0 card',
79
+ id: `participant-video-${peer.name}`
80
+ },
81
+ video,
82
+ htmlElement(
83
+ 'div',
84
+ {
85
+ class: 'card-body p-2'
86
+ },
87
+ htmlElement(
88
+ 'span',
89
+ {
90
+ class: 'text-dark m-0'
91
+ },
92
+ htmlElement(
93
+ 'strong',
94
+ {},
95
+ `CandidateId: `
96
+ ),
97
+ htmlElement(
98
+ 'strong',
99
+ {
100
+ class: 'video-user-id'
101
+ },
102
+ peer.name
103
+ ),
104
+ ),
105
+ htmlElement(
106
+ 'span',
107
+ {
108
+ class: 'status-online text-success'
109
+ },
110
+ '(Online)'
111
+ ),
112
+ htmlElement(
113
+ 'button',
114
+ {
115
+ class: 'float-right btn btn-sm btn-rounded btn-primary my-0 p-0 px-1 sound-btn',
116
+ onclick: `toggleCandidateSound('${peer.id}')`,
117
+ id: `sound-btn-${peer.id}`
118
+ },
119
+ 'Start Sound',
120
+ )
121
+ )
122
+ )
123
+
124
+ divMeetingRoom.append(peerContainer);
125
+ } else {
126
+ proctorSet.add(peer.name);
127
+ }
128
+ });
129
+
130
+ document.getElementById('assigned-candidates').innerHTML = candidateSet.size;
131
+ document.getElementById('connected-recruiters').innerHTML = proctorSet.size;
132
+ }
133
+
134
+ const onConnection = (isConnected) => {
135
+ const loader = document.getElementById('loader');
136
+ const joinVideoProctoringBtn = document.getElementById('join-proctoring-room-btn');
137
+ const proctoringDataComponent = document.getElementById('proctoring-data-component');
138
+
139
+ if(isConnected){
140
+ loader.classList.add('d-none');
141
+ joinVideoProctoringBtn.classList.add('d-none');
142
+ proctoringDataComponent.classList.remove('d-none');
143
+ } else {
144
+ loader.classList.remove('d-none');
145
+ }
146
+ }
147
+
148
+ window.toggleCandidateSound = toggleCandidateSound;
149
+ connectToRoom(props, renderPeers, onConnection, hmsStore, hmsActions, hmsNotifications);
150
+ }
151
+ window.connectToVideoProctoringRoom = connectToVideoProctoringRoom;
152
+