Skip to content

Commit e764c35

Browse files
committed
Release line when device is inactive or after being idle for a while (#150)
1 parent 783408e commit e764c35

4 files changed

Lines changed: 130 additions & 20 deletions

File tree

core/src/main/java/xyz/gianlu/librespot/FileConfiguration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ public int crossfadeDuration() {
285285
return config.get("player.crossfadeDuration");
286286
}
287287

288+
@Override
289+
public int releaseLineDelay() {
290+
return config.get("player.releaseLineDelay");
291+
}
292+
288293
@Override
289294
public @Nullable String deviceName() {
290295
return config.get("deviceName");

core/src/main/java/xyz/gianlu/librespot/player/Player.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.jetbrains.annotations.NotNull;
88
import org.jetbrains.annotations.Nullable;
99
import spotify.player.proto.transfer.TransferStateOuterClass;
10+
import xyz.gianlu.librespot.common.NameThreadFactory;
1011
import xyz.gianlu.librespot.common.Utils;
1112
import xyz.gianlu.librespot.connectstate.DeviceStateHandler;
1213
import xyz.gianlu.librespot.connectstate.DeviceStateHandler.PlayCommandHelper;
@@ -26,6 +27,10 @@
2627
import java.io.IOException;
2728
import java.util.List;
2829
import java.util.Map;
30+
import java.util.concurrent.Executors;
31+
import java.util.concurrent.ScheduledExecutorService;
32+
import java.util.concurrent.ScheduledFuture;
33+
import java.util.concurrent.TimeUnit;
2934

3035
import static spotify.player.proto.ContextTrackOuterClass.ContextTrack;
3136

@@ -34,13 +39,15 @@
3439
*/
3540
public class Player implements Closeable, DeviceStateHandler.Listener, PlayerRunner.Listener {
3641
private static final Logger LOGGER = Logger.getLogger(Player.class);
42+
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new NameThreadFactory((r) -> "release-line-scheduler-" + r.hashCode()));
3743
private final Session session;
3844
private final Configuration conf;
3945
private final PlayerRunner runner;
4046
private StateWrapper state;
4147
private TrackHandler trackHandler;
4248
private TrackHandler crossfadeHandler;
4349
private TrackHandler preloadTrackHandler;
50+
private ScheduledFuture releaseLineFuture = null;
4451

4552
public Player(@NotNull Player.Configuration conf, @NotNull Session session) {
4653
this.conf = conf;
@@ -206,7 +213,7 @@ public void volumeChanged() {
206213

207214
@Override
208215
public void notActive() {
209-
runner.stopMixer();
216+
if (runner.stopAndRelease()) LOGGER.debug("Released line due to inactivity.");
210217
}
211218

212219
@Override
@@ -428,6 +435,11 @@ private void loadTrack(boolean play, @NotNull PushToMixerReason reason) {
428435
runner.playMixer();
429436
}
430437
}
438+
439+
if (releaseLineFuture != null) {
440+
releaseLineFuture.cancel(true);
441+
releaseLineFuture = null;
442+
}
431443
}
432444

433445
private void handleResume() {
@@ -442,6 +454,11 @@ private void handleResume() {
442454
}
443455

444456
state.updated();
457+
458+
if (releaseLineFuture != null) {
459+
releaseLineFuture.cancel(true);
460+
releaseLineFuture = null;
461+
}
445462
}
446463
}
447464

@@ -456,6 +473,13 @@ private void handlePause() {
456473
}
457474

458475
state.updated();
476+
477+
if (releaseLineFuture != null) releaseLineFuture.cancel(true);
478+
releaseLineFuture = scheduler.schedule(() -> {
479+
if (!state.isPaused()) return;
480+
481+
if (runner.pauseAndRelease()) LOGGER.debug("Released line after a period of inactivity.");
482+
}, conf.releaseLineDelay(), TimeUnit.SECONDS);
459483
}
460484
}
461485

@@ -631,5 +655,7 @@ public interface Configuration {
631655
boolean autoplayEnabled();
632656

633657
int crossfadeDuration();
658+
659+
int releaseLineDelay();
634660
}
635661
}

core/src/main/java/xyz/gianlu/librespot/player/PlayerRunner.java

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.spotify.metadata.proto.Metadata;
44
import javazoom.jl.decoder.BitstreamException;
55
import org.apache.log4j.Logger;
6-
import org.jetbrains.annotations.Contract;
76
import org.jetbrains.annotations.NotNull;
87
import org.jetbrains.annotations.Nullable;
98
import xyz.gianlu.librespot.common.NameThreadFactory;
@@ -65,10 +64,7 @@ public class PlayerRunner implements Runnable, Closeable {
6564
switch (conf.output()) {
6665
case MIXER:
6766
try {
68-
SourceDataLine line = LineHelper.getLineFor(conf, Output.OUTPUT_FORMAT);
69-
line.open(Output.OUTPUT_FORMAT);
70-
71-
output = new Output(mixing, line, null, null);
67+
output = new Output(Output.Type.MIXER, mixing, conf, null, null);
7268
} catch (LineUnavailableException ex) {
7369
throw new IllegalStateException("Failed opening line!", ex);
7470
}
@@ -78,10 +74,18 @@ public class PlayerRunner implements Runnable, Closeable {
7874
if (pipe == null || !pipe.exists() || !pipe.canWrite())
7975
throw new IllegalArgumentException("Invalid pipe file: " + pipe);
8076

81-
output = new Output(mixing, null, pipe, null);
77+
try {
78+
output = new Output(Output.Type.PIPE, mixing, conf, pipe, null);
79+
} catch (LineUnavailableException ignored) {
80+
throw new IllegalStateException(); // Cannot be thrown
81+
}
8282
break;
8383
case STDOUT:
84-
output = new Output(mixing, null, null, System.out);
84+
try {
85+
output = new Output(Output.Type.STREAM, mixing, conf, null, System.out);
86+
} catch (LineUnavailableException ignored) {
87+
throw new IllegalStateException(); // Cannot be thrown
88+
}
8589
break;
8690
default:
8791
throw new IllegalArgumentException("Unknown output: " + conf.output());
@@ -93,6 +97,40 @@ public class PlayerRunner implements Runnable, Closeable {
9397
new Thread(looper = new Looper(), "player-runner-looper-" + looper.hashCode()).start();
9498
}
9599

100+
/**
101+
* Pauses the mixer and then releases the {@link javax.sound.sampled.Line} if acquired.
102+
*
103+
* @return Whether the line was released.
104+
*/
105+
public boolean pauseAndRelease() {
106+
pauseMixer();
107+
while (!paused) {
108+
synchronized (pauseLock) {
109+
try {
110+
pauseLock.wait(100);
111+
} catch (InterruptedException ignored) {
112+
}
113+
}
114+
}
115+
116+
return output.releaseLine();
117+
}
118+
119+
/**
120+
* Stops the mixer and then releases the {@link javax.sound.sampled.Line} if acquired.
121+
*/
122+
public boolean stopAndRelease() {
123+
stopMixer();
124+
synchronized (pauseLock) {
125+
try {
126+
pauseLock.wait();
127+
} catch (InterruptedException ignored) {
128+
}
129+
}
130+
131+
return output.releaseLine();
132+
}
133+
96134
private void sendCommand(@NotNull Command command, int id, Object... args) {
97135
commands.add(new CommandBundle(command, id, args));
98136
}
@@ -128,7 +166,7 @@ public void run() {
128166
int r = count % mixing.getFrameSize();
129167
if (r != 0) count += mixing.read(buffer, count, mixing.getFrameSize() - r);
130168
output.write(buffer, 0, count);
131-
} catch (IOException ex) {
169+
} catch (IOException | LineUnavailableException ex) {
132170
if (closed) break;
133171

134172
paused = true;
@@ -222,25 +260,46 @@ public interface Listener {
222260

223261
private static class Output implements Closeable {
224262
private static final AudioFormat OUTPUT_FORMAT = new AudioFormat(44100, 16, 2, true, false);
225-
private final SourceDataLine line;
226263
private final File pipe;
227264
private final MixingLine mixing;
265+
private final Player.Configuration conf;
266+
private final Type type;
267+
private SourceDataLine line;
228268
private OutputStream out;
229269

230-
@Contract("_, null, null, null -> fail")
231-
Output(@NotNull MixingLine mixing, @Nullable SourceDataLine line, @Nullable File pipe, @Nullable OutputStream out) {
232-
if (line == null && pipe == null && out == null) throw new IllegalArgumentException();
233-
270+
Output(@NotNull Type type, @NotNull MixingLine mixing, @NotNull Player.Configuration conf, @Nullable File pipe, @Nullable OutputStream out) throws LineUnavailableException {
271+
this.conf = conf;
234272
this.mixing = mixing;
235-
this.line = line;
273+
this.type = type;
236274
this.pipe = pipe;
237275
this.out = out;
276+
277+
switch (type) {
278+
case MIXER:
279+
acquireLine();
280+
break;
281+
case PIPE:
282+
if (pipe == null) throw new IllegalArgumentException();
283+
break;
284+
case STREAM:
285+
if (out == null) throw new IllegalArgumentException();
286+
break;
287+
default:
288+
throw new IllegalArgumentException(String.valueOf(type));
289+
}
238290
}
239291

240292
private static float calcLogarithmic(int val) {
241293
return (float) (Math.log10((double) val / VOLUME_MAX) * 20f);
242294
}
243295

296+
private void acquireLine() throws LineUnavailableException {
297+
if (line != null) return;
298+
299+
line = LineHelper.getLineFor(conf, Output.OUTPUT_FORMAT);
300+
line.open(Output.OUTPUT_FORMAT);
301+
}
302+
244303
void stop() {
245304
if (line != null) line.stop();
246305
}
@@ -249,13 +308,12 @@ void start() {
249308
if (line != null) line.start();
250309
}
251310

252-
void write(byte[] buffer, int off, int len) throws IOException {
253-
if (line != null) {
311+
void write(byte[] buffer, int off, int len) throws IOException, LineUnavailableException {
312+
if (type == Type.MIXER) {
313+
acquireLine();
254314
line.write(buffer, off, len);
255-
} else {
315+
} else if (type == Type.PIPE) {
256316
if (out == null) {
257-
if (pipe == null) throw new IllegalStateException();
258-
259317
if (!pipe.exists()) {
260318
try {
261319
Process p = new ProcessBuilder().command("mkfifo " + pipe.getAbsolutePath())
@@ -274,6 +332,10 @@ void write(byte[] buffer, int off, int len) throws IOException {
274332
}
275333

276334
out.write(buffer, off, len);
335+
} else if (type == Type.STREAM) {
336+
out.write(buffer, off, len);
337+
} else {
338+
throw new IllegalStateException();
277339
}
278340
}
279341

@@ -306,6 +368,18 @@ void setVolume(int volume) {
306368
// Cannot set volume through line
307369
mixing.setGlobalGain(((float) volume) / VOLUME_MAX);
308370
}
371+
372+
boolean releaseLine() {
373+
if (line == null) return false;
374+
375+
line.close();
376+
line = null;
377+
return true;
378+
}
379+
380+
enum Type {
381+
MIXER, PIPE, STREAM
382+
}
309383
}
310384

311385
private static class CommandBundle {
@@ -404,6 +478,10 @@ public void run() {
404478
firstHandler = null;
405479
secondHandler = null;
406480
loadedTracks.clear();
481+
482+
synchronized (pauseLock) {
483+
pauseLock.notifyAll();
484+
}
407485
break;
408486
case TerminateMixer:
409487
return;

core/src/main/resources/default.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ logAvailableMixers = true # Log available mixers
3535
mixerSearchKeywords = "" # Mixer/backend search keywords (semicolon separated)
3636
crossfadeDuration = 0 # Crossfade overlap time (in milliseconds)
3737
output = "MIXER" # Audio output device (MIXER, PIPE, STDOUT)
38+
releaseLineDelay = 20 # Release mixer line after set delay (in seconds)
3839
pipe = "" # Output raw (signed) PCM to this file (`player.output` must be PIPE)

0 commit comments

Comments
 (0)