alphatheta-connect 0.23.1 → 0.25.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 +389 -46
- package/lib/cli.js.map +1 -1
- package/lib/constants.d.ts +4 -2
- package/lib/db/getMetadata.d.ts +23 -1
- package/lib/db/index.d.ts +2 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.js +391 -47
- package/lib/index.js.map +1 -1
- package/lib/network.d.ts +10 -9
- package/lib/remotedb/fields.d.ts +6 -6
- package/lib/remotedb/index.d.ts +12 -1
- package/lib/status/types.d.ts +54 -6
- package/lib/types.js +20 -1
- package/lib/types.js.map +1 -1
- package/lib/virtualcdj/device-id.d.ts +36 -7
- package/lib/virtualcdj/heartbeat.d.ts +51 -0
- package/lib/virtualcdj/heartbeat.test.d.ts +1 -0
- package/lib/virtualcdj/index.d.ts +1 -0
- package/package.json +1 -1
package/lib/cli.js
CHANGED
|
@@ -742,8 +742,10 @@ exports.MAX_CDJ_DEVICE_ID = exports.MIN_CDJ_DEVICE_ID = exports.VIRTUAL_CDJ_FIRM
|
|
|
742
742
|
/**
|
|
743
743
|
* The default virtual CDJ ID to use.
|
|
744
744
|
*
|
|
745
|
-
*
|
|
746
|
-
* metadata
|
|
745
|
+
* Out of the 1-6 player range, so it can never collide with a real CDJ.
|
|
746
|
+
* Remotedb metadata still works from here: the 1-6 restriction applies to the
|
|
747
|
+
* device-ID byte inside remotedb messages, which RemoteDatabase picks
|
|
748
|
+
* per-connection independently of the announced ID.
|
|
747
749
|
*/
|
|
748
750
|
exports.DEFAULT_VCDJ_ID = 0x07;
|
|
749
751
|
/**
|
|
@@ -1383,11 +1385,11 @@ async function viaLocal(local, device, opts) {
|
|
|
1383
1385
|
}
|
|
1384
1386
|
const adapter = await local.get(deviceId, trackSlot);
|
|
1385
1387
|
if (adapter === null) {
|
|
1386
|
-
return null;
|
|
1388
|
+
return { track: null, miss: 'no-database' };
|
|
1387
1389
|
}
|
|
1388
1390
|
const dbTrack = adapter.findTrack(trackId);
|
|
1389
1391
|
if (dbTrack === null) {
|
|
1390
|
-
return null;
|
|
1392
|
+
return { track: null, miss: 'track-absent' };
|
|
1391
1393
|
}
|
|
1392
1394
|
const anlz = await (0, rekordbox_1.loadAnlz)(dbTrack, 'DAT', (0, utils_1.anlzLoader)({ device, slot: trackSlot }));
|
|
1393
1395
|
const track = {
|
|
@@ -1395,7 +1397,7 @@ async function viaLocal(local, device, opts) {
|
|
|
1395
1397
|
beatGrid: anlz.beatGrid,
|
|
1396
1398
|
waveformHd: null,
|
|
1397
1399
|
};
|
|
1398
|
-
return track;
|
|
1400
|
+
return { track, miss: null };
|
|
1399
1401
|
}
|
|
1400
1402
|
|
|
1401
1403
|
|
|
@@ -1645,8 +1647,9 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
1645
1647
|
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");
|
|
1646
1648
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
1647
1649
|
};
|
|
1648
|
-
var _Database_deviceManager, _Database_localDatabase, _Database_remoteDatabase, _Database_getTrackLookupStrategy, _Database_getMediaLookupStrategy;
|
|
1650
|
+
var _Database_instances, _Database_deviceManager, _Database_localDatabase, _Database_remoteDatabase, _Database_logger, _Database_getTrackLookupStrategy, _Database_getMediaLookupStrategy, _Database_metadataViaRemoteFallback;
|
|
1649
1651
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1652
|
+
const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
|
|
1650
1653
|
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
1651
1654
|
const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
|
|
1652
1655
|
const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
|
|
@@ -1669,7 +1672,8 @@ var LookupStrategy;
|
|
|
1669
1672
|
* network for information from their databases.
|
|
1670
1673
|
*/
|
|
1671
1674
|
class Database {
|
|
1672
|
-
constructor(local, remote, deviceManager) {
|
|
1675
|
+
constructor(local, remote, deviceManager, logger = logger_1.noopLogger) {
|
|
1676
|
+
_Database_instances.add(this);
|
|
1673
1677
|
_Database_deviceManager.set(this, void 0);
|
|
1674
1678
|
/**
|
|
1675
1679
|
* The local database service, used when querying media devices connected
|
|
@@ -1681,6 +1685,7 @@ class Database {
|
|
|
1681
1685
|
* CDJ with an unanalyzed media device connected (when possible).
|
|
1682
1686
|
*/
|
|
1683
1687
|
_Database_remoteDatabase.set(this, void 0);
|
|
1688
|
+
_Database_logger.set(this, void 0);
|
|
1684
1689
|
_Database_getTrackLookupStrategy.set(this, (device, type) => {
|
|
1685
1690
|
const isUnanalyzed = type === types_1.TrackType.AudioCD || type === types_1.TrackType.Unanalyzed;
|
|
1686
1691
|
const isStreaming = type === types_1.TrackType.Streaming;
|
|
@@ -1703,6 +1708,7 @@ class Database {
|
|
|
1703
1708
|
__classPrivateFieldSet(this, _Database_localDatabase, local, "f");
|
|
1704
1709
|
__classPrivateFieldSet(this, _Database_remoteDatabase, remote, "f");
|
|
1705
1710
|
__classPrivateFieldSet(this, _Database_deviceManager, deviceManager, "f");
|
|
1711
|
+
__classPrivateFieldSet(this, _Database_logger, logger, "f");
|
|
1706
1712
|
}
|
|
1707
1713
|
/**
|
|
1708
1714
|
* Get the database type (oneLibrary or pdb) for a loaded device slot.
|
|
@@ -1733,7 +1739,18 @@ class Database {
|
|
|
1733
1739
|
track = await GetMetadata.viaRemote(__classPrivateFieldGet(this, _Database_remoteDatabase, "f"), callOpts);
|
|
1734
1740
|
}
|
|
1735
1741
|
if (strategy === LookupStrategy.Local) {
|
|
1736
|
-
|
|
1742
|
+
const local = await GetMetadata.viaLocal(__classPrivateFieldGet(this, _Database_localDatabase, "f"), device, callOpts);
|
|
1743
|
+
track = local.track;
|
|
1744
|
+
// A local miss used to end the lookup, so the DJ's track silently never
|
|
1745
|
+
// reached the overlay for the rest of the set (NP3-361). Say what was
|
|
1746
|
+
// missed, then give the CDJ's own database a chance to answer.
|
|
1747
|
+
if (local.miss !== null) {
|
|
1748
|
+
__classPrivateFieldGet(this, _Database_logger, "f").warn(`Local metadata lookup missed track ${opts.trackId} on device ${deviceId} ` +
|
|
1749
|
+
`${(0, utils_1.getSlotName)(trackSlot)}: ${local.miss === 'no-database'
|
|
1750
|
+
? 'no rekordbox database is loaded for that slot'
|
|
1751
|
+
: 'the loaded database holds no track with that id'}`);
|
|
1752
|
+
track = await __classPrivateFieldGet(this, _Database_instances, "m", _Database_metadataViaRemoteFallback).call(this, callOpts);
|
|
1753
|
+
}
|
|
1737
1754
|
}
|
|
1738
1755
|
if (strategy === LookupStrategy.NoneAvailable) {
|
|
1739
1756
|
tx.setStatus(telemetry_1.SpanStatus.Unavailable);
|
|
@@ -1927,7 +1944,34 @@ class Database {
|
|
|
1927
1944
|
return contents;
|
|
1928
1945
|
}
|
|
1929
1946
|
}
|
|
1930
|
-
_Database_deviceManager = new WeakMap(), _Database_localDatabase = new WeakMap(), _Database_remoteDatabase = new WeakMap(), _Database_getTrackLookupStrategy = new WeakMap(), _Database_getMediaLookupStrategy = new WeakMap()
|
|
1947
|
+
_Database_deviceManager = new WeakMap(), _Database_localDatabase = new WeakMap(), _Database_remoteDatabase = new WeakMap(), _Database_logger = new WeakMap(), _Database_getTrackLookupStrategy = new WeakMap(), _Database_getMediaLookupStrategy = new WeakMap(), _Database_instances = new WeakSet(), _Database_metadataViaRemoteFallback =
|
|
1948
|
+
/**
|
|
1949
|
+
* Second chance for a track the local database could not answer for.
|
|
1950
|
+
*
|
|
1951
|
+
* Available regardless of the virtual CDJ's announced ID: CDJs restrict
|
|
1952
|
+
* remote database queries to a device-ID byte in the 1-6 range, but that
|
|
1953
|
+
* byte lives inside the remotedb messages and RemoteDatabase picks an
|
|
1954
|
+
* in-range one per connection — the announced ID never constrains this
|
|
1955
|
+
* lookup. A failure here is logged rather than thrown, so a field report
|
|
1956
|
+
* says what actually went wrong.
|
|
1957
|
+
*/
|
|
1958
|
+
async function _Database_metadataViaRemoteFallback(opts) {
|
|
1959
|
+
try {
|
|
1960
|
+
const track = await GetMetadata.viaRemote(__classPrivateFieldGet(this, _Database_remoteDatabase, "f"), opts);
|
|
1961
|
+
if (track === null) {
|
|
1962
|
+
__classPrivateFieldGet(this, _Database_logger, "f").warn(`Remote fallback found no track ${opts.trackId} on device ${opts.deviceId}`);
|
|
1963
|
+
return null;
|
|
1964
|
+
}
|
|
1965
|
+
__classPrivateFieldGet(this, _Database_logger, "f").info(`Remote fallback recovered track ${opts.trackId} from device ${opts.deviceId}`);
|
|
1966
|
+
return track;
|
|
1967
|
+
}
|
|
1968
|
+
catch (e) {
|
|
1969
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1970
|
+
__classPrivateFieldGet(this, _Database_logger, "f").warn(`Remote fallback failed for track ${opts.trackId} on device ` +
|
|
1971
|
+
`${opts.deviceId}: ${message}`);
|
|
1972
|
+
return null;
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1931
1975
|
exports["default"] = Database;
|
|
1932
1976
|
|
|
1933
1977
|
|
|
@@ -6669,7 +6713,7 @@ class MixstatusProcessor {
|
|
|
6669
6713
|
// able to report it as live
|
|
6670
6714
|
const startedAt = __classPrivateFieldGet(this, _MixstatusProcessor_lastStartTime, "f").get(deviceId);
|
|
6671
6715
|
const requiredPlayTime = __classPrivateFieldGet(this, _MixstatusProcessor_config, "f").beatsUntilReported *
|
|
6672
|
-
(0, utils_1.bpmToSeconds)(state.trackBPM, state.
|
|
6716
|
+
(0, utils_1.bpmToSeconds)(state.trackBPM, state.effectivePitch) *
|
|
6673
6717
|
1000;
|
|
6674
6718
|
if (__classPrivateFieldGet(this, _MixstatusProcessor_config, "f").mode === types_1.MixstatusMode.SmartTiming &&
|
|
6675
6719
|
startedAt !== undefined &&
|
|
@@ -6680,7 +6724,7 @@ class MixstatusProcessor {
|
|
|
6680
6724
|
// we can mark the track as truly stopped.
|
|
6681
6725
|
const stoppedAt = __classPrivateFieldGet(this, _MixstatusProcessor_lastStoppedTimes, "f").get(deviceId);
|
|
6682
6726
|
const requiredStopTime = __classPrivateFieldGet(this, _MixstatusProcessor_config, "f").allowedInterruptBeats *
|
|
6683
|
-
(0, utils_1.bpmToSeconds)(state.trackBPM, state.
|
|
6727
|
+
(0, utils_1.bpmToSeconds)(state.trackBPM, state.effectivePitch) *
|
|
6684
6728
|
1000;
|
|
6685
6729
|
if (stoppedAt !== undefined && requiredStopTime <= Date.now() - stoppedAt) {
|
|
6686
6730
|
__classPrivateFieldGet(this, _MixstatusProcessor_markPlayerStopped, "f").call(this, state);
|
|
@@ -6971,31 +7015,40 @@ class ProlinkNetwork {
|
|
|
6971
7015
|
announcer = new virtualcdj_1.Announcer(vcdj, __classPrivateFieldGet(this, _ProlinkNetwork_announceSocket, "f"), this.deviceManager, __classPrivateFieldGet(this, _ProlinkNetwork_config, "f").iface, (_d = __classPrivateFieldGet(this, _ProlinkNetwork_config, "f").fullStartup) !== null && _d !== void 0 ? _d : false, (_e = __classPrivateFieldGet(this, _ProlinkNetwork_config, "f").announceToStagehand) !== null && _e !== void 0 ? _e : false, __classPrivateFieldGet(this, _ProlinkNetwork_logger, "f"));
|
|
6972
7016
|
}
|
|
6973
7017
|
announcer.start();
|
|
7018
|
+
// In Stagehand mode, unicast keep-alives to discovered players and the
|
|
7019
|
+
// mixer so they push live state to us (mixer 0x39/0x58, CDJ 0x69/waveforms).
|
|
7020
|
+
// Broadcast presence alone does not bootstrap that stream.
|
|
7021
|
+
let heartbeat = null;
|
|
7022
|
+
if (connectMethod === 'stagehand') {
|
|
7023
|
+
heartbeat = new virtualcdj_1.StagehandHeartbeat(vcdj, __classPrivateFieldGet(this, _ProlinkNetwork_statusSocket, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_logger, "f"));
|
|
7024
|
+
heartbeat.start();
|
|
7025
|
+
}
|
|
6974
7026
|
// Create remote and local databases
|
|
6975
7027
|
const remotedb = new remotedb_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), vcdj);
|
|
6976
7028
|
const localdb = new localdb_1.default(vcdj, __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_statusEmitter, "f"), (_f = __classPrivateFieldGet(this, _ProlinkNetwork_config, "f").databasePreference) !== null && _f !== void 0 ? _f : 'auto');
|
|
6977
7029
|
// Create unified database
|
|
6978
|
-
const database = new db_1.default(localdb, remotedb, __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"));
|
|
7030
|
+
const database = new db_1.default(localdb, remotedb, __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_logger, "f"));
|
|
6979
7031
|
// Create controller service
|
|
6980
7032
|
const control = new control_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_beatSocket, "f"), vcdj);
|
|
6981
7033
|
__classPrivateFieldSet(this, _ProlinkNetwork_state, types_1.NetworkState.Connected, "f");
|
|
6982
|
-
__classPrivateFieldSet(this, _ProlinkNetwork_connection, { announcer, control, remotedb, localdb, database }, "f");
|
|
7034
|
+
__classPrivateFieldSet(this, _ProlinkNetwork_connection, { announcer, heartbeat, control, remotedb, localdb, database }, "f");
|
|
6983
7035
|
tx.finish();
|
|
6984
7036
|
}
|
|
6985
7037
|
/**
|
|
6986
7038
|
* Disconnect from the network
|
|
6987
7039
|
*/
|
|
6988
7040
|
disconnect() {
|
|
6989
|
-
var _a, _b, _c;
|
|
7041
|
+
var _a, _b, _c, _d, _e;
|
|
6990
7042
|
if (__classPrivateFieldGet(this, _ProlinkNetwork_config, "f") === null) {
|
|
6991
7043
|
throw new Error(connectErrorHelp);
|
|
6992
7044
|
}
|
|
6993
7045
|
// Stop announcing ourself
|
|
6994
7046
|
(_a = __classPrivateFieldGet(this, _ProlinkNetwork_connection, "f")) === null || _a === void 0 ? void 0 : _a.announcer.stop();
|
|
7047
|
+
(_c = (_b = __classPrivateFieldGet(this, _ProlinkNetwork_connection, "f")) === null || _b === void 0 ? void 0 : _b.heartbeat) === null || _c === void 0 ? void 0 : _c.stop();
|
|
6995
7048
|
// Disconnect devices from the remote and local databases
|
|
6996
7049
|
for (const device of this.deviceManager.devices.values()) {
|
|
6997
|
-
(
|
|
6998
|
-
(
|
|
7050
|
+
(_d = this.remotedb) === null || _d === void 0 ? void 0 : _d.disconnectFromDevice(device);
|
|
7051
|
+
(_e = this.localdb) === null || _e === void 0 ? void 0 : _e.disconnectForDevice(device);
|
|
6999
7052
|
}
|
|
7000
7053
|
return this.close;
|
|
7001
7054
|
}
|
|
@@ -8455,15 +8508,17 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
8455
8508
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8456
8509
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8457
8510
|
};
|
|
8458
|
-
var _Connection_socket, _Connection_txId, _Connection_lock, _QueryInterface_conn, _QueryInterface_hostDevice, _QueryInterface_lock, _RemoteDatabase_hostDevice, _RemoteDatabase_deviceManager, _RemoteDatabase_connections, _RemoteDatabase_deviceLocks;
|
|
8511
|
+
var _Connection_socket, _Connection_txId, _Connection_lock, _QueryInterface_conn, _QueryInterface_hostDevice, _QueryInterface_lock, _RemoteDatabase_instances, _RemoteDatabase_hostDevice, _RemoteDatabase_deviceManager, _RemoteDatabase_connections, _RemoteDatabase_deviceLocks, _RemoteDatabase_queryIds, _RemoteDatabase_pickQueryId, _RemoteDatabase_queryIdFor;
|
|
8459
8512
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
8460
8513
|
exports.QueryInterface = exports.Connection = exports.Query = exports.MenuTarget = void 0;
|
|
8461
8514
|
exports.getQueryName = getQueryName;
|
|
8462
8515
|
const async_mutex_1 = __webpack_require__(/*! async-mutex */ "async-mutex");
|
|
8463
8516
|
const promise_socket_1 = __importDefault(__webpack_require__(/*! promise-socket */ "promise-socket"));
|
|
8464
8517
|
const net_1 = __webpack_require__(/*! net */ "net");
|
|
8518
|
+
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
8465
8519
|
const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
|
|
8466
|
-
const
|
|
8520
|
+
const device_id_1 = __webpack_require__(/*! src/virtualcdj/device-id */ "./src/virtualcdj/device-id.ts");
|
|
8521
|
+
const types_2 = __webpack_require__(/*! ./message/types */ "./src/remotedb/message/types.ts");
|
|
8467
8522
|
const constants_1 = __webpack_require__(/*! ./constants */ "./src/remotedb/constants.ts");
|
|
8468
8523
|
const fields_1 = __webpack_require__(/*! ./fields */ "./src/remotedb/fields.ts");
|
|
8469
8524
|
const message_1 = __webpack_require__(/*! ./message */ "./src/remotedb/message/index.ts");
|
|
@@ -8476,7 +8531,7 @@ var MenuTarget;
|
|
|
8476
8531
|
(function (MenuTarget) {
|
|
8477
8532
|
MenuTarget[MenuTarget["Main"] = 1] = "Main";
|
|
8478
8533
|
})(MenuTarget || (exports.MenuTarget = MenuTarget = {}));
|
|
8479
|
-
exports.Query =
|
|
8534
|
+
exports.Query = types_2.Request;
|
|
8480
8535
|
const QueryInverse = Object.fromEntries(Object.entries(exports.Query).map(e => [e[1], e[0]]));
|
|
8481
8536
|
/**
|
|
8482
8537
|
* Returns a string representation of a remote query
|
|
@@ -8522,7 +8577,7 @@ class Connection {
|
|
|
8522
8577
|
var _a;
|
|
8523
8578
|
const tx = span.startChild({
|
|
8524
8579
|
op: 'writeMessage',
|
|
8525
|
-
description: (0,
|
|
8580
|
+
description: (0, types_2.getMessageName)(message.type),
|
|
8526
8581
|
});
|
|
8527
8582
|
message.transactionId = __classPrivateFieldSet(this, _Connection_txId, (_a = __classPrivateFieldGet(this, _Connection_txId, "f"), ++_a), "f");
|
|
8528
8583
|
await __classPrivateFieldGet(this, _Connection_socket, "f").write(message.buffer);
|
|
@@ -8583,7 +8638,8 @@ _QueryInterface_conn = new WeakMap(), _QueryInterface_hostDevice = new WeakMap()
|
|
|
8583
8638
|
* Service that maintains remote database connections with devices on the network.
|
|
8584
8639
|
*/
|
|
8585
8640
|
class RemoteDatabase {
|
|
8586
|
-
constructor(deviceManager, hostDevice) {
|
|
8641
|
+
constructor(deviceManager, hostDevice, { pickQueryId = true } = {}) {
|
|
8642
|
+
_RemoteDatabase_instances.add(this);
|
|
8587
8643
|
_RemoteDatabase_hostDevice.set(this, void 0);
|
|
8588
8644
|
_RemoteDatabase_deviceManager.set(this, void 0);
|
|
8589
8645
|
/**
|
|
@@ -8594,6 +8650,17 @@ class RemoteDatabase {
|
|
|
8594
8650
|
* Locks for each device when locating the connection
|
|
8595
8651
|
*/
|
|
8596
8652
|
_RemoteDatabase_deviceLocks.set(this, new Map());
|
|
8653
|
+
/**
|
|
8654
|
+
* The device ID each connection introduced itself with. Queries on that
|
|
8655
|
+
* connection must carry the same ID.
|
|
8656
|
+
*/
|
|
8657
|
+
_RemoteDatabase_queryIds.set(this, new Map());
|
|
8658
|
+
/**
|
|
8659
|
+
* Whether to substitute an in-range query ID when the host device sits
|
|
8660
|
+
* outside 1-6. Disabled only by protocol probes that need to observe how a
|
|
8661
|
+
* device treats the raw announced ID (see examples/remotedb-id-probe.ts).
|
|
8662
|
+
*/
|
|
8663
|
+
_RemoteDatabase_pickQueryId.set(this, void 0);
|
|
8597
8664
|
/**
|
|
8598
8665
|
* Open a connection to the specified device for querying
|
|
8599
8666
|
*/
|
|
@@ -8618,20 +8685,22 @@ class RemoteDatabase {
|
|
|
8618
8685
|
throw new Error(`Expected 0x01 during preamble handshake. Got ${data.value}`);
|
|
8619
8686
|
}
|
|
8620
8687
|
// Send introduction message to set context for querying
|
|
8688
|
+
const queryId = __classPrivateFieldGet(this, _RemoteDatabase_instances, "m", _RemoteDatabase_queryIdFor).call(this, device);
|
|
8621
8689
|
const intro = new message_1.Message({
|
|
8622
8690
|
transactionId: 0xfffffffe,
|
|
8623
|
-
type:
|
|
8624
|
-
args: [new fields_1.UInt32(
|
|
8691
|
+
type: types_2.MessageType.Introduce,
|
|
8692
|
+
args: [new fields_1.UInt32(queryId)],
|
|
8625
8693
|
});
|
|
8626
8694
|
await socket.write(intro.buffer);
|
|
8627
|
-
const resp = await message_1.Message.fromStream(socket,
|
|
8628
|
-
if (resp.type !==
|
|
8695
|
+
const resp = await message_1.Message.fromStream(socket, types_2.MessageType.Success, tx);
|
|
8696
|
+
if (resp.type !== types_2.MessageType.Success) {
|
|
8629
8697
|
throw new Error(`Failed to introduce self to device ID: ${device.id}`);
|
|
8630
8698
|
}
|
|
8631
8699
|
// Clear socket timeout after successful connection — the per-device mutex
|
|
8632
8700
|
// and application-level Promise.race timeouts handle query timeouts
|
|
8633
8701
|
socket.stream.setTimeout(0);
|
|
8634
8702
|
__classPrivateFieldGet(this, _RemoteDatabase_connections, "f").set(device.id, new Connection(device, socket));
|
|
8703
|
+
__classPrivateFieldGet(this, _RemoteDatabase_queryIds, "f").set(device.id, queryId);
|
|
8635
8704
|
tx.finish();
|
|
8636
8705
|
};
|
|
8637
8706
|
/**
|
|
@@ -8645,16 +8714,29 @@ class RemoteDatabase {
|
|
|
8645
8714
|
}
|
|
8646
8715
|
const goodbye = new message_1.Message({
|
|
8647
8716
|
transactionId: 0xfffffffe,
|
|
8648
|
-
type:
|
|
8717
|
+
type: types_2.MessageType.Disconnect,
|
|
8649
8718
|
args: [],
|
|
8650
8719
|
});
|
|
8651
8720
|
await conn.writeMessage(goodbye, tx);
|
|
8652
8721
|
conn.close();
|
|
8653
8722
|
__classPrivateFieldGet(this, _RemoteDatabase_connections, "f").delete(device.id);
|
|
8723
|
+
__classPrivateFieldGet(this, _RemoteDatabase_queryIds, "f").delete(device.id);
|
|
8654
8724
|
tx.finish();
|
|
8655
8725
|
};
|
|
8656
8726
|
__classPrivateFieldSet(this, _RemoteDatabase_deviceManager, deviceManager, "f");
|
|
8657
8727
|
__classPrivateFieldSet(this, _RemoteDatabase_hostDevice, hostDevice, "f");
|
|
8728
|
+
__classPrivateFieldSet(this, _RemoteDatabase_pickQueryId, pickQueryId, "f");
|
|
8729
|
+
}
|
|
8730
|
+
/**
|
|
8731
|
+
* The device this service belongs to — the virtual CDJ announced on the
|
|
8732
|
+
* network. Note that this is NOT necessarily the ID carried inside remotedb
|
|
8733
|
+
* messages: CDJs only answer queries whose in-protocol device-ID byte is
|
|
8734
|
+
* 1-6, so when this device sits outside that range (announced above the
|
|
8735
|
+
* player range to avoid collisions), each connection picks an in-range
|
|
8736
|
+
* query ID via {@link pickRemoteDbQueryId} instead.
|
|
8737
|
+
*/
|
|
8738
|
+
get hostDevice() {
|
|
8739
|
+
return __classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f");
|
|
8658
8740
|
}
|
|
8659
8741
|
/**
|
|
8660
8742
|
* Gets the remote database query interface for the given device.
|
|
@@ -8665,7 +8747,7 @@ class RemoteDatabase {
|
|
|
8665
8747
|
* @returns null if the device does not export a remote database service
|
|
8666
8748
|
*/
|
|
8667
8749
|
async get(deviceId) {
|
|
8668
|
-
var _a;
|
|
8750
|
+
var _a, _b;
|
|
8669
8751
|
const device = __classPrivateFieldGet(this, _RemoteDatabase_deviceManager, "f").devices.get(deviceId);
|
|
8670
8752
|
if (device === undefined) {
|
|
8671
8753
|
return null;
|
|
@@ -8680,14 +8762,28 @@ class RemoteDatabase {
|
|
|
8680
8762
|
conn = __classPrivateFieldGet(this, _RemoteDatabase_connections, "f").get(deviceId);
|
|
8681
8763
|
// NOTE: We pass the same lock we use for this device to the query
|
|
8682
8764
|
// interface to ensure all query interfaces use the same lock.
|
|
8683
|
-
|
|
8765
|
+
// Queries must carry the same device ID the connection introduced
|
|
8766
|
+
// itself with, which is not always the announced host ID.
|
|
8767
|
+
const queryId = (_b = __classPrivateFieldGet(this, _RemoteDatabase_queryIds, "f").get(device.id)) !== null && _b !== void 0 ? _b : __classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f").id;
|
|
8768
|
+
return new QueryInterface(conn, lock, { ...__classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f"), id: queryId });
|
|
8684
8769
|
}
|
|
8685
8770
|
finally {
|
|
8686
8771
|
releaseLock();
|
|
8687
8772
|
}
|
|
8688
8773
|
}
|
|
8689
8774
|
}
|
|
8690
|
-
_RemoteDatabase_hostDevice = new WeakMap(), _RemoteDatabase_deviceManager = new WeakMap(), _RemoteDatabase_connections = new WeakMap(), _RemoteDatabase_deviceLocks = new WeakMap()
|
|
8775
|
+
_RemoteDatabase_hostDevice = new WeakMap(), _RemoteDatabase_deviceManager = new WeakMap(), _RemoteDatabase_connections = new WeakMap(), _RemoteDatabase_deviceLocks = new WeakMap(), _RemoteDatabase_queryIds = new WeakMap(), _RemoteDatabase_pickQueryId = new WeakMap(), _RemoteDatabase_instances = new WeakSet(), _RemoteDatabase_queryIdFor = function _RemoteDatabase_queryIdFor(device) {
|
|
8776
|
+
const hostId = __classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f").id;
|
|
8777
|
+
// An announced ID already inside the answered range keeps working exactly
|
|
8778
|
+
// as it always has. Rekordbox (which numbers itself far above 6) answers
|
|
8779
|
+
// queries regardless, so only CDJ targets need an in-range stand-in.
|
|
8780
|
+
if (!__classPrivateFieldGet(this, _RemoteDatabase_pickQueryId, "f") ||
|
|
8781
|
+
hostId <= device_id_1.REMOTEDB_MAX_DEVICE_ID ||
|
|
8782
|
+
device.type !== types_1.DeviceType.CDJ) {
|
|
8783
|
+
return hostId;
|
|
8784
|
+
}
|
|
8785
|
+
return (0, device_id_1.pickRemoteDbQueryId)(device.id, __classPrivateFieldGet(this, _RemoteDatabase_deviceManager, "f").devices.values());
|
|
8786
|
+
};
|
|
8691
8787
|
exports["default"] = RemoteDatabase;
|
|
8692
8788
|
|
|
8693
8789
|
|
|
@@ -9981,10 +10077,29 @@ exports["default"] = PositionEmitter;
|
|
|
9981
10077
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
9982
10078
|
exports.PlayState = exports.StatusFlag = void 0;
|
|
9983
10079
|
/**
|
|
9984
|
-
* Status flag bitmasks
|
|
10080
|
+
* Status flag bitmasks (byte 0x89 of the CDJ status packet).
|
|
10081
|
+
*
|
|
10082
|
+
* Verified against CDJ-3000 firmware (EP122 FW3.20): the deck assembles this
|
|
10083
|
+
* byte in `sub_d7d3c8` (0xd7d3c8) from the BeatSyncMaster state struct, one
|
|
10084
|
+
* source boolean per bit. The bits not named here:
|
|
10085
|
+
*
|
|
10086
|
+
* - bit 0 (0x01): structurally unused — no code path ever sets it (always 0).
|
|
10087
|
+
* - bit 2 (0x04): a real, dedicated beat-sync boolean (struct +5), but its
|
|
10088
|
+
* name did not survive in the stripped `usecase::sync` async-task code. It
|
|
10089
|
+
* is normally 0, which is why it has never been observed on the wire.
|
|
10090
|
+
* - bit 7 (0x80): a real bit (struct +0xa) that is default-set at init and
|
|
10091
|
+
* whose de-assertion itself triggers a state-change notification — best read
|
|
10092
|
+
* as a "sync-master active / handoff-in-progress" toggle. Also normally 0 in
|
|
10093
|
+
* steady state.
|
|
9985
10094
|
*/
|
|
9986
10095
|
var StatusFlag;
|
|
9987
10096
|
(function (StatusFlag) {
|
|
10097
|
+
/**
|
|
10098
|
+
* Degraded to BPM Sync: the player is still tracking the master's tempo, but
|
|
10099
|
+
* beat alignment was dropped after a pitch-bend / jog nudge. Firmware sources
|
|
10100
|
+
* this from BeatSyncMaster struct +4.
|
|
10101
|
+
*/
|
|
10102
|
+
StatusFlag[StatusFlag["BpmSync"] = 2] = "BpmSync";
|
|
9988
10103
|
StatusFlag[StatusFlag["OnAir"] = 8] = "OnAir";
|
|
9989
10104
|
StatusFlag[StatusFlag["Sync"] = 16] = "Sync";
|
|
9990
10105
|
StatusFlag[StatusFlag["Master"] = 32] = "Master";
|
|
@@ -10057,14 +10172,24 @@ function statusFromPacket(packet) {
|
|
|
10057
10172
|
playState: packet[0x7b],
|
|
10058
10173
|
isOnAir: (packet[0x89] & types_1.CDJStatus.StatusFlag.OnAir) !== 0,
|
|
10059
10174
|
isSync: (packet[0x89] & types_1.CDJStatus.StatusFlag.Sync) !== 0,
|
|
10175
|
+
isBpmSync: (packet[0x89] & types_1.CDJStatus.StatusFlag.BpmSync) !== 0,
|
|
10060
10176
|
isMaster: (packet[0x89] & types_1.CDJStatus.StatusFlag.Master) !== 0,
|
|
10061
10177
|
isEmergencyMode: !!packet[0xba],
|
|
10062
10178
|
trackBPM,
|
|
10063
|
-
|
|
10064
|
-
|
|
10179
|
+
// The CDJ status packet carries four pitch values (firmware CDJ-3000 FW3.20,
|
|
10180
|
+
// sub_d7d610): Pitch1@0x8c and Pitch3@0xc0 are the *effective* pitch (in
|
|
10181
|
+
// effect / on the BPM display, from the fader or a synced master), while
|
|
10182
|
+
// Pitch2@0x98 and Pitch4@0xc4 track the *local fader* position. We expose
|
|
10183
|
+
// Pitch1 as `effectivePitch` and Pitch2 as `sliderPitch`.
|
|
10184
|
+
effectivePitch: calcPitch(packet.slice(0x8d, 0x8d + 3)),
|
|
10185
|
+
sliderPitch: calcPitch(packet.slice(0x99, 0x99 + 3)),
|
|
10065
10186
|
beatInMeasure: packet[0xa6],
|
|
10066
10187
|
beatsUntilCue,
|
|
10067
10188
|
beat,
|
|
10189
|
+
// Player-type / capability byte (dysentery's "nx"). Guarded because the
|
|
10190
|
+
// length check above only guarantees 0xc8; older/short packets may not
|
|
10191
|
+
// reach 0xcc, in which case the field reads as 0 (version-gated to 0 anyway).
|
|
10192
|
+
deviceType: packet.length > 0xcc ? packet[0xcc] : 0,
|
|
10068
10193
|
packetNum: packet.readUInt32BE(0xc8),
|
|
10069
10194
|
};
|
|
10070
10195
|
return status;
|
|
@@ -10809,11 +10934,17 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
|
10809
10934
|
exports.DEFAULT_MIXER_PLAYER_CEILING = exports.MIXER_PLAYER_CEILING = exports.MAX_DEVICE_ID = exports.REMOTEDB_MAX_DEVICE_ID = void 0;
|
|
10810
10935
|
exports.playerNumberCeiling = playerNumberCeiling;
|
|
10811
10936
|
exports.pickAvailableDeviceId = pickAvailableDeviceId;
|
|
10937
|
+
exports.pickRemoteDbQueryId = pickRemoteDbQueryId;
|
|
10812
10938
|
/**
|
|
10813
10939
|
* The highest device ID a CDJ will answer remotedb queries from.
|
|
10814
10940
|
*
|
|
10815
|
-
*
|
|
10816
|
-
*
|
|
10941
|
+
* This limit applies to the device-ID byte inside the remotedb protocol (the
|
|
10942
|
+
* Introduce message and every query header), NOT to the ID we announce on the
|
|
10943
|
+
* network. Hardware-verified on a CDJ-3000 (2026-08-30, two players + DJM-V5,
|
|
10944
|
+
* announced as VCDJ 7 throughout): queries carrying 1-6 are all answered —
|
|
10945
|
+
* including IDs of absent players and the target's own ID — while 7 and above
|
|
10946
|
+
* are silently ignored (the TCP connection and Introduce succeed, the query
|
|
10947
|
+
* never gets a response). See {@link pickRemoteDbQueryId}.
|
|
10817
10948
|
*/
|
|
10818
10949
|
exports.REMOTEDB_MAX_DEVICE_ID = 6;
|
|
10819
10950
|
/**
|
|
@@ -10821,11 +10952,13 @@ exports.REMOTEDB_MAX_DEVICE_ID = 6;
|
|
|
10821
10952
|
*/
|
|
10822
10953
|
exports.MAX_DEVICE_ID = 32;
|
|
10823
10954
|
/**
|
|
10824
|
-
* `DeviceType.Mixer`, inlined so this module stays free
|
|
10825
|
-
* it is re-exported into consumers' test environments,
|
|
10826
|
-
* whole type module (and its ip-address dependency) is
|
|
10955
|
+
* `DeviceType.Mixer` and `DeviceType.CDJ`, inlined so this module stays free
|
|
10956
|
+
* of runtime imports — it is re-exported into consumers' test environments,
|
|
10957
|
+
* where pulling in the whole type module (and its ip-address dependency) is
|
|
10958
|
+
* dead weight.
|
|
10827
10959
|
*/
|
|
10828
10960
|
const MIXER_DEVICE_TYPE = 0x03;
|
|
10961
|
+
const CDJ_DEVICE_TYPE = 0x01;
|
|
10829
10962
|
/**
|
|
10830
10963
|
* How many player numbers each mixer can hand out.
|
|
10831
10964
|
*
|
|
@@ -10924,24 +11057,233 @@ const ascending = (from, to) => {
|
|
|
10924
11057
|
* Returns null when every ID from 1 to 32 is occupied, in which case there is
|
|
10925
11058
|
* no safe ID and the caller must not join the network.
|
|
10926
11059
|
*/
|
|
10927
|
-
function pickAvailableDeviceId(usedIds, {
|
|
11060
|
+
function pickAvailableDeviceId(usedIds, { preferPlayerRange = false, playerCeiling } = {}) {
|
|
10928
11061
|
var _a;
|
|
10929
11062
|
const used = new Set(usedIds);
|
|
10930
11063
|
const aboveThePlayers = playerCeiling === undefined
|
|
10931
11064
|
? descending(exports.REMOTEDB_MAX_DEVICE_ID, 1)
|
|
10932
11065
|
: ascending(playerCeiling + 1, exports.REMOTEDB_MAX_DEVICE_ID);
|
|
10933
|
-
const
|
|
11066
|
+
const outsidePlayerRange = ascending(exports.REMOTEDB_MAX_DEVICE_ID + 1, exports.MAX_DEVICE_ID);
|
|
10934
11067
|
// Numbers a player on this rig could be assigned. Last resort: better to
|
|
10935
11068
|
// contend for a slot than not to join at all, and the announcer moves us if
|
|
10936
11069
|
// the player that owns it shows up.
|
|
10937
11070
|
const amongThePlayers = playerCeiling === undefined
|
|
10938
11071
|
? []
|
|
10939
11072
|
: descending(Math.min(playerCeiling, exports.REMOTEDB_MAX_DEVICE_ID), 1);
|
|
10940
|
-
const search =
|
|
10941
|
-
? [...aboveThePlayers, ...
|
|
10942
|
-
: [...
|
|
11073
|
+
const search = preferPlayerRange
|
|
11074
|
+
? [...aboveThePlayers, ...outsidePlayerRange, ...amongThePlayers]
|
|
11075
|
+
: [...outsidePlayerRange, ...aboveThePlayers, ...amongThePlayers];
|
|
10943
11076
|
return (_a = search.find(id => !used.has(id))) !== null && _a !== void 0 ? _a : null;
|
|
10944
11077
|
}
|
|
11078
|
+
/**
|
|
11079
|
+
* Choose the device ID to carry inside remotedb messages when querying a CDJ.
|
|
11080
|
+
*
|
|
11081
|
+
* CDJs only answer remotedb queries whose in-protocol device-ID byte is in
|
|
11082
|
+
* 1-6, but that byte is independent of the ID we announce on the network — so
|
|
11083
|
+
* a virtual CDJ announced safely above the player range (say 7 behind a
|
|
11084
|
+
* DJM-V10) can still query metadata by carrying an in-range ID here. Verified
|
|
11085
|
+
* on a CDJ-3000 (2026-08-30): absent IDs and even the target's own ID are
|
|
11086
|
+
* answered; 7+ is silently dropped.
|
|
11087
|
+
*
|
|
11088
|
+
* Older players are documented (dysentery, nexus era) as stricter: the byte
|
|
11089
|
+
* had to be 1-4, belong to a player actually on the network, and not be the
|
|
11090
|
+
* player being queried. The preference order below satisfies those rules
|
|
11091
|
+
* whenever the rig makes it possible, then falls back through choices newer
|
|
11092
|
+
* players accept.
|
|
11093
|
+
*/
|
|
11094
|
+
function pickRemoteDbQueryId(targetId, devices) {
|
|
11095
|
+
const playerIds = new Set();
|
|
11096
|
+
for (const device of devices) {
|
|
11097
|
+
if (device.type === CDJ_DEVICE_TYPE && device.id <= exports.REMOTEDB_MAX_DEVICE_ID) {
|
|
11098
|
+
playerIds.add(device.id);
|
|
11099
|
+
}
|
|
11100
|
+
}
|
|
11101
|
+
// A live player that isn't the target, lowest first — 1-4 satisfies every
|
|
11102
|
+
// documented era of the restriction.
|
|
11103
|
+
const otherPlayers = [...playerIds].filter(id => id !== targetId).sort((a, b) => a - b);
|
|
11104
|
+
if (otherPlayers.length > 0) {
|
|
11105
|
+
return otherPlayers[0];
|
|
11106
|
+
}
|
|
11107
|
+
// No other player on the rig: take the lowest 1-6 ID no player holds.
|
|
11108
|
+
for (let id = 1; id <= exports.REMOTEDB_MAX_DEVICE_ID; id++) {
|
|
11109
|
+
if (id !== targetId && !playerIds.has(id)) {
|
|
11110
|
+
return id;
|
|
11111
|
+
}
|
|
11112
|
+
}
|
|
11113
|
+
// Six players and every one of them is the target? Impossible, but the
|
|
11114
|
+
// target's own ID is answered too, so it is a safe closing fallback.
|
|
11115
|
+
return targetId;
|
|
11116
|
+
}
|
|
11117
|
+
|
|
11118
|
+
|
|
11119
|
+
/***/ },
|
|
11120
|
+
|
|
11121
|
+
/***/ "./src/virtualcdj/heartbeat.ts"
|
|
11122
|
+
/*!*************************************!*\
|
|
11123
|
+
!*** ./src/virtualcdj/heartbeat.ts ***!
|
|
11124
|
+
\*************************************/
|
|
11125
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
11126
|
+
|
|
11127
|
+
"use strict";
|
|
11128
|
+
|
|
11129
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
11130
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
11131
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
11132
|
+
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");
|
|
11133
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11134
|
+
};
|
|
11135
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
11136
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
11137
|
+
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");
|
|
11138
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11139
|
+
};
|
|
11140
|
+
var _StagehandHeartbeat_instances, _StagehandHeartbeat_statusSocket, _StagehandHeartbeat_vcdj, _StagehandHeartbeat_deviceManager, _StagehandHeartbeat_logger, _StagehandHeartbeat_intervalHandle, _StagehandHeartbeat_sendHeartbeats;
|
|
11141
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
11142
|
+
exports.StagehandHeartbeat = exports.STAGEHAND_HEARTBEAT_INTERVAL = void 0;
|
|
11143
|
+
exports.makeStagehandMixerHeartbeat = makeStagehandMixerHeartbeat;
|
|
11144
|
+
exports.makeStagehandPlayerHeartbeat = makeStagehandPlayerHeartbeat;
|
|
11145
|
+
const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
|
|
11146
|
+
const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
|
|
11147
|
+
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
11148
|
+
/**
|
|
11149
|
+
* Cadence of the Stagehand unicast keep-alive, in milliseconds.
|
|
11150
|
+
*
|
|
11151
|
+
* The real iOS Stagehand app heartbeats every discovered player and the mixer
|
|
11152
|
+
* at ~4 Hz (250ms median inter-packet gap in captured traffic). This is the
|
|
11153
|
+
* stream that keeps AlphaTheta hardware unicasting live state (`0x39` mixer
|
|
11154
|
+
* fader/EQ, `0x58` VU on the mixer; `0x69` slim status, waveform families on
|
|
11155
|
+
* the CDJs) to our IP — subnet-broadcast presence alone (the `0x06` keep-alive)
|
|
11156
|
+
* is not enough to bootstrap it.
|
|
11157
|
+
*/
|
|
11158
|
+
exports.STAGEHAND_HEARTBEAT_INTERVAL = 250;
|
|
11159
|
+
/**
|
|
11160
|
+
* Builds a Stagehand name field for a unicast frame.
|
|
11161
|
+
*
|
|
11162
|
+
* Unlike the 20-byte {@link buildName} used by the broadcast announce/claim
|
|
11163
|
+
* frames, unicast frames carry a 19-byte name field (offsets 11..29) with the
|
|
11164
|
+
* device type / model bytes beginning at offset 30. The name is ASCII, NUL
|
|
11165
|
+
* padded.
|
|
11166
|
+
*/
|
|
11167
|
+
function buildUnicastName(name) {
|
|
11168
|
+
const field = new Uint8Array(19);
|
|
11169
|
+
field.set(Buffer.from(name, 'ascii').subarray(0, 19));
|
|
11170
|
+
return field;
|
|
11171
|
+
}
|
|
11172
|
+
/**
|
|
11173
|
+
* Build the Stagehand mixer keep-alive (`0x3a`, 40 bytes).
|
|
11174
|
+
*
|
|
11175
|
+
* Verified byte-for-byte against the real iOS Stagehand app unicasting to a
|
|
11176
|
+
* DJM-A9 at 4 Hz (Phase 4/5 captures §4.14.2). The 10-byte trailer
|
|
11177
|
+
* `21 02 00 fe 00 04 00 1c 00 00` is constant across every observed sample —
|
|
11178
|
+
* byte 33 (`0xfe`) is a VCDJ sentinel, not our runtime device number, so no
|
|
11179
|
+
* per-device bytes vary here.
|
|
11180
|
+
*/
|
|
11181
|
+
function makeStagehandMixerHeartbeat(vcdj) {
|
|
11182
|
+
const parts = [
|
|
11183
|
+
...constants_1.PROLINK_HEADER, // 0-9: magic
|
|
11184
|
+
0x3a, // 10: type 0x3a (mixer keep-alive)
|
|
11185
|
+
...buildUnicastName(vcdj.name), // 11-29: name (19 bytes)
|
|
11186
|
+
0x21,
|
|
11187
|
+
0x02,
|
|
11188
|
+
0x00,
|
|
11189
|
+
0xfe,
|
|
11190
|
+
0x00,
|
|
11191
|
+
0x04,
|
|
11192
|
+
0x00,
|
|
11193
|
+
0x1c,
|
|
11194
|
+
0x00,
|
|
11195
|
+
0x00, // 30-39: constant trailer
|
|
11196
|
+
];
|
|
11197
|
+
return Uint8Array.from(parts);
|
|
11198
|
+
}
|
|
11199
|
+
/**
|
|
11200
|
+
* Build the Stagehand player keep-alive (`0x68`, 36 bytes).
|
|
11201
|
+
*
|
|
11202
|
+
* Verified byte-for-byte against the real iOS Stagehand app unicasting to a
|
|
11203
|
+
* CDJ-3000 at ~2-4 Hz. Body (offsets 30-35) is `03 01 00 3a 00 00`: `0x03`
|
|
11204
|
+
* persona/channel, `0x01` device subkind, `0x00` flag, `0x3a` Stagehand
|
|
11205
|
+
* model-code stamp, two reserved bytes. Constant across every observed sample.
|
|
11206
|
+
*/
|
|
11207
|
+
function makeStagehandPlayerHeartbeat(vcdj) {
|
|
11208
|
+
const parts = [
|
|
11209
|
+
...constants_1.PROLINK_HEADER, // 0-9: magic
|
|
11210
|
+
0x68, // 10: type 0x68 (player keep-alive)
|
|
11211
|
+
...buildUnicastName(vcdj.name), // 11-29: name (19 bytes)
|
|
11212
|
+
0x03,
|
|
11213
|
+
0x01,
|
|
11214
|
+
0x00,
|
|
11215
|
+
0x3a,
|
|
11216
|
+
0x00,
|
|
11217
|
+
0x00, // 30-35: constant body
|
|
11218
|
+
];
|
|
11219
|
+
return Uint8Array.from(parts);
|
|
11220
|
+
}
|
|
11221
|
+
/**
|
|
11222
|
+
* Unicasts Stagehand keep-alive frames to every discovered player and mixer so
|
|
11223
|
+
* that AlphaTheta hardware begins (and keeps) pushing live state to our IP.
|
|
11224
|
+
*
|
|
11225
|
+
* The {@link StagehandAnnouncer} makes us *visible* on the network via subnet
|
|
11226
|
+
* broadcast; this heartbeat is what makes hardware actually *talk back*. It
|
|
11227
|
+
* targets the mixer with `0x3a` and each CDJ with `0x68`, mirroring the real
|
|
11228
|
+
* iOS Stagehand app. Frames are sent from the status socket (port 50002) to
|
|
11229
|
+
* each device's port 50002, which is where their unicast state replies land and
|
|
11230
|
+
* where {@link StatusEmitter} listens for them.
|
|
11231
|
+
*/
|
|
11232
|
+
class StagehandHeartbeat {
|
|
11233
|
+
constructor(vcdj, statusSocket, deviceManager, logger = logger_1.noopLogger) {
|
|
11234
|
+
_StagehandHeartbeat_instances.add(this);
|
|
11235
|
+
_StagehandHeartbeat_statusSocket.set(this, void 0);
|
|
11236
|
+
_StagehandHeartbeat_vcdj.set(this, void 0);
|
|
11237
|
+
_StagehandHeartbeat_deviceManager.set(this, void 0);
|
|
11238
|
+
_StagehandHeartbeat_logger.set(this, void 0);
|
|
11239
|
+
_StagehandHeartbeat_intervalHandle.set(this, void 0);
|
|
11240
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_vcdj, vcdj, "f");
|
|
11241
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_statusSocket, statusSocket, "f");
|
|
11242
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_deviceManager, deviceManager, "f");
|
|
11243
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_logger, logger, "f");
|
|
11244
|
+
}
|
|
11245
|
+
start() {
|
|
11246
|
+
if (__classPrivateFieldGet(this, _StagehandHeartbeat_intervalHandle, "f") !== undefined) {
|
|
11247
|
+
return;
|
|
11248
|
+
}
|
|
11249
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_logger, "f").info('Starting Stagehand heartbeat (unicast keep-alive)');
|
|
11250
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_instances, "m", _StagehandHeartbeat_sendHeartbeats).call(this);
|
|
11251
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_intervalHandle, setInterval(() => __classPrivateFieldGet(this, _StagehandHeartbeat_instances, "m", _StagehandHeartbeat_sendHeartbeats).call(this), exports.STAGEHAND_HEARTBEAT_INTERVAL), "f");
|
|
11252
|
+
}
|
|
11253
|
+
stop() {
|
|
11254
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_logger, "f").info('Stopping Stagehand heartbeat');
|
|
11255
|
+
if (__classPrivateFieldGet(this, _StagehandHeartbeat_intervalHandle, "f") !== undefined) {
|
|
11256
|
+
clearInterval(__classPrivateFieldGet(this, _StagehandHeartbeat_intervalHandle, "f"));
|
|
11257
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_intervalHandle, undefined, "f");
|
|
11258
|
+
}
|
|
11259
|
+
}
|
|
11260
|
+
}
|
|
11261
|
+
exports.StagehandHeartbeat = StagehandHeartbeat;
|
|
11262
|
+
_StagehandHeartbeat_statusSocket = new WeakMap(), _StagehandHeartbeat_vcdj = new WeakMap(), _StagehandHeartbeat_deviceManager = new WeakMap(), _StagehandHeartbeat_logger = new WeakMap(), _StagehandHeartbeat_intervalHandle = new WeakMap(), _StagehandHeartbeat_instances = new WeakSet(), _StagehandHeartbeat_sendHeartbeats = function _StagehandHeartbeat_sendHeartbeats() {
|
|
11263
|
+
for (const device of __classPrivateFieldGet(this, _StagehandHeartbeat_deviceManager, "f").devices.values()) {
|
|
11264
|
+
// Never heartbeat ourselves.
|
|
11265
|
+
if (device.id === __classPrivateFieldGet(this, _StagehandHeartbeat_vcdj, "f").id) {
|
|
11266
|
+
continue;
|
|
11267
|
+
}
|
|
11268
|
+
let packet = null;
|
|
11269
|
+
switch (device.type) {
|
|
11270
|
+
case types_1.DeviceType.Mixer:
|
|
11271
|
+
packet = makeStagehandMixerHeartbeat(__classPrivateFieldGet(this, _StagehandHeartbeat_vcdj, "f"));
|
|
11272
|
+
break;
|
|
11273
|
+
case types_1.DeviceType.CDJ:
|
|
11274
|
+
packet = makeStagehandPlayerHeartbeat(__classPrivateFieldGet(this, _StagehandHeartbeat_vcdj, "f"));
|
|
11275
|
+
break;
|
|
11276
|
+
default:
|
|
11277
|
+
// Rekordbox / other Stagehand peers are not heartbeat targets.
|
|
11278
|
+
continue;
|
|
11279
|
+
}
|
|
11280
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_statusSocket, "f").send(packet, constants_1.STATUS_PORT, device.ip.address, err => {
|
|
11281
|
+
if (err) {
|
|
11282
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_logger, "f").debug(`Stagehand heartbeat to ${device.name} (${device.ip.address}) failed: ${err.message}`);
|
|
11283
|
+
}
|
|
11284
|
+
});
|
|
11285
|
+
}
|
|
11286
|
+
};
|
|
10945
11287
|
|
|
10946
11288
|
|
|
10947
11289
|
/***/ },
|
|
@@ -11370,16 +11712,16 @@ _Announcer_announceSocket = new WeakMap(), _Announcer_deviceManager = new WeakMa
|
|
|
11370
11712
|
}, _Announcer_handleConflict = function _Announcer_handleConflict() {
|
|
11371
11713
|
// Stop announcing under the contested ID
|
|
11372
11714
|
__classPrivateFieldGet(this, _Announcer_instances, "m", _Announcer_clearTimer).call(this);
|
|
11373
|
-
// Stay in whichever range we were already in
|
|
11374
|
-
//
|
|
11375
|
-
//
|
|
11715
|
+
// Stay in whichever range we were already in, so a conflict on player 5
|
|
11716
|
+
// looks for another free player number first rather than jumping the
|
|
11717
|
+
// whole device out of the range the DJ configured it into.
|
|
11376
11718
|
//
|
|
11377
11719
|
// The mixer decides which of those numbers are actually safe: a player can
|
|
11378
11720
|
// only be assigned a number its mixer has a channel for, so anything above
|
|
11379
11721
|
// that is out of reach of the rig that just contested us.
|
|
11380
11722
|
const devices = [...__classPrivateFieldGet(this, _Announcer_deviceManager, "f").devices.values()];
|
|
11381
11723
|
const newId = (0, device_id_1.pickAvailableDeviceId)(devices.map(d => d.id), {
|
|
11382
|
-
|
|
11724
|
+
preferPlayerRange: __classPrivateFieldGet(this, _Announcer_vcdj, "f").id <= device_id_1.REMOTEDB_MAX_DEVICE_ID,
|
|
11383
11725
|
playerCeiling: (0, device_id_1.playerNumberCeiling)(devices),
|
|
11384
11726
|
});
|
|
11385
11727
|
if (newId === null) {
|
|
@@ -11482,6 +11824,7 @@ _Announcer_announceSocket = new WeakMap(), _Announcer_deviceManager = new WeakMa
|
|
|
11482
11824
|
__classPrivateFieldSet(this, _Announcer_intervalHandle, setInterval(sendKeepAlive, constants_1.ANNOUNCE_INTERVAL), "f");
|
|
11483
11825
|
};
|
|
11484
11826
|
__exportStar(__webpack_require__(/*! ./device-id */ "./src/virtualcdj/device-id.ts"), exports);
|
|
11827
|
+
__exportStar(__webpack_require__(/*! ./heartbeat */ "./src/virtualcdj/heartbeat.ts"), exports);
|
|
11485
11828
|
__exportStar(__webpack_require__(/*! ./stagehand */ "./src/virtualcdj/stagehand.ts"), exports);
|
|
11486
11829
|
|
|
11487
11830
|
|