diff --git a/CHANGELOG.md b/CHANGELOG.md index faeaa959..cab64ff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Change Log +## [2.0.3] -- 2023-05-11 + +- Updated `IDENTITY_REGEX` in https://github.com/lavalink-devs/lavaplayer/pull/44 + +## [2.0.2] -- 2023-27-09 + +- Fix a bug with MPEG parsing that would lead to range exceptions in https://github.com/lavalink-devs/lavaplayer/pull/31 +- Specify all request timeouts in https://github.com/lavalink-devs/lavaplayer/pull/33 + +## [2.0.1] -- 2023-14-08 + +### Added + +- Support MPEG 2.5 by [@markozajc](https://github.com/markozajc) in https://github.com/lavalink-devs/lavaplayer/pull/30 + ## [2.0.0] -- 2023-03-08 ### Fixed diff --git a/README.md b/README.md index 6a53d7fb..181811e9 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,14 @@ number: [![Maven Central](https://img.shields.io/maven-central/v/dev.arbjerg/lav * Artifact: **dev.arbjerg:lavaplayer:x.y.z** Snapshots are published -to https://maven.arbjerg.dev/snapshots & https://s01.oss.sonatype.org/content/repositories/snapshots +to https://maven.lavalink.dev/snapshots & https://s01.oss.sonatype.org/content/repositories/snapshots Using in Gradle: ```gradle repositories { mavenCentral() + maven { url "https://jitpack.io" } // For com.github.walkyst.JAADec-fork:jaadec-ext-aac & ibxm-fork:com.github.walkyst:ibxm-fork } dependencies { @@ -35,6 +36,13 @@ dependencies { Using in Maven: ```xml + + + jitpack + https://jitpack.io + + + dev.arbjerg diff --git a/build.gradle.kts b/build.gradle.kts index e5d364c9..62a10470 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -39,8 +39,8 @@ subprojects { configure { if (findProperty("MAVEN_PASSWORD") != null && findProperty("MAVEN_USERNAME") != null) { repositories { - val snapshots = "https://maven.arbjerg.dev/snapshots" - val releases = "https://maven.arbjerg.dev/releases" + val snapshots = "https://maven.lavalink.dev/snapshots" + val releases = "https://maven.lavalink.dev/releases" maven(if (release) releases else snapshots) { credentials { @@ -50,7 +50,7 @@ subprojects { } } } else { - logger.lifecycle("Not publishing to maven.arbjerg.dev because credentials are not set") + logger.lifecycle("Not publishing to maven.lavalink.dev because credentials are not set") } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3FrameReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3FrameReader.java index 31e2930e..0698c626 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3FrameReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3FrameReader.java @@ -88,16 +88,22 @@ private int copyScanBufferEndToBeginning() { } private boolean parseFrameAt(int scanOffset) { - if (scanOffset >= HEADER_SIZE && (frameSize = Mp3Decoder.getFrameSize(scanBuffer, scanOffset - HEADER_SIZE)) > 0) { - for (int i = 0; i < HEADER_SIZE; i++) { - frameBuffer[i] = scanBuffer[scanOffset - HEADER_SIZE + i]; - } + int offset = scanOffset - HEADER_SIZE; + boolean invalid = offset < 0 + || !Mp3Decoder.hasFrameSync(scanBuffer, offset) + || Mp3Decoder.isUnsupportedVersion(scanBuffer, offset) + || !Mp3Decoder.isValidFrame(scanBuffer, offset); - frameBufferPosition = HEADER_SIZE; - return true; + if (invalid) + return false; + + frameSize = Mp3Decoder.getFrameSize(scanBuffer, offset); + for (int i = 0; i < HEADER_SIZE; i++) { + frameBuffer[i] = scanBuffer[offset + i]; } - return false; + frameBufferPosition = HEADER_SIZE; + return true; } /** diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/mp3/Mp3Decoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/mp3/Mp3Decoder.java index 6df56ac1..3dfc5ad7 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/mp3/Mp3Decoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/mp3/Mp3Decoder.java @@ -2,6 +2,8 @@ import com.sedmelluq.lava.common.natives.NativeResourceHolder; +import java.util.Arrays; + import java.nio.ByteBuffer; import java.nio.ShortBuffer; @@ -69,81 +71,7 @@ protected void freeResources() { } private static int getFrameBitRate(byte[] buffer, int offset) { - return isMpegVersionOne(buffer, offset) ? getFrameBitRateV1(buffer, offset) : getFrameBitRateV2(buffer, offset); - } - - private static int getFrameBitRateV1(byte[] buffer, int offset) { - switch ((buffer[offset + 2] & 0xF0) >>> 4) { - case 1: - return 32000; - case 2: - return 40000; - case 3: - return 48000; - case 4: - return 56000; - case 5: - return 64000; - case 6: - return 80000; - case 7: - return 96000; - case 8: - return 112000; - case 9: - return 128000; - case 10: - return 160000; - case 11: - return 192000; - case 12: - return 224000; - case 13: - return 256000; - case 14: - return 320000; - default: - throw new IllegalArgumentException("Not valid bitrate"); - } - } - - private static int getFrameBitRateV2(byte[] buffer, int offset) { - switch ((buffer[offset + 2] & 0xF0) >>> 4) { - case 1: - return 8000; - case 2: - return 16000; - case 3: - return 24000; - case 4: - return 32000; - case 5: - return 40000; - case 6: - return 48000; - case 7: - return 56000; - case 8: - return 64000; - case 9: - return 80000; - case 10: - return 96000; - case 11: - return 112000; - case 12: - return 128000; - case 13: - return 144000; - case 14: - return 160000; - default: - throw new IllegalArgumentException("Not valid bitrate"); - } - } - - private static int calculateFrameSize(boolean isVersionOne, int bitRate, int sampleRate, boolean hasPadding) { - return (isVersionOne ? 144 : 72) * bitRate / sampleRate + (hasPadding ? 1 : 0); + return MpegVersion.getVersion(buffer, offset).getBitRate(buffer, offset); } /** @@ -154,7 +82,7 @@ private static int calculateFrameSize(boolean isVersionOne, int bitRate, int sam * @return Sample rate */ public static int getFrameSampleRate(byte[] buffer, int offset) { - return isMpegVersionOne(buffer, offset) ? getFrameSampleRateV1(buffer, offset) : getFrameSampleRateV2(buffer, offset); + return MpegVersion.getVersion(buffer, offset).getSampleRate(buffer, offset); } /** @@ -168,30 +96,22 @@ public static int getFrameChannelCount(byte[] buffer, int offset) { return (buffer[offset + 3] & 0xC0) == 0xC0 ? 1 : 2; } - private static int getFrameSampleRateV1(byte[] buffer, int offset) { - switch ((buffer[offset + 2] & 0x0C) >>> 2) { - case 0: - return 44100; - case 1: - return 48000; - case 2: - return 32000; - default: - throw new IllegalArgumentException("Not valid sample rate"); - } + public static boolean hasFrameSync(byte[] buffer, int offset) { + // must start with 11 high bits + return (buffer[offset] & 0xFF) == 0xFF && (buffer[offset + 1] & 0xE0) == 0xE0; } - private static int getFrameSampleRateV2(byte[] buffer, int offset) { - switch ((buffer[offset + 2] & 0x0C) >>> 2) { - case 0: - return 22050; - case 1: - return 24000; - case 2: - return 16000; - default: - throw new IllegalArgumentException("Not valid sample rate"); - } + public static boolean isUnsupportedVersion(byte[] buffer, int offset) { + return (buffer[offset + 1] & 0x18) >> 3 == 0x01; + } + + public static boolean isValidFrame(byte[] buffer, int offset) { + int second = buffer[offset + 1] & 0xFF; + int third = buffer[offset + 2] & 0xFF; + return (second & 0x06) == 0x02 // Is Layer III + && (third & 0xF0) != 0x00 // Has defined bitrate + && (third & 0xF0) != 0xF0 // Valid bitrate + && (third & 0x0C) != 0x0C; // Valid sampling rate } /** @@ -202,26 +122,7 @@ private static int getFrameSampleRateV2(byte[] buffer, int offset) { * @return Frame size, or zero if not a valid frame header */ public static int getFrameSize(byte[] buffer, int offset) { - int first = buffer[offset] & 0xFF; - int second = buffer[offset + 1] & 0xFF; - int third = buffer[offset + 2] & 0xFF; - - boolean invalid = (first != 0xFF || (second & 0xE0) != 0xE0) // Frame sync does not match - || (second & 0x10) != 0x10 // Not MPEG-1 nor MPEG-2, not dealing with this stuff - || (second & 0x06) != 0x02 // Not Layer III, not dealing with this stuff - || (third & 0xF0) == 0x00 // No defined bitrate - || (third & 0xF0) == 0xF0 // Invalid bitrate - || (third & 0x0C) == 0x0C; // Invalid sampling rate - - if (invalid) { - return 0; - } - - int bitRate = getFrameBitRate(buffer, offset); - int sampleRate = getFrameSampleRate(buffer, offset); - boolean hasPadding = (third & 0x02) != 0; - - return calculateFrameSize(isMpegVersionOne(buffer, offset), bitRate, sampleRate, hasPadding); + return MpegVersion.getVersion(buffer, offset).getFrameSize(buffer, offset); } /** @@ -232,10 +133,7 @@ public static int getFrameSize(byte[] buffer, int offset) { * @return Average frame size, assuming CBR */ public static double getAverageFrameSize(byte[] buffer, int offset) { - int bitRate = getFrameBitRate(buffer, offset); - int sampleRate = getFrameSampleRate(buffer, offset); - - return (isMpegVersionOne(buffer, offset) ? 144.0 : 72.0) * bitRate / sampleRate; + return MpegVersion.getVersion(buffer, offset).getAverageFrameSize(buffer, offset); } /** @@ -244,14 +142,99 @@ public static double getAverageFrameSize(byte[] buffer, int offset) { * @return Number of samples per frame. */ public static long getSamplesPerFrame(byte[] buffer, int offset) { - return isMpegVersionOne(buffer, offset) ? MPEG1_SAMPLES_PER_FRAME : MPEG2_SAMPLES_PER_FRAME; - } - - private static boolean isMpegVersionOne(byte[] buffer, int offset) { - return (buffer[offset + 1] & 0x08) == 0x08; + return MpegVersion.getVersion(buffer, offset).getSamplesPerFrame(); } public static int getMaximumFrameSize() { - return calculateFrameSize(true, 320000, 32000, true); + return MpegVersion.MAX_FRAME_SIZE; + } + + private static final int[] SAMPLE_RATE_BASE = { 11025, 12000, 8000 }; + + public static enum MpegVersion { + + MPEG_1(4, 1152, new int[] { 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320 }), + MPEG_2(2, 576, new int[] { 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 }), + MPEG_2_5(1, MPEG_2.samplesPerFrame, MPEG_2.bitrateIndex); + + public static final int MAX_FRAME_SIZE = getMaxFrameSize(); + + public static MpegVersion getVersion(byte[] buffer, int offset) { + // 0 - MPEG 2.5 + // 1 - reserved (unsupported) + // 2 - MPEG 2 + // 3 - MPEG 1 + int index = (buffer[offset + 1] & 0x18) >> 3; + switch (index) { + case 0: + return MPEG_2_5; + case 2: + return MPEG_2; + case 3: + return MPEG_1; + default: + throw new IllegalArgumentException("Invalid version"); + } + } + + private static int getMaxFrameSize() { + int bitRate = MPEG_1.bitrateIndex[MPEG_1.bitrateIndex.length - 1] * 1000; + int sampleRate = MPEG_1.samplerateIndex[2]; + return MPEG_1.calculateFrameSize(bitRate, sampleRate, true); + } + + private final int samplesPerFrame; + private final int frameLengthMultiplier; + private final int[] bitrateIndex; + private final int[] samplerateIndex; + + MpegVersion(int samplerateMultiplier, int samplesPerFrame, int[] bitrateIndex) { + this.samplesPerFrame = samplesPerFrame; + this.frameLengthMultiplier = samplesPerFrame / 8; + this.bitrateIndex = bitrateIndex; + this.samplerateIndex = Arrays.stream(SAMPLE_RATE_BASE).map(r -> r * samplerateMultiplier).toArray(); + } + + public int getSamplesPerFrame() { + return this.samplesPerFrame; + } + + public int getFrameLengthMultiplier() { + return this.frameLengthMultiplier; + } + + public int getBitRate(byte[] buffer, int offset) { + int index = (buffer[offset + 2] & 0xF0) >> 4; + if (index == 0 || index == 15) + throw new IllegalArgumentException("Invalid bitrate"); + + return this.bitrateIndex[index - 1] * 1000; + } + + public int getSampleRate(byte[] buffer, int offset) { + int index = (buffer[offset + 2] & 0x0C) >> 2; + if (index == 3) + throw new IllegalArgumentException("Invalid samplerate"); + + return this.samplerateIndex[index]; + } + + public int getFrameSize(byte[] buffer, int offset) { + return calculateFrameSize(getBitRate(buffer, offset), getSampleRate(buffer, offset), + hasPadding(buffer, offset)); + } + + public double getAverageFrameSize(byte[] buffer, int offset) { + return (double) getFrameLengthMultiplier() * getBitRate(buffer, offset) / getSampleRate(buffer, offset); + } + + private int calculateFrameSize(int bitRate, int sampleRate, boolean hasPadding) { + return getFrameLengthMultiplier() * bitRate / sampleRate + (hasPadding ? 1 : 0); + } + + private static boolean hasPadding(byte[] buffer, int offset) { + return (buffer[offset + 2] & 0x02) != 0; + } + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudDataReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudDataReader.java index c7098517..aec76ae2 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudDataReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudDataReader.java @@ -37,7 +37,7 @@ public AudioTrackInfo readTrackInfo(JsonBrowser trackData, String identifier) { false, trackData.get("permalink_url").text(), ThumbnailTools.getSoundCloudThumbnail(trackData), - null + trackData.get("publisher_metadata").get("isrc").text() ); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/twitch/TwitchStreamAudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/twitch/TwitchStreamAudioSourceManager.java index b8880064..f860e646 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/twitch/TwitchStreamAudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/twitch/TwitchStreamAudioSourceManager.java @@ -25,6 +25,7 @@ import java.io.DataOutput; import java.io.IOException; import java.net.URI; +import java.util.Locale; import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Matcher; @@ -81,7 +82,11 @@ public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) } else { String title = channelInfo.get("lastBroadcast").get("title").text(); - final String thumbnail = channelInfo.get("profileImageURL").text().replaceFirst("-70x70", "-300x300"); + final String thumbnail = String.format( + "https://static-cdn.jtvnw.net/previews-ttv/live_user_%s-440x248.jpg", + // Using root because the turkish lowercase "i" does not have the little dot above the letter when defaulted + streamName.toLowerCase(Locale.ROOT) + ); return new TwitchStreamAudioTrack(new AudioTrackInfo( title, diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubePlaylistLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubePlaylistLoader.java index ceb473c4..cde3bfa4 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubePlaylistLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubePlaylistLoader.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -167,9 +166,8 @@ private String extractPlaylistTracks(JsonBrowser playlistVideoList, List"; - private static final String IDENTITY_REGEX = "\\{clientId:\"(.+?)\",.+?:\"(.+?)\""; + private static final String IDENTITY_REGEX = "\\{clientId:\"(.+?)\",\\n?.+?:\"(.+?)\""; private static final Pattern authScriptPattern = Pattern.compile(AUTH_SCRIPT_REGEX); private static final Pattern identityPattern = Pattern.compile(IDENTITY_REGEX); diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/JsonBrowser.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/JsonBrowser.java index 07241603..a9b302e2 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/JsonBrowser.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/JsonBrowser.java @@ -209,6 +209,11 @@ public String text() { return null; } + public String textOrDefault(String defaultValue) { + String value = text(); + return value != null ? value : defaultValue; + } + public boolean asBoolean(boolean defaultValue) { if (node != null) { if (node.isBoolean()) { diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpClientTools.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpClientTools.java index c9a3099a..e4024200 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpClientTools.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpClientTools.java @@ -39,11 +39,15 @@ public class HttpClientTools { public static final RequestConfig DEFAULT_REQUEST_CONFIG = RequestConfig.custom() .setConnectTimeout(3000) + .setConnectionRequestTimeout(3000) + .setSocketTimeout(3000) .setCookieSpec(CookieSpecs.STANDARD) .build(); private static final RequestConfig NO_COOKIES_REQUEST_CONFIG = RequestConfig.custom() .setConnectTimeout(3000) + .setConnectionRequestTimeout(3000) + .setSocketTimeout(3000) .setCookieSpec(CookieSpecs.IGNORE_COOKIES) .build();