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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +245 -0
- data/Rakefile +32 -0
- data/app/assets/config/100ms_manifest.js +1 -0
- data/app/assets/config/knights_watch_manifest.js +4 -0
- data/app/assets/config/kurento_manifest.js +1 -0
- data/app/assets/config/proctoring_manifest.js +3 -0
- data/app/assets/config/videojs_manifest.js +2 -0
- data/app/assets/images/proctoring/poster.png +0 -0
- data/app/assets/javascripts/100ms/examine.js +27 -0
- data/app/assets/javascripts/100ms/hundred_ms.js +143 -0
- data/app/assets/javascripts/100ms/join_proctor_room.js +17 -0
- data/app/assets/javascripts/100ms/proctor.js +152 -0
- data/app/assets/javascripts/kurento/LiveVideoUsingSignalingServer.js +344 -0
- data/app/assets/javascripts/kurento/VideoPlayer.js +63 -0
- data/app/assets/javascripts/kurento/VideoRecording.js +286 -0
- data/app/assets/javascripts/kurento/VideoRecordingUsingSignalingServer.js +224 -0
- data/app/assets/javascripts/kurento/co.js +299 -0
- data/app/assets/javascripts/kurento/kurento-utils.js +4418 -0
- data/app/assets/javascripts/proctoring/stream_channel.js +66 -0
- data/app/assets/javascripts/proctoring/stream_room.js +131 -0
- data/app/assets/javascripts/proctoring/video_recorder.js +172 -0
- data/app/assets/javascripts/videojs/videojs-playlist-ui.js +516 -0
- data/app/assets/javascripts/videojs/videojs-playlist.js +909 -0
- data/app/assets/stylesheets/proctoring/application.css +15 -0
- data/app/assets/stylesheets/proctoring/video_player_100ms.css +34 -0
- data/app/assets/stylesheets/proctoring/video_streamings.css +49 -0
- data/app/assets/stylesheets/scaffold.css +80 -0
- data/app/assets/stylesheets/videojs/videojs-playlist-ui.css +1 -0
- data/app/controllers/proctoring/api/v1/authentication_controller.rb +31 -0
- data/app/controllers/proctoring/api/v1/hundred_ms/services_controller.rb +24 -0
- data/app/controllers/proctoring/application_controller.rb +23 -0
- data/app/controllers/proctoring/video_streamings_controller.rb +108 -0
- data/app/helpers/proctoring/application_helper.rb +4 -0
- data/app/helpers/proctoring/hundred_ms_service_helper.rb +90 -0
- data/app/helpers/proctoring/tokens_helper.rb +55 -0
- data/app/helpers/proctoring/video_streamings_helper.rb +4 -0
- data/app/jobs/proctoring/application_job.rb +4 -0
- data/app/mailers/proctoring/application_mailer.rb +6 -0
- data/app/models/proctoring/application_record.rb +5 -0
- data/app/models/proctoring/video_streaming.rb +54 -0
- data/app/models/proctoring/video_streaming_room.rb +29 -0
- data/app/views/layouts/proctoring/application.html.erb +15 -0
- data/app/views/proctoring/video_player/live_video_proctoring.html.erb +84 -0
- data/app/views/proctoring/video_player/live_video_proctoring_100ms.html.erb +48 -0
- data/app/views/proctoring/video_player/video_player.html.erb +40 -0
- data/app/views/proctoring/video_streamings/_form.html.erb +27 -0
- data/app/views/proctoring/video_streamings/_list.html.erb +27 -0
- data/app/views/proctoring/video_streamings/_record_video_from_client.html.erb +8 -0
- data/app/views/proctoring/video_streamings/_socket_rtc_scripts.html.erb +5 -0
- data/app/views/proctoring/video_streamings/_stream_video.html.erb +8 -0
- data/app/views/proctoring/video_streamings/_video_recording.html.erb +3 -0
- data/app/views/proctoring/video_streamings/_video_recording100ms.html.erb +4 -0
- data/app/views/proctoring/video_streamings/distribute_channel_to_rooms.html.erb +39 -0
- data/app/views/proctoring/video_streamings/edit.html.erb +6 -0
- data/app/views/proctoring/video_streamings/event.html.erb +9 -0
- data/app/views/proctoring/video_streamings/index.html.erb +1 -0
- data/app/views/proctoring/video_streamings/new.html.erb +5 -0
- data/app/views/proctoring/video_streamings/show.html.erb +19 -0
- data/app/views/proctoring/video_streamings/stream_channel.html.erb +1 -0
- data/app/views/proctoring/video_streamings/stream_room.html.erb +1 -0
- data/config/routes.rb +24 -0
- data/db/migrate/20200526061313_create_proctoring_video_streamings.rb +15 -0
- data/db/migrate/20200527045158_create_proctoring_video_streaming_rooms.rb +13 -0
- data/lib/proctoring/engine.rb +41 -0
- data/lib/proctoring/version.rb +3 -0
- data/lib/proctoring.rb +5 -0
- data/lib/tasks/proctoring_tasks.rake +4 -0
- 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
|
+

|
4
|
+

|
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 @@
|
|
1
|
+
//= link_directory ../javascripts/kurento .js
|
Binary file
|
@@ -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
|
+
|