diff --git a/src/components/pages/Neutral.vue b/src/components/pages/Neutral.vue index 41afe1ec..b593311e 100644 --- a/src/components/pages/Neutral.vue +++ b/src/components/pages/Neutral.vue @@ -425,6 +425,7 @@ import axios from "axios"; import { mapMutations, mapActions, mapState } from "vuex"; import { apiError, apiSuccess, apiErrorRes, apiWarning, apiInfo, clearToastMessages } from "@/util/ErrorMessage.js"; import { playNeutralFinishedSound } from "@/util/SoundMessage.js"; +import { axiosGetWithRetry } from "@/util/network.js"; import MainLayout from "@/layout/MainLayout"; import ExampleImage from "@/components/ui/ExampleImage"; import DialogComponent from '@/components/ui/SubjectDialog.vue' @@ -645,7 +646,7 @@ export default { if (this.isMonocularMode) { this.n_calibrated_cameras = 1 } else { - const res = await axios.get(`/sessions/${this.$route.params.id}/get_n_calibrated_cameras/`, {}) + const res = await axiosGetWithRetry(`/sessions/${this.$route.params.id}/get_n_calibrated_cameras/`, {}) this.n_calibrated_cameras = res.data.data } this.loadSubjectsList(false) @@ -790,7 +791,7 @@ export default { async saveAdvancedSettings() { this.savingAdvancedSettings = true try { - await axios.get( + await axiosGetWithRetry( `/sessions/${this.session.id}/set_metadata/`, { params: this.getAdvancedSettingsMetadataParams(), @@ -817,7 +818,7 @@ export default { if (this.hasSavedAdvancedSettingsMetadata) return try { - await axios.get( + await axiosGetWithRetry( `/sessions/${this.session.id}/set_metadata/`, { params: this.getAdvancedSettingsMetadataParams(), @@ -973,7 +974,7 @@ export default { filter_frequency: this.filter_frequency, }); try { - await axios.get( + await axiosGetWithRetry( `/sessions/${this.session.id}/set_metadata/`, { params: { @@ -983,7 +984,7 @@ export default { } ); - await axios.get( + await axiosGetWithRetry( `/sessions/${this.session.id}/set_subject/`, { params: { @@ -992,7 +993,7 @@ export default { } ) - const res = await axios.get( + const res = await axiosGetWithRetry( `/sessions/${this.session.id}/record/`, { params: { @@ -1005,12 +1006,19 @@ export default { subject_data_sharing: this.data_sharing, subject_pose_model: this.pose_model, }, + }, + { + retries: 2, + backoffFactor: 0.2, + maxJitterMs: 100, + timeout: 4500, } ); this.setTrialId(res.data.id); this.pollStatus(); } catch (error) { apiError(error); + this.busy = false; } } } @@ -1018,7 +1026,7 @@ export default { }, async pollStatus() { try { - const res = await axios.get( + const res = await axiosGetWithRetry( `/sessions/${this.session.id}/neutral_img/` ); switch (res.data.status) { @@ -1034,17 +1042,17 @@ export default { break; } case "error": { - const resTrial = await axios.get(`/trials/${this.trialId}/`); + const resTrial = await axiosGetWithRetry(`/trials/${this.trialId}/`); clearToastMessages(); apiErrorRes(resTrial, "Error in processing neutral pose"); this.busy = false; - const resStatus = await axios.get(`/sessions/${this.$route.params.id}/status/`, {}) + const resStatus = await axiosGetWithRetry(`/sessions/${this.$route.params.id}/status/`, {}) this.n_cameras_connected = resStatus.data.n_cameras_connected this.n_videos_uploaded = resStatus.data.n_videos_uploaded - const resCalibratedCameras = await axios.get(`/sessions/${this.$route.params.id}/get_n_calibrated_cameras/`, {}) + const resCalibratedCameras = await axiosGetWithRetry(`/sessions/${this.$route.params.id}/get_n_calibrated_cameras/`, {}) this.n_calibrated_cameras = resCalibratedCameras.data.data @@ -1056,7 +1064,7 @@ export default { break; } default: { - const resStatus = await axios.get(`/sessions/${this.$route.params.id}/status/`, {}) + const resStatus = await axiosGetWithRetry(`/sessions/${this.$route.params.id}/status/`, {}) this.n_videos_uploaded = resStatus.data.n_videos_uploaded @@ -1077,6 +1085,7 @@ export default { } } catch (error) { apiError(error); + this.busy = false; } }, openAdvancedSettings() { @@ -1084,7 +1093,7 @@ export default { this.getAvailableFramerates() }, async getAvailableFramerates() { - const session_settings = await axios.get(`/sessions/${this.session.id}/get_session_settings/`) + const session_settings = await axiosGetWithRetry(`/sessions/${this.session.id}/get_session_settings/`) if('data' in session_settings && 'framerates' in session_settings.data) { this.framerates_available = [] session_settings.data.framerates.forEach(element => { @@ -1140,7 +1149,7 @@ export default { if (await this.$refs.observer.validate()) { this.busy = true; try { - await axios.get( + await axiosGetWithRetry( `/sessions/${this.session.id}/set_metadata/`, { params: { @@ -1157,7 +1166,7 @@ export default { } ); - await axios.get( + await axiosGetWithRetry( `/sessions/${this.session.id}/set_subject/`, { params: { diff --git a/src/components/pages/Session.vue b/src/components/pages/Session.vue index 85ba81fb..ebf71aa5 100644 --- a/src/components/pages/Session.vue +++ b/src/components/pages/Session.vue @@ -83,7 +83,7 @@ - + {{ buttonCaption }}

@@ -913,6 +913,7 @@ import { apiError, apiErrorRes, apiSuccess, clearToastMessages } from '@/util/ErrorMessage.js' import { playRecordingSound, playRecordingFinishedSound } from "@/util/SoundMessage.js"; import { getSessionDeepLink } from '@/util/SessionDeepLink.js' + import { axiosGetWithRetry } from "@/util/network.js"; import Status from '@/components/ui/Status' import * as THREE from 'three' import * as THREE_OC from '@/orbitControls' @@ -1030,6 +1031,7 @@ recordingTimePassed: 0, recordingTimer: null, recordingStatusPoll: null, + recordingActionInFlight: false, trialsPoll: null, showSessionMenuButtons: false, @@ -1496,125 +1498,160 @@ // Tie key to the active trial so DOM can't be reused across trials. return `trial-${trialId}-video-${id || media || index}` }, + recordingActionMinLockDuration(state) { + return state === 'processing' ? 1500 : 1000 + }, + async waitForRecordingActionMinLock(startedAt, duration) { + const remaining = Math.max(0, duration - (Date.now() - startedAt)) + if (remaining > 0) { + await new Promise(resolve => window.setTimeout(resolve, remaining)) + } + }, async changeState() { - switch (this.state) { - case 'ready': { - this.submitted = true - - if (await this.$refs.observer.validate()) { - this.busy = true - - try { - // store in vuex - this.setSessionStep5(this.trialName) - - // api - const res = await axios.get(`/sessions/${this.session.id}/record/`, { - params: { - name: this.trialName - } - }) - - // add to the list - this.trialInProcess = res.data - this.addTrial(this.trialInProcess) + if (this.recordingActionInFlight) return + this.recordingActionInFlight = true + const actionStartedAt = Date.now() + const minLockDuration = this.recordingActionMinLockDuration(this.state) + let enforceMinLock = false - // Get n_cameras_connected. - const res_status = await axios.get(`/sessions/${this.session.id}/status/`, {}) - this.n_videos_uploaded = res_status.data.n_videos_uploaded - this.n_cameras_connected = res_status.data.n_cameras_connected + try { + switch (this.state) { + case 'ready': { + this.submitted = true + + if (await this.$refs.observer.validate()) { + this.busy = true + + try { + // store in vuex + this.setSessionStep5(this.trialName) + + // api + const res = await axiosGetWithRetry(`/sessions/${this.session.id}/record/`, { + params: { + name: this.trialName + } + }, { retries: 2, backoffFactor: 0.2, maxJitterMs: 100, timeout: 4500 }) + + // add to the list + this.trialInProcess = res.data + this.addTrial(this.trialInProcess) + + // Get n_cameras_connected. + const res_status = await axiosGetWithRetry(`/sessions/${this.session.id}/status/`, {}, { retries: 1, backoffFactor: 0.2, maxJitterMs: 100, timeout: 2000 }) + this.n_videos_uploaded = res_status.data.n_videos_uploaded + this.n_cameras_connected = res_status.data.n_cameras_connected + + // If no calibrated cameras... + if (this.n_calibrated_cameras === 0) { + const noCamMsg = "There are no calibrated cameras for this trial." + apiError(noCamMsg) + throw new Error(noCamMsg) + } - // If no calibrated cameras... - if (this.n_calibrated_cameras === 0) { - const noCamMsg = "There are no calibrated cameras for this trial." - apiError(noCamMsg) - throw new Error(noCamMsg) - } + // Transition to recording state + this.state = 'recording'; - // Transition to recording state - this.state = 'recording'; + // If too many cameras are connected, warn but allow the trial to continue. + if (this.n_cameras_connected > this.n_calibrated_cameras) { + this.showExtraCameraWarning() + } - // If too many cameras are connected, warn but allow the trial to continue. - if (this.n_cameras_connected > this.n_calibrated_cameras) { - this.showExtraCameraWarning() - } + // Check if the appropriate number of cameras is connected. + const startTime = Date.now(); + while (this.n_cameras_connected < this.n_calibrated_cameras) { + if (Date.now() - startTime > 5000) { // 5-second timeout + await axiosGetWithRetry(`/sessions/${this.session.id}/stop/`, {}, { retries: 2, backoffFactor: 0.25, maxJitterMs: 100, timeout: 5000 }) + await axiosGetWithRetry(`/sessions/${this.session.id}/cancel_trial/`, {}, { retries: 2, backoffFactor: 0.25, maxJitterMs: 100, timeout: 5000 }) + this.cancelPoll() + this.cancelRecordingStatusPoll() + this.state = 'ready' + this.trialInProcess.status = "error" + const timeoutMsg = (this.n_calibrated_cameras === 1 && this.n_cameras_connected === 0) + ? "No camera connected. Please connect 1 camera to start recording." + : `Expected ${this.n_calibrated_cameras} camera${this.n_calibrated_cameras === 1 ? '' : 's'} but ${this.n_cameras_connected} connected. Please connect the required cameras to start recording.` + throw new Error(timeoutMsg) + } - // Check if the appropriate number of cameras is connected. - const startTime = Date.now(); - while (this.n_cameras_connected < this.n_calibrated_cameras) { - if (Date.now() - startTime > 5000) { // 5-second timeout - const res_stop = await axios.get(`/sessions/${this.session.id}/stop/`, {}) - const res_cancel = await axios.get(`/sessions/${this.session.id}/cancel_trial/`, {}) - this.cancelPoll() - this.cancelRecordingStatusPoll() - this.state = 'ready' - this.trialInProcess.status = "error" - const timeoutMsg = (this.n_calibrated_cameras === 1 && this.n_cameras_connected === 0) - ? "No camera connected. Please connect 1 camera to start recording." - : `Expected ${this.n_calibrated_cameras} camera${this.n_calibrated_cameras === 1 ? '' : 's'} but ${this.n_cameras_connected} connected. Please connect the required cameras to start recording.` - throw new Error(timeoutMsg) - } + // Retry fetching the status + await new Promise(r => setTimeout(r, 500)); // Wait before retrying + const retryRes = await axiosGetWithRetry(`/sessions/${this.session.id}/status/`, {}, { retries: 1, backoffFactor: 0.2, maxJitterMs: 100, timeout: 2000 }); + this.n_cameras_connected = retryRes.data.n_cameras_connected; + if (this.n_cameras_connected > this.n_calibrated_cameras) { + this.showExtraCameraWarning() + } + } - // Retry fetching the status - await new Promise(r => setTimeout(r, 500)); // Wait before retrying - const retryRes = await axios.get(`/sessions/${this.session.id}/status/`, {}); - this.n_cameras_connected = retryRes.data.n_cameras_connected; - if (this.n_cameras_connected > this.n_calibrated_cameras) { - this.showExtraCameraWarning() - } - } + if (this.n_cameras_connected <= this.n_calibrated_cameras) { + this.sessionNotification = { show: false, text: '', type: 'error' } + } - if (this.n_cameras_connected <= this.n_calibrated_cameras) { - this.sessionNotification = { show: false, text: '', type: 'error' } + // Start recording timer. + this.recordingStarted = moment() + this.recordingTimePassed = 0 + this.recordingTimer = window.setTimeout(this.recordTimerHandler, 500) + this.startRecordingStatusPoll() + + // Play sound indicating the subject can start motion. + if (this.isAuditoryFeedbackEnabled) + playRecordingSound() + enforceMinLock = true + } catch (error) { + apiError(error) } + + this.busy = false + } + + break + } + case 'recording': { + this.busy = true + this.cancelRecordTimer() + this.cancelRecordingStatusPoll() - // Start recording timer. - this.recordingStarted = moment() - this.recordingTimePassed = 0 - this.recordingTimer = window.setTimeout(this.recordTimerHandler, 500) - this.startRecordingStatusPoll() + try { + const res = await axiosGetWithRetry(`/sessions/${this.session.id}/stop/`, {}, { retries: 2, backoffFactor: 0.25, maxJitterMs: 100, timeout: 5000 }) - // Play sound indicating the subject can start motion. + // Play sound indicating the subject can stop motion. if (this.isAuditoryFeedbackEnabled) - playRecordingSound() + playRecordingFinishedSound(); + + this.trialInProcess.status = res.data.status + this.state = 'processing' + enforceMinLock = true + + this.startPoll() } catch (error) { apiError(error) + } finally { + this.busy = false } - - this.busy = false + + break } - - break - } - case 'recording': { - this.cancelRecordTimer() - this.cancelRecordingStatusPoll() - - try { - const res = await axios.get(`/sessions/${this.session.id}/stop/`, {}) - - // Play sound indicating the subject can stop motion. - if (this.isAuditoryFeedbackEnabled) - playRecordingFinishedSound(); - - this.trialInProcess.status = res.data.status - this.state = 'processing' - - this.startPoll() - } catch (error) { - apiError(error) + case 'processing': { + this.busy = true + try { + await axiosGetWithRetry(`/sessions/${this.session.id}/cancel_trial/`, {}, { retries: 2, backoffFactor: 0.25, maxJitterMs: 100, timeout: 5000 }) + this.cancelPoll() + this.state = 'ready' + enforceMinLock = true + } catch (error) { + apiError(error) + } finally { + this.busy = false + } + break } - - break } - case 'processing': { - const res = await axios.get(`/sessions/${this.session.id}/cancel_trial/`, {}) - this.cancelPoll() - this.state = 'ready' - break + + if (enforceMinLock) { + await this.waitForRecordingActionMinLock(actionStartedAt, minLockDuration) } + } finally { + this.recordingActionInFlight = false } - await new Promise(r => setTimeout(r, 500)); }, recordTimerHandler() { this.recordingTimePassed = moment().diff(this.recordingStarted, 'seconds') @@ -1884,7 +1921,7 @@ }, startPoll() { this.statusPoll = window.setTimeout(async () => { - const res = await axios.get(`/sessions/${this.session.id}/status/`) + const res = await axiosGetWithRetry(`/sessions/${this.session.id}/status/`) this.n_cameras_connected = res.data.n_cameras_connected this.n_videos_uploaded = res.data.n_videos_uploaded