alphatheta-connect 0.23.2 → 0.25.1
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 +515 -115
- package/lib/cli.js.map +1 -1
- package/lib/constants.d.ts +4 -2
- package/lib/db/getMetadata.d.ts +13 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.js +517 -116
- package/lib/index.js.map +1 -1
- package/lib/localdb/index.d.ts +47 -1
- package/lib/network.d.ts +10 -9
- package/lib/remotedb/index.d.ts +9 -5
- 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/index.js
CHANGED
|
@@ -631,8 +631,10 @@ exports.MAX_CDJ_DEVICE_ID = exports.MIN_CDJ_DEVICE_ID = exports.VIRTUAL_CDJ_FIRM
|
|
|
631
631
|
/**
|
|
632
632
|
* The default virtual CDJ ID to use.
|
|
633
633
|
*
|
|
634
|
-
*
|
|
635
|
-
* metadata
|
|
634
|
+
* Out of the 1-6 player range, so it can never collide with a real CDJ.
|
|
635
|
+
* Remotedb metadata still works from here: the 1-6 restriction applies to the
|
|
636
|
+
* device-ID byte inside remotedb messages, which RemoteDatabase picks
|
|
637
|
+
* per-connection independently of the announced ID.
|
|
636
638
|
*/
|
|
637
639
|
exports.DEFAULT_VCDJ_ID = 0x07;
|
|
638
640
|
/**
|
|
@@ -1266,25 +1268,24 @@ async function viaRemote(remote, opts) {
|
|
|
1266
1268
|
return track;
|
|
1267
1269
|
}
|
|
1268
1270
|
async function viaLocal(local, device, opts) {
|
|
1269
|
-
const { deviceId, trackSlot, trackId } = opts;
|
|
1271
|
+
const { deviceId, trackSlot, trackId, trackBPM } = opts;
|
|
1270
1272
|
if (trackSlot !== types_1.MediaSlot.USB && trackSlot !== types_1.MediaSlot.SD) {
|
|
1271
1273
|
throw new Error('Expected USB or SD slot for local database query');
|
|
1272
1274
|
}
|
|
1273
|
-
const
|
|
1274
|
-
if (adapter === null) {
|
|
1275
|
-
return { track: null, miss: 'no-database' };
|
|
1275
|
+
const lookup = await local.findTrack(deviceId, trackSlot, trackId, { trackBPM });
|
|
1276
|
+
if (lookup.adapter === null) {
|
|
1277
|
+
return { track: null, miss: 'no-database', switchedTo: null };
|
|
1276
1278
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
return { track: null, miss: 'track-absent' };
|
|
1279
|
+
if (lookup.track === null) {
|
|
1280
|
+
return { track: null, miss: 'track-absent', switchedTo: null };
|
|
1280
1281
|
}
|
|
1281
|
-
const anlz = await (0, rekordbox_1.loadAnlz)(
|
|
1282
|
+
const anlz = await (0, rekordbox_1.loadAnlz)(lookup.track, 'DAT', (0, utils_1.anlzLoader)({ device, slot: trackSlot }));
|
|
1282
1283
|
const track = {
|
|
1283
|
-
...
|
|
1284
|
+
...lookup.track,
|
|
1284
1285
|
beatGrid: anlz.beatGrid,
|
|
1285
1286
|
waveformHd: null,
|
|
1286
1287
|
};
|
|
1287
|
-
return { track, miss: null };
|
|
1288
|
+
return { track, miss: null, switchedTo: lookup.switchedTo };
|
|
1288
1289
|
}
|
|
1289
1290
|
|
|
1290
1291
|
|
|
@@ -1536,7 +1537,6 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
1536
1537
|
};
|
|
1537
1538
|
var _Database_instances, _Database_deviceManager, _Database_localDatabase, _Database_remoteDatabase, _Database_logger, _Database_getTrackLookupStrategy, _Database_getMediaLookupStrategy, _Database_metadataViaRemoteFallback;
|
|
1538
1539
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1539
|
-
const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
|
|
1540
1540
|
const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
|
|
1541
1541
|
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
1542
1542
|
const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
|
|
@@ -1609,6 +1609,7 @@ class Database {
|
|
|
1609
1609
|
* Retrieve metadata for a track on a specific device slot.
|
|
1610
1610
|
*/
|
|
1611
1611
|
async getMetadata(opts) {
|
|
1612
|
+
var _a;
|
|
1612
1613
|
const { deviceId, trackType, trackSlot, span } = opts;
|
|
1613
1614
|
const tx = span
|
|
1614
1615
|
? span.startChild({ op: 'dbGetMetadata' })
|
|
@@ -1616,7 +1617,7 @@ class Database {
|
|
|
1616
1617
|
tx.setTag('deviceId', deviceId.toString());
|
|
1617
1618
|
tx.setTag('trackType', (0, utils_1.getTrackTypeName)(trackType));
|
|
1618
1619
|
tx.setTag('trackSlot', (0, utils_1.getSlotName)(trackSlot));
|
|
1619
|
-
const callOpts = { ...opts, span: tx };
|
|
1620
|
+
const callOpts = { ...opts, trackBPM: (_a = opts.trackBPM) !== null && _a !== void 0 ? _a : null, span: tx };
|
|
1620
1621
|
const device = await __classPrivateFieldGet(this, _Database_deviceManager, "f").getDeviceEnsured(deviceId);
|
|
1621
1622
|
if (device === null) {
|
|
1622
1623
|
return null;
|
|
@@ -1629,6 +1630,12 @@ class Database {
|
|
|
1629
1630
|
if (strategy === LookupStrategy.Local) {
|
|
1630
1631
|
const local = await GetMetadata.viaLocal(__classPrivateFieldGet(this, _Database_localDatabase, "f"), device, callOpts);
|
|
1631
1632
|
track = local.track;
|
|
1633
|
+
if (local.switchedTo !== null) {
|
|
1634
|
+
__classPrivateFieldGet(this, _Database_logger, "f").info(`Device ${deviceId} ${(0, utils_1.getSlotName)(trackSlot)} is now read from the ` +
|
|
1635
|
+
`${local.switchedTo === 'pdb' ? 'legacy PDB' : 'OneLibrary'} database: ` +
|
|
1636
|
+
`it is the one that holds track ${opts.trackId} as the player reports it ` +
|
|
1637
|
+
`(NP3-399)`);
|
|
1638
|
+
}
|
|
1632
1639
|
// A local miss used to end the lookup, so the DJ's track silently never
|
|
1633
1640
|
// reached the overlay for the rest of the set (NP3-361). Say what was
|
|
1634
1641
|
// missed, then give the CDJ's own database a chance to answer.
|
|
@@ -1836,20 +1843,14 @@ _Database_deviceManager = new WeakMap(), _Database_localDatabase = new WeakMap()
|
|
|
1836
1843
|
/**
|
|
1837
1844
|
* Second chance for a track the local database could not answer for.
|
|
1838
1845
|
*
|
|
1839
|
-
*
|
|
1840
|
-
* ID in the 1-6 range,
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1846
|
+
* Available regardless of the virtual CDJ's announced ID: CDJs restrict
|
|
1847
|
+
* remote database queries to a device-ID byte in the 1-6 range, but that
|
|
1848
|
+
* byte lives inside the remotedb messages and RemoteDatabase picks an
|
|
1849
|
+
* in-range one per connection — the announced ID never constrains this
|
|
1850
|
+
* lookup. A failure here is logged rather than thrown, so a field report
|
|
1851
|
+
* says what actually went wrong.
|
|
1844
1852
|
*/
|
|
1845
1853
|
async function _Database_metadataViaRemoteFallback(opts) {
|
|
1846
|
-
const hostId = __classPrivateFieldGet(this, _Database_remoteDatabase, "f").hostDevice.id;
|
|
1847
|
-
if (hostId < constants_1.MIN_CDJ_DEVICE_ID || hostId > constants_1.MAX_CDJ_DEVICE_ID) {
|
|
1848
|
-
__classPrivateFieldGet(this, _Database_logger, "f").warn(`No remote fallback for track ${opts.trackId}: this player is device ` +
|
|
1849
|
-
`${hostId}, and CDJs only answer remote database queries from ` +
|
|
1850
|
-
`devices ${constants_1.MIN_CDJ_DEVICE_ID}-${constants_1.MAX_CDJ_DEVICE_ID}`);
|
|
1851
|
-
return null;
|
|
1852
|
-
}
|
|
1853
1854
|
try {
|
|
1854
1855
|
const track = await GetMetadata.viaRemote(__classPrivateFieldGet(this, _Database_remoteDatabase, "f"), opts);
|
|
1855
1856
|
if (track === null) {
|
|
@@ -2134,7 +2135,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
2134
2135
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
2135
2136
|
};
|
|
2136
2137
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2137
|
-
exports.noopLogger = exports.REMOTEDB_MAX_DEVICE_ID = exports.playerNumberCeiling = 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;
|
|
2138
|
+
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;
|
|
2138
2139
|
__exportStar(__webpack_require__(/*! ./entities */ "./src/entities.ts"), exports);
|
|
2139
2140
|
__exportStar(__webpack_require__(/*! ./mixstatus */ "./src/mixstatus/index.ts"), exports);
|
|
2140
2141
|
__exportStar(__webpack_require__(/*! ./network */ "./src/network.ts"), exports);
|
|
@@ -2162,6 +2163,7 @@ Object.defineProperty(exports, "DEFAULT_MIXER_PLAYER_CEILING", ({ enumerable: tr
|
|
|
2162
2163
|
Object.defineProperty(exports, "MAX_DEVICE_ID", ({ enumerable: true, get: function () { return device_id_1.MAX_DEVICE_ID; } }));
|
|
2163
2164
|
Object.defineProperty(exports, "MIXER_PLAYER_CEILING", ({ enumerable: true, get: function () { return device_id_1.MIXER_PLAYER_CEILING; } }));
|
|
2164
2165
|
Object.defineProperty(exports, "pickAvailableDeviceId", ({ enumerable: true, get: function () { return device_id_1.pickAvailableDeviceId; } }));
|
|
2166
|
+
Object.defineProperty(exports, "pickRemoteDbQueryId", ({ enumerable: true, get: function () { return device_id_1.pickRemoteDbQueryId; } }));
|
|
2165
2167
|
Object.defineProperty(exports, "playerNumberCeiling", ({ enumerable: true, get: function () { return device_id_1.playerNumberCeiling; } }));
|
|
2166
2168
|
Object.defineProperty(exports, "REMOTEDB_MAX_DEVICE_ID", ({ enumerable: true, get: function () { return device_id_1.REMOTEDB_MAX_DEVICE_ID; } }));
|
|
2167
2169
|
var logger_1 = __webpack_require__(/*! ./logger */ "./src/logger.ts");
|
|
@@ -2224,7 +2226,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
2224
2226
|
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");
|
|
2225
2227
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
2226
2228
|
};
|
|
2227
|
-
var _LocalDatabase_hostDevice, _LocalDatabase_deviceManager, _LocalDatabase_statusEmitter, _LocalDatabase_emitter, _LocalDatabase_slotLocks, _LocalDatabase_dbs, _LocalDatabase_preference, _LocalDatabase_handleDeviceRemoved, _LocalDatabase_fetchFileWithFallback, _LocalDatabase_tryLoadOneLibrary, _LocalDatabase_loadPdbDatabase, _LocalDatabase_hydrateDatabase;
|
|
2229
|
+
var _LocalDatabase_instances, _LocalDatabase_hostDevice, _LocalDatabase_deviceManager, _LocalDatabase_statusEmitter, _LocalDatabase_emitter, _LocalDatabase_slotLocks, _LocalDatabase_dbs, _LocalDatabase_preference, _LocalDatabase_handleDeviceRemoved, _LocalDatabase_closeLoaded, _LocalDatabase_fetchFileWithFallback, _LocalDatabase_tryLoadOneLibrary, _LocalDatabase_loadPdbDatabase, _LocalDatabase_hydrateDatabase, _LocalDatabase_loadAlternate, _LocalDatabase_getItem;
|
|
2228
2230
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2229
2231
|
const async_mutex_1 = __webpack_require__(/*! async-mutex */ "async-mutex");
|
|
2230
2232
|
const onelibrary_connect_1 = __webpack_require__(/*! onelibrary-connect */ "onelibrary-connect");
|
|
@@ -2240,6 +2242,18 @@ const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
|
|
|
2240
2242
|
const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
|
|
2241
2243
|
const orm_1 = __webpack_require__(/*! ./orm */ "./src/localdb/orm.ts");
|
|
2242
2244
|
const rekordbox_1 = __webpack_require__(/*! ./rekordbox */ "./src/localdb/rekordbox.ts");
|
|
2245
|
+
/**
|
|
2246
|
+
* A row is only trusted when the player's reported BPM agrees with it. Either
|
|
2247
|
+
* side missing (unanalysed track, or a player that reports no BPM) is
|
|
2248
|
+
* inconclusive and counts as agreement.
|
|
2249
|
+
*/
|
|
2250
|
+
const trackAgreesWithPlayer = (track, hint) => {
|
|
2251
|
+
const reported = hint.trackBPM;
|
|
2252
|
+
if (reported === null || reported === undefined || !track.tempo) {
|
|
2253
|
+
return true;
|
|
2254
|
+
}
|
|
2255
|
+
return Math.abs(track.tempo - reported) < 0.05;
|
|
2256
|
+
};
|
|
2243
2257
|
/**
|
|
2244
2258
|
* Compute the identifier for media device in a CDJ. This is used to determine
|
|
2245
2259
|
* if we have already hydrated the device or not into our local database.
|
|
@@ -2266,6 +2280,7 @@ const getMediaId = (info) => {
|
|
|
2266
2280
|
*/
|
|
2267
2281
|
class LocalDatabase {
|
|
2268
2282
|
constructor(hostDevice, deviceManager, statusEmitter, preference = 'auto') {
|
|
2283
|
+
_LocalDatabase_instances.add(this);
|
|
2269
2284
|
_LocalDatabase_hostDevice.set(this, void 0);
|
|
2270
2285
|
_LocalDatabase_deviceManager.set(this, void 0);
|
|
2271
2286
|
_LocalDatabase_statusEmitter.set(this, void 0);
|
|
@@ -2297,19 +2312,25 @@ class LocalDatabase {
|
|
|
2297
2312
|
_LocalDatabase_handleDeviceRemoved.set(this, (device) => {
|
|
2298
2313
|
const db = __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").find(db => db.media.deviceId === device.id);
|
|
2299
2314
|
if (db) {
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
try {
|
|
2304
|
-
fs.unlinkSync(db.tempFile);
|
|
2305
|
-
}
|
|
2306
|
-
catch {
|
|
2307
|
-
// Ignore cleanup errors
|
|
2308
|
-
}
|
|
2315
|
+
__classPrivateFieldGet(this, _LocalDatabase_closeLoaded, "f").call(this, db);
|
|
2316
|
+
if (db.alternate) {
|
|
2317
|
+
__classPrivateFieldGet(this, _LocalDatabase_closeLoaded, "f").call(this, db.alternate);
|
|
2309
2318
|
}
|
|
2310
2319
|
}
|
|
2311
2320
|
__classPrivateFieldSet(this, _LocalDatabase_dbs, __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").filter(db => db.media.deviceId !== device.id), "f");
|
|
2312
2321
|
});
|
|
2322
|
+
_LocalDatabase_closeLoaded.set(this, (loaded) => {
|
|
2323
|
+
loaded.adapter.close();
|
|
2324
|
+
// Clean up temp file if it exists (OneLibrary databases)
|
|
2325
|
+
if (loaded.tempFile) {
|
|
2326
|
+
try {
|
|
2327
|
+
fs.unlinkSync(loaded.tempFile);
|
|
2328
|
+
}
|
|
2329
|
+
catch {
|
|
2330
|
+
// Ignore cleanup errors
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
});
|
|
2313
2334
|
/**
|
|
2314
2335
|
* Helper to fetch a file from device, trying both dotted and non-dotted paths
|
|
2315
2336
|
*/
|
|
@@ -2416,11 +2437,54 @@ class LocalDatabase {
|
|
|
2416
2437
|
}
|
|
2417
2438
|
}
|
|
2418
2439
|
__classPrivateFieldGet(this, _LocalDatabase_emitter, "f").emit('hydrationDone', { device, slot });
|
|
2419
|
-
const db = {
|
|
2440
|
+
const db = {
|
|
2441
|
+
adapter,
|
|
2442
|
+
media,
|
|
2443
|
+
device,
|
|
2444
|
+
slot,
|
|
2445
|
+
id: getMediaId(media),
|
|
2446
|
+
tempFile,
|
|
2447
|
+
alternateLock: new async_mutex_1.Mutex(),
|
|
2448
|
+
};
|
|
2420
2449
|
__classPrivateFieldGet(this, _LocalDatabase_dbs, "f").push(db);
|
|
2421
2450
|
tx.finish();
|
|
2422
2451
|
return db;
|
|
2423
2452
|
});
|
|
2453
|
+
/**
|
|
2454
|
+
* Loads the database format the slot is *not* currently served from, so a
|
|
2455
|
+
* lookup can be checked against it. Only meaningful under the 'auto'
|
|
2456
|
+
* preference: a forced format has no alternate. The result (or its absence)
|
|
2457
|
+
* is remembered so the media is never downloaded twice.
|
|
2458
|
+
*/
|
|
2459
|
+
_LocalDatabase_loadAlternate.set(this, (db) => db.alternateLock.runExclusive(async () => {
|
|
2460
|
+
if (db.alternate !== undefined) {
|
|
2461
|
+
return db.alternate;
|
|
2462
|
+
}
|
|
2463
|
+
if (__classPrivateFieldGet(this, _LocalDatabase_preference, "f") !== 'auto') {
|
|
2464
|
+
db.alternate = null;
|
|
2465
|
+
return null;
|
|
2466
|
+
}
|
|
2467
|
+
const tx = Telemetry.startTransaction({ name: 'hydrateAlternateDatabase' });
|
|
2468
|
+
tx.setTag('slot', (0, utils_1.getSlotName)(db.media.slot));
|
|
2469
|
+
tx.setTag('activeDbType', db.adapter.type);
|
|
2470
|
+
try {
|
|
2471
|
+
if (db.adapter.type === 'oneLibrary') {
|
|
2472
|
+
const adapter = await __classPrivateFieldGet(this, _LocalDatabase_loadPdbDatabase, "f").call(this, db.device, db.slot, tx);
|
|
2473
|
+
db.alternate = { adapter };
|
|
2474
|
+
tx.setTag('dbType', 'pdb');
|
|
2475
|
+
}
|
|
2476
|
+
else {
|
|
2477
|
+
db.alternate = await __classPrivateFieldGet(this, _LocalDatabase_tryLoadOneLibrary, "f").call(this, db.device, db.slot, tx);
|
|
2478
|
+
tx.setTag('dbType', db.alternate ? 'oneLibrary' : 'none');
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
catch {
|
|
2482
|
+
// The media simply has no such file (or it could not be read).
|
|
2483
|
+
db.alternate = null;
|
|
2484
|
+
}
|
|
2485
|
+
tx.finish();
|
|
2486
|
+
return db.alternate;
|
|
2487
|
+
}));
|
|
2424
2488
|
__classPrivateFieldSet(this, _LocalDatabase_hostDevice, hostDevice, "f");
|
|
2425
2489
|
__classPrivateFieldSet(this, _LocalDatabase_deviceManager, deviceManager, "f");
|
|
2426
2490
|
__classPrivateFieldSet(this, _LocalDatabase_statusEmitter, statusEmitter, "f");
|
|
@@ -2445,6 +2509,45 @@ class LocalDatabase {
|
|
|
2445
2509
|
disconnectForDevice(device) {
|
|
2446
2510
|
__classPrivateFieldGet(this, _LocalDatabase_handleDeviceRemoved, "f").call(this, device);
|
|
2447
2511
|
}
|
|
2512
|
+
/**
|
|
2513
|
+
* Looks a track up in the databases of a device slot, checking the answer
|
|
2514
|
+
* against what the player reports.
|
|
2515
|
+
*
|
|
2516
|
+
* A Device Library Plus export carries both `exportLibrary.db` and the
|
|
2517
|
+
* legacy `export.pdb`, and their track IDs are different number spaces. A
|
|
2518
|
+
* player that reads one while we loaded the other resolves most IDs to a
|
|
2519
|
+
* different track and some to nothing at all (NP3-399). So when the active
|
|
2520
|
+
* database has no such row, or its row's BPM is not the BPM the player is
|
|
2521
|
+
* showing, the other format is loaded and asked. If it agrees with the
|
|
2522
|
+
* player, the slot switches to it for every later lookup — artwork,
|
|
2523
|
+
* analysis and the next track all follow the database the player is using.
|
|
2524
|
+
*
|
|
2525
|
+
* When neither database can be confirmed, the active database's row (if any)
|
|
2526
|
+
* is returned as before.
|
|
2527
|
+
*/
|
|
2528
|
+
async findTrack(deviceId, slot, trackId, hint = {}) {
|
|
2529
|
+
const db = await __classPrivateFieldGet(this, _LocalDatabase_instances, "m", _LocalDatabase_getItem).call(this, deviceId, slot);
|
|
2530
|
+
if (db === null) {
|
|
2531
|
+
return { adapter: null, track: null, switchedTo: null };
|
|
2532
|
+
}
|
|
2533
|
+
const active = db.adapter.findTrack(trackId);
|
|
2534
|
+
if (active !== null && trackAgreesWithPlayer(active, hint)) {
|
|
2535
|
+
return { adapter: db.adapter, track: active, switchedTo: null };
|
|
2536
|
+
}
|
|
2537
|
+
const alternate = await __classPrivateFieldGet(this, _LocalDatabase_loadAlternate, "f").call(this, db);
|
|
2538
|
+
if (alternate !== null) {
|
|
2539
|
+
const other = alternate.adapter.findTrack(trackId);
|
|
2540
|
+
if (other !== null && trackAgreesWithPlayer(other, hint)) {
|
|
2541
|
+
db.alternate = { adapter: db.adapter, tempFile: db.tempFile };
|
|
2542
|
+
db.adapter = alternate.adapter;
|
|
2543
|
+
db.tempFile = alternate.tempFile;
|
|
2544
|
+
return { adapter: db.adapter, track: other, switchedTo: db.adapter.type };
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return active === null
|
|
2548
|
+
? { adapter: db.adapter, track: null, switchedTo: null }
|
|
2549
|
+
: { adapter: db.adapter, track: active, switchedTo: null };
|
|
2550
|
+
}
|
|
2448
2551
|
/**
|
|
2449
2552
|
* Gets the database adapter for the media metadata in the provided device slot.
|
|
2450
2553
|
*
|
|
@@ -2455,43 +2558,8 @@ class LocalDatabase {
|
|
|
2455
2558
|
*/
|
|
2456
2559
|
async get(deviceId, slot) {
|
|
2457
2560
|
var _a;
|
|
2458
|
-
const
|
|
2459
|
-
|
|
2460
|
-
const device = __classPrivateFieldGet(this, _LocalDatabase_deviceManager, "f").devices.get(deviceId);
|
|
2461
|
-
if (device === undefined) {
|
|
2462
|
-
return null;
|
|
2463
|
-
}
|
|
2464
|
-
if (device.type !== types_1.DeviceType.CDJ ||
|
|
2465
|
-
device.id < constants_1.MIN_CDJ_DEVICE_ID ||
|
|
2466
|
-
device.id > constants_1.MAX_CDJ_DEVICE_ID) {
|
|
2467
|
-
return null;
|
|
2468
|
-
}
|
|
2469
|
-
let media;
|
|
2470
|
-
try {
|
|
2471
|
-
media = await __classPrivateFieldGet(this, _LocalDatabase_statusEmitter, "f").queryMediaSlot({
|
|
2472
|
-
hostDevice: __classPrivateFieldGet(this, _LocalDatabase_hostDevice, "f"),
|
|
2473
|
-
device,
|
|
2474
|
-
slot,
|
|
2475
|
-
});
|
|
2476
|
-
}
|
|
2477
|
-
catch {
|
|
2478
|
-
// Timeout or other error - treat as no media
|
|
2479
|
-
return null;
|
|
2480
|
-
}
|
|
2481
|
-
if (media.tracksType !== types_1.TrackType.RB) {
|
|
2482
|
-
return null;
|
|
2483
|
-
}
|
|
2484
|
-
const id = getMediaId(media);
|
|
2485
|
-
// Acquire a lock for this device slot that will not release until we've
|
|
2486
|
-
// guaranteed the existence of the database.
|
|
2487
|
-
const db = await lock.runExclusive(() => {
|
|
2488
|
-
const cached = __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").find(db => db.id === id);
|
|
2489
|
-
if (cached) {
|
|
2490
|
-
return cached;
|
|
2491
|
-
}
|
|
2492
|
-
return __classPrivateFieldGet(this, _LocalDatabase_hydrateDatabase, "f").call(this, device, slot, media);
|
|
2493
|
-
});
|
|
2494
|
-
return db.adapter;
|
|
2561
|
+
const db = await __classPrivateFieldGet(this, _LocalDatabase_instances, "m", _LocalDatabase_getItem).call(this, deviceId, slot);
|
|
2562
|
+
return (_a = db === null || db === void 0 ? void 0 : db.adapter) !== null && _a !== void 0 ? _a : null;
|
|
2495
2563
|
}
|
|
2496
2564
|
/**
|
|
2497
2565
|
* Get the database type for an already-loaded device slot.
|
|
@@ -2517,7 +2585,50 @@ class LocalDatabase {
|
|
|
2517
2585
|
await Promise.all(loaders);
|
|
2518
2586
|
}
|
|
2519
2587
|
}
|
|
2520
|
-
_LocalDatabase_hostDevice = new WeakMap(), _LocalDatabase_deviceManager = new WeakMap(), _LocalDatabase_statusEmitter = new WeakMap(), _LocalDatabase_emitter = new WeakMap(), _LocalDatabase_slotLocks = new WeakMap(), _LocalDatabase_dbs = new WeakMap(), _LocalDatabase_preference = new WeakMap(), _LocalDatabase_handleDeviceRemoved = new WeakMap(), _LocalDatabase_fetchFileWithFallback = new WeakMap(), _LocalDatabase_tryLoadOneLibrary = new WeakMap(), _LocalDatabase_loadPdbDatabase = new WeakMap(), _LocalDatabase_hydrateDatabase = new WeakMap()
|
|
2588
|
+
_LocalDatabase_hostDevice = new WeakMap(), _LocalDatabase_deviceManager = new WeakMap(), _LocalDatabase_statusEmitter = new WeakMap(), _LocalDatabase_emitter = new WeakMap(), _LocalDatabase_slotLocks = new WeakMap(), _LocalDatabase_dbs = new WeakMap(), _LocalDatabase_preference = new WeakMap(), _LocalDatabase_handleDeviceRemoved = new WeakMap(), _LocalDatabase_closeLoaded = new WeakMap(), _LocalDatabase_fetchFileWithFallback = new WeakMap(), _LocalDatabase_tryLoadOneLibrary = new WeakMap(), _LocalDatabase_loadPdbDatabase = new WeakMap(), _LocalDatabase_hydrateDatabase = new WeakMap(), _LocalDatabase_loadAlternate = new WeakMap(), _LocalDatabase_instances = new WeakSet(), _LocalDatabase_getItem =
|
|
2589
|
+
/**
|
|
2590
|
+
* The loaded database entry for a device slot, hydrating it first if needed.
|
|
2591
|
+
*/
|
|
2592
|
+
async function _LocalDatabase_getItem(deviceId, slot) {
|
|
2593
|
+
var _a;
|
|
2594
|
+
const lockKey = `${deviceId}-${slot}`;
|
|
2595
|
+
const lock = (_a = __classPrivateFieldGet(this, _LocalDatabase_slotLocks, "f").get(lockKey)) !== null && _a !== void 0 ? _a : __classPrivateFieldGet(this, _LocalDatabase_slotLocks, "f").set(lockKey, new async_mutex_1.Mutex()).get(lockKey);
|
|
2596
|
+
const device = __classPrivateFieldGet(this, _LocalDatabase_deviceManager, "f").devices.get(deviceId);
|
|
2597
|
+
if (device === undefined) {
|
|
2598
|
+
return null;
|
|
2599
|
+
}
|
|
2600
|
+
if (device.type !== types_1.DeviceType.CDJ ||
|
|
2601
|
+
device.id < constants_1.MIN_CDJ_DEVICE_ID ||
|
|
2602
|
+
device.id > constants_1.MAX_CDJ_DEVICE_ID) {
|
|
2603
|
+
return null;
|
|
2604
|
+
}
|
|
2605
|
+
let media;
|
|
2606
|
+
try {
|
|
2607
|
+
media = await __classPrivateFieldGet(this, _LocalDatabase_statusEmitter, "f").queryMediaSlot({
|
|
2608
|
+
hostDevice: __classPrivateFieldGet(this, _LocalDatabase_hostDevice, "f"),
|
|
2609
|
+
device,
|
|
2610
|
+
slot,
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
catch {
|
|
2614
|
+
// Timeout or other error - treat as no media
|
|
2615
|
+
return null;
|
|
2616
|
+
}
|
|
2617
|
+
if (media.tracksType !== types_1.TrackType.RB) {
|
|
2618
|
+
return null;
|
|
2619
|
+
}
|
|
2620
|
+
const id = getMediaId(media);
|
|
2621
|
+
// Acquire a lock for this device slot that will not release until we've
|
|
2622
|
+
// guaranteed the existence of the database.
|
|
2623
|
+
const db = await lock.runExclusive(() => {
|
|
2624
|
+
const cached = __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").find(db => db.id === id);
|
|
2625
|
+
if (cached) {
|
|
2626
|
+
return cached;
|
|
2627
|
+
}
|
|
2628
|
+
return __classPrivateFieldGet(this, _LocalDatabase_hydrateDatabase, "f").call(this, device, slot, media);
|
|
2629
|
+
});
|
|
2630
|
+
return db;
|
|
2631
|
+
};
|
|
2521
2632
|
exports["default"] = LocalDatabase;
|
|
2522
2633
|
|
|
2523
2634
|
|
|
@@ -6793,7 +6904,7 @@ class MixstatusProcessor {
|
|
|
6793
6904
|
// able to report it as live
|
|
6794
6905
|
const startedAt = __classPrivateFieldGet(this, _MixstatusProcessor_lastStartTime, "f").get(deviceId);
|
|
6795
6906
|
const requiredPlayTime = __classPrivateFieldGet(this, _MixstatusProcessor_config, "f").beatsUntilReported *
|
|
6796
|
-
(0, utils_1.bpmToSeconds)(state.trackBPM, state.
|
|
6907
|
+
(0, utils_1.bpmToSeconds)(state.trackBPM, state.effectivePitch) *
|
|
6797
6908
|
1000;
|
|
6798
6909
|
if (__classPrivateFieldGet(this, _MixstatusProcessor_config, "f").mode === types_1.MixstatusMode.SmartTiming &&
|
|
6799
6910
|
startedAt !== undefined &&
|
|
@@ -6804,7 +6915,7 @@ class MixstatusProcessor {
|
|
|
6804
6915
|
// we can mark the track as truly stopped.
|
|
6805
6916
|
const stoppedAt = __classPrivateFieldGet(this, _MixstatusProcessor_lastStoppedTimes, "f").get(deviceId);
|
|
6806
6917
|
const requiredStopTime = __classPrivateFieldGet(this, _MixstatusProcessor_config, "f").allowedInterruptBeats *
|
|
6807
|
-
(0, utils_1.bpmToSeconds)(state.trackBPM, state.
|
|
6918
|
+
(0, utils_1.bpmToSeconds)(state.trackBPM, state.effectivePitch) *
|
|
6808
6919
|
1000;
|
|
6809
6920
|
if (stoppedAt !== undefined && requiredStopTime <= Date.now() - stoppedAt) {
|
|
6810
6921
|
__classPrivateFieldGet(this, _MixstatusProcessor_markPlayerStopped, "f").call(this, state);
|
|
@@ -7095,6 +7206,14 @@ class ProlinkNetwork {
|
|
|
7095
7206
|
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"));
|
|
7096
7207
|
}
|
|
7097
7208
|
announcer.start();
|
|
7209
|
+
// In Stagehand mode, unicast keep-alives to discovered players and the
|
|
7210
|
+
// mixer so they push live state to us (mixer 0x39/0x58, CDJ 0x69/waveforms).
|
|
7211
|
+
// Broadcast presence alone does not bootstrap that stream.
|
|
7212
|
+
let heartbeat = null;
|
|
7213
|
+
if (connectMethod === 'stagehand') {
|
|
7214
|
+
heartbeat = new virtualcdj_1.StagehandHeartbeat(vcdj, __classPrivateFieldGet(this, _ProlinkNetwork_statusSocket, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_logger, "f"));
|
|
7215
|
+
heartbeat.start();
|
|
7216
|
+
}
|
|
7098
7217
|
// Create remote and local databases
|
|
7099
7218
|
const remotedb = new remotedb_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), vcdj);
|
|
7100
7219
|
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');
|
|
@@ -7103,23 +7222,24 @@ class ProlinkNetwork {
|
|
|
7103
7222
|
// Create controller service
|
|
7104
7223
|
const control = new control_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_beatSocket, "f"), vcdj);
|
|
7105
7224
|
__classPrivateFieldSet(this, _ProlinkNetwork_state, types_1.NetworkState.Connected, "f");
|
|
7106
|
-
__classPrivateFieldSet(this, _ProlinkNetwork_connection, { announcer, control, remotedb, localdb, database }, "f");
|
|
7225
|
+
__classPrivateFieldSet(this, _ProlinkNetwork_connection, { announcer, heartbeat, control, remotedb, localdb, database }, "f");
|
|
7107
7226
|
tx.finish();
|
|
7108
7227
|
}
|
|
7109
7228
|
/**
|
|
7110
7229
|
* Disconnect from the network
|
|
7111
7230
|
*/
|
|
7112
7231
|
disconnect() {
|
|
7113
|
-
var _a, _b, _c;
|
|
7232
|
+
var _a, _b, _c, _d, _e;
|
|
7114
7233
|
if (__classPrivateFieldGet(this, _ProlinkNetwork_config, "f") === null) {
|
|
7115
7234
|
throw new Error(connectErrorHelp);
|
|
7116
7235
|
}
|
|
7117
7236
|
// Stop announcing ourself
|
|
7118
7237
|
(_a = __classPrivateFieldGet(this, _ProlinkNetwork_connection, "f")) === null || _a === void 0 ? void 0 : _a.announcer.stop();
|
|
7238
|
+
(_c = (_b = __classPrivateFieldGet(this, _ProlinkNetwork_connection, "f")) === null || _b === void 0 ? void 0 : _b.heartbeat) === null || _c === void 0 ? void 0 : _c.stop();
|
|
7119
7239
|
// Disconnect devices from the remote and local databases
|
|
7120
7240
|
for (const device of this.deviceManager.devices.values()) {
|
|
7121
|
-
(
|
|
7122
|
-
(
|
|
7241
|
+
(_d = this.remotedb) === null || _d === void 0 ? void 0 : _d.disconnectFromDevice(device);
|
|
7242
|
+
(_e = this.localdb) === null || _e === void 0 ? void 0 : _e.disconnectForDevice(device);
|
|
7123
7243
|
}
|
|
7124
7244
|
return this.close;
|
|
7125
7245
|
}
|
|
@@ -10649,15 +10769,17 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
10649
10769
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10650
10770
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10651
10771
|
};
|
|
10652
|
-
var _Connection_socket, _Connection_txId, _Connection_lock, _QueryInterface_conn, _QueryInterface_hostDevice, _QueryInterface_lock, _RemoteDatabase_hostDevice, _RemoteDatabase_deviceManager, _RemoteDatabase_connections, _RemoteDatabase_deviceLocks;
|
|
10772
|
+
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;
|
|
10653
10773
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
10654
10774
|
exports.QueryInterface = exports.Connection = exports.Query = exports.MenuTarget = void 0;
|
|
10655
10775
|
exports.getQueryName = getQueryName;
|
|
10656
10776
|
const async_mutex_1 = __webpack_require__(/*! async-mutex */ "async-mutex");
|
|
10657
10777
|
const promise_socket_1 = __importDefault(__webpack_require__(/*! promise-socket */ "promise-socket"));
|
|
10658
10778
|
const net_1 = __webpack_require__(/*! net */ "net");
|
|
10779
|
+
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
10659
10780
|
const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
|
|
10660
|
-
const
|
|
10781
|
+
const device_id_1 = __webpack_require__(/*! src/virtualcdj/device-id */ "./src/virtualcdj/device-id.ts");
|
|
10782
|
+
const types_2 = __webpack_require__(/*! ./message/types */ "./src/remotedb/message/types.ts");
|
|
10661
10783
|
const constants_1 = __webpack_require__(/*! ./constants */ "./src/remotedb/constants.ts");
|
|
10662
10784
|
const fields_1 = __webpack_require__(/*! ./fields */ "./src/remotedb/fields.ts");
|
|
10663
10785
|
const message_1 = __webpack_require__(/*! ./message */ "./src/remotedb/message/index.ts");
|
|
@@ -10670,7 +10792,7 @@ var MenuTarget;
|
|
|
10670
10792
|
(function (MenuTarget) {
|
|
10671
10793
|
MenuTarget[MenuTarget["Main"] = 1] = "Main";
|
|
10672
10794
|
})(MenuTarget || (exports.MenuTarget = MenuTarget = {}));
|
|
10673
|
-
exports.Query =
|
|
10795
|
+
exports.Query = types_2.Request;
|
|
10674
10796
|
const QueryInverse = Object.fromEntries(Object.entries(exports.Query).map(e => [e[1], e[0]]));
|
|
10675
10797
|
/**
|
|
10676
10798
|
* Returns a string representation of a remote query
|
|
@@ -10716,7 +10838,7 @@ class Connection {
|
|
|
10716
10838
|
var _a;
|
|
10717
10839
|
const tx = span.startChild({
|
|
10718
10840
|
op: 'writeMessage',
|
|
10719
|
-
description: (0,
|
|
10841
|
+
description: (0, types_2.getMessageName)(message.type),
|
|
10720
10842
|
});
|
|
10721
10843
|
message.transactionId = __classPrivateFieldSet(this, _Connection_txId, (_a = __classPrivateFieldGet(this, _Connection_txId, "f"), ++_a), "f");
|
|
10722
10844
|
await __classPrivateFieldGet(this, _Connection_socket, "f").write(message.buffer);
|
|
@@ -10777,7 +10899,8 @@ _QueryInterface_conn = new WeakMap(), _QueryInterface_hostDevice = new WeakMap()
|
|
|
10777
10899
|
* Service that maintains remote database connections with devices on the network.
|
|
10778
10900
|
*/
|
|
10779
10901
|
class RemoteDatabase {
|
|
10780
|
-
constructor(deviceManager, hostDevice) {
|
|
10902
|
+
constructor(deviceManager, hostDevice, { pickQueryId = true } = {}) {
|
|
10903
|
+
_RemoteDatabase_instances.add(this);
|
|
10781
10904
|
_RemoteDatabase_hostDevice.set(this, void 0);
|
|
10782
10905
|
_RemoteDatabase_deviceManager.set(this, void 0);
|
|
10783
10906
|
/**
|
|
@@ -10788,6 +10911,17 @@ class RemoteDatabase {
|
|
|
10788
10911
|
* Locks for each device when locating the connection
|
|
10789
10912
|
*/
|
|
10790
10913
|
_RemoteDatabase_deviceLocks.set(this, new Map());
|
|
10914
|
+
/**
|
|
10915
|
+
* The device ID each connection introduced itself with. Queries on that
|
|
10916
|
+
* connection must carry the same ID.
|
|
10917
|
+
*/
|
|
10918
|
+
_RemoteDatabase_queryIds.set(this, new Map());
|
|
10919
|
+
/**
|
|
10920
|
+
* Whether to substitute an in-range query ID when the host device sits
|
|
10921
|
+
* outside 1-6. Disabled only by protocol probes that need to observe how a
|
|
10922
|
+
* device treats the raw announced ID (see examples/remotedb-id-probe.ts).
|
|
10923
|
+
*/
|
|
10924
|
+
_RemoteDatabase_pickQueryId.set(this, void 0);
|
|
10791
10925
|
/**
|
|
10792
10926
|
* Open a connection to the specified device for querying
|
|
10793
10927
|
*/
|
|
@@ -10812,20 +10946,22 @@ class RemoteDatabase {
|
|
|
10812
10946
|
throw new Error(`Expected 0x01 during preamble handshake. Got ${data.value}`);
|
|
10813
10947
|
}
|
|
10814
10948
|
// Send introduction message to set context for querying
|
|
10949
|
+
const queryId = __classPrivateFieldGet(this, _RemoteDatabase_instances, "m", _RemoteDatabase_queryIdFor).call(this, device);
|
|
10815
10950
|
const intro = new message_1.Message({
|
|
10816
10951
|
transactionId: 0xfffffffe,
|
|
10817
|
-
type:
|
|
10818
|
-
args: [new fields_1.UInt32(
|
|
10952
|
+
type: types_2.MessageType.Introduce,
|
|
10953
|
+
args: [new fields_1.UInt32(queryId)],
|
|
10819
10954
|
});
|
|
10820
10955
|
await socket.write(intro.buffer);
|
|
10821
|
-
const resp = await message_1.Message.fromStream(socket,
|
|
10822
|
-
if (resp.type !==
|
|
10956
|
+
const resp = await message_1.Message.fromStream(socket, types_2.MessageType.Success, tx);
|
|
10957
|
+
if (resp.type !== types_2.MessageType.Success) {
|
|
10823
10958
|
throw new Error(`Failed to introduce self to device ID: ${device.id}`);
|
|
10824
10959
|
}
|
|
10825
10960
|
// Clear socket timeout after successful connection — the per-device mutex
|
|
10826
10961
|
// and application-level Promise.race timeouts handle query timeouts
|
|
10827
10962
|
socket.stream.setTimeout(0);
|
|
10828
10963
|
__classPrivateFieldGet(this, _RemoteDatabase_connections, "f").set(device.id, new Connection(device, socket));
|
|
10964
|
+
__classPrivateFieldGet(this, _RemoteDatabase_queryIds, "f").set(device.id, queryId);
|
|
10829
10965
|
tx.finish();
|
|
10830
10966
|
};
|
|
10831
10967
|
/**
|
|
@@ -10839,22 +10975,26 @@ class RemoteDatabase {
|
|
|
10839
10975
|
}
|
|
10840
10976
|
const goodbye = new message_1.Message({
|
|
10841
10977
|
transactionId: 0xfffffffe,
|
|
10842
|
-
type:
|
|
10978
|
+
type: types_2.MessageType.Disconnect,
|
|
10843
10979
|
args: [],
|
|
10844
10980
|
});
|
|
10845
10981
|
await conn.writeMessage(goodbye, tx);
|
|
10846
10982
|
conn.close();
|
|
10847
10983
|
__classPrivateFieldGet(this, _RemoteDatabase_connections, "f").delete(device.id);
|
|
10984
|
+
__classPrivateFieldGet(this, _RemoteDatabase_queryIds, "f").delete(device.id);
|
|
10848
10985
|
tx.finish();
|
|
10849
10986
|
};
|
|
10850
10987
|
__classPrivateFieldSet(this, _RemoteDatabase_deviceManager, deviceManager, "f");
|
|
10851
10988
|
__classPrivateFieldSet(this, _RemoteDatabase_hostDevice, hostDevice, "f");
|
|
10989
|
+
__classPrivateFieldSet(this, _RemoteDatabase_pickQueryId, pickQueryId, "f");
|
|
10852
10990
|
}
|
|
10853
10991
|
/**
|
|
10854
|
-
* The device
|
|
10855
|
-
*
|
|
10856
|
-
*
|
|
10857
|
-
*
|
|
10992
|
+
* The device this service belongs to — the virtual CDJ announced on the
|
|
10993
|
+
* network. Note that this is NOT necessarily the ID carried inside remotedb
|
|
10994
|
+
* messages: CDJs only answer queries whose in-protocol device-ID byte is
|
|
10995
|
+
* 1-6, so when this device sits outside that range (announced above the
|
|
10996
|
+
* player range to avoid collisions), each connection picks an in-range
|
|
10997
|
+
* query ID via {@link pickRemoteDbQueryId} instead.
|
|
10858
10998
|
*/
|
|
10859
10999
|
get hostDevice() {
|
|
10860
11000
|
return __classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f");
|
|
@@ -10868,7 +11008,7 @@ class RemoteDatabase {
|
|
|
10868
11008
|
* @returns null if the device does not export a remote database service
|
|
10869
11009
|
*/
|
|
10870
11010
|
async get(deviceId) {
|
|
10871
|
-
var _a;
|
|
11011
|
+
var _a, _b;
|
|
10872
11012
|
const device = __classPrivateFieldGet(this, _RemoteDatabase_deviceManager, "f").devices.get(deviceId);
|
|
10873
11013
|
if (device === undefined) {
|
|
10874
11014
|
return null;
|
|
@@ -10883,14 +11023,28 @@ class RemoteDatabase {
|
|
|
10883
11023
|
conn = __classPrivateFieldGet(this, _RemoteDatabase_connections, "f").get(deviceId);
|
|
10884
11024
|
// NOTE: We pass the same lock we use for this device to the query
|
|
10885
11025
|
// interface to ensure all query interfaces use the same lock.
|
|
10886
|
-
|
|
11026
|
+
// Queries must carry the same device ID the connection introduced
|
|
11027
|
+
// itself with, which is not always the announced host ID.
|
|
11028
|
+
const queryId = (_b = __classPrivateFieldGet(this, _RemoteDatabase_queryIds, "f").get(device.id)) !== null && _b !== void 0 ? _b : __classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f").id;
|
|
11029
|
+
return new QueryInterface(conn, lock, { ...__classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f"), id: queryId });
|
|
10887
11030
|
}
|
|
10888
11031
|
finally {
|
|
10889
11032
|
releaseLock();
|
|
10890
11033
|
}
|
|
10891
11034
|
}
|
|
10892
11035
|
}
|
|
10893
|
-
_RemoteDatabase_hostDevice = new WeakMap(), _RemoteDatabase_deviceManager = new WeakMap(), _RemoteDatabase_connections = new WeakMap(), _RemoteDatabase_deviceLocks = new WeakMap()
|
|
11036
|
+
_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) {
|
|
11037
|
+
const hostId = __classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f").id;
|
|
11038
|
+
// An announced ID already inside the answered range keeps working exactly
|
|
11039
|
+
// as it always has. Rekordbox (which numbers itself far above 6) answers
|
|
11040
|
+
// queries regardless, so only CDJ targets need an in-range stand-in.
|
|
11041
|
+
if (!__classPrivateFieldGet(this, _RemoteDatabase_pickQueryId, "f") ||
|
|
11042
|
+
hostId <= device_id_1.REMOTEDB_MAX_DEVICE_ID ||
|
|
11043
|
+
device.type !== types_1.DeviceType.CDJ) {
|
|
11044
|
+
return hostId;
|
|
11045
|
+
}
|
|
11046
|
+
return (0, device_id_1.pickRemoteDbQueryId)(device.id, __classPrivateFieldGet(this, _RemoteDatabase_deviceManager, "f").devices.values());
|
|
11047
|
+
};
|
|
10894
11048
|
exports["default"] = RemoteDatabase;
|
|
10895
11049
|
|
|
10896
11050
|
|
|
@@ -12184,10 +12338,29 @@ exports["default"] = PositionEmitter;
|
|
|
12184
12338
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
12185
12339
|
exports.PlayState = exports.StatusFlag = void 0;
|
|
12186
12340
|
/**
|
|
12187
|
-
* Status flag bitmasks
|
|
12341
|
+
* Status flag bitmasks (byte 0x89 of the CDJ status packet).
|
|
12342
|
+
*
|
|
12343
|
+
* Verified against CDJ-3000 firmware (EP122 FW3.20): the deck assembles this
|
|
12344
|
+
* byte in `sub_d7d3c8` (0xd7d3c8) from the BeatSyncMaster state struct, one
|
|
12345
|
+
* source boolean per bit. The bits not named here:
|
|
12346
|
+
*
|
|
12347
|
+
* - bit 0 (0x01): structurally unused — no code path ever sets it (always 0).
|
|
12348
|
+
* - bit 2 (0x04): a real, dedicated beat-sync boolean (struct +5), but its
|
|
12349
|
+
* name did not survive in the stripped `usecase::sync` async-task code. It
|
|
12350
|
+
* is normally 0, which is why it has never been observed on the wire.
|
|
12351
|
+
* - bit 7 (0x80): a real bit (struct +0xa) that is default-set at init and
|
|
12352
|
+
* whose de-assertion itself triggers a state-change notification — best read
|
|
12353
|
+
* as a "sync-master active / handoff-in-progress" toggle. Also normally 0 in
|
|
12354
|
+
* steady state.
|
|
12188
12355
|
*/
|
|
12189
12356
|
var StatusFlag;
|
|
12190
12357
|
(function (StatusFlag) {
|
|
12358
|
+
/**
|
|
12359
|
+
* Degraded to BPM Sync: the player is still tracking the master's tempo, but
|
|
12360
|
+
* beat alignment was dropped after a pitch-bend / jog nudge. Firmware sources
|
|
12361
|
+
* this from BeatSyncMaster struct +4.
|
|
12362
|
+
*/
|
|
12363
|
+
StatusFlag[StatusFlag["BpmSync"] = 2] = "BpmSync";
|
|
12191
12364
|
StatusFlag[StatusFlag["OnAir"] = 8] = "OnAir";
|
|
12192
12365
|
StatusFlag[StatusFlag["Sync"] = 16] = "Sync";
|
|
12193
12366
|
StatusFlag[StatusFlag["Master"] = 32] = "Master";
|
|
@@ -12260,14 +12433,24 @@ function statusFromPacket(packet) {
|
|
|
12260
12433
|
playState: packet[0x7b],
|
|
12261
12434
|
isOnAir: (packet[0x89] & types_1.CDJStatus.StatusFlag.OnAir) !== 0,
|
|
12262
12435
|
isSync: (packet[0x89] & types_1.CDJStatus.StatusFlag.Sync) !== 0,
|
|
12436
|
+
isBpmSync: (packet[0x89] & types_1.CDJStatus.StatusFlag.BpmSync) !== 0,
|
|
12263
12437
|
isMaster: (packet[0x89] & types_1.CDJStatus.StatusFlag.Master) !== 0,
|
|
12264
12438
|
isEmergencyMode: !!packet[0xba],
|
|
12265
12439
|
trackBPM,
|
|
12266
|
-
|
|
12267
|
-
|
|
12440
|
+
// The CDJ status packet carries four pitch values (firmware CDJ-3000 FW3.20,
|
|
12441
|
+
// sub_d7d610): Pitch1@0x8c and Pitch3@0xc0 are the *effective* pitch (in
|
|
12442
|
+
// effect / on the BPM display, from the fader or a synced master), while
|
|
12443
|
+
// Pitch2@0x98 and Pitch4@0xc4 track the *local fader* position. We expose
|
|
12444
|
+
// Pitch1 as `effectivePitch` and Pitch2 as `sliderPitch`.
|
|
12445
|
+
effectivePitch: calcPitch(packet.slice(0x8d, 0x8d + 3)),
|
|
12446
|
+
sliderPitch: calcPitch(packet.slice(0x99, 0x99 + 3)),
|
|
12268
12447
|
beatInMeasure: packet[0xa6],
|
|
12269
12448
|
beatsUntilCue,
|
|
12270
12449
|
beat,
|
|
12450
|
+
// Player-type / capability byte (dysentery's "nx"). Guarded because the
|
|
12451
|
+
// length check above only guarantees 0xc8; older/short packets may not
|
|
12452
|
+
// reach 0xcc, in which case the field reads as 0 (version-gated to 0 anyway).
|
|
12453
|
+
deviceType: packet.length > 0xcc ? packet[0xcc] : 0,
|
|
12271
12454
|
packetNum: packet.readUInt32BE(0xc8),
|
|
12272
12455
|
};
|
|
12273
12456
|
return status;
|
|
@@ -13012,11 +13195,17 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
|
13012
13195
|
exports.DEFAULT_MIXER_PLAYER_CEILING = exports.MIXER_PLAYER_CEILING = exports.MAX_DEVICE_ID = exports.REMOTEDB_MAX_DEVICE_ID = void 0;
|
|
13013
13196
|
exports.playerNumberCeiling = playerNumberCeiling;
|
|
13014
13197
|
exports.pickAvailableDeviceId = pickAvailableDeviceId;
|
|
13198
|
+
exports.pickRemoteDbQueryId = pickRemoteDbQueryId;
|
|
13015
13199
|
/**
|
|
13016
13200
|
* The highest device ID a CDJ will answer remotedb queries from.
|
|
13017
13201
|
*
|
|
13018
|
-
*
|
|
13019
|
-
*
|
|
13202
|
+
* This limit applies to the device-ID byte inside the remotedb protocol (the
|
|
13203
|
+
* Introduce message and every query header), NOT to the ID we announce on the
|
|
13204
|
+
* network. Hardware-verified on a CDJ-3000 (2026-08-30, two players + DJM-V5,
|
|
13205
|
+
* announced as VCDJ 7 throughout): queries carrying 1-6 are all answered —
|
|
13206
|
+
* including IDs of absent players and the target's own ID — while 7 and above
|
|
13207
|
+
* are silently ignored (the TCP connection and Introduce succeed, the query
|
|
13208
|
+
* never gets a response). See {@link pickRemoteDbQueryId}.
|
|
13020
13209
|
*/
|
|
13021
13210
|
exports.REMOTEDB_MAX_DEVICE_ID = 6;
|
|
13022
13211
|
/**
|
|
@@ -13024,11 +13213,13 @@ exports.REMOTEDB_MAX_DEVICE_ID = 6;
|
|
|
13024
13213
|
*/
|
|
13025
13214
|
exports.MAX_DEVICE_ID = 32;
|
|
13026
13215
|
/**
|
|
13027
|
-
* `DeviceType.Mixer`, inlined so this module stays free
|
|
13028
|
-
* it is re-exported into consumers' test environments,
|
|
13029
|
-
* whole type module (and its ip-address dependency) is
|
|
13216
|
+
* `DeviceType.Mixer` and `DeviceType.CDJ`, inlined so this module stays free
|
|
13217
|
+
* of runtime imports — it is re-exported into consumers' test environments,
|
|
13218
|
+
* where pulling in the whole type module (and its ip-address dependency) is
|
|
13219
|
+
* dead weight.
|
|
13030
13220
|
*/
|
|
13031
13221
|
const MIXER_DEVICE_TYPE = 0x03;
|
|
13222
|
+
const CDJ_DEVICE_TYPE = 0x01;
|
|
13032
13223
|
/**
|
|
13033
13224
|
* How many player numbers each mixer can hand out.
|
|
13034
13225
|
*
|
|
@@ -13127,24 +13318,233 @@ const ascending = (from, to) => {
|
|
|
13127
13318
|
* Returns null when every ID from 1 to 32 is occupied, in which case there is
|
|
13128
13319
|
* no safe ID and the caller must not join the network.
|
|
13129
13320
|
*/
|
|
13130
|
-
function pickAvailableDeviceId(usedIds, {
|
|
13321
|
+
function pickAvailableDeviceId(usedIds, { preferPlayerRange = false, playerCeiling } = {}) {
|
|
13131
13322
|
var _a;
|
|
13132
13323
|
const used = new Set(usedIds);
|
|
13133
13324
|
const aboveThePlayers = playerCeiling === undefined
|
|
13134
13325
|
? descending(exports.REMOTEDB_MAX_DEVICE_ID, 1)
|
|
13135
13326
|
: ascending(playerCeiling + 1, exports.REMOTEDB_MAX_DEVICE_ID);
|
|
13136
|
-
const
|
|
13327
|
+
const outsidePlayerRange = ascending(exports.REMOTEDB_MAX_DEVICE_ID + 1, exports.MAX_DEVICE_ID);
|
|
13137
13328
|
// Numbers a player on this rig could be assigned. Last resort: better to
|
|
13138
13329
|
// contend for a slot than not to join at all, and the announcer moves us if
|
|
13139
13330
|
// the player that owns it shows up.
|
|
13140
13331
|
const amongThePlayers = playerCeiling === undefined
|
|
13141
13332
|
? []
|
|
13142
13333
|
: descending(Math.min(playerCeiling, exports.REMOTEDB_MAX_DEVICE_ID), 1);
|
|
13143
|
-
const search =
|
|
13144
|
-
? [...aboveThePlayers, ...
|
|
13145
|
-
: [...
|
|
13334
|
+
const search = preferPlayerRange
|
|
13335
|
+
? [...aboveThePlayers, ...outsidePlayerRange, ...amongThePlayers]
|
|
13336
|
+
: [...outsidePlayerRange, ...aboveThePlayers, ...amongThePlayers];
|
|
13146
13337
|
return (_a = search.find(id => !used.has(id))) !== null && _a !== void 0 ? _a : null;
|
|
13147
13338
|
}
|
|
13339
|
+
/**
|
|
13340
|
+
* Choose the device ID to carry inside remotedb messages when querying a CDJ.
|
|
13341
|
+
*
|
|
13342
|
+
* CDJs only answer remotedb queries whose in-protocol device-ID byte is in
|
|
13343
|
+
* 1-6, but that byte is independent of the ID we announce on the network — so
|
|
13344
|
+
* a virtual CDJ announced safely above the player range (say 7 behind a
|
|
13345
|
+
* DJM-V10) can still query metadata by carrying an in-range ID here. Verified
|
|
13346
|
+
* on a CDJ-3000 (2026-08-30): absent IDs and even the target's own ID are
|
|
13347
|
+
* answered; 7+ is silently dropped.
|
|
13348
|
+
*
|
|
13349
|
+
* Older players are documented (dysentery, nexus era) as stricter: the byte
|
|
13350
|
+
* had to be 1-4, belong to a player actually on the network, and not be the
|
|
13351
|
+
* player being queried. The preference order below satisfies those rules
|
|
13352
|
+
* whenever the rig makes it possible, then falls back through choices newer
|
|
13353
|
+
* players accept.
|
|
13354
|
+
*/
|
|
13355
|
+
function pickRemoteDbQueryId(targetId, devices) {
|
|
13356
|
+
const playerIds = new Set();
|
|
13357
|
+
for (const device of devices) {
|
|
13358
|
+
if (device.type === CDJ_DEVICE_TYPE && device.id <= exports.REMOTEDB_MAX_DEVICE_ID) {
|
|
13359
|
+
playerIds.add(device.id);
|
|
13360
|
+
}
|
|
13361
|
+
}
|
|
13362
|
+
// A live player that isn't the target, lowest first — 1-4 satisfies every
|
|
13363
|
+
// documented era of the restriction.
|
|
13364
|
+
const otherPlayers = [...playerIds].filter(id => id !== targetId).sort((a, b) => a - b);
|
|
13365
|
+
if (otherPlayers.length > 0) {
|
|
13366
|
+
return otherPlayers[0];
|
|
13367
|
+
}
|
|
13368
|
+
// No other player on the rig: take the lowest 1-6 ID no player holds.
|
|
13369
|
+
for (let id = 1; id <= exports.REMOTEDB_MAX_DEVICE_ID; id++) {
|
|
13370
|
+
if (id !== targetId && !playerIds.has(id)) {
|
|
13371
|
+
return id;
|
|
13372
|
+
}
|
|
13373
|
+
}
|
|
13374
|
+
// Six players and every one of them is the target? Impossible, but the
|
|
13375
|
+
// target's own ID is answered too, so it is a safe closing fallback.
|
|
13376
|
+
return targetId;
|
|
13377
|
+
}
|
|
13378
|
+
|
|
13379
|
+
|
|
13380
|
+
/***/ },
|
|
13381
|
+
|
|
13382
|
+
/***/ "./src/virtualcdj/heartbeat.ts"
|
|
13383
|
+
/*!*************************************!*\
|
|
13384
|
+
!*** ./src/virtualcdj/heartbeat.ts ***!
|
|
13385
|
+
\*************************************/
|
|
13386
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
13387
|
+
|
|
13388
|
+
"use strict";
|
|
13389
|
+
|
|
13390
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
13391
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
13392
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
13393
|
+
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");
|
|
13394
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
13395
|
+
};
|
|
13396
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
13397
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
13398
|
+
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");
|
|
13399
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
13400
|
+
};
|
|
13401
|
+
var _StagehandHeartbeat_instances, _StagehandHeartbeat_statusSocket, _StagehandHeartbeat_vcdj, _StagehandHeartbeat_deviceManager, _StagehandHeartbeat_logger, _StagehandHeartbeat_intervalHandle, _StagehandHeartbeat_sendHeartbeats;
|
|
13402
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
13403
|
+
exports.StagehandHeartbeat = exports.STAGEHAND_HEARTBEAT_INTERVAL = void 0;
|
|
13404
|
+
exports.makeStagehandMixerHeartbeat = makeStagehandMixerHeartbeat;
|
|
13405
|
+
exports.makeStagehandPlayerHeartbeat = makeStagehandPlayerHeartbeat;
|
|
13406
|
+
const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
|
|
13407
|
+
const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
|
|
13408
|
+
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
13409
|
+
/**
|
|
13410
|
+
* Cadence of the Stagehand unicast keep-alive, in milliseconds.
|
|
13411
|
+
*
|
|
13412
|
+
* The real iOS Stagehand app heartbeats every discovered player and the mixer
|
|
13413
|
+
* at ~4 Hz (250ms median inter-packet gap in captured traffic). This is the
|
|
13414
|
+
* stream that keeps AlphaTheta hardware unicasting live state (`0x39` mixer
|
|
13415
|
+
* fader/EQ, `0x58` VU on the mixer; `0x69` slim status, waveform families on
|
|
13416
|
+
* the CDJs) to our IP — subnet-broadcast presence alone (the `0x06` keep-alive)
|
|
13417
|
+
* is not enough to bootstrap it.
|
|
13418
|
+
*/
|
|
13419
|
+
exports.STAGEHAND_HEARTBEAT_INTERVAL = 250;
|
|
13420
|
+
/**
|
|
13421
|
+
* Builds a Stagehand name field for a unicast frame.
|
|
13422
|
+
*
|
|
13423
|
+
* Unlike the 20-byte {@link buildName} used by the broadcast announce/claim
|
|
13424
|
+
* frames, unicast frames carry a 19-byte name field (offsets 11..29) with the
|
|
13425
|
+
* device type / model bytes beginning at offset 30. The name is ASCII, NUL
|
|
13426
|
+
* padded.
|
|
13427
|
+
*/
|
|
13428
|
+
function buildUnicastName(name) {
|
|
13429
|
+
const field = new Uint8Array(19);
|
|
13430
|
+
field.set(Buffer.from(name, 'ascii').subarray(0, 19));
|
|
13431
|
+
return field;
|
|
13432
|
+
}
|
|
13433
|
+
/**
|
|
13434
|
+
* Build the Stagehand mixer keep-alive (`0x3a`, 40 bytes).
|
|
13435
|
+
*
|
|
13436
|
+
* Verified byte-for-byte against the real iOS Stagehand app unicasting to a
|
|
13437
|
+
* DJM-A9 at 4 Hz (Phase 4/5 captures §4.14.2). The 10-byte trailer
|
|
13438
|
+
* `21 02 00 fe 00 04 00 1c 00 00` is constant across every observed sample —
|
|
13439
|
+
* byte 33 (`0xfe`) is a VCDJ sentinel, not our runtime device number, so no
|
|
13440
|
+
* per-device bytes vary here.
|
|
13441
|
+
*/
|
|
13442
|
+
function makeStagehandMixerHeartbeat(vcdj) {
|
|
13443
|
+
const parts = [
|
|
13444
|
+
...constants_1.PROLINK_HEADER, // 0-9: magic
|
|
13445
|
+
0x3a, // 10: type 0x3a (mixer keep-alive)
|
|
13446
|
+
...buildUnicastName(vcdj.name), // 11-29: name (19 bytes)
|
|
13447
|
+
0x21,
|
|
13448
|
+
0x02,
|
|
13449
|
+
0x00,
|
|
13450
|
+
0xfe,
|
|
13451
|
+
0x00,
|
|
13452
|
+
0x04,
|
|
13453
|
+
0x00,
|
|
13454
|
+
0x1c,
|
|
13455
|
+
0x00,
|
|
13456
|
+
0x00, // 30-39: constant trailer
|
|
13457
|
+
];
|
|
13458
|
+
return Uint8Array.from(parts);
|
|
13459
|
+
}
|
|
13460
|
+
/**
|
|
13461
|
+
* Build the Stagehand player keep-alive (`0x68`, 36 bytes).
|
|
13462
|
+
*
|
|
13463
|
+
* Verified byte-for-byte against the real iOS Stagehand app unicasting to a
|
|
13464
|
+
* CDJ-3000 at ~2-4 Hz. Body (offsets 30-35) is `03 01 00 3a 00 00`: `0x03`
|
|
13465
|
+
* persona/channel, `0x01` device subkind, `0x00` flag, `0x3a` Stagehand
|
|
13466
|
+
* model-code stamp, two reserved bytes. Constant across every observed sample.
|
|
13467
|
+
*/
|
|
13468
|
+
function makeStagehandPlayerHeartbeat(vcdj) {
|
|
13469
|
+
const parts = [
|
|
13470
|
+
...constants_1.PROLINK_HEADER, // 0-9: magic
|
|
13471
|
+
0x68, // 10: type 0x68 (player keep-alive)
|
|
13472
|
+
...buildUnicastName(vcdj.name), // 11-29: name (19 bytes)
|
|
13473
|
+
0x03,
|
|
13474
|
+
0x01,
|
|
13475
|
+
0x00,
|
|
13476
|
+
0x3a,
|
|
13477
|
+
0x00,
|
|
13478
|
+
0x00, // 30-35: constant body
|
|
13479
|
+
];
|
|
13480
|
+
return Uint8Array.from(parts);
|
|
13481
|
+
}
|
|
13482
|
+
/**
|
|
13483
|
+
* Unicasts Stagehand keep-alive frames to every discovered player and mixer so
|
|
13484
|
+
* that AlphaTheta hardware begins (and keeps) pushing live state to our IP.
|
|
13485
|
+
*
|
|
13486
|
+
* The {@link StagehandAnnouncer} makes us *visible* on the network via subnet
|
|
13487
|
+
* broadcast; this heartbeat is what makes hardware actually *talk back*. It
|
|
13488
|
+
* targets the mixer with `0x3a` and each CDJ with `0x68`, mirroring the real
|
|
13489
|
+
* iOS Stagehand app. Frames are sent from the status socket (port 50002) to
|
|
13490
|
+
* each device's port 50002, which is where their unicast state replies land and
|
|
13491
|
+
* where {@link StatusEmitter} listens for them.
|
|
13492
|
+
*/
|
|
13493
|
+
class StagehandHeartbeat {
|
|
13494
|
+
constructor(vcdj, statusSocket, deviceManager, logger = logger_1.noopLogger) {
|
|
13495
|
+
_StagehandHeartbeat_instances.add(this);
|
|
13496
|
+
_StagehandHeartbeat_statusSocket.set(this, void 0);
|
|
13497
|
+
_StagehandHeartbeat_vcdj.set(this, void 0);
|
|
13498
|
+
_StagehandHeartbeat_deviceManager.set(this, void 0);
|
|
13499
|
+
_StagehandHeartbeat_logger.set(this, void 0);
|
|
13500
|
+
_StagehandHeartbeat_intervalHandle.set(this, void 0);
|
|
13501
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_vcdj, vcdj, "f");
|
|
13502
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_statusSocket, statusSocket, "f");
|
|
13503
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_deviceManager, deviceManager, "f");
|
|
13504
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_logger, logger, "f");
|
|
13505
|
+
}
|
|
13506
|
+
start() {
|
|
13507
|
+
if (__classPrivateFieldGet(this, _StagehandHeartbeat_intervalHandle, "f") !== undefined) {
|
|
13508
|
+
return;
|
|
13509
|
+
}
|
|
13510
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_logger, "f").info('Starting Stagehand heartbeat (unicast keep-alive)');
|
|
13511
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_instances, "m", _StagehandHeartbeat_sendHeartbeats).call(this);
|
|
13512
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_intervalHandle, setInterval(() => __classPrivateFieldGet(this, _StagehandHeartbeat_instances, "m", _StagehandHeartbeat_sendHeartbeats).call(this), exports.STAGEHAND_HEARTBEAT_INTERVAL), "f");
|
|
13513
|
+
}
|
|
13514
|
+
stop() {
|
|
13515
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_logger, "f").info('Stopping Stagehand heartbeat');
|
|
13516
|
+
if (__classPrivateFieldGet(this, _StagehandHeartbeat_intervalHandle, "f") !== undefined) {
|
|
13517
|
+
clearInterval(__classPrivateFieldGet(this, _StagehandHeartbeat_intervalHandle, "f"));
|
|
13518
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_intervalHandle, undefined, "f");
|
|
13519
|
+
}
|
|
13520
|
+
}
|
|
13521
|
+
}
|
|
13522
|
+
exports.StagehandHeartbeat = StagehandHeartbeat;
|
|
13523
|
+
_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() {
|
|
13524
|
+
for (const device of __classPrivateFieldGet(this, _StagehandHeartbeat_deviceManager, "f").devices.values()) {
|
|
13525
|
+
// Never heartbeat ourselves.
|
|
13526
|
+
if (device.id === __classPrivateFieldGet(this, _StagehandHeartbeat_vcdj, "f").id) {
|
|
13527
|
+
continue;
|
|
13528
|
+
}
|
|
13529
|
+
let packet = null;
|
|
13530
|
+
switch (device.type) {
|
|
13531
|
+
case types_1.DeviceType.Mixer:
|
|
13532
|
+
packet = makeStagehandMixerHeartbeat(__classPrivateFieldGet(this, _StagehandHeartbeat_vcdj, "f"));
|
|
13533
|
+
break;
|
|
13534
|
+
case types_1.DeviceType.CDJ:
|
|
13535
|
+
packet = makeStagehandPlayerHeartbeat(__classPrivateFieldGet(this, _StagehandHeartbeat_vcdj, "f"));
|
|
13536
|
+
break;
|
|
13537
|
+
default:
|
|
13538
|
+
// Rekordbox / other Stagehand peers are not heartbeat targets.
|
|
13539
|
+
continue;
|
|
13540
|
+
}
|
|
13541
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_statusSocket, "f").send(packet, constants_1.STATUS_PORT, device.ip.address, err => {
|
|
13542
|
+
if (err) {
|
|
13543
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_logger, "f").debug(`Stagehand heartbeat to ${device.name} (${device.ip.address}) failed: ${err.message}`);
|
|
13544
|
+
}
|
|
13545
|
+
});
|
|
13546
|
+
}
|
|
13547
|
+
};
|
|
13148
13548
|
|
|
13149
13549
|
|
|
13150
13550
|
/***/ },
|
|
@@ -13573,16 +13973,16 @@ _Announcer_announceSocket = new WeakMap(), _Announcer_deviceManager = new WeakMa
|
|
|
13573
13973
|
}, _Announcer_handleConflict = function _Announcer_handleConflict() {
|
|
13574
13974
|
// Stop announcing under the contested ID
|
|
13575
13975
|
__classPrivateFieldGet(this, _Announcer_instances, "m", _Announcer_clearTimer).call(this);
|
|
13576
|
-
// Stay in whichever range we were already in
|
|
13577
|
-
//
|
|
13578
|
-
//
|
|
13976
|
+
// Stay in whichever range we were already in, so a conflict on player 5
|
|
13977
|
+
// looks for another free player number first rather than jumping the
|
|
13978
|
+
// whole device out of the range the DJ configured it into.
|
|
13579
13979
|
//
|
|
13580
13980
|
// The mixer decides which of those numbers are actually safe: a player can
|
|
13581
13981
|
// only be assigned a number its mixer has a channel for, so anything above
|
|
13582
13982
|
// that is out of reach of the rig that just contested us.
|
|
13583
13983
|
const devices = [...__classPrivateFieldGet(this, _Announcer_deviceManager, "f").devices.values()];
|
|
13584
13984
|
const newId = (0, device_id_1.pickAvailableDeviceId)(devices.map(d => d.id), {
|
|
13585
|
-
|
|
13985
|
+
preferPlayerRange: __classPrivateFieldGet(this, _Announcer_vcdj, "f").id <= device_id_1.REMOTEDB_MAX_DEVICE_ID,
|
|
13586
13986
|
playerCeiling: (0, device_id_1.playerNumberCeiling)(devices),
|
|
13587
13987
|
});
|
|
13588
13988
|
if (newId === null) {
|
|
@@ -13685,6 +14085,7 @@ _Announcer_announceSocket = new WeakMap(), _Announcer_deviceManager = new WeakMa
|
|
|
13685
14085
|
__classPrivateFieldSet(this, _Announcer_intervalHandle, setInterval(sendKeepAlive, constants_1.ANNOUNCE_INTERVAL), "f");
|
|
13686
14086
|
};
|
|
13687
14087
|
__exportStar(__webpack_require__(/*! ./device-id */ "./src/virtualcdj/device-id.ts"), exports);
|
|
14088
|
+
__exportStar(__webpack_require__(/*! ./heartbeat */ "./src/virtualcdj/heartbeat.ts"), exports);
|
|
13688
14089
|
__exportStar(__webpack_require__(/*! ./stagehand */ "./src/virtualcdj/stagehand.ts"), exports);
|
|
13689
14090
|
|
|
13690
14091
|
|