@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,44 @@
|
|
|
1
|
+
import { SonosWebSocketApi } from './api/websocket';
|
|
2
|
+
import { EventType } from './constants';
|
|
3
|
+
import { SonosGroup } from './group';
|
|
4
|
+
import { SonosPlayer } from './player';
|
|
5
|
+
import { DiscoveryOptions } from './utils';
|
|
6
|
+
import { SonosEvent } from './types';
|
|
7
|
+
type Subscriber = {
|
|
8
|
+
cb: (event: SonosEvent) => void;
|
|
9
|
+
eventFilter?: Set<EventType>;
|
|
10
|
+
objectIdFilter?: Set<string>;
|
|
11
|
+
};
|
|
12
|
+
export interface SonosClientOptions extends DiscoveryOptions {
|
|
13
|
+
logger?: Console;
|
|
14
|
+
heartbeatIntervalMs?: number;
|
|
15
|
+
retryDelayMs?: number;
|
|
16
|
+
retryJitterMs?: number;
|
|
17
|
+
maxReconnects?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class SonosClient {
|
|
20
|
+
private playerIp;
|
|
21
|
+
api: SonosWebSocketApi;
|
|
22
|
+
playerId: string;
|
|
23
|
+
householdId: string;
|
|
24
|
+
player: SonosPlayer | null;
|
|
25
|
+
private groupMap;
|
|
26
|
+
private subscribers;
|
|
27
|
+
private logger;
|
|
28
|
+
private bootstrapPromise;
|
|
29
|
+
private connectedPlayerIds;
|
|
30
|
+
constructor(playerIp: string, options?: SonosClientOptions);
|
|
31
|
+
get groups(): SonosGroup[];
|
|
32
|
+
subscribe(cb: Subscriber['cb'], eventFilter?: EventType | EventType[] | null, objectIdFilter?: string | string[] | null): () => void;
|
|
33
|
+
signalEvent(event: SonosEvent): void;
|
|
34
|
+
connect(options?: SonosClientOptions): Promise<void>;
|
|
35
|
+
disconnect(): Promise<void>;
|
|
36
|
+
start(): Promise<void>;
|
|
37
|
+
private bootstrap;
|
|
38
|
+
createGroup(playerIds: string[], musicContextGroupId?: string): Promise<void>;
|
|
39
|
+
private setupGroup;
|
|
40
|
+
private handleGroupsEvent;
|
|
41
|
+
private handleSocketConnected;
|
|
42
|
+
private handleSocketDisconnected;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SonosClient = void 0;
|
|
4
|
+
const websocket_1 = require("./api/websocket");
|
|
5
|
+
const constants_1 = require("./constants");
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
const group_1 = require("./group");
|
|
8
|
+
const player_1 = require("./player");
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
class SonosClient {
|
|
11
|
+
playerIp;
|
|
12
|
+
api;
|
|
13
|
+
playerId;
|
|
14
|
+
householdId;
|
|
15
|
+
player = null;
|
|
16
|
+
groupMap = new Map();
|
|
17
|
+
subscribers = [];
|
|
18
|
+
logger;
|
|
19
|
+
bootstrapPromise = null;
|
|
20
|
+
connectedPlayerIds = new Set();
|
|
21
|
+
constructor(playerIp, options = {}) {
|
|
22
|
+
this.playerIp = playerIp;
|
|
23
|
+
this.logger = options.logger ?? console;
|
|
24
|
+
}
|
|
25
|
+
get groups() {
|
|
26
|
+
return Array.from(this.groupMap.values());
|
|
27
|
+
}
|
|
28
|
+
subscribe(cb, eventFilter, objectIdFilter) {
|
|
29
|
+
const eventSet = eventFilter == null
|
|
30
|
+
? undefined
|
|
31
|
+
: Array.isArray(eventFilter)
|
|
32
|
+
? new Set(eventFilter)
|
|
33
|
+
: new Set([eventFilter]);
|
|
34
|
+
const objectSet = objectIdFilter == null
|
|
35
|
+
? undefined
|
|
36
|
+
: Array.isArray(objectIdFilter)
|
|
37
|
+
? new Set(objectIdFilter)
|
|
38
|
+
: new Set([objectIdFilter]);
|
|
39
|
+
const sub = {
|
|
40
|
+
cb,
|
|
41
|
+
eventFilter: eventSet,
|
|
42
|
+
objectIdFilter: objectSet,
|
|
43
|
+
};
|
|
44
|
+
this.subscribers.push(sub);
|
|
45
|
+
return () => {
|
|
46
|
+
this.subscribers = this.subscribers.filter((s) => s !== sub);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
signalEvent(event) {
|
|
50
|
+
for (const sub of this.subscribers) {
|
|
51
|
+
if (sub.eventFilter && !sub.eventFilter.has(event.eventType))
|
|
52
|
+
continue;
|
|
53
|
+
if (sub.objectIdFilter && event.objectId && !sub.objectIdFilter.has(event.objectId)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
sub.cb(event);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async connect(options) {
|
|
60
|
+
const discovery = await (0, utils_1.getDiscoveryInfo)(this.playerIp, options);
|
|
61
|
+
this.playerId = discovery.playerId;
|
|
62
|
+
this.householdId = discovery.householdId;
|
|
63
|
+
this.api = new websocket_1.SonosWebSocketApi(discovery.websocketUrl, {
|
|
64
|
+
heartbeatIntervalMs: options?.heartbeatIntervalMs,
|
|
65
|
+
retryDelayMs: options?.retryDelayMs,
|
|
66
|
+
retryJitterMs: options?.retryJitterMs,
|
|
67
|
+
maxReconnects: options?.maxReconnects,
|
|
68
|
+
});
|
|
69
|
+
if (options?.logger)
|
|
70
|
+
this.api.logger = options.logger;
|
|
71
|
+
this.api.onConnect = () => this.handleSocketConnected();
|
|
72
|
+
this.api.onDisconnect = (reason) => this.handleSocketDisconnected(reason);
|
|
73
|
+
await this.api.connect();
|
|
74
|
+
}
|
|
75
|
+
async disconnect() {
|
|
76
|
+
for (const group of this.groupMap.values()) {
|
|
77
|
+
group.cleanup();
|
|
78
|
+
}
|
|
79
|
+
await this.api.disconnect();
|
|
80
|
+
}
|
|
81
|
+
async start() {
|
|
82
|
+
await this.bootstrap();
|
|
83
|
+
await this.api.startListening();
|
|
84
|
+
}
|
|
85
|
+
async bootstrap(reconnect = false) {
|
|
86
|
+
if (this.bootstrapPromise)
|
|
87
|
+
return this.bootstrapPromise;
|
|
88
|
+
this.bootstrapPromise = (async () => {
|
|
89
|
+
if (!this.api || !this.api.connected) {
|
|
90
|
+
await this.connect();
|
|
91
|
+
}
|
|
92
|
+
if (reconnect || this.groupMap.size) {
|
|
93
|
+
for (const group of this.groupMap.values())
|
|
94
|
+
group.cleanup();
|
|
95
|
+
this.groupMap.clear();
|
|
96
|
+
}
|
|
97
|
+
const groupsData = await this.api.groups.getGroups(this.householdId, true);
|
|
98
|
+
for (const groupData of groupsData.groups) {
|
|
99
|
+
await this.setupGroup(groupData);
|
|
100
|
+
}
|
|
101
|
+
const playerData = groupsData.players.find((p) => p.id === this.playerId);
|
|
102
|
+
if (!playerData) {
|
|
103
|
+
throw new errors_1.FailedCommand('playerNotFound', 'Local player not returned in groups response');
|
|
104
|
+
}
|
|
105
|
+
this.player = new player_1.SonosPlayer(this, playerData);
|
|
106
|
+
await this.player.init();
|
|
107
|
+
this.connectedPlayerIds = new Set(groupsData.players.map((p) => p.id));
|
|
108
|
+
await this.api.groups.subscribe(this.householdId, (data) => this.handleGroupsEvent(data));
|
|
109
|
+
this.signalEvent({ eventType: constants_1.EventType.CONNECTED, objectId: this.playerId });
|
|
110
|
+
})();
|
|
111
|
+
try {
|
|
112
|
+
await this.bootstrapPromise;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
this.bootstrapPromise = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async createGroup(playerIds, musicContextGroupId) {
|
|
119
|
+
await this.api.groups.createGroup(this.householdId, playerIds, musicContextGroupId);
|
|
120
|
+
}
|
|
121
|
+
async setupGroup(groupData) {
|
|
122
|
+
const group = new group_1.SonosGroup(this, groupData);
|
|
123
|
+
this.groupMap.set(group.id, group);
|
|
124
|
+
await group.init();
|
|
125
|
+
if (this.player)
|
|
126
|
+
this.player.checkActiveGroup();
|
|
127
|
+
}
|
|
128
|
+
handleGroupsEvent(groupsData) {
|
|
129
|
+
for (const groupData of groupsData.groups) {
|
|
130
|
+
const existing = this.groupMap.get(groupData.id);
|
|
131
|
+
if (existing) {
|
|
132
|
+
existing.updateData(groupData);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
void this.setupGroup(groupData);
|
|
136
|
+
}
|
|
137
|
+
const removedIds = new Set(this.groupMap.keys());
|
|
138
|
+
for (const g of groupsData.groups)
|
|
139
|
+
removedIds.delete(g.id);
|
|
140
|
+
for (const removedId of removedIds) {
|
|
141
|
+
const group = this.groupMap.get(removedId);
|
|
142
|
+
group?.cleanup();
|
|
143
|
+
this.groupMap.delete(removedId);
|
|
144
|
+
this.signalEvent({
|
|
145
|
+
eventType: constants_1.EventType.GROUP_REMOVED,
|
|
146
|
+
objectId: removedId,
|
|
147
|
+
data: group,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const incomingPlayers = new Set(groupsData.players.map((p) => p.id));
|
|
151
|
+
// removals
|
|
152
|
+
for (const prev of Array.from(this.connectedPlayerIds)) {
|
|
153
|
+
if (!incomingPlayers.has(prev)) {
|
|
154
|
+
this.signalEvent({
|
|
155
|
+
eventType: constants_1.EventType.PLAYER_REMOVED,
|
|
156
|
+
objectId: prev,
|
|
157
|
+
data: prev,
|
|
158
|
+
});
|
|
159
|
+
this.connectedPlayerIds.delete(prev);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// updates/additions
|
|
163
|
+
for (const playerData of groupsData.players) {
|
|
164
|
+
if (playerData.id === this.playerId && this.player) {
|
|
165
|
+
this.player.updateData(playerData);
|
|
166
|
+
}
|
|
167
|
+
if (!this.connectedPlayerIds.has(playerData.id)) {
|
|
168
|
+
this.connectedPlayerIds.add(playerData.id);
|
|
169
|
+
this.signalEvent({
|
|
170
|
+
eventType: constants_1.EventType.PLAYER_ADDED,
|
|
171
|
+
objectId: playerData.id,
|
|
172
|
+
data: playerData,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async handleSocketConnected() {
|
|
178
|
+
try {
|
|
179
|
+
await this.bootstrap(true);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
this.logger.error?.('Failed to bootstrap after reconnect', err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
handleSocketDisconnected(reason) {
|
|
186
|
+
this.signalEvent({
|
|
187
|
+
eventType: constants_1.EventType.DISCONNECTED,
|
|
188
|
+
objectId: this.playerId,
|
|
189
|
+
data: reason,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
exports.SonosClient = SonosClient;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const LOCAL_API_TOKEN = "123e4567-e89b-12d3-a456-426655440000";
|
|
2
|
+
export declare const LOG_LEVEL_VERBOSE = 5;
|
|
3
|
+
export declare const API_VERSION = 1;
|
|
4
|
+
export declare enum EventType {
|
|
5
|
+
GROUP_ADDED = "group_added",
|
|
6
|
+
GROUP_UPDATED = "group_updated",
|
|
7
|
+
GROUP_REMOVED = "group_removed",
|
|
8
|
+
PLAYER_ADDED = "player_added",
|
|
9
|
+
PLAYER_UPDATED = "player_updated",
|
|
10
|
+
PLAYER_REMOVED = "player_removed",
|
|
11
|
+
CONNECTED = "connected",
|
|
12
|
+
DISCONNECTED = "disconnected",
|
|
13
|
+
MATCH_ALL = "match_all"
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventType = exports.API_VERSION = exports.LOG_LEVEL_VERBOSE = exports.LOCAL_API_TOKEN = void 0;
|
|
4
|
+
exports.LOCAL_API_TOKEN = '123e4567-e89b-12d3-a456-426655440000';
|
|
5
|
+
exports.LOG_LEVEL_VERBOSE = 5;
|
|
6
|
+
exports.API_VERSION = 1;
|
|
7
|
+
var EventType;
|
|
8
|
+
(function (EventType) {
|
|
9
|
+
EventType["GROUP_ADDED"] = "group_added";
|
|
10
|
+
EventType["GROUP_UPDATED"] = "group_updated";
|
|
11
|
+
EventType["GROUP_REMOVED"] = "group_removed";
|
|
12
|
+
EventType["PLAYER_ADDED"] = "player_added";
|
|
13
|
+
EventType["PLAYER_UPDATED"] = "player_updated";
|
|
14
|
+
EventType["PLAYER_REMOVED"] = "player_removed";
|
|
15
|
+
EventType["CONNECTED"] = "connected";
|
|
16
|
+
EventType["DISCONNECTED"] = "disconnected";
|
|
17
|
+
EventType["MATCH_ALL"] = "match_all";
|
|
18
|
+
})(EventType || (exports.EventType = EventType = {}));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class SonosError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class TransportError extends SonosError {
|
|
5
|
+
readonly causeError?: Error;
|
|
6
|
+
constructor(message: string, error?: Error);
|
|
7
|
+
}
|
|
8
|
+
export declare class ConnectionClosed extends TransportError {
|
|
9
|
+
}
|
|
10
|
+
export declare class CannotConnect extends TransportError {
|
|
11
|
+
}
|
|
12
|
+
export declare class ConnectionFailed extends TransportError {
|
|
13
|
+
constructor(error?: Error);
|
|
14
|
+
}
|
|
15
|
+
export declare class NotConnected extends SonosError {
|
|
16
|
+
}
|
|
17
|
+
export declare class InvalidState extends SonosError {
|
|
18
|
+
}
|
|
19
|
+
export declare class InvalidMessage extends SonosError {
|
|
20
|
+
}
|
|
21
|
+
export declare class FailedCommand extends SonosError {
|
|
22
|
+
readonly errorCode: string;
|
|
23
|
+
readonly details?: string;
|
|
24
|
+
constructor(errorCode: string, details?: string);
|
|
25
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FailedCommand = exports.InvalidMessage = exports.InvalidState = exports.NotConnected = exports.ConnectionFailed = exports.CannotConnect = exports.ConnectionClosed = exports.TransportError = exports.SonosError = void 0;
|
|
4
|
+
class SonosError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = this.constructor.name;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.SonosError = SonosError;
|
|
11
|
+
class TransportError extends SonosError {
|
|
12
|
+
causeError;
|
|
13
|
+
constructor(message, error) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.causeError = error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.TransportError = TransportError;
|
|
19
|
+
class ConnectionClosed extends TransportError {
|
|
20
|
+
}
|
|
21
|
+
exports.ConnectionClosed = ConnectionClosed;
|
|
22
|
+
class CannotConnect extends TransportError {
|
|
23
|
+
}
|
|
24
|
+
exports.CannotConnect = CannotConnect;
|
|
25
|
+
class ConnectionFailed extends TransportError {
|
|
26
|
+
constructor(error) {
|
|
27
|
+
super(error ? `${error.message}` : 'Connection failed.', error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.ConnectionFailed = ConnectionFailed;
|
|
31
|
+
class NotConnected extends SonosError {
|
|
32
|
+
}
|
|
33
|
+
exports.NotConnected = NotConnected;
|
|
34
|
+
class InvalidState extends SonosError {
|
|
35
|
+
}
|
|
36
|
+
exports.InvalidState = InvalidState;
|
|
37
|
+
class InvalidMessage extends SonosError {
|
|
38
|
+
}
|
|
39
|
+
exports.InvalidMessage = InvalidMessage;
|
|
40
|
+
class FailedCommand extends SonosError {
|
|
41
|
+
errorCode;
|
|
42
|
+
details;
|
|
43
|
+
constructor(errorCode, details) {
|
|
44
|
+
super(`Command failed: ${details ?? errorCode}`);
|
|
45
|
+
this.errorCode = errorCode;
|
|
46
|
+
this.details = details;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.FailedCommand = FailedCommand;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { SonosClient } from './client';
|
|
2
|
+
import { Container, ContainerType, Group, MetadataStatus, MusicService, PlayBackState, PlayModes, SessionStatus, Track } from './types';
|
|
3
|
+
import { PlaybackActionsWrapper, PlayModesWrapper } from './models';
|
|
4
|
+
export declare class SonosGroup {
|
|
5
|
+
private client;
|
|
6
|
+
private data;
|
|
7
|
+
activeSessionId: string | null;
|
|
8
|
+
private playbackStatus;
|
|
9
|
+
private playbackMetadata;
|
|
10
|
+
private playbackStatusUpdatedAt;
|
|
11
|
+
private volumeData;
|
|
12
|
+
private playModes;
|
|
13
|
+
private playbackActions;
|
|
14
|
+
private unsubscribeCallbacks;
|
|
15
|
+
constructor(client: SonosClient, data: Group);
|
|
16
|
+
init(): Promise<void>;
|
|
17
|
+
cleanup(): void;
|
|
18
|
+
get id(): string;
|
|
19
|
+
get name(): string;
|
|
20
|
+
get coordinatorId(): string;
|
|
21
|
+
get playerIds(): string[];
|
|
22
|
+
get areaIds(): string[];
|
|
23
|
+
get playbackState(): PlayBackState;
|
|
24
|
+
get playbackMetadataStatus(): MetadataStatus | null;
|
|
25
|
+
get playbackActionsWrapper(): PlaybackActionsWrapper;
|
|
26
|
+
get playModesWrapper(): PlayModesWrapper;
|
|
27
|
+
get positionSeconds(): number;
|
|
28
|
+
get isDucking(): boolean;
|
|
29
|
+
get activeService(): MusicService | string | null;
|
|
30
|
+
get containerType(): ContainerType | string | null;
|
|
31
|
+
play(): Promise<void>;
|
|
32
|
+
pause(): Promise<void>;
|
|
33
|
+
stop(): Promise<void>;
|
|
34
|
+
togglePlayPause(): Promise<void>;
|
|
35
|
+
skipToNextTrack(): Promise<void>;
|
|
36
|
+
skipToPreviousTrack(): Promise<void>;
|
|
37
|
+
setPlayModes(options: PlayModes): Promise<void>;
|
|
38
|
+
seek(positionMillis: number): Promise<void>;
|
|
39
|
+
seekRelative(deltaMillis: number): Promise<void>;
|
|
40
|
+
loadLineIn(deviceId?: string, playOnCompletion?: boolean): Promise<void>;
|
|
41
|
+
modifyGroupMembers(playerIdsToAdd: string[], playerIdsToRemove: string[]): Promise<void>;
|
|
42
|
+
setGroupMembers(playerIds: string[], areaIds?: string[]): Promise<void>;
|
|
43
|
+
createPlaybackSession(appId?: string, appContext?: string, accountId?: string, customData?: Record<string, unknown>): Promise<SessionStatus>;
|
|
44
|
+
playStreamUrl(url: string, metadata?: Container): Promise<void>;
|
|
45
|
+
playCloudQueue(queueBaseUrl: string, options: {
|
|
46
|
+
httpAuthorization?: string;
|
|
47
|
+
useHttpAuthorizationForMedia?: boolean;
|
|
48
|
+
itemId?: string;
|
|
49
|
+
queueVersion?: string;
|
|
50
|
+
positionMillis?: number;
|
|
51
|
+
trackMetadata?: Track;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
updateData(newData: Group): void;
|
|
54
|
+
private ensureSession;
|
|
55
|
+
private handlePlaybackStatusUpdate;
|
|
56
|
+
private handleMetadataUpdate;
|
|
57
|
+
private handleVolumeUpdate;
|
|
58
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SonosGroup = void 0;
|
|
4
|
+
const constants_1 = require("./constants");
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
const types_1 = require("./types");
|
|
7
|
+
const models_1 = require("./models");
|
|
8
|
+
class SonosGroup {
|
|
9
|
+
client;
|
|
10
|
+
data;
|
|
11
|
+
activeSessionId = null;
|
|
12
|
+
playbackStatus = null;
|
|
13
|
+
playbackMetadata = null;
|
|
14
|
+
playbackStatusUpdatedAt = 0;
|
|
15
|
+
volumeData = null;
|
|
16
|
+
playModes = new models_1.PlayModesWrapper({});
|
|
17
|
+
playbackActions = new models_1.PlaybackActionsWrapper({});
|
|
18
|
+
unsubscribeCallbacks = [];
|
|
19
|
+
constructor(client, data) {
|
|
20
|
+
this.client = client;
|
|
21
|
+
this.data = data;
|
|
22
|
+
}
|
|
23
|
+
async init() {
|
|
24
|
+
try {
|
|
25
|
+
this.volumeData = await this.client.api.groupVolume.getVolume(this.id);
|
|
26
|
+
this.playbackStatus = await this.client.api.playback.getPlaybackStatus(this.id);
|
|
27
|
+
this.playbackStatusUpdatedAt = Date.now();
|
|
28
|
+
this.playbackActions = new models_1.PlaybackActionsWrapper(this.playbackStatus.availablePlaybackActions ?? {});
|
|
29
|
+
this.playModes = new models_1.PlayModesWrapper(this.playbackStatus.playModes ?? {});
|
|
30
|
+
this.playbackMetadata = await this.client.api.playbackMetadata.getMetadataStatus(this.id);
|
|
31
|
+
this.unsubscribeCallbacks = [
|
|
32
|
+
await this.client.api.playback.subscribe(this.id, (data) => this.handlePlaybackStatusUpdate(data)),
|
|
33
|
+
await this.client.api.groupVolume.subscribe(this.id, (data) => this.handleVolumeUpdate(data)),
|
|
34
|
+
await this.client.api.playbackMetadata.subscribe(this.id, (data) => this.handleMetadataUpdate(data)),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err instanceof errors_1.FailedCommand && err.errorCode === 'groupCoordinatorChanged') {
|
|
39
|
+
this.volumeData = null;
|
|
40
|
+
this.playbackStatus = null;
|
|
41
|
+
this.playbackActions = new models_1.PlaybackActionsWrapper({});
|
|
42
|
+
this.playModes = new models_1.PlayModesWrapper({});
|
|
43
|
+
this.playbackMetadata = null;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
this.client.signalEvent({
|
|
49
|
+
eventType: constants_1.EventType.GROUP_ADDED,
|
|
50
|
+
objectId: this.id,
|
|
51
|
+
data: this,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
cleanup() {
|
|
55
|
+
for (const cb of this.unsubscribeCallbacks)
|
|
56
|
+
cb();
|
|
57
|
+
this.unsubscribeCallbacks = [];
|
|
58
|
+
}
|
|
59
|
+
get id() {
|
|
60
|
+
return this.data.id;
|
|
61
|
+
}
|
|
62
|
+
get name() {
|
|
63
|
+
return this.data.name;
|
|
64
|
+
}
|
|
65
|
+
get coordinatorId() {
|
|
66
|
+
return this.data.coordinatorId;
|
|
67
|
+
}
|
|
68
|
+
get playerIds() {
|
|
69
|
+
return this.data.playerIds;
|
|
70
|
+
}
|
|
71
|
+
get areaIds() {
|
|
72
|
+
return this.data.areaIds ?? [];
|
|
73
|
+
}
|
|
74
|
+
get playbackState() {
|
|
75
|
+
return (this.playbackStatus?.playbackState ??
|
|
76
|
+
this.data.playbackState ??
|
|
77
|
+
types_1.PlayBackState.IDLE);
|
|
78
|
+
}
|
|
79
|
+
get playbackMetadataStatus() {
|
|
80
|
+
return this.playbackMetadata;
|
|
81
|
+
}
|
|
82
|
+
get playbackActionsWrapper() {
|
|
83
|
+
return this.playbackActions;
|
|
84
|
+
}
|
|
85
|
+
get playModesWrapper() {
|
|
86
|
+
return this.playModes;
|
|
87
|
+
}
|
|
88
|
+
get positionSeconds() {
|
|
89
|
+
if (!this.playbackStatus)
|
|
90
|
+
return 0;
|
|
91
|
+
if (this.playbackState === types_1.PlayBackState.PLAYING) {
|
|
92
|
+
const elapsed = (Date.now() - this.playbackStatusUpdatedAt) / 1000;
|
|
93
|
+
return (this.playbackStatus.positionMillis ?? 0) / 1000 + elapsed;
|
|
94
|
+
}
|
|
95
|
+
return (this.playbackStatus.positionMillis ?? 0) / 1000;
|
|
96
|
+
}
|
|
97
|
+
get isDucking() {
|
|
98
|
+
return this.playbackStatus?.isDucking ?? false;
|
|
99
|
+
}
|
|
100
|
+
get activeService() {
|
|
101
|
+
return (0, models_1.normalizeMusicService)(this.playbackMetadata?.container?.id?.serviceId);
|
|
102
|
+
}
|
|
103
|
+
get containerType() {
|
|
104
|
+
return (0, models_1.normalizeContainerType)(this.playbackMetadata?.container?.type);
|
|
105
|
+
}
|
|
106
|
+
async play() {
|
|
107
|
+
await this.client.api.playback.play(this.id);
|
|
108
|
+
}
|
|
109
|
+
async pause() {
|
|
110
|
+
await this.client.api.playback.pause(this.id);
|
|
111
|
+
}
|
|
112
|
+
async stop() {
|
|
113
|
+
try {
|
|
114
|
+
if (this.activeSessionId) {
|
|
115
|
+
await this.client.api.playbackSession.suspend(this.activeSessionId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (!(err instanceof errors_1.FailedCommand))
|
|
120
|
+
throw err;
|
|
121
|
+
try {
|
|
122
|
+
await this.playStreamUrl('clear');
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// ignore
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
this.activeSessionId = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async togglePlayPause() {
|
|
133
|
+
await this.client.api.playback.togglePlayPause(this.id);
|
|
134
|
+
}
|
|
135
|
+
async skipToNextTrack() {
|
|
136
|
+
await this.client.api.playback.skipToNextTrack(this.id);
|
|
137
|
+
}
|
|
138
|
+
async skipToPreviousTrack() {
|
|
139
|
+
await this.client.api.playback.skipToPreviousTrack(this.id);
|
|
140
|
+
}
|
|
141
|
+
async setPlayModes(options) {
|
|
142
|
+
await this.client.api.playback.setPlayModes(this.id, options);
|
|
143
|
+
}
|
|
144
|
+
async seek(positionMillis) {
|
|
145
|
+
await this.client.api.playback.seek(this.id, positionMillis);
|
|
146
|
+
}
|
|
147
|
+
async seekRelative(deltaMillis) {
|
|
148
|
+
await this.client.api.playback.seekRelative(this.id, deltaMillis);
|
|
149
|
+
}
|
|
150
|
+
async loadLineIn(deviceId, playOnCompletion = false) {
|
|
151
|
+
await this.client.api.playback.loadLineIn(this.id, deviceId, playOnCompletion);
|
|
152
|
+
}
|
|
153
|
+
async modifyGroupMembers(playerIdsToAdd, playerIdsToRemove) {
|
|
154
|
+
await this.client.api.groups.modifyGroupMembers(this.id, playerIdsToAdd, playerIdsToRemove);
|
|
155
|
+
}
|
|
156
|
+
async setGroupMembers(playerIds, areaIds) {
|
|
157
|
+
await this.client.api.groups.setGroupMembers(this.id, playerIds, areaIds);
|
|
158
|
+
}
|
|
159
|
+
async createPlaybackSession(appId = 'com.lox.sonos.playback', appContext = '1', accountId, customData) {
|
|
160
|
+
const session = await this.client.api.playbackSession.createSession(this.id, appId, appContext, accountId, customData);
|
|
161
|
+
this.activeSessionId = session.sessionId;
|
|
162
|
+
return session;
|
|
163
|
+
}
|
|
164
|
+
async playStreamUrl(url, metadata) {
|
|
165
|
+
await this.ensureSession();
|
|
166
|
+
if (!this.activeSessionId)
|
|
167
|
+
throw new Error('No active session');
|
|
168
|
+
await this.client.api.playbackSession.loadStreamUrl(this.activeSessionId, url, {
|
|
169
|
+
playOnCompletion: true,
|
|
170
|
+
stationMetadata: metadata,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async playCloudQueue(queueBaseUrl, options) {
|
|
174
|
+
await this.ensureSession();
|
|
175
|
+
if (!this.activeSessionId)
|
|
176
|
+
throw new Error('No active session');
|
|
177
|
+
await this.client.api.playbackSession.loadCloudQueue(this.activeSessionId, queueBaseUrl, {
|
|
178
|
+
...options,
|
|
179
|
+
playOnCompletion: true,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
updateData(newData) {
|
|
183
|
+
let changed = false;
|
|
184
|
+
for (const [key, value] of Object.entries(newData)) {
|
|
185
|
+
if (this.data[key] !== value) {
|
|
186
|
+
this.data[key] = value;
|
|
187
|
+
changed = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (changed) {
|
|
191
|
+
this.client.signalEvent({
|
|
192
|
+
eventType: constants_1.EventType.GROUP_UPDATED,
|
|
193
|
+
objectId: this.id,
|
|
194
|
+
data: this,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async ensureSession() {
|
|
199
|
+
if (!this.activeSessionId) {
|
|
200
|
+
await this.createPlaybackSession();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
handlePlaybackStatusUpdate(data) {
|
|
204
|
+
this.playbackStatus = data;
|
|
205
|
+
this.playbackStatusUpdatedAt = Date.now();
|
|
206
|
+
this.playbackActions = new models_1.PlaybackActionsWrapper(data.availablePlaybackActions ?? {});
|
|
207
|
+
this.playModes = new models_1.PlayModesWrapper(data.playModes ?? {});
|
|
208
|
+
this.client.signalEvent({
|
|
209
|
+
eventType: constants_1.EventType.GROUP_UPDATED,
|
|
210
|
+
objectId: this.id,
|
|
211
|
+
data: this,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
handleMetadataUpdate(data) {
|
|
215
|
+
this.playbackMetadata = data;
|
|
216
|
+
this.client.signalEvent({
|
|
217
|
+
eventType: constants_1.EventType.GROUP_UPDATED,
|
|
218
|
+
objectId: this.id,
|
|
219
|
+
data: this,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
handleVolumeUpdate(data) {
|
|
223
|
+
this.volumeData = data;
|
|
224
|
+
this.client.signalEvent({
|
|
225
|
+
eventType: constants_1.EventType.GROUP_UPDATED,
|
|
226
|
+
objectId: this.id,
|
|
227
|
+
data: this,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
exports.SonosGroup = SonosGroup;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ContainerType, MusicService, PlaybackActions, PlayModes } from './types';
|
|
2
|
+
export declare class PlaybackActionsWrapper {
|
|
3
|
+
private raw;
|
|
4
|
+
constructor(raw: PlaybackActions);
|
|
5
|
+
get canSkipForward(): boolean;
|
|
6
|
+
get canSkipBackward(): boolean;
|
|
7
|
+
get canPlay(): boolean;
|
|
8
|
+
get canPause(): boolean;
|
|
9
|
+
get canStop(): boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class PlayModesWrapper {
|
|
12
|
+
raw: PlayModes;
|
|
13
|
+
constructor(raw: PlayModes);
|
|
14
|
+
get crossfade(): boolean | undefined;
|
|
15
|
+
get repeat(): boolean | undefined;
|
|
16
|
+
get repeatOne(): boolean | undefined;
|
|
17
|
+
get shuffle(): boolean | undefined;
|
|
18
|
+
}
|
|
19
|
+
export declare function normalizeContainerType(containerType?: string): ContainerType | string | null;
|
|
20
|
+
export declare function normalizeMusicService(serviceId?: string): MusicService | string | null;
|