// 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/>.
package net.avadeaux.klipspringer;
import java.io.*;
import net.avadeaux.klipspringer.codec.PcmFormat;
import static net.avadeaux.klipspringer.PlayState.Status.*;
/**
* Maintains the playback state, getting updated from three sides: feeder (what is currently being
* decoded), consumer (what is currently being sent to the destination), and stream (what is
* currently being played by a web client). Exposes two user interfaces: one for server-side players
* (which display consumer status) and one for web clients.
*/
public class PlayState implements PlayMonitor {
public static class ResumeException extends IOException {
public final Track track;
public final double time;
private ResumeException(Track track, double time) {
this.track = track;
this.time = time;
}
}
public static class Info implements PlayMonitor.Info {
private Track track;
private boolean monomix, paused;
private double time;
private long timestamp = 0;
public String trackName() { return track == null ? null : track.path(); }
public int trackIndex() { return track == null ? -1 : track.index; }
public PcmFormat format() { return track == null ? null : track.format(); }
public boolean monomix() { return monomix; }
public boolean paused() { return paused; }
public double time() { return time; }
public String quality() { return track == null ? null : track.quality(); }
}
enum Status { STOPPED, STREAM_SWITCHING, PLAYING, PAUSED }
private final Track.List tracks;
// When state is STOPPED, track and index is set to -1. When consumer initiates going to another
// state, both consumer and stream tracks are set to the first track that is played. Times
// are relative to the start of the track list.
private Status consStatus = STOPPED;
private long changeNo = 1; // timestamp of consumer status change
private double streamStartTime = Double.NaN; // set leaving STOPPED
private Track streamTr = null; // first track in stream, set leaving STOPPED
private Track consTr = null; // set by consumer
private double consTime = Double.NaN; // set by consumer
private double consTimeSetTime = Double.NaN; // time of most recent update from consumer
private boolean monomix = false; // set to nofify of monomix of current audio
public PlayState(Track.List tracks) { this.tracks = tracks; }
private double adjTime() {
return consTime + (consStatus == PAUSED || Double.isNaN(consTimeSetTime)
? 0
: System.currentTimeMillis() / 1000.0 - consTimeSetTime);
}
/** Invoked by consumer thread to inform the state service that it has potentially changed its
* status, or shifted track index or track time.
*/
public synchronized void consumerStatus(Status newStatus, Track newTr, double newTime) {
while (newTime < 0) { // compensate for play time behind consume time
Track tr = null;
try { tr = tracks.ofIndex(newTr.index-1); } catch (Throwable ex) { }
if (tr == null) { break; }
newTime = tr.playTimeSecs() + newTime;
newTr = tr;
}
double currConsTime = adjTime();
// Check for significant status change.
if ((newStatus == STOPPED) != (consStatus == STOPPED) // in or out of stopped state
|| (newStatus == PAUSED) != (consStatus == PAUSED) // in or out of paused state
|| newTr != consTr // track change
|| Math.abs(newTime - currConsTime) > 0.2 // time shifted by more than 0.2 seconds
|| newTime == 0 && currConsTime > 0) { // track time reset
changeNo++;
notifyAll();
}
if (newStatus != STOPPED && newStatus != STREAM_SWITCHING) {
consTr = newTr;
consTime = newTime;
consTimeSetTime = System.currentTimeMillis() / 1000.0;
if (consStatus == STOPPED || consStatus == STREAM_SWITCHING) {
streamTr = newTr;
streamStartTime = newTime + newTr.startTimeSecs();
}
}
consStatus = newStatus;
}
public synchronized void monomixing(boolean monomix) { this.monomix = monomix; }
public synchronized Query.Onoff monomixing() { return monomix ? Query.Onoff.ON : Query.Onoff.OFF; }
public synchronized Track playingTrack() { return consTr; }
public synchronized int playingTrackIndex() { return consTr == null ? -1 : consTr.index; }
public static String channelsString(int channels) {
switch (channels) {
case 1:
return "\"Mono\"";
case 2:
return "\"Stereo\"";
default:
return "\""+channels+"-channel\"";
}
}
public synchronized double playingTime() {
return consTr == null ? Double.NaN : adjTime() + consTr.startTimeSecs();
}
public synchronized ResumeException resumeException() {
return consTr == null ? null : new ResumeException(consTr, adjTime());
}
private void appendTrackInfo(Track tr, Writer w) throws IOException {
if (tr != null) {
w.append("\"track_number\":").append(tr.trackNo()+",");
w.append("\"track_ix\":").append(tr.index+",");
w.append("\"first_track\":").append(tracks.firstTrackNo+",");
PcmFormat fmt = tr.format();
w.append("\"sample_rate\":\"").append(fmt.rate()+" Hz\",");
w.append("\"bits_per_sample\":").append(fmt.significantBips()+",");
w.append("\"channels\":").append(channelsString(fmt.channels())).append(",");
if (monomix) { w.append("\"monomix\":true,"); }
String q = tr.quality();
if (q != null && q.length() > 0) { w.append("\"quality\":\""+q+"\","); }
if (tr.index < tracks.length-1) { w.append("\"has_following_track\":true,"); }
}
}
private void waitForUpdate(long alreadySeen, long timeoutMillis) {
long stopWaitingAtMillis = System.currentTimeMillis() + (timeoutMillis > 0 ? timeoutMillis : 10000);
while (true) {
if (changeNo > alreadySeen) { break; }
long waitTime = stopWaitingAtMillis - System.currentTimeMillis();
if (waitTime <= 0) { break; }
try { wait(waitTime); } catch (InterruptedException e) { }
}
}
/** Potentially waits for an update of newer timestamp or for timeout, then writes status
* (including track name etc.) in JSON format to the given writer. The lowest timestamp is 1,
* so if the value given t this function is 0, it returns without waiting. Timeout defaults to
* ten seconds if a negative value is given.
*/
public synchronized void serverStatus(long alreadySeen, long timeoutMillis, Writer w) throws IOException {
waitForUpdate(alreadySeen, timeoutMillis);
if (consStatus != STOPPED) {
appendTrackInfo(consTr, w);
double trackTime = Math.max(0, adjTime());
if (!Double.isNaN(trackTime)) { w.append("\"track_time\":").append(trackTime+","); }
}
w.append("\"paused\":").append((consStatus == PAUSED)+",");
w.append("\"status_timestamp\":"+changeNo);
}
public Info getInfoObject() { return new Info(); }
/** Potentially waits for an update newer than the one already set in the given info record,
* then sets the record fields.
*/
public synchronized void getStatus(PlayMonitor.Info istatus, long timeoutMillis) {
Info info = (Info) istatus;
waitForUpdate(info.timestamp, timeoutMillis);
if (consStatus != STOPPED) {
info.track = consTr;
info.time = Math.max(0, adjTime());
} else {
info.track = null;
info.time = Double.NaN;
}
info.monomix = monomix;
info.paused = consStatus == PAUSED;
info.timestamp = changeNo;
}
/** Gets current status when playing stream. */
public synchronized void streamStatus(double time, Writer w) throws IOException {
if (consStatus == STOPPED) {
w.append("\"err\": \"currently not streaming\"");
return;
}
while (true) {
Track nextTr = tracks.ofIndex(streamTr.index+1);
if (nextTr == null || streamStartTime+time < nextTr.startTimeSecs()) { break; }
streamTr = nextTr;
}
appendTrackInfo(streamTr, w);
// Offset of current track in playing stream (can be negative in the start track).
w.append("\"current_track_start\":").append(Double.toString(streamTr.startTimeSecs() - streamStartTime));
// Length in seconds of the playing track.
w.append(",\"current_track_length\":").append(Double.toString(streamTr.playTimeSecs()));
}
public synchronized boolean paused() { return consStatus == PAUSED; }
}
Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home