diff --git a/lib/src/main/java/xyz/gianlu/librespot/core/Session.java b/lib/src/main/java/xyz/gianlu/librespot/core/Session.java index 1df0b9c9..0b1609b3 100644 --- a/lib/src/main/java/xyz/gianlu/librespot/core/Session.java +++ b/lib/src/main/java/xyz/gianlu/librespot/core/Session.java @@ -388,7 +388,7 @@ private void authenticate(@NotNull Authentication.LoginCredentials credentials) * {@code true} for {@link Session#reconnect()}. */ private void authenticatePartial(@NotNull Authentication.LoginCredentials credentials, boolean removeLock) throws IOException, GeneralSecurityException, SpotifyAuthenticationException { - if (cipherPair == null) throw new IllegalStateException("Connection not established!"); + if (conn == null || cipherPair == null) throw new IllegalStateException("Connection not established!"); Authentication.ClientResponseEncrypted clientResponseEncrypted = Authentication.ClientResponseEncrypted.newBuilder() .setLoginCredentials(credentials) @@ -409,7 +409,6 @@ private void authenticatePartial(@NotNull Authentication.LoginCredentials creden receiver = new Receiver(); - byte[] bytes0x0f = new byte[20]; random().nextBytes(bytes0x0f); sendUnchecked(Packet.Type.Unknown_0x0f, bytes0x0f); @@ -452,6 +451,8 @@ private void authenticatePartial(@NotNull Authentication.LoginCredentials creden public void close() throws IOException { LOGGER.info("Closing session. {deviceId: {}}", inner.deviceId); + if (scheduledReconnect != null) scheduledReconnect.cancel(true); + closing = true; scheduler.shutdownNow(); @@ -513,6 +514,9 @@ public void close() throws IOException { } private void sendUnchecked(Packet.Type cmd, byte[] payload) throws IOException { + if (conn == null) + throw new IOException("Cannot write to missing connection."); + cipherPair.sendEncoded(conn.out, cmd.val, payload); } @@ -692,6 +696,9 @@ public Configuration configuration() { } private void reconnect() { + if (closing) + return; + synchronized (reconnectionListeners) { reconnectionListeners.forEach(ReconnectionListener::onConnectionDropped); } @@ -716,6 +723,9 @@ private void reconnect() { reconnectionListeners.forEach(ReconnectionListener::onConnectionEstablished); } } catch (IOException | GeneralSecurityException | SpotifyAuthenticationException ex) { + if (closing) + return; + conn = null; LOGGER.error("Failed reconnecting, retrying in 10 seconds...", ex); @@ -1310,7 +1320,7 @@ public void run() { continue; } } catch (IOException | GeneralSecurityException ex) { - if (running) { + if (running && !closing) { LOGGER.error("Failed reading packet!", ex); reconnect(); } diff --git a/player/src/main/java/xyz/gianlu/librespot/player/codecs/Codec.java b/player/src/main/java/xyz/gianlu/librespot/player/codecs/Codec.java index 1c680ad2..10c76773 100644 --- a/player/src/main/java/xyz/gianlu/librespot/player/codecs/Codec.java +++ b/player/src/main/java/xyz/gianlu/librespot/player/codecs/Codec.java @@ -44,7 +44,7 @@ public abstract class Codec implements Closeable { protected int seekZero = 0; private OutputAudioFormat format; - Codec(@NotNull GeneralAudioStream audioFile, @Nullable NormalizationData normalizationData, @NotNull PlayerConfiguration conf, int duration) { + public Codec(@NotNull GeneralAudioStream audioFile, @Nullable NormalizationData normalizationData, @NotNull PlayerConfiguration conf, int duration) { this.audioIn = audioFile.stream(); this.audioFile = audioFile; this.duration = duration; diff --git a/player/src/main/java/xyz/gianlu/librespot/player/codecs/Codecs.java b/player/src/main/java/xyz/gianlu/librespot/player/codecs/Codecs.java new file mode 100644 index 00000000..af655042 --- /dev/null +++ b/player/src/main/java/xyz/gianlu/librespot/player/codecs/Codecs.java @@ -0,0 +1,76 @@ +/* + * 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.codecs; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import xyz.gianlu.librespot.audio.GeneralAudioStream; +import xyz.gianlu.librespot.audio.NormalizationData; +import xyz.gianlu.librespot.audio.format.SuperAudioFormat; +import xyz.gianlu.librespot.player.PlayerConfiguration; + +import java.util.*; + +/** + * @author devgianlu + */ +public final class Codecs { + private static final Map>> codecs = new EnumMap<>(SuperAudioFormat.class); + private static final Logger LOGGER = LoggerFactory.getLogger(Codecs.class); + + static { + registerCodec(SuperAudioFormat.VORBIS, VorbisCodec.class); + registerCodec(SuperAudioFormat.MP3, Mp3Codec.class); + } + + private Codecs() { + } + + @Nullable + public static Codec initCodec(@NotNull SuperAudioFormat format, @NotNull GeneralAudioStream audioFile, @Nullable NormalizationData normalizationData, @NotNull PlayerConfiguration conf, int duration) { + Set> set = codecs.get(format); + if (set == null) return null; + + Optional> opt = set.stream().findFirst(); + if (!opt.isPresent()) return null; + + try { + Class clazz = opt.get(); + return clazz.getConstructor(GeneralAudioStream.class, NormalizationData.class, PlayerConfiguration.class, int.class).newInstance(audioFile, normalizationData, conf, duration); + } catch (ReflectiveOperationException ex) { + LOGGER.error("Failed initializing Codec instance for {}", format, ex); + return null; + } + } + + public static void registerCodec(@NotNull SuperAudioFormat format, @NotNull Class clazz) { + codecs.computeIfAbsent(format, (key) -> new HashSet<>(5)).add(clazz); + } + + public static void replaceCodecs(@NotNull SuperAudioFormat format, @NotNull Class clazz) { + Set> set = codecs.get(format); + if (set != null) set.clear(); + registerCodec(format, clazz); + } + + public static void unregisterCodec(@NotNull Class clazz) { + for (Set> set : codecs.values()) + set.remove(clazz); + } +} diff --git a/player/src/main/java/xyz/gianlu/librespot/player/playback/PlayerQueueEntry.java b/player/src/main/java/xyz/gianlu/librespot/player/playback/PlayerQueueEntry.java index fffbad6a..2dc2e9fe 100644 --- a/player/src/main/java/xyz/gianlu/librespot/player/playback/PlayerQueueEntry.java +++ b/player/src/main/java/xyz/gianlu/librespot/player/playback/PlayerQueueEntry.java @@ -16,7 +16,6 @@ package xyz.gianlu.librespot.player.playback; -import javazoom.jl.decoder.BitstreamException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -33,8 +32,7 @@ import xyz.gianlu.librespot.player.PlayerConfiguration; import xyz.gianlu.librespot.player.StateWrapper; import xyz.gianlu.librespot.player.codecs.Codec; -import xyz.gianlu.librespot.player.codecs.Mp3Codec; -import xyz.gianlu.librespot.player.codecs.VorbisCodec; +import xyz.gianlu.librespot.player.codecs.Codecs; import xyz.gianlu.librespot.player.codecs.VorbisOnlyAudioQuality; import xyz.gianlu.librespot.player.crossfade.CrossfadeController; import xyz.gianlu.librespot.player.metrics.PlaybackMetrics; @@ -132,20 +130,9 @@ private void load(boolean preload) throws IOException, Codec.CodecException, Mer if (crossfade.hasAnyFadeOut() || conf.preloadEnabled) notifyInstant(INSTANT_PRELOAD, (int) (crossfade.fadeOutStartTimeMin() - TimeUnit.SECONDS.toMillis(20))); - switch (stream.in.codec()) { - case VORBIS: - codec = new VorbisCodec(stream.in, stream.normalizationData, conf, metadata.duration()); - break; - case MP3: - try { - codec = new Mp3Codec(stream.in, stream.normalizationData, conf, metadata.duration()); - } catch (BitstreamException ex) { - throw new IOException(ex); - } - break; - default: - throw new UnsupportedEncodingException(stream.in.codec().toString()); - } + codec = Codecs.initCodec(stream.in.codec(), stream.in, stream.normalizationData, conf, metadata.duration()); + if (codec == null) + throw new UnsupportedEncodingException(stream.in.codec().toString()); LOGGER.trace("Loaded {} codec. {of: {}, format: {}, playbackId: {}}", stream.in.codec(), stream.in.describe(), codec.getAudioFormat(), playbackId); } diff --git a/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java b/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java index ee9adcd2..3f62e938 100644 --- a/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java +++ b/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java @@ -44,6 +44,7 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.*; +import java.util.concurrent.RejectedExecutionException; /** * @author Gianlu @@ -65,6 +66,7 @@ public final class DeviceStateHandler implements Closeable, DealerClient.Message private final Connect.PutStateRequest.Builder putState; private final AsyncWorker putStateWorker; private volatile String connectionId = null; + private volatile boolean closing = false; public DeviceStateHandler(@NotNull Session session, @NotNull PlayerConfiguration conf) { this.session = session; @@ -226,7 +228,11 @@ public synchronized void updateState(@NotNull Connect.PutStateReason reason, int .setClientSideTimestamp(TimeProvider.currentTimeMillis()) .getDeviceBuilder().setDeviceInfo(deviceInfo).setPlayerState(state); - putStateWorker.submit(putState.build()); + try { + putStateWorker.submit(putState.build()); + } catch (RejectedExecutionException ex) { + if (!closing) LOGGER.error("Failed to submit update state task.", ex); + } } public synchronized int getVolume() { @@ -244,6 +250,8 @@ public void setVolume(int val) { @Override public void close() { + closing = true; + session.dealer().removeMessageListener(this); session.dealer().removeRequestListener(this);