fix(audio,schedule): 音乐只扫 audio/music 避免错播短提示音假失败 + 远端主机完全离线时通过 HTTP REST + WS 主动推送拿到时刻表
- listMusicTracks/getPlaybackAudioByBaseName 彻底去掉 audio/beep fallback:空目录就空,提示音 wav(fc1/ksts) 不再出现在音乐列表,杜绝 API 返回 sent=1 但实际播放 0.25s 导致用户以为失败 - music enqueue 优先级从 0 下调到 -1,approaching/alert/arrival 永远干净抢占,equal-prio 不会把整首歌切成碎段 - 新增 4 套取时刻表的兜底方法(最远端主机完全加载不到也能用): 1. POST/GET /api/stations/:name/schedule — 管理员或远端屏幕 HTTP 拉/手动注入 upcomingTrains+currentTrain,完全不依赖本地 Lua block-reader 2. GET /api/stations/schedule(?line=CE) — 批量按线路拉取全量时刻表摘要 3. 同站点 schedule TTL 10min 鲜度字段 scheduleIsStale / scheduleFreshForMs 返回给客户端自行判断 4. 每次 heartbeat/station_schedule_sync 触发 WS 广播 station_schedule_available + same_cable_group_schedule 推送到同站/同线缆分组全部连接(不只 master) - normalizeStationRecord + POST schedule 新站自动补 lastScheduleSyncedAt=0 避免 500
This commit is contained in:
+253
-46
@@ -450,6 +450,7 @@ function upsertStationNode(stationName, ws, data) {
|
|||||||
st.upcomingTrains = bestSyncedNode.upcomingTrains || [];
|
st.upcomingTrains = bestSyncedNode.upcomingTrains || [];
|
||||||
st.upcomingCount = bestSyncedNode.upcomingCount || 0;
|
st.upcomingCount = bestSyncedNode.upcomingCount || 0;
|
||||||
st.currentTrain = bestSyncedNode.currentTrain || null;
|
st.currentTrain = bestSyncedNode.currentTrain || null;
|
||||||
|
st.lastScheduleSyncedAt = bestSynced;
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeStationRecord(st);
|
normalizeStationRecord(st);
|
||||||
@@ -1388,13 +1389,9 @@ function getManagedMediaUrl(kind, fileName) {
|
|||||||
|
|
||||||
function listMusicTracks() {
|
function listMusicTracks() {
|
||||||
const out = new Set();
|
const out = new Set();
|
||||||
const scanDirs = [audioMusicDir];
|
|
||||||
if (fs.existsSync(audioMusicDir) && fs.readdirSync(audioMusicDir).length === 0) {
|
|
||||||
scanDirs.push(...getBeepAudioDirs());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const dir of scanDirs) {
|
const addTracksFromDir = (dir) => {
|
||||||
if (!fs.existsSync(dir)) continue;
|
if (!dir || !fs.existsSync(dir)) return;
|
||||||
for (const file of fs.readdirSync(dir)) {
|
for (const file of fs.readdirSync(dir)) {
|
||||||
if (!file || file.startsWith('.')) continue;
|
if (!file || file.startsWith('.')) continue;
|
||||||
const full = path.join(dir, file);
|
const full = path.join(dir, file);
|
||||||
@@ -1403,8 +1400,13 @@ function listMusicTracks() {
|
|||||||
const base = normalizeBaseName(file);
|
const base = normalizeBaseName(file);
|
||||||
if (base) out.add(base);
|
if (base) out.add(base);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
addTracksFromDir(audioMusicDir);
|
||||||
|
// NOTE: DO NOT fallback to beep/station/line dirs — those are short ~1s
|
||||||
|
// announcement stingers. Users see "fc1/ksts" in music list, select them,
|
||||||
|
// the API returns success -> plays 0.25s of stinger -> user thinks music
|
||||||
|
// player is broken / didn't play at all. Music list = only music dir tracks.
|
||||||
return Array.from(out).sort((a, b) => a.localeCompare(b));
|
return Array.from(out).sort((a, b) => a.localeCompare(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1451,48 +1453,39 @@ function resolveAudioPathWithFuzzy(dir, base, ext) {
|
|||||||
function getPlaybackAudioByBaseName(baseName) {
|
function getPlaybackAudioByBaseName(baseName) {
|
||||||
const base = normalizeBaseName(baseName);
|
const base = normalizeBaseName(baseName);
|
||||||
if (!base) throw new Error('Invalid track name');
|
if (!base) throw new Error('Invalid track name');
|
||||||
|
const SEARCH_KINDS = STREAMABLE_AUDIO_EXTENSIONS;
|
||||||
|
|
||||||
for (const ext of STREAMABLE_AUDIO_EXTENSIONS) {
|
// Music tracks only live in audio/music. Users historically got confused when
|
||||||
const searchDirs = [
|
// music list showed station beeps (fc1/ksts via empty-dir fallback), clicked them,
|
||||||
{ dir: audioMusicDir, kind: 'audio/music' },
|
// and received a successful HTTP response that actually played a 0.25s stinger —
|
||||||
...getBeepAudioDirs().map(d => ({ dir: d, kind: 'audio/beep' })),
|
// the UX looked like "music failed". Force music tracks to resolve only inside
|
||||||
{ dir: audioStationDir, kind: 'audio/station' },
|
// audioMusicDir; missing songs → 404 explicitly instead of silent fallback to
|
||||||
{ dir: audioLineDir, kind: 'audio/station' }
|
// audio/beep or audio/station.
|
||||||
];
|
const kindTag = 'audio/music';
|
||||||
for (const { dir, kind } of searchDirs) {
|
for (const ext of SEARCH_KINDS) {
|
||||||
const p = resolveAudioPathWithFuzzy(dir, base, ext);
|
const p = resolveAudioPathWithFuzzy(audioMusicDir, base, ext);
|
||||||
if (!p) continue;
|
|
||||||
let durationSec = 0;
|
|
||||||
if (ext === '.wav') {
|
|
||||||
const buffer = fs.readFileSync(p);
|
|
||||||
durationSec = getWavDurationSec(buffer) || 0;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
sourcePath: p,
|
|
||||||
audioFormat: 'stream',
|
|
||||||
streamUrl: getManagedMediaUrl(kind, path.basename(p)),
|
|
||||||
durationSec
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const { dir } of [
|
|
||||||
{ dir: audioMusicDir },
|
|
||||||
...getBeepAudioDirs().map(d => ({ dir: d })),
|
|
||||||
{ dir: audioStationDir },
|
|
||||||
{ dir: audioLineDir }
|
|
||||||
]) {
|
|
||||||
const p = resolveAudioPathWithFuzzy(dir, base, '.dfpwm');
|
|
||||||
if (!p) continue;
|
if (!p) continue;
|
||||||
const buffer = fs.readFileSync(p);
|
let durationSec = 0;
|
||||||
|
if (ext === '.wav') {
|
||||||
|
const buffer = fs.readFileSync(p);
|
||||||
|
durationSec = getWavDurationSec(buffer) || 0;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
buffer,
|
sourcePath: p,
|
||||||
audioFormat: 'dfpwm',
|
audioFormat: 'stream',
|
||||||
durationSec: buffer.length / 6000
|
streamUrl: getManagedMediaUrl(kindTag, path.basename(p)),
|
||||||
|
durationSec
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Audio not found: ${base}`);
|
// dfpwm fallback only inside audio/music too
|
||||||
|
const pd = resolveAudioPathWithFuzzy(audioMusicDir, base, '.dfpwm');
|
||||||
|
if (pd) {
|
||||||
|
const buffer = fs.readFileSync(pd);
|
||||||
|
return { buffer, audioFormat: 'dfpwm', durationSec: buffer.length / 6000 };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Music track not found in audio/music/: ${base} (upload .wav files to audio/music/ directory)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelStationAudioTask(task) {
|
function cancelStationAudioTask(task) {
|
||||||
@@ -1940,6 +1933,7 @@ function normalizeStationRecord(station) {
|
|||||||
if (!station || !station.name) return;
|
if (!station || !station.name) return;
|
||||||
station.platform = normalizeStationPlatform(station.platform || getPlatformNumberFromName(station.name));
|
station.platform = normalizeStationPlatform(station.platform || getPlatformNumberFromName(station.name));
|
||||||
station.code = normalizeStationCode(station.code || getStationCodeFromName(station.name)) || station.code || '';
|
station.code = normalizeStationCode(station.code || getStationCodeFromName(station.name)) || station.code || '';
|
||||||
|
if (station.lastScheduleSyncedAt === undefined) station.lastScheduleSyncedAt = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshStationIndex() {
|
function refreshStationIndex() {
|
||||||
@@ -2800,6 +2794,10 @@ function handleHeartbeat(ws, data) {
|
|||||||
sendStationGroupState(cleanName);
|
sendStationGroupState(cleanName);
|
||||||
broadcastUpdate();
|
broadcastUpdate();
|
||||||
}
|
}
|
||||||
|
// If heartbeat brought in new schedule data, notify every connected node
|
||||||
|
if (data.upcomingTrains || data.currentTrain) {
|
||||||
|
try { broadcastStationScheduleToEveryNode(cleanName); } catch {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScheduleSync(ws, data) {
|
function handleScheduleSync(ws, data) {
|
||||||
@@ -2815,6 +2813,7 @@ function handleScheduleSync(ws, data) {
|
|||||||
}
|
}
|
||||||
sendStationGroupState(cleanName);
|
sendStationGroupState(cleanName);
|
||||||
broadcastUpdate();
|
broadcastUpdate();
|
||||||
|
try { broadcastStationScheduleToEveryNode(cleanName); } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStationRegister(ws, data) {
|
function handleStationRegister(ws, data) {
|
||||||
@@ -3574,7 +3573,8 @@ app.post('/api/stations', express.json(), (req, res) => {
|
|||||||
upcomingCount: 0,
|
upcomingCount: 0,
|
||||||
upcomingTrains: [],
|
upcomingTrains: [],
|
||||||
currentTrain: null,
|
currentTrain: null,
|
||||||
nodes: []
|
nodes: [],
|
||||||
|
lastScheduleSyncedAt: 0
|
||||||
};
|
};
|
||||||
normalizeStationRecord(stations[name]);
|
normalizeStationRecord(stations[name]);
|
||||||
saveData();
|
saveData();
|
||||||
@@ -3863,6 +3863,213 @@ app.delete('/api/trains/:id', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =================== Schedule APIs (offline-station / remote-host fallback) ===================
|
||||||
|
//
|
||||||
|
// Problem statement the user reported:
|
||||||
|
// "The station at the FAR END of a long cable may not load (e.g. because
|
||||||
|
// cable middle node died / cable line is weak). If a train parks exactly
|
||||||
|
// at that far-end position, the station client can't fetch the schedule
|
||||||
|
// from the local Lua-side list.txt/block-reader because that node never
|
||||||
|
// came online. How can we get the schedule remotely / by other means?"
|
||||||
|
//
|
||||||
|
// Solution (cloud-side REST + multi-sync aggregation):
|
||||||
|
// [Level 1] Per-station schedule aggregation: ANY node of station X that
|
||||||
|
// loads list.txt / runs a block-reader → calls
|
||||||
|
// WS type=station_schedule_sync (already in handleScheduleSync)
|
||||||
|
// or WS heartbeat with upcomingTrains/currentTrain (already merged
|
||||||
|
// into stations[X].upcomingTrains via bestSynced node).
|
||||||
|
// [Level 2] HTTP REST: GET /api/stations/:nameOrCode/schedule is callable
|
||||||
|
// from ANYWHERE on the network (far-end box, admin tool, even a
|
||||||
|
// second display that's running pure browser without Lua loaded).
|
||||||
|
// Returns upcomingTrains, currentTrain, activeTrains for station,
|
||||||
|
// with TTL, and tells caller the "schedule freshness" so it can
|
||||||
|
// choose to fall back to another station.
|
||||||
|
// [Level 3] Cross-station fallback: if station X has NO synced schedule,
|
||||||
|
// but the *same line code* stations CE-01..CE-14 have schedules,
|
||||||
|
// we merge upcomingTrains with CE line's global activeTrains so
|
||||||
|
// a stopped train at far-end can at least be seen through another
|
||||||
|
// station's data.
|
||||||
|
// [Level 4] Upstream list.txt + block-reader manual upload APIs:
|
||||||
|
// POST /api/stations/:name/schedule lets a remote tool inject
|
||||||
|
// list.txt content / upcomingTrains even if the box never boots.
|
||||||
|
|
||||||
|
function pickScheduleStation(selector) {
|
||||||
|
const key = String(selector || '').trim();
|
||||||
|
if (!key) return null;
|
||||||
|
// 1. exact station name
|
||||||
|
if (stations[key]) return stations[key];
|
||||||
|
// 2. exact station code
|
||||||
|
for (const k of Object.keys(stations)) {
|
||||||
|
const s = stations[k];
|
||||||
|
if (s && String(s.code || '').toLowerCase() === key.toLowerCase()) return s;
|
||||||
|
}
|
||||||
|
// 3. flexible fuzzy name
|
||||||
|
const flex = findStationByFlexibleName(key);
|
||||||
|
return flex || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSchedulePayload(st, options) {
|
||||||
|
const opts = options || {};
|
||||||
|
const NAME = (st && st.name) || '';
|
||||||
|
const SYNC_TTL_MS = 10 * 60 * 1000; // 10 minutes: schedules older than this are marked as stale
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const upcoming = (st && Array.isArray(st.upcomingTrains)) ? st.upcomingTrains.slice() : [];
|
||||||
|
const current = (st && st.currentTrain) ? (typeof st.currentTrain === 'object' ? { ...st.currentTrain } : st.currentTrain) : null;
|
||||||
|
const lastSyncedAt = Number(st && st.lastScheduleSyncedAt) || 0;
|
||||||
|
const freshForMs = lastSyncedAt ? Math.max(0, SYNC_TTL_MS - (now - lastSyncedAt)) : 0;
|
||||||
|
const stale = lastSyncedAt === 0 ? true : (now - lastSyncedAt) >= SYNC_TTL_MS;
|
||||||
|
|
||||||
|
const lineCode = String(st?.code || '').split(/[-_/]/)[0] || '';
|
||||||
|
const onlineWs = getAllStationConnections(NAME).length;
|
||||||
|
|
||||||
|
const sameLineTrains = [];
|
||||||
|
if (opts.includeLineTrains !== false) {
|
||||||
|
for (const t of Array.isArray(activeTrains) ? activeTrains : []) {
|
||||||
|
if (!t) continue;
|
||||||
|
const tLine = String(t?.stationCode || t?.lineCode || '').split(/[-_/]/)[0];
|
||||||
|
if (lineCode && tLine === lineCode) sameLineTrains.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stationName: NAME,
|
||||||
|
stationCode: st?.code || '',
|
||||||
|
stationPlatform: st?.platform || '',
|
||||||
|
onlineNodeCount: onlineWs,
|
||||||
|
totalNodeCount: Array.isArray(st?.nodes) ? st.nodes.length : 0,
|
||||||
|
cableGroupId: st?.audioCableGroup || '',
|
||||||
|
lastScheduleSyncedAt,
|
||||||
|
lastSeen: Number(st?.lastSeen || 0),
|
||||||
|
scheduleFreshForMs: freshForMs,
|
||||||
|
scheduleIsStale: stale,
|
||||||
|
scheduleTtlMs: SYNC_TTL_MS,
|
||||||
|
upcomingCount: Number(st?.upcomingCount) || upcoming.length,
|
||||||
|
upcomingTrains: upcoming,
|
||||||
|
currentTrain: current,
|
||||||
|
sameLineActiveTrains: sameLineTrains
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/api/stations/:name/schedule', (req, res) => {
|
||||||
|
const rawName = decodeURIComponent(String(req.params.name || '')).trim();
|
||||||
|
const includeLine = req.query.line === '0' ? false : true;
|
||||||
|
const st = pickScheduleStation(rawName);
|
||||||
|
if (!st) {
|
||||||
|
// Even when we can't find the exact station, try giving cross-line data
|
||||||
|
// so a far-end client with a stationName typo / not yet registered
|
||||||
|
// still gets something usable.
|
||||||
|
const codePrefix = rawName.split(/[-_/]/)[0] || '';
|
||||||
|
const lineTrains = [];
|
||||||
|
if (codePrefix) {
|
||||||
|
for (const t of Array.isArray(activeTrains) ? activeTrains : []) {
|
||||||
|
const tLine = String(t?.stationCode || t?.lineCode || '').split(/[-_/]/)[0];
|
||||||
|
if (tLine.toLowerCase() === codePrefix.toLowerCase()) lineTrains.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: `Station not found: ${rawName}`,
|
||||||
|
hint: `Register via POST /api/stations or call POST /api/stations/${encodeURIComponent(rawName)}/schedule to upload schedule manually`,
|
||||||
|
sameLineActiveTrains: lineTrains
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.json({ success: true, schedule: buildSchedulePayload(st, { includeLineTrains: includeLine }) });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convenience: list ALL stations' schedule summary. This is handy when a
|
||||||
|
// far-end admin panel or a secondary sign doesn't know which station code to
|
||||||
|
// pull — it can just GET /api/stations/schedule every ~30s.
|
||||||
|
app.get('/api/stations/schedule', (req, res) => {
|
||||||
|
const linesOnly = req.query.line;
|
||||||
|
const out = {};
|
||||||
|
for (const k of Object.keys(stations)) {
|
||||||
|
const s = stations[k];
|
||||||
|
if (!s) continue;
|
||||||
|
if (linesOnly) {
|
||||||
|
const lineCode = String(s.code || '').split(/[-_/]/)[0];
|
||||||
|
if (String(linesOnly).toLowerCase() !== lineCode.toLowerCase()) continue;
|
||||||
|
}
|
||||||
|
out[k] = buildSchedulePayload(s, { includeLineTrains: false });
|
||||||
|
}
|
||||||
|
res.json({ success: true, schedules: out });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manual schedule push (for far-end offline stations): lets admin / a
|
||||||
|
// remote script inject upcomingTrains + currentTrain when the Lua host
|
||||||
|
// never boots at the far end. Exactly equivalent to the station box itself
|
||||||
|
// calling WS station_schedule_sync, but via HTTP.
|
||||||
|
app.post('/api/stations/:name/schedule', express.json({ limit: '2mb' }), (req, res) => {
|
||||||
|
const rawName = decodeURIComponent(String(req.params.name || '')).trim();
|
||||||
|
if (!stations[rawName]) {
|
||||||
|
const code = String(req.body?.code || '').trim() || String(req.body?.stationCode || '').trim() || '';
|
||||||
|
stations[rawName] = {
|
||||||
|
name: rawName,
|
||||||
|
code,
|
||||||
|
platform: getPlatformNumberFromName(rawName),
|
||||||
|
audioCableGroup: '',
|
||||||
|
version: '',
|
||||||
|
lastSeen: Date.now(),
|
||||||
|
upcomingCount: 0,
|
||||||
|
upcomingTrains: [],
|
||||||
|
currentTrain: null,
|
||||||
|
nodes: [],
|
||||||
|
lastScheduleSyncedAt: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const st = stations[rawName];
|
||||||
|
const upc = Array.isArray(req.body?.upcomingTrains) ? req.body.upcomingTrains.slice() : (Array.isArray(st.upcomingTrains) ? st.upcomingTrains.slice() : []);
|
||||||
|
const upcCount = Number(req.body?.upcomingCount ?? upc.length);
|
||||||
|
const cur = (req.body?.currentTrain !== undefined) ? req.body.currentTrain : st.currentTrain;
|
||||||
|
st.upcomingTrains = upc;
|
||||||
|
st.upcomingCount = upcCount;
|
||||||
|
st.currentTrain = cur;
|
||||||
|
st.lastScheduleSyncedAt = Date.now();
|
||||||
|
st.lastSeen = Math.max(Number(st.lastSeen || 0), st.lastScheduleSyncedAt);
|
||||||
|
saveData();
|
||||||
|
sendStationGroupState(st.name);
|
||||||
|
broadcastUpdate();
|
||||||
|
log('INFO', `Manual schedule push for "${st.name}": upcoming=${upcCount} hasCurrent=${cur ? 'yes' : 'no'}`);
|
||||||
|
res.json({ success: true, schedule: buildSchedulePayload(st) });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also push schedule as soon as ANY node syncs, not just to console. Broadcast
|
||||||
|
// station_schedule_available to ALL connected station WS, so far-end boxes
|
||||||
|
// that never loaded their own list.txt can still subscribe via WS and
|
||||||
|
// repaint instantly.
|
||||||
|
function broadcastStationScheduleToEveryNode(stationName) {
|
||||||
|
const st = stations[stationName];
|
||||||
|
if (!st) return;
|
||||||
|
const msg = JSON.stringify({
|
||||||
|
type: 'station_schedule_available',
|
||||||
|
stationName: st.name,
|
||||||
|
stationCode: st.code || '',
|
||||||
|
schedule: buildSchedulePayload(st, { includeLineTrains: true })
|
||||||
|
});
|
||||||
|
for (const conn of getAllStationConnections(stationName)) {
|
||||||
|
try { conn.send(msg); } catch {}
|
||||||
|
}
|
||||||
|
// And the same cable group's other stations receive same-line active trains
|
||||||
|
const cgId = st.audioCableGroup || '';
|
||||||
|
const cg = cgId ? audioCableGroups[cgId] : null;
|
||||||
|
if (cg && Array.isArray(cg.stationNames)) {
|
||||||
|
for (const other of cg.stationNames) {
|
||||||
|
if (other === stationName) continue;
|
||||||
|
const o = stations[other];
|
||||||
|
if (!o) continue;
|
||||||
|
for (const conn of getAllStationConnections(other)) {
|
||||||
|
try {
|
||||||
|
conn.send(JSON.stringify({
|
||||||
|
type: 'same_cable_group_schedule',
|
||||||
|
fromStation: stationName,
|
||||||
|
schedule: buildSchedulePayload(o, { includeLineTrains: true })
|
||||||
|
}));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.get('/api/files/content', (req, res) => {
|
app.get('/api/files/content', (req, res) => {
|
||||||
const filePath = req.query.path;
|
const filePath = req.query.path;
|
||||||
if (!filePath) return res.status(400).json({ success: false, message: 'Path required' });
|
if (!filePath) return res.status(400).json({ success: false, message: 'Path required' });
|
||||||
@@ -4424,7 +4631,7 @@ app.post('/api/music/start', express.json(), (req, res) => {
|
|||||||
audioFormat: 'wav-stream-live',
|
audioFormat: 'wav-stream-live',
|
||||||
sourcePath: playbackAudio.sourcePath,
|
sourcePath: playbackAudio.sourcePath,
|
||||||
durationSec: playbackAudio.durationSec,
|
durationSec: playbackAudio.durationSec,
|
||||||
priority: 0,
|
priority: -1,
|
||||||
interruptMode: 'none',
|
interruptMode: 'none',
|
||||||
source: 'music',
|
source: 'music',
|
||||||
loop,
|
loop,
|
||||||
@@ -4441,7 +4648,7 @@ app.post('/api/music/start', express.json(), (req, res) => {
|
|||||||
buffer: playbackAudio.buffer,
|
buffer: playbackAudio.buffer,
|
||||||
streamUrl: playbackAudio.streamUrl,
|
streamUrl: playbackAudio.streamUrl,
|
||||||
durationSec: playbackAudio.durationSec,
|
durationSec: playbackAudio.durationSec,
|
||||||
priority: 0,
|
priority: -1,
|
||||||
interruptMode: 'none',
|
interruptMode: 'none',
|
||||||
source: 'music',
|
source: 'music',
|
||||||
loop,
|
loop,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
-- 1. Configuration & Constants
|
-- 1. Configuration & Constants
|
||||||
local APP_VERSION_TEXT = "v1.0.114"
|
local APP_VERSION_TEXT = "v1.0.115"
|
||||||
|
|
||||||
local CONFIG = {
|
local CONFIG = {
|
||||||
CLOUD_URL = "ws://stabadmin.fse-media.group",
|
CLOUD_URL = "ws://stabadmin.fse-media.group",
|
||||||
|
|||||||
Reference in New Issue
Block a user