// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.avadeaux.net/>
//
// Klipspringer is free software: you can redistribute it and/or modify it under the terms of the
// GNU General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// Klipspringer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with Klipspringer. If
// not, see <https://www.gnu.org/licenses/>.
import { createPlayerCommon } from './player_common.js';
import { log } from './log.js';
export const createVlcPlayer = (name, vlc, resource, extraArgs) => {
const args = ['-I', 'rc'].concat(extraArgs || [], resource);
let status = { };
let trackTime = 0, trackTimeTime = 0;
let errMsg = [];
const intf = createPlayerCommon(name, vlc, args);
intf.readyPattern = "Command Line Interface initialized";
intf.player_start_time = Date.now();
const outputStatus = (includeTracks) => intf.writeProcStdin('info\n')
.then(() => intf.writeProcStdin('status\n'))
.then(() => intf.writeProcStdin('get_time\n'))
.then(() => includeTracks ? intf.writeProcStdin('playlist\n') : null)
.catch(() => { });
intf.togglePauseTo = (target) => {
return intf.writeProcStdin("pause\n");
}
intf.processStdout = (bufferedString) => {
let off = 0;
const infoM = bufferedString.match(/\[ end of stream info \]/mi);
if (infoM) {
for (const line of bufferedString.slice(0, infoM.index).split(/[\n\r]+/)) {
const m = line.match(/^[^a-z_]*([^:]+?)\s*:\s*(.*?)\s*$/i);
if (m) {
const label = m[1].toLowerCase().replaceAll(/ /g, '_');
const value = ['track_number', 'bits_per_sample'].includes(label) ? +m[2] : m[2];
if (value !== '') { status[label] = value }
}
}
off += infoM.index + infoM[0].length;
}
const pauseM = bufferedString.slice(off).match(/\(\s*state\s*(playing)|(paused)\s*\)\s*/mi);
if (pauseM) {
status.paused = !!pauseM[2];
off += pauseM.index + pauseM[0].length;
} else if (!status.hasOwnProperty('paused')) {
status.paused = true; // stop the clock by default
}
const timeM = bufferedString.slice(off).match(/([0-9]+)$/m);
if (timeM && timeM[1] > 0 && status.sample_rate) {
trackTime = +timeM[1];
trackTimeTime = Date.now();
off += timeM.index + timeM[0].length;
}
const playlistM = bufferedString.match(/\[ end of playlist \]/mi);
if (playlistM) {
let tracks = [];
for (const line of bufferedString.slice(0, playlistM.index).split(/[\n\r]+/)) {
const m = line.match(/^[^0-9]*([0-9]+)\s+-\s+(.*)\s+\([0-9:]+\)(?:\s+\[.*\])?$/i);
if (m) {
if (tracks.length === 0) { intf.playlistOff = +m[1] }
tracks.push(m[2].replace(/_/g, ' '));
}
}
status.tracks = tracks;
off += playlistM.index + playlistM[0].length;
}
return off;
}
intf.noteStderr = (buf) => {
errMsg = errMsg.filter(e => Date.now() - e[1] < 20000);
for (const m of buf.toString().matchAll(/^.*error.*$/gmi)) {
errMsg.push([m[0], Date.now()]);
}
// something happened, renew status
outputStatus().catch(err => { log.error(name+" info command failed: "+err) });
}
// It's hopeless to sync everything perfectly (the time in particular) with
// the rc interface. So just do something simple: let get_status take a
// second or so, and it will update the client reasonably quickly.
intf.httpCallable.get_status = (qstring, query) => new Promise((resolve, reject) => {
setTimeout(() => {
const updateTracks = query && query.info_id !== undefined && (query.info_id !== intf.id || Date.now() - intf.player_start_time < 10000);
status = { new_info: updateTracks };
outputStatus(updateTracks).then(() => {
setTimeout(() => {
if (errMsg.length > 0) {
status.error = errMsg.reduce((acc, e) => { return acc + e[0] + "<br>"}, "");
}
if (trackTimeTime) {
const since = (Date.now() - trackTimeTime)/1000;
status.track_time = trackTime + (status.paused ? 0 : since);
}
if (query && query.info_id) { status.info_id = intf.id }
resolve(status);
}, 500);
}).catch(err => { reject(err) });
}, 1500);
});
intf.httpCallable.next = () => {
intf.writeProcStdin('next\n').catch(() => { });
}
intf.httpCallable.prev = (qstring, query) => {
// prev restarts current track, unless repeated within 1s
intf.writeProcStdin(!(query.rept < 1000) ? 'seek 0\n' : 'prev\n').catch(() => { });
}
intf.httpCallable.seekr = (qstring, query) => {
const t = query.time || 0;
intf.writeProcStdin((t < 0 ? 'seek ' : 'seek +')+t+'\n').catch(() => { });
}
intf.httpCallable.seeka = (qstring, query) => {
const tr = +(query.track || 1) + (intf.playlistOff || 0);
intf.writeProcStdin('goto +'+tr+'\n').catch(() => { });
if (query.time !== undefined) {
const ti = query.time;
intf.writeProcStdin((ti < 0 ? 'seek ' : 'seek +')+ti+'\n').catch(() => { });
}
}
return intf;
}
Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home