alphatheta-connect 0.26.1 → 0.27.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/lib/cli.js +160 -32
- package/lib/cli.js.map +1 -1
- package/lib/control/index.d.ts +18 -2
- package/lib/control/load.d.ts +29 -0
- package/lib/control/stagehand.d.ts +14 -4
- package/lib/index.d.ts +5 -0
- package/lib/index.js +945 -33
- package/lib/index.js.map +1 -1
- package/lib/network.d.ts +7 -1
- package/lib/remote/hue.d.ts +66 -0
- package/lib/remote/index.d.ts +100 -0
- package/lib/remote/server.d.ts +33 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -700,14 +700,17 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
700
700
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
701
701
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
702
702
|
};
|
|
703
|
-
var _Control_hostDevice, _Control_beatSocket, _Control_correlationByte;
|
|
703
|
+
var _Control_hostDevice, _Control_beatSocket, _Control_statusSocket, _Control_correlationByte;
|
|
704
704
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
705
|
-
exports.makePlaystatePacket = void 0;
|
|
705
|
+
exports.makePlaystatePacket = exports.makeLoadTrackPacket = void 0;
|
|
706
706
|
const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
|
|
707
707
|
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
708
708
|
const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
|
|
709
709
|
const udp_1 = __webpack_require__(/*! src/utils/udp */ "./src/utils/udp.ts");
|
|
710
|
+
const load_1 = __webpack_require__(/*! ./load */ "./src/control/load.ts");
|
|
710
711
|
const stagehand_1 = __webpack_require__(/*! ./stagehand */ "./src/control/stagehand.ts");
|
|
712
|
+
var load_2 = __webpack_require__(/*! ./load */ "./src/control/load.ts");
|
|
713
|
+
Object.defineProperty(exports, "makeLoadTrackPacket", ({ enumerable: true, get: function () { return load_2.makeLoadTrackPacket; } }));
|
|
711
714
|
const STATE_MAP = {
|
|
712
715
|
[types_1.CDJStatus.PlayState.Cued]: 0x01,
|
|
713
716
|
[types_1.CDJStatus.PlayState.Playing]: 0x00,
|
|
@@ -728,20 +731,51 @@ const makePlaystatePacket = ({ hostDevice, device, playState }) => Uint8Array.fr
|
|
|
728
731
|
]);
|
|
729
732
|
exports.makePlaystatePacket = makePlaystatePacket;
|
|
730
733
|
class Control {
|
|
731
|
-
constructor(beatSocket, hostDevice) {
|
|
734
|
+
constructor(beatSocket, hostDevice, statusSocket = null) {
|
|
732
735
|
_Control_hostDevice.set(this, void 0);
|
|
733
736
|
/**
|
|
734
737
|
* The socket used to send control packets
|
|
735
738
|
*/
|
|
736
739
|
_Control_beatSocket.set(this, void 0);
|
|
740
|
+
/**
|
|
741
|
+
* The socket bound to the status port, which the Load Track command must
|
|
742
|
+
* leave from: a CDJ-3000 ignores it from any other port. Absent when the
|
|
743
|
+
* control service was built without one.
|
|
744
|
+
*/
|
|
745
|
+
_Control_statusSocket.set(this, void 0);
|
|
737
746
|
/**
|
|
738
747
|
* Randomized correlation byte per session for Stagehand commands
|
|
739
748
|
*/
|
|
740
749
|
_Control_correlationByte.set(this, void 0);
|
|
741
750
|
__classPrivateFieldSet(this, _Control_beatSocket, beatSocket, "f");
|
|
751
|
+
__classPrivateFieldSet(this, _Control_statusSocket, statusSocket, "f");
|
|
742
752
|
__classPrivateFieldSet(this, _Control_hostDevice, hostDevice, "f");
|
|
743
753
|
__classPrivateFieldSet(this, _Control_correlationByte, Math.floor(Math.random() * 256), "f");
|
|
744
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* Send one Stagehand transport packet (0x07) carrying `op` with the press /
|
|
757
|
+
* release flag, for opcodes the named methods do not cover.
|
|
758
|
+
*/
|
|
759
|
+
async transport(device, op, press) {
|
|
760
|
+
const p = (0, stagehand_1.makeStagehandTransportPacket)(__classPrivateFieldGet(this, _Control_hostDevice, "f"), op, press, __classPrivateFieldGet(this, _Control_correlationByte, "f"));
|
|
761
|
+
await (0, udp_1.udpSend)(__classPrivateFieldGet(this, _Control_beatSocket, "f"), p, constants_1.BEAT_PORT, device.ip.address);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Tell a player to load a track, as rekordbox does when a row is dragged
|
|
765
|
+
* onto a deck: the Load Track command (0x19) from our status port to the
|
|
766
|
+
* player's. The player answers 0x1a and the load shows in its next status.
|
|
767
|
+
*
|
|
768
|
+
* `source` names where the track lives: a device number and its slot (a
|
|
769
|
+
* player's USB or SD, or the rekordbox slot of a library served over the
|
|
770
|
+
* link).
|
|
771
|
+
*/
|
|
772
|
+
async loadTrack(device, trackId, source) {
|
|
773
|
+
if (__classPrivateFieldGet(this, _Control_statusSocket, "f") === null) {
|
|
774
|
+
throw new Error('loadTrack needs the status socket; connect through ProlinkNetwork');
|
|
775
|
+
}
|
|
776
|
+
const p = (0, load_1.makeLoadTrackPacket)(__classPrivateFieldGet(this, _Control_hostDevice, "f"), device, trackId, source);
|
|
777
|
+
await (0, udp_1.udpSend)(__classPrivateFieldGet(this, _Control_statusSocket, "f"), p, constants_1.STATUS_PORT, device.ip.address);
|
|
778
|
+
}
|
|
745
779
|
/**
|
|
746
780
|
* Start or stop a CDJ on the network.
|
|
747
781
|
* Delegates automatically to Stagehand-specific transport control if connected in Stagehand mode.
|
|
@@ -807,10 +841,79 @@ class Control {
|
|
|
807
841
|
await (0, udp_1.udpSend)(__classPrivateFieldGet(this, _Control_beatSocket, "f"), p, constants_1.STATUS_PORT, device.ip.address);
|
|
808
842
|
}
|
|
809
843
|
}
|
|
810
|
-
_Control_hostDevice = new WeakMap(), _Control_beatSocket = new WeakMap(), _Control_correlationByte = new WeakMap();
|
|
844
|
+
_Control_hostDevice = new WeakMap(), _Control_beatSocket = new WeakMap(), _Control_statusSocket = new WeakMap(), _Control_correlationByte = new WeakMap();
|
|
811
845
|
exports["default"] = Control;
|
|
812
846
|
|
|
813
847
|
|
|
848
|
+
/***/ },
|
|
849
|
+
|
|
850
|
+
/***/ "./src/control/load.ts"
|
|
851
|
+
/*!*****************************!*\
|
|
852
|
+
!*** ./src/control/load.ts ***!
|
|
853
|
+
\*****************************/
|
|
854
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
855
|
+
|
|
856
|
+
"use strict";
|
|
857
|
+
|
|
858
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
859
|
+
exports.LOAD_TRACK_LENGTH = void 0;
|
|
860
|
+
exports.makeLoadTrackPacket = makeLoadTrackPacket;
|
|
861
|
+
const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
|
|
862
|
+
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
863
|
+
const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
|
|
864
|
+
/**
|
|
865
|
+
* Byte length of a Load Track command.
|
|
866
|
+
*/
|
|
867
|
+
exports.LOAD_TRACK_LENGTH = 0x58;
|
|
868
|
+
/**
|
|
869
|
+
* Generates the Load Track command (0x19, 88 bytes), unicast to a player's
|
|
870
|
+
* status port.
|
|
871
|
+
*
|
|
872
|
+
* Byte for byte what rekordbox 7.2 sends a CDJ-3000 (captured 2026-09-13 and
|
|
873
|
+
* verified live on two CDJ-3000s): the status-style header with a 20-byte
|
|
874
|
+
* name, `01 01`, our device number, the length `0034` of what follows, our
|
|
875
|
+
* number again, then the source device, slot and track type, the track ID
|
|
876
|
+
* big-endian, `32` at 0x33, the destination player counted from zero at
|
|
877
|
+
* 0x40, and `32` at 0x4b.
|
|
878
|
+
*
|
|
879
|
+
* A player only honours the command from the address its keep-alives
|
|
880
|
+
* announced and from port 50002.
|
|
881
|
+
*/
|
|
882
|
+
function makeLoadTrackPacket(hostDevice, device, trackId, source) {
|
|
883
|
+
var _a;
|
|
884
|
+
const packet = new Uint8Array(exports.LOAD_TRACK_LENGTH);
|
|
885
|
+
// 0-9: magic header
|
|
886
|
+
packet.set(constants_1.PROLINK_HEADER, 0);
|
|
887
|
+
// 10: kind 0x19
|
|
888
|
+
packet[10] = 0x19;
|
|
889
|
+
// 11-30: device name (20 bytes)
|
|
890
|
+
packet.set((0, utils_1.buildName)(hostDevice), 11);
|
|
891
|
+
// 31-35: 01, subtype 01, our number, then the length 0x0034 of what follows
|
|
892
|
+
packet[31] = 0x01;
|
|
893
|
+
packet[32] = 0x01;
|
|
894
|
+
packet[33] = hostDevice.id;
|
|
895
|
+
packet[34] = 0x00;
|
|
896
|
+
packet[35] = 0x34;
|
|
897
|
+
// 36: our number again
|
|
898
|
+
packet[36] = hostDevice.id;
|
|
899
|
+
// 40-42: the source device, its slot and the track type
|
|
900
|
+
packet[40] = source.deviceId;
|
|
901
|
+
packet[41] = source.slot;
|
|
902
|
+
packet[42] = (_a = source.trackType) !== null && _a !== void 0 ? _a : types_1.TrackType.RB;
|
|
903
|
+
// 44-47: the track ID, big-endian
|
|
904
|
+
packet[44] = (trackId >>> 24) & 0xff;
|
|
905
|
+
packet[45] = (trackId >>> 16) & 0xff;
|
|
906
|
+
packet[46] = (trackId >>> 8) & 0xff;
|
|
907
|
+
packet[47] = trackId & 0xff;
|
|
908
|
+
packet[0x33] = 0x32;
|
|
909
|
+
// 0x40: the player to load onto, counted from zero; a player accepts the
|
|
910
|
+
// command by address regardless
|
|
911
|
+
packet[0x40] = Math.max(device.id - 1, 0);
|
|
912
|
+
packet[0x4b] = 0x32;
|
|
913
|
+
return packet;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
|
|
814
917
|
/***/ },
|
|
815
918
|
|
|
816
919
|
/***/ "./src/control/stagehand.ts"
|
|
@@ -827,59 +930,75 @@ exports.makeStagehandPrefWritePacket = makeStagehandPrefWritePacket;
|
|
|
827
930
|
const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
|
|
828
931
|
const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
|
|
829
932
|
/**
|
|
830
|
-
*
|
|
933
|
+
* The name field of a Stagehand unicast frame: 19 bytes, one shorter than the
|
|
934
|
+
* broadcast frames' 20, so the bytes that follow it sit at offset 30.
|
|
935
|
+
*/
|
|
936
|
+
function buildUnicastName(device) {
|
|
937
|
+
return (0, utils_1.buildName)(device).subarray(0, 19);
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Generates a Stagehand transport control packet (0x07, 48 bytes).
|
|
941
|
+
*
|
|
942
|
+
* Byte for byte what the iPad app sends a CDJ-3000 (captured 2026-05-23,
|
|
943
|
+
* `stagehand-allproto-20260523T142615.pcap`, pinned in the tests): the
|
|
944
|
+
* unicast header `03 01 00`, a byte that varies per command, the length
|
|
945
|
+
* `0030`, then `3a`, `01`, the opcode and the press flag each in their own
|
|
946
|
+
* 16-bit slot.
|
|
831
947
|
*
|
|
832
948
|
* @param hostDevice - The Stagehand device posing as sender
|
|
833
|
-
* @param op - The command opcode (e.g. 0x0f, 0x14, 0x18, 0x1a, 0x1b)
|
|
949
|
+
* @param op - The command opcode (e.g. 0x0f, 0x14, 0x18, 0x19, 0x1a, 0x1b)
|
|
834
950
|
* @param press - Whether the action is press (true) or release (false)
|
|
835
|
-
* @param correlationByte - The
|
|
951
|
+
* @param correlationByte - The per-session correlation byte
|
|
836
952
|
*/
|
|
837
953
|
function makeStagehandTransportPacket(hostDevice, op, press, correlationByte) {
|
|
838
|
-
const packet = new Uint8Array(
|
|
954
|
+
const packet = new Uint8Array(48);
|
|
839
955
|
// 0-9: magic header
|
|
840
956
|
packet.set(constants_1.PROLINK_HEADER, 0);
|
|
841
957
|
// 10: opcode 0x07
|
|
842
958
|
packet[10] = 0x07;
|
|
843
|
-
// 11-
|
|
844
|
-
packet.set((
|
|
845
|
-
//
|
|
959
|
+
// 11-29: device name
|
|
960
|
+
packet.set(buildUnicastName(hostDevice), 11);
|
|
961
|
+
// 30-32: unicast header 03 01 00
|
|
962
|
+
packet[30] = 0x03;
|
|
846
963
|
packet[31] = 0x01;
|
|
847
|
-
|
|
848
|
-
packet[32] = 0x03;
|
|
964
|
+
packet[32] = 0x00;
|
|
849
965
|
// 33: per-session correlation byte
|
|
850
966
|
packet[33] = correlationByte;
|
|
851
|
-
// 34-35:
|
|
967
|
+
// 34-35: packet length 0x0030 (48 bytes)
|
|
852
968
|
packet[34] = 0x00;
|
|
853
969
|
packet[35] = 0x30;
|
|
854
|
-
//
|
|
855
|
-
packet[
|
|
856
|
-
//
|
|
857
|
-
packet[
|
|
858
|
-
//
|
|
859
|
-
packet[
|
|
970
|
+
// 39: Stagehand sub-id 0x3a
|
|
971
|
+
packet[39] = 0x3a;
|
|
972
|
+
// 41: 0x01
|
|
973
|
+
packet[41] = 0x01;
|
|
974
|
+
// 43: command opcode
|
|
975
|
+
packet[43] = op;
|
|
976
|
+
// 45: press/release flag
|
|
977
|
+
packet[45] = press ? 0x01 : 0x00;
|
|
860
978
|
return packet;
|
|
861
979
|
}
|
|
862
980
|
/**
|
|
863
|
-
* Generates a Stagehand preference write packet (0x6b,
|
|
981
|
+
* Generates a Stagehand preference write packet (0x6b, 116 bytes).
|
|
982
|
+
*
|
|
983
|
+
* As captured from the iPad app (same capture as the transport packet): the
|
|
984
|
+
* unicast header `03 01 00 3a`, the length `0050`, the write flag, then the
|
|
985
|
+
* preference slots.
|
|
864
986
|
*
|
|
865
987
|
* @param hostDevice - The Stagehand device posing as sender
|
|
866
988
|
* @param options - The preferences to write (onAir, quantize)
|
|
867
989
|
*/
|
|
868
990
|
function makeStagehandPrefWritePacket(hostDevice, options) {
|
|
869
|
-
const packet = new Uint8Array(
|
|
991
|
+
const packet = new Uint8Array(116);
|
|
870
992
|
// 0-9: magic header
|
|
871
993
|
packet.set(constants_1.PROLINK_HEADER, 0);
|
|
872
994
|
// 10: opcode 0x6b
|
|
873
995
|
packet[10] = 0x6b;
|
|
874
|
-
// 11-
|
|
875
|
-
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
packet.set(name, 11);
|
|
879
|
-
// 31: 0x01 (subscription-id-a constant)
|
|
996
|
+
// 11-29: device name
|
|
997
|
+
packet.set(buildUnicastName(hostDevice), 11);
|
|
998
|
+
// 30-32: unicast header 03 01 00
|
|
999
|
+
packet[30] = 0x03;
|
|
880
1000
|
packet[31] = 0x01;
|
|
881
|
-
|
|
882
|
-
packet[32] = 0x03;
|
|
1001
|
+
packet[32] = 0x00;
|
|
883
1002
|
// 33: Stagehand sub-id constant 0x3a
|
|
884
1003
|
packet[33] = 0x3a;
|
|
885
1004
|
// 34-35: body length 0x0050 (80 bytes)
|
|
@@ -2137,10 +2256,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
2137
2256
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
2138
2257
|
};
|
|
2139
2258
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2140
|
-
exports.noopLogger = exports.REMOTEDB_MAX_DEVICE_ID = exports.playerNumberCeiling = exports.pickRemoteDbQueryId = exports.pickAvailableDeviceId = exports.MIXER_PLAYER_CEILING = exports.MAX_DEVICE_ID = exports.DEFAULT_MIXER_PLAYER_CEILING = exports.OneLibraryAdapter = exports.fetchFile = exports.loadAnlz = exports.isMetadataExtractionSupported = exports.extractMetadataFromDevice = exports.extractFullMetadata = exports.PictureType = exports.isArtworkExtractionSupported = exports.extractArtworkFromDevice = exports.extractArtwork = exports.PositionEmitter = void 0;
|
|
2259
|
+
exports.noopLogger = exports.REMOTEDB_MAX_DEVICE_ID = exports.playerNumberCeiling = exports.pickRemoteDbQueryId = exports.pickAvailableDeviceId = exports.MIXER_PLAYER_CEILING = exports.MAX_DEVICE_ID = exports.DEFAULT_MIXER_PLAYER_CEILING = exports.OneLibraryAdapter = exports.fetchFile = exports.loadAnlz = exports.isMetadataExtractionSupported = exports.extractMetadataFromDevice = exports.extractFullMetadata = exports.PictureType = exports.isArtworkExtractionSupported = exports.extractArtworkFromDevice = exports.extractArtwork = exports.PositionEmitter = exports.createRemoteServer = exports.Control = void 0;
|
|
2260
|
+
var control_1 = __webpack_require__(/*! ./control */ "./src/control/index.ts");
|
|
2261
|
+
Object.defineProperty(exports, "Control", ({ enumerable: true, get: function () { return __importDefault(control_1).default; } }));
|
|
2141
2262
|
__exportStar(__webpack_require__(/*! ./entities */ "./src/entities.ts"), exports);
|
|
2142
2263
|
__exportStar(__webpack_require__(/*! ./mixstatus */ "./src/mixstatus/index.ts"), exports);
|
|
2143
2264
|
__exportStar(__webpack_require__(/*! ./network */ "./src/network.ts"), exports);
|
|
2265
|
+
__exportStar(__webpack_require__(/*! ./remote */ "./src/remote/index.ts"), exports);
|
|
2266
|
+
__exportStar(__webpack_require__(/*! ./remote/hue */ "./src/remote/hue.ts"), exports);
|
|
2267
|
+
var server_1 = __webpack_require__(/*! ./remote/server */ "./src/remote/server.ts");
|
|
2268
|
+
Object.defineProperty(exports, "createRemoteServer", ({ enumerable: true, get: function () { return server_1.createServer; } }));
|
|
2144
2269
|
var position_1 = __webpack_require__(/*! ./status/position */ "./src/status/position.ts");
|
|
2145
2270
|
Object.defineProperty(exports, "PositionEmitter", ({ enumerable: true, get: function () { return __importDefault(position_1).default; } }));
|
|
2146
2271
|
// Passive mode (pcap-based monitoring without announcing a VCDJ)
|
|
@@ -7252,9 +7377,9 @@ class ProlinkNetwork {
|
|
|
7252
7377
|
// Create unified database
|
|
7253
7378
|
const database = new db_1.default(localdb, remotedb, __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_logger, "f"));
|
|
7254
7379
|
// Create controller service
|
|
7255
|
-
const control = new control_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_beatSocket, "f"), vcdj);
|
|
7380
|
+
const control = new control_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_beatSocket, "f"), vcdj, __classPrivateFieldGet(this, _ProlinkNetwork_statusSocket, "f"));
|
|
7256
7381
|
__classPrivateFieldSet(this, _ProlinkNetwork_state, types_1.NetworkState.Connected, "f");
|
|
7257
|
-
__classPrivateFieldSet(this, _ProlinkNetwork_connection, { announcer, heartbeat, control, remotedb, localdb, database }, "f");
|
|
7382
|
+
__classPrivateFieldSet(this, _ProlinkNetwork_connection, { vcdj, announcer, heartbeat, control, remotedb, localdb, database }, "f");
|
|
7258
7383
|
tx.finish();
|
|
7259
7384
|
}
|
|
7260
7385
|
/**
|
|
@@ -7341,6 +7466,15 @@ class ProlinkNetwork {
|
|
|
7341
7466
|
get positionEmitter() {
|
|
7342
7467
|
return __classPrivateFieldGet(this, _ProlinkNetwork_state, "f") === types_1.NetworkState.Connected ? __classPrivateFieldGet(this, _ProlinkNetwork_positionEmitter, "f") : null;
|
|
7343
7468
|
}
|
|
7469
|
+
/**
|
|
7470
|
+
* The device this network announces itself as, once connected: the
|
|
7471
|
+
* `hostDevice` the status emitter's media query and the control service
|
|
7472
|
+
* send from.
|
|
7473
|
+
*/
|
|
7474
|
+
get virtualDevice() {
|
|
7475
|
+
var _a, _b;
|
|
7476
|
+
return (_b = (_a = __classPrivateFieldGet(this, _ProlinkNetwork_connection, "f")) === null || _a === void 0 ? void 0 : _a.vcdj) !== null && _b !== void 0 ? _b : null;
|
|
7477
|
+
}
|
|
7344
7478
|
/**
|
|
7345
7479
|
* Get the {@link Control} service. This service can be used to control the
|
|
7346
7480
|
* Playstate of CDJs on the network.
|
|
@@ -10551,6 +10685,773 @@ _PassiveStatusEmitter_emitter = new WeakMap(), _PassiveStatusEmitter_adapter = n
|
|
|
10551
10685
|
exports["default"] = PassiveStatusEmitter;
|
|
10552
10686
|
|
|
10553
10687
|
|
|
10688
|
+
/***/ },
|
|
10689
|
+
|
|
10690
|
+
/***/ "./src/remote/hue.ts"
|
|
10691
|
+
/*!***************************!*\
|
|
10692
|
+
!*** ./src/remote/hue.ts ***!
|
|
10693
|
+
\***************************/
|
|
10694
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
10695
|
+
|
|
10696
|
+
"use strict";
|
|
10697
|
+
|
|
10698
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
10699
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
10700
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
10701
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
10702
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
10703
|
+
};
|
|
10704
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
10705
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
10706
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10707
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
10708
|
+
};
|
|
10709
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10710
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10711
|
+
};
|
|
10712
|
+
var _Hue_instances, _Hue_config, _Hue_url;
|
|
10713
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
10714
|
+
exports.Hue = exports.HueNoGroupError = exports.HueUnconfiguredError = exports.HueError = void 0;
|
|
10715
|
+
exports.hueConfigPath = hueConfigPath;
|
|
10716
|
+
exports.loadHueConfig = loadHueConfig;
|
|
10717
|
+
exports.saveHueConfig = saveHueConfig;
|
|
10718
|
+
exports.pairHue = pairHue;
|
|
10719
|
+
exports.discoverHueBridge = discoverHueBridge;
|
|
10720
|
+
/**
|
|
10721
|
+
* The Hue bridge: the room or zone the booth's gear is plugged into, switched
|
|
10722
|
+
* as one group over the bridge's v1 API (plain HTTP, no certificate dance).
|
|
10723
|
+
*
|
|
10724
|
+
* The application key comes from `hue pair`, which asks the bridge for one
|
|
10725
|
+
* while its link button is pressed, and is kept in a config file.
|
|
10726
|
+
*/
|
|
10727
|
+
const fs_1 = __webpack_require__(/*! fs */ "fs");
|
|
10728
|
+
const os_1 = __webpack_require__(/*! os */ "os");
|
|
10729
|
+
const path_1 = __importDefault(__webpack_require__(/*! path */ "path"));
|
|
10730
|
+
class HueError extends Error {
|
|
10731
|
+
}
|
|
10732
|
+
exports.HueError = HueError;
|
|
10733
|
+
/**
|
|
10734
|
+
* Raised when nothing has been paired and the environment names no bridge.
|
|
10735
|
+
*/
|
|
10736
|
+
class HueUnconfiguredError extends HueError {
|
|
10737
|
+
constructor() {
|
|
10738
|
+
super('Hue is not configured: run `alphatheta-remote hue pair` (or set HUE_BRIDGE and HUE_APP_KEY)');
|
|
10739
|
+
}
|
|
10740
|
+
}
|
|
10741
|
+
exports.HueUnconfiguredError = HueUnconfiguredError;
|
|
10742
|
+
/**
|
|
10743
|
+
* Raised when the bridge has no group by the name asked for.
|
|
10744
|
+
*/
|
|
10745
|
+
class HueNoGroupError extends HueError {
|
|
10746
|
+
constructor(name, groups) {
|
|
10747
|
+
const have = groups.map(g => JSON.stringify(g.name)).join(', ');
|
|
10748
|
+
super(`no Hue group named ${JSON.stringify(name)}; the bridge has: ${have}`);
|
|
10749
|
+
}
|
|
10750
|
+
}
|
|
10751
|
+
exports.HueNoGroupError = HueNoGroupError;
|
|
10752
|
+
const REQUEST_TIMEOUT = 10000;
|
|
10753
|
+
function hueConfigPath() {
|
|
10754
|
+
var _a, _b;
|
|
10755
|
+
const base = (_a = process.env.XDG_CONFIG_HOME) !== null && _a !== void 0 ? _a : path_1.default.join((_b = process.env.HOME) !== null && _b !== void 0 ? _b : '.', '.config');
|
|
10756
|
+
return path_1.default.join(base, 'alphatheta-remote', 'hue.json');
|
|
10757
|
+
}
|
|
10758
|
+
/**
|
|
10759
|
+
* The environment first (`HUE_BRIDGE`, `HUE_APP_KEY`), then the file `hue
|
|
10760
|
+
* pair` wrote; `null` when neither is there.
|
|
10761
|
+
*/
|
|
10762
|
+
async function loadHueConfig() {
|
|
10763
|
+
const { HUE_BRIDGE, HUE_APP_KEY } = process.env;
|
|
10764
|
+
if (HUE_BRIDGE && HUE_APP_KEY) {
|
|
10765
|
+
return { bridge: HUE_BRIDGE, appKey: HUE_APP_KEY };
|
|
10766
|
+
}
|
|
10767
|
+
try {
|
|
10768
|
+
const text = await fs_1.promises.readFile(hueConfigPath(), 'utf8');
|
|
10769
|
+
const parsed = JSON.parse(text);
|
|
10770
|
+
if (typeof parsed.bridge !== 'string' || typeof parsed.appKey !== 'string') {
|
|
10771
|
+
throw new HueError(`${hueConfigPath()}: expected {bridge, appKey}`);
|
|
10772
|
+
}
|
|
10773
|
+
return { bridge: parsed.bridge, appKey: parsed.appKey };
|
|
10774
|
+
}
|
|
10775
|
+
catch (e) {
|
|
10776
|
+
if (e.code === 'ENOENT') {
|
|
10777
|
+
return null;
|
|
10778
|
+
}
|
|
10779
|
+
throw e;
|
|
10780
|
+
}
|
|
10781
|
+
}
|
|
10782
|
+
async function saveHueConfig(config) {
|
|
10783
|
+
const file = hueConfigPath();
|
|
10784
|
+
await fs_1.promises.mkdir(path_1.default.dirname(file), { recursive: true });
|
|
10785
|
+
await fs_1.promises.writeFile(file, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
10786
|
+
return file;
|
|
10787
|
+
}
|
|
10788
|
+
async function request(url, init) {
|
|
10789
|
+
const response = await fetch(url, {
|
|
10790
|
+
...init,
|
|
10791
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT),
|
|
10792
|
+
});
|
|
10793
|
+
if (!response.ok) {
|
|
10794
|
+
throw new HueError(`hue bridge: HTTP ${response.status} ${await response.text()}`);
|
|
10795
|
+
}
|
|
10796
|
+
return (await response.json());
|
|
10797
|
+
}
|
|
10798
|
+
/**
|
|
10799
|
+
* A v1 reply is a list of `{success: …}` / `{error: …}` items; any error
|
|
10800
|
+
* item fails the call.
|
|
10801
|
+
*/
|
|
10802
|
+
function bridgeErrors(reply) {
|
|
10803
|
+
if (!Array.isArray(reply)) {
|
|
10804
|
+
return;
|
|
10805
|
+
}
|
|
10806
|
+
const errors = reply
|
|
10807
|
+
.map(item => { var _a; return (_a = item === null || item === void 0 ? void 0 : item.error) === null || _a === void 0 ? void 0 : _a.description; })
|
|
10808
|
+
.filter((d) => typeof d === 'string');
|
|
10809
|
+
if (errors.length > 0) {
|
|
10810
|
+
throw new HueError(`hue bridge answered: ${errors.join('; ')}`);
|
|
10811
|
+
}
|
|
10812
|
+
}
|
|
10813
|
+
class Hue {
|
|
10814
|
+
constructor(config) {
|
|
10815
|
+
_Hue_instances.add(this);
|
|
10816
|
+
_Hue_config.set(this, void 0);
|
|
10817
|
+
__classPrivateFieldSet(this, _Hue_config, config, "f");
|
|
10818
|
+
}
|
|
10819
|
+
get bridge() {
|
|
10820
|
+
return __classPrivateFieldGet(this, _Hue_config, "f").bridge;
|
|
10821
|
+
}
|
|
10822
|
+
/**
|
|
10823
|
+
* Every room and zone on the bridge, by name.
|
|
10824
|
+
*/
|
|
10825
|
+
async groups() {
|
|
10826
|
+
const raw = await request(__classPrivateFieldGet(this, _Hue_instances, "m", _Hue_url).call(this, 'groups'));
|
|
10827
|
+
bridgeErrors(raw);
|
|
10828
|
+
return Object.entries(raw)
|
|
10829
|
+
.map(([id, g]) => {
|
|
10830
|
+
var _a, _b, _c, _d, _e;
|
|
10831
|
+
return ({
|
|
10832
|
+
id,
|
|
10833
|
+
name: g.name,
|
|
10834
|
+
type: g.type,
|
|
10835
|
+
lights: (_a = g.lights) !== null && _a !== void 0 ? _a : [],
|
|
10836
|
+
allOn: (_c = (_b = g.state) === null || _b === void 0 ? void 0 : _b.all_on) !== null && _c !== void 0 ? _c : false,
|
|
10837
|
+
anyOn: (_e = (_d = g.state) === null || _d === void 0 ? void 0 : _d.any_on) !== null && _e !== void 0 ? _e : false,
|
|
10838
|
+
});
|
|
10839
|
+
})
|
|
10840
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
10841
|
+
}
|
|
10842
|
+
/**
|
|
10843
|
+
* The group called `name`, matched without regard to case.
|
|
10844
|
+
*/
|
|
10845
|
+
async groupNamed(name) {
|
|
10846
|
+
const groups = await this.groups();
|
|
10847
|
+
const wanted = name.trim().toLowerCase();
|
|
10848
|
+
const group = groups.find(g => g.name.trim().toLowerCase() === wanted);
|
|
10849
|
+
if (group === undefined) {
|
|
10850
|
+
throw new HueNoGroupError(name, groups);
|
|
10851
|
+
}
|
|
10852
|
+
return group;
|
|
10853
|
+
}
|
|
10854
|
+
/**
|
|
10855
|
+
* Switch every light (and plug) in the group.
|
|
10856
|
+
*/
|
|
10857
|
+
async setGroupOn(groupId, on) {
|
|
10858
|
+
const reply = await request(__classPrivateFieldGet(this, _Hue_instances, "m", _Hue_url).call(this, `groups/${groupId}/action`), {
|
|
10859
|
+
method: 'PUT',
|
|
10860
|
+
body: JSON.stringify({ on }),
|
|
10861
|
+
});
|
|
10862
|
+
bridgeErrors(reply);
|
|
10863
|
+
}
|
|
10864
|
+
}
|
|
10865
|
+
exports.Hue = Hue;
|
|
10866
|
+
_Hue_config = new WeakMap(), _Hue_instances = new WeakSet(), _Hue_url = function _Hue_url(tail) {
|
|
10867
|
+
return `http://${__classPrivateFieldGet(this, _Hue_config, "f").bridge}/api/${__classPrivateFieldGet(this, _Hue_config, "f").appKey}/${tail}`;
|
|
10868
|
+
};
|
|
10869
|
+
/**
|
|
10870
|
+
* Ask the bridge for an application key. The bridge only issues one in the
|
|
10871
|
+
* thirty seconds after its link button is pressed, so this asks once a second
|
|
10872
|
+
* until it does or `waitMs` runs out.
|
|
10873
|
+
*/
|
|
10874
|
+
async function pairHue(bridge, waitMs = 60000) {
|
|
10875
|
+
var _a, _b, _c, _d;
|
|
10876
|
+
const deadline = Date.now() + waitMs;
|
|
10877
|
+
// The bridge takes `<application>#<device>` with the device name at most
|
|
10878
|
+
// 19 characters, so the host's name is cut at its first dot and there.
|
|
10879
|
+
const host = (0, os_1.hostname)().split('.')[0].slice(0, 19);
|
|
10880
|
+
const body = JSON.stringify({ devicetype: `alphatheta-remote#${host}` });
|
|
10881
|
+
for (;;) {
|
|
10882
|
+
const reply = await request(`http://${bridge}/api`, { method: 'POST', body });
|
|
10883
|
+
const key = (_b = (_a = reply === null || reply === void 0 ? void 0 : reply[0]) === null || _a === void 0 ? void 0 : _a.success) === null || _b === void 0 ? void 0 : _b.username;
|
|
10884
|
+
if (typeof key === 'string') {
|
|
10885
|
+
return { bridge, appKey: key };
|
|
10886
|
+
}
|
|
10887
|
+
const linkButtonNotPressed = ((_d = (_c = reply === null || reply === void 0 ? void 0 : reply[0]) === null || _c === void 0 ? void 0 : _c.error) === null || _d === void 0 ? void 0 : _d.type) === 101;
|
|
10888
|
+
if (!linkButtonNotPressed) {
|
|
10889
|
+
bridgeErrors(reply);
|
|
10890
|
+
}
|
|
10891
|
+
if (Date.now() >= deadline) {
|
|
10892
|
+
throw new HueError('hue bridge: link button was not pressed in time');
|
|
10893
|
+
}
|
|
10894
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
10895
|
+
}
|
|
10896
|
+
}
|
|
10897
|
+
/**
|
|
10898
|
+
* Where the bridge is: `HUE_BRIDGE`, else Signify's discovery service, which
|
|
10899
|
+
* lists the bridges that have phoned home from this public address.
|
|
10900
|
+
*/
|
|
10901
|
+
async function discoverHueBridge() {
|
|
10902
|
+
var _a;
|
|
10903
|
+
if (process.env.HUE_BRIDGE) {
|
|
10904
|
+
return process.env.HUE_BRIDGE;
|
|
10905
|
+
}
|
|
10906
|
+
const found = await request('https://discovery.meethue.com');
|
|
10907
|
+
const bridge = (_a = found[0]) === null || _a === void 0 ? void 0 : _a.internalipaddress;
|
|
10908
|
+
if (bridge === undefined) {
|
|
10909
|
+
throw new HueError('no Hue bridge found; set HUE_BRIDGE to its address');
|
|
10910
|
+
}
|
|
10911
|
+
return bridge;
|
|
10912
|
+
}
|
|
10913
|
+
|
|
10914
|
+
|
|
10915
|
+
/***/ },
|
|
10916
|
+
|
|
10917
|
+
/***/ "./src/remote/index.ts"
|
|
10918
|
+
/*!*****************************!*\
|
|
10919
|
+
!*** ./src/remote/index.ts ***!
|
|
10920
|
+
\*****************************/
|
|
10921
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
10922
|
+
|
|
10923
|
+
"use strict";
|
|
10924
|
+
|
|
10925
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
10926
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
10927
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
10928
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
10929
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
10930
|
+
};
|
|
10931
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
10932
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
10933
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10934
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
10935
|
+
};
|
|
10936
|
+
var _Remote_instances, _Remote_network, _Remote_statuses, _Remote_cueOp, _Remote_status_get, _Remote_control_get, _Remote_me_get;
|
|
10937
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
10938
|
+
exports.Remote = exports.CueUnknownError = exports.NoPlayerError = exports.RemoteError = exports.TransportOp = void 0;
|
|
10939
|
+
/**
|
|
10940
|
+
* The remote: on the Pro DJ Link network as a Stagehand, the way the iPad app
|
|
10941
|
+
* joins, driving players with the transport packets the app's Sound Check
|
|
10942
|
+
* sends and remembering the last status each player reported. The REST API
|
|
10943
|
+
* over it is in `./server`.
|
|
10944
|
+
*/
|
|
10945
|
+
const os_1 = __webpack_require__(/*! os */ "os");
|
|
10946
|
+
const network_1 = __webpack_require__(/*! src/network */ "./src/network.ts");
|
|
10947
|
+
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
10948
|
+
const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
|
|
10949
|
+
/**
|
|
10950
|
+
* The Stagehand transport opcodes (packet 0x07) the iPad app was captured
|
|
10951
|
+
* sending, besides play (0x0f then 0x14 pressed) and pause (0x14 released),
|
|
10952
|
+
* which `Control` sends itself. These are held while pressed.
|
|
10953
|
+
*/
|
|
10954
|
+
exports.TransportOp = {
|
|
10955
|
+
SkipForward: 0x18,
|
|
10956
|
+
SkipBackward: 0x19,
|
|
10957
|
+
SeekForward: 0x1a,
|
|
10958
|
+
SeekBackward: 0x1b,
|
|
10959
|
+
};
|
|
10960
|
+
/**
|
|
10961
|
+
* The device numbers the Stagehand app was seen using: 141..=211.
|
|
10962
|
+
*/
|
|
10963
|
+
const STAGEHAND_FIRST_NUMBER = 141;
|
|
10964
|
+
const STAGEHAND_NUMBER_COUNT = 71;
|
|
10965
|
+
const TAP_MS = 100;
|
|
10966
|
+
class RemoteError extends Error {
|
|
10967
|
+
}
|
|
10968
|
+
exports.RemoteError = RemoteError;
|
|
10969
|
+
class NoPlayerError extends RemoteError {
|
|
10970
|
+
constructor(number) {
|
|
10971
|
+
super(`no player ${number} on the link`);
|
|
10972
|
+
}
|
|
10973
|
+
}
|
|
10974
|
+
exports.NoPlayerError = NoPlayerError;
|
|
10975
|
+
class CueUnknownError extends RemoteError {
|
|
10976
|
+
constructor() {
|
|
10977
|
+
super('cue has no known Stagehand opcode; set the cue opcode once one is found (probe with POST /players/<n>/op)');
|
|
10978
|
+
}
|
|
10979
|
+
}
|
|
10980
|
+
exports.CueUnknownError = CueUnknownError;
|
|
10981
|
+
function resolveIface(hint) {
|
|
10982
|
+
const all = (0, os_1.networkInterfaces)();
|
|
10983
|
+
for (const [name, infos] of Object.entries(all)) {
|
|
10984
|
+
for (const info of infos !== null && infos !== void 0 ? infos : []) {
|
|
10985
|
+
if (info.family !== 'IPv4' || info.internal) {
|
|
10986
|
+
continue;
|
|
10987
|
+
}
|
|
10988
|
+
if (name === hint || info.address === hint) {
|
|
10989
|
+
return info;
|
|
10990
|
+
}
|
|
10991
|
+
}
|
|
10992
|
+
}
|
|
10993
|
+
const available = Object.entries(all)
|
|
10994
|
+
.flatMap(([name, infos]) => (infos !== null && infos !== void 0 ? infos : [])
|
|
10995
|
+
.filter(i => i.family === 'IPv4' && !i.internal)
|
|
10996
|
+
.map(i => `${name}:${i.address}`))
|
|
10997
|
+
.join(', ');
|
|
10998
|
+
throw new RemoteError(`no interface "${hint}"; available: ${available}`);
|
|
10999
|
+
}
|
|
11000
|
+
/**
|
|
11001
|
+
* A device number that does not change between runs. A CDJ-3000 keeps one
|
|
11002
|
+
* record per peer address and ignores a later claim from that address under
|
|
11003
|
+
* another number or MAC until it reboots, so a random number per run may
|
|
11004
|
+
* never be served.
|
|
11005
|
+
*/
|
|
11006
|
+
function numberFor(iface) {
|
|
11007
|
+
var _a;
|
|
11008
|
+
const mac = iface.mac.split(':').map(b => parseInt(b, 16));
|
|
11009
|
+
const last = (_a = mac[5]) !== null && _a !== void 0 ? _a : 0;
|
|
11010
|
+
return STAGEHAND_FIRST_NUMBER + (last % STAGEHAND_NUMBER_COUNT);
|
|
11011
|
+
}
|
|
11012
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
11013
|
+
class Remote {
|
|
11014
|
+
constructor(network, cueOp) {
|
|
11015
|
+
_Remote_instances.add(this);
|
|
11016
|
+
_Remote_network.set(this, void 0);
|
|
11017
|
+
_Remote_statuses.set(this, new Map());
|
|
11018
|
+
_Remote_cueOp.set(this, void 0);
|
|
11019
|
+
__classPrivateFieldSet(this, _Remote_network, network, "f");
|
|
11020
|
+
__classPrivateFieldSet(this, _Remote_cueOp, cueOp, "f");
|
|
11021
|
+
__classPrivateFieldGet(this, _Remote_instances, "a", _Remote_status_get).on('status', s => __classPrivateFieldGet(this, _Remote_statuses, "f").set(s.deviceId, s));
|
|
11022
|
+
}
|
|
11023
|
+
/**
|
|
11024
|
+
* Join the link and start collecting status. Resolves once the Stagehand
|
|
11025
|
+
* join sequence is through and keep-alives are running.
|
|
11026
|
+
*/
|
|
11027
|
+
static async join(opts = {}) {
|
|
11028
|
+
var _a, _b, _c;
|
|
11029
|
+
const network = await (0, network_1.bringOnlineStagehand)({
|
|
11030
|
+
logger: opts.logger,
|
|
11031
|
+
});
|
|
11032
|
+
let iface;
|
|
11033
|
+
if (opts.iface !== undefined) {
|
|
11034
|
+
iface = resolveIface(opts.iface);
|
|
11035
|
+
}
|
|
11036
|
+
else {
|
|
11037
|
+
(_a = opts.logger) === null || _a === void 0 ? void 0 : _a.info('waiting for a player to pick the interface');
|
|
11038
|
+
const dm = network.deviceManager;
|
|
11039
|
+
const first = await new Promise(resolve => dm.once('connected', resolve));
|
|
11040
|
+
const match = (0, utils_1.getMatchingInterface)(first.ip);
|
|
11041
|
+
if (match === null) {
|
|
11042
|
+
throw new RemoteError(`no local interface matches ${first.ip.address}`);
|
|
11043
|
+
}
|
|
11044
|
+
iface = match;
|
|
11045
|
+
}
|
|
11046
|
+
network.configure({ iface, vcdjId: (_b = opts.number) !== null && _b !== void 0 ? _b : numberFor(iface) });
|
|
11047
|
+
network.connect();
|
|
11048
|
+
if (!network.isConnected()) {
|
|
11049
|
+
throw new RemoteError('failed to connect to the link');
|
|
11050
|
+
}
|
|
11051
|
+
await network.startupReady;
|
|
11052
|
+
const me = network.virtualDevice;
|
|
11053
|
+
(_c = opts.logger) === null || _c === void 0 ? void 0 : _c.info(`on the link as "${me === null || me === void 0 ? void 0 : me.name}" #${me === null || me === void 0 ? void 0 : me.id} at ${iface.address}`);
|
|
11054
|
+
return new Remote(network, opts.cueOp);
|
|
11055
|
+
}
|
|
11056
|
+
get network() {
|
|
11057
|
+
return __classPrivateFieldGet(this, _Remote_network, "f");
|
|
11058
|
+
}
|
|
11059
|
+
/**
|
|
11060
|
+
* Our own device as announced.
|
|
11061
|
+
*/
|
|
11062
|
+
get me() {
|
|
11063
|
+
return __classPrivateFieldGet(this, _Remote_network, "f").virtualDevice;
|
|
11064
|
+
}
|
|
11065
|
+
/**
|
|
11066
|
+
* Every device heard on the link, by number.
|
|
11067
|
+
*/
|
|
11068
|
+
get devices() {
|
|
11069
|
+
return [...__classPrivateFieldGet(this, _Remote_network, "f").deviceManager.devices.values()].sort((a, b) => a.id - b.id);
|
|
11070
|
+
}
|
|
11071
|
+
/**
|
|
11072
|
+
* The players, with the last status each one sent.
|
|
11073
|
+
*/
|
|
11074
|
+
get players() {
|
|
11075
|
+
return this.devices
|
|
11076
|
+
.filter(d => d.type === types_1.DeviceType.CDJ)
|
|
11077
|
+
.map(d => ({
|
|
11078
|
+
number: d.id,
|
|
11079
|
+
name: d.name,
|
|
11080
|
+
ip: d.ip.address,
|
|
11081
|
+
status: __classPrivateFieldGet(this, _Remote_statuses, "f").get(d.id),
|
|
11082
|
+
}));
|
|
11083
|
+
}
|
|
11084
|
+
player(number) {
|
|
11085
|
+
const device = __classPrivateFieldGet(this, _Remote_network, "f").deviceManager.devices.get(number);
|
|
11086
|
+
if (device === undefined || device.type !== types_1.DeviceType.CDJ) {
|
|
11087
|
+
throw new NoPlayerError(number);
|
|
11088
|
+
}
|
|
11089
|
+
return device;
|
|
11090
|
+
}
|
|
11091
|
+
play(number) {
|
|
11092
|
+
return __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_control_get).play(this.player(number));
|
|
11093
|
+
}
|
|
11094
|
+
pause(number) {
|
|
11095
|
+
return __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_control_get).pause(this.player(number));
|
|
11096
|
+
}
|
|
11097
|
+
/**
|
|
11098
|
+
* Press and release the cue button, once its opcode is known.
|
|
11099
|
+
*/
|
|
11100
|
+
cue(number) {
|
|
11101
|
+
if (__classPrivateFieldGet(this, _Remote_cueOp, "f") === undefined) {
|
|
11102
|
+
throw new CueUnknownError();
|
|
11103
|
+
}
|
|
11104
|
+
return this.tap(number, __classPrivateFieldGet(this, _Remote_cueOp, "f"), TAP_MS);
|
|
11105
|
+
}
|
|
11106
|
+
/**
|
|
11107
|
+
* Hold fast-forward for `holdMs`.
|
|
11108
|
+
*/
|
|
11109
|
+
fastForward(number, holdMs) {
|
|
11110
|
+
return this.tap(number, exports.TransportOp.SeekForward, holdMs);
|
|
11111
|
+
}
|
|
11112
|
+
/**
|
|
11113
|
+
* Hold rewind for `holdMs`.
|
|
11114
|
+
*/
|
|
11115
|
+
rewind(number, holdMs) {
|
|
11116
|
+
return this.tap(number, exports.TransportOp.SeekBackward, holdMs);
|
|
11117
|
+
}
|
|
11118
|
+
skipForward(number) {
|
|
11119
|
+
return this.tap(number, exports.TransportOp.SkipForward, TAP_MS);
|
|
11120
|
+
}
|
|
11121
|
+
skipBackward(number) {
|
|
11122
|
+
return this.tap(number, exports.TransportOp.SkipBackward, TAP_MS);
|
|
11123
|
+
}
|
|
11124
|
+
/**
|
|
11125
|
+
* One transport packet: `op` pressed or released.
|
|
11126
|
+
*/
|
|
11127
|
+
transport(number, op, press) {
|
|
11128
|
+
return __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_control_get).transport(this.player(number), op, press);
|
|
11129
|
+
}
|
|
11130
|
+
/**
|
|
11131
|
+
* Press `op`, hold it for `holdMs`, release it.
|
|
11132
|
+
*/
|
|
11133
|
+
async tap(number, op, holdMs) {
|
|
11134
|
+
await this.transport(number, op, true);
|
|
11135
|
+
await sleep(holdMs);
|
|
11136
|
+
await this.transport(number, op, false);
|
|
11137
|
+
}
|
|
11138
|
+
/**
|
|
11139
|
+
* What is in a player's USB and SD slots, as the player answers a media
|
|
11140
|
+
* query; a slot it does not answer for is left out.
|
|
11141
|
+
*/
|
|
11142
|
+
async media(number) {
|
|
11143
|
+
const device = this.player(number);
|
|
11144
|
+
const slots = [];
|
|
11145
|
+
for (const slot of [types_1.MediaSlot.USB, types_1.MediaSlot.SD]) {
|
|
11146
|
+
try {
|
|
11147
|
+
slots.push(await __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_status_get).queryMediaSlot({ hostDevice: __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_me_get), device, slot }));
|
|
11148
|
+
}
|
|
11149
|
+
catch {
|
|
11150
|
+
// an empty slot is not answered for
|
|
11151
|
+
}
|
|
11152
|
+
}
|
|
11153
|
+
return slots;
|
|
11154
|
+
}
|
|
11155
|
+
/**
|
|
11156
|
+
* Tell a player to load a track from `source` (another player's stick, or a
|
|
11157
|
+
* library served over the link).
|
|
11158
|
+
*/
|
|
11159
|
+
loadTrack(number, trackId, source) {
|
|
11160
|
+
return __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_control_get).loadTrack(this.player(number), trackId, source);
|
|
11161
|
+
}
|
|
11162
|
+
async leave() {
|
|
11163
|
+
__classPrivateFieldGet(this, _Remote_network, "f").disconnect();
|
|
11164
|
+
await __classPrivateFieldGet(this, _Remote_network, "f").close();
|
|
11165
|
+
}
|
|
11166
|
+
}
|
|
11167
|
+
exports.Remote = Remote;
|
|
11168
|
+
_Remote_network = new WeakMap(), _Remote_statuses = new WeakMap(), _Remote_cueOp = new WeakMap(), _Remote_instances = new WeakSet(), _Remote_status_get = function _Remote_status_get() {
|
|
11169
|
+
const emitter = __classPrivateFieldGet(this, _Remote_network, "f").statusEmitter;
|
|
11170
|
+
if (emitter === null) {
|
|
11171
|
+
throw new RemoteError('not connected to the link');
|
|
11172
|
+
}
|
|
11173
|
+
return emitter;
|
|
11174
|
+
}, _Remote_control_get = function _Remote_control_get() {
|
|
11175
|
+
const control = __classPrivateFieldGet(this, _Remote_network, "f").control;
|
|
11176
|
+
if (control === null) {
|
|
11177
|
+
throw new RemoteError('not connected to the link');
|
|
11178
|
+
}
|
|
11179
|
+
return control;
|
|
11180
|
+
}, _Remote_me_get = function _Remote_me_get() {
|
|
11181
|
+
const me = __classPrivateFieldGet(this, _Remote_network, "f").virtualDevice;
|
|
11182
|
+
if (me === null) {
|
|
11183
|
+
throw new RemoteError('not connected to the link');
|
|
11184
|
+
}
|
|
11185
|
+
return me;
|
|
11186
|
+
};
|
|
11187
|
+
|
|
11188
|
+
|
|
11189
|
+
/***/ },
|
|
11190
|
+
|
|
11191
|
+
/***/ "./src/remote/server.ts"
|
|
11192
|
+
/*!******************************!*\
|
|
11193
|
+
!*** ./src/remote/server.ts ***!
|
|
11194
|
+
\******************************/
|
|
11195
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
11196
|
+
|
|
11197
|
+
"use strict";
|
|
11198
|
+
|
|
11199
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11200
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11201
|
+
};
|
|
11202
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
11203
|
+
exports.createServer = createServer;
|
|
11204
|
+
/**
|
|
11205
|
+
* The REST API over a {@link Remote}. JSON in and out; every error is
|
|
11206
|
+
* `{"error": "…"}` with a status that says whose fault it was.
|
|
11207
|
+
*
|
|
11208
|
+
* GET / who we are, every device heard, the booth's power
|
|
11209
|
+
* GET /players the players with their last status
|
|
11210
|
+
* GET /players/{n}/media what is in the player's USB and SD slots
|
|
11211
|
+
* POST /players/{n}/play
|
|
11212
|
+
* POST /players/{n}/pause
|
|
11213
|
+
* POST /players/{n}/cue
|
|
11214
|
+
* POST /players/{n}/ff {"hold_ms": 500} hold fast-forward, then release
|
|
11215
|
+
* POST /players/{n}/rew {"hold_ms": 500}
|
|
11216
|
+
* POST /players/{n}/skip {"direction": "forward" | "backward"}
|
|
11217
|
+
* POST /players/{n}/op {"op": 26, "press": true} one raw transport packet
|
|
11218
|
+
* POST /players/{n}/load {"track_id": 1, "source": {"device": 2, "slot": "usb"}}
|
|
11219
|
+
* GET /booth the Hue group and whether it is on
|
|
11220
|
+
* PUT /booth {"on": true}
|
|
11221
|
+
* POST /booth/on
|
|
11222
|
+
* POST /booth/off
|
|
11223
|
+
* GET /hue/groups every room and zone the bridge has
|
|
11224
|
+
*/
|
|
11225
|
+
const http_1 = __importDefault(__webpack_require__(/*! http */ "http"));
|
|
11226
|
+
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
11227
|
+
const hue_1 = __webpack_require__(/*! ./hue */ "./src/remote/hue.ts");
|
|
11228
|
+
const index_1 = __webpack_require__(/*! ./index */ "./src/remote/index.ts");
|
|
11229
|
+
class HttpError extends Error {
|
|
11230
|
+
constructor(status, message) {
|
|
11231
|
+
super(message);
|
|
11232
|
+
this.status = status;
|
|
11233
|
+
}
|
|
11234
|
+
}
|
|
11235
|
+
const MAX_HOLD_MS = 30000;
|
|
11236
|
+
const DEFAULT_HOLD_MS = 500;
|
|
11237
|
+
const SLOTS = {
|
|
11238
|
+
rekordbox: types_1.MediaSlot.RB,
|
|
11239
|
+
rb: types_1.MediaSlot.RB,
|
|
11240
|
+
usb: types_1.MediaSlot.USB,
|
|
11241
|
+
sd: types_1.MediaSlot.SD,
|
|
11242
|
+
};
|
|
11243
|
+
function statusOf(e) {
|
|
11244
|
+
if (e instanceof HttpError) {
|
|
11245
|
+
return e.status;
|
|
11246
|
+
}
|
|
11247
|
+
if (e instanceof index_1.NoPlayerError || e instanceof hue_1.HueNoGroupError) {
|
|
11248
|
+
return 404;
|
|
11249
|
+
}
|
|
11250
|
+
if (e instanceof index_1.CueUnknownError || e instanceof hue_1.HueUnconfiguredError) {
|
|
11251
|
+
return 501;
|
|
11252
|
+
}
|
|
11253
|
+
return 502;
|
|
11254
|
+
}
|
|
11255
|
+
async function readJson(req) {
|
|
11256
|
+
const chunks = [];
|
|
11257
|
+
for await (const chunk of req) {
|
|
11258
|
+
chunks.push(chunk);
|
|
11259
|
+
}
|
|
11260
|
+
const text = Buffer.concat(chunks).toString('utf8').trim();
|
|
11261
|
+
if (text === '') {
|
|
11262
|
+
return {};
|
|
11263
|
+
}
|
|
11264
|
+
try {
|
|
11265
|
+
return JSON.parse(text);
|
|
11266
|
+
}
|
|
11267
|
+
catch (e) {
|
|
11268
|
+
throw new HttpError(400, `body is not JSON: ${e.message}`);
|
|
11269
|
+
}
|
|
11270
|
+
}
|
|
11271
|
+
function integer(value, name, min = 0, max = Number.MAX_SAFE_INTEGER) {
|
|
11272
|
+
if (typeof value !== 'number' ||
|
|
11273
|
+
!Number.isInteger(value) ||
|
|
11274
|
+
value < min ||
|
|
11275
|
+
value > max) {
|
|
11276
|
+
throw new HttpError(400, `${name} must be an integer in ${min}..${max}`);
|
|
11277
|
+
}
|
|
11278
|
+
return value;
|
|
11279
|
+
}
|
|
11280
|
+
function holdMs(body) {
|
|
11281
|
+
if (body.hold_ms === undefined) {
|
|
11282
|
+
return DEFAULT_HOLD_MS;
|
|
11283
|
+
}
|
|
11284
|
+
return integer(body.hold_ms, 'hold_ms', 0, MAX_HOLD_MS);
|
|
11285
|
+
}
|
|
11286
|
+
function createServer({ remote, hue, boothGroup, logger }) {
|
|
11287
|
+
const needHue = () => {
|
|
11288
|
+
if (hue === null) {
|
|
11289
|
+
throw new hue_1.HueUnconfiguredError();
|
|
11290
|
+
}
|
|
11291
|
+
return hue;
|
|
11292
|
+
};
|
|
11293
|
+
const boothState = async () => {
|
|
11294
|
+
const group = await needHue().groupNamed(boothGroup);
|
|
11295
|
+
return {
|
|
11296
|
+
group: group.name,
|
|
11297
|
+
id: group.id,
|
|
11298
|
+
type: group.type,
|
|
11299
|
+
on: group.anyOn,
|
|
11300
|
+
all_on: group.allOn,
|
|
11301
|
+
lights: group.lights,
|
|
11302
|
+
};
|
|
11303
|
+
};
|
|
11304
|
+
const setBooth = async (on) => {
|
|
11305
|
+
const bridge = needHue();
|
|
11306
|
+
const group = await bridge.groupNamed(boothGroup);
|
|
11307
|
+
await bridge.setGroupOn(group.id, on);
|
|
11308
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`booth "${group.name}" switched ${on ? 'on' : 'off'}`);
|
|
11309
|
+
return { group: group.name, on };
|
|
11310
|
+
};
|
|
11311
|
+
const ok = { ok: true };
|
|
11312
|
+
const route = async (method, path, body) => {
|
|
11313
|
+
var _a, _b;
|
|
11314
|
+
const parts = path.split('/').filter(p => p !== '');
|
|
11315
|
+
if (parts.length === 0 && method === 'GET') {
|
|
11316
|
+
const me = remote.me;
|
|
11317
|
+
const booth = await boothState().catch(e => ({
|
|
11318
|
+
group: boothGroup,
|
|
11319
|
+
error: e.message,
|
|
11320
|
+
}));
|
|
11321
|
+
return {
|
|
11322
|
+
stagehand: me && { name: me.name, number: me.id, ip: me.ip.address },
|
|
11323
|
+
devices: remote.devices.map(d => ({
|
|
11324
|
+
number: d.id,
|
|
11325
|
+
name: d.name,
|
|
11326
|
+
type: d.type,
|
|
11327
|
+
ip: d.ip.address,
|
|
11328
|
+
})),
|
|
11329
|
+
players: remote.players,
|
|
11330
|
+
booth,
|
|
11331
|
+
};
|
|
11332
|
+
}
|
|
11333
|
+
if (parts[0] === 'players') {
|
|
11334
|
+
if (parts.length === 1 && method === 'GET') {
|
|
11335
|
+
return remote.players;
|
|
11336
|
+
}
|
|
11337
|
+
const n = parseInt(parts[1], 10);
|
|
11338
|
+
if (!Number.isInteger(n)) {
|
|
11339
|
+
throw new HttpError(404, `no route ${method} ${path}`);
|
|
11340
|
+
}
|
|
11341
|
+
const action = parts[2];
|
|
11342
|
+
if (parts.length !== 3) {
|
|
11343
|
+
throw new HttpError(404, `no route ${method} ${path}`);
|
|
11344
|
+
}
|
|
11345
|
+
if (method === 'GET' && action === 'media') {
|
|
11346
|
+
return remote.media(n);
|
|
11347
|
+
}
|
|
11348
|
+
if (method !== 'POST') {
|
|
11349
|
+
throw new HttpError(405, `${method} not allowed on ${path}`);
|
|
11350
|
+
}
|
|
11351
|
+
switch (action) {
|
|
11352
|
+
case 'play':
|
|
11353
|
+
await remote.play(n);
|
|
11354
|
+
return ok;
|
|
11355
|
+
case 'pause':
|
|
11356
|
+
await remote.pause(n);
|
|
11357
|
+
return ok;
|
|
11358
|
+
case 'cue':
|
|
11359
|
+
await remote.cue(n);
|
|
11360
|
+
return ok;
|
|
11361
|
+
case 'ff':
|
|
11362
|
+
await remote.fastForward(n, holdMs(await body()));
|
|
11363
|
+
return ok;
|
|
11364
|
+
case 'rew':
|
|
11365
|
+
await remote.rewind(n, holdMs(await body()));
|
|
11366
|
+
return ok;
|
|
11367
|
+
case 'skip': {
|
|
11368
|
+
const { direction } = await body();
|
|
11369
|
+
if (direction === 'forward') {
|
|
11370
|
+
await remote.skipForward(n);
|
|
11371
|
+
}
|
|
11372
|
+
else if (direction === 'backward') {
|
|
11373
|
+
await remote.skipBackward(n);
|
|
11374
|
+
}
|
|
11375
|
+
else {
|
|
11376
|
+
throw new HttpError(400, 'direction must be "forward" or "backward"');
|
|
11377
|
+
}
|
|
11378
|
+
return ok;
|
|
11379
|
+
}
|
|
11380
|
+
case 'op': {
|
|
11381
|
+
const b = await body();
|
|
11382
|
+
await remote.transport(n, integer(b.op, 'op', 0, 255), b.press !== false);
|
|
11383
|
+
return ok;
|
|
11384
|
+
}
|
|
11385
|
+
case 'load': {
|
|
11386
|
+
const b = await body();
|
|
11387
|
+
const trackId = integer(b.track_id, 'track_id', 1, 0xffffffff);
|
|
11388
|
+
const source = (_a = b.source) !== null && _a !== void 0 ? _a : {};
|
|
11389
|
+
const device = integer(source.device, 'source.device', 1, 255);
|
|
11390
|
+
const slotName = String((_b = source.slot) !== null && _b !== void 0 ? _b : 'rekordbox').toLowerCase();
|
|
11391
|
+
const slot = SLOTS[slotName];
|
|
11392
|
+
if (slot === undefined) {
|
|
11393
|
+
throw new HttpError(400, 'source.slot must be "rekordbox", "usb" or "sd"');
|
|
11394
|
+
}
|
|
11395
|
+
await remote.loadTrack(n, trackId, { deviceId: device, slot });
|
|
11396
|
+
return ok;
|
|
11397
|
+
}
|
|
11398
|
+
default:
|
|
11399
|
+
throw new HttpError(404, `no route ${method} ${path}`);
|
|
11400
|
+
}
|
|
11401
|
+
}
|
|
11402
|
+
if (parts[0] === 'booth') {
|
|
11403
|
+
if (parts.length === 1) {
|
|
11404
|
+
if (method === 'GET') {
|
|
11405
|
+
return boothState();
|
|
11406
|
+
}
|
|
11407
|
+
if (method === 'PUT') {
|
|
11408
|
+
const { on } = await body();
|
|
11409
|
+
if (typeof on !== 'boolean') {
|
|
11410
|
+
throw new HttpError(400, 'on must be true or false');
|
|
11411
|
+
}
|
|
11412
|
+
return setBooth(on);
|
|
11413
|
+
}
|
|
11414
|
+
}
|
|
11415
|
+
if (parts.length === 2 && method === 'POST') {
|
|
11416
|
+
if (parts[1] === 'on') {
|
|
11417
|
+
return setBooth(true);
|
|
11418
|
+
}
|
|
11419
|
+
if (parts[1] === 'off') {
|
|
11420
|
+
return setBooth(false);
|
|
11421
|
+
}
|
|
11422
|
+
}
|
|
11423
|
+
}
|
|
11424
|
+
if (parts[0] === 'hue' &&
|
|
11425
|
+
parts[1] === 'groups' &&
|
|
11426
|
+
parts.length === 2 &&
|
|
11427
|
+
method === 'GET') {
|
|
11428
|
+
return needHue().groups();
|
|
11429
|
+
}
|
|
11430
|
+
throw new HttpError(404, `no route ${method} ${path}`);
|
|
11431
|
+
};
|
|
11432
|
+
return http_1.default.createServer(async (req, res) => {
|
|
11433
|
+
var _a, _b;
|
|
11434
|
+
const method = (_a = req.method) !== null && _a !== void 0 ? _a : 'GET';
|
|
11435
|
+
const path = ((_b = req.url) !== null && _b !== void 0 ? _b : '/').split('?')[0];
|
|
11436
|
+
const send = (status, payload) => {
|
|
11437
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
11438
|
+
res.end(`${JSON.stringify(payload)}\n`);
|
|
11439
|
+
};
|
|
11440
|
+
try {
|
|
11441
|
+
send(200, await route(method, path, () => readJson(req)));
|
|
11442
|
+
}
|
|
11443
|
+
catch (e) {
|
|
11444
|
+
const status = statusOf(e);
|
|
11445
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
11446
|
+
if (status >= 500) {
|
|
11447
|
+
logger === null || logger === void 0 ? void 0 : logger.warn(`${method} ${path}: ${message}`);
|
|
11448
|
+
}
|
|
11449
|
+
send(status, { error: message });
|
|
11450
|
+
}
|
|
11451
|
+
});
|
|
11452
|
+
}
|
|
11453
|
+
|
|
11454
|
+
|
|
10554
11455
|
/***/ },
|
|
10555
11456
|
|
|
10556
11457
|
/***/ "./src/remotedb/constants.ts"
|
|
@@ -14468,6 +15369,17 @@ module.exports = require("fs");
|
|
|
14468
15369
|
|
|
14469
15370
|
/***/ },
|
|
14470
15371
|
|
|
15372
|
+
/***/ "http"
|
|
15373
|
+
/*!***********************!*\
|
|
15374
|
+
!*** external "http" ***!
|
|
15375
|
+
\***********************/
|
|
15376
|
+
(module) {
|
|
15377
|
+
|
|
15378
|
+
"use strict";
|
|
15379
|
+
module.exports = require("http");
|
|
15380
|
+
|
|
15381
|
+
/***/ },
|
|
15382
|
+
|
|
14471
15383
|
/***/ "ip-address"
|
|
14472
15384
|
/*!*****************************!*\
|
|
14473
15385
|
!*** external "ip-address" ***!
|