-
Notifications
You must be signed in to change notification settings - Fork 3
/
main_codec_macro.js
1223 lines (1033 loc) · 47.8 KB
/
main_codec_macro.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
Copyright (c) 2021 Cisco and/or its affiliates.
This software is licensed to you under the terms of the Cisco Sample
Code License, Version 1.1 (the "License"). You may obtain a copy of the
License at
https://developer.cisco.com/docs/licenses
All use of the material herein must be in accordance with the terms of
the License. All rights not expressly granted by the License are
reserved. Unless required by applicable law or agreed to separately in
writing, software distributed under the License is distributed on an "AS
IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied.
*
* Repository: gve_devnet_webex_devices_executive_room_voice_activated_switching_macro
* Macro file: main_codec_macro
* Version: 2.1.2
* Released: May 23, 2023
* Latest RoomOS version tested: 11.4
*
* Macro Author: Gerardo Chaves
* Technical Solutions Architect
* gchaves@cisco.com
* Cisco Systems
*
* Consulting Engineer: Robert(Bobby) McGonigle Jr
* Technical Marketing Engineer
* bomcgoni@cisco.com
* Cisco Systems
*
*
*
* As a macro, the features and functions of this webex devices executive room voice activated
* switching macro are not supported by Cisco TAC
*
* Hardware and Software support are provided by their respective manufacturers
* and the service agreements they offer
*
* Should you need assistance with this macro, reach out to your Cisco sales representative
* so they can engage the GVE DevNet team.
*/
/////////////////////////////////////////////////////////////////////////////////////////
// REQUIREMENTS
/////////////////////////////////////////////////////////////////////////////////////////
import xapi from 'xapi';
import { GMM } from './GMM_Lib'
//TODO: Test pause speakertrack with perma side by side mode since it originall would try to turn on ST
/////////////////////////////////////////////////////////////////////////////////////////
// INSTALLER SETTINGS
/////////////////////////////////////////////////////////////////////////////////////////
/*
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ SECTION 1 - SECTION 1 - SECTION 1 - SECTION 1 - SECTION 1 - SECTION 1 +
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*/
// IP Address of AUX codec (i.e. CodecPlus)
// NOTE: if there is no auxiliary codec, you must set the value of AUX_CODEC_IP to '' (const AUX_CODEC_IP ='')
const AUX_CODEC_IP = '10.10.10.10';
// AUX_CODEC_USERNAME and AUX_CODEC_PASSWORD are the username and password of a admin-level user on the Auxiliary codec
// Here are instructions on how to configure local user accounts on Webex Devices:
// https://help.webex.com/en-us/jkhs20/Local-User-Administration-on-Room-and-Desk-Devices)
const AUX_CODEC_USERNAME = 'username';
const AUX_CODEC_PASSWORD = 'password';
// Video source and SpeakerTrack constants needed for defining mapping. DO NOT EDIT
const SP = 0, V1 = 1, V2 = 2, V3 = 3, V4 = 4, V5 = 5, V6 = 6
// Set USE_ST_BG_MODE to true if you want keep Quacams Speaker Tracking even while not being used
const USE_ST_BG_MODE = true;
/*
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ SECTION 2 - SECTION 2 - SECTION 2 - SECTION 2 - SECTION 2 - SECTION 2 +
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*/
// CAMERA / MICROPHONE ZONE PRESET OBJECTS (Z1 - Z8)
// This section is used if you have one or two PTZ cameras (either Precision 60 or PTZ 4K),
// and you want to define up to 8 microphone zones that will be serviced by Pan Tilt Zoom cameras.
// This can be in combination with one or two Quad Cameras, or without any Quad Cameras.
// The maximum number of PTZ Microphone Zones is 8. If you have one Quad Camera, it will use one of your mic inputs,
// and if you have two Quad Cameras, they will use two of your mic inputs. This leaves you with 7 or 6 zones for PTZ cameras.
// FOR EACH PTZ MICROPHONE ZONE (UP TO 8) YOU MUST DEFINE AT LEAST A PRIMARY CAMERA PRESET ID.
// If you have two PTZ cameras, you can define a primary and a secondary camera for each microphone zone.
// The reason: if Camera "A" is in use already, you will want to use Camera "B" for the next shot,
// so that the far end does not see camera motion, which could be distracting/dizzying.
// WARNING: Do not delete Z0 even if you do not intend to use camera zones, it is needed to initialize the "last camera zone used" global.
// You can define as many camera preset objects as needed up to 8, using the ZN naming convention.
// If you do not have any PTZ cameras connected to the codec, simply leave Z1 and Z2 defined as below as examples but
// do not use them in your MAP_CAMERA_SOURCES array
// NOTE: Mic inputs that trigger Quad Cameras do not use "PTZ Microphone Zones". Instead they trigger either "SP" (SpeakerTrack = local Quad Camera),
// V1, or V2 (video inputs used by Aux Codec Plus that run their own Quad Camera)
// NOTE: If you do not have a secondary preset for a zone, just use the same as the primary as the code needs that 'secondary' key present
const Z0 = { 'primary': 0, 'secondary': 0 } //DO NOT DELETE OR COMMENT ME!!!!!
const Z1 = { 'primary': 11, 'secondary': 12 } // These are ok to change
const Z2 = { 'primary': 14, 'secondary': 13 } // These are ok to change
// Add camera zones below if needed to map in MAP_CAMERA_SOURCES, up to to Z8 but they can reference
// preset IDs 11-35 depending on which are configured on the codec. PresetID 30 IS RESERVED FOR USE BY THE PROGRAM
//Z3= {'primary': 5,'secondary': 6}
/*
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ SECTION 3 - SECTION 3 - SECTION 3 - SECTION 3 - SECTION 3 - SECTION 3 +
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*/
// Microphone Input Numbers to Monitor
// Specify the input connectors associated to the microphones being used in the room
// For example, if you set the value to [1,2,3,4,5,6,7,8] the macro will evaluate mic input id's 1-8 for its switching logic
const MICROPHONE_CONNECTORS = [1, 2, 3, 4, 5, 6, 7, 8];
// Camera source IDs that correspond to each microphone in MICROPHONE_CONNECTORS array
// Associate the connectors to specific input source type/id corresponding to the camera that covers where the mic is located.
// For example, if you set MICROPHONE_CONNECTORS = [1,2,3,4,5,6,7,8] and MAP_CAMERA_SOURCES to [V1,V1,V1,V2,V2,V2,Z1,Z2]
// you are specifying that
// mics 1,2 and 3 located where Camera associated to video input 1 (V1) is pointing at and
// mics 4,5 and 6 are located where Camera associated to video input 2 (V2) is pointing at and
// mic 7 is associated to PTZ camera defined in the zone Z1 object above and
// mic 8 is associated to PTZ camera defined in the zone Z2 object above
// Valid values for entries in the MAP_CAMERA_SOURCES array are: SP, V1-V2 and Z1-Z8
const MAP_CAMERA_SOURCES = [V1, V1, V1, V2, V2, V2, Z1, Z2];
// Specifying which sourceID belongs to local QuadCam
// MAIN_CODEC_QUADCAM_SOURCE_ID should contain the SourceID where the QuadCam connected
// to the main codec (if any) is connected. This it typically SourceID 1. If no QuadCam is connected
// then set this to 0
const MAIN_CODEC_QUADCAM_SOURCE_ID = 1;
// Mapping of video sources to CameraIDs for PTZ cameras
// MAP_PTZ_CAMERA_VIDEO_SOURCE_ID contains an object of key/value pairs that maps
// each Camera ID (key) to the video input source ID it is connected to (value).
// so, if we set it to { '1':1, '2':2, '3':6 } then it indicates that camera ID 1 is connected
// to video source 1, camera ID 2 is connected to video source 2 and camera ID 3 is connected
// to video source 6. You can define as many cameras as needed in this object or leave it with the
// sample values defined below if you are not using PTZ cameras.
// Only cameras involved in the camera zone preset objects (Z1 - Z8) need to be mapped here
const MAP_PTZ_CAMERA_VIDEO_SOURCE_ID = { '2': 6, '3': 2, '4': 4 };
// In RoomOS 11 there are multiple SpeakerTrack default behaviors to choose from on the navigator or
// Touch10 device. Set ST_DEFAULT_BEHAVIOR to the one you want this macro to use from these choices:
// Auto: The same as BestOverview.
// BestOverview: The default framing mode is Best overview.
// Closeup: The default framing mode is Closeup (speaker tracking).
// Current: The framing mode is kept unchanged when leaving a call.
// Frames: The default framing mode is Frames.
const ST_DEFAULT_BEHAVIOR = 'Closeup'
// This next line hides the mid-call controls “Lock meeting” and “Record”. The reason for this is so that the
// “Camera Control” button can be seen. If you prefer to have the mid-call controls showing, change the value of this from “Hidden” to “Auto”
xapi.Config.UserInterface.Features.Call.MidCallControls.set("Hidden");
/*
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ SECTION 4 - SECTION 4 - SECTION 4 - SECTION 4 - SECTION 4 - SECTION 4 +
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*/
// overviewShowDouble defines what is shown on the far end (the video the main codec sends into the call or conference) when in "OVERVIEW" mode where nobody is speaking or there is no
// prominent speaker detected by any of the microphones
// INSTRUCTIONS: If you are using side-by-side mode as your default - "overviewShowDouble = true" - then you must set up a camera preset for each Quad Camera
// with a Preset ID of 30. The JavaScript for side-by-side mode uses Preset 30.
const overviewShowDouble = true;
// OVERVIEW_SINGLE_SOURCE_ID specifies the source video ID to use when in overview mode if you set overviewShowDouble to false
const OVERVIEW_SINGLE_SOURCE_ID = 3;
// OVERVIEW_PRESET_ZONE specifies the PTZ camera defined zone to be used for showing an 'overview' of the room
// NOTE: OVERVIEW_PRESET_ZONE takes precedence over OVERVIEW_SINGLE_SOURCE_ID. Leave it as Z0 if you do not want to use it, otherwise
// define it like any other zone (i.e.{'primary': 1,'secondary': 2} )
// NOTE: You still need to set overviewShowDouble to false to be able to use OVERVIEW_PRESET_ZONE
const OVERVIEW_PRESET_ZONE = Z0;
//const OVERVIEW_PRESET_ZONE = {'primary': 1,'secondary': 2};
// OVERVIEW_DOUBLE_SOURCE_IDS specifies the source video array of two IDs to use when in overview mode if you set overviewShowDouble to true
// it will display the two sources side by side on the main screen with the first value of the array on the
// left and the second on the right.
const OVERVIEW_DOUBLE_SOURCE_IDS = [2, 1];
/*
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ SECTION 5 - SECTION 5 - SECTION 5 - SECTION 5 - SECTION 5 - SECTION 5 +
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
TIMERS and THRESHOLDS
*/
// Time to wait for silence before setting Speakertrack Side-by-Side mode
const SIDE_BY_SIDE_TIME = 10000; // 10 seconds
// Time to wait before switching to a new speaker
const NEW_SPEAKER_TIME = 2000; // 2 seconds
// Time to wait before activating automatic mode at the beginning of a call
const INITIAL_CALL_TIME = 15000; // 15 seconds
// time to wait after setting a camera preset before switching to it's source to prevent
// transmitting video during camera movement for P60 and PTZ cameras
const VIDEO_SOURCE_SWITCH_WAIT_TIME = 500; // 500 ms
/////////////////////////////////////////////////////////////////////////////////////////
// CONSTANTS/ENUMS
/////////////////////////////////////////////////////////////////////////////////////////
const AUX_CODEC_AUTH = encode(AUX_CODEC_USERNAME + ':' + AUX_CODEC_PASSWORD); // DO NOT EDIT
// Microphone High/Low Thresholds
const MICROPHONELOW = 6;
const MICROPHONEHIGH = 25;
const minOS10Version = '10.17.1.0';
const minOS11Version = '11.0.0.4';
/*
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ DO NOT EDIT ANYTHING BELOW THIS LINE +
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*/
const localCallout = new GMM.Connect.Local(module.name.replace('./', ''))
/////////////////////
// MAPPING VALIDATION
/////////////////////
const sleep = (timeout) => new Promise((resolve) => {
setTimeout(resolve, timeout);
});
async function validate_mappings() {
const timeout = 2000; // Milliseconds, equals 2 seconds
if (MICROPHONE_CONNECTORS.length != MAP_CAMERA_SOURCES.length) {
console.log('ERROR: There is a mismatch between the number of microphones defined and the number of camera sources mapped:');
console.log('Microphone connectors defined: ', MICROPHONE_CONNECTORS);
console.log('Map of camera sources: ', MAP_CAMERA_SOURCES);
while (true) {
console.log('Please stop this Camera Switcher Macro, correct the microphones/camera sources mismatch and re-start...');
await sleep(timeout);
}
}
if (MAP_CAMERA_SOURCES.indexOf(SP) != -1) {
if (MAP_CAMERA_SOURCES.indexOf(SP) != MAP_CAMERA_SOURCES.lastIndexOf(SP)) {
console.log('ERROR: There can only be one or zero SpeakerTrack (value 0) cameras sources defined in the map:');
console.log('Map of camera sources: ', MAP_CAMERA_SOURCES);
while (true) {
console.log('Please stop this Camera Switcher Macro, make sure there is only 1 or 0 SpeakerTrack cameras configured and re-start...');
await sleep(timeout);
}
}
}
}
validate_mappings();
// below we check for the existence of a SpeakerTrack camera configured for the codec
// so we can safely issue SpeakerTrack related commands
let has_SpeakerTrack = MAP_CAMERA_SOURCES.indexOf(SP) != -1 ||
MAP_CAMERA_SOURCES.indexOf(V1) != -1;
/////////////////////////////////////////////////////////////////////////////////////////
// VARIABLES
/////////////////////////////////////////////////////////////////////////////////////////
let AUX_CODEC = { enable: (AUX_CODEC_IP != ''), online: false, url: AUX_CODEC_IP, auth: AUX_CODEC_AUTH };
//Declare your object for GMM communication
var auxCodec;
let micArrays = {};
for (var i in MICROPHONE_CONNECTORS) {
micArrays[MICROPHONE_CONNECTORS[i].toString()] = [0, 0, 0, 0];
}
let lowWasRecalled = false;
let lastActiveHighInput = 0;
let allowSideBySide = true;
let sideBySideTimer = null;
let InitialCallTimer = null;
let allowCameraSwitching = false;
let allowNewSpeaker = true;
let newSpeakerTimer = null;
let manual_mode = true;
let lastActivePTZCameraZoneObj = Z0;
let lastActivePTZCameraZoneCamera = '0';
let perma_sbs = false; // set to true if you want to start with side by side view always
let micHandler = () => void 0;
let usb_mode = false;
let webrtc_mode = false;
let isOSTen = false;
let isOSEleven = false;
let presenterTrackConfigured = false;
let presenterSuspendedAuto = false;
/////////////////////////////////////////////////////////////////////////////////////////
// UTILITIES
/////////////////////////////////////////////////////////////////////////////////////////
function encode(s) {
var c = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
o = [];
for (var i = 0, n = s.length; i < n;) {
var c1 = s.charCodeAt(i++),
c2 = s.charCodeAt(i++),
c3 = s.charCodeAt(i++);
o.push(c.charAt(c1 >> 2));
o.push(c.charAt(((c1 & 3) << 4) | (c2 >> 4)));
o.push(c.charAt(i < n + 2 ? ((c2 & 15) << 2) | (c3 >> 6) : 64));
o.push(c.charAt(i < n + 1 ? c3 & 63 : 64));
}
return o.join("");
}
async function getPresetCamera(prID) {
const value = await xapi.Command.Camera.Preset.Show({ PresetId: prID });
return (value.CameraId)
}
async function check4_Minimum_Version_Required(minimumOs) {
const reg = /^\D*(?<MAJOR>\d*)\.(?<MINOR>\d*)\.(?<EXTRAVERSION>\d*)\.(?<BUILDID>\d*).*$/i;
const minOs = minimumOs;
const os = await xapi.Status.SystemUnit.Software.Version.get();
console.log(os)
const x = (reg.exec(os)).groups;
const y = (reg.exec(minOs)).groups;
if (parseInt(x.MAJOR) > parseInt(y.MAJOR)) return true;
if (parseInt(x.MAJOR) < parseInt(y.MAJOR)) return false;
if (parseInt(x.MINOR) > parseInt(y.MINOR)) return true;
if (parseInt(x.MINOR) < parseInt(y.MINOR)) return false;
if (parseInt(x.EXTRAVERSION) > parseInt(y.EXTRAVERSION)) return true;
if (parseInt(x.EXTRAVERSION) < parseInt(y.EXTRAVERSION)) return false;
if (parseInt(x.BUILDID) > parseInt(y.BUILDID)) return true;
if (parseInt(x.BUILDID) < parseInt(y.BUILDID)) return false;
return false;
}
/////////////////////////////////////////////////////////////////////////////////////////
// INITIALIZATION
/////////////////////////////////////////////////////////////////////////////////////////
function evalFullScreen(value) {
if (value == 'On') {
xapi.command('UserInterface Extensions Widget SetValue', { WidgetId: 'widget_FS_selfview', Value: 'on' });
}
else {
xapi.command('UserInterface Extensions Widget SetValue', { WidgetId: 'widget_FS_selfview', Value: 'off' });
}
}
// evalFullScreenEvent is needed because we have to check when someone manually turns on full screen
// when self view is already selected... it will eventually check FullScreen again, but that should be
// harmless
function evalFullScreenEvent(value) {
if (value == 'On') {
xapi.Status.Video.Selfview.Mode.get().then(evalSelfView);
}
else {
xapi.command('UserInterface Extensions Widget SetValue', { WidgetId: 'widget_FS_selfview', Value: 'off' });
}
}
function evalSelfView(value) {
if (value == 'On') {
xapi.Status.Video.Selfview.FullscreenMode.get().then(evalFullScreen);
}
else {
xapi.command('UserInterface Extensions Widget SetValue', { WidgetId: 'widget_FS_selfview', Value: 'off' });
}
}
async function init() {
console.log('init');
try {
auxCodec = new GMM.Connect.IP(AUX_CODEC_AUTH, '', AUX_CODEC_IP)
} catch (e) {
console.error(e)
}
// check for presenterTrack being configured
let enabledGet = await xapi.Config.Cameras.PresenterTrack.Enabled.get()
presenterTrackConfigured = (enabledGet == 'True') ? true : false;
// Stop any VuMeters that might have been left from a previous macro run with a different MICROPHONE_CONNECTORS constant
// to prevent errors due to unhandled vuMeter events.
xapi.Command.Audio.VuMeter.StopAll({});
// register callback for processing manual mute setting on codec
xapi.Status.Audio.Microphones.Mute.on((state) => {
console.log(`handleMicMuteResponse: ${state}`);
if (state == 'On') {
stopSideBySideTimer();
setTimeout(handleMicMuteOn, 2000);
}
else if (state == 'Off') {
handleMicMuteOff();
}
});
// register event handlers for local events
xapi.Status.Standby.State
.on(value => {
console.log(value);
if (value == "Off") handleWakeUp();
if (value == "Standby") handleShutDown();
});
// register handler for Widget actions
xapi.event.on('UserInterface Extensions Widget Action', (event) =>
handleOverrideWidget(event));
// register handler for Call Successful
xapi.Event.CallSuccessful.on(async () => {
console.log("Starting new call timer...");
await startAutomation();
recallSideBySideMode();
startInitialCallTimer();
});
// register handler for Call Disconnect
xapi.Event.CallDisconnect.on(async () => {
console.log("Turning off Self View....");
xapi.Command.Video.Selfview.Set({ Mode: 'off' });
webrtc_mode = false; // ending webrtc calls is being notified here now in RoomOS11
stopAutomation();
});
// check RoomOS versions
isOSTen = await check4_Minimum_Version_Required(minOS10Version);
isOSEleven = await check4_Minimum_Version_Required(minOS11Version);
// register WebRTC Mode and HDMI Passhtorugh mode handlers if RoomOS 11
if (isOSEleven) {
xapi.Status.UserInterface.WebView.Type.on(async (value) => {
if (value === 'WebRTCMeeting') {
webrtc_mode = true;
console.log("Starting automation due to WebRTCMeeting event...");
startAutomation();
startInitialCallTimer();
} else {
webrtc_mode = false;
if (!usb_mode) {
console.log("Turning off Self View....");
xapi.Command.Video.Selfview.Set({ Mode: 'off' });
console.log("Stopping automation due to a non-WebRTCMeeting event...");
stopAutomation();
}
}
});
xapi.Status.Video.Output.HDMI.Passthrough.Status.on(value => {
console.log(value)
if (value == 'Active') {
console.warn(`System is in Passthrough Active Mode`)
startAutomation();
usb_mode = true;
} else {
console.warn(`System is in Passthrough Inactive Mode`)
stopAutomation();
usb_mode = false;
}
});
}
// set self-view toggle on custom panel depending on Codec status that might have been set manually
xapi.Status.Video.Selfview.Mode.get().then(evalSelfView);
// register to receive events when someone manually turns on self-view
// so we can keep the custom toggle button in the right state
xapi.Status.Video.Selfview.Mode.on(evalSelfView);
// register to receive events when someone manually turns on full screen mode
// so we can keep the custom toggle button in the right state if also in self view
xapi.Status.Video.Selfview.FullscreenMode.on(evalFullScreenEvent);
// next, set Automatic mode toggle switch on custom panel off since the macro starts that way
xapi.command('UserInterface Extensions Widget SetValue', { WidgetId: 'widget_override', Value: 'off' });
// next, set side by side mode panel to whatever is configured initially
xapi.command('UserInterface Extensions Widget SetValue', { WidgetId: 'widget_sbs_control', Value: (perma_sbs) ? 'on' : 'off' });
}
/////////////////////////////////////////////////////////////////////////////////////////
// START/STOP AUTOMATION FUNCTIONS
/////////////////////////////////////////////////////////////////////////////////////////
async function startAutomation() {
console.log('startAutomation');
//setting overall manual mode to false
manual_mode = false;
allowCameraSwitching = true;
// presenterTrack cannot be on when we start automation
if (presenterTrackConfigured) {
xapi.Command.Cameras.PresenterTrack.Set({ Mode: 'Off' });
}
if (isOSEleven) {
try {
xapi.Config.Cameras.SpeakerTrack.DefaultBehavior.set(ST_DEFAULT_BEHAVIOR);
const webViewType = await xapi.Status.UserInterface.WebView.Type.get()
if (webViewType == 'WebRTCMeeting') webrtc_mode = true;
} catch (e) {
console.log('Unable to read WebView Type.. assuming not in webrtc mode')
}
}
// Always turn on SpeakerTrack when the Automation is started. It is also turned on when a call connects so that
// if it is manually turned off while outside of a call it goes back to the correct state
if (has_SpeakerTrack) xapi.command('Cameras SpeakerTrack Activate').catch(handleError);
//registering vuMeter event handler
micHandler = xapi.event.on('Audio Input Connectors Microphone', (event) => {
//adding protection for mis-configured mics
if (typeof micArrays[event.id[0]] != 'undefined') {
micArrays[event.id[0]].pop();
micArrays[event.id[0]].push(event.VuMeter);
// checking on manual_mode might be unnecessary because in manual mode,
// audio events should not be triggered
if (manual_mode == false) {
// invoke main logic to check mic levels ans switch to correct camera input
checkMicLevelsToSwitchCamera();
}
}
});
// start VuMeter monitoring
console.log("Turning on VuMeter monitoring...")
for (var i in MICROPHONE_CONNECTORS) {
xapi.command('Audio VuMeter Start', {
ConnectorId: MICROPHONE_CONNECTORS[i],
ConnectorType: 'Microphone',
IntervalMs: 500,
Source: 'AfterAEC'
});
}
// set toggle button on custom panel to reflect that automation is turned on.
xapi.command('UserInterface Extensions Widget SetValue', { WidgetId: 'widget_override', Value: 'on' });
}
function stopAutomation(reset_source = true) {
//setting overall manual mode to true
manual_mode = true;
stopSideBySideTimer();
stopNewSpeakerTimer();
stopInitialCallTimer();
lastActiveHighInput = 0; //TODO: check to see if this improves turning on/off the automation
lowWasRecalled = true; //TODO: check to see if this improves turning on/of the automation
console.log("Stopping all VuMeters...");
xapi.Command.Audio.VuMeter.StopAll({});
//TODO: check to see if when we stop automation we really want to switch to connectorID 1
if (reset_source) {
console.log("Switching to MainVideoSource connectorID 1 ...");
xapi.Command.Video.Input.SetMainVideoSource({ SourceId: 1 });
}
// using proper way to de-register handlers
micHandler();
micHandler = () => void 0;
// set toggle button on custom panel to reflect that automation is turned off.
xapi.command('UserInterface Extensions Widget SetValue', { WidgetId: 'widget_override', Value: 'off' });
}
/////////////////////////////////////////////////////////////////////////////////////////
// MICROPHONE DETECTION AND CAMERA SWITCHING LOGIC FUNCTIONS
/////////////////////////////////////////////////////////////////////////////////////////
function checkMicLevelsToSwitchCamera() {
// make sure we've gotten enough samples from each mic in order to do averages
if (allowCameraSwitching) {
// figure out which of the inputs has the highest average level then perform logic for that input *ONLY* if allowCameraSwitching is true
let array_key = largestMicValue();
let array = [];
array = micArrays[array_key];
// get the average level for the currently active input
let average = averageArray(array);
//get the input number as an int since it is passed as a string (since it is a key to a dict)
let input = parseInt(array_key);
// someone is speaking
if (average > MICROPHONEHIGH) {
// start timer to prevent Side-by-Side mode too quickly
restartSideBySideTimer();
if (input > 0) {
lowWasRecalled = false;
// no one was talking before
if (lastActiveHighInput === 0) {
makeCameraSwitch(input, average);
}
// the same person is talking
else if (lastActiveHighInput === input) {
restartNewSpeakerTimer();
}
// a different person is talking
else if (lastActiveHighInput !== input) {
if (allowNewSpeaker) {
makeCameraSwitch(input, average);
}
}
}
}
// no one is speaking
else if (average < MICROPHONELOW) {
// only trigger if enough time has elapsed since someone spoke last
if (allowSideBySide) {
if (input > 0 && !lowWasRecalled) {
lastActiveHighInput = 0;
lowWasRecalled = true;
console.log("-------------------------------------------------");
console.log("Low Triggered");
console.log("-------------------------------------------------");
recallSideBySideMode();
}
}
}
}
}
// function to actually switch the camera input
async function makeCameraSwitch(input, average) {
console.log("-------------------------------------------------");
console.log("High Triggered: ");
console.log(`Input = ${input} | Average = ${average}`);
console.log("-------------------------------------------------");
// first obtain the Map Camera Sources value that corresponds to the loudest microphone
// we want to use for switching camera input
var selectedSource = MAP_CAMERA_SOURCES[MICROPHONE_CONNECTORS.indexOf(input)]
if (!perma_sbs) {
// We do not need to check for has_SpeakerTrack below because we are implicitly
// checking for that by evaluating typeof selectedSource
if (typeof selectedSource == 'number') {
if (selectedSource == SP) {
// if the active camera is a SpeakerTrack camera, just activate it, no need to set main video source to it
console.log('Switching to SpeakerTrack camera');
resumeSpeakerTrack();
xapi.command('Cameras SpeakerTrack Activate').catch(handleError);
}
else {
// the Video Input SetMainVideoSource does not work while Speakertrack is active
// so we need to turn it off in case the previous video input was from a source where
// SpeakerTrack is used.
//xapi.command('Cameras SpeakerTrack Deactivate').catch(handleError);
pauseSpeakerTrack();
// Switch to the source that is speficied in the same index position in MAP_CAMERA_SOURCE_IDS
let sourceDict = { SourceID: '0' }
sourceDict["SourceID"] = selectedSource.toString();
console.log("Switching to input with SetMainVideoSource with dict: ", sourceDict)
xapi.command('Video Input SetMainVideoSource', sourceDict).catch(handleError);
if ((MAP_CAMERA_SOURCES.indexOf(SP) == -1) && (selectedSource == MAIN_CODEC_QUADCAM_SOURCE_ID)) {
// if the codec is using a QuadCam (no SpeakerTrack camera allowed) then
// turn back on SpeakerTrack function on the codec in case it was turned off in side by side mode.
//xapi.command('Cameras SpeakerTrack Activate').catch(handleError);
resumeSpeakerTrack();
}
}
// if we are not switching to a camera zone with PTZ cameras, we need to re-set the
// lastActivePTZCameraZone Object to the "non-camera" value of Z0 as when we started the macro
// because the decision tree on switching or not from a camera that was already pointed at someone
// relies on the last video input source having been a PTZ camera video zone
lastActivePTZCameraZoneObj = Z0;
lastActivePTZCameraZoneCamera = '0';
}
else if (typeof selectedSource == 'object') {
switchToVideoZone(selectedSource);
}
// send required messages to auxiliary codec that also turns on speakertrack over there
await sendIntercodecMessage(AUX_CODEC, 'automatic_mode');
} else {
// if "permanent" side by side is selected, just switch to that
console.log("Permanent side by side active when inside makeCameraSwitch()...")
permaSideBySideMode(selectedSource);
}
lastActiveHighInput = input;
restartNewSpeakerTimer();
}
async function switchToVideoZone(selectedSource) {
// The mic input mapped to a PTZ camera is to be selected, first check that camera zone was already being used
if (lastActivePTZCameraZoneObj == selectedSource) {
// same camera zone as before, so we do not want to change the inUse value of that zone object (keep it inUse=true)
console.log("Still using same camera zone, no need to Activate camera preset.")
}
else {
var selectedSourcePrimaryCamID = '';
var selectedSourceSecondaryCamID = '';
var thePresetId = 0;
var thePresetVideoSource = 0;
// Since this is a camera zone, first check if primary or secondary to be selected based on the possibility
// that the previous zone was using the same physical camera than the new zone selected.
selectedSourcePrimaryCamID = await getPresetCamera(selectedSource['primary']);
if (selectedSourcePrimaryCamID != lastActivePTZCameraZoneCamera) {
thePresetId = selectedSource['primary'];
thePresetVideoSource = MAP_PTZ_CAMERA_VIDEO_SOURCE_ID[selectedSourcePrimaryCamID]
lastActivePTZCameraZoneObj = selectedSource;
lastActivePTZCameraZoneCamera = selectedSourcePrimaryCamID;
}
else {
selectedSourceSecondaryCamID = await getPresetCamera(selectedSource['secondary']);
thePresetId = selectedSource['secondary'];
thePresetVideoSource = MAP_PTZ_CAMERA_VIDEO_SOURCE_ID[selectedSourceSecondaryCamID]
lastActivePTZCameraZoneObj = selectedSource;
lastActivePTZCameraZoneCamera = selectedSourceSecondaryCamID;
}
// instruct the codec to now use the correct camera preset
console.log('Switching to preset ID: ' + thePresetId + ' which uses camera: ' + lastActivePTZCameraZoneCamera);
xapi.Command.Camera.Preset.Activate({ PresetId: thePresetId });
// now set main video source to where the camera is connected
setTimeout(function () {
setMainVideoSource(thePresetVideoSource);
}, VIDEO_SOURCE_SWITCH_WAIT_TIME);
}
}
function setMainVideoSource(thePresetVideoSource) {
// the Video Input SetMainVideoSource does not work while Speakertrack is active
// so we need to turn it off in case the previous video input was from a source where
// SpeakerTrack is used.
//if (has_SpeakerTrack) xapi.command('Cameras SpeakerTrack Deactivate').catch(handleError);
if (has_SpeakerTrack) pauseSpeakerTrack();
let sourceDict = { SourceID: '0' }
sourceDict["SourceID"] = thePresetVideoSource.toString();
console.log("In setMainVideoSource() switching to input with SetMainVideoSource with dict: ", sourceDict)
xapi.command('Video Input SetMainVideoSource', sourceDict).catch(handleError);
}
function largestMicValue() {
// figure out which of the inputs has the highest average level and return the corresponding key
let currentMaxValue = 0;
let currentMaxKey = '';
let theAverage = 0;
for (var i in MICROPHONE_CONNECTORS) {
theAverage = averageArray(micArrays[MICROPHONE_CONNECTORS[i].toString()]);
if (theAverage >= currentMaxValue) {
currentMaxKey = MICROPHONE_CONNECTORS[i].toString();
currentMaxValue = theAverage;
}
}
return currentMaxKey;
}
function averageArray(arrayIn) {
let sum = 0;
for (var i = 0; i < arrayIn.length; i++) {
sum = sum + parseInt(arrayIn[i], 10);
}
let avg = (sum / arrayIn.length) * arrayIn.length;
return avg;
}
async function recallSideBySideMode() {
//first we need to clear out the lastActivePTZCameraZone vars since we want to make sure
// that after SideBySideMode is called, the next call to switchToVideoZone() does actually force
// a switch
lastActivePTZCameraZoneObj = Z0;
lastActivePTZCameraZoneCamera = '0';
if (overviewShowDouble && !webrtc_mode) { //WebRTC mode does not support composing yet even in RoomOS11
let connectorDict = { ConnectorId: [0, 0] };
connectorDict["ConnectorId"] = OVERVIEW_DOUBLE_SOURCE_IDS;
console.log("Trying to use this for connector dict in recallSideBySideMode(): ", connectorDict)
xapi.command('Video Input SetMainVideoSource', connectorDict).catch(handleError);
//if (has_SpeakerTrack) xapi.command('Cameras SpeakerTrack Deactivate').catch(handleError);
if (has_SpeakerTrack) pauseSpeakerTrack();
xapi.command('Camera Preset Activate', { PresetId: 30 }).catch(handleError);
const payload = { EditMatrixOutput: { sources: connectorDict["ConnectorId"] } };
setTimeout(function () {
//Let USB Macro know we are composing
localCallout.command(payload).post()
}, 250) //250ms delay to allow the main source to resolve first
}
else {
// Check for OVERVIEW_PRESET_ZONE. If set to default Z0, just SetMainVideoSource
if (OVERVIEW_PRESET_ZONE == Z0) {
let sourceDict = { SourceID: '0' };
sourceDict["SourceID"] = OVERVIEW_SINGLE_SOURCE_ID.toString();
console.log("Trying to use this for source dict in recallSideBySideMode(): ", sourceDict)
xapi.command('Video Input SetMainVideoSource', sourceDict).catch(handleError);
//if (has_SpeakerTrack) xapi.command('Cameras SpeakerTrack Deactivate').catch(handleError);
if (has_SpeakerTrack) pauseSpeakerTrack();
}
else {
// If OVERVIEW_PRESET_ZONE is defined as something other than Z0, switch to that
console.log('Recall side by side mode switching to preset OVERVIEW_PRESET_ZONE...');
//if (has_SpeakerTrack) xapi.command('Cameras SpeakerTrack Deactivate').catch(handleError);
if (has_SpeakerTrack) pauseSpeakerTrack();
switchToVideoZone(OVERVIEW_PRESET_ZONE);
}
}
// send required messages to other codecs
await sendIntercodecMessage(AUX_CODEC, 'side_by_side');
lastActiveHighInput = 0;
lowWasRecalled = true;
}
async function permaSideBySideMode(selectedSource) {
if (overviewShowDouble && !webrtc_mode) { //WebRTC mode does not support composing yet even in RoomOS11
let connectorDict = { ConnectorId: [0, 0] };
connectorDict["ConnectorId"] = OVERVIEW_DOUBLE_SOURCE_IDS;
console.log("Trying to use this for connector dict in permaSideBySideMode(): ", connectorDict)
xapi.command('Video Input SetMainVideoSource', connectorDict).catch(handleError);
if (MAIN_CODEC_QUADCAM_SOURCE_ID == selectedSource) {
await sendIntercodecMessage(AUX_CODEC, 'side_by_side');
//if (has_SpeakerTrack) xapi.command('Cameras SpeakerTrack Activate').catch(handleError);
resumeSpeakerTrack();
} else {
await sendIntercodecMessage(AUX_CODEC, 'automatic_mode');
//if (has_SpeakerTrack) xapi.command('Cameras SpeakerTrack Deactivate').catch(handleError);
if (has_SpeakerTrack) pauseSpeakerTrack();
xapi.command('Camera Preset Activate', { PresetId: 30 }).catch(handleError);
}
const payload = { EditMatrixOutput: { sources: connectorDict["ConnectorId"] } };
setTimeout(function () {
//Let USB Macro know we are composing
localCallout.command(payload).post()
}, 250) //250ms delay to allow the main source to resolve first
}
else {
console.log('Cannot set permanent side by side mode without overviewShowDouble set to true or in WebRTC mode... ');
perma_sbs = false;
xapi.command('UserInterface Extensions Widget SetValue', { WidgetId: 'widget_sbs_control', Value: (perma_sbs) ? 'on' : 'off' });
}
}
/////////////////////////////////////////////////////////////////////////////////////////
// TOUCH 10 UI FUNCTION HANDLERS
/////////////////////////////////////////////////////////////////////////////////////////
function handleOverrideWidget(event) {
if (event.WidgetId === 'widget_override') {
console.log("Camera Control button selected.....")
if (event.Value === 'off') {
console.log("Camera Control is set to Manual...");
console.log("Stopping automation...")
stopAutomation();
}
else {
// start VuMeter monitoring
console.log("Camera Control is set to Automatic...");
console.log("Starting automation...")
startAutomation();
}
}
if (event.WidgetId === 'widget_sbs_control') {
console.log("Side by side control selected.....")
if (event.Value === 'off') {
console.log("Side by side control is set to overview...");
perma_sbs = false;
}
else {
console.log("Side by side control is set to always...");
perma_sbs = true;
}
// trigger a cameraSwitch evaluation
lastActiveHighInput = 0;
}
if (event.WidgetId === 'widget_FS_selfview') {
console.log("Selfview button selected.....")
if (event.Value === 'off') {
console.log("Selfview is set to Off...");
console.log("turning off self-view...")
xapi.Command.Video.Selfview.Set({ FullscreenMode: 'Off', Mode: 'Off', OnMonitorRole: 'First' });
}
else {
console.log("Selfview is set to On...");
console.log("turning on self-view...")
// TODO: determine if turning off self-view should also turn off fullscreenmode
xapi.Command.Video.Selfview.Set({ FullscreenMode: 'On', Mode: 'On', OnMonitorRole: 'First' });
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////
// ERROR HANDLING
/////////////////////////////////////////////////////////////////////////////////////////
function handleError(error) {
console.log(error);
}
/////////////////////////////////////////////////////////////////////////////////////////
// INTER-MACRO MESSAGE HANDLING
/////////////////////////////////////////////////////////////////////////////////////////
async function updateUSBModeConfig() {
var object = { AlterUSBConfig: { config: 'matrix_Camera_Mode', value: true } }
await localCallout.command(object).post()
}
GMM.Event.Receiver.on(event => {
const usb_mode_reg = /USB_Mode_Version_[0-9]*.*/gm
if ((typeof event) != 'string')
if (event.Source.Id == 'localhost') {
// we are evaluating a local event, first check to see if from the USB Mode macro
if (usb_mode_reg.test(event.App)) {
if (event.Type == 'Error') {
console.error(event)
} else {
switch (event.Value) {
case 'Initialized':
console.warn(`USB mode initialized...`)
updateUSBModeConfig();
break;
case 'EnteringWebexMode': case 'Entering_Default_Mode': case 'EnteringDefaultMode':
console.warn(`You are entering Webex Mode`)
//Run code here when Default Mode starts to configure
break;
case 'WebexModeStarted': case 'DefaultModeStarted':
console.warn(`System is in Default Mode`)
stopAutomation();
usb_mode = false;
// always tell the other codec when your are in or out of a call
//otherCodec.status('CALL_DISCONNECTED').post();
break;
case 'enteringUSBMode':
console.warn(`You are entering USB Mode`)
//Run code here when USB Mode starts to configure
break;
case 'USBModeStarted':
console.warn(`System is in Default Mode`)
startAutomation();
usb_mode = true;
// always tell the other codec when your are in or out of a call
//otherCodec.status('CALL_CONNECTED').post();
break;
default:
break;
}
}
}
else {
console.debug({
Message: `Received Message from ${event.App} and was not processed`
})
}
}
else
switch (event.Value) {
case "VTC-1_OK":
handleCodecOnline(AUX_CODEC);
break;
default:
break;
}
})