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,66 @@
|
|
1
|
+
let channelElm = document.getElementById('channel-data');
|
2
|
+
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia || navigator.mediaDevices.getUserMedia;
|
3
|
+
let connection = new RTCMultiConnection();
|
4
|
+
connection.socketURL = channelElm.dataset.mediaServerUrl;
|
5
|
+
|
6
|
+
connection.session = {
|
7
|
+
audio: false,
|
8
|
+
video: true
|
9
|
+
};
|
10
|
+
|
11
|
+
connection.sdpConstraints.mandatory = {
|
12
|
+
OfferToReceiveAudio: false,
|
13
|
+
OfferToReceiveVideo: true
|
14
|
+
};
|
15
|
+
|
16
|
+
connection.extra = {
|
17
|
+
userType: 'proctor',
|
18
|
+
};
|
19
|
+
|
20
|
+
connection.onstream = function(event) {
|
21
|
+
const video = event.mediaElement;
|
22
|
+
if(event && event.userid) {
|
23
|
+
if(event.extra && event.extra.userType === 'proctor') {
|
24
|
+
return;
|
25
|
+
}
|
26
|
+
video.id = event.userid;
|
27
|
+
const videoContainer = document.getElementById('video-container');
|
28
|
+
const userVideoElm = document.getElementById(event.userid);
|
29
|
+
if(userVideoElm) {
|
30
|
+
userVideoElm.parentNode.removeChild(userVideoElm);
|
31
|
+
}
|
32
|
+
videoContainer.querySelector(".video-div").prepend(video);
|
33
|
+
const statusElm = videoContainer.querySelector('.video-status');
|
34
|
+
statusElm.innerHTML = `Status: ${event.status}`;
|
35
|
+
}
|
36
|
+
};
|
37
|
+
connection.onUserStatusChanged = function (event) {
|
38
|
+
const userVideoElm = document.getElementById(event.userid);
|
39
|
+
if (userVideoElm && event && event.status) {
|
40
|
+
userVideoElm.parentElement.style.backgroundColor =
|
41
|
+
event.status === "online" ? "#34A853" : "#EA4335";
|
42
|
+
const status = document.getElementById(`video-status-${event.userid}`);
|
43
|
+
if (status && event.status) status.innerHTML = `Status: ${event.status}`;
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
connection.onstreamended = function(event) {
|
48
|
+
let video = document.getElementById(event.userid);
|
49
|
+
if (video && video.parentNode) {
|
50
|
+
video.parentNode.removeChild(video);
|
51
|
+
}
|
52
|
+
};
|
53
|
+
|
54
|
+
|
55
|
+
let url = new URL(window.location);
|
56
|
+
let channel = channelElm.dataset.channel;
|
57
|
+
if (channel) {
|
58
|
+
connection.dontCaptureUserMedia = true;
|
59
|
+
navigator.getUserMedia({
|
60
|
+
video: true,
|
61
|
+
audio: false
|
62
|
+
}, function(externalStream) {
|
63
|
+
connection.attachStreams = [externalStream];
|
64
|
+
connection.openOrJoin(channel);
|
65
|
+
}, function(error) {});
|
66
|
+
}
|
@@ -0,0 +1,131 @@
|
|
1
|
+
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia || navigator.mediaDevices.getUserMedia;
|
2
|
+
const channelDiv = document.getElementById("channel-data");
|
3
|
+
let connection = new RTCMultiConnection();
|
4
|
+
connection.socketURL = `https://${channelDiv.dataset.mediaServerUrl}/`;
|
5
|
+
var listOfRecorders = {};
|
6
|
+
|
7
|
+
connection.session = {
|
8
|
+
audio: false,
|
9
|
+
video: true
|
10
|
+
};
|
11
|
+
|
12
|
+
connection.sdpConstraints.mandatory = {
|
13
|
+
OfferToReceiveAudio: false,
|
14
|
+
OfferToReceiveVideo: true
|
15
|
+
};
|
16
|
+
|
17
|
+
// connection.setUserPreferences = function(userPreferences) {
|
18
|
+
// userPreferences.dontAttachLocalStream = true;
|
19
|
+
// return userPreferences;
|
20
|
+
// };
|
21
|
+
|
22
|
+
connection.onstream = function(event) {
|
23
|
+
if(event && event.userid) {
|
24
|
+
if (event.extra && event.extra.userType === "proctor") {
|
25
|
+
return;
|
26
|
+
}
|
27
|
+
const userVideoElm = document.getElementById(event.userid);
|
28
|
+
if (userVideoElm) {
|
29
|
+
userVideoElm.parentNode.removeChild(userVideoElm);
|
30
|
+
}
|
31
|
+
const videoContainer = document.getElementById('video-container');
|
32
|
+
let div = document.createElement('div');
|
33
|
+
div.id = event.userid;
|
34
|
+
div.className = 'video-div';
|
35
|
+
div.appendChild(event.mediaElement); // appending VIDOE to DIV
|
36
|
+
|
37
|
+
let h2 = document.createElement('h4');
|
38
|
+
h2.innerHTML = `UserID: ${event.extra.userId}`;
|
39
|
+
div.appendChild(h2);
|
40
|
+
|
41
|
+
let pElm = document.createElement('p');
|
42
|
+
pElm.id = `video-status-${event.userid}`;
|
43
|
+
pElm.innerHTML = `Status: ${event.status}`;
|
44
|
+
div.appendChild(pElm);
|
45
|
+
|
46
|
+
let pElm2 = document.createElement('p');
|
47
|
+
pElm2.id = `video-recording-status-${event.userid}`;
|
48
|
+
pElm2.innerHTML = `Status: Recording...`;
|
49
|
+
div.appendChild(pElm2);
|
50
|
+
|
51
|
+
let button = document.createElement('button');
|
52
|
+
button.id = `video-recording-stop-${event.userid}`;
|
53
|
+
button.dataset = { userid: event.userid };
|
54
|
+
button.style.color = '#000';
|
55
|
+
button.innerHTML = `Stop Recording`;
|
56
|
+
div.appendChild(button);
|
57
|
+
|
58
|
+
div.style.backgroundColor = '#34A853';
|
59
|
+
div.style.textAlign = 'center';
|
60
|
+
div.style.color = '#fff';
|
61
|
+
|
62
|
+
videoContainer.appendChild(div);
|
63
|
+
|
64
|
+
|
65
|
+
/** Recording **/
|
66
|
+
let recorder = RecordRTC(event.stream, {
|
67
|
+
type: 'video',
|
68
|
+
recorderType: MediaStreamRecorder
|
69
|
+
});
|
70
|
+
|
71
|
+
recorder.startRecording();
|
72
|
+
|
73
|
+
listOfRecorders[event.userid] = recorder;
|
74
|
+
|
75
|
+
console.log(button);
|
76
|
+
|
77
|
+
button.addEventListener("click", function(){
|
78
|
+
let streamid = event.userid;
|
79
|
+
|
80
|
+
if(!listOfRecorders[streamid]) throw 'Wrong stream-id';
|
81
|
+
|
82
|
+
let recorder = listOfRecorders[streamid];
|
83
|
+
recorder.stopRecording(function() {
|
84
|
+
let blob = recorder.getBlob();
|
85
|
+
invokeSaveAsDialog(blob);
|
86
|
+
// window.open( URL.createObjectURL(blob) );
|
87
|
+
// or upload to server
|
88
|
+
// var formData = new FormData();
|
89
|
+
// formData.append('file', blob);
|
90
|
+
// $.post('/server-address', formData, serverCallback);
|
91
|
+
});
|
92
|
+
});
|
93
|
+
}
|
94
|
+
};
|
95
|
+
connection.onUserStatusChanged = function (event) {
|
96
|
+
const userVideoElm = document.getElementById(event.userid);
|
97
|
+
if(userVideoElm && event && event.status) {
|
98
|
+
userVideoElm.style.backgroundColor = event.status === 'online' ? '#34A853' : '#EA4335';
|
99
|
+
const status = document.getElementById(`video-status-${event.userid}`);
|
100
|
+
if(status && event.status) status.innerHTML = `Status: ${event.status}`;
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
connection.onstreamended = function(event) {
|
105
|
+
let video = document.getElementById(event.userid);
|
106
|
+
if (video && video.parentNode) {
|
107
|
+
video.parentNode.removeChild(video);
|
108
|
+
}
|
109
|
+
};
|
110
|
+
|
111
|
+
const connectVideos = () => {
|
112
|
+
setTimeout(function() {
|
113
|
+
if(channels.length) {
|
114
|
+
const channel = channels.shift()
|
115
|
+
navigator.getUserMedia({
|
116
|
+
video: true,
|
117
|
+
audio: false
|
118
|
+
}, function(externalStream) {
|
119
|
+
connection.attachStreams = [externalStream];
|
120
|
+
connection.openOrJoin(channel);
|
121
|
+
}, function(error) {});
|
122
|
+
connectVideos();
|
123
|
+
}
|
124
|
+
}, 3000)
|
125
|
+
}
|
126
|
+
|
127
|
+
|
128
|
+
let channels = [];
|
129
|
+
channels = JSON.parse(channelDiv.dataset.streamChannels);
|
130
|
+
connection.dontCaptureUserMedia = true;
|
131
|
+
connectVideos()
|
@@ -0,0 +1,172 @@
|
|
1
|
+
class VideoRecorder {
|
2
|
+
constructor(eventId, userId) {
|
3
|
+
this.eventId = eventId;
|
4
|
+
this.userId = userId;
|
5
|
+
let videoRecordingInfoElm = document.getElementById('video-recording-info');
|
6
|
+
this.appName = videoRecordingInfoElm.dataset.appName;
|
7
|
+
this.recordEndpoint = videoRecordingInfoElm.dataset.recordEndpoint;
|
8
|
+
this.uploadUrl = videoRecordingInfoElm.dataset.uploadUrl;
|
9
|
+
this.proctorResp = {};
|
10
|
+
this.listOfRecorders = {};
|
11
|
+
}
|
12
|
+
|
13
|
+
connectVideo() {
|
14
|
+
const { eventId, userId } = this;
|
15
|
+
fetch(
|
16
|
+
`/proctoring/video_streamings/user_channel.json?event_id=${eventId}&user_id=${userId}`
|
17
|
+
)
|
18
|
+
.then((response) => response.json())
|
19
|
+
.then((data) => {
|
20
|
+
this.proctorResp = data;
|
21
|
+
this.startWebrtcAndRecord();
|
22
|
+
});
|
23
|
+
|
24
|
+
}
|
25
|
+
|
26
|
+
startWebrtcAndRecord() {
|
27
|
+
const {
|
28
|
+
eventId,
|
29
|
+
userId,
|
30
|
+
proctorResp,
|
31
|
+
appName,
|
32
|
+
listOfRecorders,
|
33
|
+
recordEndpoint,
|
34
|
+
uploadUrl,
|
35
|
+
} = this;
|
36
|
+
const userConferenceId = `proctoring-video_streaming-${appName}-${eventId}-${userId}`;
|
37
|
+
const connection = new RTCMultiConnection();
|
38
|
+
window.listOfRecorders = listOfRecorders;
|
39
|
+
|
40
|
+
connection.socketURL = proctorResp.socketURL;
|
41
|
+
connection.session = {
|
42
|
+
audio: false,
|
43
|
+
video: true,
|
44
|
+
};
|
45
|
+
|
46
|
+
connection.sdpConstraints.mandatory = {
|
47
|
+
OfferToReceiveAudio: false,
|
48
|
+
OfferToReceiveVideo: true,
|
49
|
+
};
|
50
|
+
|
51
|
+
connection.userid = userConferenceId;
|
52
|
+
|
53
|
+
connection.extra = {
|
54
|
+
eventId,
|
55
|
+
userId,
|
56
|
+
};
|
57
|
+
|
58
|
+
const supports = navigator.mediaDevices.getSupportedConstraints();
|
59
|
+
if (supports.width && supports.height) {
|
60
|
+
connection.applyConstraints({
|
61
|
+
video: {
|
62
|
+
width: 320,
|
63
|
+
height: 240,
|
64
|
+
},
|
65
|
+
});
|
66
|
+
}
|
67
|
+
|
68
|
+
const resolutions = "VGA";
|
69
|
+
let videoConstraints = {};
|
70
|
+
|
71
|
+
if (resolutions === "VGA") {
|
72
|
+
videoConstraints = {
|
73
|
+
width: { min: 320, ideal: 320 },
|
74
|
+
height: { min: 240 },
|
75
|
+
frameRate: 15,
|
76
|
+
facingMode: "user",
|
77
|
+
};
|
78
|
+
}
|
79
|
+
|
80
|
+
connection.mediaConstraints = {
|
81
|
+
video: videoConstraints,
|
82
|
+
audio: true,
|
83
|
+
};
|
84
|
+
|
85
|
+
connection.setUserPreferences = (userPreferences) => {
|
86
|
+
// eslint-disable-next-line no-param-reassign
|
87
|
+
userPreferences.dontGetRemoteStream = true;
|
88
|
+
return userPreferences;
|
89
|
+
};
|
90
|
+
|
91
|
+
const recordAndUploadVideo = (event) => {
|
92
|
+
// eslint-disable-next-line no-undef
|
93
|
+
const recorder = RecordRTC(event.stream, {
|
94
|
+
type: "video",
|
95
|
+
// eslint-disable-next-line no-undef
|
96
|
+
// recorderType: MediaStreamRecorder,
|
97
|
+
timeSlice: 1000 * 60 * 1,
|
98
|
+
canvas: {
|
99
|
+
width: 320,
|
100
|
+
height: 240,
|
101
|
+
},
|
102
|
+
frameRate: 5,
|
103
|
+
mimeType: "video/x-matroska;codecs=avc1",
|
104
|
+
ondataavailable(blob) {
|
105
|
+
const fileOfBlob = new File(
|
106
|
+
[blob],
|
107
|
+
`videorecording-${appName}-${eventId}-${userId}-${+new Date()}.webm`,
|
108
|
+
{ type: "video/webm" }
|
109
|
+
);
|
110
|
+
console.log(blob, fileOfBlob);
|
111
|
+
invokeSaveAsDialog(blob);
|
112
|
+
// const upload = new ActiveStorage.DirectUpload(
|
113
|
+
// fileOfBlob,
|
114
|
+
// uploadUrl,
|
115
|
+
// this
|
116
|
+
// );
|
117
|
+
// upload.create((error, blob) => {
|
118
|
+
// if (error) {
|
119
|
+
// console.log(error);
|
120
|
+
// } else {
|
121
|
+
// console.log(blob);
|
122
|
+
|
123
|
+
// const csrfElement = document.getElementsByName("csrf-token")[0];
|
124
|
+
// const csrfToken = csrfElement ? csrfElement.content : "";
|
125
|
+
|
126
|
+
// const submitData = {
|
127
|
+
// video_streaming: {
|
128
|
+
// videos: [blob.signed_id],
|
129
|
+
// user_id: userId,
|
130
|
+
// },
|
131
|
+
// };
|
132
|
+
|
133
|
+
// console.log(submitData);
|
134
|
+
|
135
|
+
// // fetch(`${recordEndpoint}/${proctorResp.id}/upload_video`, {
|
136
|
+
// // method: "POST",
|
137
|
+
// // headers: {
|
138
|
+
// // "Content-Type": "application/json",
|
139
|
+
// // "X-Requested-With": "XMLHttpRequest",
|
140
|
+
// // "X-CSRF-TOKEN": csrfToken,
|
141
|
+
// // Accept: "application/json",
|
142
|
+
// // },
|
143
|
+
// // body: JSON.stringify(submitData),
|
144
|
+
// // credentials: "include",
|
145
|
+
// // })
|
146
|
+
// // .then((res) => res.json())
|
147
|
+
// // .then((res) => console.log(res));
|
148
|
+
// }
|
149
|
+
// });
|
150
|
+
},
|
151
|
+
});
|
152
|
+
|
153
|
+
recorder.startRecording();
|
154
|
+
|
155
|
+
listOfRecorders[event.userid] = recorder;
|
156
|
+
};
|
157
|
+
|
158
|
+
connection.onstream = (event) => {
|
159
|
+
recordAndUploadVideo(event);
|
160
|
+
};
|
161
|
+
|
162
|
+
if (proctorResp && proctorResp.channel) {
|
163
|
+
console.log(proctorResp);
|
164
|
+
connection.openOrJoin(proctorResp.channel, () => {
|
165
|
+
setTimeout(() => {
|
166
|
+
const localStream = connection.attachStreams[0];
|
167
|
+
localStream.mute("audio");
|
168
|
+
}, 500);
|
169
|
+
});
|
170
|
+
}
|
171
|
+
}
|
172
|
+
}
|