@livedesk/hub 0.1.41 → 0.1.42

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.
package/src/lzo1x.js CHANGED
@@ -1,109 +1,109 @@
1
- function requireInput(input, index, length) {
2
- if (length < 0 || index < 0 || index + length > input.length) {
3
- throw new Error('Invalid LZO1X block: input overrun.');
4
- }
5
- }
6
-
7
- function readExtendedLength(input, state, basis) {
8
- let zeroBytes = 0;
9
- while (true) {
10
- requireInput(input, state.inputIndex, 1);
11
- if (input[state.inputIndex] !== 0) break;
12
- zeroBytes += 1;
13
- state.inputIndex += 1;
14
- }
15
- const length = zeroBytes * 255 + basis + input[state.inputIndex];
16
- state.inputIndex += 1;
17
- return length;
18
- }
19
-
20
- function copyLiteral(input, state, output, length) {
21
- requireInput(input, state.inputIndex, length);
22
- if (state.outputIndex + length > output.length) {
23
- throw new Error('Invalid LZO1X block: output overrun.');
24
- }
25
- output.set(input.subarray(state.inputIndex, state.inputIndex + length), state.outputIndex);
26
- state.inputIndex += length;
27
- state.outputIndex += length;
28
- }
29
-
30
- export function decodeLzo1xBlock(input, outputLength) {
31
- if (!(input instanceof Uint8Array) || input.length < 3 || outputLength < 0) {
32
- throw new Error('Invalid LZO1X block.');
33
- }
34
- const output = new Uint8Array(outputLength);
35
- const cursor = { inputIndex: 0, outputIndex: 0 };
36
- let state = 0;
37
- let matchLength = 0;
38
- if (input[cursor.inputIndex] >= 22) {
39
- const length = input[cursor.inputIndex] - 17;
40
- cursor.inputIndex += 1;
41
- copyLiteral(input, cursor, output, length);
42
- state = 4;
43
- } else if (input[cursor.inputIndex] >= 18) {
44
- state = input[cursor.inputIndex] - 17;
45
- cursor.inputIndex += 1;
46
- copyLiteral(input, cursor, output, state);
47
- }
48
- while (true) {
49
- requireInput(input, cursor.inputIndex, 1);
50
- const instruction = input[cursor.inputIndex++];
51
- let nextState;
52
- let matchIndex;
53
- if ((instruction & 0xc0) !== 0) {
54
- requireInput(input, cursor.inputIndex, 1);
55
- matchIndex = cursor.outputIndex - ((input[cursor.inputIndex] << 3) + ((instruction >> 2) & 7) + 1);
56
- cursor.inputIndex += 1;
57
- matchLength = (instruction >> 5) + 1;
58
- nextState = instruction & 3;
59
- } else if ((instruction & 0x20) !== 0) {
60
- matchLength = (instruction & 0x1f) + 2;
61
- if (matchLength === 2) matchLength += readExtendedLength(input, cursor, 31);
62
- requireInput(input, cursor.inputIndex, 2);
63
- const encoded = input[cursor.inputIndex] | (input[cursor.inputIndex + 1] << 8);
64
- cursor.inputIndex += 2;
65
- matchIndex = cursor.outputIndex - ((encoded >> 2) + 1);
66
- nextState = encoded & 3;
67
- } else if ((instruction & 0x10) !== 0) {
68
- matchLength = (instruction & 7) + 2;
69
- if (matchLength === 2) matchLength += readExtendedLength(input, cursor, 7);
70
- requireInput(input, cursor.inputIndex, 2);
71
- const encoded = input[cursor.inputIndex] | (input[cursor.inputIndex + 1] << 8);
72
- cursor.inputIndex += 2;
73
- matchIndex = cursor.outputIndex - (((instruction & 8) << 11) + (encoded >> 2));
74
- nextState = encoded & 3;
75
- if (matchIndex === cursor.outputIndex) break;
76
- matchIndex -= 0x4000;
77
- } else if (state === 0) {
78
- let length = instruction + 3;
79
- if (length === 3) length += readExtendedLength(input, cursor, 15);
80
- copyLiteral(input, cursor, output, length);
81
- state = 4;
82
- continue;
83
- } else if (state !== 4) {
84
- requireInput(input, cursor.inputIndex, 1);
85
- nextState = instruction & 3;
86
- matchIndex = cursor.outputIndex - ((instruction >> 2) + (input[cursor.inputIndex] << 2) + 1);
87
- cursor.inputIndex += 1;
88
- matchLength = 2;
89
- } else {
90
- requireInput(input, cursor.inputIndex, 1);
91
- nextState = instruction & 3;
92
- matchIndex = cursor.outputIndex - ((instruction >> 2) + (input[cursor.inputIndex] << 2) + 2049);
93
- cursor.inputIndex += 1;
94
- matchLength = 3;
95
- }
96
- if (matchIndex < 0 || cursor.outputIndex + matchLength + nextState > output.length) {
97
- throw new Error('Invalid LZO1X block: lookbehind overrun.');
98
- }
99
- for (let index = 0; index < matchLength; index += 1) {
100
- output[cursor.outputIndex++] = output[matchIndex++];
101
- }
102
- state = nextState;
103
- copyLiteral(input, cursor, output, nextState);
104
- }
105
- if (matchLength !== 3 || cursor.inputIndex !== input.length || cursor.outputIndex !== output.length) {
106
- throw new Error('Invalid LZO1X block: incomplete output.');
107
- }
108
- return output;
109
- }
1
+ function requireInput(input, index, length) {
2
+ if (length < 0 || index < 0 || index + length > input.length) {
3
+ throw new Error('Invalid LZO1X block: input overrun.');
4
+ }
5
+ }
6
+
7
+ function readExtendedLength(input, state, basis) {
8
+ let zeroBytes = 0;
9
+ while (true) {
10
+ requireInput(input, state.inputIndex, 1);
11
+ if (input[state.inputIndex] !== 0) break;
12
+ zeroBytes += 1;
13
+ state.inputIndex += 1;
14
+ }
15
+ const length = zeroBytes * 255 + basis + input[state.inputIndex];
16
+ state.inputIndex += 1;
17
+ return length;
18
+ }
19
+
20
+ function copyLiteral(input, state, output, length) {
21
+ requireInput(input, state.inputIndex, length);
22
+ if (state.outputIndex + length > output.length) {
23
+ throw new Error('Invalid LZO1X block: output overrun.');
24
+ }
25
+ output.set(input.subarray(state.inputIndex, state.inputIndex + length), state.outputIndex);
26
+ state.inputIndex += length;
27
+ state.outputIndex += length;
28
+ }
29
+
30
+ export function decodeLzo1xBlock(input, outputLength) {
31
+ if (!(input instanceof Uint8Array) || input.length < 3 || outputLength < 0) {
32
+ throw new Error('Invalid LZO1X block.');
33
+ }
34
+ const output = new Uint8Array(outputLength);
35
+ const cursor = { inputIndex: 0, outputIndex: 0 };
36
+ let state = 0;
37
+ let matchLength = 0;
38
+ if (input[cursor.inputIndex] >= 22) {
39
+ const length = input[cursor.inputIndex] - 17;
40
+ cursor.inputIndex += 1;
41
+ copyLiteral(input, cursor, output, length);
42
+ state = 4;
43
+ } else if (input[cursor.inputIndex] >= 18) {
44
+ state = input[cursor.inputIndex] - 17;
45
+ cursor.inputIndex += 1;
46
+ copyLiteral(input, cursor, output, state);
47
+ }
48
+ while (true) {
49
+ requireInput(input, cursor.inputIndex, 1);
50
+ const instruction = input[cursor.inputIndex++];
51
+ let nextState;
52
+ let matchIndex;
53
+ if ((instruction & 0xc0) !== 0) {
54
+ requireInput(input, cursor.inputIndex, 1);
55
+ matchIndex = cursor.outputIndex - ((input[cursor.inputIndex] << 3) + ((instruction >> 2) & 7) + 1);
56
+ cursor.inputIndex += 1;
57
+ matchLength = (instruction >> 5) + 1;
58
+ nextState = instruction & 3;
59
+ } else if ((instruction & 0x20) !== 0) {
60
+ matchLength = (instruction & 0x1f) + 2;
61
+ if (matchLength === 2) matchLength += readExtendedLength(input, cursor, 31);
62
+ requireInput(input, cursor.inputIndex, 2);
63
+ const encoded = input[cursor.inputIndex] | (input[cursor.inputIndex + 1] << 8);
64
+ cursor.inputIndex += 2;
65
+ matchIndex = cursor.outputIndex - ((encoded >> 2) + 1);
66
+ nextState = encoded & 3;
67
+ } else if ((instruction & 0x10) !== 0) {
68
+ matchLength = (instruction & 7) + 2;
69
+ if (matchLength === 2) matchLength += readExtendedLength(input, cursor, 7);
70
+ requireInput(input, cursor.inputIndex, 2);
71
+ const encoded = input[cursor.inputIndex] | (input[cursor.inputIndex + 1] << 8);
72
+ cursor.inputIndex += 2;
73
+ matchIndex = cursor.outputIndex - (((instruction & 8) << 11) + (encoded >> 2));
74
+ nextState = encoded & 3;
75
+ if (matchIndex === cursor.outputIndex) break;
76
+ matchIndex -= 0x4000;
77
+ } else if (state === 0) {
78
+ let length = instruction + 3;
79
+ if (length === 3) length += readExtendedLength(input, cursor, 15);
80
+ copyLiteral(input, cursor, output, length);
81
+ state = 4;
82
+ continue;
83
+ } else if (state !== 4) {
84
+ requireInput(input, cursor.inputIndex, 1);
85
+ nextState = instruction & 3;
86
+ matchIndex = cursor.outputIndex - ((instruction >> 2) + (input[cursor.inputIndex] << 2) + 1);
87
+ cursor.inputIndex += 1;
88
+ matchLength = 2;
89
+ } else {
90
+ requireInput(input, cursor.inputIndex, 1);
91
+ nextState = instruction & 3;
92
+ matchIndex = cursor.outputIndex - ((instruction >> 2) + (input[cursor.inputIndex] << 2) + 2049);
93
+ cursor.inputIndex += 1;
94
+ matchLength = 3;
95
+ }
96
+ if (matchIndex < 0 || cursor.outputIndex + matchLength + nextState > output.length) {
97
+ throw new Error('Invalid LZO1X block: lookbehind overrun.');
98
+ }
99
+ for (let index = 0; index < matchLength; index += 1) {
100
+ output[cursor.outputIndex++] = output[matchIndex++];
101
+ }
102
+ state = nextState;
103
+ copyLiteral(input, cursor, output, nextState);
104
+ }
105
+ if (matchLength !== 3 || cursor.inputIndex !== input.length || cursor.outputIndex !== output.length) {
106
+ throw new Error('Invalid LZO1X block: incomplete output.');
107
+ }
108
+ return output;
109
+ }
@@ -1,137 +1,137 @@
1
- import { Mode4AtlasSession } from './mode4-atlas.js';
2
- import { randomUUID } from 'node:crypto';
3
-
4
- export function buildMode4AtlasSessionKey(config = {}) {
5
- return JSON.stringify({
6
- deviceIds: Array.isArray(config.deviceIds) ? config.deviceIds : [],
7
- width: Number(config.width || 1920),
8
- height: Number(config.height || 1080),
9
- tileWidth: Number(config.tileWidth || 320),
10
- tileHeight: Number(config.tileHeight || 180),
11
- fps: Number(config.fps || 20)
12
- });
13
- }
14
-
15
- export function planMode4AtlasInputTransitions(previous = {}, next = {}) {
16
- const previousIds = new Set(Array.isArray(previous.inputDeviceIds) ? previous.inputDeviceIds : []);
17
- const nextIds = new Set(Array.isArray(next.inputDeviceIds) ? next.inputDeviceIds : []);
18
- const previousMonitorSelections = previous.monitorSelections || {};
19
- const nextMonitorSelections = next.monitorSelections || {};
20
- const captureSizeChanged = previousIds.size > 0
21
- && (Number(previous.tileWidth || 0) !== Number(next.tileWidth || 0)
22
- || Number(previous.tileHeight || 0) !== Number(next.tileHeight || 0));
23
- const captureFpsChanged = previousIds.size > 0
24
- && Number(previous.inputFps || 0) !== Number(next.inputFps || 0);
25
- const stop = [...previousIds].filter(deviceId => !nextIds.has(deviceId));
26
- const start = [];
27
- const restart = [];
28
- const unchanged = [];
29
-
30
- for (const deviceId of nextIds) {
31
- if (!previousIds.has(deviceId)) {
32
- start.push(deviceId);
33
- continue;
34
- }
35
- const previousMonitorIndex = Number(previousMonitorSelections[deviceId] || 0);
36
- const nextMonitorIndex = Number(nextMonitorSelections[deviceId] || 0);
37
- if (captureSizeChanged || captureFpsChanged || previousMonitorIndex !== nextMonitorIndex) {
38
- restart.push(deviceId);
39
- } else {
40
- unchanged.push(deviceId);
41
- }
42
- }
43
-
44
- return { stop, start, restart, unchanged };
45
- }
46
-
47
- export class Mode4AtlasPool {
48
- constructor(options = {}) {
49
- this.createSession = typeof options.createSession === 'function'
50
- ? options.createSession
51
- : callbacks => new Mode4AtlasSession(callbacks);
52
- this.onStatus = typeof options.onStatus === 'function' ? options.onStatus : () => {};
53
- this.entries = new Map();
54
- this.closingByKey = new Map();
55
- }
56
-
57
- acquire(config, callbacks = {}) {
58
- const key = buildMode4AtlasSessionKey(config);
59
- let entry = this.entries.get(key);
60
- if (!entry) {
61
- entry = {
62
- key,
63
- clients: new Map(),
64
- latestLayout: null,
65
- session: null,
66
- streamId: `mode4-atlas-shared-${randomUUID()}`
67
- };
68
- entry.session = this.createSession({
69
- startAfter: this.closingByKey.get(key) || null,
70
- onFrame: output => {
71
- for (const listener of entry.clients.values()) listener.onFrame?.(output);
72
- },
73
- onLayout: layout => {
74
- entry.latestLayout = layout;
75
- for (const listener of entry.clients.values()) listener.onLayout?.(layout);
76
- },
77
- onStatus: status => {
78
- this.onStatus(status);
79
- for (const listener of entry.clients.values()) listener.onStatus?.(status);
80
- }
81
- });
82
- entry.session.configure(config);
83
- this.entries.set(key, entry);
84
- }
85
-
86
- const clientId = Symbol(key);
87
- entry.clients.set(clientId, callbacks);
88
- if (entry.latestLayout) {
89
- queueMicrotask(() => {
90
- if (entry.clients.has(clientId)) callbacks.onLayout?.(entry.latestLayout);
91
- });
92
- }
93
-
94
- let released = false;
95
- return {
96
- key,
97
- session: entry.session,
98
- streamId: entry.streamId,
99
- release: () => {
100
- if (released) return;
101
- released = true;
102
- entry.clients.delete(clientId);
103
- if (entry.clients.size === 0) {
104
- this.entries.delete(key);
105
- const closePromise = Promise.resolve(entry.session.close());
106
- this.closingByKey.set(key, closePromise);
107
- const retireCloseOwner = () => {
108
- if (this.closingByKey.get(key) === closePromise) {
109
- this.closingByKey.delete(key);
110
- }
111
- };
112
- void closePromise.then(retireCloseOwner, retireCloseOwner);
113
- }
114
- }
115
- };
116
- }
117
-
118
- ingest(frameEvent) {
119
- for (const entry of this.entries.values()) {
120
- entry.session.ingest(frameEvent);
121
- }
122
- }
123
-
124
- get size() {
125
- return this.entries.size;
126
- }
127
-
128
- async close() {
129
- const closePromises = [...this.closingByKey.values()];
130
- for (const entry of this.entries.values()) {
131
- closePromises.push(Promise.resolve(entry.session.close()));
132
- }
133
- this.entries.clear();
134
- this.closingByKey.clear();
135
- await Promise.allSettled(closePromises);
136
- }
137
- }
1
+ import { Mode4AtlasSession } from './mode4-atlas.js';
2
+ import { randomUUID } from 'node:crypto';
3
+
4
+ export function buildMode4AtlasSessionKey(config = {}) {
5
+ return JSON.stringify({
6
+ deviceIds: Array.isArray(config.deviceIds) ? config.deviceIds : [],
7
+ width: Number(config.width || 1920),
8
+ height: Number(config.height || 1080),
9
+ tileWidth: Number(config.tileWidth || 320),
10
+ tileHeight: Number(config.tileHeight || 180),
11
+ fps: Number(config.fps || 20)
12
+ });
13
+ }
14
+
15
+ export function planMode4AtlasInputTransitions(previous = {}, next = {}) {
16
+ const previousIds = new Set(Array.isArray(previous.inputDeviceIds) ? previous.inputDeviceIds : []);
17
+ const nextIds = new Set(Array.isArray(next.inputDeviceIds) ? next.inputDeviceIds : []);
18
+ const previousMonitorSelections = previous.monitorSelections || {};
19
+ const nextMonitorSelections = next.monitorSelections || {};
20
+ const captureSizeChanged = previousIds.size > 0
21
+ && (Number(previous.tileWidth || 0) !== Number(next.tileWidth || 0)
22
+ || Number(previous.tileHeight || 0) !== Number(next.tileHeight || 0));
23
+ const captureFpsChanged = previousIds.size > 0
24
+ && Number(previous.inputFps || 0) !== Number(next.inputFps || 0);
25
+ const stop = [...previousIds].filter(deviceId => !nextIds.has(deviceId));
26
+ const start = [];
27
+ const restart = [];
28
+ const unchanged = [];
29
+
30
+ for (const deviceId of nextIds) {
31
+ if (!previousIds.has(deviceId)) {
32
+ start.push(deviceId);
33
+ continue;
34
+ }
35
+ const previousMonitorIndex = Number(previousMonitorSelections[deviceId] || 0);
36
+ const nextMonitorIndex = Number(nextMonitorSelections[deviceId] || 0);
37
+ if (captureSizeChanged || captureFpsChanged || previousMonitorIndex !== nextMonitorIndex) {
38
+ restart.push(deviceId);
39
+ } else {
40
+ unchanged.push(deviceId);
41
+ }
42
+ }
43
+
44
+ return { stop, start, restart, unchanged };
45
+ }
46
+
47
+ export class Mode4AtlasPool {
48
+ constructor(options = {}) {
49
+ this.createSession = typeof options.createSession === 'function'
50
+ ? options.createSession
51
+ : callbacks => new Mode4AtlasSession(callbacks);
52
+ this.onStatus = typeof options.onStatus === 'function' ? options.onStatus : () => {};
53
+ this.entries = new Map();
54
+ this.closingByKey = new Map();
55
+ }
56
+
57
+ acquire(config, callbacks = {}) {
58
+ const key = buildMode4AtlasSessionKey(config);
59
+ let entry = this.entries.get(key);
60
+ if (!entry) {
61
+ entry = {
62
+ key,
63
+ clients: new Map(),
64
+ latestLayout: null,
65
+ session: null,
66
+ streamId: `mode4-atlas-shared-${randomUUID()}`
67
+ };
68
+ entry.session = this.createSession({
69
+ startAfter: this.closingByKey.get(key) || null,
70
+ onFrame: output => {
71
+ for (const listener of entry.clients.values()) listener.onFrame?.(output);
72
+ },
73
+ onLayout: layout => {
74
+ entry.latestLayout = layout;
75
+ for (const listener of entry.clients.values()) listener.onLayout?.(layout);
76
+ },
77
+ onStatus: status => {
78
+ this.onStatus(status);
79
+ for (const listener of entry.clients.values()) listener.onStatus?.(status);
80
+ }
81
+ });
82
+ entry.session.configure(config);
83
+ this.entries.set(key, entry);
84
+ }
85
+
86
+ const clientId = Symbol(key);
87
+ entry.clients.set(clientId, callbacks);
88
+ if (entry.latestLayout) {
89
+ queueMicrotask(() => {
90
+ if (entry.clients.has(clientId)) callbacks.onLayout?.(entry.latestLayout);
91
+ });
92
+ }
93
+
94
+ let released = false;
95
+ return {
96
+ key,
97
+ session: entry.session,
98
+ streamId: entry.streamId,
99
+ release: () => {
100
+ if (released) return;
101
+ released = true;
102
+ entry.clients.delete(clientId);
103
+ if (entry.clients.size === 0) {
104
+ this.entries.delete(key);
105
+ const closePromise = Promise.resolve(entry.session.close());
106
+ this.closingByKey.set(key, closePromise);
107
+ const retireCloseOwner = () => {
108
+ if (this.closingByKey.get(key) === closePromise) {
109
+ this.closingByKey.delete(key);
110
+ }
111
+ };
112
+ void closePromise.then(retireCloseOwner, retireCloseOwner);
113
+ }
114
+ }
115
+ };
116
+ }
117
+
118
+ ingest(frameEvent) {
119
+ for (const entry of this.entries.values()) {
120
+ entry.session.ingest(frameEvent);
121
+ }
122
+ }
123
+
124
+ get size() {
125
+ return this.entries.size;
126
+ }
127
+
128
+ async close() {
129
+ const closePromises = [...this.closingByKey.values()];
130
+ for (const entry of this.entries.values()) {
131
+ closePromises.push(Promise.resolve(entry.session.close()));
132
+ }
133
+ this.entries.clear();
134
+ this.closingByKey.clear();
135
+ await Promise.allSettled(closePromises);
136
+ }
137
+ }
@@ -1,32 +1,32 @@
1
- const TILE_PRESETS = Object.freeze([
2
- Object.freeze({ width: 320, height: 180 }),
3
- Object.freeze({ width: 480, height: 270 }),
4
- Object.freeze({ width: 640, height: 360 })
5
- ]);
6
-
7
- export function resolveMode4AtlasTileSize(options = {}) {
8
- const deviceCount = Math.max(1, Math.floor(Number(options.deviceCount) || 1));
9
- const atlasWidth = Math.max(640, Math.floor(Number(options.atlasWidth) || 1920));
10
- const atlasHeight = Math.max(360, Math.floor(Number(options.atlasHeight) || 1080));
11
- const requestedWidth = Number(options.requestedWidth) || 320;
12
- const requestedHeight = Number(options.requestedHeight) || 180;
13
- const columns = Math.max(1, Math.ceil(Math.sqrt(deviceCount)));
14
- const rows = Math.max(1, Math.ceil(deviceCount / columns));
15
- const requestedPresetIndex = requestedWidth >= 640 || requestedHeight >= 360
16
- ? 2
17
- : requestedWidth >= 480 || requestedHeight >= 270
18
- ? 1
19
- : 0;
20
- const supportedCandidates = TILE_PRESETS.slice(0, requestedPresetIndex + 1).reverse();
21
- const selected = supportedCandidates.find(candidate => (
22
- candidate.width * columns <= atlasWidth
23
- && candidate.height * rows <= atlasHeight
24
- )) || TILE_PRESETS[0];
25
-
26
- return {
27
- width: selected.width,
28
- height: selected.height,
29
- columns,
30
- rows
31
- };
32
- }
1
+ const TILE_PRESETS = Object.freeze([
2
+ Object.freeze({ width: 320, height: 180 }),
3
+ Object.freeze({ width: 480, height: 270 }),
4
+ Object.freeze({ width: 640, height: 360 })
5
+ ]);
6
+
7
+ export function resolveMode4AtlasTileSize(options = {}) {
8
+ const deviceCount = Math.max(1, Math.floor(Number(options.deviceCount) || 1));
9
+ const atlasWidth = Math.max(640, Math.floor(Number(options.atlasWidth) || 1920));
10
+ const atlasHeight = Math.max(360, Math.floor(Number(options.atlasHeight) || 1080));
11
+ const requestedWidth = Number(options.requestedWidth) || 320;
12
+ const requestedHeight = Number(options.requestedHeight) || 180;
13
+ const columns = Math.max(1, Math.ceil(Math.sqrt(deviceCount)));
14
+ const rows = Math.max(1, Math.ceil(deviceCount / columns));
15
+ const requestedPresetIndex = requestedWidth >= 640 || requestedHeight >= 360
16
+ ? 2
17
+ : requestedWidth >= 480 || requestedHeight >= 270
18
+ ? 1
19
+ : 0;
20
+ const supportedCandidates = TILE_PRESETS.slice(0, requestedPresetIndex + 1).reverse();
21
+ const selected = supportedCandidates.find(candidate => (
22
+ candidate.width * columns <= atlasWidth
23
+ && candidate.height * rows <= atlasHeight
24
+ )) || TILE_PRESETS[0];
25
+
26
+ return {
27
+ width: selected.width,
28
+ height: selected.height,
29
+ columns,
30
+ rows
31
+ };
32
+ }