xyz.gianlu.librespot.player.playback.PlayerSession Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2021 devgianlu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.gianlu.librespot.player.playback;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xyz.gianlu.librespot.audio.MetadataWrapper;
import xyz.gianlu.librespot.audio.PlayableContentFeeder;
import xyz.gianlu.librespot.common.NameThreadFactory;
import xyz.gianlu.librespot.core.Session;
import xyz.gianlu.librespot.metadata.PlayableId;
import xyz.gianlu.librespot.player.PlayerConfiguration;
import xyz.gianlu.librespot.player.crossfade.CrossfadeController;
import xyz.gianlu.librespot.player.decoders.Decoder;
import xyz.gianlu.librespot.player.metrics.PlaybackMetrics.Reason;
import xyz.gianlu.librespot.player.metrics.PlayerMetrics;
import xyz.gianlu.librespot.player.mixing.AudioSink;
import xyz.gianlu.librespot.player.mixing.MixingLine;
import java.io.Closeable;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Handles a session which is a container for entries (each with its own playback ID). This is responsible for higher level prev/next operations (using {@link PlayerQueue},
* receiving and creating instants, dispatching events to the player and operating the sink.
*
* @author devgianlu
*/
public class PlayerSession implements Closeable, PlayerQueueEntry.Listener {
private static final Logger LOGGER = LoggerFactory.getLogger(PlayerSession.class);
private final ExecutorService executorService = Executors.newCachedThreadPool(new NameThreadFactory((r) -> "player-session-" + r.hashCode()));
private final Session session;
private final AudioSink sink;
private final PlayerConfiguration conf;
private final String sessionId;
private final Listener listener;
private final PlayerQueue queue;
private int lastPlayPos = 0;
private Reason lastPlayReason = null;
private volatile boolean closed = false;
public PlayerSession(@NotNull Session session, @NotNull AudioSink sink, @NotNull PlayerConfiguration conf, @NotNull String sessionId, @NotNull Listener listener) {
this.session = session;
this.sink = sink;
this.conf = conf;
this.sessionId = sessionId;
this.listener = listener;
this.queue = new PlayerQueue();
LOGGER.info("Created new session. {id: {}}", sessionId);
sink.clearOutputs();
}
/**
* Creates and adds a new entry to the queue.
*
* @param playable The content for the new entry
*/
private void add(@NotNull PlayableId playable, boolean preloaded) {
PlayerQueueEntry entry = new PlayerQueueEntry(sink, session, conf, playable, preloaded, this);
queue.add(entry);
if (queue.next() == entry) {
PlayerQueueEntry head = queue.head();
if (head != null && head.crossfade != null) {
boolean customFade = entry.playable.equals(head.crossfade.fadeOutPlayable());
CrossfadeController.FadeInterval fadeOut;
if ((fadeOut = head.crossfade.selectFadeOut(Reason.TRACK_DONE, customFade)) != null)
head.notifyInstant(PlayerQueueEntry.INSTANT_START_NEXT, fadeOut.start());
}
}
}
/**
* Adds the next content to the queue (considered as preloading).
*/
private void addNext() {
PlayableId playable = listener.nextPlayableDoNotSet();
if (playable != null) add(playable, true);
}
/**
* Tries to advance to the given content. This is a destructive operation as it will close every entry that passes by.
* Also checks if the next entry has the same content, in that case it advances (repeating track fix).
*
* @param id The target content
* @return Whether the operation was successful
*/
private boolean advanceTo(@NotNull PlayableId id) {
do {
PlayerQueueEntry entry = queue.head();
if (entry == null) return false;
if (entry.playable.equals(id)) {
PlayerQueueEntry next = queue.next();
if (next == null || !next.playable.equals(id))
return true;
}
} while (queue.advance());
return false;
}
/**
* Gets the next content and tries to advance, notifying if successful.
*/
private void advance(@NotNull Reason reason) {
if (closed) return;
PlayableId next = listener.nextPlayable();
if (next == null)
return;
EntryWithPos entry = playInternal(next, 0, reason);
listener.trackChanged(entry.entry.playbackId, entry.entry.metadata(), entry.pos, reason);
}
@Override
public void instantReached(@NotNull PlayerQueueEntry entry, int callbackId, int exactTime) {
switch (callbackId) {
case PlayerQueueEntry.INSTANT_PRELOAD:
if (entry == queue.head()) executorService.execute(this::addNext);
break;
case PlayerQueueEntry.INSTANT_START_NEXT:
executorService.execute(() -> advance(Reason.TRACK_DONE));
break;
case PlayerQueueEntry.INSTANT_END:
entry.close();
break;
default:
throw new IllegalArgumentException("Unknown callback: " + callbackId);
}
}
@Override
public void playbackEnded(@NotNull PlayerQueueEntry entry) {
listener.trackPlayed(entry.playbackId, entry.endReason, entry.metrics(), entry.getTimeNoThrow());
if (entry == queue.head())
advance(Reason.TRACK_DONE);
}
@Override
public void startedLoading(@NotNull PlayerQueueEntry entry) {
LOGGER.trace("{} started loading.", entry);
if (entry == queue.head()) listener.startedLoading();
}
@Override
public void loadingError(@NotNull PlayerQueueEntry entry, @NotNull Exception ex, boolean retried) {
if (entry == queue.head()) {
if (ex instanceof PlayableContentFeeder.ContentRestrictedException) {
advance(Reason.TRACK_ERROR);
} else if (!retried) {
PlayerQueueEntry newEntry = entry.retrySelf(false);
executorService.execute(() -> {
queue.swap(entry, newEntry);
playInternal(newEntry.playable, lastPlayPos, lastPlayReason == null ? Reason.TRACK_ERROR : lastPlayReason);
});
return;
}
listener.loadingError(ex);
} else if (entry == queue.next()) {
if (!(ex instanceof PlayableContentFeeder.ContentRestrictedException) && !retried) {
PlayerQueueEntry newEntry = entry.retrySelf(true);
executorService.execute(() -> queue.swap(entry, newEntry));
return;
}
}
queue.remove(entry);
}
@Override
public void finishedLoading(@NotNull PlayerQueueEntry entry, @NotNull MetadataWrapper metadata) {
LOGGER.trace("{} finished loading.", entry);
if (entry == queue.head()) listener.finishedLoading(metadata);
}
@Override
public @NotNull Optional
© 2015 - 2025 Weber Informatics LLC | Privacy Policy