Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preloading implementation #21

Merged
merged 9 commits into from
Nov 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public float normalisationPregain() {
return 0;
}

@Override
public boolean preloadEnabled() {
return true;
}

//****************//
//---- CACHE -----//
//****************//
Expand Down
120 changes: 101 additions & 19 deletions core/src/main/java/org/librespot/spotify/player/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.librespot.spotify.Utils;
import org.librespot.spotify.core.Session;
import org.librespot.spotify.proto.Metadata;
import org.librespot.spotify.proto.Spirc;
import org.librespot.spotify.spirc.FrameListener;
import org.librespot.spotify.spirc.SpotifyIrc;
Expand All @@ -12,18 +14,24 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
* @author Gianlu
*/
public class Player implements FrameListener, TrackHandler.Listener {
private static final Logger LOGGER = Logger.getLogger(Player.class);
private static final long TRACK_PRELOAD_THRESHOLD = TimeUnit.SECONDS.toMillis(10);
private final Session session;
private final SpotifyIrc spirc;
private final Spirc.State.Builder state;
private final PlayerConfiguration conf;
private final CacheManager cacheManager;
private final PreloadScheduler scheduler = new PreloadScheduler();
private TrackHandler trackHandler;
private TrackHandler preloadTrackHandler;

public Player(@NotNull PlayerConfiguration conf, @NotNull CacheManager.CacheConfiguration cacheConfiguration, @NotNull Session session) {
this.conf = conf;
Expand Down Expand Up @@ -121,15 +129,15 @@ private void handleSetVolume(int volume) {
private void handleVolumeDown() {
if (trackHandler != null) {
PlayerRunner.Controller controller = trackHandler.controller();
if (controller != null) controller.volumeDown();
if (controller != null) spirc.deviceState().setVolume(controller.volumeDown());
stateUpdated();
}
}

private void handleVolumeUp() {
if (trackHandler != null) {
PlayerRunner.Controller controller = trackHandler.controller();
if (controller != null) controller.volumeUp();
if (controller != null) spirc.deviceState().setVolume(controller.volumeUp());
stateUpdated();
}
}
Expand Down Expand Up @@ -169,6 +177,8 @@ private void handleSeek(int pos) {
state.setPositionMeasuredAt(System.currentTimeMillis());
if (trackHandler != null) trackHandler.sendSeek(pos);
stateUpdated();

scheduler.reschedule();
}

private void updatedTracks(@NotNull Spirc.Frame frame) {
Expand All @@ -181,23 +191,42 @@ private void updatedTracks(@NotNull Spirc.Frame frame) {
}

@Override
public void finishedLoading(boolean play) {
if (play) state.setStatus(Spirc.PlayStatus.kPlayStatusPlay);
else state.setStatus(Spirc.PlayStatus.kPlayStatusPause);
stateUpdated();
public void finishedLoading(@NotNull TrackHandler handler, boolean play) {
if (handler == trackHandler) {
if (play) state.setStatus(Spirc.PlayStatus.kPlayStatusPlay);
else state.setStatus(Spirc.PlayStatus.kPlayStatusPause);
stateUpdated();
} else if (handler == preloadTrackHandler) {
LOGGER.trace("Preloaded track is ready.");
}
}

@Override
public void loadingError(@NotNull Exception ex) {
LOGGER.fatal("Failed loading track!", ex);
state.setStatus(Spirc.PlayStatus.kPlayStatusStop);
stateUpdated();
public void loadingError(@NotNull TrackHandler handler, @NotNull Exception ex) {
if (handler == trackHandler) {
LOGGER.fatal("Failed loading track!", ex);
state.setStatus(Spirc.PlayStatus.kPlayStatusStop);
stateUpdated();
} else if (handler == preloadTrackHandler) {
LOGGER.warn("Preloaded track loading failed!", ex);
preloadTrackHandler = null;
}
}

@Override
public void endOfTrack() {
LOGGER.trace("End of track. Proceeding with next.");
handleNext();
public void endOfTrack(@NotNull TrackHandler handler) {
if (handler == trackHandler) {
LOGGER.trace("End of track. Proceeding with next.");
handleNext();
}
}

private void preloadNextTrack() {
Spirc.TrackRef next = state.getTrack(getQueuedTrack(false));

preloadTrackHandler = new TrackHandler(session, cacheManager, conf, this);
preloadTrackHandler.sendLoad(next, false, 0);
LOGGER.trace("Started next track preload, gid: " + Utils.bytesToHex(next.getGid()));
}

private void handleLoad(@NotNull Spirc.Frame frame) {
Expand All @@ -223,11 +252,27 @@ private void handleLoad(@NotNull Spirc.Frame frame) {

private void loadTrack(boolean play) {
if (trackHandler != null) trackHandler.close();
trackHandler = new TrackHandler(session, cacheManager, conf, this);
trackHandler.sendLoad(state.getTrack(state.getPlayingTrackIndex()), play, state.getPositionMs());
state.setStatus(Spirc.PlayStatus.kPlayStatusLoading);

Spirc.TrackRef ref = state.getTrack(state.getPlayingTrackIndex());
if (preloadTrackHandler != null && preloadTrackHandler.isTrack(ref)) {
trackHandler = preloadTrackHandler;
preloadTrackHandler = null;
trackHandler.sendSeek(state.getPositionMs());
if (play) {
state.setStatus(Spirc.PlayStatus.kPlayStatusPlay);
trackHandler.sendPlay();
} else {
state.setStatus(Spirc.PlayStatus.kPlayStatusPause);
}
} else {
trackHandler = new TrackHandler(session, cacheManager, conf, this);
trackHandler.sendLoad(ref, play, state.getPositionMs());
state.setStatus(Spirc.PlayStatus.kPlayStatusLoading);
}

stateUpdated();

scheduler.reschedule();
}

private void handlePlay() {
Expand All @@ -236,6 +281,8 @@ private void handlePlay() {
state.setStatus(Spirc.PlayStatus.kPlayStatusPlay);
state.setPositionMeasuredAt(System.currentTimeMillis());
stateUpdated();

scheduler.reschedule();
}
}

Expand All @@ -250,11 +297,13 @@ private void handlePause() {
state.setPositionMs(pos + diff);
state.setPositionMeasuredAt(now);
stateUpdated();

scheduler.reschedule();
}
}

private void handleNext() {
int newTrack = consumeQueuedTrack();
int newTrack = getQueuedTrack(true);
boolean play = true;
if (newTrack >= state.getTrackCount()) {
newTrack = 0;
Expand Down Expand Up @@ -299,13 +348,15 @@ private void handlePrev() {
state.setPositionMeasuredAt(System.currentTimeMillis());
if (trackHandler != null) trackHandler.sendSeek(0);
stateUpdated();

scheduler.reschedule();
}
}

private int consumeQueuedTrack() {
private int getQueuedTrack(boolean consume) {
int current = state.getPlayingTrackIndex();
if (state.getTrack(current).getQueued()) {
state.removeTrack(current);
if (consume) state.removeTrack(current);
return current;
}

Expand All @@ -316,6 +367,37 @@ public interface PlayerConfiguration {
@NotNull
TrackHandler.AudioQuality preferredQuality();

boolean preloadEnabled();

float normalisationPregain();
}

private class PreloadScheduler {
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private Task activeTask;

void reschedule() {
if (activeTask != null) activeTask.abort();

if (!conf.preloadEnabled() || state.getStatus() != Spirc.PlayStatus.kPlayStatusPlay) return;
Metadata.Track track = trackHandler != null ? trackHandler.track() : null;
if (track == null) return;

activeTask = new Task();
executor.schedule(activeTask, (track.getDuration() - getPosition()) - TRACK_PRELOAD_THRESHOLD, TimeUnit.MILLISECONDS);
}

private class Task implements Runnable {
private volatile boolean aborted = false;

@Override
public void run() {
if (!aborted) preloadNextTrack();
}

private void abort() {
aborted = true;
}
}
}
}
18 changes: 10 additions & 8 deletions core/src/main/java/org/librespot/spotify/player/PlayerRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,15 @@ void pause() {
}

void seek(int positionMs) {
if (positionMs > 0) {
if (positionMs >= 0) {
try {
audioIn.reset();
int skip = Math.round(audioIn.available() / (float) duration * positionMs);
long skipped = audioIn.skip(skip);
if (skip != skipped)
throw new IOException(String.format("Failed seeking, skip: %d, skipped: %d", skip, skipped));
if (positionMs > 0) {
int skip = Math.round(audioIn.available() / (float) duration * positionMs);
long skipped = audioIn.skip(skip);
if (skip != skipped)
throw new IOException(String.format("Failed seeking, skip: %d, skipped: %d", skip, skipped));
}
} catch (IOException ex) {
LOGGER.fatal("Failed seeking!", ex);
}
Expand Down Expand Up @@ -337,12 +339,12 @@ private static class NotVorbisException extends PlayerException {
private static class HoleInDataException extends PlayerException {
}

public static class PlayerException extends Exception {
static class PlayerException extends Exception {

PlayerException() {
private PlayerException() {
}

PlayerException(@NotNull Throwable ex) {
private PlayerException(@NotNull Throwable ex) {
super(ex);
}
}
Expand Down
26 changes: 18 additions & 8 deletions core/src/main/java/org/librespot/spotify/player/TrackHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class TrackHandler implements PlayerRunner.Listener, Closeable {
private final Listener listener;
private final Looper looper;
private PlayerRunner playerRunner;
private Metadata.Track track;

TrackHandler(@NotNull Session session, @NotNull CacheManager cacheManager, @NotNull Player.PlayerConfiguration conf, @NotNull Listener listener) {
this.session = session;
Expand Down Expand Up @@ -58,7 +59,7 @@ private static Metadata.Track pickAlternativeIfNecessary(@NotNull Metadata.Track
}

private void load(@NotNull Spirc.TrackRef ref, boolean play, int pos) throws IOException, MercuryClient.MercuryException {
Metadata.Track track = session.mercury().requestSync(MercuryRequests.getTrack(new TrackId(ref)));
track = session.mercury().requestSync(MercuryRequests.getTrack(new TrackId(ref)));
track = pickAlternativeIfNecessary(track);
if (track == null) {
LOGGER.fatal("Couldn't find playable track: " + Utils.bytesToHex(ref.getGid()));
Expand Down Expand Up @@ -98,12 +99,12 @@ private void load(@NotNull Spirc.TrackRef ref, boolean play, int pos) throws IOE

playerRunner.seek(pos);

listener.finishedLoading(play);
listener.finishedLoading(this, play);

if (play) playerRunner.play();
} catch (PlayerRunner.PlayerException ex) {
LOGGER.fatal("Failed starting playback!", ex);
listener.loadingError(ex);
listener.loadingError(this, ex);
}
}

Expand Down Expand Up @@ -133,7 +134,7 @@ void sendLoad(@NotNull Spirc.TrackRef ref, boolean play, int pos) {

@Override
public void endOfTrack() {
listener.endOfTrack();
listener.endOfTrack(this);
}

@Override
Expand All @@ -152,6 +153,15 @@ public void close() {
looper.stop();
}

@Nullable
public Metadata.Track track() {
return track;
}

boolean isTrack(Spirc.TrackRef ref) {
return track != null && ref.getGid().equals(track.getGid());
}

public enum AudioQuality {
VORBIS_96(Metadata.AudioFile.Format.OGG_VORBIS_96),
VORBIS_160(Metadata.AudioFile.Format.OGG_VORBIS_160),
Expand Down Expand Up @@ -200,11 +210,11 @@ public enum Command {
}

public interface Listener {
void finishedLoading(boolean play);
void finishedLoading(@NotNull TrackHandler handler, boolean play);

void loadingError(@NotNull Exception ex);
void loadingError(@NotNull TrackHandler handler, @NotNull Exception ex);

void endOfTrack();
void endOfTrack(@NotNull TrackHandler handler);
}

private class Looper implements Runnable {
Expand All @@ -220,7 +230,7 @@ public void run() {
try {
load((Spirc.TrackRef) cmd.args[0], (Boolean) cmd.args[1], (Integer) cmd.args[2]);
} catch (IOException | MercuryClient.MercuryException ex) {
listener.loadingError(ex);
listener.loadingError(TrackHandler.this, ex);
}
break;
case Play:
Expand Down