@lox-audio-server/sonos 0.1.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.
- package/README.md +57 -0
- package/dist/esm/index.js +27 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/sonos/api/namespace.js +38 -0
- package/dist/esm/sonos/api/namespaces/audioClip.js +25 -0
- package/dist/esm/sonos/api/namespaces/groupVolume.js +25 -0
- package/dist/esm/sonos/api/namespaces/groups.js +31 -0
- package/dist/esm/sonos/api/namespaces/homeTheater.js +13 -0
- package/dist/esm/sonos/api/namespaces/playback.js +56 -0
- package/dist/esm/sonos/api/namespaces/playbackMetadata.js +16 -0
- package/dist/esm/sonos/api/namespaces/playbackSession.js +67 -0
- package/dist/esm/sonos/api/namespaces/playerVolume.js +41 -0
- package/dist/esm/sonos/api/websocket.js +310 -0
- package/dist/esm/sonos/client.js +193 -0
- package/dist/esm/sonos/constants.js +18 -0
- package/dist/esm/sonos/errors.js +49 -0
- package/dist/esm/sonos/group.js +231 -0
- package/dist/esm/sonos/models.js +63 -0
- package/dist/esm/sonos/player.js +112 -0
- package/dist/esm/sonos/types.js +46 -0
- package/dist/esm/sonos/utils.js +18 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +27 -0
- package/dist/sonos/api/namespace.d.ts +16 -0
- package/dist/sonos/api/namespace.js +38 -0
- package/dist/sonos/api/namespaces/audioClip.d.ts +16 -0
- package/dist/sonos/api/namespaces/audioClip.js +25 -0
- package/dist/sonos/api/namespaces/groupVolume.d.ts +10 -0
- package/dist/sonos/api/namespaces/groupVolume.js +25 -0
- package/dist/sonos/api/namespaces/groups.d.ts +10 -0
- package/dist/sonos/api/namespaces/groups.js +31 -0
- package/dist/sonos/api/namespaces/homeTheater.d.ts +5 -0
- package/dist/sonos/api/namespaces/homeTheater.js +13 -0
- package/dist/sonos/api/namespaces/playback.d.ts +17 -0
- package/dist/sonos/api/namespaces/playback.js +56 -0
- package/dist/sonos/api/namespaces/playbackMetadata.d.ts +7 -0
- package/dist/sonos/api/namespaces/playbackMetadata.js +16 -0
- package/dist/sonos/api/namespaces/playbackSession.d.ts +30 -0
- package/dist/sonos/api/namespaces/playbackSession.js +67 -0
- package/dist/sonos/api/namespaces/playerVolume.d.ts +12 -0
- package/dist/sonos/api/namespaces/playerVolume.js +41 -0
- package/dist/sonos/api/websocket.d.ts +51 -0
- package/dist/sonos/api/websocket.js +310 -0
- package/dist/sonos/client.d.ts +44 -0
- package/dist/sonos/client.js +193 -0
- package/dist/sonos/constants.d.ts +14 -0
- package/dist/sonos/constants.js +18 -0
- package/dist/sonos/errors.d.ts +25 -0
- package/dist/sonos/errors.js +49 -0
- package/dist/sonos/group.d.ts +58 -0
- package/dist/sonos/group.js +231 -0
- package/dist/sonos/models.d.ts +20 -0
- package/dist/sonos/models.js +63 -0
- package/dist/sonos/player.d.ts +33 -0
- package/dist/sonos/player.js +112 -0
- package/dist/sonos/types.d.ts +279 -0
- package/dist/sonos/types.js +46 -0
- package/dist/sonos/utils.d.ts +6 -0
- package/dist/sonos/utils.js +18 -0
- package/package.json +49 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GroupsNamespace = void 0;
|
|
4
|
+
const namespace_1 = require("../namespace");
|
|
5
|
+
class GroupsNamespace extends namespace_1.SonosNamespace {
|
|
6
|
+
constructor(api) {
|
|
7
|
+
super(api, 'groups', 'groups', 'householdId');
|
|
8
|
+
}
|
|
9
|
+
async modifyGroupMembers(groupId, playerIdsToAdd, playerIdsToRemove) {
|
|
10
|
+
return this.api.sendCommand(this.namespace, 'modifyGroupMembers', { playerIdsToAdd, playerIdsToRemove }, { groupId });
|
|
11
|
+
}
|
|
12
|
+
async setGroupMembers(groupId, playerIds, areaIds) {
|
|
13
|
+
const options = { playerIds };
|
|
14
|
+
if (areaIds)
|
|
15
|
+
options.areaIds = areaIds;
|
|
16
|
+
return this.api.sendCommand(this.namespace, 'setGroupMembers', options, { groupId });
|
|
17
|
+
}
|
|
18
|
+
async getGroups(householdId, includeDeviceInfo = false) {
|
|
19
|
+
return this.api.sendCommand(this.namespace, 'getGroups', { includeDeviceInfo }, { householdId });
|
|
20
|
+
}
|
|
21
|
+
async createGroup(householdId, playerIds, musicContextGroupId) {
|
|
22
|
+
const options = { playerIds };
|
|
23
|
+
if (musicContextGroupId)
|
|
24
|
+
options.musicContextGroupId = musicContextGroupId;
|
|
25
|
+
return this.api.sendCommand(this.namespace, 'createGroup', options, { householdId });
|
|
26
|
+
}
|
|
27
|
+
async subscribe(householdId, callback) {
|
|
28
|
+
return this.handleSubscribe(householdId, callback);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.GroupsNamespace = GroupsNamespace;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HomeTheaterNamespace = void 0;
|
|
4
|
+
const namespace_1 = require("../namespace");
|
|
5
|
+
class HomeTheaterNamespace extends namespace_1.SonosNamespace {
|
|
6
|
+
constructor(api) {
|
|
7
|
+
super(api, 'homeTheater', 'homeTheaterStatus', 'playerId');
|
|
8
|
+
}
|
|
9
|
+
async loadHomeTheaterPlayback(playerId) {
|
|
10
|
+
await this.api.sendCommand(this.namespace, 'loadHomeTheaterPlayback', undefined, { playerId });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.HomeTheaterNamespace = HomeTheaterNamespace;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LoadContentRequest, PlaybackStatus, PlayModes } from '../../types';
|
|
2
|
+
import { SonosNamespace, SubscribeCallback, UnsubscribeCallback } from '../namespace';
|
|
3
|
+
export declare class PlaybackNamespace extends SonosNamespace<PlaybackStatus> {
|
|
4
|
+
constructor(api: any);
|
|
5
|
+
getPlaybackStatus(groupId: string): Promise<PlaybackStatus>;
|
|
6
|
+
loadLineIn(groupId: string, deviceId?: string, playOnCompletion?: boolean): Promise<void>;
|
|
7
|
+
loadContent(groupId: string, content: LoadContentRequest): Promise<void>;
|
|
8
|
+
pause(groupId: string): Promise<void>;
|
|
9
|
+
play(groupId: string): Promise<void>;
|
|
10
|
+
togglePlayPause(groupId: string): Promise<void>;
|
|
11
|
+
setPlayModes(groupId: string, playModes: PlayModes): Promise<void>;
|
|
12
|
+
seek(groupId: string, positionMillis: number, itemId?: string): Promise<void>;
|
|
13
|
+
seekRelative(groupId: string, deltaMillis: number, itemId?: string): Promise<void>;
|
|
14
|
+
skipToNextTrack(groupId: string): Promise<void>;
|
|
15
|
+
skipToPreviousTrack(groupId: string): Promise<void>;
|
|
16
|
+
subscribe(groupId: string, callback: SubscribeCallback<PlaybackStatus>): Promise<UnsubscribeCallback>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PlaybackNamespace = void 0;
|
|
4
|
+
const namespace_1 = require("../namespace");
|
|
5
|
+
class PlaybackNamespace extends namespace_1.SonosNamespace {
|
|
6
|
+
constructor(api) {
|
|
7
|
+
super(api, 'playback', 'playbackStatus', 'groupId');
|
|
8
|
+
}
|
|
9
|
+
async getPlaybackStatus(groupId) {
|
|
10
|
+
return this.api.sendCommand(this.namespace, 'getPlaybackStatus', undefined, { groupId });
|
|
11
|
+
}
|
|
12
|
+
async loadLineIn(groupId, deviceId, playOnCompletion = false) {
|
|
13
|
+
const options = {};
|
|
14
|
+
if (deviceId)
|
|
15
|
+
options.deviceId = deviceId;
|
|
16
|
+
options.playOnCompletion = playOnCompletion;
|
|
17
|
+
await this.api.sendCommand(this.namespace, 'loadLineIn', options, { groupId });
|
|
18
|
+
}
|
|
19
|
+
async loadContent(groupId, content) {
|
|
20
|
+
await this.api.sendCommand(this.namespace, 'loadContent', content, { groupId });
|
|
21
|
+
}
|
|
22
|
+
async pause(groupId) {
|
|
23
|
+
await this.api.sendCommand(this.namespace, 'pause', undefined, { groupId });
|
|
24
|
+
}
|
|
25
|
+
async play(groupId) {
|
|
26
|
+
await this.api.sendCommand(this.namespace, 'play', undefined, { groupId });
|
|
27
|
+
}
|
|
28
|
+
async togglePlayPause(groupId) {
|
|
29
|
+
await this.api.sendCommand(this.namespace, 'togglePlayPause', undefined, { groupId });
|
|
30
|
+
}
|
|
31
|
+
async setPlayModes(groupId, playModes) {
|
|
32
|
+
await this.api.sendCommand(this.namespace, 'setPlayModes', { playModes }, { groupId });
|
|
33
|
+
}
|
|
34
|
+
async seek(groupId, positionMillis, itemId) {
|
|
35
|
+
const options = { positionMillis };
|
|
36
|
+
if (itemId)
|
|
37
|
+
options.itemId = itemId;
|
|
38
|
+
await this.api.sendCommand(this.namespace, 'seek', options, { groupId });
|
|
39
|
+
}
|
|
40
|
+
async seekRelative(groupId, deltaMillis, itemId) {
|
|
41
|
+
const options = { deltaMillis };
|
|
42
|
+
if (itemId)
|
|
43
|
+
options.itemId = itemId;
|
|
44
|
+
await this.api.sendCommand(this.namespace, 'seekRelative', options, { groupId });
|
|
45
|
+
}
|
|
46
|
+
async skipToNextTrack(groupId) {
|
|
47
|
+
await this.api.sendCommand(this.namespace, 'skipToNextTrack', undefined, { groupId });
|
|
48
|
+
}
|
|
49
|
+
async skipToPreviousTrack(groupId) {
|
|
50
|
+
await this.api.sendCommand(this.namespace, 'skipToPreviousTrack', undefined, { groupId });
|
|
51
|
+
}
|
|
52
|
+
async subscribe(groupId, callback) {
|
|
53
|
+
return this.handleSubscribe(groupId, callback);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.PlaybackNamespace = PlaybackNamespace;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { MetadataStatus } from '../../types';
|
|
2
|
+
import { SonosNamespace, SubscribeCallback, UnsubscribeCallback } from '../namespace';
|
|
3
|
+
export declare class PlaybackMetadataNamespace extends SonosNamespace<MetadataStatus> {
|
|
4
|
+
constructor(api: any);
|
|
5
|
+
getMetadataStatus(groupId: string): Promise<MetadataStatus>;
|
|
6
|
+
subscribe(groupId: string, callback: SubscribeCallback<MetadataStatus>): Promise<UnsubscribeCallback>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PlaybackMetadataNamespace = void 0;
|
|
4
|
+
const namespace_1 = require("../namespace");
|
|
5
|
+
class PlaybackMetadataNamespace extends namespace_1.SonosNamespace {
|
|
6
|
+
constructor(api) {
|
|
7
|
+
super(api, 'playbackMetadata', 'metadataStatus', 'groupId');
|
|
8
|
+
}
|
|
9
|
+
async getMetadataStatus(groupId) {
|
|
10
|
+
return this.api.sendCommand(this.namespace, 'getMetadataStatus', undefined, { groupId });
|
|
11
|
+
}
|
|
12
|
+
async subscribe(groupId, callback) {
|
|
13
|
+
return this.handleSubscribe(groupId, callback);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.PlaybackMetadataNamespace = PlaybackMetadataNamespace;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SessionStatus, Track } from '../../types';
|
|
2
|
+
import { SonosNamespace, SubscribeCallback, UnsubscribeCallback } from '../namespace';
|
|
3
|
+
export declare class PlaybackSessionNamespace extends SonosNamespace<SessionStatus> {
|
|
4
|
+
constructor(api: any);
|
|
5
|
+
createSession(groupId: string, appId: string, appContext: string, accountId?: string, customData?: Record<string, unknown>): Promise<SessionStatus>;
|
|
6
|
+
loadCloudQueue(sessionId: string, queueBaseUrl: string, options: {
|
|
7
|
+
httpAuthorization?: string;
|
|
8
|
+
useHttpAuthorizationForMedia?: boolean;
|
|
9
|
+
itemId?: string;
|
|
10
|
+
queueVersion?: string;
|
|
11
|
+
positionMillis?: number;
|
|
12
|
+
playOnCompletion?: boolean;
|
|
13
|
+
trackMetadata?: Track;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
loadStreamUrl(sessionId: string, streamUrl: string, options?: {
|
|
16
|
+
playOnCompletion?: boolean;
|
|
17
|
+
stationMetadata?: Record<string, unknown>;
|
|
18
|
+
itemId?: string;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
refreshCloudQueue(sessionId: string): Promise<void>;
|
|
21
|
+
seek(sessionId: string, positionMillis: number, itemId?: string): Promise<void>;
|
|
22
|
+
seekRelative(sessionId: string, deltaMillis: number, itemId?: string): Promise<void>;
|
|
23
|
+
skipToItem(sessionId: string, itemId: string, options?: {
|
|
24
|
+
queueVersion?: string;
|
|
25
|
+
positionMillis?: number;
|
|
26
|
+
playOnCompletion?: boolean;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
suspend(sessionId: string, queueVersion?: string): Promise<void>;
|
|
29
|
+
subscribe(sessionId: string, callback: SubscribeCallback<SessionStatus>): Promise<UnsubscribeCallback>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PlaybackSessionNamespace = void 0;
|
|
4
|
+
const namespace_1 = require("../namespace");
|
|
5
|
+
class PlaybackSessionNamespace extends namespace_1.SonosNamespace {
|
|
6
|
+
constructor(api) {
|
|
7
|
+
super(api, 'playbackSession', 'playbackSession', 'sessionId');
|
|
8
|
+
}
|
|
9
|
+
async createSession(groupId, appId, appContext, accountId, customData) {
|
|
10
|
+
const options = { appId, appContext };
|
|
11
|
+
if (accountId)
|
|
12
|
+
options.accountId = accountId;
|
|
13
|
+
if (customData)
|
|
14
|
+
options.customData = customData;
|
|
15
|
+
return this.api.sendCommand(this.namespace, 'createSession', options, { groupId });
|
|
16
|
+
}
|
|
17
|
+
async loadCloudQueue(sessionId, queueBaseUrl, options) {
|
|
18
|
+
await this.api.sendCommand(this.namespace, 'loadCloudQueue', {
|
|
19
|
+
queueBaseUrl,
|
|
20
|
+
httpAuthorization: options.httpAuthorization,
|
|
21
|
+
useHttpAuthorizationForMedia: options.useHttpAuthorizationForMedia,
|
|
22
|
+
itemId: options.itemId,
|
|
23
|
+
queueVersion: options.queueVersion,
|
|
24
|
+
positionMillis: options.positionMillis,
|
|
25
|
+
playOnCompletion: options.playOnCompletion,
|
|
26
|
+
trackMetadata: options.trackMetadata,
|
|
27
|
+
}, { sessionId });
|
|
28
|
+
}
|
|
29
|
+
async loadStreamUrl(sessionId, streamUrl, options = {}) {
|
|
30
|
+
await this.api.sendCommand(this.namespace, 'loadStreamUrl', {
|
|
31
|
+
streamUrl,
|
|
32
|
+
playOnCompletion: options.playOnCompletion,
|
|
33
|
+
stationMetadata: options.stationMetadata,
|
|
34
|
+
itemId: options.itemId,
|
|
35
|
+
}, { sessionId });
|
|
36
|
+
}
|
|
37
|
+
async refreshCloudQueue(sessionId) {
|
|
38
|
+
await this.api.sendCommand(this.namespace, 'refreshCloudQueue', undefined, { sessionId });
|
|
39
|
+
}
|
|
40
|
+
async seek(sessionId, positionMillis, itemId) {
|
|
41
|
+
const options = { positionMillis };
|
|
42
|
+
if (itemId)
|
|
43
|
+
options.itemId = itemId;
|
|
44
|
+
await this.api.sendCommand(this.namespace, 'seek', options, { sessionId });
|
|
45
|
+
}
|
|
46
|
+
async seekRelative(sessionId, deltaMillis, itemId) {
|
|
47
|
+
const options = { deltaMillis };
|
|
48
|
+
if (itemId)
|
|
49
|
+
options.itemId = itemId;
|
|
50
|
+
await this.api.sendCommand(this.namespace, 'seekRelative', options, { sessionId });
|
|
51
|
+
}
|
|
52
|
+
async skipToItem(sessionId, itemId, options = {}) {
|
|
53
|
+
await this.api.sendCommand(this.namespace, 'skipToItem', {
|
|
54
|
+
itemId,
|
|
55
|
+
queueVersion: options.queueVersion,
|
|
56
|
+
positionMillis: options.positionMillis,
|
|
57
|
+
playOnCompletion: options.playOnCompletion,
|
|
58
|
+
}, { sessionId });
|
|
59
|
+
}
|
|
60
|
+
async suspend(sessionId, queueVersion) {
|
|
61
|
+
await this.api.sendCommand(this.namespace, 'suspend', queueVersion ? { queueVersion } : undefined, { sessionId });
|
|
62
|
+
}
|
|
63
|
+
async subscribe(sessionId, callback) {
|
|
64
|
+
return this.handleSubscribe(sessionId, callback);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.PlaybackSessionNamespace = PlaybackSessionNamespace;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PlayerVolume } from '../../types';
|
|
2
|
+
import { SonosNamespace, SubscribeCallback, UnsubscribeCallback } from '../namespace';
|
|
3
|
+
export declare class PlayerVolumeNamespace extends SonosNamespace<PlayerVolume> {
|
|
4
|
+
constructor(api: any);
|
|
5
|
+
setVolume(playerId: string, volume?: number, muted?: boolean): Promise<void>;
|
|
6
|
+
getVolume(playerId: string): Promise<PlayerVolume>;
|
|
7
|
+
duck(playerId: string, durationMillis?: number): Promise<void>;
|
|
8
|
+
unduck(playerId: string): Promise<void>;
|
|
9
|
+
setMute(playerId: string, muted: boolean): Promise<void>;
|
|
10
|
+
setRelativeVolume(playerId: string, volumeDelta?: number, muted?: boolean): Promise<void>;
|
|
11
|
+
subscribe(playerId: string, callback: SubscribeCallback<PlayerVolume>): Promise<UnsubscribeCallback>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PlayerVolumeNamespace = void 0;
|
|
4
|
+
const namespace_1 = require("../namespace");
|
|
5
|
+
class PlayerVolumeNamespace extends namespace_1.SonosNamespace {
|
|
6
|
+
constructor(api) {
|
|
7
|
+
super(api, 'playerVolume', 'playerVolume', 'playerId');
|
|
8
|
+
}
|
|
9
|
+
async setVolume(playerId, volume, muted) {
|
|
10
|
+
const options = {};
|
|
11
|
+
if (typeof volume === 'number')
|
|
12
|
+
options.volume = volume;
|
|
13
|
+
if (typeof muted === 'boolean')
|
|
14
|
+
options.muted = muted;
|
|
15
|
+
await this.api.sendCommand(this.namespace, 'setVolume', options, { playerId });
|
|
16
|
+
}
|
|
17
|
+
async getVolume(playerId) {
|
|
18
|
+
return this.api.sendCommand(this.namespace, 'getVolume', undefined, { playerId });
|
|
19
|
+
}
|
|
20
|
+
async duck(playerId, durationMillis) {
|
|
21
|
+
await this.api.sendCommand(this.namespace, 'duck', durationMillis ? { durationMillis } : undefined, { playerId });
|
|
22
|
+
}
|
|
23
|
+
async unduck(playerId) {
|
|
24
|
+
await this.api.sendCommand(this.namespace, 'unduck', undefined, { playerId });
|
|
25
|
+
}
|
|
26
|
+
async setMute(playerId, muted) {
|
|
27
|
+
await this.api.sendCommand(this.namespace, 'setMute', { muted }, { playerId });
|
|
28
|
+
}
|
|
29
|
+
async setRelativeVolume(playerId, volumeDelta, muted) {
|
|
30
|
+
const options = {};
|
|
31
|
+
if (typeof volumeDelta === 'number')
|
|
32
|
+
options.volumeDelta = volumeDelta;
|
|
33
|
+
if (typeof muted === 'boolean')
|
|
34
|
+
options.muted = muted;
|
|
35
|
+
await this.api.sendCommand(this.namespace, 'setRelativeVolume', options, { playerId });
|
|
36
|
+
}
|
|
37
|
+
async subscribe(playerId, callback) {
|
|
38
|
+
return this.handleSubscribe(playerId, callback);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.PlayerVolumeNamespace = PlayerVolumeNamespace;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AudioClipNamespace } from './namespaces/audioClip';
|
|
2
|
+
import { GroupsNamespace } from './namespaces/groups';
|
|
3
|
+
import { GroupVolumeNamespace } from './namespaces/groupVolume';
|
|
4
|
+
import { PlaybackNamespace } from './namespaces/playback';
|
|
5
|
+
import { PlaybackMetadataNamespace } from './namespaces/playbackMetadata';
|
|
6
|
+
import { PlaybackSessionNamespace } from './namespaces/playbackSession';
|
|
7
|
+
import { PlayerVolumeNamespace } from './namespaces/playerVolume';
|
|
8
|
+
import { HomeTheaterNamespace } from './namespaces/homeTheater';
|
|
9
|
+
export interface WebSocketReliabilityOptions {
|
|
10
|
+
heartbeatIntervalMs?: number;
|
|
11
|
+
retryDelayMs?: number;
|
|
12
|
+
retryJitterMs?: number;
|
|
13
|
+
maxReconnects?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class SonosWebSocketApi {
|
|
16
|
+
private websocketUrl;
|
|
17
|
+
private ws?;
|
|
18
|
+
private resultFutures;
|
|
19
|
+
private stopCalled;
|
|
20
|
+
private heartbeatTimer?;
|
|
21
|
+
private heartbeatIntervalMs;
|
|
22
|
+
private retryDelayMs;
|
|
23
|
+
private retryJitterMs;
|
|
24
|
+
private maxReconnects?;
|
|
25
|
+
readonly audioClip: AudioClipNamespace;
|
|
26
|
+
readonly groups: GroupsNamespace;
|
|
27
|
+
readonly groupVolume: GroupVolumeNamespace;
|
|
28
|
+
readonly playback: PlaybackNamespace;
|
|
29
|
+
readonly playbackMetadata: PlaybackMetadataNamespace;
|
|
30
|
+
readonly playbackSession: PlaybackSessionNamespace;
|
|
31
|
+
readonly playerVolume: PlayerVolumeNamespace;
|
|
32
|
+
readonly homeTheater: HomeTheaterNamespace;
|
|
33
|
+
logger: Console;
|
|
34
|
+
onConnect?: () => void | Promise<void>;
|
|
35
|
+
onDisconnect?: (reason?: string) => void | Promise<void>;
|
|
36
|
+
constructor(websocketUrl: string, opts?: WebSocketReliabilityOptions);
|
|
37
|
+
get connected(): boolean;
|
|
38
|
+
connect(): Promise<void>;
|
|
39
|
+
startListening(): Promise<void>;
|
|
40
|
+
disconnect(): Promise<void>;
|
|
41
|
+
sendCommand(namespace: string, command: string, options?: Record<string, unknown>, pathParams?: Record<string, unknown>): Promise<any>;
|
|
42
|
+
sendCommandNoWait(namespace: string, command: string, options?: Record<string, unknown>, pathParams?: Record<string, unknown>): void;
|
|
43
|
+
private send;
|
|
44
|
+
private handleRawMessage;
|
|
45
|
+
private handleIncoming;
|
|
46
|
+
private waitForClose;
|
|
47
|
+
private startHeartbeat;
|
|
48
|
+
private clearHeartbeat;
|
|
49
|
+
private delay;
|
|
50
|
+
private computeRetryDelay;
|
|
51
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SonosWebSocketApi = void 0;
|
|
7
|
+
const ws_1 = __importDefault(require("ws"));
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const constants_1 = require("../constants");
|
|
10
|
+
const errors_1 = require("../errors");
|
|
11
|
+
const audioClip_1 = require("./namespaces/audioClip");
|
|
12
|
+
const groups_1 = require("./namespaces/groups");
|
|
13
|
+
const groupVolume_1 = require("./namespaces/groupVolume");
|
|
14
|
+
const playback_1 = require("./namespaces/playback");
|
|
15
|
+
const playbackMetadata_1 = require("./namespaces/playbackMetadata");
|
|
16
|
+
const playbackSession_1 = require("./namespaces/playbackSession");
|
|
17
|
+
const playerVolume_1 = require("./namespaces/playerVolume");
|
|
18
|
+
const homeTheater_1 = require("./namespaces/homeTheater");
|
|
19
|
+
class SonosWebSocketApi {
|
|
20
|
+
websocketUrl;
|
|
21
|
+
ws;
|
|
22
|
+
resultFutures = new Map();
|
|
23
|
+
stopCalled = false;
|
|
24
|
+
heartbeatTimer;
|
|
25
|
+
heartbeatIntervalMs;
|
|
26
|
+
retryDelayMs;
|
|
27
|
+
retryJitterMs;
|
|
28
|
+
maxReconnects;
|
|
29
|
+
audioClip;
|
|
30
|
+
groups;
|
|
31
|
+
groupVolume;
|
|
32
|
+
playback;
|
|
33
|
+
playbackMetadata;
|
|
34
|
+
playbackSession;
|
|
35
|
+
playerVolume;
|
|
36
|
+
homeTheater;
|
|
37
|
+
logger = console;
|
|
38
|
+
onConnect;
|
|
39
|
+
onDisconnect;
|
|
40
|
+
constructor(websocketUrl, opts = {}) {
|
|
41
|
+
this.websocketUrl = websocketUrl;
|
|
42
|
+
this.audioClip = new audioClip_1.AudioClipNamespace(this);
|
|
43
|
+
this.groups = new groups_1.GroupsNamespace(this);
|
|
44
|
+
this.groupVolume = new groupVolume_1.GroupVolumeNamespace(this);
|
|
45
|
+
this.playback = new playback_1.PlaybackNamespace(this);
|
|
46
|
+
this.playbackMetadata = new playbackMetadata_1.PlaybackMetadataNamespace(this);
|
|
47
|
+
this.playbackSession = new playbackSession_1.PlaybackSessionNamespace(this);
|
|
48
|
+
this.playerVolume = new playerVolume_1.PlayerVolumeNamespace(this);
|
|
49
|
+
this.homeTheater = new homeTheater_1.HomeTheaterNamespace(this);
|
|
50
|
+
this.heartbeatIntervalMs = opts.heartbeatIntervalMs ?? 30000;
|
|
51
|
+
this.retryDelayMs = opts.retryDelayMs ?? 2000;
|
|
52
|
+
this.retryJitterMs = opts.retryJitterMs ?? 500;
|
|
53
|
+
this.maxReconnects = opts.maxReconnects;
|
|
54
|
+
}
|
|
55
|
+
get connected() {
|
|
56
|
+
return Boolean(this.ws && this.ws.readyState === ws_1.default.OPEN);
|
|
57
|
+
}
|
|
58
|
+
async connect() {
|
|
59
|
+
if (this.ws) {
|
|
60
|
+
throw new errors_1.InvalidState('Already connected');
|
|
61
|
+
}
|
|
62
|
+
this.logger.debug?.(`Connecting to Sonos WebSocket ${this.websocketUrl}`);
|
|
63
|
+
this.stopCalled = false;
|
|
64
|
+
try {
|
|
65
|
+
this.ws = new ws_1.default(this.websocketUrl, {
|
|
66
|
+
headers: {
|
|
67
|
+
'X-Sonos-Api-Key': constants_1.LOCAL_API_TOKEN,
|
|
68
|
+
'Sec-WebSocket-Protocol': 'v1.api.smartspeaker.audio',
|
|
69
|
+
},
|
|
70
|
+
rejectUnauthorized: false,
|
|
71
|
+
perMessageDeflate: true,
|
|
72
|
+
maxPayload: 0,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
throw new errors_1.CannotConnect('Failed to create websocket', err);
|
|
77
|
+
}
|
|
78
|
+
await new Promise((resolve, reject) => {
|
|
79
|
+
if (!this.ws)
|
|
80
|
+
return reject(new errors_1.CannotConnect('No websocket'));
|
|
81
|
+
this.ws.once('open', () => resolve());
|
|
82
|
+
this.ws.once('error', (err) => reject(new errors_1.CannotConnect('Failed to connect', err)));
|
|
83
|
+
});
|
|
84
|
+
this.ws.on('message', (data) => this.handleRawMessage(data));
|
|
85
|
+
this.ws.on('close', () => {
|
|
86
|
+
this.clearHeartbeat();
|
|
87
|
+
});
|
|
88
|
+
this.ws.on('error', (err) => {
|
|
89
|
+
this.logger.warn?.('WebSocket error', err);
|
|
90
|
+
});
|
|
91
|
+
this.ws.on('pong', () => {
|
|
92
|
+
// noop; pong receipt confirms liveness
|
|
93
|
+
});
|
|
94
|
+
this.startHeartbeat();
|
|
95
|
+
await this.onConnect?.();
|
|
96
|
+
}
|
|
97
|
+
async startListening() {
|
|
98
|
+
this.stopCalled = false;
|
|
99
|
+
while (!this.stopCalled) {
|
|
100
|
+
let attempts = 0;
|
|
101
|
+
try {
|
|
102
|
+
if (!this.connected) {
|
|
103
|
+
await this.connect();
|
|
104
|
+
}
|
|
105
|
+
const reason = await this.waitForClose();
|
|
106
|
+
if (this.stopCalled)
|
|
107
|
+
break;
|
|
108
|
+
await this.onDisconnect?.(reason);
|
|
109
|
+
this.logger.warn?.('WebSocket closed, reconnecting...', reason);
|
|
110
|
+
attempts += 1;
|
|
111
|
+
if (this.maxReconnects && attempts > this.maxReconnects) {
|
|
112
|
+
this.logger.error?.('Max reconnect attempts reached');
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
await this.delay(this.computeRetryDelay(attempts));
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (this.stopCalled)
|
|
119
|
+
break;
|
|
120
|
+
this.logger.warn?.('WebSocket listen error, reconnecting...', err);
|
|
121
|
+
await this.onDisconnect?.(err instanceof Error ? err.message : undefined);
|
|
122
|
+
attempts += 1;
|
|
123
|
+
if (this.maxReconnects && attempts > this.maxReconnects) {
|
|
124
|
+
this.logger.error?.('Max reconnect attempts reached after error');
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
await this.delay(this.computeRetryDelay(attempts));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async disconnect() {
|
|
132
|
+
this.stopCalled = true;
|
|
133
|
+
this.clearHeartbeat();
|
|
134
|
+
for (const pending of this.resultFutures.values()) {
|
|
135
|
+
pending.reject(new errors_1.ConnectionClosed('Connection closed'));
|
|
136
|
+
}
|
|
137
|
+
this.resultFutures.clear();
|
|
138
|
+
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
139
|
+
await new Promise((resolve) => {
|
|
140
|
+
this.ws?.once('close', () => resolve());
|
|
141
|
+
this.ws?.close();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
this.ws = undefined;
|
|
145
|
+
}
|
|
146
|
+
async sendCommand(namespace, command, options, pathParams) {
|
|
147
|
+
if (!this.connected || !this.ws)
|
|
148
|
+
throw new errors_1.InvalidState('Not connected');
|
|
149
|
+
const cmdId = (0, node_crypto_1.randomUUID)();
|
|
150
|
+
const cmdMessage = {
|
|
151
|
+
namespace: `${namespace}:${constants_1.API_VERSION}`,
|
|
152
|
+
command,
|
|
153
|
+
cmdId,
|
|
154
|
+
...(pathParams ?? {}),
|
|
155
|
+
};
|
|
156
|
+
const payload = [cmdMessage, options ?? {}];
|
|
157
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
158
|
+
this.resultFutures.set(cmdId, { resolve, reject });
|
|
159
|
+
});
|
|
160
|
+
await this.send(payload);
|
|
161
|
+
return resultPromise.finally(() => this.resultFutures.delete(cmdId));
|
|
162
|
+
}
|
|
163
|
+
sendCommandNoWait(namespace, command, options, pathParams) {
|
|
164
|
+
if (!this.connected || !this.ws)
|
|
165
|
+
throw new errors_1.NotConnected('Not connected');
|
|
166
|
+
const cmdMessage = {
|
|
167
|
+
namespace: `${namespace}:${constants_1.API_VERSION}`,
|
|
168
|
+
command,
|
|
169
|
+
cmdId: (0, node_crypto_1.randomUUID)(),
|
|
170
|
+
...(pathParams ?? {}),
|
|
171
|
+
};
|
|
172
|
+
const payload = [cmdMessage, options ?? {}];
|
|
173
|
+
void this.send(payload);
|
|
174
|
+
}
|
|
175
|
+
async send(payload) {
|
|
176
|
+
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
|
|
177
|
+
throw new errors_1.NotConnected('Not connected');
|
|
178
|
+
}
|
|
179
|
+
if (this.logger && this.logger.log && constants_1.LOG_LEVEL_VERBOSE) {
|
|
180
|
+
this.logger.log?.(constants_1.LOG_LEVEL_VERBOSE, 'Publishing message', payload);
|
|
181
|
+
}
|
|
182
|
+
const data = JSON.stringify(payload);
|
|
183
|
+
await new Promise((resolve, reject) => {
|
|
184
|
+
this.ws?.send(data, (err) => {
|
|
185
|
+
if (err)
|
|
186
|
+
reject(err);
|
|
187
|
+
else
|
|
188
|
+
resolve();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
handleRawMessage(raw) {
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(raw.toString());
|
|
195
|
+
try {
|
|
196
|
+
this.handleIncoming(parsed);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
this.logger.error?.('Failed to handle Sonos message', err);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
this.logger.error?.('Invalid JSON from Sonos', err);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
handleIncoming(raw) {
|
|
207
|
+
if (!Array.isArray(raw) || raw.length !== 2) {
|
|
208
|
+
this.logger.error?.('Invalid Sonos message shape', raw);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const [msg, msgData] = raw;
|
|
212
|
+
if (!msg || typeof msg !== 'object') {
|
|
213
|
+
throw new errors_1.InvalidMessage('Received malformed message');
|
|
214
|
+
}
|
|
215
|
+
// error response
|
|
216
|
+
if ('errorCode' in msgData) {
|
|
217
|
+
const errData = msgData;
|
|
218
|
+
if ('cmdId' in msg && msg.cmdId) {
|
|
219
|
+
const future = this.resultFutures.get(msg.cmdId);
|
|
220
|
+
future?.reject(new errors_1.FailedCommand(errData.errorCode, errData.reason));
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
this.logger.error?.('Unhandled error', msgData);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// command result
|
|
228
|
+
if ('success' in msg) {
|
|
229
|
+
if (msg.cmdId && this.resultFutures.has(msg.cmdId)) {
|
|
230
|
+
const pending = this.resultFutures.get(msg.cmdId);
|
|
231
|
+
if (msg.success) {
|
|
232
|
+
pending?.resolve(msgData);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
pending?.reject(new errors_1.FailedCommand(String(msgData['_objectType'] ?? 'unknown')));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// event
|
|
241
|
+
if (msg.type) {
|
|
242
|
+
const namespaces = [
|
|
243
|
+
this.audioClip,
|
|
244
|
+
this.groups,
|
|
245
|
+
this.groupVolume,
|
|
246
|
+
this.playbackMetadata,
|
|
247
|
+
this.playbackSession,
|
|
248
|
+
this.playback,
|
|
249
|
+
this.playerVolume,
|
|
250
|
+
this.homeTheater,
|
|
251
|
+
];
|
|
252
|
+
const target = namespaces.find((ns) => ns.eventType === msg.type);
|
|
253
|
+
if (target) {
|
|
254
|
+
target.handleEvent(msg, msgData);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this.logger.debug?.(`Unhandled event type ${msg.type}`);
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.logger.debug?.('Unhandled message', raw);
|
|
262
|
+
}
|
|
263
|
+
async waitForClose() {
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
const ws = this.ws;
|
|
266
|
+
if (!ws)
|
|
267
|
+
return reject(new errors_1.NotConnected('No websocket'));
|
|
268
|
+
const onClose = (code, reason) => {
|
|
269
|
+
ws.removeListener('error', onError);
|
|
270
|
+
resolve(`${code}:${reason.toString()}`);
|
|
271
|
+
};
|
|
272
|
+
const onError = (err) => {
|
|
273
|
+
ws.removeListener('close', onClose);
|
|
274
|
+
reject(new errors_1.ConnectionFailed(err));
|
|
275
|
+
};
|
|
276
|
+
ws.once('close', onClose);
|
|
277
|
+
ws.once('error', onError);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
startHeartbeat() {
|
|
281
|
+
this.clearHeartbeat();
|
|
282
|
+
if (!this.heartbeatIntervalMs)
|
|
283
|
+
return;
|
|
284
|
+
this.heartbeatTimer = setInterval(() => {
|
|
285
|
+
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
|
|
286
|
+
return;
|
|
287
|
+
try {
|
|
288
|
+
this.ws.ping();
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
this.logger.warn?.('Heartbeat ping failed', err);
|
|
292
|
+
this.ws.terminate();
|
|
293
|
+
}
|
|
294
|
+
}, this.heartbeatIntervalMs);
|
|
295
|
+
}
|
|
296
|
+
clearHeartbeat() {
|
|
297
|
+
if (this.heartbeatTimer)
|
|
298
|
+
clearInterval(this.heartbeatTimer);
|
|
299
|
+
this.heartbeatTimer = undefined;
|
|
300
|
+
}
|
|
301
|
+
async delay(ms) {
|
|
302
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
303
|
+
}
|
|
304
|
+
computeRetryDelay(attempt) {
|
|
305
|
+
const base = this.retryDelayMs * Math.min(2 ** (attempt - 1), 8);
|
|
306
|
+
const jitter = Math.random() * this.retryJitterMs;
|
|
307
|
+
return base + jitter;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
exports.SonosWebSocketApi = SonosWebSocketApi;
|