@@ -2,6 +2,7 @@ import https from 'node:https'
22import axios , { AxiosInstance , AxiosRequestConfig , AxiosResponse } from 'axios'
33import { io , Socket } from 'socket.io-client'
44import { InstanceBase , InstanceStatus , Regex , combineRgb , runEntrypoint } from '@companion-module/base'
5+ import type { CompanionActionEvent } from '@companion-module/base'
56import { getConfigFields } from './config.js'
67import { initActions as defineActions } from './actions.js'
78import { initFeedbacks as defineFeedbacks } from './feedbacks.js'
@@ -116,6 +117,7 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
116117 socket : Socket | null
117118 pollTimer : NodeJS . Timeout | null
118119 offlineFlashTimer : NodeJS . Timeout | null
120+ uiRefreshTimer : NodeJS . Timeout | null
119121 reauthPromise : Promise < void > | null
120122 users : Map < number , UserState >
121123 conferences : Map < number , { id : number ; name : string } >
@@ -131,6 +133,9 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
131133 scopeUserId : number | null
132134 scopeUserName : string
133135 lastCommand : LastCommandState
136+ pendingVariableRefresh : boolean
137+ pendingDefinitionRefresh : boolean
138+ pendingFeedbackChecks : Set < string >
134139
135140 constructor ( internal : unknown ) {
136141 super ( internal )
@@ -140,6 +145,7 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
140145 this . socket = null
141146 this . pollTimer = null
142147 this . offlineFlashTimer = null
148+ this . uiRefreshTimer = null
143149 this . reauthPromise = null
144150
145151 this . users = new Map < number , UserState > ( )
@@ -166,6 +172,9 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
166172 targetId : '' ,
167173 at : 0 ,
168174 }
175+ this . pendingVariableRefresh = false
176+ this . pendingDefinitionRefresh = false
177+ this . pendingFeedbackChecks = new Set ( )
169178 }
170179
171180 async init ( config : ModuleConfig , _isFirstInit : boolean , secrets : ModuleSecrets ) : Promise < void > {
@@ -442,6 +451,14 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
442451 this . offlineFlashTimer = null
443452 }
444453
454+ if ( this . uiRefreshTimer ) {
455+ clearTimeout ( this . uiRefreshTimer )
456+ this . uiRefreshTimer = null
457+ }
458+ this . pendingVariableRefresh = false
459+ this . pendingDefinitionRefresh = false
460+ this . pendingFeedbackChecks . clear ( )
461+
445462 if ( this . socket ) {
446463 this . socket . removeAllListeners ( )
447464 this . socket . disconnect ( )
@@ -478,6 +495,57 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
478495 } , 10000 )
479496 }
480497
498+ scheduleUiRefresh (
499+ feedbackIds : string [ ] = [ ] ,
500+ {
501+ refreshVariables = true ,
502+ refreshDefinitions = false ,
503+ } : { refreshVariables ?: boolean ; refreshDefinitions ?: boolean } = { } ,
504+ ) : void {
505+ if ( refreshVariables ) {
506+ this . pendingVariableRefresh = true
507+ }
508+ if ( refreshDefinitions ) {
509+ this . pendingDefinitionRefresh = true
510+ }
511+ for ( const feedbackId of feedbackIds ) {
512+ const normalized = asString ( feedbackId )
513+ if ( ! normalized ) continue
514+ this . pendingFeedbackChecks . add ( normalized )
515+ }
516+ if ( this . uiRefreshTimer ) return
517+
518+ this . uiRefreshTimer = setTimeout ( ( ) => {
519+ this . flushScheduledUiRefresh ( )
520+ } , 20 )
521+ }
522+
523+ flushScheduledUiRefresh ( ) : void {
524+ if ( this . uiRefreshTimer ) {
525+ clearTimeout ( this . uiRefreshTimer )
526+ this . uiRefreshTimer = null
527+ }
528+
529+ const shouldRefreshDefinitions = this . pendingDefinitionRefresh
530+ const shouldRefreshVariables = this . pendingVariableRefresh
531+ const feedbackIds = Array . from ( this . pendingFeedbackChecks )
532+
533+ this . pendingDefinitionRefresh = false
534+ this . pendingVariableRefresh = false
535+ this . pendingFeedbackChecks . clear ( )
536+
537+ if ( shouldRefreshDefinitions ) {
538+ this . refreshChoiceCaches ( )
539+ this . refreshDefinitions ( )
540+ }
541+ if ( shouldRefreshVariables ) {
542+ this . updateVariableValuesFromState ( )
543+ }
544+ if ( feedbackIds . length > 0 ) {
545+ this . checkFeedbacks ( ...feedbackIds )
546+ }
547+ }
548+
481549 async apiRequest ( method : string , path : string , data ?: unknown ) : Promise < AxiosResponse > {
482550 if ( ! this . http ) {
483551 this . createHttpClient ( )
@@ -720,8 +788,7 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
720788
721789 this . socket . on ( 'cut-camera' , ( payload ) => {
722790 this . cutCameraUser = asString ( payload ?. user )
723- this . updateVariableValuesFromState ( )
724- this . checkFeedbacks ( 'user_cut_camera' )
791+ this . scheduleUiRefresh ( [ 'user_cut_camera' ] )
725792 } )
726793
727794 this . socket . on ( 'command-result' , ( payload ) => {
@@ -886,18 +953,19 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
886953 }
887954
888955 if ( changed ) {
889- this . refreshDefinitions ( )
890- this . updateVariableValuesFromState ( )
891- this . checkFeedbacks (
892- 'target_volume_bar' ,
893- 'target_muted' ,
894- 'target_online' ,
895- 'target_offline' ,
896- 'user_talking_target' ,
897- 'user_talking_reply' ,
898- 'target_addressed_now' ,
899- 'reply_available' ,
900- 'user_addressed_now' ,
956+ this . scheduleUiRefresh (
957+ [
958+ 'target_volume_bar' ,
959+ 'target_muted' ,
960+ 'target_online' ,
961+ 'target_offline' ,
962+ 'user_talking_target' ,
963+ 'user_talking_reply' ,
964+ 'target_addressed_now' ,
965+ 'reply_available' ,
966+ 'user_addressed_now' ,
967+ ] ,
968+ { refreshDefinitions : true } ,
901969 )
902970 }
903971 }
@@ -927,29 +995,33 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
927995 const userId = Number ( raw . userId )
928996 if ( ! Number . isFinite ( userId ) ) return
929997
930- const user = this . users . get ( userId ) || this . makeEmptyUserState ( userId )
998+ const existingUser = this . users . get ( userId ) || null
999+ const user = existingUser || this . makeEmptyUserState ( userId )
1000+ const previousName = existingUser ?. name || ''
9311001 this . mergeUserState ( user , raw )
9321002 this . users . set ( userId , user )
9331003
934- this . refreshChoiceCaches ( )
935- this . refreshDefinitions ( )
936- this . updateVariableValuesFromState ( )
937- this . checkFeedbacks (
938- 'module_not_running' ,
939- 'user_online' ,
940- 'user_talking' ,
941- 'user_talking_target' ,
942- 'user_talking_reply' ,
943- 'user_locked' ,
944- 'target_volume_bar' ,
945- 'target_muted' ,
946- 'target_online' ,
947- 'target_offline' ,
948- 'target_addressed_now' ,
949- 'reply_available' ,
950- 'user_addressed_now' ,
951- 'operator_not_logged_in' ,
952- 'user_cut_camera' ,
1004+ this . scheduleUiRefresh (
1005+ [
1006+ 'module_not_running' ,
1007+ 'user_online' ,
1008+ 'user_talking' ,
1009+ 'user_talking_target' ,
1010+ 'user_talking_reply' ,
1011+ 'user_locked' ,
1012+ 'target_volume_bar' ,
1013+ 'target_muted' ,
1014+ 'target_online' ,
1015+ 'target_offline' ,
1016+ 'target_addressed_now' ,
1017+ 'reply_available' ,
1018+ 'user_addressed_now' ,
1019+ 'operator_not_logged_in' ,
1020+ 'user_cut_camera' ,
1021+ ] ,
1022+ {
1023+ refreshDefinitions : ! existingUser || previousName !== user . name ,
1024+ } ,
9531025 )
9541026 }
9551027
@@ -962,7 +1034,9 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
9621034 talkLocked : false ,
9631035 socketId : '' ,
9641036 currentTarget : null ,
1037+ currentTargets : [ ] ,
9651038 lastTarget : null ,
1039+ lastTargets : [ ] ,
9661040 addressedNow : [ ] ,
9671041 replyTarget : null ,
9681042 targetAudioStates : [ ] ,
@@ -978,8 +1052,14 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
9781052 target . talking = Boolean ( raw . talking )
9791053 target . talkLocked = Boolean ( raw . talkLocked )
9801054 target . socketId = asString ( raw . socketId )
981- target . currentTarget = this . normalizeStateTarget ( raw . currentTarget )
982- target . lastTarget = this . normalizeStateTarget ( raw . lastTarget )
1055+ const currentTarget = this . normalizeStateTarget ( raw . currentTarget )
1056+ const currentTargets = this . normalizeStateTargets ( raw . currentTargets )
1057+ const lastTarget = this . normalizeStateTarget ( raw . lastTarget )
1058+ const lastTargets = this . normalizeStateTargets ( raw . lastTargets )
1059+ target . currentTargets = currentTargets . length > 0 ? currentTargets : currentTarget ? [ currentTarget ] : [ ]
1060+ target . currentTarget = target . currentTargets [ 0 ] || currentTarget
1061+ target . lastTargets = lastTargets . length > 0 ? lastTargets : lastTarget ? [ lastTarget ] : [ ]
1062+ target . lastTarget = target . lastTargets [ 0 ] || lastTarget
9831063 target . addressedNow = this . normalizeAddressedEntries ( raw . addressedNow )
9841064 target . replyTarget = this . normalizeAddressedEntry ( raw . replyTarget )
9851065 target . lastCommandId = asString ( raw . lastCommandId )
@@ -1070,6 +1150,21 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
10701150 return { type : type as NormalizedTarget [ 'type' ] , id }
10711151 }
10721152
1153+ normalizeStateTargets ( rawTargets : unknown ) : NormalizedTarget [ ] {
1154+ if ( ! Array . isArray ( rawTargets ) ) return [ ]
1155+ const normalizedTargets : NormalizedTarget [ ] = [ ]
1156+ const seen = new Set < string > ( )
1157+ for ( const rawTarget of rawTargets ) {
1158+ const target = this . normalizeStateTarget ( rawTarget )
1159+ if ( ! target ) continue
1160+ const key = `${ target . type } :${ target . id } `
1161+ if ( seen . has ( key ) ) continue
1162+ seen . add ( key )
1163+ normalizedTargets . push ( target )
1164+ }
1165+ return normalizedTargets
1166+ }
1167+
10731168 normalizeTimestamp ( rawValue : unknown ) : number | null {
10741169 if ( rawValue === null || rawValue === undefined || rawValue === '' ) return null
10751170 const numeric = Number ( rawValue )
@@ -1132,26 +1227,42 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
11321227 } )
11331228 }
11341229
1135- isUserTalkingToExactTarget ( userId : unknown , targetType : unknown , targetId : unknown ) : boolean {
1230+ getUserCurrentTargets ( userId : unknown ) : NormalizedTarget [ ] {
11361231 const normalizedUserId = Number ( userId )
1137- if ( ! Number . isFinite ( normalizedUserId ) ) return false
1232+ if ( ! Number . isFinite ( normalizedUserId ) ) return [ ]
11381233 const user = this . users . get ( normalizedUserId )
1139- if ( ! user ?. talking ) return false
1234+ if ( ! user ?. talking ) return [ ]
11401235
1141- const currentTarget = this . normalizeStateTarget ( user . currentTarget )
1236+ const explicitTargets = this . normalizeStateTargets ( user . currentTargets )
1237+ if ( explicitTargets . length > 0 ) {
1238+ return explicitTargets
1239+ }
1240+
1241+ const singleTarget = this . normalizeStateTarget ( user . currentTarget )
1242+ return singleTarget ? [ singleTarget ] : [ ]
1243+ }
1244+
1245+ isUserTalkingToExactTarget ( userId : unknown , targetType : unknown , targetId : unknown ) : boolean {
1246+ const normalizedUserId = Number ( userId )
1247+ if ( ! Number . isFinite ( normalizedUserId ) ) return false
11421248 const expectedTarget = this . normalizeStateTarget ( { type : targetType , id : targetId } )
1143- return this . areTargetsEquivalent ( currentTarget , expectedTarget )
1249+ if ( ! expectedTarget ) return false
1250+
1251+ return this . getUserCurrentTargets ( normalizedUserId ) . some ( ( currentTarget ) =>
1252+ this . areTargetsEquivalent ( currentTarget , expectedTarget ) ,
1253+ )
11441254 }
11451255
11461256 isUserTalkingToReply ( userId : unknown ) : boolean {
11471257 const normalizedUserId = Number ( userId )
11481258 if ( ! Number . isFinite ( normalizedUserId ) ) return false
1149- const user = this . users . get ( normalizedUserId )
1150- if ( ! user ?. talking ) return false
11511259
1152- const currentTarget = this . normalizeStateTarget ( user . currentTarget )
11531260 const replyTarget = this . resolveReplyReferenceTarget ( normalizedUserId )
1154- return this . areTargetsEquivalent ( currentTarget , replyTarget )
1261+ if ( ! replyTarget ) return false
1262+
1263+ return this . getUserCurrentTargets ( normalizedUserId ) . some ( ( currentTarget ) =>
1264+ this . areTargetsEquivalent ( currentTarget , replyTarget ) ,
1265+ )
11551266 }
11561267
11571268 isAddressingEntryMatchingTarget (
@@ -1343,8 +1454,7 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
13431454 this . applyUserState ( raw . state )
13441455 }
13451456
1346- this . updateVariableValuesFromState ( )
1347- this . checkFeedbacks (
1457+ this . scheduleUiRefresh ( [
13481458 'last_command_failed' ,
13491459 'user_talking' ,
13501460 'user_talking_target' ,
@@ -1358,7 +1468,7 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
13581468 'reply_available' ,
13591469 'user_addressed_now' ,
13601470 'operator_not_logged_in' ,
1361- )
1471+ ] )
13621472
13631473 if ( reason === 'Target offline' ) {
13641474 this . triggerTargetOfflineFeedbackFlash ( )
@@ -1404,7 +1514,16 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
14041514 return id
14051515 }
14061516
1407- async executeTalkCommand ( options : Record < string , unknown > ) : Promise < void > {
1517+ buildCompanionTalkInputKey ( actionEvent : CompanionActionEvent | undefined , userId : number ) : string | null {
1518+ if ( ! actionEvent ) return null
1519+ const instanceId = asString ( this . id || this . label ) || 'talktome'
1520+ const controlId = asString ( actionEvent . controlId )
1521+ if ( ! controlId ) return null
1522+ const surfaceId = asString ( actionEvent . surfaceId ) || 'surface'
1523+ return `companion:${ instanceId } :user:${ userId } :surface:${ surfaceId } :control:${ controlId } `
1524+ }
1525+
1526+ async executeTalkCommand ( options : Record < string , unknown > , actionEvent ?: CompanionActionEvent ) : Promise < void > {
14081527 const userId = this . resolveChoiceId ( options . userId )
14091528 if ( ! userId ) {
14101529 throw new Error ( 'Invalid user' )
@@ -1424,6 +1543,10 @@ export class TalkToMeCompanionInstance extends InstanceBase<ModuleConfig, Module
14241543 targetType,
14251544 waitMs,
14261545 }
1546+ const inputKey = this . buildCompanionTalkInputKey ( actionEvent , userId )
1547+ if ( inputKey ) {
1548+ payload . inputKey = inputKey
1549+ }
14271550
14281551 if ( targetType === 'conference' ) {
14291552 const conferenceId = this . resolveChoiceId ( options . targetConferenceId )
0 commit comments