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,344 @@
|
|
1
|
+
function liveVideoUsingSignalingServer(props) {
|
2
|
+
// variables
|
3
|
+
let roomName;
|
4
|
+
let userName;
|
5
|
+
let appName;
|
6
|
+
let participants = {};
|
7
|
+
let currentRtcPeer;
|
8
|
+
const iceCandidateQueue = {};
|
9
|
+
const connectedEvent = document.getElementById("connected-event");
|
10
|
+
const assignedUsers = document.getElementById("assigned-candidates");
|
11
|
+
const connectedUsers = document.getElementById("connected-candidates");
|
12
|
+
const connectedAdminUsers = document.getElementById("connected-recruiters");
|
13
|
+
const connectedUsersList = document.getElementById("connected-candidates-list");
|
14
|
+
const updateTimer = 5 * 1000; // 5 seconds
|
15
|
+
const { socket, event, user, assignedUserIds } = props;
|
16
|
+
|
17
|
+
var divMeetingRoom = document.getElementById(
|
18
|
+
props.videoDivId || "proctoringVideos"
|
19
|
+
);
|
20
|
+
|
21
|
+
let proctoringData = document.getElementById("proctoring-data");
|
22
|
+
appName = proctoringData.dataset.appName;
|
23
|
+
roomName = props.event.toString();
|
24
|
+
userName = props.user.toString();
|
25
|
+
adminUserName = userName + '-admin'
|
26
|
+
if (roomName && userName) {
|
27
|
+
let message = {
|
28
|
+
event: "joinRoom",
|
29
|
+
roomName,
|
30
|
+
userName: adminUserName,
|
31
|
+
appName,
|
32
|
+
extraInfo: {},
|
33
|
+
};
|
34
|
+
|
35
|
+
sendMessage(message);
|
36
|
+
}
|
37
|
+
|
38
|
+
function socketListener(message) {
|
39
|
+
console.log("Message arrived", message);
|
40
|
+
|
41
|
+
switch (message.event) {
|
42
|
+
case "newParticipantArrived":
|
43
|
+
onNewParticipant(message.userId, message.userName, message.roomName);
|
44
|
+
break;
|
45
|
+
case "existingParticipants":
|
46
|
+
onExistingParticipants(message.userId, message.existingUsers);
|
47
|
+
break;
|
48
|
+
case "receiveVideoAnswer":
|
49
|
+
onReceiveVideoAnswer(message.senderId, message.sdpAnswer);
|
50
|
+
break;
|
51
|
+
case "participantLeft":
|
52
|
+
setOffline(message.userName);
|
53
|
+
break;
|
54
|
+
case "candidate":
|
55
|
+
addIceCandidate(message.userId, message.candidate);
|
56
|
+
break;
|
57
|
+
case "analytics-data":
|
58
|
+
setUpAnalytics(message.roomInfo);
|
59
|
+
break;
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
socket.on("signaling-message", socketListener);
|
64
|
+
getLiveVideoProctoringAnalyticsData({ socket, event });
|
65
|
+
setInterval(() => {
|
66
|
+
getLiveVideoProctoringAnalyticsData({ socket, event });
|
67
|
+
}, updateTimer);
|
68
|
+
|
69
|
+
function sendMessage(message) {
|
70
|
+
console.log("sending " + message.event + " message to server");
|
71
|
+
socket.emit("signaling-message", message);
|
72
|
+
}
|
73
|
+
|
74
|
+
function getLiveVideoProctoringAnalyticsData(props) {
|
75
|
+
let roomName = props.event.toString();
|
76
|
+
let message = {
|
77
|
+
event: "analytics-data",
|
78
|
+
roomName,
|
79
|
+
};
|
80
|
+
sendMessage(message);
|
81
|
+
}
|
82
|
+
|
83
|
+
|
84
|
+
function stopRecordingAndRestart() {
|
85
|
+
let message = {
|
86
|
+
event: "stopRecordingAndRestart",
|
87
|
+
appName,
|
88
|
+
};
|
89
|
+
sendMessage(message);
|
90
|
+
currentRtcPeer.dispose();
|
91
|
+
socket.removeListener("signaling-message", socketListener);
|
92
|
+
liveVideoUsingSignalingServer(props);
|
93
|
+
}
|
94
|
+
|
95
|
+
window.onbeforeunload = function () {
|
96
|
+
currentRtcPeer.dispose();
|
97
|
+
socket.disconnect();
|
98
|
+
};
|
99
|
+
|
100
|
+
function setOffline(userid) {
|
101
|
+
const container = document.getElementById(`participant-video-${userid}`);
|
102
|
+
if(container) {
|
103
|
+
container.classList.remove("border-success");
|
104
|
+
container.classList.add("border-danger");
|
105
|
+
// const callButton = container.querySelector(".connect-candidate");
|
106
|
+
// callButton.disabled = true;
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
function receiveVideo(userIdWs, userNameWs) {
|
111
|
+
const checkContainer = document.getElementById(
|
112
|
+
`participant-video-${userNameWs}`
|
113
|
+
);
|
114
|
+
let video, div;
|
115
|
+
if (checkContainer) {
|
116
|
+
div = checkContainer;
|
117
|
+
const videoElm = checkContainer.querySelector('video');
|
118
|
+
video = videoElm;
|
119
|
+
} else {
|
120
|
+
const nodeToCopy = document.getElementById("sample-video-div").querySelector('div');
|
121
|
+
const newDiv = nodeToCopy.cloneNode(true);
|
122
|
+
div = newDiv;
|
123
|
+
video = newDiv.querySelector('video');
|
124
|
+
let name = newDiv.querySelector(".video-user-id");
|
125
|
+
name.innerText = userNameWs;
|
126
|
+
div.id = `participant-video-${userNameWs}`;
|
127
|
+
video.id = `video-elm-${userNameWs}`;
|
128
|
+
video.muted = true;
|
129
|
+
divMeetingRoom.appendChild(div);
|
130
|
+
}
|
131
|
+
|
132
|
+
if(div) {
|
133
|
+
div.classList.remove("border-danger");
|
134
|
+
div.classList.add("border-success");
|
135
|
+
// const callButton = div.querySelector(".connect-candidate");
|
136
|
+
// callButton.disabled = false;
|
137
|
+
}
|
138
|
+
|
139
|
+
const onOffer = (_err, offer, _wp) => {
|
140
|
+
// console.log("On Offer");
|
141
|
+
let message = {
|
142
|
+
event: "receiveVideoFrom",
|
143
|
+
userId: user.id,
|
144
|
+
roomName: roomName,
|
145
|
+
sdpOffer: offer,
|
146
|
+
};
|
147
|
+
sendMessage(message);
|
148
|
+
};
|
149
|
+
|
150
|
+
// send Icecandidate
|
151
|
+
const onIceCandidate = (candidate, wp) => {
|
152
|
+
// console.log("sending ice candidates");
|
153
|
+
var message = {
|
154
|
+
event: "candidate",
|
155
|
+
userId: user.id,
|
156
|
+
roomName: roomName,
|
157
|
+
candidate: candidate,
|
158
|
+
};
|
159
|
+
sendMessage(message);
|
160
|
+
};
|
161
|
+
|
162
|
+
let user = {
|
163
|
+
id: userIdWs,
|
164
|
+
userName: userNameWs,
|
165
|
+
video: video,
|
166
|
+
rtcPeer: null,
|
167
|
+
};
|
168
|
+
|
169
|
+
participants[user.id] = user;
|
170
|
+
|
171
|
+
let options = {
|
172
|
+
remoteVideo: video,
|
173
|
+
onicecandidate: onIceCandidate,
|
174
|
+
};
|
175
|
+
|
176
|
+
// This is for receving candidates
|
177
|
+
user.rtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(
|
178
|
+
options,
|
179
|
+
function (err) {
|
180
|
+
if (err) {
|
181
|
+
return console.error(err);
|
182
|
+
}
|
183
|
+
if (iceCandidateQueue) {
|
184
|
+
while (iceCandidateQueue.length) {
|
185
|
+
const ice = iceCandidateQueue.shift();
|
186
|
+
user.rtcPeer.addIceCandidate(ice.candidate);
|
187
|
+
}
|
188
|
+
}
|
189
|
+
this.generateOffer(onOffer);
|
190
|
+
}
|
191
|
+
);
|
192
|
+
}
|
193
|
+
|
194
|
+
function onNewParticipant(userIdWs, userNameWs, roomNameWs) {
|
195
|
+
if (validCandidate(userNameWs, roomNameWs)) receiveVideo(userIdWs, userNameWs);
|
196
|
+
}
|
197
|
+
|
198
|
+
function onExistingParticipants(userIdWs, existingUsers) {
|
199
|
+
let video = document.createElement("video");
|
200
|
+
video.id = userIdWs;
|
201
|
+
video.autoplay = false;
|
202
|
+
|
203
|
+
let user = {
|
204
|
+
id: userIdWs,
|
205
|
+
userName: userName,
|
206
|
+
video: video,
|
207
|
+
rtcPeer: null,
|
208
|
+
};
|
209
|
+
|
210
|
+
participants[user.id] = user;
|
211
|
+
|
212
|
+
let constraints = {
|
213
|
+
audio: true,
|
214
|
+
video: {
|
215
|
+
width: { min: 320, ideal: 320, max: 640 },
|
216
|
+
height: { min: 240, ideal: 240, max: 480 },
|
217
|
+
},
|
218
|
+
};
|
219
|
+
|
220
|
+
const onOffer = (_err, offer, _wp) => {
|
221
|
+
// console.log("On Offer");
|
222
|
+
let message = {
|
223
|
+
event: "receiveVideoFrom",
|
224
|
+
userId: user.id,
|
225
|
+
roomName: roomName,
|
226
|
+
sdpOffer: offer,
|
227
|
+
};
|
228
|
+
sendMessage(message);
|
229
|
+
};
|
230
|
+
|
231
|
+
// send Icecandidate
|
232
|
+
const onIceCandidate = (candidate, wp) => {
|
233
|
+
// console.log("sending ice candidates");
|
234
|
+
var message = {
|
235
|
+
event: "candidate",
|
236
|
+
userId: user.id,
|
237
|
+
roomName: roomName,
|
238
|
+
candidate: candidate,
|
239
|
+
};
|
240
|
+
sendMessage(message);
|
241
|
+
};
|
242
|
+
|
243
|
+
let options = {
|
244
|
+
// localVideo: video,
|
245
|
+
// mediaConstraints: constraints,
|
246
|
+
onicecandidate: onIceCandidate,
|
247
|
+
};
|
248
|
+
|
249
|
+
// This is for sending candidate
|
250
|
+
user.rtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(
|
251
|
+
options,
|
252
|
+
function (err) {
|
253
|
+
if (err) {
|
254
|
+
return console.error(err);
|
255
|
+
}
|
256
|
+
if (iceCandidateQueue) {
|
257
|
+
while (iceCandidateQueue.length) {
|
258
|
+
const ice = iceCandidateQueue.shift();
|
259
|
+
user.rtcPeer.addIceCandidate(ice.candidate);
|
260
|
+
}
|
261
|
+
}
|
262
|
+
this.generateOffer(onOffer);
|
263
|
+
}
|
264
|
+
);
|
265
|
+
|
266
|
+
existingUsers.forEach(function (element) {
|
267
|
+
if (validCandidate(element.name, element.roomName)) receiveVideo(element.id, element.name);
|
268
|
+
});
|
269
|
+
|
270
|
+
currentRtcPeer = user.rtcPeer;
|
271
|
+
|
272
|
+
setTimeout(() => {
|
273
|
+
stopRecordingAndRestart();
|
274
|
+
}, 5*60*1000);
|
275
|
+
}
|
276
|
+
|
277
|
+
function setUpAnalytics(roomInfo) {
|
278
|
+
connectedEvent.innerText = event;
|
279
|
+
let candidateCount = 0;
|
280
|
+
let adminCount = 0;
|
281
|
+
let assignedCount = 0;
|
282
|
+
|
283
|
+
if (roomInfo) {
|
284
|
+
Object.keys(roomInfo).forEach((key) => {
|
285
|
+
if (checkAdminUser(key)) adminCount += 1;
|
286
|
+
else candidateCount += 1;
|
287
|
+
|
288
|
+
if (validCandidate(key, roomInfo[key].room)) assignedCount += 1;
|
289
|
+
});
|
290
|
+
}
|
291
|
+
assignedUsers.innerText = `${assignedCount}/${JSON.parse(assignedUserIds).length}`;
|
292
|
+
connectedUsers.innerText = candidateCount;
|
293
|
+
connectedAdminUsers.innerText = adminCount;
|
294
|
+
// List update
|
295
|
+
let div = document.createElement("div");
|
296
|
+
div.className = "list-group";
|
297
|
+
listClass = "list-group-item list-group-item-action rounded-0";
|
298
|
+
if (roomInfo) {
|
299
|
+
Object.keys(roomInfo).forEach((key) => {
|
300
|
+
if (validCandidate(key, roomInfo[key].room)) {
|
301
|
+
let link = document.createElement('a');
|
302
|
+
const text = document.createTextNode(key);
|
303
|
+
link.appendChild(text);
|
304
|
+
link.id = `candidate-list-elm-${key}`;
|
305
|
+
link.className = listClass;
|
306
|
+
div.appendChild(link);
|
307
|
+
}
|
308
|
+
})
|
309
|
+
} else {
|
310
|
+
let link = document.createElement("a");
|
311
|
+
const text = document.createTextNode("No users connected!");
|
312
|
+
link.appendChild(text);
|
313
|
+
link.className = listClass;
|
314
|
+
div.appendChild(link);
|
315
|
+
}
|
316
|
+
connectedUsersList.innerHTML = "";
|
317
|
+
connectedUsersList.appendChild(div);
|
318
|
+
}
|
319
|
+
|
320
|
+
function validCandidate(candidateUserId, room) {
|
321
|
+
if (checkAdminUser(candidateUserId)) return false;
|
322
|
+
if (roomName !== room) return false;
|
323
|
+
if (!assignedUserIds.includes(parseInt(candidateUserId))) return false;
|
324
|
+
|
325
|
+
return true;
|
326
|
+
}
|
327
|
+
|
328
|
+
function checkAdminUser(userName) {
|
329
|
+
return userName.split('-').includes('admin');
|
330
|
+
}
|
331
|
+
|
332
|
+
function onReceiveVideoAnswer(senderId, sdpAnswer) {
|
333
|
+
participants[senderId].rtcPeer.processAnswer(sdpAnswer);
|
334
|
+
}
|
335
|
+
|
336
|
+
function addIceCandidate(userId, candidate) {
|
337
|
+
const user = participants[userId]
|
338
|
+
if (user) participants[userId].rtcPeer.addIceCandidate(candidate);
|
339
|
+
else {
|
340
|
+
if (!iceCandidateQueue[userId]) iceCandidateQueue[userId] = [];
|
341
|
+
iceCandidateQueue[userId].push({ candidate });
|
342
|
+
}
|
343
|
+
}
|
344
|
+
};
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class VideoPlayer {
|
2
|
+
constructor(elementId='preview-player', playlist) {
|
3
|
+
this.player = videojs(elementId, {
|
4
|
+
fluid: true,
|
5
|
+
});
|
6
|
+
const defaultDataEl = document.getElementById('video-player-proctoring');
|
7
|
+
if(defaultDataEl && defaultDataEl.dataset.defaultThumbnail) {
|
8
|
+
this.thumbnail = defaultDataEl.dataset.defaultThumbnail;
|
9
|
+
}
|
10
|
+
}
|
11
|
+
|
12
|
+
playPlaylist(playlist) {
|
13
|
+
if (this.thumbnail) {
|
14
|
+
this.playlist = playlist.map((list) => {
|
15
|
+
if (!list.thumbnail) {
|
16
|
+
list.thumbnail = [{ src: this.thumbnail }];
|
17
|
+
}
|
18
|
+
return list;
|
19
|
+
});
|
20
|
+
} else {
|
21
|
+
this.playlist = playlist;
|
22
|
+
}
|
23
|
+
this.player.playlist(this.playlist);
|
24
|
+
// playlist structure
|
25
|
+
// [{
|
26
|
+
// name: "Sintel open movie",
|
27
|
+
// description: "Explore the depths of our planet's oceans. ",
|
28
|
+
// duration: 10,
|
29
|
+
// // src: 'http://media.w3.org/2010/05/sintel/trailer.mp4',
|
30
|
+
// sources: [
|
31
|
+
// {
|
32
|
+
// src: "http://media.w3.org/2010/05/sintel/trailer.mp4",
|
33
|
+
// type: "video/mp4",
|
34
|
+
// },
|
35
|
+
// ],
|
36
|
+
// thumbnail: [{ src: "http://media.w3.org/2010/05/sintel/poster.png" }],
|
37
|
+
// },
|
38
|
+
// {
|
39
|
+
// name: "Sintel open movie",
|
40
|
+
// description: "Explore the depths of our planet's oceans. ",
|
41
|
+
// duration: 10,
|
42
|
+
// // src: 'http://media.w3.org/2010/05/sintel/trailer.mp4',
|
43
|
+
// sources: [
|
44
|
+
// {
|
45
|
+
// src: "http://media.w3.org/2010/05/sintel/trailer.mp4",
|
46
|
+
// type: "video/mp4",
|
47
|
+
// },
|
48
|
+
// ],
|
49
|
+
// thumbnail: [{ src: "http://media.w3.org/2010/05/sintel/poster.png" }],
|
50
|
+
// }];
|
51
|
+
// populate playlist UI
|
52
|
+
this.player.playlistUi();
|
53
|
+
// Auto advance one video after another
|
54
|
+
this.player.playlist.autoadvance(0);
|
55
|
+
}
|
56
|
+
|
57
|
+
pauseVideo() {
|
58
|
+
if (this.player) {
|
59
|
+
this.player.pause();
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
}
|
@@ -0,0 +1,286 @@
|
|
1
|
+
class VideoRecording {
|
2
|
+
constructor({
|
3
|
+
event,
|
4
|
+
user,
|
5
|
+
inputVideoElmId = false,
|
6
|
+
proctoringDataElmId = "proctoring-data",
|
7
|
+
iceServers = undefined,
|
8
|
+
}) {
|
9
|
+
this.webRtcPeer;
|
10
|
+
this.client;
|
11
|
+
this.pipeline;
|
12
|
+
this.recorder;
|
13
|
+
this.mediaServerUrl;
|
14
|
+
this.player;
|
15
|
+
this.eventName = event;
|
16
|
+
this.user = user;
|
17
|
+
this.retryCount = 0;
|
18
|
+
if (this.inputVideoElmId) {
|
19
|
+
this.inputVideoElm = document.getElementById(inputVideoElmId);
|
20
|
+
this.showInputVideo = true;
|
21
|
+
}
|
22
|
+
const proctoringData = document.getElementById(proctoringDataElmId);
|
23
|
+
if (proctoringData) {
|
24
|
+
this.mediaServerUrl = proctoringData.dataset.mediaServerUrl;
|
25
|
+
this.appName = proctoringData.dataset.appName;
|
26
|
+
try {
|
27
|
+
this.iceServers = JSON.parse(proctoringData.dataset.iceServers);
|
28
|
+
} catch(error) {
|
29
|
+
console.log("Ice servers not defined!");
|
30
|
+
}
|
31
|
+
} else {
|
32
|
+
this.mediaServerUrl = window.location.host;
|
33
|
+
this.appName = window.location.host;
|
34
|
+
this.iceServers = undefined;
|
35
|
+
}
|
36
|
+
this.streamConfig = {
|
37
|
+
ws_uri: `ws${location.protocol === "http:" ? "" : "s"}://${
|
38
|
+
this.mediaServerUrl
|
39
|
+
}/kurento`,
|
40
|
+
ice_servers: this.iceServers,
|
41
|
+
};
|
42
|
+
this.onStartOffer = this.onStartOffer.bind(this);
|
43
|
+
this.onPlayOffer = this.onPlayOffer.bind(this);
|
44
|
+
}
|
45
|
+
|
46
|
+
setIceCandidateCallbacks(webRtcPeer, webRtcEp, onerror) {
|
47
|
+
webRtcPeer.on("icecandidate", function (candidate) {
|
48
|
+
// console.log("Local candidate:",candidate);
|
49
|
+
|
50
|
+
candidate = kurentoClient.getComplexType("IceCandidate")(candidate);
|
51
|
+
|
52
|
+
webRtcEp.addIceCandidate(candidate, onerror);
|
53
|
+
});
|
54
|
+
|
55
|
+
webRtcEp.on("OnIceCandidate", function (event) {
|
56
|
+
let candidate = event.candidate;
|
57
|
+
|
58
|
+
// console.log("Remote candidate:",candidate);
|
59
|
+
|
60
|
+
webRtcPeer.addIceCandidate(candidate, onerror);
|
61
|
+
});
|
62
|
+
webRtcEp.on("ConnectionStateChanged", (event) => {
|
63
|
+
if (event.newState === "DISCONNECTED") {
|
64
|
+
this.connectionStatus = null;
|
65
|
+
} else {
|
66
|
+
this.connectionStatus = event.newState;
|
67
|
+
}
|
68
|
+
this.clearTimeDilation();
|
69
|
+
});
|
70
|
+
}
|
71
|
+
|
72
|
+
startRecordingSingleSession() {
|
73
|
+
const { eventName, user, appName } = this;
|
74
|
+
this.streamConfig.file_uri = `file:///recordings/app_data/${appName}/video/${eventName}/${user}/${user}-${+new Date()}.webm`;
|
75
|
+
let constraints = {
|
76
|
+
audio: true,
|
77
|
+
video: {
|
78
|
+
width: 640,
|
79
|
+
framerate: 15,
|
80
|
+
},
|
81
|
+
};
|
82
|
+
let options = {
|
83
|
+
mediaConstraints: constraints,
|
84
|
+
};
|
85
|
+
if (this.showInputVideo) {
|
86
|
+
options.localVideo = this.inputVideoElm;
|
87
|
+
this.inputVideoElm.muted = true;
|
88
|
+
}
|
89
|
+
|
90
|
+
if (this.streamConfig.ice_servers) {
|
91
|
+
// console.log("Use ICE servers: ", this.streamConfig.ice_servers);
|
92
|
+
options.configuration = {
|
93
|
+
iceServers: this.streamConfig.ice_servers,
|
94
|
+
};
|
95
|
+
} else {
|
96
|
+
console.log("Use freeice");
|
97
|
+
}
|
98
|
+
const self = this;
|
99
|
+
self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(
|
100
|
+
options,
|
101
|
+
function (error) {
|
102
|
+
if (error) return self.onError(error);
|
103
|
+
|
104
|
+
this.generateOffer(self.onStartOffer);
|
105
|
+
}
|
106
|
+
);
|
107
|
+
}
|
108
|
+
|
109
|
+
onStartOffer(error, sdpOffer) {
|
110
|
+
const self = this;
|
111
|
+
if (error) return self.onError(error);
|
112
|
+
|
113
|
+
co(function* () {
|
114
|
+
try {
|
115
|
+
if (!self.client)
|
116
|
+
self.client = yield kurentoClient(self.streamConfig.ws_uri);
|
117
|
+
|
118
|
+
self.pipeline = yield self.client.create("MediaPipeline");
|
119
|
+
|
120
|
+
let webRtc = yield self.pipeline.create("WebRtcEndpoint");
|
121
|
+
self.setIceCandidateCallbacks(self.webRtcPeer, webRtc, self.onError);
|
122
|
+
|
123
|
+
self.recorder = yield self.pipeline.create("RecorderEndpoint", {
|
124
|
+
uri: self.streamConfig.file_uri,
|
125
|
+
});
|
126
|
+
|
127
|
+
yield webRtc.connect(self.recorder);
|
128
|
+
yield webRtc.connect(self.webRtc);
|
129
|
+
|
130
|
+
yield self.recorder.record();
|
131
|
+
|
132
|
+
let sdpAnswer = yield webRtc.processOffer(sdpOffer);
|
133
|
+
webRtc.gatherCandidates(self.onError);
|
134
|
+
self.webRtcPeer.processAnswer(sdpAnswer);
|
135
|
+
|
136
|
+
// setStatus(CALLING);
|
137
|
+
} catch (e) {
|
138
|
+
self.onError(e);
|
139
|
+
}
|
140
|
+
})();
|
141
|
+
}
|
142
|
+
|
143
|
+
playVideo(elm = "videoOutput") {
|
144
|
+
const videoOutput = document.getElementById(elm);
|
145
|
+
let options = {
|
146
|
+
remoteVideo: videoOutput,
|
147
|
+
};
|
148
|
+
|
149
|
+
if (this.streamConfig.ice_servers) {
|
150
|
+
console.log("Use ICE servers: " + this.streamConfig.ice_servers);
|
151
|
+
options.configuration = {
|
152
|
+
iceServers: this.streamConfig.ice_servers,
|
153
|
+
};
|
154
|
+
} else {
|
155
|
+
console.log("Use freeice");
|
156
|
+
}
|
157
|
+
const self = this;
|
158
|
+
self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(
|
159
|
+
options,
|
160
|
+
function (error) {
|
161
|
+
if (error) return onError(error);
|
162
|
+
|
163
|
+
this.generateOffer(self.onPlayOffer);
|
164
|
+
}
|
165
|
+
);
|
166
|
+
}
|
167
|
+
|
168
|
+
stopAndPlayVideo(elm = "videoOutput") {
|
169
|
+
this.stopRecording();
|
170
|
+
this.playVideo(elm);
|
171
|
+
}
|
172
|
+
|
173
|
+
onPlayOffer(error, sdpOffer) {
|
174
|
+
const self = this;
|
175
|
+
if (error) return self.onError(error);
|
176
|
+
|
177
|
+
co(function* () {
|
178
|
+
try {
|
179
|
+
if (!self.client)
|
180
|
+
self.client = yield kurentoClient(self.streamConfig.ws_uri);
|
181
|
+
|
182
|
+
self.pipeline = yield self.client.create("MediaPipeline");
|
183
|
+
|
184
|
+
let webRtc = yield self.pipeline.create("WebRtcEndpoint");
|
185
|
+
self.setIceCandidateCallbacks(self.webRtcPeer, webRtc, self.onError);
|
186
|
+
|
187
|
+
self.player = yield self.pipeline.create("PlayerEndpoint", {
|
188
|
+
uri: self.streamConfig.file_uri,
|
189
|
+
});
|
190
|
+
|
191
|
+
self.player.on("EndOfStream", self.stopRecording);
|
192
|
+
|
193
|
+
yield self.player.connect(webRtc);
|
194
|
+
|
195
|
+
let sdpAnswer = yield webRtc.processOffer(sdpOffer);
|
196
|
+
webRtc.gatherCandidates(self.onError);
|
197
|
+
self.webRtcPeer.processAnswer(sdpAnswer);
|
198
|
+
|
199
|
+
yield self.player.play();
|
200
|
+
} catch (e) {
|
201
|
+
self.onError(e);
|
202
|
+
}
|
203
|
+
})();
|
204
|
+
}
|
205
|
+
|
206
|
+
onError(error) {
|
207
|
+
if (error) {
|
208
|
+
console.error(error);
|
209
|
+
this.stopRecording();
|
210
|
+
this.retryCount += 1;
|
211
|
+
if (this.retryCount < 5) {
|
212
|
+
setTimeout(() => {
|
213
|
+
if (this.interval) {
|
214
|
+
this.startRecordingSingleSessionWithInterval(this.interval);
|
215
|
+
}
|
216
|
+
}, 1000 * this.retryCount);
|
217
|
+
} else {
|
218
|
+
throw "Something went wrong with video recording.";
|
219
|
+
}
|
220
|
+
}
|
221
|
+
}
|
222
|
+
|
223
|
+
stopRecording() {
|
224
|
+
if (this.recorder) {
|
225
|
+
this.recorder.stop();
|
226
|
+
this.recorder = null;
|
227
|
+
}
|
228
|
+
if (this.webRtcPeer) {
|
229
|
+
this.webRtcPeer.dispose();
|
230
|
+
this.webRtcPeer = null;
|
231
|
+
}
|
232
|
+
if (this.pipeline) {
|
233
|
+
this.pipeline.release();
|
234
|
+
this.pipeline = null;
|
235
|
+
}
|
236
|
+
}
|
237
|
+
|
238
|
+
startRecordingSingleSessionWithInterval(interval = 30000) {
|
239
|
+
if (typeof interval !== "number" || interval < 30000) {
|
240
|
+
// In ms
|
241
|
+
throw "Interval for single session must be more than or equal to 30sec.";
|
242
|
+
}
|
243
|
+
this.interval = interval;
|
244
|
+
this.startRecordingSingleSession();
|
245
|
+
const self = this;
|
246
|
+
setInterval(() => {
|
247
|
+
console.log("retry");
|
248
|
+
if (self.recorder || self.pipeline || self.webRtcPeer) {
|
249
|
+
self.stopRecording();
|
250
|
+
}
|
251
|
+
setTimeout(() => {
|
252
|
+
self.startRecordingSingleSession();
|
253
|
+
}, 300);
|
254
|
+
}, interval + 300);
|
255
|
+
}
|
256
|
+
|
257
|
+
recordAndPlaySessionWithTimeout(timeout = 10000, videInput, videoOutput) {
|
258
|
+
if (typeof timeout !== "number" || timeout < 10000) {
|
259
|
+
throw "Timeout for single session must be more than or equal to 10sec.";
|
260
|
+
}
|
261
|
+
if (videInput) {
|
262
|
+
this.inputVideoElm = document.getElementById(videInput);
|
263
|
+
this.showInputVideo = true;
|
264
|
+
}
|
265
|
+
this.startRecordingSingleSession();
|
266
|
+
this.setupTimeDilation();
|
267
|
+
setTimeout(() => {
|
268
|
+
this.stopAndPlayVideoAfterTimeAddition(videoOutput);
|
269
|
+
}, timeout); // extra buffer of 300ms
|
270
|
+
}
|
271
|
+
|
272
|
+
stopAndPlayVideoAfterTimeAddition(videoOutput) {
|
273
|
+
setTimeout(() => this.stopAndPlayVideo(videoOutput), this.timeDilation);
|
274
|
+
}
|
275
|
+
|
276
|
+
setupTimeDilation() {
|
277
|
+
this.timeDilation = 0;
|
278
|
+
this.timeDilationInterval = setInterval(() => {
|
279
|
+
this.timeDilation += 1000;
|
280
|
+
}, 1000);
|
281
|
+
}
|
282
|
+
|
283
|
+
clearTimeDilation() {
|
284
|
+
if (this.timeDilationInterval) clearInterval(this.timeDilationInterval);
|
285
|
+
}
|
286
|
+
}
|