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
@@ -0,0 +1,224 @@
|
|
1
|
+
function videoRecordingUsingSignalingServer(props) {
|
2
|
+
// variables
|
3
|
+
let roomName;
|
4
|
+
let userName;
|
5
|
+
let appName;
|
6
|
+
let participants = {};
|
7
|
+
let currentRtcPeer;
|
8
|
+
let iceServers = [];
|
9
|
+
|
10
|
+
let socket = props.socket;
|
11
|
+
|
12
|
+
let proctoringData = document.getElementById("proctoring-data");
|
13
|
+
appName = proctoringData.dataset.appName;
|
14
|
+
roomName = props.event.toString();
|
15
|
+
userName = props.user.toString();
|
16
|
+
|
17
|
+
if (roomName && userName) {
|
18
|
+
let message = {
|
19
|
+
event: "joinRoom",
|
20
|
+
roomName,
|
21
|
+
userName,
|
22
|
+
appName,
|
23
|
+
extraInfo: {},
|
24
|
+
};
|
25
|
+
|
26
|
+
sendMessage(message);
|
27
|
+
}
|
28
|
+
|
29
|
+
function socketListener(message) {
|
30
|
+
switch (message.event) {
|
31
|
+
case "existingParticipants":
|
32
|
+
onExistingParticipants(message.userId, message.existingUsers);
|
33
|
+
break;
|
34
|
+
case "receiveVideoAnswer":
|
35
|
+
onReceiveVideoAnswer(message.senderId, message.sdpAnswer);
|
36
|
+
break;
|
37
|
+
case "candidate":
|
38
|
+
addIceCandidate(message.userId, message.candidate);
|
39
|
+
break;
|
40
|
+
case "turnServer":
|
41
|
+
setTurnServer(message.turnserver);
|
42
|
+
break;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
socket.on("signaling-message", socketListener);
|
47
|
+
|
48
|
+
function sendMessage(message) {
|
49
|
+
socket.emit("signaling-message", message);
|
50
|
+
}
|
51
|
+
|
52
|
+
function setTurnServer(turnServer) {
|
53
|
+
iceServers = turnServer;
|
54
|
+
}
|
55
|
+
|
56
|
+
function stopRecordingAndRestart() {
|
57
|
+
let message = {
|
58
|
+
event: "stopRecordingAndRestart",
|
59
|
+
appName,
|
60
|
+
};
|
61
|
+
sendMessage(message);
|
62
|
+
currentRtcPeer.dispose();
|
63
|
+
socket.removeListener("signaling-message", socketListener);
|
64
|
+
videoRecordingUsingSignalingServer(props);
|
65
|
+
}
|
66
|
+
|
67
|
+
window.onbeforeunload = function () {
|
68
|
+
currentRtcPeer.dispose();
|
69
|
+
socket.disconnect();
|
70
|
+
};
|
71
|
+
|
72
|
+
function receiveVideo(userIdWs, userNameWs) {
|
73
|
+
let video = document.createElement("video");
|
74
|
+
let div = document.createElement("div");
|
75
|
+
div.className = "videoContainer";
|
76
|
+
div.id = `participant-video-${userIdWs}-${userNameWs}`;
|
77
|
+
let name = document.createElement("div");
|
78
|
+
video.id = userIdWs;
|
79
|
+
video.autoplay = true;
|
80
|
+
name.appendChild(document.createTextNode(userNameWs));
|
81
|
+
div.appendChild(video);
|
82
|
+
div.appendChild(name);
|
83
|
+
// divMeetingRoom.appendChild(div);
|
84
|
+
|
85
|
+
const onOffer = (_err, offer, _wp) => {
|
86
|
+
let message = {
|
87
|
+
event: "receiveVideoFrom",
|
88
|
+
userId: user.id,
|
89
|
+
roomName: roomName,
|
90
|
+
sdpOffer: offer,
|
91
|
+
};
|
92
|
+
sendMessage(message);
|
93
|
+
};
|
94
|
+
|
95
|
+
// send Icecandidate
|
96
|
+
const onIceCandidate = (candidate, wp) => {
|
97
|
+
var message = {
|
98
|
+
event: "candidate",
|
99
|
+
userId: user.id,
|
100
|
+
roomName: roomName,
|
101
|
+
candidate: candidate,
|
102
|
+
};
|
103
|
+
sendMessage(message);
|
104
|
+
};
|
105
|
+
|
106
|
+
let user = {
|
107
|
+
id: userIdWs,
|
108
|
+
userName: userNameWs,
|
109
|
+
video: video,
|
110
|
+
rtcPeer: null,
|
111
|
+
};
|
112
|
+
|
113
|
+
participants[user.id] = user;
|
114
|
+
|
115
|
+
let options = {
|
116
|
+
remoteVideo: video,
|
117
|
+
onicecandidate: onIceCandidate,
|
118
|
+
};
|
119
|
+
|
120
|
+
if (iceServers) {
|
121
|
+
options.configurations = {
|
122
|
+
iceServers: iceServers,
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
// This is for receving candidates
|
127
|
+
user.rtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(
|
128
|
+
options,
|
129
|
+
function (err) {
|
130
|
+
if (err) {
|
131
|
+
return console.error(err);
|
132
|
+
}
|
133
|
+
this.generateOffer(onOffer);
|
134
|
+
}
|
135
|
+
);
|
136
|
+
}
|
137
|
+
|
138
|
+
function onExistingParticipants(userIdWs, existingUsers) {
|
139
|
+
let video = document.createElement("video");
|
140
|
+
video.id = userIdWs;
|
141
|
+
video.autoplay = true;
|
142
|
+
|
143
|
+
let user = {
|
144
|
+
id: userIdWs,
|
145
|
+
userName: userName,
|
146
|
+
video: video,
|
147
|
+
rtcPeer: null,
|
148
|
+
};
|
149
|
+
|
150
|
+
participants[user.id] = user;
|
151
|
+
|
152
|
+
let constraints = {
|
153
|
+
audio: true,
|
154
|
+
video: {
|
155
|
+
mandatory: {
|
156
|
+
maxWidth: 320,
|
157
|
+
maxFrameRate: 15,
|
158
|
+
minFrameRate: 5,
|
159
|
+
},
|
160
|
+
},
|
161
|
+
};
|
162
|
+
|
163
|
+
const onOffer = (_err, offer, _wp) => {
|
164
|
+
console.log("On Offer");
|
165
|
+
let message = {
|
166
|
+
event: "receiveVideoFrom",
|
167
|
+
userId: user.id,
|
168
|
+
roomName: roomName,
|
169
|
+
sdpOffer: offer,
|
170
|
+
};
|
171
|
+
sendMessage(message);
|
172
|
+
};
|
173
|
+
|
174
|
+
// send Icecandidate
|
175
|
+
const onIceCandidate = (candidate, wp) => {
|
176
|
+
var message = {
|
177
|
+
event: "candidate",
|
178
|
+
userId: user.id,
|
179
|
+
roomName: roomName,
|
180
|
+
candidate: candidate,
|
181
|
+
};
|
182
|
+
sendMessage(message);
|
183
|
+
};
|
184
|
+
|
185
|
+
let options = {
|
186
|
+
localVideo: video,
|
187
|
+
mediaConstraints: constraints,
|
188
|
+
onicecandidate: onIceCandidate,
|
189
|
+
};
|
190
|
+
|
191
|
+
if (iceServers) {
|
192
|
+
options.configurations = {
|
193
|
+
iceServers: iceServers,
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
// This is for sending candidate
|
198
|
+
user.rtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(
|
199
|
+
options,
|
200
|
+
function (err) {
|
201
|
+
if (err) {
|
202
|
+
return console.error(err);
|
203
|
+
}
|
204
|
+
this.generateOffer(onOffer);
|
205
|
+
}
|
206
|
+
);
|
207
|
+
|
208
|
+
currentRtcPeer = user.rtcPeer;
|
209
|
+
|
210
|
+
setTimeout(() => {
|
211
|
+
stopRecordingAndRestart();
|
212
|
+
}, 5*60*1000);
|
213
|
+
}
|
214
|
+
|
215
|
+
function onReceiveVideoAnswer(senderId, sdpAnswer) {
|
216
|
+
const user = participants[senderId];
|
217
|
+
if (user) user.rtcPeer.processAnswer(sdpAnswer);
|
218
|
+
}
|
219
|
+
|
220
|
+
function addIceCandidate(userId, candidate) {
|
221
|
+
const user = participants[userId];
|
222
|
+
if (user) user.rtcPeer.addIceCandidate(candidate);
|
223
|
+
}
|
224
|
+
};
|
@@ -0,0 +1,299 @@
|
|
1
|
+
(function(exports){
|
2
|
+
|
3
|
+
/**
|
4
|
+
* toString() reference.
|
5
|
+
*/
|
6
|
+
var toString = Object.prototype.toString;
|
7
|
+
|
8
|
+
/**
|
9
|
+
* slice() reference.
|
10
|
+
*/
|
11
|
+
var slice = Array.prototype.slice;
|
12
|
+
|
13
|
+
/**
|
14
|
+
* setImmediate() in node.js environment
|
15
|
+
*/
|
16
|
+
if (typeof window === 'undefined') {
|
17
|
+
setImmediate = global.setImmediate || process.nextTick
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Wrap the given generator `fn` and
|
22
|
+
* return a thunk.
|
23
|
+
*
|
24
|
+
* @param {Function} fn
|
25
|
+
* @return {Function}
|
26
|
+
* @api public
|
27
|
+
*/
|
28
|
+
|
29
|
+
var co = function co(fn) {
|
30
|
+
var isGenFun = isGeneratorFunction(fn);
|
31
|
+
|
32
|
+
return function (done) {
|
33
|
+
var ctx = this;
|
34
|
+
|
35
|
+
// in toThunk() below we invoke co()
|
36
|
+
// with a generator, so optimize for
|
37
|
+
// this case
|
38
|
+
var gen = fn;
|
39
|
+
|
40
|
+
// we only need to parse the arguments
|
41
|
+
// if gen is a generator function.
|
42
|
+
if (isGenFun) {
|
43
|
+
var args = slice.call(arguments), len = args.length;
|
44
|
+
var hasCallback = len && 'function' == typeof args[len - 1];
|
45
|
+
done = hasCallback ? args.pop() : error;
|
46
|
+
gen = fn.apply(this, args);
|
47
|
+
} else {
|
48
|
+
done = done || error;
|
49
|
+
}
|
50
|
+
|
51
|
+
next();
|
52
|
+
|
53
|
+
// #92
|
54
|
+
// wrap the callback in a setImmediate
|
55
|
+
// so that any of its errors aren't caught by `co`
|
56
|
+
function exit(err, res) {
|
57
|
+
setImmediate(done.bind(ctx, err, res));
|
58
|
+
}
|
59
|
+
|
60
|
+
function next(err, res) {
|
61
|
+
var ret;
|
62
|
+
|
63
|
+
// multiple args
|
64
|
+
if (arguments.length > 2) res = slice.call(arguments, 1);
|
65
|
+
|
66
|
+
// error
|
67
|
+
if (err) {
|
68
|
+
try {
|
69
|
+
ret = gen.throw(err);
|
70
|
+
} catch (e) {
|
71
|
+
return exit(e);
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
// ok
|
76
|
+
if (!err) {
|
77
|
+
try {
|
78
|
+
ret = gen.next(res);
|
79
|
+
} catch (e) {
|
80
|
+
return exit(e);
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
// done
|
85
|
+
if (ret.done) return exit(null, ret.value);
|
86
|
+
|
87
|
+
// normalize
|
88
|
+
ret.value = toThunk(ret.value, ctx);
|
89
|
+
|
90
|
+
// run
|
91
|
+
if ('function' == typeof ret.value) {
|
92
|
+
var called = false;
|
93
|
+
try {
|
94
|
+
ret.value.call(ctx, function(){
|
95
|
+
if (called) return;
|
96
|
+
called = true;
|
97
|
+
next.apply(ctx, arguments);
|
98
|
+
});
|
99
|
+
} catch (e) {
|
100
|
+
setImmediate(function(){
|
101
|
+
if (called) return;
|
102
|
+
called = true;
|
103
|
+
next(e);
|
104
|
+
});
|
105
|
+
}
|
106
|
+
return;
|
107
|
+
}
|
108
|
+
|
109
|
+
// invalid
|
110
|
+
next(new Error('yield a function, promise, generator, array, or object'));
|
111
|
+
}
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Convert `obj` into a normalized thunk.
|
117
|
+
*
|
118
|
+
* @param {Mixed} obj
|
119
|
+
* @param {Mixed} ctx
|
120
|
+
* @return {Function}
|
121
|
+
* @api private
|
122
|
+
*/
|
123
|
+
|
124
|
+
function toThunk(obj, ctx) {
|
125
|
+
|
126
|
+
if (isGeneratorFunction(obj)) {
|
127
|
+
return co(obj.call(ctx));
|
128
|
+
}
|
129
|
+
|
130
|
+
if (isGenerator(obj)) {
|
131
|
+
return co(obj);
|
132
|
+
}
|
133
|
+
|
134
|
+
if (isPromise(obj)) {
|
135
|
+
return promiseToThunk(obj);
|
136
|
+
}
|
137
|
+
|
138
|
+
if ('function' == typeof obj) {
|
139
|
+
return obj;
|
140
|
+
}
|
141
|
+
|
142
|
+
if (isObject(obj) || Array.isArray(obj)) {
|
143
|
+
return objectToThunk.call(ctx, obj);
|
144
|
+
}
|
145
|
+
|
146
|
+
return obj;
|
147
|
+
}
|
148
|
+
|
149
|
+
/**
|
150
|
+
* Convert an object of yieldables to a thunk.
|
151
|
+
*
|
152
|
+
* @param {Object} obj
|
153
|
+
* @return {Function}
|
154
|
+
* @api private
|
155
|
+
*/
|
156
|
+
|
157
|
+
function objectToThunk(obj){
|
158
|
+
var ctx = this;
|
159
|
+
|
160
|
+
return function(done){
|
161
|
+
var keys = Object.keys(obj);
|
162
|
+
var pending = keys.length;
|
163
|
+
var results = new obj.constructor();
|
164
|
+
var finished;
|
165
|
+
|
166
|
+
if (!pending) {
|
167
|
+
setImmediate(function(){
|
168
|
+
done(null, results)
|
169
|
+
});
|
170
|
+
return;
|
171
|
+
}
|
172
|
+
|
173
|
+
for (var i = 0; i < keys.length; i++) {
|
174
|
+
run(obj[keys[i]], keys[i]);
|
175
|
+
}
|
176
|
+
|
177
|
+
function run(fn, key) {
|
178
|
+
if (finished) return;
|
179
|
+
try {
|
180
|
+
fn = toThunk(fn, ctx);
|
181
|
+
|
182
|
+
if ('function' != typeof fn) {
|
183
|
+
results[key] = fn;
|
184
|
+
return --pending || done(null, results);
|
185
|
+
}
|
186
|
+
|
187
|
+
fn.call(ctx, function(err, res){
|
188
|
+
if (finished) return;
|
189
|
+
|
190
|
+
if (err) {
|
191
|
+
finished = true;
|
192
|
+
return done(err);
|
193
|
+
}
|
194
|
+
|
195
|
+
results[key] = res;
|
196
|
+
--pending || done(null, results);
|
197
|
+
});
|
198
|
+
} catch (err) {
|
199
|
+
finished = true;
|
200
|
+
done(err);
|
201
|
+
}
|
202
|
+
}
|
203
|
+
}
|
204
|
+
}
|
205
|
+
|
206
|
+
/**
|
207
|
+
* Convert `promise` to a thunk.
|
208
|
+
*
|
209
|
+
* @param {Object} promise
|
210
|
+
* @return {Function}
|
211
|
+
* @api private
|
212
|
+
*/
|
213
|
+
|
214
|
+
function promiseToThunk(promise) {
|
215
|
+
return function(fn){
|
216
|
+
promise.then(function(res) {
|
217
|
+
fn(null, res);
|
218
|
+
}, fn);
|
219
|
+
}
|
220
|
+
}
|
221
|
+
|
222
|
+
/**
|
223
|
+
* Check if `obj` is a promise.
|
224
|
+
*
|
225
|
+
* @param {Object} obj
|
226
|
+
* @return {Boolean}
|
227
|
+
* @api private
|
228
|
+
*/
|
229
|
+
|
230
|
+
function isPromise(obj) {
|
231
|
+
return obj && 'function' == typeof obj.then;
|
232
|
+
}
|
233
|
+
|
234
|
+
/**
|
235
|
+
* Check if `obj` is a generator.
|
236
|
+
*
|
237
|
+
* @param {Mixed} obj
|
238
|
+
* @return {Boolean}
|
239
|
+
* @api private
|
240
|
+
*/
|
241
|
+
|
242
|
+
function isGenerator(obj) {
|
243
|
+
return obj && 'function' == typeof obj.next && 'function' == typeof obj.throw;
|
244
|
+
}
|
245
|
+
|
246
|
+
/**
|
247
|
+
* Check if `obj` is a generator function.
|
248
|
+
*
|
249
|
+
* @param {Mixed} obj
|
250
|
+
* @return {Boolean}
|
251
|
+
* @api private
|
252
|
+
*/
|
253
|
+
|
254
|
+
function isGeneratorFunction(obj) {
|
255
|
+
return obj && obj.constructor && 'GeneratorFunction' == obj.constructor.name;
|
256
|
+
}
|
257
|
+
|
258
|
+
/**
|
259
|
+
* Check for plain object.
|
260
|
+
*
|
261
|
+
* @param {Mixed} val
|
262
|
+
* @return {Boolean}
|
263
|
+
* @api private
|
264
|
+
*/
|
265
|
+
|
266
|
+
function isObject(val) {
|
267
|
+
return val && Object == val.constructor;
|
268
|
+
}
|
269
|
+
|
270
|
+
/**
|
271
|
+
* Throw `err` in a new stack.
|
272
|
+
*
|
273
|
+
* This is used when co() is invoked
|
274
|
+
* without supplying a callback, which
|
275
|
+
* should only be for demonstrational
|
276
|
+
* purposes.
|
277
|
+
*
|
278
|
+
* @param {Error} err
|
279
|
+
* @api private
|
280
|
+
*/
|
281
|
+
|
282
|
+
function error(err) {
|
283
|
+
if (!err) return;
|
284
|
+
setImmediate(function(){
|
285
|
+
throw err;
|
286
|
+
});
|
287
|
+
}
|
288
|
+
|
289
|
+
/**
|
290
|
+
* Set module reference
|
291
|
+
*/
|
292
|
+
if(typeof window === 'undefined') {
|
293
|
+
module.exports = co;
|
294
|
+
} else {
|
295
|
+
exports['co'] = co;
|
296
|
+
}
|
297
|
+
|
298
|
+
})(typeof module === 'undefined' ? this : module);
|
299
|
+
|