Skip to content

Commit 8aece31

Browse files
committed
Trigger loading state when playback halts due to network (closes #76)
1 parent 50a1813 commit 8aece31

File tree

8 files changed

+112
-45
lines changed

8 files changed

+112
-45
lines changed

core/src/main/java/xyz/gianlu/librespot/cdn/CdnManager.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,14 @@ private InputStream getHead(@NotNull ByteString fileId) throws IOException {
6565
}
6666

6767
@NotNull
68-
public Streamer streamEpisode(@NotNull Metadata.Episode episode, @NotNull HttpUrl externalUrl) throws IOException {
69-
return new Streamer(new StreamId(episode), SuperAudioFormat.MP3, externalUrl, session.cache(), new NoopAudioDecrypt());
68+
public Streamer streamEpisode(@NotNull Metadata.Episode episode, @NotNull HttpUrl externalUrl, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException {
69+
return new Streamer(new StreamId(episode), SuperAudioFormat.MP3, externalUrl, session.cache(), new NoopAudioDecrypt(), haltListener);
7070
}
7171

7272
@NotNull
73-
public Streamer streamTrack(@NotNull Metadata.AudioFile file, @NotNull byte[] key) throws IOException, MercuryClient.MercuryException, CdnException {
73+
public Streamer streamTrack(@NotNull Metadata.AudioFile file, @NotNull byte[] key, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException, MercuryClient.MercuryException, CdnException {
7474
return new Streamer(new StreamId(file), SuperAudioFormat.get(file.getFormat()),
75-
getAudioUrl(file.getFileId()), session.cache(), new AesAudioDecrypt(key));
75+
getAudioUrl(file.getFileId()), session.cache(), new AesAudioDecrypt(key), haltListener);
7676
}
7777

7878
@NotNull
@@ -129,7 +129,8 @@ public class Streamer implements GeneralAudioStream, GeneralWritableStream {
129129
private final InternalStream internalStream;
130130
private final CacheManager.Handler cacheHandler;
131131

132-
private Streamer(@NotNull StreamId streamId, @NotNull SuperAudioFormat format, @NotNull HttpUrl cdnUrl, @Nullable CacheManager cache, @Nullable AudioDecrypt audioDecrypt) throws IOException {
132+
private Streamer(@NotNull StreamId streamId, @NotNull SuperAudioFormat format, @NotNull HttpUrl cdnUrl, @Nullable CacheManager cache,
133+
@Nullable AudioDecrypt audioDecrypt, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException {
133134
this.streamId = streamId;
134135
this.format = format;
135136
this.audioDecrypt = audioDecrypt;
@@ -169,7 +170,7 @@ private Streamer(@NotNull StreamId streamId, @NotNull SuperAudioFormat format, @
169170
buffer = new byte[chunks][CHUNK_SIZE];
170171
buffer[chunks - 1] = new byte[size % CHUNK_SIZE];
171172

172-
this.internalStream = new InternalStream();
173+
this.internalStream = new InternalStream(haltListener);
173174
writeChunk(firstChunk, 0, false);
174175
}
175176

@@ -253,6 +254,10 @@ public synchronized InternalResponse request(int rangeStart, int rangeEnd) throw
253254

254255
private class InternalStream extends AbsChunckedInputStream {
255256

257+
protected InternalStream(@Nullable HaltListener haltListener) {
258+
super(haltListener);
259+
}
260+
256261
@Override
257262
protected byte[][] buffer() {
258263
return buffer;

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package xyz.gianlu.librespot.player;
22

33
import org.jetbrains.annotations.NotNull;
4+
import org.jetbrains.annotations.Nullable;
45

56
import java.io.IOException;
67
import java.io.InputStream;
@@ -14,12 +15,14 @@
1415
public abstract class AbsChunckedInputStream extends InputStream {
1516
private static final int PRELOAD_AHEAD = 3;
1617
private final AtomicInteger waitForChunk = new AtomicInteger(-1);
18+
private final HaltListener haltListener;
1719
private ChunkException chunkException = null;
1820
private int pos = 0;
1921
private int mark = 0;
2022
private volatile boolean closed = false;
2123

22-
protected AbsChunckedInputStream() {
24+
protected AbsChunckedInputStream(@Nullable HaltListener haltListener) {
25+
this.haltListener = haltListener;
2326
}
2427

2528
public final boolean isClosed() {
@@ -113,7 +116,13 @@ private void checkAvailability(int chunk, boolean wait) throws IOException {
113116
}
114117
}
115118

116-
if (wait) waitFor(chunk);
119+
if (availableChunks()[chunk]) return;
120+
121+
if (wait) {
122+
if (haltListener != null) haltListener.streamReadHalted(chunk, System.currentTimeMillis());
123+
waitFor(chunk);
124+
if (haltListener != null) haltListener.streamReadResumed(chunk, System.currentTimeMillis());
125+
}
117126
}
118127

119128
@Override
@@ -183,6 +192,12 @@ public final void notifyChunkError(int index, @NotNull ChunkException ex) {
183192
}
184193
}
185194

195+
public interface HaltListener {
196+
void streamReadHalted(int chunk, long time);
197+
198+
void streamReadResumed(int chunk, long time);
199+
}
200+
186201
public static class ChunkException extends IOException {
187202
public ChunkException(@NotNull Throwable cause) {
188203
super(cause);

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,31 @@ public void playbackError(@NotNull TrackHandler handler, @NotNull Exception ex)
391391
}
392392
}
393393

394+
@Override
395+
public void playbackHalted(@NotNull TrackHandler handler, int chunk) {
396+
if (handler == trackHandler) {
397+
LOGGER.debug(String.format("Playback halted on retrieving chunk %d.", chunk));
398+
399+
if (conf.enableLoadingState()) {
400+
state.setStatus(Spirc.PlayStatus.kPlayStatusLoading);
401+
stateUpdated();
402+
}
403+
}
404+
}
405+
406+
@Override
407+
public void playbackResumedFromHalt(@NotNull TrackHandler handler, int chunk, long diff) {
408+
if (handler == trackHandler) {
409+
LOGGER.debug(String.format("Playback resumed, chunk %d retrieved, took %dms.", chunk, diff));
410+
411+
long now = TimeProvider.currentTimeMillis();
412+
state.setPositionMs(state.getPositionMs() + (int) (now - state.getPositionMeasuredAt() - diff));
413+
state.setPositionMeasuredAt(now);
414+
state.setStatus(Spirc.PlayStatus.kPlayStatusPlay);
415+
stateUpdated();
416+
}
417+
}
418+
394419
private void handleLoad(@NotNull Remote3Frame frame) {
395420
if (!spirc.deviceState().getIsActive()) {
396421
spirc.deviceState()
@@ -453,9 +478,7 @@ private void handlePause() {
453478
state.setStatus(Spirc.PlayStatus.kPlayStatusPause);
454479

455480
long now = TimeProvider.currentTimeMillis();
456-
int pos = state.getPositionMs();
457-
int diff = (int) (now - state.getPositionMeasuredAt());
458-
state.setPositionMs(pos + diff);
481+
state.setPositionMs(state.getPositionMs() + (int) (now - state.getPositionMeasuredAt()));
459482
state.setPositionMeasuredAt(now);
460483
stateUpdated();
461484
}

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
/**
2727
* @author Gianlu
2828
*/
29-
public class TrackHandler implements PlayerRunner.Listener, Closeable {
29+
public class TrackHandler implements PlayerRunner.Listener, Closeable, AbsChunckedInputStream.HaltListener {
3030
private static final Logger LOGGER = Logger.getLogger(TrackHandler.class);
3131
private final BlockingQueue<CommandBundle> commands = new LinkedBlockingQueue<>();
3232
private final Session session;
@@ -38,6 +38,7 @@ public class TrackHandler implements PlayerRunner.Listener, Closeable {
3838
private Metadata.Episode episode;
3939
private PlayerRunner playerRunner;
4040
private volatile boolean stopped = false;
41+
private long haltedAt = -1;
4142

4243
TrackHandler(@NotNull Session session, @NotNull LinesHolder lines, @NotNull Player.Configuration conf, @NotNull Listener listener) {
4344
this.session = session;
@@ -61,11 +62,11 @@ private void load(@NotNull PlayableId id, boolean play, int pos) throws IOExcept
6162

6263
BaseFeeder.LoadedStream stream;
6364
try {
64-
stream = feeder.load(id, new VorbisOnlyAudioQuality(conf.preferredQuality()));
65+
stream = feeder.load(id, new VorbisOnlyAudioQuality(conf.preferredQuality()), this);
6566
} catch (CdnFeeder.CanNotAvailable ex) {
6667
LOGGER.warn(String.format("Cdn not available for %s, using storage", Utils.bytesToHex(id.getGid())));
6768
feeder = new StorageFeeder(session, id);
68-
stream = feeder.load(id, new VorbisOnlyAudioQuality(conf.preferredQuality()));
69+
stream = feeder.load(id, new VorbisOnlyAudioQuality(conf.preferredQuality()), this);
6970
}
7071

7172
track = stream.track;
@@ -175,6 +176,22 @@ public Metadata.Episode episode() {
175176
return episode;
176177
}
177178

179+
@Override
180+
public void streamReadHalted(int chunk, long time) {
181+
haltedAt = time;
182+
listener.playbackHalted(this, chunk);
183+
}
184+
185+
@Override
186+
public void streamReadResumed(int chunk, long time) {
187+
if (haltedAt != -1) {
188+
long diff = time - haltedAt;
189+
haltedAt = -1;
190+
191+
listener.playbackResumedFromHalt(this, chunk, diff);
192+
}
193+
}
194+
178195
public enum Command {
179196
Load, Play, Pause,
180197
Stop, Seek, Terminate
@@ -192,6 +209,10 @@ public interface Listener {
192209
void preloadNextTrack(@NotNull TrackHandler handler);
193210

194211
void playbackError(@NotNull TrackHandler handler, @NotNull Exception ex);
212+
213+
void playbackHalted(@NotNull TrackHandler handler, int chunk);
214+
215+
void playbackResumedFromHalt(@NotNull TrackHandler handler, int chunk, long diff);
195216
}
196217

197218
private class Looper implements Runnable {

core/src/main/java/xyz/gianlu/librespot/player/feeders/BaseFeeder.java

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@
1313
import xyz.gianlu.librespot.mercury.model.EpisodeId;
1414
import xyz.gianlu.librespot.mercury.model.PlayableId;
1515
import xyz.gianlu.librespot.mercury.model.TrackId;
16-
import xyz.gianlu.librespot.player.ContentRestrictedException;
17-
import xyz.gianlu.librespot.player.GeneralAudioStream;
18-
import xyz.gianlu.librespot.player.NormalizationData;
19-
import xyz.gianlu.librespot.player.Player;
16+
import xyz.gianlu.librespot.player.*;
2017
import xyz.gianlu.librespot.player.codecs.AudioQuality;
2118
import xyz.gianlu.librespot.player.codecs.AudioQualityPreference;
2219
import xyz.gianlu.librespot.player.codecs.SuperAudioFormat;
@@ -66,13 +63,13 @@ private static Metadata.Track pickAlternativeIfNecessary(@NotNull Metadata.Track
6663
}
6764

6865
@NotNull
69-
public final LoadedStream load(@NotNull PlayableId id, @NotNull AudioQualityPreference audioQualityPreference) throws CdnManager.CdnException, ContentRestrictedException, MercuryClient.MercuryException, IOException {
70-
if (id instanceof TrackId) return loadTrack((TrackId) id, audioQualityPreference);
71-
else if (id instanceof EpisodeId) return loadEpisode((EpisodeId) id, audioQualityPreference);
66+
public final LoadedStream load(@NotNull PlayableId id, @NotNull AudioQualityPreference audioQualityPreference, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws CdnManager.CdnException, ContentRestrictedException, MercuryClient.MercuryException, IOException {
67+
if (id instanceof TrackId) return loadTrack((TrackId) id, audioQualityPreference, haltListener);
68+
else if (id instanceof EpisodeId) return loadEpisode((EpisodeId) id, haltListener);
7269
else throw new IllegalArgumentException("Unknown PlayableId: " + id);
7370
}
7471

75-
public final @NotNull LoadedStream loadTrack(@NotNull TrackId id, @NotNull AudioQualityPreference audioQualityPreference) throws IOException, MercuryClient.MercuryException, ContentRestrictedException, CdnManager.CdnException {
72+
public final @NotNull LoadedStream loadTrack(@NotNull TrackId id, @NotNull AudioQualityPreference audioQualityPreference, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException, MercuryClient.MercuryException, ContentRestrictedException, CdnManager.CdnException {
7673
Metadata.Track original = session.mercury().sendSync(MercuryRequests.getTrack(id)).proto();
7774
Metadata.Track track = pickAlternativeIfNecessary(original);
7875
if (track == null) {
@@ -83,29 +80,29 @@ public final LoadedStream load(@NotNull PlayableId id, @NotNull AudioQualityPref
8380
throw new FeederException();
8481
}
8582

86-
return loadTrack(track, audioQualityPreference);
83+
return loadTrack(track, audioQualityPreference, haltListener);
8784
}
8885

8986
@NotNull
90-
public final LoadedStream loadTrack(@NotNull Metadata.Track track, @NotNull AudioQualityPreference audioQualityPreference) throws IOException, MercuryClient.MercuryException, CdnManager.CdnException {
87+
public final LoadedStream loadTrack(@NotNull Metadata.Track track, @NotNull AudioQualityPreference audioQualityPreference, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException, CdnManager.CdnException, MercuryClient.MercuryException {
9188
Metadata.AudioFile file = audioQualityPreference.getFile(track);
9289
if (file == null) {
9390
LOGGER.fatal(String.format("Couldn't find any suitable audio file, available: %s", AudioQuality.listFormats(track)));
9491
throw new FeederException();
9592
}
9693

97-
return loadTrack(track, file);
94+
return loadTrack(track, file, haltListener);
9895
}
9996

10097
@NotNull
101-
public final LoadedStream loadTrack(@NotNull Spirc.TrackRef ref, @NotNull AudioQualityPreference audioQualityPreference) throws IOException, MercuryClient.MercuryException, CdnManager.CdnException, ContentRestrictedException {
102-
return loadTrack(TrackId.fromTrackRef(ref), audioQualityPreference);
98+
public final LoadedStream loadTrack(@NotNull Spirc.TrackRef ref, @NotNull AudioQualityPreference audioQualityPreference, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException, MercuryClient.MercuryException, CdnManager.CdnException, ContentRestrictedException {
99+
return loadTrack(TrackId.fromTrackRef(ref), audioQualityPreference, haltListener);
103100
}
104101

105102
@NotNull
106-
public abstract LoadedStream loadTrack(@NotNull Metadata.Track track, @NotNull Metadata.AudioFile file) throws IOException, CdnManager.CdnException, MercuryClient.MercuryException;
103+
public abstract LoadedStream loadTrack(@NotNull Metadata.Track track, @NotNull Metadata.AudioFile file, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException, CdnManager.CdnException, MercuryClient.MercuryException;
107104

108-
public final @NotNull LoadedStream loadEpisode(@NotNull EpisodeId id, @NotNull AudioQualityPreference audioQualityPreference) throws IOException, MercuryClient.MercuryException, CdnManager.CdnException {
105+
public final @NotNull LoadedStream loadEpisode(@NotNull EpisodeId id, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException, MercuryClient.MercuryException, CdnManager.CdnException {
109106
Metadata.Episode episode = session.mercury().sendSync(MercuryRequests.getEpisode(id)).proto();
110107

111108
Metadata.AudioFile file = null;
@@ -124,11 +121,11 @@ public final LoadedStream loadTrack(@NotNull Spirc.TrackRef ref, @NotNull AudioQ
124121
throw new FeederException();
125122
}
126123

127-
return loadEpisode(episode, file);
124+
return loadEpisode(episode, file, haltListener);
128125
}
129126

130127
@NotNull
131-
public abstract LoadedStream loadEpisode(@NotNull Metadata.Episode episode, @NotNull Metadata.AudioFile file) throws IOException, CdnManager.CdnException, MercuryClient.MercuryException;
128+
public abstract LoadedStream loadEpisode(@NotNull Metadata.Episode episode, @NotNull Metadata.AudioFile file, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException, CdnManager.CdnException, MercuryClient.MercuryException;
132129

133130
public static class LoadedStream {
134131
public final Metadata.Episode episode;

core/src/main/java/xyz/gianlu/librespot/player/feeders/CdnFeeder.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import okhttp3.Response;
55
import org.apache.log4j.Logger;
66
import org.jetbrains.annotations.NotNull;
7+
import org.jetbrains.annotations.Nullable;
78
import xyz.gianlu.librespot.cdn.CdnManager;
89
import xyz.gianlu.librespot.common.proto.Metadata;
910
import xyz.gianlu.librespot.core.Session;
1011
import xyz.gianlu.librespot.mercury.MercuryClient;
1112
import xyz.gianlu.librespot.mercury.model.PlayableId;
13+
import xyz.gianlu.librespot.player.AbsChunckedInputStream;
1214
import xyz.gianlu.librespot.player.NormalizationData;
1315

1416
import java.io.IOException;
@@ -25,9 +27,9 @@ public CdnFeeder(@NotNull Session session, @NotNull PlayableId id) {
2527
}
2628

2729
@Override
28-
public @NotNull LoadedStream loadTrack(Metadata.@NotNull Track track, Metadata.@NotNull AudioFile file) throws IOException, CdnManager.CdnException, MercuryClient.MercuryException {
30+
public @NotNull LoadedStream loadTrack(Metadata.@NotNull Track track, Metadata.@NotNull AudioFile file, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException, CdnManager.CdnException, MercuryClient.MercuryException {
2931
byte[] key = session.audioKey().getAudioKey(track.getGid(), file.getFileId());
30-
CdnManager.Streamer streamer = session.cdn().streamTrack(file, key);
32+
CdnManager.Streamer streamer = session.cdn().streamTrack(file, key, haltListener);
3133

3234
InputStream in = streamer.stream();
3335
NormalizationData normalizationData = NormalizationData.read(in);
@@ -38,7 +40,7 @@ public CdnFeeder(@NotNull Session session, @NotNull PlayableId id) {
3840
}
3941

4042
@Override
41-
public @NotNull LoadedStream loadEpisode(Metadata.@NotNull Episode episode, Metadata.@NotNull AudioFile file) throws IOException {
43+
public @NotNull LoadedStream loadEpisode(Metadata.@NotNull Episode episode, Metadata.@NotNull AudioFile file, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException {
4244
if (!episode.hasExternalUrl())
4345
throw new CanNotAvailable("Missing external_url!");
4446

@@ -48,7 +50,7 @@ public CdnFeeder(@NotNull Session session, @NotNull PlayableId id) {
4850
if (resp.code() != 200)
4951
LOGGER.warn("Couldn't resolve redirect!");
5052

51-
CdnManager.Streamer streamer = session.cdn().streamEpisode(episode, resp.request().url());
53+
CdnManager.Streamer streamer = session.cdn().streamEpisode(episode, resp.request().url(), haltListener);
5254
return new LoadedStream(episode, streamer, null);
5355
}
5456

core/src/main/java/xyz/gianlu/librespot/player/feeders/StorageFeeder.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package xyz.gianlu.librespot.player.feeders;
22

3-
import org.apache.log4j.Logger;
43
import org.jetbrains.annotations.NotNull;
4+
import org.jetbrains.annotations.Nullable;
55
import xyz.gianlu.librespot.common.proto.Metadata;
66
import xyz.gianlu.librespot.core.Session;
77
import xyz.gianlu.librespot.crypto.Packet;
88
import xyz.gianlu.librespot.mercury.model.PlayableId;
9+
import xyz.gianlu.librespot.player.AbsChunckedInputStream;
910
import xyz.gianlu.librespot.player.NormalizationData;
1011
import xyz.gianlu.librespot.player.feeders.storage.AudioFileStreaming;
1112

@@ -16,19 +17,17 @@
1617
* @author Gianlu
1718
*/
1819
public class StorageFeeder extends BaseFeeder {
19-
private static final Logger LOGGER = Logger.getLogger(StorageFeeder.class);
20-
2120
public StorageFeeder(@NotNull Session session, @NotNull PlayableId id) {
2221
super(session, id);
2322
}
2423

2524
@Override
26-
public @NotNull LoadedStream loadTrack(@NotNull Metadata.Track track, @NotNull Metadata.AudioFile file) throws IOException {
25+
public @NotNull LoadedStream loadTrack(@NotNull Metadata.Track track, @NotNull Metadata.AudioFile file, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException {
2726
byte[] key = session.audioKey().getAudioKey(track.getGid(), file.getFileId());
2827

2928
session.send(Packet.Type.Unknown_0x4f, new byte[0]);
3029

31-
AudioFileStreaming stream = new AudioFileStreaming(session, file, key);
30+
AudioFileStreaming stream = new AudioFileStreaming(session, file, key, haltListener);
3231
stream.open();
3332

3433
InputStream in = stream.stream();
@@ -40,9 +39,9 @@ public StorageFeeder(@NotNull Session session, @NotNull PlayableId id) {
4039
}
4140

4241
@Override
43-
public @NotNull LoadedStream loadEpisode(Metadata.@NotNull Episode episode, Metadata.@NotNull AudioFile file) throws IOException {
42+
public @NotNull LoadedStream loadEpisode(Metadata.@NotNull Episode episode, Metadata.@NotNull AudioFile file, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException {
4443
byte[] key = session.audioKey().getAudioKey(episode.getGid(), file.getFileId());
45-
AudioFileStreaming stream = new AudioFileStreaming(session, file, key);
44+
AudioFileStreaming stream = new AudioFileStreaming(session, file, key, haltListener);
4645
stream.open();
4746

4847
InputStream in = stream.stream();

0 commit comments

Comments
 (0)