From 1a80db7b8a62f0a33b9ce98d007ac862363f94c1 Mon Sep 17 00:00:00 2001 From: Vibhu Dujari Date: Mon, 29 Apr 2024 13:27:17 +0530 Subject: [PATCH] integrate monitorer package with face detection --- app/index.html | 3 +- app/models/contest-attempt.js | 6 + app/models/contest.js | 1 + .../full-screen-contest-view/component.js | 5 + .../full-screen-contest-view/template.hbs | 26 ++- .../intermediate-contest-view/component.js | 22 +- .../intermediate-contest-view/template.hbs | 17 +- .../contests/contest/attempt/controller.js | 36 +++ app/pods/contests/contest/attempt/route.js | 9 + .../contests/contest/attempt/template.hbs | 3 +- app/pods/contests/contest/index/controller.js | 3 +- app/pods/contests/contest/index/route.js | 3 + app/pods/contests/contest/index/template.hbs | 3 +- app/services/monitorer.js | 217 +++++++++--------- app/styles/app.scss | 9 + 15 files changed, 243 insertions(+), 120 deletions(-) diff --git a/app/index.html b/app/index.html index 7fa5ada1..45281ea2 100644 --- a/app/index.html +++ b/app/index.html @@ -57,7 +57,8 @@ 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f); })(window, document, 'script', 'dataLayer', 'GTM-TWLQ78S'); - + + {{content-for "head-footer"}} diff --git a/app/models/contest-attempt.js b/app/models/contest-attempt.js index 2cf156b2..6f99a76c 100644 --- a/app/models/contest-attempt.js +++ b/app/models/contest-attempt.js @@ -28,4 +28,10 @@ export default Model.extend({ windowResizeTimePenaltyMinutes: Ember.computed('monitorerData', function() { return this.monitorerData && this.monitorerData['window-resize-count'] * 10 }), + noFaceCount: Ember.computed('monitorerData', function() { + return this.monitorerData && this.monitorerData['no-face-count'] + }), + noFaceTimePenaltyMinutes: Ember.computed('monitorerData', function() { + return this.monitorerData && this.monitorerData['no-face-count'] * 10 + }), }); diff --git a/app/models/contest.js b/app/models/contest.js index 1d105531..15002963 100644 --- a/app/models/contest.js +++ b/app/models/contest.js @@ -63,4 +63,5 @@ export default Model.extend({ allowPaste: DS.attr(), disallowTabSwitch: DS.attr(), disallowWindowResize: DS.attr(), + disallowNoFace: DS.attr(), }); diff --git a/app/pods/components/full-screen-contest-view/component.js b/app/pods/components/full-screen-contest-view/component.js index e468c094..22fcf6e9 100644 --- a/app/pods/components/full-screen-contest-view/component.js +++ b/app/pods/components/full-screen-contest-view/component.js @@ -4,4 +4,9 @@ import { inject as service } from '@ember/service'; export default class FullScreenContestView extends Component { @service monitorer showSubmitDialog = false + + didReceiveAttrs() { + this._super(...arguments) + this.setupMonitorer() + } } diff --git a/app/pods/components/full-screen-contest-view/template.hbs b/app/pods/components/full-screen-contest-view/template.hbs index e61fb3e3..484a722e 100644 --- a/app/pods/components/full-screen-contest-view/template.hbs +++ b/app/pods/components/full-screen-contest-view/template.hbs @@ -1,7 +1,7 @@
-
+

{{contest.name}}

@@ -25,7 +25,12 @@ {{/if}} {{#if contest.disallowWindowResize}}
  • - Window Resizing Prohibited You will get a penalty of 10 mins every 10 secs if your window is not fullscreen + Window Resizing Prohibited You will get a penalty of 10 mins every 10 secs if your window is not fullscreen. +
  • + {{/if}} + {{#if contest.disallowNoFace}} +
  • + Face Detection Enabled You will get a penalty of 10 mins every 5 secs if your face is not visible in camera.
  • {{/if}} @@ -36,8 +41,11 @@
    This is a timed contest. You have limited attempts to attempt this test.
    -
    +
    +
    + +
      @@ -75,6 +83,12 @@
      Total Time Penalty: {{or attempt.windowResizeTimePenaltyMinutes 0}} mins {{/if}} +
      + {{#if contest.disallowWindowResize}} + Total Face Undetected: {{or attempt.noFaceCount 0}} +
      + Total Time Penalty: {{or attempt.noFaceTimePenaltyMinutes 0}} mins + {{/if}}
    @@ -129,6 +143,10 @@
    Your Window is not Fullscreened. 10 minutes deducted.
    - + + +
    + No face detected in camera. 10 minutes deducted. +
    \ No newline at end of file diff --git a/app/pods/components/intermediate-contest-view/component.js b/app/pods/components/intermediate-contest-view/component.js index a25a1664..71bb4ac5 100644 --- a/app/pods/components/intermediate-contest-view/component.js +++ b/app/pods/components/intermediate-contest-view/component.js @@ -1,5 +1,5 @@ import Component from '@ember/component'; -import { computed } from '@ember/object'; +import { computed, action } from '@ember/object'; import { alias } from '@ember/object/computed'; import { restartableTask, dropTask } from 'ember-concurrency-decorators'; import { timeout } from 'ember-concurrency'; @@ -38,6 +38,13 @@ export default class IntermediateContestComponent extends Component { } } + @computed('monitorerError') + get monitorerErrorText() { + switch(this.monitorerError) { + case "CAMERAACCESSDENIED": return 'Please grant camera permissions to continue with test.' + } + } + didReceiveAttrs() { if (this.contest.acceptRegistrations) { this.fetchRegistrationTask.perform() @@ -94,4 +101,17 @@ export default class IntermediateContestComponent extends Component { throw err } } + + @action promptCameraPermission() { + navigator.mediaDevices.getUserMedia ({video: true}, + // successCallback + function() { + this.set('monitorerError', '') + }, + + // errorCallback + function(err) { + console.log(err) + }) + } } diff --git a/app/pods/components/intermediate-contest-view/template.hbs b/app/pods/components/intermediate-contest-view/template.hbs index 84fd5271..f52a6d91 100644 --- a/app/pods/components/intermediate-contest-view/template.hbs +++ b/app/pods/components/intermediate-contest-view/template.hbs @@ -1,4 +1,11 @@ -
    +
    + {{#if monitorerErrorText}} +
    + {{monitorerErrorText}} +
    + {{/if}}
    @@ -79,7 +86,7 @@
    {{/if}}
    - {{#if (or contest.disallowTabSwitch contest.disallowWindowResize)}} + {{#if (or contest.disallowTabSwitch contest.disallowWindowResize contest.disallowNoFace)}}
    {{#if contest.disallowTabSwitch}} Tab Switching is prohibited on this contest. You will face a penalty of 10 mins in case you :- @@ -96,6 +103,12 @@
  • • Resize the browser window
  • {{/if}} + {{#if contest.disallowWindowResize}} + Face detection is enabled on this contest. You will face a penalty of 10 mins every 5 secs in case :- +
      +
    • • Your face is not visible in camera.
    • +
    + {{/if}}
    {{/if}}
    diff --git a/app/pods/contests/contest/attempt/controller.js b/app/pods/contests/contest/attempt/controller.js index 210989ea..50218340 100644 --- a/app/pods/contests/contest/attempt/controller.js +++ b/app/pods/contests/contest/attempt/controller.js @@ -6,6 +6,42 @@ import { later } from '@ember/runloop'; export default class AttemptController extends Controller{ @service api + @service monitorer + @service router + + isMonitorerSet = false + + init() { + this._super(...arguments) + + this.setupMonitorer = this.setupMonitorer.bind(this) + } + + async setupMonitorer() { + if(this.isMonitorerSet) return + + await this.monitorer.setup({ + contest: this.contest, + onError: this.onMonitorerError.bind(this) + }) + + this.set('isMonitorerSet', true) + } + + async onMonitorerError(detail) { + await this.monitorer.disable() + this.set('isMonitorerSet', false) + + switch(detail.code) { + case "CAMERAACCESSDENIED": + this.transitionToRoute('contests.contest', this.contest.id, { + queryParams: { + monitorerError: detail.code + } + }) + break; + } + } @dropTask submitTask = function* () { later(() => { diff --git a/app/pods/contests/contest/attempt/route.js b/app/pods/contests/contest/attempt/route.js index e1591789..0bbe5108 100644 --- a/app/pods/contests/contest/attempt/route.js +++ b/app/pods/contests/contest/attempt/route.js @@ -5,6 +5,7 @@ import { action } from '@ember/object'; export default class AttemptRoute extends Route { @service navigation; @service currentUser; + @service monitorer async beforeModel() { super.beforeModel() @@ -61,4 +62,12 @@ export default class AttemptRoute extends Route { } throw err } + @action + async willTransition(transition) { + this._super(...arguments) + if(!transition.to.name.includes('contests.contest.attempt.content')) { + this.controller.set('isMonitorerSet', false) + await this.monitorer.disable() + } + } } diff --git a/app/pods/contests/contest/attempt/template.hbs b/app/pods/contests/contest/attempt/template.hbs index 835b47be..7083c425 100644 --- a/app/pods/contests/contest/attempt/template.hbs +++ b/app/pods/contests/contest/attempt/template.hbs @@ -3,6 +3,7 @@ @attempt={{contest.currentAttempt}} @onTimerEnd={{onTimerEnd}} @submitTask={{submitTask}} - @contest={{contest}}> + @contest={{contest}} + @setupMonitorer={{setupMonitorer}}> {{outlet}} \ No newline at end of file diff --git a/app/pods/contests/contest/index/controller.js b/app/pods/contests/contest/index/controller.js index f3bd1b09..6346ddeb 100644 --- a/app/pods/contests/contest/index/controller.js +++ b/app/pods/contests/contest/index/controller.js @@ -7,12 +7,13 @@ export default class IndexController extends Controller { @service store @service router - queryParams = ['offset', 'limit', 'status', 'difficulty', 'tags', 'q'] + queryParams = ['offset', 'limit', 'status', 'difficulty', 'tags', 'q', 'monitorerError'] offset = 0 limit = 10 difficulty = [] tags = [] q = '' + monitorerError = null @computed('offset', 'limit') get page() { diff --git a/app/pods/contests/contest/index/route.js b/app/pods/contests/contest/index/route.js index 83a9960b..a1c75ca7 100644 --- a/app/pods/contests/contest/index/route.js +++ b/app/pods/contests/contest/index/route.js @@ -22,6 +22,9 @@ export default class IndexRoute extends Route { }, q: { refreshModel: true + }, + monitorerError: { + refreshModel: false } } diff --git a/app/pods/contests/contest/index/template.hbs b/app/pods/contests/contest/index/template.hbs index 2b75dc87..1e12aa22 100644 --- a/app/pods/contests/contest/index/template.hbs +++ b/app/pods/contests/contest/index/template.hbs @@ -11,7 +11,8 @@ @nextRoute={{nextRoute}} @handleUnverifiedEmail={{handleUnverifiedEmail}} @onAfterCreate={{onAfterCreate}} - @contest={{contest}} /> + @contest={{contest}} + @monitorerError={{monitorerError}} /> {{else}}
    diff --git a/app/services/monitorer.js b/app/services/monitorer.js index c6d6881a..0efe951a 100644 --- a/app/services/monitorer.js +++ b/app/services/monitorer.js @@ -1,122 +1,119 @@ import Service from '@ember/service'; import { inject as service } from '@ember/service'; import { computed } from '@ember/object'; -import { timeout } from 'ember-concurrency'; -import { later } from '@ember/runloop'; +// import Monitorer from '@coding-blocks/monitorer'; export default Service.extend({ router: service(), api: service(), store: service(), - isTabSwitchEventListenerAdded: false, tabSwitchTrigger: false, + noFaceTrigger: false, windowResizeTrigger: false, - isWindowResizeEventThrottled: false, - isBrowserFullScreened: window.screen.availHeight === window.outerHeight && window.screen.availWidth === window.outerWidth, - monitoredRoutes: [ - 'contests.contest.attempt.content.problem', - 'contests.contest.attempt.content.quiz', - // 'contests.contest.attempt.content.project', - ], - windowResizeInterval: null, - isCurrentRouteMonitored: computed('router.currentRouteName', function(){ - return this.monitoredRoutes.includes(this.router.currentRouteName) - }), - isTabSwitchDisabledOnContest: computed('router.currentRoute.attributes.contest', function() { - if(this.router.currentRoute) { - return !!this.router.get('currentRoute.attributes.contest.disallowTabSwitch') - } else return false - }), - isWindowResizeDisabledOnContest: computed('router.currentRoute.attributes.contest', function() { - if(this.router.currentRoute) { - return !!this.router.get('currentRoute.attributes.contest.disallowWindowResize') - } else return false - }), - isMonitoringEnabled: computed('isCurrentRouteMonitored', 'isTabSwitchDisabledOnContest', 'isWindowResizeDisabledOnContest', function() { - return this.isCurrentRouteMonitored && (this.isTabSwitchDisabledOnContest || this.isWindowResizeDisabledOnContest) - }), + noFaceThrottled: false, + noFaceDetected: false, + isMonitorerFaultEventHandlerAdded: false, + failureRedirect: null, init() { this._super(...arguments) - this.tabSwitchEventHandler = this.tabSwitchEventHandler.bind(this) - this.windowResizeEventHandler = this.windowResizeEventHandler.bind(this) - this.windowResizeFaultReporter = this.windowResizeFaultReporter.bind(this) - this.addObserver('isMonitoringEnabled', this, 'enableOrDisableMonitorerEvents') - this.enableOrDisableMonitorerEvents() + this.monitorerFaultEventHandler = this.monitorerFaultEventHandler.bind(this) + this.monitorerErrorEventHandler = this.monitorerErrorEventHandler.bind(this) + this.monitorerSuccessEventHandler = this.monitorerSuccessEventHandler.bind(this) }, - setIsBrowserFullScreened() { - this.set('isBrowserFullScreened', window.screen.availHeight <= window.outerHeight && window.screen.availWidth <= window.outerWidth) - }, + async setup(options) { + if(!this.monitorer) { + this.set('monitorer', new Monitorer()) + } - async enableOrDisableMonitorerEvents() { - console.log('enable monitorer') - this.setIsBrowserFullScreened() - - if(!this.isMonitoringEnabled) { - if(this.isTabSwitchEventListenerAdded) { - document.removeEventListener('visibilitychange', this.tabSwitchEventHandler) - this.set('isTabSwitchEventListenerAdded', false) - } + this.set('contest', options.contest) + this.set('onError', options.onError) - if(this.isWindowResizeEventListenerAdded) { - window.removeEventListener('resize', this.windowResizeEventHandler) - return this.set('isWindowResizeEventListenerAdded', false) - } - if(this.windowResizeInterval) { - clearInterval(this.windowResizeInterval) - this.set('windowResizeInterval', null) - } + if(!this.isMonitorerFaultEventHandlerAdded) { + window.addEventListener('monitorerfault', this.monitorerFaultEventHandler) + window.addEventListener('monitorererror', this.monitorerErrorEventHandler) + window.addEventListener('monitorersuccess', this.monitorerSuccessEventHandler) } - - if(this.isTabSwitchDisabledOnContest && !this.isTabSwitchEventListenerAdded) { - this.setTabSwitchEvents() - this.set('isTabSwitchEventListenerAdded', true) + + + if(this.contest.disallowTabSwitch) { + await this.enableTabSwitchMonitorer() } - - if(this.isWindowResizeDisabledOnContest && !this.isWindowResizeEventListenerAdded) { - this.setWindowResizeEvents() - this.set('isWindowResizeEventListenerAdded', true) - later(() => { - }) + + if(this.contest.disallowWindowResize) { + await this.enableWindowResizeMonitorer() + } + + if(this.contest.disallowNoFace) { + await this.enableNoFaceMonitorer({ noFace: true }) } }, - clearPreviousEventListeners() { - getEventListeners(document) + async disable() { + this.set('contest', null) + this.set('onError', null) + + await this.disableTabSwitchMonitorer() + await this.disableWindowResizeMonitorer() + await this.disableNoFaceMonitorer() + + window.removeEventListener('monitorerfault', this.monitorerFaultEventHandler) + }, + + async enableTabSwitchMonitorer() { + await this.monitorer.enable({ tabSwitch: true }) }, - async setTabSwitchEvents() {//called based on route activation - const currentAttempt = await this.router.get('currentRoute.attributes.contest.currentAttempt') + async enableWindowResizeMonitorer() { + await this.monitorer.enable({ windowResize: true }) + }, + + async enableNoFaceMonitorer() { + await this.monitorer.enable({ noFace: true }) + await this.monitorer.enable({ liveFeed: true }) + }, + + async disableTabSwitchMonitorer() { + await this.monitorer.disable({ tabSwitch: true }) + }, + + async disableWindowResizeMonitorer() { + await this.monitorer.disable({ windowResize: true }) + }, + + async disableNoFaceMonitorer() { + await this.monitorer.disable({ noFace: true }) + await this.monitorer.disable({ liveFeed: true }) + }, + + async monitorerFaultEventHandler(e) { + const currentAttempt = await this.contest.currentAttempt if(!!!currentAttempt.id) return - if('webkitHidden' in document) { - document.addEventListener("webkitvisibilitychange", this.tabSwitchEventHandler); - console.log('webkitvisibilitychange event added') - } else { - document.addEventListener("visibilitychange", this.tabSwitchEventHandler); - console.log('visibilitychange event added') + switch(e.detail.code) { + case "TAB_SWITCHED": await this.handleTabSwitchFault(); break; + case "WINDOW_RESIZED": await this.handleWindowResizeFault(e.detail); break; + case "NO_FACE_DETECTED": await this.handleNoFaceFault(e.detail); + this.set('noFaceDetected', true); break; } }, - - async setWindowResizeEvents() {//called based on route activation - const currentAttempt = await this.router.get('currentRoute.attributes.contest.currentAttempt') - if(!!!currentAttempt.id) return - if(!this.isBrowserFullScreened) { - if(!this.windowResizeInterval) { - this.windowResizeFaultReporter(true) - this.set('windowResizeInterval', setInterval(this.windowResizeFaultReporter, 10000)) - } + async monitorerErrorEventHandler(e) { + if(this.onError) { + this.onError(e.detail) } - window.addEventListener("resize", this.windowResizeEventHandler); - }, - async tabSwitchEventHandler() { - console.log('tabSwitchEventHandler', document.hidden, document.webkitHidden) + async monitorerSuccessEventHandler(e) { + switch(e.detail.code) { + case "ONEFACEDETECTED": this.set('noFaceDetected', false); break; + } + }, + + async handleTabSwitchFault() { if(!document.hidden) return this.set('tabSwitchTrigger', true) - const currentAttempt = await this.router.get('currentRoute.attributes.contest.currentAttempt') + const currentAttempt = await this.contest.currentAttempt await this.api.request(`/contest-attempts/${currentAttempt.id}/report-monitorer-fault`, { method: 'POST', data: { @@ -126,30 +123,11 @@ export default Service.extend({ await this.store.findRecord('contest-attempt', currentAttempt.id) }, - async windowResizeEventHandler() { - if(!this.isWindowResizeEventThrottled) { - this.set('isWindowResizeEventThrottled', true) - if(!this.windowResizeInterval) { - this.windowResizeFaultReporter(true) - this.set('windowResizeInterval', setInterval(this.windowResizeFaultReporter, 10000)) - } - //to prevent multiple event hits if user is continuously resizing - setTimeout(() => this.set('isWindowResizeEventThrottled', false), 2500) - } - }, - - async windowResizeFaultReporter(delayTrigger = false) { - this.setIsBrowserFullScreened() - if(this.isBrowserFullScreened) { - this.set('isWindowResizeEventThrottled', false) - if(this.windowResizeInterval) { - clearInterval(this.windowResizeInterval) - this.set('windowResizeInterval', null) - } - } - else { + async handleWindowResizeFault(details) { + if(details.message === 'browser_unfullscreened') { this.set('windowResizeTrigger', true) - const currentAttempt = await this.router.get('currentRoute.attributes.contest.currentAttempt') + + const currentAttempt = await this.contest.currentAttempt await this.api.request(`/contest-attempts/${currentAttempt.id}/report-monitorer-fault`, { method: 'POST', data: { @@ -159,4 +137,25 @@ export default Service.extend({ await this.store.findRecord('contest-attempt', currentAttempt.id) } }, + + async handleNoFaceFault(details) { + if(!this.noFaceThrottled) { + this.set('noFaceThrottled', true) + setTimeout(() => this.set('noFaceThrottled', false), 5000) + + this.set('noFaceTrigger', true) + + const currentAttempt = await this.contest.currentAttempt + const imgFileArray = new Uint8Array(await details.imageBlob.arrayBuffer()) + + await this.api.request(`/contest-attempts/${currentAttempt.id}/report-monitorer-fault`, { + method: 'POST', + data: { + fault_type: 'no_face', + image_file_array: imgFileArray + } + }) + await this.store.findRecord('contest-attempt', currentAttempt.id) + } + } }); diff --git a/app/styles/app.scss b/app/styles/app.scss index 7d088762..d2125047 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -70,10 +70,19 @@ body{ width: 250%; } +.top-left { + top: 0; + left: 0; +} + .border-dark-pink { border: solid 2px #f24f5f; } +.border-green { + border: solid 2px #2cc528; +} + .red { color: #f24f5f !important; }