-
Notifications
You must be signed in to change notification settings - Fork 397
feat(plugin-meetings): hydrate recording duration on rejoin #5017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from 1 commit
3bbe201
1d9b466
14d20ff
c34f186
6cd0d07
8b5c822
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -686,6 +686,7 @@ export default class Meeting extends StatelessWebexPlugin { | |
| recording: any; | ||
| remoteMediaManager: RemoteMediaManager | null; | ||
| recordingController: RecordingController; | ||
| pendingRecordingHydrationEvent?: string; | ||
| controlsOptionsManager: ControlsOptionsManager; | ||
| requiredCaptcha: any; | ||
| receiveSlotManager: ReceiveSlotManager; | ||
|
|
@@ -2809,6 +2810,84 @@ export default class Meeting extends StatelessWebexPlugin { | |
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Locus deltas do not include the accumulated recording duration. Native | ||
| * clients fetch it from the recording stream service | ||
| * (`GET {serviceUrl}/loci/{locusId}/resource`). This helper performs the | ||
| * same fetch and, on success, merges the duration metadata into | ||
| * `this.recording` and re-emits the supplied event so the consumer can | ||
| * hydrate its timer with the correct elapsed time (especially on rejoin | ||
| * while the meeting is paused). | ||
| * | ||
| * @param {string} event - the recording event that was just fired | ||
| * @returns {Promise<void>} | ||
| * @private | ||
| * @memberof Meeting | ||
| */ | ||
| private async hydrateRecordingDuration(event: string): Promise<void> { | ||
| if (!this.recordingController) { | ||
| return; | ||
| } | ||
|
|
||
| // The recording stream service URL is set asynchronously via the | ||
| // LINKS_SERVICES locus event. On initial join into an already RECORDING | ||
| // / PAUSED meeting, the controls update can fire before that. In that | ||
| // case, defer the hydration: it will be retried from | ||
| // `setUpLocusServicesListener` once the URL is available. | ||
| if (!this.recordingController.getServiceUrl() || !this.recordingController.getLocusId()) { | ||
| LoggerProxy.logger.debug( | ||
| `Meeting:index#hydrateRecordingDuration --> deferred (event=${event}); serviceUrl/locusId not yet available` | ||
| ); | ||
| this.pendingRecordingHydrationEvent = event; | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| this.pendingRecordingHydrationEvent = undefined; | ||
|
|
||
| try { | ||
| const duration = await this.recordingController.getRecordingStatus(); | ||
|
|
||
| LoggerProxy.logger.debug( | ||
| `Meeting:index#hydrateRecordingDuration --> event=${event} duration=${JSON.stringify( | ||
| duration | ||
| )}` | ||
| ); | ||
|
|
||
| if (!duration || (duration.lastDuration === undefined && duration.lastTime === undefined)) { | ||
| return; | ||
| } | ||
|
|
||
| // Guard against a race: if the recording was stopped while the request | ||
| // was in flight, do not resurrect stale duration metadata onto the now | ||
| // IDLE state — that would re-trigger the timer on the next start. | ||
| if (this.recording?.state === RECORDING_STATE.IDLE) { | ||
| return; | ||
| } | ||
|
|
||
| this.recording = { | ||
| ...this.recording, | ||
| lastDuration: duration.lastDuration, | ||
| lastTime: duration.lastTime, | ||
| needCalculate: duration.needCalculate, | ||
|
Comment on lines
+2912
to
+2916
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the recording resource service lags behind a Locus transition, this merges whatever duration the GET returns into the current Useful? React with 👍 / 👎. |
||
| }; | ||
|
junhao3268 marked this conversation as resolved.
|
||
|
|
||
| Trigger.trigger( | ||
| this, | ||
| { | ||
| file: 'meeting/index', | ||
| function: 'hydrateRecordingDuration', | ||
| }, | ||
| event, | ||
| this.recording | ||
|
junhao3268 marked this conversation as resolved.
|
||
| ); | ||
| } catch (error) { | ||
| LoggerProxy.logger.warn( | ||
| `Meeting:index#hydrateRecordingDuration --> failed to fetch duration: ${error}` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Set up the locus info recording update listener | ||
| * update recording value for the meeting | ||
|
|
@@ -2830,7 +2909,16 @@ export default class Meeting extends StatelessWebexPlugin { | |
| private setupLocusControlsListener() { | ||
| this.locusInfo.on( | ||
| LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED, | ||
| ({state, modifiedBy, lastModified, modifiedByServiceAppName, modifiedByServiceAppId}) => { | ||
| ({ | ||
| state, | ||
| modifiedBy, | ||
| lastModified, | ||
| modifiedByServiceAppName, | ||
| modifiedByServiceAppId, | ||
| lastDuration, | ||
| lastTime, | ||
| needCalculate, | ||
| }) => { | ||
| let event; | ||
|
|
||
| switch (state) { | ||
|
|
@@ -2851,13 +2939,53 @@ export default class Meeting extends StatelessWebexPlugin { | |
| break; | ||
| } | ||
|
|
||
| // Locus deltas do NOT carry duration data. To avoid wiping the values | ||
| // we previously hydrated from the recording stream service (which | ||
| // would cause the UI to briefly flash 00:00:00 on every state change | ||
| // such as PAUSED -> RECORDING), preserve any prior duration metadata | ||
| // until hydrate runs again with fresh server data. EXCEPT when we | ||
| // transition into IDLE (recording fully stopped) — in that case the | ||
| // previous segment's duration is no longer meaningful, and keeping it | ||
| // around would cause the next START to briefly show the stale value. | ||
| const previousRecording = this.recording || {}; | ||
| const isStopping = state === RECORDING_STATE.IDLE; | ||
| const pickDurationField = <T>( | ||
| incoming: T | undefined, | ||
| previous: T | undefined | ||
| ): T | undefined => { | ||
| if (isStopping) { | ||
| return undefined; | ||
| } | ||
| if (incoming !== undefined) { | ||
| return incoming; | ||
| } | ||
|
|
||
| return previous; | ||
| }; | ||
| const resolvedLastDuration = pickDurationField( | ||
| lastDuration, | ||
| previousRecording.lastDuration | ||
| ); | ||
| const resolvedLastTime = pickDurationField(lastTime, previousRecording.lastTime); | ||
| const resolvedNeedCalculate = pickDurationField( | ||
| needCalculate, | ||
| previousRecording.needCalculate | ||
| ); | ||
|
Comment on lines
+3014
to
+3017
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Locus omits duration fields on a pause/resume delta and the hydration request is delayed, unavailable, or fails, this keeps the previous Useful? React with 👍 / 👎. |
||
|
|
||
| // `RESUMED` state should be converted to `RECORDING` after triggering the event | ||
| this.recording = { | ||
| state: state === RECORDING_STATE.RESUMED ? RECORDING_STATE.RECORDING : state, | ||
| modifiedBy, | ||
| lastModified, | ||
| modifiedByServiceAppName, | ||
| modifiedByServiceAppId, | ||
| // Duration metadata for timer hydration on rejoin. Optional — | ||
| // present only when Locus includes `controls.record.meta.duration`, | ||
| // which today it never does. We therefore fall back to whatever we | ||
| // already had so the UI does not lose the timer baseline. | ||
| lastDuration: resolvedLastDuration, | ||
| lastTime: resolvedLastTime, | ||
| needCalculate: resolvedNeedCalculate, | ||
| }; | ||
| Trigger.trigger( | ||
| this, | ||
|
|
@@ -2868,6 +2996,23 @@ export default class Meeting extends StatelessWebexPlugin { | |
| event, | ||
| this.recording | ||
| ); | ||
|
|
||
| // Locus deltas do not carry the accumulated recording duration. | ||
| // When the meeting transitions into RECORDING or PAUSED (which also | ||
| // covers the case where we join a meeting that is already in one of | ||
| // those states), fetch the duration metadata from the recording | ||
| // stream service to mirror what native clients do. Re-emit the same | ||
| // event with the hydrated values so the UI can show the correct | ||
| // elapsed time instead of `00:00:00`. | ||
| if ( | ||
| event && | ||
| (state === RECORDING_STATE.RECORDING || | ||
| state === RECORDING_STATE.RESUMED || | ||
| state === RECORDING_STATE.PAUSED) && | ||
| (lastDuration === undefined || lastTime === undefined) | ||
| ) { | ||
| this.hydrateRecordingDuration(event); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
|
|
@@ -3539,6 +3684,16 @@ export default class Meeting extends StatelessWebexPlugin { | |
| this.annotation.approvalUrlUpdate(payload?.services?.approval?.url); | ||
| this.simultaneousInterpretation.approvalUrlUpdate(payload?.services?.approval?.url); | ||
| this.aiEnableRequest.approvalUrlUpdate(payload?.services?.approval?.url); | ||
|
|
||
| // If a recording event fired before the recording stream service URL | ||
| // was known (e.g. joining an already RECORDING/PAUSED meeting), retry | ||
| // the deferred hydration now that the URL is available. | ||
| if (this.pendingRecordingHydrationEvent) { | ||
| const event = this.pendingRecordingHydrationEvent; | ||
|
|
||
| this.pendingRecordingHydrationEvent = undefined; | ||
| this.hydrateRecordingDuration(event); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.