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/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
|
/**
|
|
@@ -1377,25 +1379,24 @@ async function viaRemote(remote, opts) {
|
|
|
1377
1379
|
return track;
|
|
1378
1380
|
}
|
|
1379
1381
|
async function viaLocal(local, device, opts) {
|
|
1380
|
-
const { deviceId, trackSlot, trackId } = opts;
|
|
1382
|
+
const { deviceId, trackSlot, trackId, trackBPM } = opts;
|
|
1381
1383
|
if (trackSlot !== types_1.MediaSlot.USB && trackSlot !== types_1.MediaSlot.SD) {
|
|
1382
1384
|
throw new Error('Expected USB or SD slot for local database query');
|
|
1383
1385
|
}
|
|
1384
|
-
const
|
|
1385
|
-
if (adapter === null) {
|
|
1386
|
-
return { track: null, miss: 'no-database' };
|
|
1386
|
+
const lookup = await local.findTrack(deviceId, trackSlot, trackId, { trackBPM });
|
|
1387
|
+
if (lookup.adapter === null) {
|
|
1388
|
+
return { track: null, miss: 'no-database', switchedTo: null };
|
|
1387
1389
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
return { track: null, miss: 'track-absent' };
|
|
1390
|
+
if (lookup.track === null) {
|
|
1391
|
+
return { track: null, miss: 'track-absent', switchedTo: null };
|
|
1391
1392
|
}
|
|
1392
|
-
const anlz = await (0, rekordbox_1.loadAnlz)(
|
|
1393
|
+
const anlz = await (0, rekordbox_1.loadAnlz)(lookup.track, 'DAT', (0, utils_1.anlzLoader)({ device, slot: trackSlot }));
|
|
1393
1394
|
const track = {
|
|
1394
|
-
...
|
|
1395
|
+
...lookup.track,
|
|
1395
1396
|
beatGrid: anlz.beatGrid,
|
|
1396
1397
|
waveformHd: null,
|
|
1397
1398
|
};
|
|
1398
|
-
return { track, miss: null };
|
|
1399
|
+
return { track, miss: null, switchedTo: lookup.switchedTo };
|
|
1399
1400
|
}
|
|
1400
1401
|
|
|
1401
1402
|
|
|
@@ -1647,7 +1648,6 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
1647
1648
|
};
|
|
1648
1649
|
var _Database_instances, _Database_deviceManager, _Database_localDatabase, _Database_remoteDatabase, _Database_logger, _Database_getTrackLookupStrategy, _Database_getMediaLookupStrategy, _Database_metadataViaRemoteFallback;
|
|
1649
1650
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1650
|
-
const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
|
|
1651
1651
|
const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
|
|
1652
1652
|
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
1653
1653
|
const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
|
|
@@ -1720,6 +1720,7 @@ class Database {
|
|
|
1720
1720
|
* Retrieve metadata for a track on a specific device slot.
|
|
1721
1721
|
*/
|
|
1722
1722
|
async getMetadata(opts) {
|
|
1723
|
+
var _a;
|
|
1723
1724
|
const { deviceId, trackType, trackSlot, span } = opts;
|
|
1724
1725
|
const tx = span
|
|
1725
1726
|
? span.startChild({ op: 'dbGetMetadata' })
|
|
@@ -1727,7 +1728,7 @@ class Database {
|
|
|
1727
1728
|
tx.setTag('deviceId', deviceId.toString());
|
|
1728
1729
|
tx.setTag('trackType', (0, utils_1.getTrackTypeName)(trackType));
|
|
1729
1730
|
tx.setTag('trackSlot', (0, utils_1.getSlotName)(trackSlot));
|
|
1730
|
-
const callOpts = { ...opts, span: tx };
|
|
1731
|
+
const callOpts = { ...opts, trackBPM: (_a = opts.trackBPM) !== null && _a !== void 0 ? _a : null, span: tx };
|
|
1731
1732
|
const device = await __classPrivateFieldGet(this, _Database_deviceManager, "f").getDeviceEnsured(deviceId);
|
|
1732
1733
|
if (device === null) {
|
|
1733
1734
|
return null;
|
|
@@ -1740,6 +1741,12 @@ class Database {
|
|
|
1740
1741
|
if (strategy === LookupStrategy.Local) {
|
|
1741
1742
|
const local = await GetMetadata.viaLocal(__classPrivateFieldGet(this, _Database_localDatabase, "f"), device, callOpts);
|
|
1742
1743
|
track = local.track;
|
|
1744
|
+
if (local.switchedTo !== null) {
|
|
1745
|
+
__classPrivateFieldGet(this, _Database_logger, "f").info(`Device ${deviceId} ${(0, utils_1.getSlotName)(trackSlot)} is now read from the ` +
|
|
1746
|
+
`${local.switchedTo === 'pdb' ? 'legacy PDB' : 'OneLibrary'} database: ` +
|
|
1747
|
+
`it is the one that holds track ${opts.trackId} as the player reports it ` +
|
|
1748
|
+
`(NP3-399)`);
|
|
1749
|
+
}
|
|
1743
1750
|
// A local miss used to end the lookup, so the DJ's track silently never
|
|
1744
1751
|
// reached the overlay for the rest of the set (NP3-361). Say what was
|
|
1745
1752
|
// missed, then give the CDJ's own database a chance to answer.
|
|
@@ -1947,20 +1954,14 @@ _Database_deviceManager = new WeakMap(), _Database_localDatabase = new WeakMap()
|
|
|
1947
1954
|
/**
|
|
1948
1955
|
* Second chance for a track the local database could not answer for.
|
|
1949
1956
|
*
|
|
1950
|
-
*
|
|
1951
|
-
* ID in the 1-6 range,
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
1954
|
-
*
|
|
1957
|
+
* Available regardless of the virtual CDJ's announced ID: CDJs restrict
|
|
1958
|
+
* remote database queries to a device-ID byte in the 1-6 range, but that
|
|
1959
|
+
* byte lives inside the remotedb messages and RemoteDatabase picks an
|
|
1960
|
+
* in-range one per connection — the announced ID never constrains this
|
|
1961
|
+
* lookup. A failure here is logged rather than thrown, so a field report
|
|
1962
|
+
* says what actually went wrong.
|
|
1955
1963
|
*/
|
|
1956
1964
|
async function _Database_metadataViaRemoteFallback(opts) {
|
|
1957
|
-
const hostId = __classPrivateFieldGet(this, _Database_remoteDatabase, "f").hostDevice.id;
|
|
1958
|
-
if (hostId < constants_1.MIN_CDJ_DEVICE_ID || hostId > constants_1.MAX_CDJ_DEVICE_ID) {
|
|
1959
|
-
__classPrivateFieldGet(this, _Database_logger, "f").warn(`No remote fallback for track ${opts.trackId}: this player is device ` +
|
|
1960
|
-
`${hostId}, and CDJs only answer remote database queries from ` +
|
|
1961
|
-
`devices ${constants_1.MIN_CDJ_DEVICE_ID}-${constants_1.MAX_CDJ_DEVICE_ID}`);
|
|
1962
|
-
return null;
|
|
1963
|
-
}
|
|
1964
1965
|
try {
|
|
1965
1966
|
const track = await GetMetadata.viaRemote(__classPrivateFieldGet(this, _Database_remoteDatabase, "f"), opts);
|
|
1966
1967
|
if (track === null) {
|
|
@@ -2255,7 +2256,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
2255
2256
|
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");
|
|
2256
2257
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
2257
2258
|
};
|
|
2258
|
-
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;
|
|
2259
|
+
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;
|
|
2259
2260
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2260
2261
|
const async_mutex_1 = __webpack_require__(/*! async-mutex */ "async-mutex");
|
|
2261
2262
|
const onelibrary_connect_1 = __webpack_require__(/*! onelibrary-connect */ "onelibrary-connect");
|
|
@@ -2271,6 +2272,18 @@ const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
|
|
|
2271
2272
|
const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
|
|
2272
2273
|
const orm_1 = __webpack_require__(/*! ./orm */ "./src/localdb/orm.ts");
|
|
2273
2274
|
const rekordbox_1 = __webpack_require__(/*! ./rekordbox */ "./src/localdb/rekordbox.ts");
|
|
2275
|
+
/**
|
|
2276
|
+
* A row is only trusted when the player's reported BPM agrees with it. Either
|
|
2277
|
+
* side missing (unanalysed track, or a player that reports no BPM) is
|
|
2278
|
+
* inconclusive and counts as agreement.
|
|
2279
|
+
*/
|
|
2280
|
+
const trackAgreesWithPlayer = (track, hint) => {
|
|
2281
|
+
const reported = hint.trackBPM;
|
|
2282
|
+
if (reported === null || reported === undefined || !track.tempo) {
|
|
2283
|
+
return true;
|
|
2284
|
+
}
|
|
2285
|
+
return Math.abs(track.tempo - reported) < 0.05;
|
|
2286
|
+
};
|
|
2274
2287
|
/**
|
|
2275
2288
|
* Compute the identifier for media device in a CDJ. This is used to determine
|
|
2276
2289
|
* if we have already hydrated the device or not into our local database.
|
|
@@ -2297,6 +2310,7 @@ const getMediaId = (info) => {
|
|
|
2297
2310
|
*/
|
|
2298
2311
|
class LocalDatabase {
|
|
2299
2312
|
constructor(hostDevice, deviceManager, statusEmitter, preference = 'auto') {
|
|
2313
|
+
_LocalDatabase_instances.add(this);
|
|
2300
2314
|
_LocalDatabase_hostDevice.set(this, void 0);
|
|
2301
2315
|
_LocalDatabase_deviceManager.set(this, void 0);
|
|
2302
2316
|
_LocalDatabase_statusEmitter.set(this, void 0);
|
|
@@ -2328,19 +2342,25 @@ class LocalDatabase {
|
|
|
2328
2342
|
_LocalDatabase_handleDeviceRemoved.set(this, (device) => {
|
|
2329
2343
|
const db = __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").find(db => db.media.deviceId === device.id);
|
|
2330
2344
|
if (db) {
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
try {
|
|
2335
|
-
fs.unlinkSync(db.tempFile);
|
|
2336
|
-
}
|
|
2337
|
-
catch {
|
|
2338
|
-
// Ignore cleanup errors
|
|
2339
|
-
}
|
|
2345
|
+
__classPrivateFieldGet(this, _LocalDatabase_closeLoaded, "f").call(this, db);
|
|
2346
|
+
if (db.alternate) {
|
|
2347
|
+
__classPrivateFieldGet(this, _LocalDatabase_closeLoaded, "f").call(this, db.alternate);
|
|
2340
2348
|
}
|
|
2341
2349
|
}
|
|
2342
2350
|
__classPrivateFieldSet(this, _LocalDatabase_dbs, __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").filter(db => db.media.deviceId !== device.id), "f");
|
|
2343
2351
|
});
|
|
2352
|
+
_LocalDatabase_closeLoaded.set(this, (loaded) => {
|
|
2353
|
+
loaded.adapter.close();
|
|
2354
|
+
// Clean up temp file if it exists (OneLibrary databases)
|
|
2355
|
+
if (loaded.tempFile) {
|
|
2356
|
+
try {
|
|
2357
|
+
fs.unlinkSync(loaded.tempFile);
|
|
2358
|
+
}
|
|
2359
|
+
catch {
|
|
2360
|
+
// Ignore cleanup errors
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
});
|
|
2344
2364
|
/**
|
|
2345
2365
|
* Helper to fetch a file from device, trying both dotted and non-dotted paths
|
|
2346
2366
|
*/
|
|
@@ -2447,11 +2467,54 @@ class LocalDatabase {
|
|
|
2447
2467
|
}
|
|
2448
2468
|
}
|
|
2449
2469
|
__classPrivateFieldGet(this, _LocalDatabase_emitter, "f").emit('hydrationDone', { device, slot });
|
|
2450
|
-
const db = {
|
|
2470
|
+
const db = {
|
|
2471
|
+
adapter,
|
|
2472
|
+
media,
|
|
2473
|
+
device,
|
|
2474
|
+
slot,
|
|
2475
|
+
id: getMediaId(media),
|
|
2476
|
+
tempFile,
|
|
2477
|
+
alternateLock: new async_mutex_1.Mutex(),
|
|
2478
|
+
};
|
|
2451
2479
|
__classPrivateFieldGet(this, _LocalDatabase_dbs, "f").push(db);
|
|
2452
2480
|
tx.finish();
|
|
2453
2481
|
return db;
|
|
2454
2482
|
});
|
|
2483
|
+
/**
|
|
2484
|
+
* Loads the database format the slot is *not* currently served from, so a
|
|
2485
|
+
* lookup can be checked against it. Only meaningful under the 'auto'
|
|
2486
|
+
* preference: a forced format has no alternate. The result (or its absence)
|
|
2487
|
+
* is remembered so the media is never downloaded twice.
|
|
2488
|
+
*/
|
|
2489
|
+
_LocalDatabase_loadAlternate.set(this, (db) => db.alternateLock.runExclusive(async () => {
|
|
2490
|
+
if (db.alternate !== undefined) {
|
|
2491
|
+
return db.alternate;
|
|
2492
|
+
}
|
|
2493
|
+
if (__classPrivateFieldGet(this, _LocalDatabase_preference, "f") !== 'auto') {
|
|
2494
|
+
db.alternate = null;
|
|
2495
|
+
return null;
|
|
2496
|
+
}
|
|
2497
|
+
const tx = Telemetry.startTransaction({ name: 'hydrateAlternateDatabase' });
|
|
2498
|
+
tx.setTag('slot', (0, utils_1.getSlotName)(db.media.slot));
|
|
2499
|
+
tx.setTag('activeDbType', db.adapter.type);
|
|
2500
|
+
try {
|
|
2501
|
+
if (db.adapter.type === 'oneLibrary') {
|
|
2502
|
+
const adapter = await __classPrivateFieldGet(this, _LocalDatabase_loadPdbDatabase, "f").call(this, db.device, db.slot, tx);
|
|
2503
|
+
db.alternate = { adapter };
|
|
2504
|
+
tx.setTag('dbType', 'pdb');
|
|
2505
|
+
}
|
|
2506
|
+
else {
|
|
2507
|
+
db.alternate = await __classPrivateFieldGet(this, _LocalDatabase_tryLoadOneLibrary, "f").call(this, db.device, db.slot, tx);
|
|
2508
|
+
tx.setTag('dbType', db.alternate ? 'oneLibrary' : 'none');
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
catch {
|
|
2512
|
+
// The media simply has no such file (or it could not be read).
|
|
2513
|
+
db.alternate = null;
|
|
2514
|
+
}
|
|
2515
|
+
tx.finish();
|
|
2516
|
+
return db.alternate;
|
|
2517
|
+
}));
|
|
2455
2518
|
__classPrivateFieldSet(this, _LocalDatabase_hostDevice, hostDevice, "f");
|
|
2456
2519
|
__classPrivateFieldSet(this, _LocalDatabase_deviceManager, deviceManager, "f");
|
|
2457
2520
|
__classPrivateFieldSet(this, _LocalDatabase_statusEmitter, statusEmitter, "f");
|
|
@@ -2476,6 +2539,45 @@ class LocalDatabase {
|
|
|
2476
2539
|
disconnectForDevice(device) {
|
|
2477
2540
|
__classPrivateFieldGet(this, _LocalDatabase_handleDeviceRemoved, "f").call(this, device);
|
|
2478
2541
|
}
|
|
2542
|
+
/**
|
|
2543
|
+
* Looks a track up in the databases of a device slot, checking the answer
|
|
2544
|
+
* against what the player reports.
|
|
2545
|
+
*
|
|
2546
|
+
* A Device Library Plus export carries both `exportLibrary.db` and the
|
|
2547
|
+
* legacy `export.pdb`, and their track IDs are different number spaces. A
|
|
2548
|
+
* player that reads one while we loaded the other resolves most IDs to a
|
|
2549
|
+
* different track and some to nothing at all (NP3-399). So when the active
|
|
2550
|
+
* database has no such row, or its row's BPM is not the BPM the player is
|
|
2551
|
+
* showing, the other format is loaded and asked. If it agrees with the
|
|
2552
|
+
* player, the slot switches to it for every later lookup — artwork,
|
|
2553
|
+
* analysis and the next track all follow the database the player is using.
|
|
2554
|
+
*
|
|
2555
|
+
* When neither database can be confirmed, the active database's row (if any)
|
|
2556
|
+
* is returned as before.
|
|
2557
|
+
*/
|
|
2558
|
+
async findTrack(deviceId, slot, trackId, hint = {}) {
|
|
2559
|
+
const db = await __classPrivateFieldGet(this, _LocalDatabase_instances, "m", _LocalDatabase_getItem).call(this, deviceId, slot);
|
|
2560
|
+
if (db === null) {
|
|
2561
|
+
return { adapter: null, track: null, switchedTo: null };
|
|
2562
|
+
}
|
|
2563
|
+
const active = db.adapter.findTrack(trackId);
|
|
2564
|
+
if (active !== null && trackAgreesWithPlayer(active, hint)) {
|
|
2565
|
+
return { adapter: db.adapter, track: active, switchedTo: null };
|
|
2566
|
+
}
|
|
2567
|
+
const alternate = await __classPrivateFieldGet(this, _LocalDatabase_loadAlternate, "f").call(this, db);
|
|
2568
|
+
if (alternate !== null) {
|
|
2569
|
+
const other = alternate.adapter.findTrack(trackId);
|
|
2570
|
+
if (other !== null && trackAgreesWithPlayer(other, hint)) {
|
|
2571
|
+
db.alternate = { adapter: db.adapter, tempFile: db.tempFile };
|
|
2572
|
+
db.adapter = alternate.adapter;
|
|
2573
|
+
db.tempFile = alternate.tempFile;
|
|
2574
|
+
return { adapter: db.adapter, track: other, switchedTo: db.adapter.type };
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
return active === null
|
|
2578
|
+
? { adapter: db.adapter, track: null, switchedTo: null }
|
|
2579
|
+
: { adapter: db.adapter, track: active, switchedTo: null };
|
|
2580
|
+
}
|
|
2479
2581
|
/**
|
|
2480
2582
|
* Gets the database adapter for the media metadata in the provided device slot.
|
|
2481
2583
|
*
|
|
@@ -2486,43 +2588,8 @@ class LocalDatabase {
|
|
|
2486
2588
|
*/
|
|
2487
2589
|
async get(deviceId, slot) {
|
|
2488
2590
|
var _a;
|
|
2489
|
-
const
|
|
2490
|
-
|
|
2491
|
-
const device = __classPrivateFieldGet(this, _LocalDatabase_deviceManager, "f").devices.get(deviceId);
|
|
2492
|
-
if (device === undefined) {
|
|
2493
|
-
return null;
|
|
2494
|
-
}
|
|
2495
|
-
if (device.type !== types_1.DeviceType.CDJ ||
|
|
2496
|
-
device.id < constants_1.MIN_CDJ_DEVICE_ID ||
|
|
2497
|
-
device.id > constants_1.MAX_CDJ_DEVICE_ID) {
|
|
2498
|
-
return null;
|
|
2499
|
-
}
|
|
2500
|
-
let media;
|
|
2501
|
-
try {
|
|
2502
|
-
media = await __classPrivateFieldGet(this, _LocalDatabase_statusEmitter, "f").queryMediaSlot({
|
|
2503
|
-
hostDevice: __classPrivateFieldGet(this, _LocalDatabase_hostDevice, "f"),
|
|
2504
|
-
device,
|
|
2505
|
-
slot,
|
|
2506
|
-
});
|
|
2507
|
-
}
|
|
2508
|
-
catch {
|
|
2509
|
-
// Timeout or other error - treat as no media
|
|
2510
|
-
return null;
|
|
2511
|
-
}
|
|
2512
|
-
if (media.tracksType !== types_1.TrackType.RB) {
|
|
2513
|
-
return null;
|
|
2514
|
-
}
|
|
2515
|
-
const id = getMediaId(media);
|
|
2516
|
-
// Acquire a lock for this device slot that will not release until we've
|
|
2517
|
-
// guaranteed the existence of the database.
|
|
2518
|
-
const db = await lock.runExclusive(() => {
|
|
2519
|
-
const cached = __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").find(db => db.id === id);
|
|
2520
|
-
if (cached) {
|
|
2521
|
-
return cached;
|
|
2522
|
-
}
|
|
2523
|
-
return __classPrivateFieldGet(this, _LocalDatabase_hydrateDatabase, "f").call(this, device, slot, media);
|
|
2524
|
-
});
|
|
2525
|
-
return db.adapter;
|
|
2591
|
+
const db = await __classPrivateFieldGet(this, _LocalDatabase_instances, "m", _LocalDatabase_getItem).call(this, deviceId, slot);
|
|
2592
|
+
return (_a = db === null || db === void 0 ? void 0 : db.adapter) !== null && _a !== void 0 ? _a : null;
|
|
2526
2593
|
}
|
|
2527
2594
|
/**
|
|
2528
2595
|
* Get the database type for an already-loaded device slot.
|
|
@@ -2548,7 +2615,50 @@ class LocalDatabase {
|
|
|
2548
2615
|
await Promise.all(loaders);
|
|
2549
2616
|
}
|
|
2550
2617
|
}
|
|
2551
|
-
_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()
|
|
2618
|
+
_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 =
|
|
2619
|
+
/**
|
|
2620
|
+
* The loaded database entry for a device slot, hydrating it first if needed.
|
|
2621
|
+
*/
|
|
2622
|
+
async function _LocalDatabase_getItem(deviceId, slot) {
|
|
2623
|
+
var _a;
|
|
2624
|
+
const lockKey = `${deviceId}-${slot}`;
|
|
2625
|
+
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);
|
|
2626
|
+
const device = __classPrivateFieldGet(this, _LocalDatabase_deviceManager, "f").devices.get(deviceId);
|
|
2627
|
+
if (device === undefined) {
|
|
2628
|
+
return null;
|
|
2629
|
+
}
|
|
2630
|
+
if (device.type !== types_1.DeviceType.CDJ ||
|
|
2631
|
+
device.id < constants_1.MIN_CDJ_DEVICE_ID ||
|
|
2632
|
+
device.id > constants_1.MAX_CDJ_DEVICE_ID) {
|
|
2633
|
+
return null;
|
|
2634
|
+
}
|
|
2635
|
+
let media;
|
|
2636
|
+
try {
|
|
2637
|
+
media = await __classPrivateFieldGet(this, _LocalDatabase_statusEmitter, "f").queryMediaSlot({
|
|
2638
|
+
hostDevice: __classPrivateFieldGet(this, _LocalDatabase_hostDevice, "f"),
|
|
2639
|
+
device,
|
|
2640
|
+
slot,
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
catch {
|
|
2644
|
+
// Timeout or other error - treat as no media
|
|
2645
|
+
return null;
|
|
2646
|
+
}
|
|
2647
|
+
if (media.tracksType !== types_1.TrackType.RB) {
|
|
2648
|
+
return null;
|
|
2649
|
+
}
|
|
2650
|
+
const id = getMediaId(media);
|
|
2651
|
+
// Acquire a lock for this device slot that will not release until we've
|
|
2652
|
+
// guaranteed the existence of the database.
|
|
2653
|
+
const db = await lock.runExclusive(() => {
|
|
2654
|
+
const cached = __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").find(db => db.id === id);
|
|
2655
|
+
if (cached) {
|
|
2656
|
+
return cached;
|
|
2657
|
+
}
|
|
2658
|
+
return __classPrivateFieldGet(this, _LocalDatabase_hydrateDatabase, "f").call(this, device, slot, media);
|
|
2659
|
+
});
|
|
2660
|
+
return db;
|
|
2661
|
+
};
|
|
2552
2662
|
exports["default"] = LocalDatabase;
|
|
2553
2663
|
|
|
2554
2664
|
|
|
@@ -6718,7 +6828,7 @@ class MixstatusProcessor {
|
|
|
6718
6828
|
// able to report it as live
|
|
6719
6829
|
const startedAt = __classPrivateFieldGet(this, _MixstatusProcessor_lastStartTime, "f").get(deviceId);
|
|
6720
6830
|
const requiredPlayTime = __classPrivateFieldGet(this, _MixstatusProcessor_config, "f").beatsUntilReported *
|
|
6721
|
-
(0, utils_1.bpmToSeconds)(state.trackBPM, state.
|
|
6831
|
+
(0, utils_1.bpmToSeconds)(state.trackBPM, state.effectivePitch) *
|
|
6722
6832
|
1000;
|
|
6723
6833
|
if (__classPrivateFieldGet(this, _MixstatusProcessor_config, "f").mode === types_1.MixstatusMode.SmartTiming &&
|
|
6724
6834
|
startedAt !== undefined &&
|
|
@@ -6729,7 +6839,7 @@ class MixstatusProcessor {
|
|
|
6729
6839
|
// we can mark the track as truly stopped.
|
|
6730
6840
|
const stoppedAt = __classPrivateFieldGet(this, _MixstatusProcessor_lastStoppedTimes, "f").get(deviceId);
|
|
6731
6841
|
const requiredStopTime = __classPrivateFieldGet(this, _MixstatusProcessor_config, "f").allowedInterruptBeats *
|
|
6732
|
-
(0, utils_1.bpmToSeconds)(state.trackBPM, state.
|
|
6842
|
+
(0, utils_1.bpmToSeconds)(state.trackBPM, state.effectivePitch) *
|
|
6733
6843
|
1000;
|
|
6734
6844
|
if (stoppedAt !== undefined && requiredStopTime <= Date.now() - stoppedAt) {
|
|
6735
6845
|
__classPrivateFieldGet(this, _MixstatusProcessor_markPlayerStopped, "f").call(this, state);
|
|
@@ -7020,6 +7130,14 @@ class ProlinkNetwork {
|
|
|
7020
7130
|
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"));
|
|
7021
7131
|
}
|
|
7022
7132
|
announcer.start();
|
|
7133
|
+
// In Stagehand mode, unicast keep-alives to discovered players and the
|
|
7134
|
+
// mixer so they push live state to us (mixer 0x39/0x58, CDJ 0x69/waveforms).
|
|
7135
|
+
// Broadcast presence alone does not bootstrap that stream.
|
|
7136
|
+
let heartbeat = null;
|
|
7137
|
+
if (connectMethod === 'stagehand') {
|
|
7138
|
+
heartbeat = new virtualcdj_1.StagehandHeartbeat(vcdj, __classPrivateFieldGet(this, _ProlinkNetwork_statusSocket, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_logger, "f"));
|
|
7139
|
+
heartbeat.start();
|
|
7140
|
+
}
|
|
7023
7141
|
// Create remote and local databases
|
|
7024
7142
|
const remotedb = new remotedb_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), vcdj);
|
|
7025
7143
|
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');
|
|
@@ -7028,23 +7146,24 @@ class ProlinkNetwork {
|
|
|
7028
7146
|
// Create controller service
|
|
7029
7147
|
const control = new control_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_beatSocket, "f"), vcdj);
|
|
7030
7148
|
__classPrivateFieldSet(this, _ProlinkNetwork_state, types_1.NetworkState.Connected, "f");
|
|
7031
|
-
__classPrivateFieldSet(this, _ProlinkNetwork_connection, { announcer, control, remotedb, localdb, database }, "f");
|
|
7149
|
+
__classPrivateFieldSet(this, _ProlinkNetwork_connection, { announcer, heartbeat, control, remotedb, localdb, database }, "f");
|
|
7032
7150
|
tx.finish();
|
|
7033
7151
|
}
|
|
7034
7152
|
/**
|
|
7035
7153
|
* Disconnect from the network
|
|
7036
7154
|
*/
|
|
7037
7155
|
disconnect() {
|
|
7038
|
-
var _a, _b, _c;
|
|
7156
|
+
var _a, _b, _c, _d, _e;
|
|
7039
7157
|
if (__classPrivateFieldGet(this, _ProlinkNetwork_config, "f") === null) {
|
|
7040
7158
|
throw new Error(connectErrorHelp);
|
|
7041
7159
|
}
|
|
7042
7160
|
// Stop announcing ourself
|
|
7043
7161
|
(_a = __classPrivateFieldGet(this, _ProlinkNetwork_connection, "f")) === null || _a === void 0 ? void 0 : _a.announcer.stop();
|
|
7162
|
+
(_c = (_b = __classPrivateFieldGet(this, _ProlinkNetwork_connection, "f")) === null || _b === void 0 ? void 0 : _b.heartbeat) === null || _c === void 0 ? void 0 : _c.stop();
|
|
7044
7163
|
// Disconnect devices from the remote and local databases
|
|
7045
7164
|
for (const device of this.deviceManager.devices.values()) {
|
|
7046
|
-
(
|
|
7047
|
-
(
|
|
7165
|
+
(_d = this.remotedb) === null || _d === void 0 ? void 0 : _d.disconnectFromDevice(device);
|
|
7166
|
+
(_e = this.localdb) === null || _e === void 0 ? void 0 : _e.disconnectForDevice(device);
|
|
7048
7167
|
}
|
|
7049
7168
|
return this.close;
|
|
7050
7169
|
}
|
|
@@ -8504,15 +8623,17 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
8504
8623
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8505
8624
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8506
8625
|
};
|
|
8507
|
-
var _Connection_socket, _Connection_txId, _Connection_lock, _QueryInterface_conn, _QueryInterface_hostDevice, _QueryInterface_lock, _RemoteDatabase_hostDevice, _RemoteDatabase_deviceManager, _RemoteDatabase_connections, _RemoteDatabase_deviceLocks;
|
|
8626
|
+
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;
|
|
8508
8627
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
8509
8628
|
exports.QueryInterface = exports.Connection = exports.Query = exports.MenuTarget = void 0;
|
|
8510
8629
|
exports.getQueryName = getQueryName;
|
|
8511
8630
|
const async_mutex_1 = __webpack_require__(/*! async-mutex */ "async-mutex");
|
|
8512
8631
|
const promise_socket_1 = __importDefault(__webpack_require__(/*! promise-socket */ "promise-socket"));
|
|
8513
8632
|
const net_1 = __webpack_require__(/*! net */ "net");
|
|
8633
|
+
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
8514
8634
|
const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
|
|
8515
|
-
const
|
|
8635
|
+
const device_id_1 = __webpack_require__(/*! src/virtualcdj/device-id */ "./src/virtualcdj/device-id.ts");
|
|
8636
|
+
const types_2 = __webpack_require__(/*! ./message/types */ "./src/remotedb/message/types.ts");
|
|
8516
8637
|
const constants_1 = __webpack_require__(/*! ./constants */ "./src/remotedb/constants.ts");
|
|
8517
8638
|
const fields_1 = __webpack_require__(/*! ./fields */ "./src/remotedb/fields.ts");
|
|
8518
8639
|
const message_1 = __webpack_require__(/*! ./message */ "./src/remotedb/message/index.ts");
|
|
@@ -8525,7 +8646,7 @@ var MenuTarget;
|
|
|
8525
8646
|
(function (MenuTarget) {
|
|
8526
8647
|
MenuTarget[MenuTarget["Main"] = 1] = "Main";
|
|
8527
8648
|
})(MenuTarget || (exports.MenuTarget = MenuTarget = {}));
|
|
8528
|
-
exports.Query =
|
|
8649
|
+
exports.Query = types_2.Request;
|
|
8529
8650
|
const QueryInverse = Object.fromEntries(Object.entries(exports.Query).map(e => [e[1], e[0]]));
|
|
8530
8651
|
/**
|
|
8531
8652
|
* Returns a string representation of a remote query
|
|
@@ -8571,7 +8692,7 @@ class Connection {
|
|
|
8571
8692
|
var _a;
|
|
8572
8693
|
const tx = span.startChild({
|
|
8573
8694
|
op: 'writeMessage',
|
|
8574
|
-
description: (0,
|
|
8695
|
+
description: (0, types_2.getMessageName)(message.type),
|
|
8575
8696
|
});
|
|
8576
8697
|
message.transactionId = __classPrivateFieldSet(this, _Connection_txId, (_a = __classPrivateFieldGet(this, _Connection_txId, "f"), ++_a), "f");
|
|
8577
8698
|
await __classPrivateFieldGet(this, _Connection_socket, "f").write(message.buffer);
|
|
@@ -8632,7 +8753,8 @@ _QueryInterface_conn = new WeakMap(), _QueryInterface_hostDevice = new WeakMap()
|
|
|
8632
8753
|
* Service that maintains remote database connections with devices on the network.
|
|
8633
8754
|
*/
|
|
8634
8755
|
class RemoteDatabase {
|
|
8635
|
-
constructor(deviceManager, hostDevice) {
|
|
8756
|
+
constructor(deviceManager, hostDevice, { pickQueryId = true } = {}) {
|
|
8757
|
+
_RemoteDatabase_instances.add(this);
|
|
8636
8758
|
_RemoteDatabase_hostDevice.set(this, void 0);
|
|
8637
8759
|
_RemoteDatabase_deviceManager.set(this, void 0);
|
|
8638
8760
|
/**
|
|
@@ -8643,6 +8765,17 @@ class RemoteDatabase {
|
|
|
8643
8765
|
* Locks for each device when locating the connection
|
|
8644
8766
|
*/
|
|
8645
8767
|
_RemoteDatabase_deviceLocks.set(this, new Map());
|
|
8768
|
+
/**
|
|
8769
|
+
* The device ID each connection introduced itself with. Queries on that
|
|
8770
|
+
* connection must carry the same ID.
|
|
8771
|
+
*/
|
|
8772
|
+
_RemoteDatabase_queryIds.set(this, new Map());
|
|
8773
|
+
/**
|
|
8774
|
+
* Whether to substitute an in-range query ID when the host device sits
|
|
8775
|
+
* outside 1-6. Disabled only by protocol probes that need to observe how a
|
|
8776
|
+
* device treats the raw announced ID (see examples/remotedb-id-probe.ts).
|
|
8777
|
+
*/
|
|
8778
|
+
_RemoteDatabase_pickQueryId.set(this, void 0);
|
|
8646
8779
|
/**
|
|
8647
8780
|
* Open a connection to the specified device for querying
|
|
8648
8781
|
*/
|
|
@@ -8667,20 +8800,22 @@ class RemoteDatabase {
|
|
|
8667
8800
|
throw new Error(`Expected 0x01 during preamble handshake. Got ${data.value}`);
|
|
8668
8801
|
}
|
|
8669
8802
|
// Send introduction message to set context for querying
|
|
8803
|
+
const queryId = __classPrivateFieldGet(this, _RemoteDatabase_instances, "m", _RemoteDatabase_queryIdFor).call(this, device);
|
|
8670
8804
|
const intro = new message_1.Message({
|
|
8671
8805
|
transactionId: 0xfffffffe,
|
|
8672
|
-
type:
|
|
8673
|
-
args: [new fields_1.UInt32(
|
|
8806
|
+
type: types_2.MessageType.Introduce,
|
|
8807
|
+
args: [new fields_1.UInt32(queryId)],
|
|
8674
8808
|
});
|
|
8675
8809
|
await socket.write(intro.buffer);
|
|
8676
|
-
const resp = await message_1.Message.fromStream(socket,
|
|
8677
|
-
if (resp.type !==
|
|
8810
|
+
const resp = await message_1.Message.fromStream(socket, types_2.MessageType.Success, tx);
|
|
8811
|
+
if (resp.type !== types_2.MessageType.Success) {
|
|
8678
8812
|
throw new Error(`Failed to introduce self to device ID: ${device.id}`);
|
|
8679
8813
|
}
|
|
8680
8814
|
// Clear socket timeout after successful connection — the per-device mutex
|
|
8681
8815
|
// and application-level Promise.race timeouts handle query timeouts
|
|
8682
8816
|
socket.stream.setTimeout(0);
|
|
8683
8817
|
__classPrivateFieldGet(this, _RemoteDatabase_connections, "f").set(device.id, new Connection(device, socket));
|
|
8818
|
+
__classPrivateFieldGet(this, _RemoteDatabase_queryIds, "f").set(device.id, queryId);
|
|
8684
8819
|
tx.finish();
|
|
8685
8820
|
};
|
|
8686
8821
|
/**
|
|
@@ -8694,22 +8829,26 @@ class RemoteDatabase {
|
|
|
8694
8829
|
}
|
|
8695
8830
|
const goodbye = new message_1.Message({
|
|
8696
8831
|
transactionId: 0xfffffffe,
|
|
8697
|
-
type:
|
|
8832
|
+
type: types_2.MessageType.Disconnect,
|
|
8698
8833
|
args: [],
|
|
8699
8834
|
});
|
|
8700
8835
|
await conn.writeMessage(goodbye, tx);
|
|
8701
8836
|
conn.close();
|
|
8702
8837
|
__classPrivateFieldGet(this, _RemoteDatabase_connections, "f").delete(device.id);
|
|
8838
|
+
__classPrivateFieldGet(this, _RemoteDatabase_queryIds, "f").delete(device.id);
|
|
8703
8839
|
tx.finish();
|
|
8704
8840
|
};
|
|
8705
8841
|
__classPrivateFieldSet(this, _RemoteDatabase_deviceManager, deviceManager, "f");
|
|
8706
8842
|
__classPrivateFieldSet(this, _RemoteDatabase_hostDevice, hostDevice, "f");
|
|
8843
|
+
__classPrivateFieldSet(this, _RemoteDatabase_pickQueryId, pickQueryId, "f");
|
|
8707
8844
|
}
|
|
8708
8845
|
/**
|
|
8709
|
-
* The device
|
|
8710
|
-
*
|
|
8711
|
-
*
|
|
8712
|
-
*
|
|
8846
|
+
* The device this service belongs to — the virtual CDJ announced on the
|
|
8847
|
+
* network. Note that this is NOT necessarily the ID carried inside remotedb
|
|
8848
|
+
* messages: CDJs only answer queries whose in-protocol device-ID byte is
|
|
8849
|
+
* 1-6, so when this device sits outside that range (announced above the
|
|
8850
|
+
* player range to avoid collisions), each connection picks an in-range
|
|
8851
|
+
* query ID via {@link pickRemoteDbQueryId} instead.
|
|
8713
8852
|
*/
|
|
8714
8853
|
get hostDevice() {
|
|
8715
8854
|
return __classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f");
|
|
@@ -8723,7 +8862,7 @@ class RemoteDatabase {
|
|
|
8723
8862
|
* @returns null if the device does not export a remote database service
|
|
8724
8863
|
*/
|
|
8725
8864
|
async get(deviceId) {
|
|
8726
|
-
var _a;
|
|
8865
|
+
var _a, _b;
|
|
8727
8866
|
const device = __classPrivateFieldGet(this, _RemoteDatabase_deviceManager, "f").devices.get(deviceId);
|
|
8728
8867
|
if (device === undefined) {
|
|
8729
8868
|
return null;
|
|
@@ -8738,14 +8877,28 @@ class RemoteDatabase {
|
|
|
8738
8877
|
conn = __classPrivateFieldGet(this, _RemoteDatabase_connections, "f").get(deviceId);
|
|
8739
8878
|
// NOTE: We pass the same lock we use for this device to the query
|
|
8740
8879
|
// interface to ensure all query interfaces use the same lock.
|
|
8741
|
-
|
|
8880
|
+
// Queries must carry the same device ID the connection introduced
|
|
8881
|
+
// itself with, which is not always the announced host ID.
|
|
8882
|
+
const queryId = (_b = __classPrivateFieldGet(this, _RemoteDatabase_queryIds, "f").get(device.id)) !== null && _b !== void 0 ? _b : __classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f").id;
|
|
8883
|
+
return new QueryInterface(conn, lock, { ...__classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f"), id: queryId });
|
|
8742
8884
|
}
|
|
8743
8885
|
finally {
|
|
8744
8886
|
releaseLock();
|
|
8745
8887
|
}
|
|
8746
8888
|
}
|
|
8747
8889
|
}
|
|
8748
|
-
_RemoteDatabase_hostDevice = new WeakMap(), _RemoteDatabase_deviceManager = new WeakMap(), _RemoteDatabase_connections = new WeakMap(), _RemoteDatabase_deviceLocks = new WeakMap()
|
|
8890
|
+
_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) {
|
|
8891
|
+
const hostId = __classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f").id;
|
|
8892
|
+
// An announced ID already inside the answered range keeps working exactly
|
|
8893
|
+
// as it always has. Rekordbox (which numbers itself far above 6) answers
|
|
8894
|
+
// queries regardless, so only CDJ targets need an in-range stand-in.
|
|
8895
|
+
if (!__classPrivateFieldGet(this, _RemoteDatabase_pickQueryId, "f") ||
|
|
8896
|
+
hostId <= device_id_1.REMOTEDB_MAX_DEVICE_ID ||
|
|
8897
|
+
device.type !== types_1.DeviceType.CDJ) {
|
|
8898
|
+
return hostId;
|
|
8899
|
+
}
|
|
8900
|
+
return (0, device_id_1.pickRemoteDbQueryId)(device.id, __classPrivateFieldGet(this, _RemoteDatabase_deviceManager, "f").devices.values());
|
|
8901
|
+
};
|
|
8749
8902
|
exports["default"] = RemoteDatabase;
|
|
8750
8903
|
|
|
8751
8904
|
|
|
@@ -10039,10 +10192,29 @@ exports["default"] = PositionEmitter;
|
|
|
10039
10192
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
10040
10193
|
exports.PlayState = exports.StatusFlag = void 0;
|
|
10041
10194
|
/**
|
|
10042
|
-
* Status flag bitmasks
|
|
10195
|
+
* Status flag bitmasks (byte 0x89 of the CDJ status packet).
|
|
10196
|
+
*
|
|
10197
|
+
* Verified against CDJ-3000 firmware (EP122 FW3.20): the deck assembles this
|
|
10198
|
+
* byte in `sub_d7d3c8` (0xd7d3c8) from the BeatSyncMaster state struct, one
|
|
10199
|
+
* source boolean per bit. The bits not named here:
|
|
10200
|
+
*
|
|
10201
|
+
* - bit 0 (0x01): structurally unused — no code path ever sets it (always 0).
|
|
10202
|
+
* - bit 2 (0x04): a real, dedicated beat-sync boolean (struct +5), but its
|
|
10203
|
+
* name did not survive in the stripped `usecase::sync` async-task code. It
|
|
10204
|
+
* is normally 0, which is why it has never been observed on the wire.
|
|
10205
|
+
* - bit 7 (0x80): a real bit (struct +0xa) that is default-set at init and
|
|
10206
|
+
* whose de-assertion itself triggers a state-change notification — best read
|
|
10207
|
+
* as a "sync-master active / handoff-in-progress" toggle. Also normally 0 in
|
|
10208
|
+
* steady state.
|
|
10043
10209
|
*/
|
|
10044
10210
|
var StatusFlag;
|
|
10045
10211
|
(function (StatusFlag) {
|
|
10212
|
+
/**
|
|
10213
|
+
* Degraded to BPM Sync: the player is still tracking the master's tempo, but
|
|
10214
|
+
* beat alignment was dropped after a pitch-bend / jog nudge. Firmware sources
|
|
10215
|
+
* this from BeatSyncMaster struct +4.
|
|
10216
|
+
*/
|
|
10217
|
+
StatusFlag[StatusFlag["BpmSync"] = 2] = "BpmSync";
|
|
10046
10218
|
StatusFlag[StatusFlag["OnAir"] = 8] = "OnAir";
|
|
10047
10219
|
StatusFlag[StatusFlag["Sync"] = 16] = "Sync";
|
|
10048
10220
|
StatusFlag[StatusFlag["Master"] = 32] = "Master";
|
|
@@ -10115,14 +10287,24 @@ function statusFromPacket(packet) {
|
|
|
10115
10287
|
playState: packet[0x7b],
|
|
10116
10288
|
isOnAir: (packet[0x89] & types_1.CDJStatus.StatusFlag.OnAir) !== 0,
|
|
10117
10289
|
isSync: (packet[0x89] & types_1.CDJStatus.StatusFlag.Sync) !== 0,
|
|
10290
|
+
isBpmSync: (packet[0x89] & types_1.CDJStatus.StatusFlag.BpmSync) !== 0,
|
|
10118
10291
|
isMaster: (packet[0x89] & types_1.CDJStatus.StatusFlag.Master) !== 0,
|
|
10119
10292
|
isEmergencyMode: !!packet[0xba],
|
|
10120
10293
|
trackBPM,
|
|
10121
|
-
|
|
10122
|
-
|
|
10294
|
+
// The CDJ status packet carries four pitch values (firmware CDJ-3000 FW3.20,
|
|
10295
|
+
// sub_d7d610): Pitch1@0x8c and Pitch3@0xc0 are the *effective* pitch (in
|
|
10296
|
+
// effect / on the BPM display, from the fader or a synced master), while
|
|
10297
|
+
// Pitch2@0x98 and Pitch4@0xc4 track the *local fader* position. We expose
|
|
10298
|
+
// Pitch1 as `effectivePitch` and Pitch2 as `sliderPitch`.
|
|
10299
|
+
effectivePitch: calcPitch(packet.slice(0x8d, 0x8d + 3)),
|
|
10300
|
+
sliderPitch: calcPitch(packet.slice(0x99, 0x99 + 3)),
|
|
10123
10301
|
beatInMeasure: packet[0xa6],
|
|
10124
10302
|
beatsUntilCue,
|
|
10125
10303
|
beat,
|
|
10304
|
+
// Player-type / capability byte (dysentery's "nx"). Guarded because the
|
|
10305
|
+
// length check above only guarantees 0xc8; older/short packets may not
|
|
10306
|
+
// reach 0xcc, in which case the field reads as 0 (version-gated to 0 anyway).
|
|
10307
|
+
deviceType: packet.length > 0xcc ? packet[0xcc] : 0,
|
|
10126
10308
|
packetNum: packet.readUInt32BE(0xc8),
|
|
10127
10309
|
};
|
|
10128
10310
|
return status;
|
|
@@ -10867,11 +11049,17 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
|
10867
11049
|
exports.DEFAULT_MIXER_PLAYER_CEILING = exports.MIXER_PLAYER_CEILING = exports.MAX_DEVICE_ID = exports.REMOTEDB_MAX_DEVICE_ID = void 0;
|
|
10868
11050
|
exports.playerNumberCeiling = playerNumberCeiling;
|
|
10869
11051
|
exports.pickAvailableDeviceId = pickAvailableDeviceId;
|
|
11052
|
+
exports.pickRemoteDbQueryId = pickRemoteDbQueryId;
|
|
10870
11053
|
/**
|
|
10871
11054
|
* The highest device ID a CDJ will answer remotedb queries from.
|
|
10872
11055
|
*
|
|
10873
|
-
*
|
|
10874
|
-
*
|
|
11056
|
+
* This limit applies to the device-ID byte inside the remotedb protocol (the
|
|
11057
|
+
* Introduce message and every query header), NOT to the ID we announce on the
|
|
11058
|
+
* network. Hardware-verified on a CDJ-3000 (2026-08-30, two players + DJM-V5,
|
|
11059
|
+
* announced as VCDJ 7 throughout): queries carrying 1-6 are all answered —
|
|
11060
|
+
* including IDs of absent players and the target's own ID — while 7 and above
|
|
11061
|
+
* are silently ignored (the TCP connection and Introduce succeed, the query
|
|
11062
|
+
* never gets a response). See {@link pickRemoteDbQueryId}.
|
|
10875
11063
|
*/
|
|
10876
11064
|
exports.REMOTEDB_MAX_DEVICE_ID = 6;
|
|
10877
11065
|
/**
|
|
@@ -10879,11 +11067,13 @@ exports.REMOTEDB_MAX_DEVICE_ID = 6;
|
|
|
10879
11067
|
*/
|
|
10880
11068
|
exports.MAX_DEVICE_ID = 32;
|
|
10881
11069
|
/**
|
|
10882
|
-
* `DeviceType.Mixer`, inlined so this module stays free
|
|
10883
|
-
* it is re-exported into consumers' test environments,
|
|
10884
|
-
* whole type module (and its ip-address dependency) is
|
|
11070
|
+
* `DeviceType.Mixer` and `DeviceType.CDJ`, inlined so this module stays free
|
|
11071
|
+
* of runtime imports — it is re-exported into consumers' test environments,
|
|
11072
|
+
* where pulling in the whole type module (and its ip-address dependency) is
|
|
11073
|
+
* dead weight.
|
|
10885
11074
|
*/
|
|
10886
11075
|
const MIXER_DEVICE_TYPE = 0x03;
|
|
11076
|
+
const CDJ_DEVICE_TYPE = 0x01;
|
|
10887
11077
|
/**
|
|
10888
11078
|
* How many player numbers each mixer can hand out.
|
|
10889
11079
|
*
|
|
@@ -10982,24 +11172,233 @@ const ascending = (from, to) => {
|
|
|
10982
11172
|
* Returns null when every ID from 1 to 32 is occupied, in which case there is
|
|
10983
11173
|
* no safe ID and the caller must not join the network.
|
|
10984
11174
|
*/
|
|
10985
|
-
function pickAvailableDeviceId(usedIds, {
|
|
11175
|
+
function pickAvailableDeviceId(usedIds, { preferPlayerRange = false, playerCeiling } = {}) {
|
|
10986
11176
|
var _a;
|
|
10987
11177
|
const used = new Set(usedIds);
|
|
10988
11178
|
const aboveThePlayers = playerCeiling === undefined
|
|
10989
11179
|
? descending(exports.REMOTEDB_MAX_DEVICE_ID, 1)
|
|
10990
11180
|
: ascending(playerCeiling + 1, exports.REMOTEDB_MAX_DEVICE_ID);
|
|
10991
|
-
const
|
|
11181
|
+
const outsidePlayerRange = ascending(exports.REMOTEDB_MAX_DEVICE_ID + 1, exports.MAX_DEVICE_ID);
|
|
10992
11182
|
// Numbers a player on this rig could be assigned. Last resort: better to
|
|
10993
11183
|
// contend for a slot than not to join at all, and the announcer moves us if
|
|
10994
11184
|
// the player that owns it shows up.
|
|
10995
11185
|
const amongThePlayers = playerCeiling === undefined
|
|
10996
11186
|
? []
|
|
10997
11187
|
: descending(Math.min(playerCeiling, exports.REMOTEDB_MAX_DEVICE_ID), 1);
|
|
10998
|
-
const search =
|
|
10999
|
-
? [...aboveThePlayers, ...
|
|
11000
|
-
: [...
|
|
11188
|
+
const search = preferPlayerRange
|
|
11189
|
+
? [...aboveThePlayers, ...outsidePlayerRange, ...amongThePlayers]
|
|
11190
|
+
: [...outsidePlayerRange, ...aboveThePlayers, ...amongThePlayers];
|
|
11001
11191
|
return (_a = search.find(id => !used.has(id))) !== null && _a !== void 0 ? _a : null;
|
|
11002
11192
|
}
|
|
11193
|
+
/**
|
|
11194
|
+
* Choose the device ID to carry inside remotedb messages when querying a CDJ.
|
|
11195
|
+
*
|
|
11196
|
+
* CDJs only answer remotedb queries whose in-protocol device-ID byte is in
|
|
11197
|
+
* 1-6, but that byte is independent of the ID we announce on the network — so
|
|
11198
|
+
* a virtual CDJ announced safely above the player range (say 7 behind a
|
|
11199
|
+
* DJM-V10) can still query metadata by carrying an in-range ID here. Verified
|
|
11200
|
+
* on a CDJ-3000 (2026-08-30): absent IDs and even the target's own ID are
|
|
11201
|
+
* answered; 7+ is silently dropped.
|
|
11202
|
+
*
|
|
11203
|
+
* Older players are documented (dysentery, nexus era) as stricter: the byte
|
|
11204
|
+
* had to be 1-4, belong to a player actually on the network, and not be the
|
|
11205
|
+
* player being queried. The preference order below satisfies those rules
|
|
11206
|
+
* whenever the rig makes it possible, then falls back through choices newer
|
|
11207
|
+
* players accept.
|
|
11208
|
+
*/
|
|
11209
|
+
function pickRemoteDbQueryId(targetId, devices) {
|
|
11210
|
+
const playerIds = new Set();
|
|
11211
|
+
for (const device of devices) {
|
|
11212
|
+
if (device.type === CDJ_DEVICE_TYPE && device.id <= exports.REMOTEDB_MAX_DEVICE_ID) {
|
|
11213
|
+
playerIds.add(device.id);
|
|
11214
|
+
}
|
|
11215
|
+
}
|
|
11216
|
+
// A live player that isn't the target, lowest first — 1-4 satisfies every
|
|
11217
|
+
// documented era of the restriction.
|
|
11218
|
+
const otherPlayers = [...playerIds].filter(id => id !== targetId).sort((a, b) => a - b);
|
|
11219
|
+
if (otherPlayers.length > 0) {
|
|
11220
|
+
return otherPlayers[0];
|
|
11221
|
+
}
|
|
11222
|
+
// No other player on the rig: take the lowest 1-6 ID no player holds.
|
|
11223
|
+
for (let id = 1; id <= exports.REMOTEDB_MAX_DEVICE_ID; id++) {
|
|
11224
|
+
if (id !== targetId && !playerIds.has(id)) {
|
|
11225
|
+
return id;
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
11228
|
+
// Six players and every one of them is the target? Impossible, but the
|
|
11229
|
+
// target's own ID is answered too, so it is a safe closing fallback.
|
|
11230
|
+
return targetId;
|
|
11231
|
+
}
|
|
11232
|
+
|
|
11233
|
+
|
|
11234
|
+
/***/ },
|
|
11235
|
+
|
|
11236
|
+
/***/ "./src/virtualcdj/heartbeat.ts"
|
|
11237
|
+
/*!*************************************!*\
|
|
11238
|
+
!*** ./src/virtualcdj/heartbeat.ts ***!
|
|
11239
|
+
\*************************************/
|
|
11240
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
11241
|
+
|
|
11242
|
+
"use strict";
|
|
11243
|
+
|
|
11244
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
11245
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
11246
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
11247
|
+
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");
|
|
11248
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11249
|
+
};
|
|
11250
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
11251
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
11252
|
+
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");
|
|
11253
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11254
|
+
};
|
|
11255
|
+
var _StagehandHeartbeat_instances, _StagehandHeartbeat_statusSocket, _StagehandHeartbeat_vcdj, _StagehandHeartbeat_deviceManager, _StagehandHeartbeat_logger, _StagehandHeartbeat_intervalHandle, _StagehandHeartbeat_sendHeartbeats;
|
|
11256
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
11257
|
+
exports.StagehandHeartbeat = exports.STAGEHAND_HEARTBEAT_INTERVAL = void 0;
|
|
11258
|
+
exports.makeStagehandMixerHeartbeat = makeStagehandMixerHeartbeat;
|
|
11259
|
+
exports.makeStagehandPlayerHeartbeat = makeStagehandPlayerHeartbeat;
|
|
11260
|
+
const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
|
|
11261
|
+
const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
|
|
11262
|
+
const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
|
|
11263
|
+
/**
|
|
11264
|
+
* Cadence of the Stagehand unicast keep-alive, in milliseconds.
|
|
11265
|
+
*
|
|
11266
|
+
* The real iOS Stagehand app heartbeats every discovered player and the mixer
|
|
11267
|
+
* at ~4 Hz (250ms median inter-packet gap in captured traffic). This is the
|
|
11268
|
+
* stream that keeps AlphaTheta hardware unicasting live state (`0x39` mixer
|
|
11269
|
+
* fader/EQ, `0x58` VU on the mixer; `0x69` slim status, waveform families on
|
|
11270
|
+
* the CDJs) to our IP — subnet-broadcast presence alone (the `0x06` keep-alive)
|
|
11271
|
+
* is not enough to bootstrap it.
|
|
11272
|
+
*/
|
|
11273
|
+
exports.STAGEHAND_HEARTBEAT_INTERVAL = 250;
|
|
11274
|
+
/**
|
|
11275
|
+
* Builds a Stagehand name field for a unicast frame.
|
|
11276
|
+
*
|
|
11277
|
+
* Unlike the 20-byte {@link buildName} used by the broadcast announce/claim
|
|
11278
|
+
* frames, unicast frames carry a 19-byte name field (offsets 11..29) with the
|
|
11279
|
+
* device type / model bytes beginning at offset 30. The name is ASCII, NUL
|
|
11280
|
+
* padded.
|
|
11281
|
+
*/
|
|
11282
|
+
function buildUnicastName(name) {
|
|
11283
|
+
const field = new Uint8Array(19);
|
|
11284
|
+
field.set(Buffer.from(name, 'ascii').subarray(0, 19));
|
|
11285
|
+
return field;
|
|
11286
|
+
}
|
|
11287
|
+
/**
|
|
11288
|
+
* Build the Stagehand mixer keep-alive (`0x3a`, 40 bytes).
|
|
11289
|
+
*
|
|
11290
|
+
* Verified byte-for-byte against the real iOS Stagehand app unicasting to a
|
|
11291
|
+
* DJM-A9 at 4 Hz (Phase 4/5 captures §4.14.2). The 10-byte trailer
|
|
11292
|
+
* `21 02 00 fe 00 04 00 1c 00 00` is constant across every observed sample —
|
|
11293
|
+
* byte 33 (`0xfe`) is a VCDJ sentinel, not our runtime device number, so no
|
|
11294
|
+
* per-device bytes vary here.
|
|
11295
|
+
*/
|
|
11296
|
+
function makeStagehandMixerHeartbeat(vcdj) {
|
|
11297
|
+
const parts = [
|
|
11298
|
+
...constants_1.PROLINK_HEADER, // 0-9: magic
|
|
11299
|
+
0x3a, // 10: type 0x3a (mixer keep-alive)
|
|
11300
|
+
...buildUnicastName(vcdj.name), // 11-29: name (19 bytes)
|
|
11301
|
+
0x21,
|
|
11302
|
+
0x02,
|
|
11303
|
+
0x00,
|
|
11304
|
+
0xfe,
|
|
11305
|
+
0x00,
|
|
11306
|
+
0x04,
|
|
11307
|
+
0x00,
|
|
11308
|
+
0x1c,
|
|
11309
|
+
0x00,
|
|
11310
|
+
0x00, // 30-39: constant trailer
|
|
11311
|
+
];
|
|
11312
|
+
return Uint8Array.from(parts);
|
|
11313
|
+
}
|
|
11314
|
+
/**
|
|
11315
|
+
* Build the Stagehand player keep-alive (`0x68`, 36 bytes).
|
|
11316
|
+
*
|
|
11317
|
+
* Verified byte-for-byte against the real iOS Stagehand app unicasting to a
|
|
11318
|
+
* CDJ-3000 at ~2-4 Hz. Body (offsets 30-35) is `03 01 00 3a 00 00`: `0x03`
|
|
11319
|
+
* persona/channel, `0x01` device subkind, `0x00` flag, `0x3a` Stagehand
|
|
11320
|
+
* model-code stamp, two reserved bytes. Constant across every observed sample.
|
|
11321
|
+
*/
|
|
11322
|
+
function makeStagehandPlayerHeartbeat(vcdj) {
|
|
11323
|
+
const parts = [
|
|
11324
|
+
...constants_1.PROLINK_HEADER, // 0-9: magic
|
|
11325
|
+
0x68, // 10: type 0x68 (player keep-alive)
|
|
11326
|
+
...buildUnicastName(vcdj.name), // 11-29: name (19 bytes)
|
|
11327
|
+
0x03,
|
|
11328
|
+
0x01,
|
|
11329
|
+
0x00,
|
|
11330
|
+
0x3a,
|
|
11331
|
+
0x00,
|
|
11332
|
+
0x00, // 30-35: constant body
|
|
11333
|
+
];
|
|
11334
|
+
return Uint8Array.from(parts);
|
|
11335
|
+
}
|
|
11336
|
+
/**
|
|
11337
|
+
* Unicasts Stagehand keep-alive frames to every discovered player and mixer so
|
|
11338
|
+
* that AlphaTheta hardware begins (and keeps) pushing live state to our IP.
|
|
11339
|
+
*
|
|
11340
|
+
* The {@link StagehandAnnouncer} makes us *visible* on the network via subnet
|
|
11341
|
+
* broadcast; this heartbeat is what makes hardware actually *talk back*. It
|
|
11342
|
+
* targets the mixer with `0x3a` and each CDJ with `0x68`, mirroring the real
|
|
11343
|
+
* iOS Stagehand app. Frames are sent from the status socket (port 50002) to
|
|
11344
|
+
* each device's port 50002, which is where their unicast state replies land and
|
|
11345
|
+
* where {@link StatusEmitter} listens for them.
|
|
11346
|
+
*/
|
|
11347
|
+
class StagehandHeartbeat {
|
|
11348
|
+
constructor(vcdj, statusSocket, deviceManager, logger = logger_1.noopLogger) {
|
|
11349
|
+
_StagehandHeartbeat_instances.add(this);
|
|
11350
|
+
_StagehandHeartbeat_statusSocket.set(this, void 0);
|
|
11351
|
+
_StagehandHeartbeat_vcdj.set(this, void 0);
|
|
11352
|
+
_StagehandHeartbeat_deviceManager.set(this, void 0);
|
|
11353
|
+
_StagehandHeartbeat_logger.set(this, void 0);
|
|
11354
|
+
_StagehandHeartbeat_intervalHandle.set(this, void 0);
|
|
11355
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_vcdj, vcdj, "f");
|
|
11356
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_statusSocket, statusSocket, "f");
|
|
11357
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_deviceManager, deviceManager, "f");
|
|
11358
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_logger, logger, "f");
|
|
11359
|
+
}
|
|
11360
|
+
start() {
|
|
11361
|
+
if (__classPrivateFieldGet(this, _StagehandHeartbeat_intervalHandle, "f") !== undefined) {
|
|
11362
|
+
return;
|
|
11363
|
+
}
|
|
11364
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_logger, "f").info('Starting Stagehand heartbeat (unicast keep-alive)');
|
|
11365
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_instances, "m", _StagehandHeartbeat_sendHeartbeats).call(this);
|
|
11366
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_intervalHandle, setInterval(() => __classPrivateFieldGet(this, _StagehandHeartbeat_instances, "m", _StagehandHeartbeat_sendHeartbeats).call(this), exports.STAGEHAND_HEARTBEAT_INTERVAL), "f");
|
|
11367
|
+
}
|
|
11368
|
+
stop() {
|
|
11369
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_logger, "f").info('Stopping Stagehand heartbeat');
|
|
11370
|
+
if (__classPrivateFieldGet(this, _StagehandHeartbeat_intervalHandle, "f") !== undefined) {
|
|
11371
|
+
clearInterval(__classPrivateFieldGet(this, _StagehandHeartbeat_intervalHandle, "f"));
|
|
11372
|
+
__classPrivateFieldSet(this, _StagehandHeartbeat_intervalHandle, undefined, "f");
|
|
11373
|
+
}
|
|
11374
|
+
}
|
|
11375
|
+
}
|
|
11376
|
+
exports.StagehandHeartbeat = StagehandHeartbeat;
|
|
11377
|
+
_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() {
|
|
11378
|
+
for (const device of __classPrivateFieldGet(this, _StagehandHeartbeat_deviceManager, "f").devices.values()) {
|
|
11379
|
+
// Never heartbeat ourselves.
|
|
11380
|
+
if (device.id === __classPrivateFieldGet(this, _StagehandHeartbeat_vcdj, "f").id) {
|
|
11381
|
+
continue;
|
|
11382
|
+
}
|
|
11383
|
+
let packet = null;
|
|
11384
|
+
switch (device.type) {
|
|
11385
|
+
case types_1.DeviceType.Mixer:
|
|
11386
|
+
packet = makeStagehandMixerHeartbeat(__classPrivateFieldGet(this, _StagehandHeartbeat_vcdj, "f"));
|
|
11387
|
+
break;
|
|
11388
|
+
case types_1.DeviceType.CDJ:
|
|
11389
|
+
packet = makeStagehandPlayerHeartbeat(__classPrivateFieldGet(this, _StagehandHeartbeat_vcdj, "f"));
|
|
11390
|
+
break;
|
|
11391
|
+
default:
|
|
11392
|
+
// Rekordbox / other Stagehand peers are not heartbeat targets.
|
|
11393
|
+
continue;
|
|
11394
|
+
}
|
|
11395
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_statusSocket, "f").send(packet, constants_1.STATUS_PORT, device.ip.address, err => {
|
|
11396
|
+
if (err) {
|
|
11397
|
+
__classPrivateFieldGet(this, _StagehandHeartbeat_logger, "f").debug(`Stagehand heartbeat to ${device.name} (${device.ip.address}) failed: ${err.message}`);
|
|
11398
|
+
}
|
|
11399
|
+
});
|
|
11400
|
+
}
|
|
11401
|
+
};
|
|
11003
11402
|
|
|
11004
11403
|
|
|
11005
11404
|
/***/ },
|
|
@@ -11428,16 +11827,16 @@ _Announcer_announceSocket = new WeakMap(), _Announcer_deviceManager = new WeakMa
|
|
|
11428
11827
|
}, _Announcer_handleConflict = function _Announcer_handleConflict() {
|
|
11429
11828
|
// Stop announcing under the contested ID
|
|
11430
11829
|
__classPrivateFieldGet(this, _Announcer_instances, "m", _Announcer_clearTimer).call(this);
|
|
11431
|
-
// Stay in whichever range we were already in
|
|
11432
|
-
//
|
|
11433
|
-
//
|
|
11830
|
+
// Stay in whichever range we were already in, so a conflict on player 5
|
|
11831
|
+
// looks for another free player number first rather than jumping the
|
|
11832
|
+
// whole device out of the range the DJ configured it into.
|
|
11434
11833
|
//
|
|
11435
11834
|
// The mixer decides which of those numbers are actually safe: a player can
|
|
11436
11835
|
// only be assigned a number its mixer has a channel for, so anything above
|
|
11437
11836
|
// that is out of reach of the rig that just contested us.
|
|
11438
11837
|
const devices = [...__classPrivateFieldGet(this, _Announcer_deviceManager, "f").devices.values()];
|
|
11439
11838
|
const newId = (0, device_id_1.pickAvailableDeviceId)(devices.map(d => d.id), {
|
|
11440
|
-
|
|
11839
|
+
preferPlayerRange: __classPrivateFieldGet(this, _Announcer_vcdj, "f").id <= device_id_1.REMOTEDB_MAX_DEVICE_ID,
|
|
11441
11840
|
playerCeiling: (0, device_id_1.playerNumberCeiling)(devices),
|
|
11442
11841
|
});
|
|
11443
11842
|
if (newId === null) {
|
|
@@ -11540,6 +11939,7 @@ _Announcer_announceSocket = new WeakMap(), _Announcer_deviceManager = new WeakMa
|
|
|
11540
11939
|
__classPrivateFieldSet(this, _Announcer_intervalHandle, setInterval(sendKeepAlive, constants_1.ANNOUNCE_INTERVAL), "f");
|
|
11541
11940
|
};
|
|
11542
11941
|
__exportStar(__webpack_require__(/*! ./device-id */ "./src/virtualcdj/device-id.ts"), exports);
|
|
11942
|
+
__exportStar(__webpack_require__(/*! ./heartbeat */ "./src/virtualcdj/heartbeat.ts"), exports);
|
|
11543
11943
|
__exportStar(__webpack_require__(/*! ./stagehand */ "./src/virtualcdj/stagehand.ts"), exports);
|
|
11544
11944
|
|
|
11545
11945
|
|