@stream-io/video-client 0.1.11 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -317,6 +317,25 @@ export interface BroadcastSettings {
317
317
  */
318
318
  hls: HLSSettings;
319
319
  }
320
+ /**
321
+ *
322
+ * @export
323
+ * @interface BroadcastSettingsRequest
324
+ */
325
+ export interface BroadcastSettingsRequest {
326
+ /**
327
+ *
328
+ * @type {boolean}
329
+ * @memberof BroadcastSettingsRequest
330
+ */
331
+ enabled?: boolean;
332
+ /**
333
+ *
334
+ * @type {HLSSettingsRequest}
335
+ * @memberof BroadcastSettingsRequest
336
+ */
337
+ hls?: HLSSettingsRequest;
338
+ }
320
339
  /**
321
340
  * This event is sent when a user accepts a notification to join a call.
322
341
  * @export
@@ -454,6 +473,12 @@ export interface CallCreatedEvent {
454
473
  * @interface CallEndedEvent
455
474
  */
456
475
  export interface CallEndedEvent {
476
+ /**
477
+ *
478
+ * @type {CallResponse}
479
+ * @memberof CallEndedEvent
480
+ */
481
+ call: CallResponse;
457
482
  /**
458
483
  *
459
484
  * @type {string}
@@ -1260,6 +1285,18 @@ export interface CallSessionResponse {
1260
1285
  * @memberof CallSessionResponse
1261
1286
  */
1262
1287
  id: string;
1288
+ /**
1289
+ *
1290
+ * @type {string}
1291
+ * @memberof CallSessionResponse
1292
+ */
1293
+ live_ended_at?: string;
1294
+ /**
1295
+ *
1296
+ * @type {string}
1297
+ * @memberof CallSessionResponse
1298
+ */
1299
+ live_started_at?: string;
1263
1300
  /**
1264
1301
  *
1265
1302
  * @type {Array<CallParticipantResponse>}
@@ -1340,6 +1377,12 @@ export interface CallSettingsRequest {
1340
1377
  * @memberof CallSettingsRequest
1341
1378
  */
1342
1379
  backstage?: BackstageSettingsRequest;
1380
+ /**
1381
+ *
1382
+ * @type {BroadcastSettingsRequest}
1383
+ * @memberof CallSettingsRequest
1384
+ */
1385
+ broadcasting?: BroadcastSettingsRequest;
1343
1386
  /**
1344
1387
  *
1345
1388
  * @type {GeofenceSettingsRequest}
@@ -2413,6 +2456,31 @@ export interface HLSSettings {
2413
2456
  */
2414
2457
  quality_tracks: Array<string>;
2415
2458
  }
2459
+ /**
2460
+ *
2461
+ * @export
2462
+ * @interface HLSSettingsRequest
2463
+ */
2464
+ export interface HLSSettingsRequest {
2465
+ /**
2466
+ *
2467
+ * @type {boolean}
2468
+ * @memberof HLSSettingsRequest
2469
+ */
2470
+ auto_on?: boolean;
2471
+ /**
2472
+ *
2473
+ * @type {boolean}
2474
+ * @memberof HLSSettingsRequest
2475
+ */
2476
+ enabled?: boolean;
2477
+ /**
2478
+ *
2479
+ * @type {Array<string>}
2480
+ * @memberof HLSSettingsRequest
2481
+ */
2482
+ quality_tracks?: Array<string>;
2483
+ }
2416
2484
  /**
2417
2485
  *
2418
2486
  * @export
@@ -2836,6 +2904,7 @@ export const OwnCapability = {
2836
2904
  JOIN_CALL: 'join-call',
2837
2905
  JOIN_ENDED_CALL: 'join-ended-call',
2838
2906
  MUTE_USERS: 'mute-users',
2907
+ PIN_FOR_EVERYONE: 'pin-for-everyone',
2839
2908
  READ_CALL: 'read-call',
2840
2909
  REMOVE_CALL_MEMBER: 'remove-call-member',
2841
2910
  SCREENSHARE: 'screenshare',
@@ -2959,6 +3028,38 @@ export interface PermissionRequestEvent {
2959
3028
  */
2960
3029
  user: UserResponse;
2961
3030
  }
3031
+ /**
3032
+ *
3033
+ * @export
3034
+ * @interface PinRequest
3035
+ */
3036
+ export interface PinRequest {
3037
+ /**
3038
+ *
3039
+ * @type {string}
3040
+ * @memberof PinRequest
3041
+ */
3042
+ session_id: string;
3043
+ /**
3044
+ *
3045
+ * @type {string}
3046
+ * @memberof PinRequest
3047
+ */
3048
+ user_id: string;
3049
+ }
3050
+ /**
3051
+ *
3052
+ * @export
3053
+ * @interface PinResponse
3054
+ */
3055
+ export interface PinResponse {
3056
+ /**
3057
+ * Duration of the request in human-readable format
3058
+ * @type {string}
3059
+ * @memberof PinResponse
3060
+ */
3061
+ duration: string;
3062
+ }
2962
3063
  /**
2963
3064
  *
2964
3065
  * @export
@@ -3774,6 +3875,38 @@ export interface UnblockedUserEvent {
3774
3875
  */
3775
3876
  user: UserResponse;
3776
3877
  }
3878
+ /**
3879
+ *
3880
+ * @export
3881
+ * @interface UnpinRequest
3882
+ */
3883
+ export interface UnpinRequest {
3884
+ /**
3885
+ *
3886
+ * @type {string}
3887
+ * @memberof UnpinRequest
3888
+ */
3889
+ session_id: string;
3890
+ /**
3891
+ *
3892
+ * @type {string}
3893
+ * @memberof UnpinRequest
3894
+ */
3895
+ user_id: string;
3896
+ }
3897
+ /**
3898
+ *
3899
+ * @export
3900
+ * @interface UnpinResponse
3901
+ */
3902
+ export interface UnpinResponse {
3903
+ /**
3904
+ * Duration of the request in human-readable format
3905
+ * @type {string}
3906
+ * @memberof UnpinResponse
3907
+ */
3908
+ duration: string;
3909
+ }
3777
3910
  /**
3778
3911
  *
3779
3912
  * @export
@@ -28,6 +28,7 @@ import {
28
28
  Participant,
29
29
  ParticipantCount,
30
30
  PeerType,
31
+ Pin,
31
32
  TrackInfo,
32
33
  TrackType,
33
34
  TrackUnpublishReason,
@@ -218,10 +219,31 @@ export interface SfuEvent {
218
219
  */
219
220
  iceRestart: ICERestart;
220
221
  }
222
+ | {
223
+ oneofKind: 'pinsUpdated';
224
+ /**
225
+ * PinsChanged is sent the list of pins in the call changes. This event contains the entire list of pins.
226
+ *
227
+ * @generated from protobuf field: stream.video.sfu.event.PinsChanged pins_updated = 22;
228
+ */
229
+ pinsUpdated: PinsChanged;
230
+ }
221
231
  | {
222
232
  oneofKind: undefined;
223
233
  };
224
234
  }
235
+ /**
236
+ * @generated from protobuf message stream.video.sfu.event.PinsChanged
237
+ */
238
+ export interface PinsChanged {
239
+ /**
240
+ * the list of pins in the call.
241
+ * Pins are ordered in descending order (most important first).
242
+ *
243
+ * @generated from protobuf field: repeated stream.video.sfu.models.Pin pins = 1;
244
+ */
245
+ pins: Pin[];
246
+ }
225
247
  /**
226
248
  * @generated from protobuf message stream.video.sfu.event.Error
227
249
  */
@@ -844,6 +866,13 @@ class SfuEvent$Type extends MessageType<SfuEvent> {
844
866
  oneof: 'eventPayload',
845
867
  T: () => ICERestart,
846
868
  },
869
+ {
870
+ no: 22,
871
+ name: 'pins_updated',
872
+ kind: 'message',
873
+ oneof: 'eventPayload',
874
+ T: () => PinsChanged,
875
+ },
847
876
  ]);
848
877
  }
849
878
  create(value?: PartialMessage<SfuEvent>): SfuEvent {
@@ -1055,6 +1084,17 @@ class SfuEvent$Type extends MessageType<SfuEvent> {
1055
1084
  ),
1056
1085
  };
1057
1086
  break;
1087
+ case /* stream.video.sfu.event.PinsChanged pins_updated */ 22:
1088
+ message.eventPayload = {
1089
+ oneofKind: 'pinsUpdated',
1090
+ pinsUpdated: PinsChanged.internalBinaryRead(
1091
+ reader,
1092
+ reader.uint32(),
1093
+ options,
1094
+ (message.eventPayload as any).pinsUpdated,
1095
+ ),
1096
+ };
1097
+ break;
1058
1098
  default:
1059
1099
  let u = options.readUnknownField;
1060
1100
  if (u === 'throw')
@@ -1198,6 +1238,13 @@ class SfuEvent$Type extends MessageType<SfuEvent> {
1198
1238
  writer.tag(21, WireType.LengthDelimited).fork(),
1199
1239
  options,
1200
1240
  ).join();
1241
+ /* stream.video.sfu.event.PinsChanged pins_updated = 22; */
1242
+ if (message.eventPayload.oneofKind === 'pinsUpdated')
1243
+ PinsChanged.internalBinaryWrite(
1244
+ message.eventPayload.pinsUpdated,
1245
+ writer.tag(22, WireType.LengthDelimited).fork(),
1246
+ options,
1247
+ ).join();
1201
1248
  let u = options.writeUnknownFields;
1202
1249
  if (u !== false)
1203
1250
  (u == true ? UnknownFieldHandler.onWrite : u)(
@@ -1213,6 +1260,90 @@ class SfuEvent$Type extends MessageType<SfuEvent> {
1213
1260
  */
1214
1261
  export const SfuEvent = new SfuEvent$Type();
1215
1262
  // @generated message type with reflection information, may provide speed optimized methods
1263
+ class PinsChanged$Type extends MessageType<PinsChanged> {
1264
+ constructor() {
1265
+ super('stream.video.sfu.event.PinsChanged', [
1266
+ {
1267
+ no: 1,
1268
+ name: 'pins',
1269
+ kind: 'message',
1270
+ repeat: 1 /*RepeatType.PACKED*/,
1271
+ T: () => Pin,
1272
+ },
1273
+ ]);
1274
+ }
1275
+ create(value?: PartialMessage<PinsChanged>): PinsChanged {
1276
+ const message = { pins: [] };
1277
+ globalThis.Object.defineProperty(message, MESSAGE_TYPE, {
1278
+ enumerable: false,
1279
+ value: this,
1280
+ });
1281
+ if (value !== undefined)
1282
+ reflectionMergePartial<PinsChanged>(this, message, value);
1283
+ return message;
1284
+ }
1285
+ internalBinaryRead(
1286
+ reader: IBinaryReader,
1287
+ length: number,
1288
+ options: BinaryReadOptions,
1289
+ target?: PinsChanged,
1290
+ ): PinsChanged {
1291
+ let message = target ?? this.create(),
1292
+ end = reader.pos + length;
1293
+ while (reader.pos < end) {
1294
+ let [fieldNo, wireType] = reader.tag();
1295
+ switch (fieldNo) {
1296
+ case /* repeated stream.video.sfu.models.Pin pins */ 1:
1297
+ message.pins.push(
1298
+ Pin.internalBinaryRead(reader, reader.uint32(), options),
1299
+ );
1300
+ break;
1301
+ default:
1302
+ let u = options.readUnknownField;
1303
+ if (u === 'throw')
1304
+ throw new globalThis.Error(
1305
+ `Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`,
1306
+ );
1307
+ let d = reader.skip(wireType);
1308
+ if (u !== false)
1309
+ (u === true ? UnknownFieldHandler.onRead : u)(
1310
+ this.typeName,
1311
+ message,
1312
+ fieldNo,
1313
+ wireType,
1314
+ d,
1315
+ );
1316
+ }
1317
+ }
1318
+ return message;
1319
+ }
1320
+ internalBinaryWrite(
1321
+ message: PinsChanged,
1322
+ writer: IBinaryWriter,
1323
+ options: BinaryWriteOptions,
1324
+ ): IBinaryWriter {
1325
+ /* repeated stream.video.sfu.models.Pin pins = 1; */
1326
+ for (let i = 0; i < message.pins.length; i++)
1327
+ Pin.internalBinaryWrite(
1328
+ message.pins[i],
1329
+ writer.tag(1, WireType.LengthDelimited).fork(),
1330
+ options,
1331
+ ).join();
1332
+ let u = options.writeUnknownFields;
1333
+ if (u !== false)
1334
+ (u == true ? UnknownFieldHandler.onWrite : u)(
1335
+ this.typeName,
1336
+ message,
1337
+ writer,
1338
+ );
1339
+ return writer;
1340
+ }
1341
+ }
1342
+ /**
1343
+ * @generated MessageType for protobuf message stream.video.sfu.event.PinsChanged
1344
+ */
1345
+ export const PinsChanged = new PinsChanged$Type();
1346
+ // @generated message type with reflection information, may provide speed optimized methods
1216
1347
  class Error$Type extends MessageType<Error> {
1217
1348
  constructor() {
1218
1349
  super('stream.video.sfu.event.Error', [
@@ -48,6 +48,13 @@ export interface CallState {
48
48
  * @generated from protobuf field: stream.video.sfu.models.ParticipantCount participant_count = 3;
49
49
  */
50
50
  participantCount?: ParticipantCount;
51
+ /**
52
+ * the list of pins in the call.
53
+ * Pins are ordered in descending order (most important first).
54
+ *
55
+ * @generated from protobuf field: repeated stream.video.sfu.models.Pin pins = 4;
56
+ */
57
+ pins: Pin[];
51
58
  }
52
59
  /**
53
60
  * @generated from protobuf message stream.video.sfu.models.ParticipantCount
@@ -67,6 +74,23 @@ export interface ParticipantCount {
67
74
  */
68
75
  anonymous: number;
69
76
  }
77
+ /**
78
+ * @generated from protobuf message stream.video.sfu.models.Pin
79
+ */
80
+ export interface Pin {
81
+ /**
82
+ * the user to pin
83
+ *
84
+ * @generated from protobuf field: string user_id = 1;
85
+ */
86
+ userId: string;
87
+ /**
88
+ * the user sesion_id to pin, if not provided, applies to all sessions
89
+ *
90
+ * @generated from protobuf field: string session_id = 2;
91
+ */
92
+ sessionId: string;
93
+ }
70
94
  /**
71
95
  * those who are online in the call
72
96
  *
@@ -695,10 +719,17 @@ class CallState$Type extends MessageType<CallState> {
695
719
  kind: 'message',
696
720
  T: () => ParticipantCount,
697
721
  },
722
+ {
723
+ no: 4,
724
+ name: 'pins',
725
+ kind: 'message',
726
+ repeat: 1 /*RepeatType.PACKED*/,
727
+ T: () => Pin,
728
+ },
698
729
  ]);
699
730
  }
700
731
  create(value?: PartialMessage<CallState>): CallState {
701
- const message = { participants: [] };
732
+ const message = { participants: [], pins: [] };
702
733
  globalThis.Object.defineProperty(message, MESSAGE_TYPE, {
703
734
  enumerable: false,
704
735
  value: this,
@@ -739,6 +770,11 @@ class CallState$Type extends MessageType<CallState> {
739
770
  message.participantCount,
740
771
  );
741
772
  break;
773
+ case /* repeated stream.video.sfu.models.Pin pins */ 4:
774
+ message.pins.push(
775
+ Pin.internalBinaryRead(reader, reader.uint32(), options),
776
+ );
777
+ break;
742
778
  default:
743
779
  let u = options.readUnknownField;
744
780
  if (u === 'throw')
@@ -784,6 +820,13 @@ class CallState$Type extends MessageType<CallState> {
784
820
  writer.tag(3, WireType.LengthDelimited).fork(),
785
821
  options,
786
822
  ).join();
823
+ /* repeated stream.video.sfu.models.Pin pins = 4; */
824
+ for (let i = 0; i < message.pins.length; i++)
825
+ Pin.internalBinaryWrite(
826
+ message.pins[i],
827
+ writer.tag(4, WireType.LengthDelimited).fork(),
828
+ options,
829
+ ).join();
787
830
  let u = options.writeUnknownFields;
788
831
  if (u !== false)
789
832
  (u == true ? UnknownFieldHandler.onWrite : u)(
@@ -878,6 +921,84 @@ class ParticipantCount$Type extends MessageType<ParticipantCount> {
878
921
  */
879
922
  export const ParticipantCount = new ParticipantCount$Type();
880
923
  // @generated message type with reflection information, may provide speed optimized methods
924
+ class Pin$Type extends MessageType<Pin> {
925
+ constructor() {
926
+ super('stream.video.sfu.models.Pin', [
927
+ { no: 1, name: 'user_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
928
+ { no: 2, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
929
+ ]);
930
+ }
931
+ create(value?: PartialMessage<Pin>): Pin {
932
+ const message = { userId: '', sessionId: '' };
933
+ globalThis.Object.defineProperty(message, MESSAGE_TYPE, {
934
+ enumerable: false,
935
+ value: this,
936
+ });
937
+ if (value !== undefined) reflectionMergePartial<Pin>(this, message, value);
938
+ return message;
939
+ }
940
+ internalBinaryRead(
941
+ reader: IBinaryReader,
942
+ length: number,
943
+ options: BinaryReadOptions,
944
+ target?: Pin,
945
+ ): Pin {
946
+ let message = target ?? this.create(),
947
+ end = reader.pos + length;
948
+ while (reader.pos < end) {
949
+ let [fieldNo, wireType] = reader.tag();
950
+ switch (fieldNo) {
951
+ case /* string user_id */ 1:
952
+ message.userId = reader.string();
953
+ break;
954
+ case /* string session_id */ 2:
955
+ message.sessionId = reader.string();
956
+ break;
957
+ default:
958
+ let u = options.readUnknownField;
959
+ if (u === 'throw')
960
+ throw new globalThis.Error(
961
+ `Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`,
962
+ );
963
+ let d = reader.skip(wireType);
964
+ if (u !== false)
965
+ (u === true ? UnknownFieldHandler.onRead : u)(
966
+ this.typeName,
967
+ message,
968
+ fieldNo,
969
+ wireType,
970
+ d,
971
+ );
972
+ }
973
+ }
974
+ return message;
975
+ }
976
+ internalBinaryWrite(
977
+ message: Pin,
978
+ writer: IBinaryWriter,
979
+ options: BinaryWriteOptions,
980
+ ): IBinaryWriter {
981
+ /* string user_id = 1; */
982
+ if (message.userId !== '')
983
+ writer.tag(1, WireType.LengthDelimited).string(message.userId);
984
+ /* string session_id = 2; */
985
+ if (message.sessionId !== '')
986
+ writer.tag(2, WireType.LengthDelimited).string(message.sessionId);
987
+ let u = options.writeUnknownFields;
988
+ if (u !== false)
989
+ (u == true ? UnknownFieldHandler.onWrite : u)(
990
+ this.typeName,
991
+ message,
992
+ writer,
993
+ );
994
+ return writer;
995
+ }
996
+ }
997
+ /**
998
+ * @generated MessageType for protobuf message stream.video.sfu.models.Pin
999
+ */
1000
+ export const Pin = new Pin$Type();
1001
+ // @generated message type with reflection information, may provide speed optimized methods
881
1002
  class Participant$Type extends MessageType<Participant> {
882
1003
  constructor() {
883
1004
  super('stream.video.sfu.models.Participant', [
@@ -22,6 +22,7 @@ const sfuEventKinds: { [key in SfuEventKinds]: undefined } = {
22
22
  callGrantsUpdated: undefined,
23
23
  goAway: undefined,
24
24
  iceRestart: undefined,
25
+ pinsUpdated: undefined,
25
26
  };
26
27
 
27
28
  export const isSfuEvent = (
@@ -112,15 +112,9 @@ export const reconcileParticipantLocalState = (
112
112
  ) => {
113
113
  if (!source) return target;
114
114
 
115
- target.audioStream = source.audioStream;
116
- target.videoStream = source.videoStream;
117
- target.screenShareStream = source.screenShareStream;
115
+ // copy everything from source to target
116
+ Object.assign(target, source);
118
117
 
119
- target.videoDimension = source.videoDimension;
120
- target.screenShareDimension = source.screenShareDimension;
121
- target.pinnedAt = source.pinnedAt;
122
- target.reaction = source.reaction;
123
- target.viewportVisibilityState = source.viewportVisibilityState;
124
118
  if (
125
119
  isStreamVideoLocalParticipant(source) &&
126
120
  isStreamVideoLocalParticipant(target)
@@ -92,7 +92,10 @@ export const participants = (): StreamVideoParticipant[] => [
92
92
  isDominantSpeaker: false,
93
93
  audioLevel: 0,
94
94
  image: '',
95
- pinnedAt: Date.now(),
95
+ pin: {
96
+ isLocalPin: true,
97
+ pinnedAt: Date.now(),
98
+ },
96
99
  roles: [],
97
100
  viewportVisibilityState: VisibilityState.VISIBLE,
98
101
  },
@@ -69,13 +69,15 @@ export const publishingAudio: Comparator<StreamVideoParticipant> = (a, b) => {
69
69
  * @param b the second participant.
70
70
  */
71
71
  export const pinned: Comparator<StreamVideoParticipant> = (a, b) => {
72
- if (a.pinnedAt && b.pinnedAt) {
73
- if (a.pinnedAt > b.pinnedAt) return -1;
74
- if (a.pinnedAt < b.pinnedAt) return 1;
72
+ if (a.pin && b.pin) {
73
+ if (!a.pin.isLocalPin && b.pin.isLocalPin) return -1;
74
+ if (a.pin.isLocalPin && !b.pin.isLocalPin) return 1;
75
+ if (a.pin.pinnedAt > b.pin.pinnedAt) return -1;
76
+ if (a.pin.pinnedAt < b.pin.pinnedAt) return 1;
75
77
  }
76
78
 
77
- if (a.pinnedAt && !b.pinnedAt) return -1;
78
- if (!a.pinnedAt && b.pinnedAt) return 1;
79
+ if (a.pin && !b.pin) return -1;
80
+ if (!a.pin && b.pin) return 1;
79
81
 
80
82
  return 0;
81
83
  };
@@ -16,7 +16,7 @@ import {
16
16
  MemberResponse,
17
17
  OwnCapability,
18
18
  } from '../gen/coordinator';
19
- import { TrackType } from '../gen/video/sfu/models/models';
19
+ import { Pin, TrackType } from '../gen/video/sfu/models/models';
20
20
  import { Comparator } from '../sorting';
21
21
  import * as SortingPreset from '../sorting/presets';
22
22
  import { getLogger } from '../logger';
@@ -287,7 +287,7 @@ export class CallState {
287
287
  );
288
288
 
289
289
  this.pinnedParticipants$ = this.participants$.pipe(
290
- map((participants) => participants.filter((p) => p.pinnedAt)),
290
+ map((participants) => participants.filter((p) => !!p.pin)),
291
291
  );
292
292
 
293
293
  this.dominantSpeaker$ = this.participants$.pipe(
@@ -678,4 +678,45 @@ export class CallState {
678
678
  }),
679
679
  );
680
680
  };
681
+
682
+ /**
683
+ * Updates the participant pinned state with server side pinning data.
684
+ *
685
+ * @param pins the latest pins from the server.
686
+ */
687
+ setServerSidePins = (pins: Pin[]) => {
688
+ const pinsLookup = pins.reduce<{ [sessionId: string]: number | undefined }>(
689
+ (lookup, pin) => {
690
+ lookup[pin.sessionId] = Date.now();
691
+ return lookup;
692
+ },
693
+ {},
694
+ );
695
+
696
+ return this.setParticipants((participants) =>
697
+ participants.map((participant) => {
698
+ const serverSidePinnedAt = pinsLookup[participant.sessionId];
699
+ // the participant is newly pinned
700
+ if (serverSidePinnedAt) {
701
+ return {
702
+ ...participant,
703
+ pin: {
704
+ isLocalPin: false,
705
+ pinnedAt: serverSidePinnedAt,
706
+ },
707
+ };
708
+ }
709
+ // the participant is no longer pinned server side
710
+ // we need to reset the pin
711
+ if (participant.pin && !participant.pin.isLocalPin) {
712
+ return {
713
+ ...participant,
714
+ pin: undefined,
715
+ };
716
+ }
717
+ // no changes to be applied
718
+ return participant;
719
+ }),
720
+ );
721
+ };
681
722
  }