@microsoft/live-share-media 1.3.0 → 1.4.0

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.
Files changed (78) hide show
  1. package/.nycrc.json +10 -10
  2. package/LICENSE +90 -90
  3. package/README.md +135 -135
  4. package/bin/LiveMediaSession.d.ts.map +1 -1
  5. package/bin/LiveMediaSession.js +9 -1
  6. package/bin/LiveMediaSession.js.map +1 -1
  7. package/bin/LiveMediaSessionCoordinator.d.ts +18 -2
  8. package/bin/LiveMediaSessionCoordinator.d.ts.map +1 -1
  9. package/bin/LiveMediaSessionCoordinator.js +52 -13
  10. package/bin/LiveMediaSessionCoordinator.js.map +1 -1
  11. package/bin/MediaPlayerSynchronizer.d.ts +6 -4
  12. package/bin/MediaPlayerSynchronizer.d.ts.map +1 -1
  13. package/bin/MediaPlayerSynchronizer.js +41 -12
  14. package/bin/MediaPlayerSynchronizer.js.map +1 -1
  15. package/bin/MediaSessionExtensions.d.ts +17 -8
  16. package/bin/MediaSessionExtensions.d.ts.map +1 -1
  17. package/bin/MediaSessionExtensions.js +4 -0
  18. package/bin/MediaSessionExtensions.js.map +1 -1
  19. package/bin/internals/GroupCoordinatorState.d.ts +15 -1
  20. package/bin/internals/GroupCoordinatorState.d.ts.map +1 -1
  21. package/bin/internals/GroupCoordinatorState.js +123 -57
  22. package/bin/internals/GroupCoordinatorState.js.map +1 -1
  23. package/bin/internals/GroupPlaybackPosition.d.ts +3 -1
  24. package/bin/internals/GroupPlaybackPosition.d.ts.map +1 -1
  25. package/bin/internals/GroupPlaybackPosition.js +7 -3
  26. package/bin/internals/GroupPlaybackPosition.js.map +1 -1
  27. package/bin/internals/GroupPlaybackRate.d.ts +39 -0
  28. package/bin/internals/GroupPlaybackRate.d.ts.map +1 -0
  29. package/bin/internals/GroupPlaybackRate.js +62 -0
  30. package/bin/internals/GroupPlaybackRate.js.map +1 -0
  31. package/bin/internals/GroupPlaybackTrack.d.ts +2 -2
  32. package/bin/internals/GroupPlaybackTrack.d.ts.map +1 -1
  33. package/bin/internals/GroupPlaybackTrack.js +3 -2
  34. package/bin/internals/GroupPlaybackTrack.js.map +1 -1
  35. package/bin/internals/GroupPlaybackTrackData.d.ts +3 -3
  36. package/bin/internals/GroupPlaybackTrackData.d.ts.map +1 -1
  37. package/bin/internals/GroupPlaybackTrackData.js +12 -11
  38. package/bin/internals/GroupPlaybackTrackData.js.map +1 -1
  39. package/bin/internals/GroupTransportState.d.ts +6 -3
  40. package/bin/internals/GroupTransportState.d.ts.map +1 -1
  41. package/bin/internals/GroupTransportState.js +16 -11
  42. package/bin/internals/GroupTransportState.js.map +1 -1
  43. package/bin/internals/consts.d.ts +10 -3
  44. package/bin/internals/consts.d.ts.map +1 -1
  45. package/bin/internals/consts.js +10 -3
  46. package/bin/internals/consts.js.map +1 -1
  47. package/bin/internals/index.d.ts +1 -0
  48. package/bin/internals/index.d.ts.map +1 -1
  49. package/bin/internals/index.js +1 -0
  50. package/bin/internals/index.js.map +1 -1
  51. package/bin/internals/interfaces.d.ts +10 -0
  52. package/bin/internals/interfaces.d.ts.map +1 -0
  53. package/bin/internals/interfaces.js +3 -0
  54. package/bin/internals/interfaces.js.map +1 -0
  55. package/package.json +45 -45
  56. package/src/IMediaPlayer.ts +29 -29
  57. package/src/LiveMediaSession.ts +349 -336
  58. package/src/LiveMediaSessionCoordinator.ts +760 -678
  59. package/src/LiveMediaSessionCoordinatorSuspension.ts +39 -39
  60. package/src/MediaPlayerSynchronizer.ts +754 -714
  61. package/src/MediaSessionActionThrottler.ts +19 -19
  62. package/src/MediaSessionExtensions.ts +198 -187
  63. package/src/RepeatedActionThrottler.ts +76 -76
  64. package/src/VolumeManager.ts +200 -200
  65. package/src/index.ts +14 -14
  66. package/src/internals/GroupCoordinatorState.ts +802 -685
  67. package/src/internals/GroupPlaybackPosition.ts +235 -228
  68. package/src/internals/GroupPlaybackRate.ts +90 -0
  69. package/src/internals/GroupPlaybackTrack.ts +183 -182
  70. package/src/internals/GroupPlaybackTrackData.ts +105 -104
  71. package/src/internals/GroupTransportState.ts +179 -170
  72. package/src/internals/consts.ts +115 -102
  73. package/src/internals/errors.ts +42 -42
  74. package/src/internals/index.ts +13 -12
  75. package/src/internals/interfaces.ts +10 -0
  76. package/tsconfig.build.json +8 -8
  77. package/tsconfig.json +76 -76
  78. package/typedoc.json +10 -10
@@ -1,19 +1,19 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation. All rights reserved.
3
- * Licensed under the Microsoft Live Share SDK License.
4
- */
5
-
6
- import {
7
- ExtendedMediaSessionActionDetails,
8
- ExtendedMediaSessionActionHandler,
9
- } from "./MediaSessionExtensions";
10
-
11
- /**
12
- * Base class for action throttlers.
13
- */
14
- export abstract class MediaSessionActionThrottler {
15
- public abstract throttled(
16
- details: MediaSessionActionDetails | ExtendedMediaSessionActionDetails,
17
- handler?: MediaSessionActionHandler | ExtendedMediaSessionActionHandler
18
- ): void;
19
- }
1
+ /**
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the Microsoft Live Share SDK License.
4
+ */
5
+
6
+ import {
7
+ ExtendedMediaSessionActionDetails,
8
+ ExtendedMediaSessionActionHandler,
9
+ } from "./MediaSessionExtensions";
10
+
11
+ /**
12
+ * Base class for action throttlers.
13
+ */
14
+ export abstract class MediaSessionActionThrottler {
15
+ public abstract throttled(
16
+ details: MediaSessionActionDetails | ExtendedMediaSessionActionDetails,
17
+ handler?: MediaSessionActionHandler | ExtendedMediaSessionActionHandler
18
+ ): void;
19
+ }
@@ -1,187 +1,198 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation. All rights reserved.
3
- * Licensed under the Microsoft Live Share SDK License.
4
- */
5
-
6
- /**
7
- * Suspension object for when group synchronization is suspended for the local user.
8
- */
9
- export interface MediaSessionCoordinatorSuspension {
10
- /**
11
- * The wait point that the suspension was created with.
12
- *
13
- * @remarks
14
- * If undefined, the suspension was not created with a wait point.
15
- */
16
- waitPoint?: CoordinationWaitPoint;
17
- /**
18
- * Ends the suspension.
19
- *
20
- * @param seekTo the timestamp to seek to after the suspension ends.
21
- * If no value is provided, the local user will return to the group playback position.
22
- *
23
- * @remarks
24
- * If there are other suspensions active, all other suspensions must be ended before group synchronization can resume.
25
- */
26
- end(seekTo?: number): void;
27
- }
28
-
29
- /**
30
- * Wait point interface for a suspension.
31
- *
32
- * @remarks
33
- * Used by {@link MediaSessionCoordinatorSuspension} to schedule a suspension at a specific media position.
34
- */
35
- export interface CoordinationWaitPoint {
36
- /**
37
- * The position in milliseconds that the suspension should begin.
38
- */
39
- position: number;
40
- /**
41
- * Optional. The reason for the suspension.
42
- */
43
- reason?: string;
44
- /**
45
- * Optional. The max clients to wait for before a suspension ends.
46
- */
47
- maxClients?: number;
48
- }
49
-
50
- /**
51
- * The state of the `LiveMediaSessionCoordinator`.
52
- */
53
- export type MediaSessionCoordinatorState = "closed" | "waiting" | "joined";
54
-
55
- /**
56
- * Event types of `LiveMediaSessionCoordinator`.
57
- */
58
- export enum MediaSessionCoordinatorEvents {
59
- /**
60
- * Event emitted after the coordinator's state changed
61
- */
62
- coordinatorstatechange = "coordinatorstatechange",
63
- /**
64
- * Event emitted when an action was triggered.
65
- */
66
- triggeraction = "triggeraction",
67
- }
68
-
69
- /**
70
- * Action types supported for `LiveMediaSession`.
71
- *
72
- * @remarks
73
- * These types extend the default actions supported by the `MediaSession` specification in browsers.
74
- * These additional actions help with group synchronization.
75
- */
76
- export type ExtendedMediaSessionAction =
77
- | MediaSessionAction
78
- | "settrack"
79
- | "wait"
80
- | "catchup"
81
- | "datachange"
82
- | "blocked";
83
-
84
- /**
85
- * Extended playback state for `LiveMediaSession`.
86
- *
87
- * @remarks
88
- * These types extend the default state types supported by the `MediaSession` specification in browsers.
89
- * These additional types help with group synchronization.
90
- */
91
- export type ExtendedMediaSessionPlaybackState =
92
- | MediaSessionPlaybackState
93
- | "suspended"
94
- | "waiting"
95
- | "ended";
96
-
97
- /**
98
- * The source of the action for `LiveMediaSession`.
99
- *
100
- * @remarks
101
- * Is `user` when the action was explicitly invoked via the `LiveMediaSessionCoordinator`.
102
- * Is `system` when the action was invoked via `GroupCoordinatorState` when the local client is out of sync.
103
- */
104
- export type ExtendedMediaSessionActionSource = "user" | "system";
105
-
106
- /**
107
- * Metadata for `LiveMediaSession`.
108
- *
109
- * @remarks
110
- * These types extend the default session metadata supported by the `MediaSession` specification in browsers.
111
- * These additional types help with group synchronization.
112
- */
113
- export interface ExtendedMediaMetadata extends MediaMetadata {
114
- /**
115
- * A unique identifier for the track (e.g., UUID, URL, etc.)
116
- */
117
- trackIdentifier: string;
118
- /**
119
- * Flag indicating whether or not the stream is a live broadcast.
120
- */
121
- liveStream?: boolean;
122
- }
123
-
124
- /**
125
- * Details for emitted actions from `LiveMediaSession`.
126
- *
127
- * @remarks
128
- * These types extend the default event details supported by the `MediaSession` specification in browsers.
129
- * These additional types help with group synchronization.
130
- */
131
- export interface ExtendedMediaSessionActionDetails {
132
- /**
133
- * The action type of this event.
134
- */
135
- action: ExtendedMediaSessionAction;
136
- /**
137
- * The source of the action.
138
- *
139
- * @remarks
140
- * Is `user` when the action was explicitly invoked via the `LiveMediaSessionCoordinator`.
141
- * Is `system` when the action was invoked via `GroupCoordinatorState` when the local client is out of sync.
142
- */
143
- source: ExtendedMediaSessionActionSource;
144
- /**
145
- * Unique identifier of the client that triggered this action.
146
- */
147
- clientId: string;
148
- /**
149
- * Flag indicating whether or not the action was triggered by the local client.
150
- */
151
- local: boolean;
152
- /**
153
- * Flag indicating whether or not this was a fast seek action.
154
- */
155
- fastSeek?: boolean | null;
156
- /**
157
- * The offset of the seek.
158
- */
159
- seekOffset?: number | null;
160
- /**
161
- * Timestamp of the seek event.
162
- */
163
- seekTime?: number | null;
164
- /**
165
- * Updated session metadata that was changed via this action.
166
- */
167
- metadata?: ExtendedMediaMetadata | null;
168
- /**
169
- * Suspension associated with this action.
170
- */
171
- suspension?: MediaSessionCoordinatorSuspension | null;
172
- /**
173
- * Custom data associated with this action.
174
- */
175
- data?: object | null;
176
- /**
177
- * Action type that was blocked.
178
- */
179
- blocked?: ExtendedMediaSessionAction | null;
180
- }
181
-
182
- /**
183
- * Handler for `LiveMediaSession` actions.
184
- */
185
- export interface ExtendedMediaSessionActionHandler {
186
- (details: ExtendedMediaSessionActionDetails): void;
187
- }
1
+ /**
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the Microsoft Live Share SDK License.
4
+ */
5
+
6
+ /**
7
+ * Suspension object for when group synchronization is suspended for the local user.
8
+ */
9
+ export interface MediaSessionCoordinatorSuspension {
10
+ /**
11
+ * The wait point that the suspension was created with.
12
+ *
13
+ * @remarks
14
+ * If undefined, the suspension was not created with a wait point.
15
+ */
16
+ waitPoint?: CoordinationWaitPoint;
17
+ /**
18
+ * Ends the suspension.
19
+ *
20
+ * @param seekTo the timestamp to seek to after the suspension ends.
21
+ * If no value is provided, the local user will return to the group playback position.
22
+ *
23
+ * @remarks
24
+ * If there are other suspensions active, all other suspensions must be ended before group synchronization can resume.
25
+ */
26
+ end(seekTo?: number): void;
27
+ }
28
+
29
+ /**
30
+ * Wait point interface for a suspension.
31
+ *
32
+ * @remarks
33
+ * Used by {@link MediaSessionCoordinatorSuspension} to schedule a suspension at a specific media position.
34
+ */
35
+ export interface CoordinationWaitPoint {
36
+ /**
37
+ * The position in milliseconds that the suspension should begin.
38
+ */
39
+ position: number;
40
+ /**
41
+ * Optional. The reason for the suspension.
42
+ */
43
+ reason?: string;
44
+ /**
45
+ * Optional. The max clients to wait for before a suspension ends.
46
+ */
47
+ maxClients?: number;
48
+ }
49
+
50
+ /**
51
+ * Event types of `LiveMediaSessionCoordinator`.
52
+ */
53
+ export enum MediaSessionCoordinatorEvents {
54
+ /**
55
+ * Event emitted after the coordinator's state changed
56
+ */
57
+ coordinatorstatechange = "coordinatorstatechange",
58
+ /**
59
+ * Event emitted when an action was triggered.
60
+ */
61
+ triggeraction = "triggeraction",
62
+ /**
63
+ * Event emitted when an action would have been triggered, but a suspension is active for the local client.
64
+ */
65
+ triggeractionignored = "triggeractionignored",
66
+ }
67
+
68
+ /**
69
+ * Action types supported for `LiveMediaSession`.
70
+ *
71
+ * @remarks
72
+ * These types extend the default actions supported by the `MediaSession` specification in browsers.
73
+ * These additional actions help with group synchronization.
74
+ */
75
+ export type ExtendedMediaSessionAction =
76
+ | MediaSessionAction
77
+ | "settrack"
78
+ | "ratechange"
79
+ | "wait"
80
+ | "catchup"
81
+ | "datachange"
82
+ | "blocked";
83
+
84
+ /**
85
+ * Extended playback state for `LiveMediaSession`.
86
+ *
87
+ * @remarks
88
+ * These types extend the default state types supported by the `MediaSession` specification in browsers.
89
+ * These additional types help with group synchronization.
90
+ */
91
+ export type ExtendedMediaSessionPlaybackState =
92
+ | MediaSessionPlaybackState
93
+ | "suspended"
94
+ | "waiting"
95
+ | "ended";
96
+
97
+ /**
98
+ * The source of the action for `LiveMediaSession`.
99
+ *
100
+ * @remarks
101
+ * Is `user` when the action was explicitly invoked via the `LiveMediaSessionCoordinator`.
102
+ * Is `system` when the action was invoked via `GroupCoordinatorState` when the local client is out of sync.
103
+ */
104
+ export type ExtendedMediaSessionActionSource = "user" | "system";
105
+
106
+ /**
107
+ * Metadata for `LiveMediaSession`.
108
+ *
109
+ * @remarks
110
+ * These types extend the default session metadata supported by the `MediaSession` specification in browsers.
111
+ * These additional types help with group synchronization.
112
+ */
113
+ export interface ExtendedMediaMetadata extends MediaMetadata {
114
+ /**
115
+ * A unique identifier for the track (e.g., UUID, URL, etc.)
116
+ */
117
+ trackIdentifier: string;
118
+ /**
119
+ * Flag indicating whether or not the stream is a live broadcast.
120
+ */
121
+ liveStream?: boolean;
122
+ }
123
+
124
+ /**
125
+ * The reasons why an action is ignored.
126
+ *
127
+ * @remarks
128
+ * Is `localusersuspended` when the action updated the group state, but ignored locally due to the local client being suspended.
129
+
130
+ */
131
+ export type ExtendedMediaSessionActionIgnore = "localusersuspended";
132
+
133
+ /**
134
+ * Details for emitted actions from `LiveMediaSession`.
135
+ *
136
+ * @remarks
137
+ * These types extend the default event details supported by the `MediaSession` specification in browsers.
138
+ * These additional types help with group synchronization.
139
+ */
140
+ export interface ExtendedMediaSessionActionDetails {
141
+ /**
142
+ * The action type of this event.
143
+ */
144
+ action: ExtendedMediaSessionAction;
145
+ /**
146
+ * The source of the action.
147
+ *
148
+ * @remarks
149
+ * Is `user` when the action was explicitly invoked via the `LiveMediaSessionCoordinator`.
150
+ * Is `system` when the action was invoked via `GroupCoordinatorState` when the local client is out of sync.
151
+ */
152
+ source: ExtendedMediaSessionActionSource;
153
+ /**
154
+ * Unique identifier of the client that triggered this action.
155
+ */
156
+ clientId: string;
157
+ /**
158
+ * Flag indicating whether or not the action was triggered by the local client.
159
+ */
160
+ local: boolean;
161
+ /**
162
+ * Flag indicating whether or not this was a fast seek action.
163
+ */
164
+ fastSeek?: boolean | null;
165
+ /**
166
+ * The offset of the seek.
167
+ */
168
+ seekOffset?: number | null;
169
+ /**
170
+ * Timestamp of the seek event.
171
+ */
172
+ seekTime?: number | null;
173
+ /**
174
+ * Updated session metadata that was changed via this action.
175
+ */
176
+ metadata?: ExtendedMediaMetadata | null;
177
+ /**
178
+ * Suspension associated with this action.
179
+ */
180
+ suspension?: MediaSessionCoordinatorSuspension | null;
181
+ /**
182
+ * Custom data associated with this action.
183
+ */
184
+ data?: object | null;
185
+ /**
186
+ * Reason an action is ignored by the local client, undefined if not ignored.
187
+ */
188
+ ignoreReason?: ExtendedMediaSessionActionIgnore | null;
189
+
190
+ playbackRate?: number | null;
191
+ }
192
+
193
+ /**
194
+ * Handler for `LiveMediaSession` actions.
195
+ */
196
+ export interface ExtendedMediaSessionActionHandler {
197
+ (details: ExtendedMediaSessionActionDetails): void;
198
+ }
@@ -1,76 +1,76 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation. All rights reserved.
3
- * Licensed under the Microsoft Live Share SDK License.
4
- */
5
-
6
- import { TimeInterval } from "@microsoft/live-share";
7
- import { MediaSessionActionThrottler } from "./MediaSessionActionThrottler";
8
- import {
9
- ExtendedMediaSessionActionDetails,
10
- ExtendedMediaSessionAction,
11
- } from "./MediaSessionExtensions";
12
-
13
- /**
14
- * A Throttler that passes through all actions but will only let an action be repeated once every 2 seconds.
15
- */
16
- export class RepeatedActionThrottler extends MediaSessionActionThrottler {
17
- private static FILTERED_ACTIONS: ExtendedMediaSessionAction[] = [
18
- "play",
19
- "pause",
20
- "seekto",
21
- "catchup",
22
- ];
23
- private _repeatInterval = new TimeInterval(2000);
24
- private _lastActionSentTime?: number;
25
- private _lastActionSent?: string;
26
-
27
- public get repeatInterval(): number {
28
- return this._repeatInterval.seconds;
29
- }
30
-
31
- public set repeatInterval(value: number) {
32
- this._repeatInterval.seconds = value;
33
- }
34
-
35
- public throttled(
36
- details: MediaSessionActionDetails | ExtendedMediaSessionActionDetails,
37
- handler?: MediaSessionActionHandler
38
- ): void {
39
- if (handler) {
40
- if (
41
- RepeatedActionThrottler.FILTERED_ACTIONS.indexOf(
42
- details.action
43
- ) >= 0
44
- ) {
45
- const changeKey = this.getChangeKey(details);
46
- const now = new Date().getTime();
47
- if (this._lastActionSent && changeKey == this._lastActionSent) {
48
- if (
49
- now - this._lastActionSentTime! >=
50
- this._repeatInterval.milliseconds
51
- ) {
52
- handler(details as MediaSessionActionDetails);
53
- this._lastActionSentTime = now;
54
- }
55
- } else {
56
- handler(details as MediaSessionActionDetails);
57
- this._lastActionSent = changeKey;
58
- this._lastActionSentTime = now;
59
- }
60
- } else {
61
- handler(details as MediaSessionActionDetails);
62
- }
63
- }
64
- }
65
-
66
- private getChangeKey(
67
- details: MediaSessionActionDetails | ExtendedMediaSessionActionDetails
68
- ): string {
69
- switch (details.action) {
70
- case "seekto":
71
- return `seekto:${details.seekTime}`;
72
- default:
73
- return details.action;
74
- }
75
- }
76
- }
1
+ /**
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the Microsoft Live Share SDK License.
4
+ */
5
+
6
+ import { TimeInterval } from "@microsoft/live-share";
7
+ import { MediaSessionActionThrottler } from "./MediaSessionActionThrottler";
8
+ import {
9
+ ExtendedMediaSessionActionDetails,
10
+ ExtendedMediaSessionAction,
11
+ } from "./MediaSessionExtensions";
12
+
13
+ /**
14
+ * A Throttler that passes through all actions but will only let an action be repeated once every 2 seconds.
15
+ */
16
+ export class RepeatedActionThrottler extends MediaSessionActionThrottler {
17
+ private static FILTERED_ACTIONS: ExtendedMediaSessionAction[] = [
18
+ "play",
19
+ "pause",
20
+ "seekto",
21
+ "catchup",
22
+ ];
23
+ private _repeatInterval = new TimeInterval(2000);
24
+ private _lastActionSentTime?: number;
25
+ private _lastActionSent?: string;
26
+
27
+ public get repeatInterval(): number {
28
+ return this._repeatInterval.seconds;
29
+ }
30
+
31
+ public set repeatInterval(value: number) {
32
+ this._repeatInterval.seconds = value;
33
+ }
34
+
35
+ public throttled(
36
+ details: MediaSessionActionDetails | ExtendedMediaSessionActionDetails,
37
+ handler?: MediaSessionActionHandler
38
+ ): void {
39
+ if (handler) {
40
+ if (
41
+ RepeatedActionThrottler.FILTERED_ACTIONS.indexOf(
42
+ details.action
43
+ ) >= 0
44
+ ) {
45
+ const changeKey = this.getChangeKey(details);
46
+ const now = new Date().getTime();
47
+ if (this._lastActionSent && changeKey == this._lastActionSent) {
48
+ if (
49
+ now - this._lastActionSentTime! >=
50
+ this._repeatInterval.milliseconds
51
+ ) {
52
+ handler(details as MediaSessionActionDetails);
53
+ this._lastActionSentTime = now;
54
+ }
55
+ } else {
56
+ handler(details as MediaSessionActionDetails);
57
+ this._lastActionSent = changeKey;
58
+ this._lastActionSentTime = now;
59
+ }
60
+ } else {
61
+ handler(details as MediaSessionActionDetails);
62
+ }
63
+ }
64
+ }
65
+
66
+ private getChangeKey(
67
+ details: MediaSessionActionDetails | ExtendedMediaSessionActionDetails
68
+ ): string {
69
+ switch (details.action) {
70
+ case "seekto":
71
+ return `seekto:${details.seekTime}`;
72
+ default:
73
+ return details.action;
74
+ }
75
+ }
76
+ }