From 9565a0d600a7ab8e302b840b93d2d4e05de7b573 Mon Sep 17 00:00:00 2001 From: Nik Date: Thu, 3 Aug 2023 19:46:07 -0700 Subject: [PATCH 1/2] Reformat code to 4 space indents --- .github/workflows/build.yml | 102 +-- .github/workflows/release.yml | 70 +- CHANGELOG.md | 441 +++++++++- FAQ.md | 26 +- README.md | 147 +++- build.gradle.kts | 166 ++-- common/build.gradle.kts | 12 +- .../natives/NativeLibraryBinaryProvider.java | 13 +- .../common/natives/NativeLibraryLoader.java | 329 ++++--- .../natives/NativeLibraryProperties.java | 88 +- .../common/natives/NativeResourceHolder.java | 70 +- .../ResourceNativeLibraryBinaryProvider.java | 31 +- .../SystemNativeLibraryProperties.java | 98 +-- .../architecture/ArchitectureType.java | 8 +- .../DefaultArchitectureTypes.java | 82 +- .../DefaultOperatingSystemTypes.java | 121 ++- .../architecture/OperatingSystemType.java | 24 +- .../natives/architecture/SystemType.java | 117 +-- .../common/tools/DaemonThreadFactory.java | 120 +-- .../lava/common/tools/ExecutorTools.java | 207 +++-- demo-d4j/build.gradle | 20 +- .../lavaplayer/demo/d4j/D4jAudioProvider.java | 29 +- .../demo/d4j/GuildMusicManager.java | 39 +- .../discord/lavaplayer/demo/d4j/Main.java | 196 ++--- .../lavaplayer/demo/d4j/TrackScheduler.java | 70 +- demo-jda/build.gradle | 12 +- .../demo/jda/AudioPlayerSendHandler.java | 64 +- .../demo/jda/GuildMusicManager.java | 47 +- .../discord/lavaplayer/demo/jda/Main.java | 160 ++-- .../lavaplayer/demo/jda/TrackScheduler.java | 70 +- extensions/format-xm/build.gradle.kts | 14 +- .../extensions/format/xm/XmAudioTrack.java | 38 +- .../format/xm/XmContainerProbe.java | 81 +- .../extensions/format/xm/XmFileLoader.java | 20 +- .../extensions/format/xm/XmTrackProvider.java | 42 +- extensions/youtube-rotator/build.gradle.kts | 12 +- .../YoutubeIpRotatorFilter.java | 197 ++--- .../YoutubeIpRotatorRetryHandler.java | 31 +- .../youtuberotator/YoutubeIpRotatorSetup.java | 151 ++-- .../planner/AbstractRoutePlanner.java | 244 +++--- .../planner/BalancingIpRoutePlanner.java | 124 +-- .../planner/NanoIpRoutePlanner.java | 107 +-- .../planner/RotatingIpRoutePlanner.java | 236 ++--- .../planner/RotatingNanoIpRoutePlanner.java | 170 ++-- .../youtuberotator/tools/BigRandom.java | 26 +- .../tools/RateLimitException.java | 18 +- .../youtuberotator/tools/Tuple.java | 15 +- .../tools/ip/CombinedIpBlock.java | 192 ++--- .../tools/ip/IpAddressTools.java | 50 +- .../youtuberotator/tools/ip/IpBlock.java | 20 +- .../youtuberotator/tools/ip/Ipv4Block.java | 164 ++-- .../youtuberotator/tools/ip/Ipv6Block.java | 230 ++--- jitpack.yml | 2 +- main/build.gradle.kts | 66 +- .../lavaplayer/container/MediaContainer.java | 56 +- .../container/MediaContainerDescriptor.java | 18 +- .../container/MediaContainerDetection.java | 259 +++--- .../MediaContainerDetectionResult.java | 204 ++--- .../container/MediaContainerHints.java | 60 +- .../container/MediaContainerProbe.java | 62 +- .../container/MediaContainerRegistry.java | 44 +- .../container/adts/AdtsAudioTrack.java | 38 +- .../container/adts/AdtsContainerProbe.java | 54 +- .../container/adts/AdtsPacketHeader.java | 14 +- .../container/adts/AdtsStreamProvider.java | 211 ++--- .../container/adts/AdtsStreamReader.java | 246 +++--- .../container/common/AacPacketRouter.java | 170 ++-- .../container/common/OpusPacketRouter.java | 338 ++++---- .../container/flac/FlacAudioTrack.java | 40 +- .../container/flac/FlacContainerProbe.java | 58 +- .../container/flac/FlacFileLoader.java | 88 +- .../container/flac/FlacMetadataHeader.java | 60 +- .../container/flac/FlacMetadataReader.java | 146 ++-- .../container/flac/FlacSeekPoint.java | 6 +- .../container/flac/FlacStreamInfo.java | 130 +-- .../container/flac/FlacTrackInfo.java | 82 +- .../container/flac/FlacTrackInfoBuilder.java | 102 +-- .../container/flac/FlacTrackProvider.java | 170 ++-- .../flac/frame/FlacFrameHeaderReader.java | 220 ++--- .../container/flac/frame/FlacFrameInfo.java | 2 +- .../container/flac/frame/FlacFrameReader.java | 218 ++--- .../flac/frame/FlacSubFrameReader.java | 300 +++---- .../matroska/MatroskaAacTrackConsumer.java | 146 ++-- .../matroska/MatroskaAudioTrack.java | 152 ++-- .../matroska/MatroskaContainerProbe.java | 78 +- .../matroska/MatroskaOpusTrackConsumer.java | 82 +- .../matroska/MatroskaStreamingFile.java | 608 +++++++------ .../matroska/MatroskaTrackConsumer.java | 76 +- .../matroska/MatroskaVorbisTrackConsumer.java | 220 ++--- .../matroska/format/MatroskaBlock.java | 52 +- .../matroska/format/MatroskaCuePoint.java | 2 +- .../matroska/format/MatroskaEbmlReader.java | 260 +++--- .../matroska/format/MatroskaElementType.java | 230 ++--- .../matroska/format/MatroskaFileReader.java | 375 ++++---- .../matroska/format/MatroskaFileTrack.java | 344 ++++---- .../matroska/format/MutableMatroskaBlock.java | 240 +++--- .../format/MutableMatroskaElement.java | 2 +- .../container/mp3/Mp3AudioTrack.java | 40 +- .../container/mp3/Mp3ConstantRateSeeker.java | 117 +-- .../container/mp3/Mp3ContainerProbe.java | 70 +- .../container/mp3/Mp3FrameReader.java | 235 ++--- .../lavaplayer/container/mp3/Mp3Seeker.java | 30 +- .../container/mp3/Mp3StreamSeeker.java | 24 +- .../container/mp3/Mp3TrackProvider.java | 635 +++++++------- .../container/mp3/Mp3XingSeeker.java | 124 +-- .../container/mpeg/MpegAacTrackConsumer.java | 178 ++-- .../container/mpeg/MpegAudioTrack.java | 118 +-- .../container/mpeg/MpegContainerProbe.java | 94 +- .../container/mpeg/MpegFileLoader.java | 481 ++++++----- .../container/mpeg/MpegNoopTrackConsumer.java | 76 +- .../container/mpeg/MpegTrackConsumer.java | 64 +- .../container/mpeg/MpegTrackInfo.java | 154 ++-- .../mpeg/reader/MpegFileTrackProvider.java | 48 +- .../mpeg/reader/MpegParseStopChecker.java | 12 +- .../container/mpeg/reader/MpegReader.java | 459 +++++----- .../mpeg/reader/MpegSectionHandler.java | 10 +- .../mpeg/reader/MpegSectionInfo.java | 44 +- .../reader/MpegVersionedSectionHandler.java | 10 +- .../mpeg/reader/MpegVersionedSectionInfo.java | 36 +- .../MpegFragmentedFileTrackProvider.java | 356 ++++---- .../reader/fragmented/MpegGlobalSeekInfo.java | 62 +- .../reader/fragmented/MpegSegmentEntry.java | 44 +- .../fragmented/MpegTrackFragmentHeader.java | 223 ++--- .../MpegStandardFileTrackProvider.java | 520 +++++------ .../container/mpegts/MpegAdtsAudioTrack.java | 28 +- .../mpegts/MpegAdtsContainerProbe.java | 86 +- .../mpegts/MpegTsElementaryInputStream.java | 517 ++++++----- .../mpegts/PesPacketInputStream.java | 146 ++-- .../container/ogg/OggAudioTrack.java | 70 +- .../container/ogg/OggCodecHandler.java | 8 +- .../container/ogg/OggContainerProbe.java | 66 +- .../lavaplayer/container/ogg/OggMetadata.java | 100 +-- .../container/ogg/OggPacketInputStream.java | 666 +++++++------- .../container/ogg/OggPageHeader.java | 120 +-- .../container/ogg/OggPageScanner.java | 210 ++--- .../container/ogg/OggSeekPoint.java | 80 +- .../container/ogg/OggStreamSizeInfo.java | 8 +- .../container/ogg/OggTrackBlueprint.java | 1 + .../container/ogg/OggTrackHandler.java | 39 +- .../container/ogg/OggTrackLoader.java | 90 +- .../ogg/flac/OggFlacCodecHandler.java | 151 ++-- .../ogg/flac/OggFlacTrackHandler.java | 113 +-- .../ogg/opus/OggOpusCodecHandler.java | 183 ++-- .../ogg/opus/OggOpusTrackHandler.java | 95 +- .../ogg/vorbis/OggVorbisCodecHandler.java | 158 ++-- .../ogg/vorbis/OggVorbisTrackHandler.java | 167 ++-- .../ogg/vorbis/VorbisCommentParser.java | 90 +- .../playlists/ExtendedM3uParser.java | 133 +-- .../container/playlists/HlsStreamSegment.java | 34 +- .../playlists/HlsStreamSegmentParser.java | 67 +- .../HlsStreamSegmentUrlProvider.java | 72 +- .../container/playlists/HlsStreamTrack.java | 56 +- .../playlists/M3uPlaylistContainerProbe.java | 148 ++-- .../PlainPlaylistContainerProbe.java | 60 +- .../playlists/PlsPlaylistContainerProbe.java | 86 +- .../container/wav/WavAudioTrack.java | 38 +- .../container/wav/WavContainerProbe.java | 62 +- .../lavaplayer/container/wav/WavFileInfo.java | 10 +- .../container/wav/WavFileLoader.java | 202 ++--- .../container/wav/WavTrackProvider.java | 214 ++--- .../lavaplayer/filter/AudioFilter.java | 36 +- .../lavaplayer/filter/AudioFilterChain.java | 46 +- .../lavaplayer/filter/AudioPipeline.java | 70 +- .../filter/AudioPipelineFactory.java | 86 +- .../lavaplayer/filter/AudioPostProcessor.java | 26 +- .../filter/BufferingPostProcessor.java | 68 +- .../filter/ChannelCountPcmAudioFilter.java | 228 ++--- .../filter/CompositeAudioFilter.java | 58 +- .../lavaplayer/filter/FilterChainBuilder.java | 123 +-- .../filter/FinalPcmAudioFilter.java | 242 +++--- .../filter/FloatPcmAudioFilter.java | 14 +- .../lavaplayer/filter/PcmFilterFactory.java | 26 +- .../discord/lavaplayer/filter/PcmFormat.java | 2 +- .../filter/ResamplingPcmAudioFilter.java | 118 +-- .../filter/ShortPcmAudioFilter.java | 24 +- .../filter/SplitShortPcmAudioFilter.java | 14 +- .../filter/UserProvidedAudioFilters.java | 134 +-- .../converter/ConverterAudioFilter.java | 32 +- .../filter/converter/ToFloatAudioFilter.java | 124 +-- .../filter/converter/ToShortAudioFilter.java | 94 +- .../converter/ToSplitShortAudioFilter.java | 110 +-- .../filter/equalizer/Equalizer.java | 262 +++--- .../equalizer/EqualizerConfiguration.java | 2 +- .../filter/equalizer/EqualizerFactory.java | 26 +- .../volume/AudioFrameVolumeChanger.java | 143 +-- .../filter/volume/PcmVolumeProcessor.java | 122 +-- .../filter/volume/VolumePostProcessor.java | 54 +- .../lavaplayer/format/AudioDataFormat.java | 180 ++-- .../format/AudioDataFormatTools.java | 36 +- .../format/AudioPlayerInputStream.java | 200 +++-- .../format/OpusAudioDataFormat.java | 92 +- .../format/Pcm16AudioDataFormat.java | 94 +- .../format/StandardAudioDataFormats.java | 40 +- .../format/transcoder/AudioChunkDecoder.java | 18 +- .../format/transcoder/AudioChunkEncoder.java | 28 +- .../format/transcoder/OpusChunkDecoder.java | 42 +- .../format/transcoder/OpusChunkEncoder.java | 80 +- .../format/transcoder/PcmChunkDecoder.java | 58 +- .../format/transcoder/PcmChunkEncoder.java | 80 +- .../natives/ConnectorNativeLibLoader.java | 24 +- .../lavaplayer/natives/aac/AacDecoder.java | 390 +++++---- .../natives/aac/AacDecoderLibrary.java | 25 +- .../lavaplayer/natives/mp3/Mp3Decoder.java | 452 +++++----- .../natives/mp3/Mp3DecoderLibrary.java | 18 +- .../lavaplayer/natives/opus/OpusDecoder.java | 179 ++-- .../natives/opus/OpusDecoderLibrary.java | 18 +- .../lavaplayer/natives/opus/OpusEncoder.java | 86 +- .../natives/opus/OpusEncoderLibrary.java | 20 +- .../samplerate/SampleRateConverter.java | 134 +-- .../natives/samplerate/SampleRateLibrary.java | 20 +- .../natives/statistics/CpuStatistics.java | 172 ++-- .../statistics/CpuStatisticsLibrary.java | 32 +- .../natives/vorbis/VorbisDecoder.java | 154 ++-- .../natives/vorbis/VorbisDecoderLibrary.java | 26 +- .../lavaplayer/player/AudioConfiguration.java | 160 ++-- .../player/AudioLoadResultHandler.java | 41 +- .../lavaplayer/player/AudioPlayer.java | 157 ++-- .../player/AudioPlayerLifecycleManager.java | 92 +- .../lavaplayer/player/AudioPlayerManager.java | 408 ++++----- .../lavaplayer/player/AudioPlayerOptions.java | 42 +- .../lavaplayer/player/DefaultAudioPlayer.java | 593 +++++++------ .../player/DefaultAudioPlayerManager.java | 815 +++++++++--------- .../player/FunctionalResultHandler.java | 84 +- .../lavaplayer/player/event/AudioEvent.java | 20 +- .../player/event/AudioEventAdapter.java | 124 +-- .../player/event/AudioEventListener.java | 8 +- .../player/event/PlayerPauseEvent.java | 12 +- .../player/event/PlayerResumeEvent.java | 12 +- .../player/event/TrackEndEvent.java | 36 +- .../player/event/TrackExceptionEvent.java | 36 +- .../player/event/TrackStartEvent.java | 24 +- .../player/event/TrackStuckEvent.java | 40 +- .../player/hook/AudioOutputHook.java | 12 +- .../lavaplayer/source/AudioSourceManager.java | 100 +-- .../source/AudioSourceManagers.java | 82 +- .../source/ProbingAudioSourceManager.java | 72 +- .../bandcamp/BandcampAudioSourceManager.java | 331 ++++--- .../source/bandcamp/BandcampAudioTrack.java | 76 +- .../source/beam/BeamAudioSourceManager.java | 211 +++-- .../source/beam/BeamAudioTrack.java | 79 +- .../source/beam/BeamSegmentUrlProvider.java | 76 +- .../getyarn/GetyarnAudioSourceManager.java | 185 ++-- .../source/getyarn/GetyarnAudioTrack.java | 55 +- .../source/http/HttpAudioSourceManager.java | 235 +++-- .../source/http/HttpAudioTrack.java | 72 +- .../source/local/LocalAudioSourceManager.java | 122 ++- .../source/local/LocalAudioTrack.java | 84 +- .../local/LocalSeekableInputStream.java | 172 ++-- .../source/nico/NicoAudioSourceManager.java | 279 +++--- .../source/nico/NicoAudioTrack.java | 98 +-- .../DefaultSoundCloudDataLoader.java | 45 +- .../DefaultSoundCloudDataReader.java | 155 ++-- .../DefaultSoundCloudFormatHandler.java | 114 +-- .../DefaultSoundCloudPlaylistLoader.java | 254 +++--- .../DefaultSoundCloudTrackFormat.java | 52 +- .../SoundCloudAudioSourceManager.java | 690 ++++++++------- .../soundcloud/SoundCloudAudioTrack.java | 125 +-- .../soundcloud/SoundCloudClientIdTracker.java | 205 ++--- .../soundcloud/SoundCloudDataLoader.java | 3 +- .../soundcloud/SoundCloudDataReader.java | 19 +- .../soundcloud/SoundCloudFormatHandler.java | 8 +- .../source/soundcloud/SoundCloudHelper.java | 82 +- .../SoundCloudHttpContextFilter.java | 105 +-- .../soundcloud/SoundCloudM3uAudioTrack.java | 253 +++--- .../source/soundcloud/SoundCloudM3uInfo.java | 14 +- .../SoundCloudMp3SegmentDecoder.java | 75 +- .../SoundCloudOpusSegmentDecoder.java | 103 +-- .../soundcloud/SoundCloudPlaylistLoader.java | 11 +- .../soundcloud/SoundCloudSegmentDecoder.java | 23 +- .../soundcloud/SoundCloudTrackFormat.java | 8 +- .../source/stream/M3uStreamAudioTrack.java | 38 +- .../stream/M3uStreamSegmentUrlProvider.java | 364 ++++---- .../stream/MpegTsM3uStreamAudioTrack.java | 24 +- .../TwitchStreamAudioSourceManager.java | 343 ++++---- .../source/twitch/TwitchStreamAudioTrack.java | 79 +- .../TwitchStreamSegmentUrlProvider.java | 198 ++--- .../source/vimeo/VimeoAudioSourceManager.java | 197 +++-- .../source/vimeo/VimeoAudioTrack.java | 108 +-- .../yamusic/AbstractYandexMusicApiLoader.java | 74 +- .../DefaultYandexMusicDirectUrlLoader.java | 91 +- .../DefaultYandexMusicPlaylistLoader.java | 174 ++-- .../DefaultYandexMusicTrackLoader.java | 22 +- .../yamusic/DefaultYandexSearchProvider.java | 180 ++-- .../yamusic/YandexHttpContextFilter.java | 68 +- .../source/yamusic/YandexMusicApiLoader.java | 5 +- .../YandexMusicAudioSourceManager.java | 360 ++++---- .../source/yamusic/YandexMusicAudioTrack.java | 60 +- .../yamusic/YandexMusicPlaylistLoader.java | 5 +- .../YandexMusicSearchResultLoader.java | 6 +- .../yamusic/YandexMusicTrackLoader.java | 2 +- .../source/yamusic/YandexMusicUtils.java | 91 +- .../youtube/DefaultYoutubeLinkRouter.java | 261 +++--- .../youtube/DefaultYoutubePlaylistLoader.java | 258 +++--- .../youtube/DefaultYoutubeTrackDetails.java | 232 +++-- .../DefaultYoutubeTrackDetailsLoader.java | 500 +++++------ .../youtube/YoutubeAccessTokenTracker.java | 796 +++++++++-------- .../youtube/YoutubeAudioSourceManager.java | 530 ++++++------ .../source/youtube/YoutubeAudioTrack.java | 211 ++--- .../youtube/YoutubeCipherOperation.java | 2 +- .../source/youtube/YoutubeClientConfig.java | 312 ++++--- .../source/youtube/YoutubeFormatInfo.java | 85 +- .../youtube/YoutubeHttpContextFilter.java | 158 ++-- .../source/youtube/YoutubeMixLoader.java | 13 +- .../source/youtube/YoutubeMixProvider.java | 160 ++-- .../youtube/YoutubeMpegStreamAudioTrack.java | 378 ++++---- .../source/youtube/YoutubePayloadHelper.java | 16 +- .../youtube/YoutubePersistentHttpStream.java | 204 ++--- .../source/youtube/YoutubePlaylistLoader.java | 7 +- .../youtube/YoutubeSearchMusicProvider.java | 250 +++--- .../YoutubeSearchMusicResultLoader.java | 3 +- .../source/youtube/YoutubeSearchProvider.java | 158 ++-- .../youtube/YoutubeSearchResultLoader.java | 5 +- .../youtube/YoutubeSignatureCipher.java | 144 ++-- .../YoutubeSignatureCipherManager.java | 452 +++++----- .../youtube/YoutubeSignatureResolver.java | 6 +- .../source/youtube/YoutubeTrackDetails.java | 7 +- .../youtube/YoutubeTrackDetailsLoader.java | 2 +- .../source/youtube/YoutubeTrackFormat.java | 210 ++--- .../source/youtube/YoutubeTrackJsonData.java | 126 +-- .../LegacyAdaptiveFormatsExtractor.java | 59 +- .../format/LegacyDashMpdFormatsExtractor.java | 119 +-- .../LegacyStreamMapFormatsExtractor.java | 117 +-- .../OfflineYoutubeTrackFormatExtractor.java | 19 +- .../format/StreamingDataFormatsExtractor.java | 141 +-- .../format/YoutubeTrackFormatExtractor.java | 13 +- .../tools/CopyOnUpdateIdentityList.java | 42 +- .../lavaplayer/tools/DataFormatTools.java | 324 +++---- .../lavaplayer/tools/DecodedException.java | 4 +- .../lavaplayer/tools/ExceptionTools.java | 472 +++++----- .../lavaplayer/tools/FriendlyException.java | 60 +- .../discord/lavaplayer/tools/FutureTools.java | 64 +- .../tools/GarbageCollectionMonitor.java | 189 ++-- .../discord/lavaplayer/tools/JsonBrowser.java | 504 +++++------ .../lavaplayer/tools/OrderedExecutor.java | 166 ++-- .../lavaplayer/tools/PlayerLibrary.java | 30 +- .../lavaplayer/tools/RingBufferMath.java | 72 +- .../discord/lavaplayer/tools/Units.java | 32 +- .../tools/http/AbstractHttpContextFilter.java | 64 +- .../http/ExtendedConnectionOperator.java | 498 ++++++----- .../tools/http/ExtendedHttpClientBuilder.java | 312 +++---- .../tools/http/ExtendedHttpConfigurable.java | 2 +- .../tools/http/HttpContextFilter.java | 10 +- .../tools/http/HttpContextRetryCounter.java | 50 +- .../tools/http/HttpStreamTools.java | 41 +- .../tools/http/MultiHttpConfigurable.java | 43 +- .../tools/http/SettableHttpRequestFilter.java | 82 +- .../SimpleHttpClientConnectionManager.java | 201 +++-- .../io/AbstractHttpInterfaceManager.java | 123 +-- .../lavaplayer/tools/io/BitBufferReader.java | 52 +- .../lavaplayer/tools/io/BitStreamReader.java | 218 ++--- .../lavaplayer/tools/io/BitStreamWriter.java | 97 +-- .../tools/io/ByteBufferInputStream.java | 54 +- .../tools/io/ByteBufferOutputStream.java | 30 +- .../tools/io/ChainedInputStream.java | 158 ++-- .../tools/io/DetachedByteChannel.java | 46 +- .../tools/io/DirectBufferStreamBroker.java | 188 ++-- .../lavaplayer/tools/io/EmptyInputStream.java | 18 +- .../tools/io/ExtendedBufferedInputStream.java | 52 +- .../tools/io/GreedyInputStream.java | 68 +- .../lavaplayer/tools/io/HttpClientTools.java | 509 ++++++----- .../lavaplayer/tools/io/HttpConfigurable.java | 16 +- .../lavaplayer/tools/io/HttpInterface.java | 223 ++--- .../tools/io/HttpInterfaceManager.java | 9 +- .../lavaplayer/tools/io/MessageInput.java | 89 +- .../lavaplayer/tools/io/MessageOutput.java | 97 ++- .../tools/io/NonSeekableInputStream.java | 82 +- .../tools/io/PersistentHttpStream.java | 480 +++++------ .../io/ResettableBoundedInputStream.java | 133 +-- .../io/SavedHeadSeekableInputStream.java | 239 ++--- .../tools/io/SeekableInputStream.java | 170 ++-- .../tools/io/SimpleHttpInterfaceManager.java | 38 +- .../lavaplayer/tools/io/StreamTools.java | 44 +- .../io/ThreadLocalHttpInterfaceManager.java | 70 +- .../tools/io/TrustManagerBuilder.java | 186 ++-- .../lavaplayer/track/AudioPlaylist.java | 32 +- .../lavaplayer/track/AudioReference.java | 114 +-- .../discord/lavaplayer/track/AudioTrack.java | 126 +-- .../lavaplayer/track/AudioTrackEndReason.java | 64 +- .../lavaplayer/track/AudioTrackInfo.java | 128 +-- .../lavaplayer/track/AudioTrackState.java | 12 +- .../lavaplayer/track/BaseAudioTrack.java | 302 +++---- .../lavaplayer/track/BasicAudioPlaylist.java | 64 +- .../lavaplayer/track/DecodedTrackHolder.java | 22 +- .../lavaplayer/track/DelegatedAudioTrack.java | 86 +- .../lavaplayer/track/InternalAudioTrack.java | 43 +- .../discord/lavaplayer/track/TrackMarker.java | 34 +- .../lavaplayer/track/TrackMarkerHandler.java | 70 +- .../lavaplayer/track/TrackMarkerTracker.java | 115 +-- .../lavaplayer/track/TrackStateListener.java | 32 +- .../track/info/AudioTrackInfoBuilder.java | 314 +++---- .../track/info/AudioTrackInfoProvider.java | 44 +- .../playback/AbstractAudioFrameBuffer.java | 110 +-- .../playback/AbstractMutableAudioFrame.java | 94 +- .../playback/AllocatingAudioFrameBuffer.java | 316 +++---- .../lavaplayer/track/playback/AudioFrame.java | 78 +- .../track/playback/AudioFrameBuffer.java | 107 +-- .../playback/AudioFrameBufferFactory.java | 16 +- .../track/playback/AudioFrameConsumer.java | 24 +- .../track/playback/AudioFrameProvider.java | 56 +- .../playback/AudioFrameProviderTools.java | 22 +- .../track/playback/AudioFrameRebuilder.java | 13 +- .../playback/AudioProcessingContext.java | 68 +- .../track/playback/AudioTrackExecutor.java | 87 +- .../track/playback/ImmutableAudioFrame.java | 118 +-- .../playback/LocalAudioTrackExecutor.java | 773 +++++++++-------- .../track/playback/MutableAudioFrame.java | 86 +- .../NonAllocatingAudioFrameBuffer.java | 486 +++++------ .../PrimordialAudioTrackExecutor.java | 181 ++-- .../playback/ReferenceMutableAudioFrame.java | 104 +-- .../track/playback/TerminatorAudioFrame.java | 72 +- natives/build.gradle | 384 +++++---- natives/natives.gradle | 152 ++-- settings.gradle.kts | 16 +- 413 files changed, 26606 insertions(+), 26054 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b79c00ad1..b93a4af15 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,59 +1,59 @@ name: Publish on: - push: - branches: [ '**' ] - paths-ignore: [ '**.md' ] - workflow_call: - secrets: - MAVEN_USERNAME: - required: false - MAVEN_PASSWORD: - required: false - ORG_GRADLE_PROJECT_mavenCentralPassword: - required: false - ORG_GRADLE_PROJECT_mavenCentralUsername: - required: false - ORG_GRADLE_PROJECT_signingInMemoryKey: - required: false - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: - required: false + push: + branches: [ '**' ] + paths-ignore: [ '**.md' ] + workflow_call: + secrets: + MAVEN_USERNAME: + required: false + MAVEN_PASSWORD: + required: false + ORG_GRADLE_PROJECT_mavenCentralPassword: + required: false + ORG_GRADLE_PROJECT_mavenCentralUsername: + required: false + ORG_GRADLE_PROJECT_signingInMemoryKey: + required: false + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: + required: false jobs: - build: - runs-on: ubuntu-latest - env: - MAVEN_USERNAME: ${{ vars.MAVEN_USERNAME }} - MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} - ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }} - ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }} - ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEY }} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEYPASSWORD }} - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 + build: + runs-on: ubuntu-latest + env: + MAVEN_USERNAME: ${{ vars.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEYPASSWORD }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 - - name: Setup Java - uses: actions/setup-java@v3 - with: - distribution: zulu - java-version: 11 - cache: gradle + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: zulu + java-version: 11 + cache: gradle - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 - - name: Build and Publish - run: ./gradlew build publish --no-daemon -PMAVEN_USERNAME=$MAVEN_USERNAME -PMAVEN_PASSWORD=$MAVEN_PASSWORD + - name: Build and Publish + run: ./gradlew build publish --no-daemon -PMAVEN_USERNAME=$MAVEN_USERNAME -PMAVEN_PASSWORD=$MAVEN_PASSWORD - - name: Upload Artifacts - uses: actions/upload-artifact@v3 - with: - name: Lavaplayer.zip - path: | - main/build/libs/lavaplayer-*.jar - common/build/libs/lava-common-*.jar - extensions/format-xm/build/libs/lavaplayer-ext-format-xm-*.jar - extensions/youtube-rotator/build/libs/lavaplayer-ext-youtube-rotator-*.jar - natives-publish/build/libs/lavaplayer-natives-*.jar + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + with: + name: Lavaplayer.zip + path: | + main/build/libs/lavaplayer-*.jar + common/build/libs/lava-common-*.jar + extensions/format-xm/build/libs/lavaplayer-ext-format-xm-*.jar + extensions/youtube-rotator/build/libs/lavaplayer-ext-youtube-rotator-*.jar + natives-publish/build/libs/lavaplayer-natives-*.jar diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 25196513a..5ecfa64ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,43 +1,43 @@ name: Release on: - release: - types: [published] + release: + types: [ published ] jobs: - build: - uses: ./.github/workflows/build.yml - secrets: - MAVEN_USERNAME: ${{ vars.MAVEN_USERNAME }} - MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} - ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }} - ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }} - ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEY }} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEYPASSWORD }} + build: + uses: ./.github/workflows/build.yml + secrets: + MAVEN_USERNAME: ${{ vars.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEYPASSWORD }} - release: - needs: build - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 + release: + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 - - name: Download Artifacts - uses: actions/download-artifact@v3 - with: - name: Lavaplayer.zip + - name: Download Artifacts + uses: actions/download-artifact@v3 + with: + name: Lavaplayer.zip - - name: Upload Artifacts to GitHub Release - uses: ncipollo/release-action@v1 - with: - artifacts: | - main/build/libs/lavaplayer-*.jar - common/build/libs/lava-common-*.jar - extensions/format-xm/build/libs/lavaplayer-ext-format-xm-*.jar - extensions/youtube-rotator/build/libs/lavaplayer-ext-youtube-rotator-*.jar - natives-publish/build/libs/lavaplayer-natives-*.jar - allowUpdates: true - omitBodyDuringUpdate: true - omitDraftDuringUpdate: true - omitNameDuringUpdate: true - omitPrereleaseDuringUpdate: true + - name: Upload Artifacts to GitHub Release + uses: ncipollo/release-action@v1 + with: + artifacts: | + main/build/libs/lavaplayer-*.jar + common/build/libs/lava-common-*.jar + extensions/format-xm/build/libs/lavaplayer-ext-format-xm-*.jar + extensions/youtube-rotator/build/libs/lavaplayer-ext-youtube-rotator-*.jar + natives-publish/build/libs/lavaplayer-natives-*.jar + allowUpdates: true + omitBodyDuringUpdate: true + omitDraftDuringUpdate: true + omitNameDuringUpdate: true + omitPrereleaseDuringUpdate: true diff --git a/CHANGELOG.md b/CHANGELOG.md index da53dcf6c..faeaa9593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,20 @@ # Change Log ## [2.0.0] -- 2023-03-08 + ### Fixed + - Fixed YouTube 403 errors in https://github.com/lavalink-devs/lavaplayer/pull/16 - Fixed YouTube access token issue in https://github.com/lavalink-devs/lavaplayer/pull/15 ### Removed + - Removed `stream-merger` module - Removed `node` module & classes in `main` module - Removed `test-samples` module ### Updated + - Updated `commons-io:commons-io` to `2.13.0` - Updated `org.apache.httpcomponents:httpclient` to `4.5.14` - Updated `com.fasterxml.jackson.core:jackson-core` to `2.15.2` @@ -23,204 +27,293 @@ - Updated `org.jsoup:jsoup` to `1.16.1` ## Added + - Added `AudioPlayerManager#loadItemSync` to allow loading tracks synchronously - Added `natives` & `natives-publish` modules for lavaplayer native libraries - Added support soundcloud short urls for in https://github.com/lavalink-devs/lavaplayer/pull/4 ## [1.3.50] -- 2020-06-17 + ### Fixed + - Fixed YT search sometimes not finding anything because of YT providing different format (PR 492 by Frederikam). ## [1.3.49] -- 2020-05-21 + ### Fixed + - Fixed Twitch streaming breaking due to API change (PR 480 by Devoxin). - Fixed YT private videos providing invalid result instead of failing (PR 479 by Devoxin). ## [1.3.48] -- 2020-05-12 + ### Fixed + - Fixed some YouTube tracks not working because of cipher detection issues (PR 475 by Devoxin). ## [1.3.47] -- 2020-04-13 + ### Fixed + - Fixed loading YouTube live streams within playlists (PR 294 by Devoxin). ## [1.3.46] -- 2020-04-07 + ### Fixed + - Fixed YouTube mix loading, mixes loaded with just one request now (PR 293 by Devoxin). ## [1.3.45] -- 2020-04-07 + ### Fixed + - Fixed some YouTube live streams not being loaded and randomly stopping (PR 291 by Devoxin). ## [1.3.44] -- 2020-04-04 + ### Fixed + - Fixed failing to load SoundCloud playlists with more than 50 tracks (PR 290 by Devoxin). ## [1.3.43] -- 2020-04-02 + ### Fixed + - Fixed SoundCloud connection leaks which eventually caused pool limit to be reached and new connections getting stuck. ## [1.3.42] -- 2020-04-01 + ### Added + - Added `AbstractHttpContextFilter` to easily create additional HTTP context filters with delegates. ## [1.3.41] -- 2020-04-01 + ### Added + - Added support for GetYarn.io as a source (PR 277 by duncte123). ### Fixed + - Fixed audio event adapter method with stuck stacktrace included not getting called. ## [1.3.40] -- 2020-03-28 + ### Added + - Added `SimpleHttpClientConnectionManager` to sources which can be used to disable pooling for HTTP of some source. ## [1.3.39] -- 2020-03-27 + ### Added + - Added method to set Apache HTTP connection manager factory to customize connection handling. ### Changed + - Track stuck events now provide stacktrace of the struck thread. ## [1.3.38] -- 2020-03-23 + ### Fixed + - Fixed unable to fetch YouTube player script for ciphered URLs. ## [1.3.37] -- 2020-03-23 + ### Changed + - Unexpected response code from YouTube player script request now logs more details. ## [1.3.36] -- 2020-03-22 + ### Added + - Added method to provide a set of IPs to be used for making an HTTP connection without choosing a specific one. ### Changed + - HTTP connection failure exceptions now contain all details about the connection that was being established. ## [1.3.35] -- 2020-03-09 + ### Fixed + - Fixed NPE for unavailable SoundCloud tracks (PR 276 by duncte123). ## [1.3.34] -- 2020-01-27 + ### Fixed + - Fixed YouTube missing fallback to embed page for "objectionable content" videos (PR 270 by Xavinlol). ## [1.3.33] -- 2020-01-05 + ### Fixed + - Fixed NPE instead of common exception for missing YouTube video (PR 264 by duncte123). - Fixed blocked tracks included in SoundCloud liked tracks result (PR 261 by nikammerlaan). ### Added + - Added builder for SoundCloud source manager. - Added a more concise way to create an extended media container registry. ## [1.3.32] -- 2019-12-01 + ### Fixed + - Fixed SoundCloud tracks longer than 30 minutes breaking during playback due to URLs expiring. ## [1.3.31] -- 2019-12-01 + ### Fixed + - Fixed redundant retry attempts for SoundCloud liked tracks and search result pages. ## [1.3.30] -- 2019-12-01 + ### Changed + - SoundCloud source manager is now modular and more easily extendable. ### Fixed + - Fixed non-opus tracks no longer working due to old stream API not being available anymore. - Fixed SoundCloud opus seeking not working when applied before track starts. ## [1.3.29] -- 2019-11-25 + ### Fixed + - Fixed SoundCloud opus seeking not working. ## [1.3.28] -- 2019-11-25 + ### Fixed + - Fixed response code 203 treated as an error everywhere. ## [1.3.27] -- 2019-11-19 + ### Fixed + - Fixed SoundCloud track loading failing if no opus track is available. ## [1.3.26] -- 2019-11-19 + ### Added + - Support for SoundCloud opus streams by default, old MP3 streams only used if opus is not available. ## [1.3.25] -- 2019-11-06 + ### Changed + - Manipulation of HTTP requests and triggering retries with HTTP context filter instead of a simple request modifier. ## [1.3.24] -- 2019-11-06 + ### Added + - HTTP request modifier interface which allows modifying outgoing requests. ## [1.3.23] -- 2019-10-28 + ### Changed + - Use YouTube pbj=1 requests for tracks and playlists. ## [1.3.22] -- 2019-09-11 + ### Fixed -- Fixed new track formats breaking YouTube track loading (PR 210 by Ferderikam) + +- Fixed new track formats breaking YouTube track loading (PR 210 by Ferderikam) ## [1.3.21] -- 2019-09-09 + ### Fixed + - Fixed playing direct SoundCloud tracks which broke due to site change (PR 208 by Devoxin). - Fixed some cases where YouTube tracks broke because of duplicate parameters (PR 180 by Joakim). - Fixed new format YouTube pages where no cipher was used (PR 205 by Devoxin). ## [1.3.20] -- 2019-08-08 + ### Fixed + - Fixed playback for YouTube when format list is only provided through player_response. ## [1.3.19] -- 2019-07-30 + ### Fixed + - Fixed broken YouTube track loading due to changes the site (PR 199 by Frederikam). ## [1.3.18] -- 2019-07-19 + ### Changed + - YouTube track info JSON now gets logged if it does not contain any expected fields. ### Fixed + - Fixed closing MKV Opus track throwing an exception if playback initialization had thrown an exception earlier. - Fixed exception from track close reported instead of playback exception on MKV close. - Fixed passing an invalid parameter to YouTube embed page URL. ## [1.3.17] -- 2019-04-26 + ### Fixed + - Fixed age-restricted videos not being playable (PR 172 by Devoxin). ## [1.3.16] -- 2019-04-02 + ### Fixed -- Fixed probe hint matching logic for aac/ts causing HTTP source to not resolve tracks when mime type was present. + +- Fixed probe hint matching logic for aac/ts causing HTTP source to not resolve tracks when mime type was present. ## [1.3.15] -- 2019-04-01 + ### Fixed + - Fixed no automatic reconnect on connection reset for some JDK versions. ## [1.3.14] -- 2019-03-28 + ### Fixed + - Fixed MKV frame size parsing which caused some MKV/WEBM files to fail in the middle of playback. ## [1.3.13] -- 2019-03-21 + ### Fixed + - Fixed an old x86 Linux native being bundled with main LP artifact, causing previous libc conflict to still manifest. ## [1.3.12] -- 2019-03-20 + ### Fixed + - Fixed natives crashing due to libc conflict. - Fixed exception when calling provide on an ending track. ## [1.3.11] -- 2019-02-28 + ### Added + - Support for playing MPEG-TS files directly, including metadata support. - Metadata for OGG vorbis files. ### Changed + - AAC and MP3 decoders updated to newer versions. - Linux native libraries have better compatibility with alternate libc implementations (musl on Alpine for example) - More flexibility for specifying which native libraries and from where to load. ### Fixed + - Fixed file format detected incorrectly due to check order when file extension is known. - Fixed exception when processing the last frame of some FLAC files. - Fixed YouTube video signature using wrong parameter name in some cases. @@ -229,775 +322,1113 @@ - Fixed a race condition which could sometimes drop the last frame of a track. ## [1.3.10] -- 2018-10-24 + ### Added + - Generic HLS stream support. - Ability to add custom file format support (as custom MediaContainerRegistry). ### Fixed + - Fixed YouTube streams not reconnecting due to SSL exception wrapper changes in JDK10+. ## [1.3.9] -- 2018-10-18 + ### Fixed + - Fixed playing age-restricted videos using the embed page (PR 136 by duncte123). ## [1.3.8] -- 2018-10-18 + ### Added + - Added support for YouTube watch_videos urls (PR 123 by matsprehn). - Added support for YouTube music.youtube.com domain links (PR 134 by GigaFyde). ### Fixed + - Fixed Mixer streams. - Fixed Twitch streams by using the Helix API (PR 132 by tdeeb). - Fixed OGG metadata memory usage exploit (PR 131 by napstr). ## [1.3.7] -- 2018-07-02 + ### Fixed + - Fixed YouTube search not working in some regions due to different time format. ## [1.3.6] -- 2018-06-19 + ### Changed + - Made it easier to use custom AudioPlayer implementations. ## [1.3.5] -- 2018-06-13 + ### Fixed + - Fixed volume formula not scaling well beyond 150 - now switches to linear mode after 150. ## [1.3.4] -- 2018-06-12 + ### Changed + - Maximum volume setting set to 1000. ### Fixed + - Fixed AAC+SBR+PS not working properly for MP4/MKV containers. ## [1.3.3] -- 2018-06-05 + ### Added + - Ability to write more custom audio players as the player is now an interface. - HTTP stream reconnect when using Conscrypt as SSL provider. ## [1.3.2] -- 2018-06-03 + ### Fixed + - Fixed mutable audio frames and non-allocating buffer exploding in some use cases. ## [1.3.1] -- 2018-06-03 + ### Fixed + - Fixed PCM output format regression. ## [1.3.0] -- 2018-06-02 + ### Added -- Option for allocation-free frame provision pipeline by allowing to set NonAllocatingAudioFrameBuffer as the default frame buffer implementation and then using the new provide methods that take mutable audio frame instances. + +- Option for allocation-free frame provision pipeline by allowing to set NonAllocatingAudioFrameBuffer as the default + frame buffer implementation and then using the new provide methods that take mutable audio frame instances. - Ability to set SSL context for internally used HTTP clients. This allows for custom SSL providers such as Conscrypt. - Support for custom output formats using custom implementations of AudioDataFormat. ### Changed + - Audio frame is now an interface, which is a breaking change for all use cases. - Removed the concept of audio hooks. ### Fixed + - Some audio sources not freeing their HTTP clients when they were discarded. - Potential NPE when the only node went down when a track was playing on it. - Not closing OGG decoders created during track probing (still got closed on finalize). - Missing Javadocs. ## [1.2.64] -- 2018-06-01 + ### Fixed + - Fixed WAV files with format header longer than 16 bytes not working. ## [1.2.63] -- 2018-04-12 + ### Fixed + - Fixed Twitch or Mixer tracks ending when next chunk of the stream is not available yet. - Fixed non-mono inputs being distorted or passing the wrong number of channels to custom filters. - Fixed loading a mix on a non-existing track throwing an exception instead of triggering no matches. ### Added + - Ogg FLAC and Ogg Opus metadata (title and artist). - Track length detection for Ogg Opus and Ogg FLAC on a seekable stream. - Metadata detection from ShoutCast stream headers. ## [1.2.62] -- 2018-04-02 + ### Added + - Method to set track frame buffer duration per player. ## [1.2.61] -- 2018-04-02 + ### Fixed + - Fixed HTTP 403 for some YouTube tracks due to adding one parameter to the playback URL twice. ## [1.2.60] -- 2018-04-02 + ### Added + - Equalizer filter and its filter factory to LavaPlayer classes (still need to be applied manually). ## [1.2.59] -- 2018-03-30 + ### Added + - Ability to add a custom audio filter factory to an instance of an audio player. ## [1.2.58] - 2018-03-26 + ### Fixed + - Fixed Twitch stream loading regression. ## [1.2.57] - 2018-03-26 + ### Fixed + - Fixed YouTube mix loading regression. ## [1.2.56] - 2018-02-28 + ### Added + - Added method to get Twitch track channel name. ### Fixed + - Fixed NPE when YouTube URL has parameters without values. ## [1.2.55] - 2018-02-27 + ### Fixed + - Fixed Windows native libraries being mixed up. ## [1.2.54] - 2018-02-23 + ### Changed + - Attempt to retry connection in case of socket read timeout. ### Fixed + - Fixed mobile SoundCloud links not working. - Fixed 32-bit Windows natives being incompatible. ## [1.2.53] - 2018-02-21 + ### Changed + - YouTube search result loader performs HTTP requests with cookies disabled by default now. ## [1.2.52] - 2018-02-20 + ### Fixed + - Fixed YouTube URLs with extra characters after video ID not working even though YouTube accepts them. - Fixed NPE when video ID is not in valid format, now triggers `noMatches` instead. - Fixed missing YouTube video triggering `loadFailed` instead of `noMatches`. ## [1.2.51] - 2018-02-15 + ### Fixed + - Fixed attempting to load a native library that does not exist since last version on Windows x86. - Fixed all consecutive HTTP connections failing after the HTTP of a source manager is reconfigured. ## [1.2.50] - 2018-02-15 + ### Fixed + - Fixed ADTS/AAC streams with SBR and/or PS enabled (also known as AAC+ streams). - Fixed ADTS/AAC streams with MPEG version 2 refused. - Fixed MPEG2 MP3 file frames read incorrectly, causing artifacts and spam in standard error stream. ## [1.2.49] - 2018-02-12 + ### Added + - Methods to allow reconfiguring HTTP builders used by source managers. ## [1.2.48] - 2018-02-11 + ### Added + - Method to change YouTube mix loader thread pool size. ## [1.2.47] - 2018-02-07 + ### Fixed + - Fixed YouTube ciphers being broken due to slight change to YouTube player script. ## [1.2.46] - 2018-02-04 + ### Changed + - All dependencies updated to latest versions (solves some issues with mismatching versions of Jackson). ## [1.2.45] - 2017-11-25 + ### Added + - Support for twitch streams with domain `go.twitch.tv`. - Support for `m.soundcloud.com` domain for Soundcloud URLs. ### Fixed + - Fixed Twitch stream segment URL resolution occasionally producing incorrect URLs. - Fixed YouTube links not working when they contain extra parameters. ## [1.2.44] - 2017-10-08 + ### Added -- Local tracks can now be encoded. + +- Local tracks can now be encoded. ### Fixed + - Fixed Bandcamp source being broken due to URL format change. ## [1.2.43] - 2017-08-15 + ### Added + - Support for `mixer.com` (new `beam.pro` domain name). ### Fixed + - Fixed YouTube search being broken due to page style change. ## [1.2.42] - 2017-06-17 + ### Changed + - Improved efficiency of `.mkv`/`.webm` processing by making it practically garbage-free. ## [1.2.41] - 2017-06-14 + ### Fixed + - Fixed NPE thrown when an MPEG stream ends gracefully. ## [1.2.40] - 2017-06-05 + ### Fixed + - Fixed dual channel mono MP3 files not working. ## [1.2.39] - 2017-05-18 + ### Fixed + - Properly fixed the YouTube cipher detection (previous one missed some operations). ## [1.2.38] - 2017-05-18 + ### Fixed + - Partially fixed YouTube cipher detection where reserved keywords are function names. ## [1.2.37] - 2017-05-18 + ### Added + - Ability to store one user data object in audio track instances. - Dumping YouTube scripts where cipher detection does not work. ## [1.2.36] - 2017-04-09 + ### Fixed + - Fixed YouTube tracks with only mixed audio-video MP4. ## [1.2.35] - 2017-04-08 + ### Changed + - Twitch stream title is now stream status rather than channel name. ## [1.2.34] - 2017-03-22 + ### Fixed + - Fixed MP3 not working with local files. ## [1.2.33] - 2017-03-22 + ### Fixed + - Fixed only MP3 working with local files. ## [1.2.32] - 2017-03-21 + ### Fixed + - Fixed deadlock when starting tracks from exception event ## [1.2.31] - 2017-03-19 + ### Fixed + - Fixed seeking while track is initialising possibly interrupting load. ## [1.2.30] - 2017-03-12 + ### Fixed + - Fixed stopped track threads sometimes not exiting. ## [1.2.29] - 2017-03-08 + ### Added + - Added track URL field to track info object. ## [1.2.28] - 2017-03-06 + ### Fixed + - Fixed SoundCloud client ID not updated if expiration was discovered on a request for search or playlist. ## [1.2.27] - 2017-03-06 + ### Fixed + - Fixed SoundCloud not working due to client ID change. ### Changed + - SoundCloud client ID now fetched from their page now, not hardcoded. ## [1.2.26] - 2017-03-05 + ### Added + - Support for mono MP3 format. - PlayerLibrary which exposes the library version. ## [1.2.25] - 2017-03-04 + ### Fixed + - Fixed using nodes not working after node list is cleared once. ## [1.2.24] - 2017-03-04 + ### Fixed + - Fixed read timeout exception thrown when playing some YouTube live streams. - Fixed excessive amount of requests made for YouTube live streams. ## [1.2.23] - 2017-03-02 + ### Added + - Added a way to configure HTTP request settings for source managers. ### Fixed + - Changing volume with PCM output rebuilds frame buffer into silence. ## [1.2.22] - 2017-03-01 + ### Added + - Added NicoNico source manager. ## [1.2.21] - 2017-02-26 + ### Fixed + - Fixed exploding on seekable YouTube live streams. - Fixed using an unnecessary high track start message version when communicating with node. ## [1.2.20] - 2017-02-26 + ### Fixed + - Fixed a potential issue with YouTube ciphers not being detected correctly (403 error) and added more cipher logging. ## [1.2.19] - 2017-02-26 + ### Fixed + - Fixed some reconnect cases for HTTP connections not actually working. - Fixed inaccurate seek on resampled streams. ## [1.2.18] - 2017-02-26 + ### Fixed + - Retry connection also on premature HTTP response end exception. ## [1.2.17] - 2017-02-26 + ### Changed + - Abandoned tracks are taken over by other nodes more quickly. ## [1.2.16] - 2017-02-26 + ### Changed + - When remote nodes go offline, their tracks are slowly taken over by other nodes. ### Fixed + - Fixed Beam.pro tracks not playable in nodes. - Fixed seeking on tracks which had not been started yet. ## [1.2.15] - 2017-02-25 + ### Fixed + - Fixed a possible crash when a Vorbis track was shut down before being successfully initialised. ## [1.2.14] - 2017-02-24 + ### Fixed + - Retry stream connection on NoHttpResponseException, which may be caused by connection reuse. - Retry stream connection on SSL errors, which may be caused by severed connection. - Retry stream connection once on 500 series errors, due to possibly flaky server side issues. - Fixed loadFailed being called for exceptions in other callbacks. ### Changed + - Improved logging to know causes of all failed requests and changes some inappropriate logging levels. ## [1.2.13] - 2017-02-22 + ### Fixed + - Fixed a regression with YouTube dashmpd-only tracks not working. ## [1.2.12] - 2017-02-18 + ### Added + - Support for alternative YouTube JSON format (adaptive_fmts -> url_encoded_fmt_stream_map). ## [1.2.11] - 2017-02-18 + ### Fixed + - Fixed interrupts from previous tracks may be carried over to executors of new tracks. ## [1.2.10] - 2017-02-11 + ### Added + - Beam.pro source manager. ## [1.2.9] - 2017-02-09 + ### Fixed + - Fixed reused clients restricting the number of concurrent connections to a very low value. - Fixed track stop or seek throwing an exception in some conditions due to InterruptedException getting wrapped. ## [1.2.8] - 2017-02-09 + ### Changed + - Track info loader queue full exception is passed to loadFailed instead of being thrown directly. ### Fixed + - Fixed using wrong version of lavaplayer-common dependency (again). ## [1.2.7] - 2017-02-09 + ### Fixed + - Fixed track loading queue throwing "queue full" exception when the queue is not empty. ## [1.2.6] - 2017-02-08 + ### Fixed + - Fixed using wrong version of lavaplayer-common dependency. ## [1.2.5] - 2017-02-08 + ### Added + - Added the option to increase the number of threads used to load tracks. ## Changed + - Reusing HTTP client objects as much as possible. - Limited the size of the track info loader queue. ## [1.2.4] - 2017-02-08 + ### Fixed + - Fixed YouTube live streams opening a new connection for each request. - Fixed no read timeout applied to requests to nodes. - Fixed track getting locked when seek gets stuck. - Fixed attempting to read the entire response on closing a stream mid-way. ## [1.2.3] - 2017-02-07 + ### Changed + - Increased timeout for terminating the tracks of a node if its processing is stuck. ## [1.2.2] - 2017-02-06 + ### Fixed + - Fixed Vorbis MKV tracks not working if the channel count was not specified in MKV audio section. ## [1.2.1] - 2017-02-05 + ### Added + - Partial support for YouTube live streams (only MP4). ## [1.2.0] - 2017-02-04 + ### Added + - Method to poll frames from AudioPlayer in a blocking way. - Support for different audio output formats. - Utility class for getting an AudioInputStream from AudioPlayer. ## [1.1.47] - 2017-02-01 + ### Changed + - Fixed another smaller leak with Vorbis. ## [1.1.46] - 2017-02-01 + ### Fixed + - Fixed a native memory leak with Vorbis tracks. ## [1.1.45] - 2017-01-28 + ### Changed + - ARM binaries are now loaded from natives/linux-arm and natives/linux-aarch64 directories instead of x86 ones. ## [1.1.44] - 2017-01-28 + ### Added + - WAV file support (16-bit PCM). ## [1.1.43] - 2017-01-28 + ### Added + - Loading unlisted SoundCloud tracks. - Searching on SoundCloud with scsearch: prefix. - Option to specify maximum number of YouTube playlist pages to load (was hardcoded to 6). ### Fixed + - Fixed SoundCloud playlist tracks in wrong order. - Fixed paid movies appearing in YouTube search results. - Fixed YouTube playlists with UU prefix not working. ## [1.1.42] - 2017-01-16 + ### Added + - Support for OS X (native library). ## [1.1.41] - 2017-01-15 + ### Fixed + - Fixed YouTube tracks broken when player.js URL was given without hostname. ## [1.1.40] - 2017-01-14 + ### Fixed + - Fixed constant delay on processing node messages. - Fixed making a new HTTP connection for each node request. ## [1.1.39] - 2017-01-14 + ### Changed + - Reduced playing track count effect on node balancing. ## [1.1.38] - 2017-01-09 + ### Changed + - Node balancing takes CPU and latency more seriously. - Node messaging changed, requires node update to 1.1.38. ## [1.1.37] - 2017-01-04 + ### Changed + - MKV file handling refactored to be more lightweight. ## [1.1.36] - 2017-01-01 + ### Fixed + - Fixed track marker and position reset when starting the track. ## [1.1.35] - 2016-12-31 + ### Fixed + - Fixed loading HTTP urls with local redirects. ## [1.1.34] - 2016-12-29 + ### Fixed + - YouTube search results no longer include ads. ## [1.1.33] - 2016-12-27 + ### Fixed + - Fixed a regression with loading native libraries on Windows. ## [1.1.32] - 2016-12-24 + ### Fixed + - Fixed an issue when JDA-NAS and LavaPlayer were used together with different classloaders. ## [1.1.31] - 2016-12-22 + ### Added + - Made node balancing weight data available. ### Changed + - Request time to nodes also used for balancing between nodes. ## [1.1.30] - 2016-12-19 + ### Fixed + - Fixed an issue with MP3 files sometimes being detected as ADTS streams. ## [1.1.29] - 2016-12-17 + ### Added + - Made various statistics about remote nodes available. ## [1.1.28] - 2016-12-13 + ### Changed + - Made requests to nodes tolerate higher latency without stuttering. ## [1.1.27] - 2016-12-12 + ### Changed + - Changing remote node list does not stop tracks on unaffected nodes. ## [1.1.26] - 2016-12-11 + ### Changed + - Dependency restructuring for compatibility with JDA-NAS. ## [1.1.25] - 2016-12-08 + ### Added + - Support for legacy MP3 ID3v2.2 tags. ### Fixed + - Fixed nonexistent YouTube tracks being tried as raw HTTP urls. ## [1.1.24] - 2016-12-08 + ### Added + - Title and artist information for MP4/M4A files. ## [1.1.23] - 2016-12-01 + ### Added + - Can check if playlist is a search result. ### Fixed + - Fixed SoundCloud not working because because of SoundCloud client ID expiring. ## [1.1.22] - 2016-11-19 + ### Added + - Option to disable YouTube searches. ## [1.1.21] - 2016-11-18 + ### Added + - Support YouTube searches by using "ytsearch: query" as identifier. ## [1.1.20] - 2016-11-18 + ### Added + - Made it simple to register all bundled sources through the methods of AudioSourceManagers. ## [1.1.19] - 2016-11-18 + ### Changed + - Allow redirects from raw HTTP urls to other source providers. ### Fixed + - Fixed an exception on the end of some MP4 tracks. ## [1.1.18] - 2016-11-18 + ### Added + - Added mayEndTrack field to end reasons. ### Changed + - Special end reason for failed track initialization. ## [1.1.17] - 2016-11-16 + ### Fixed + - Fixed an issue with resolving correct YouTube stream URL due to a bug in cipher detection. ## [1.1.16] - 2016-11-13 + ### Changed + - Lowered default resampling quality. ## [1.1.15] - 2016-11-12 + ### Fixed + - Fixed some tracks mistakenly detected as MP3 streams. ## [1.1.14] - 2016-11-11 + ### Added + - Allow icy:// urls, which are used in some radio stream playlists. ## [1.1.13] - 2016-11-11 + ### Fixed + - Fixed exception on OGG streams when they end. - SoundCloud fix + provider more resilient to site updates. ## [1.1.12] - 2016-11-11 + ### Added + - Track markers. ### Removed + - Loop feature. It can be done with markers. ## [1.1.11] - 2016-11-06 + ### Added + - SoundCloud liked tracks page loadable as a playlist. ## [1.1.10] - 2016-11-05 + ### Changed + - Common exception in case a playlist is private. ### Fixed + - Fixed YouTube track not loaded if it is part of a private playlist. ## [1.1.9] - 2016-11-05 + ### Added + - Twitch stream support. ## [1.1.8] - 2016-11-05 + ### Added + - Vimeo support. - Support for the more common non-fragmented format of MP4/M4A. ## [1.1.7] - 2016-11-03 + ### Added + - IDv2.3 tag support. ### Fixed + - Fixed exception on long ID3 tags on streams. ## [1.1.6] - 2016-11-03 + ### Added + - Plain text files with an URL loaded as radio stream playlist. ### Fixed + - Fixed SoundCloud not working due to site update. ## [1.1.5] - 2016-11-03 + ### Added + - Support for PLS playlists for radio streams. - Support for ICY protocol to fix some SHOUTcast streams. ## [1.1.4] - 2016-11-01 + ### Added + - Support for ADTS+AAC radio streams. ## [1.1.3] - 2016-10-31 + ### Added + - Bundled some LetsEncrypt SSL root certificate missing on some JDK installations. - Support for adding custom SSL certificates. ### Fixed + - Fixed SoundCloud URLs not working with a slash in the end. ## [1.1.2] - 2016-10-30 + ### Added + - BandCamp support. ## [1.1.1] - 2016-10-28 + ### Added + - M3U support for radio streams. ## [1.1.0] - 2016-10-27 + ### Added + - MPEG2+layerIII MP3 format support. ### Fixed + - Fixed an exception on OGG stream playback. ## [1.0.14] - 2016-10-27 + ### Added + - OGG stream support. ### Fixed + - Fixed exception on loading age-restricted YouTube videos. - Fixed an exception on decoding some FLAC files. ## [1.0.13] - 2016-10-23 + ### Fixed + - Fixed huge CPU load on tracks from local files. ## [1.0.12] - 2016-10-23 + ### Added + - FLAC support. ## [1.0.11] - 2016-10-22 + ### Added + - MP3 stream support. ### Fixed + - Fixed NPE on missing Content-Length in HTTP responses. ## [1.0.10] - 2016-10-22 + ### Added + - HTTP source for loading any URL as a track. - Loading artist and title from MP3 tags. - Custom executor support. ### Fixed + - Fixed track end not triggering on exception. ## [1.0.9] - 2016-10-22 + ### Added + - Accurate seeking and duration for VBR MP3 files. ## [1.0.8] - 2016-10-22 + ### Added + - Track serialization. ### Fixed + - Fixed only one thread used for loading track info. ## [1.0.7] - 2016-10-21 + ### Added + - Shutting down player managers and source managers. - Cleaning up abandoned players. - File type detection based on content. - Track end reasons. ### Fixed + - Fixed TrackStartEvent event not called. ## [1.0.6] - 2016-10-17 + ### Added + - Audio output hook for experimental native UDP packet scheduling. ## [1.0.5] - 2016-10-17 + ### Fixed + - Fixed huge CPU load from GC monitor. - Fixed track duration reported in microsecons. ## [1.0.4] - 2016-10-16 + ### Added + - GC pause monitor for logging the number of GC pauses in different duration ranges. ## [1.0.3] - 2016-10-16 + ### Changed + - Made null a valid argument for playTrack with identical behavior to stopTrack. ## [1.0.2] - 2016-10-15 + ### Added + - SoundCloud playlist support. ## [1.0.1] - 2016-10-15 + ### Fixed + - Fixed YouTube tracks which use dash XML and are protected with cipher. ## [1.0.0] - 2016-10-14 + ### Added + - Offloading audio processing to remote nodes with load balancing. ### Changed + - Refactoring of track execution. ## [0.1.9] - 2016-10-08 + ### Added + - Method for loading items in order. - Opus encoding quality setting. ### Fixed -- Fixed SoundCloud tracks with underscores not working. + +- Fixed SoundCloud tracks with underscores not working. ## [0.1.8] - 2016-10-07 + ### Added + - Resampling quality setting. ## [0.1.7] - 2016-10-07 + ### Changed + - Track length info provided in milliseconds instead of seconds. ### Fixed + - Fixed getPosition always returning 0 when setPosition was called before start. ## [0.1.6] - 2016-10-07 + ### Added + - Tracks can be cloned (for replay). ## [0.1.5] - 2016-10-06 + ### Added + - SoundCloud support (single tracks) - Support for special playlist types on YouTube (liked videos, favorites, mixes). - MP3 support. ## [0.1.4] - 2016-10-05 + ### Changed + - YouTube track is loaded as a playlist if playlist is referenced in the URL. ## [0.1.3] - 2016-10-05 + ### Fixed + - Fixed broken special characters in track info when system default charset is not UTF8. ## [0.1.2] - 2016-10-04 + ### Added + - Severity levels for FriendlyException. - Report YouTube errors with the proper error message and COMMON severity. ### Fixed + - Fixed smooth transition not working from volume 0. ## [0.1.1] - 2016-10-04 + ### Added + - Volume support with smooth transition. - Support for nonstandard opus streams, such as mono. ### Fixed + - Fixed onTrackException not being called. ## [0.1.0] - 2016-10-02 + ### Added + - Initial release. diff --git a/FAQ.md b/FAQ.md index 1f84445dd..b7b21a5b3 100644 --- a/FAQ.md +++ b/FAQ.md @@ -6,11 +6,19 @@ ### `NoSuchMethodError` and `NoClassDefFoundError` -If Maven is used for building, then this is likely a problem with the way Maven handles version conflicts. When there are two things which require different versions of a dependency, Maven uses the braindead strategy of choosing the one which is the least transitive. This way it will likely choose an older version than the one required by LavaPlayer, which will cause runtime errors. +If Maven is used for building, then this is likely a problem with the way Maven handles version conflicts. When there +are two things which require different versions of a dependency, Maven uses the braindead strategy of choosing the one +which is the least transitive. This way it will likely choose an older version than the one required by LavaPlayer, +which will cause runtime errors. -Unfortunately it is [not possible](https://stackoverflow.com/questions/34201120/maven-set-dependency-mediation-strategy-to-newest-rather-than-nearest) to currently change the version conflict resolution strategy in Maven, so the only solution is to [check the dependency tree](https://maven.apache.org/plugins/maven-dependency-plugin/examples/resolving-conflicts-using-the-dependency-tree.html) and manually declare a dependency on the highest version of the affected package that can be seen in the tree. +Unfortunately it +is [not possible](https://stackoverflow.com/questions/34201120/maven-set-dependency-mediation-strategy-to-newest-rather-than-nearest) +to currently change the version conflict resolution strategy in Maven, so the only solution is +to [check the dependency tree](https://maven.apache.org/plugins/maven-dependency-plugin/examples/resolving-conflicts-using-the-dependency-tree.html) +and manually declare a dependency on the highest version of the affected package that can be seen in the tree. -An alternative is to use Gradle, which has a sane default (and if necessary, highly configurable) version conflict resolution strategy. +An alternative is to use Gradle, which has a sane default (and if necessary, highly configurable) version conflict +resolution strategy. ### Error `Something went wrong when...` @@ -34,10 +42,16 @@ AudioSourceManagers.registerLocalSource(playerManager); ### Sources are registered, but all YouTube tracks trigger `noMatches`. -Make sure you do not convert the input you pass to `AudioPlayerManager#loadItem` to lowercase as YouTube URLs are case-sensitive. +Make sure you do not convert the input you pass to `AudioPlayerManager#loadItem` to lowercase as YouTube URLs are +case-sensitive. ### Playback stutters -In the class that calls `AudioPlayer#provide`, record the number of times it returns a `null`. If the count constantly increases over time (excluding around when a track starts when it is fine to happen), it might be LavaPlayer lagging behind (possibly the CPU is under too much load). +In the class that calls `AudioPlayer#provide`, record the number of times it returns a `null`. If the count constantly +increases over time (excluding around when a track starts when it is fine to happen), it might be LavaPlayer lagging +behind (possibly the CPU is under too much load). -Otherwise, it is an issue with either packet sending or network. Packet sending issues might be caused by garbage collection pauses in the JVM - for JDA this is mitigated by [JDA-NAS](https://github.com/sedmelluq/jda-nas). For other Discord libraries, you should compare with a test bot with JDA+JDA-NAS on the same machine to verify it is not an issue with the library. \ No newline at end of file +Otherwise, it is an issue with either packet sending or network. Packet sending issues might be caused by garbage +collection pauses in the JVM - for JDA this is mitigated by [JDA-NAS](https://github.com/sedmelluq/jda-nas). For other +Discord libraries, you should compare with a test bot with JDA+JDA-NAS on the same machine to verify it is not an issue +with the library. diff --git a/README.md b/README.md index 752276bbc..6a53d7fbc 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,27 @@ # LavaPlayer - Audio player library for Discord -LavaPlayer is an audio player library written in Java which can load audio tracks from various sources and convert them into a stream of Opus frames. It is designed for use with Discord bots, but it can be used anywhere where Opus format output is required. +LavaPlayer is an audio player library written in Java which can load audio tracks from various sources and convert them +into a stream of Opus frames. It is designed for use with Discord bots, but it can be used anywhere where Opus format +output is required. -**Note:** This is a fork of the original [Lavaplayer](https://github.com/sedmelluq/lavaplayer) with some additional features and fixes from [Walkyst's fork](https://github.com/Walkyst/lavaplayer-fork). +**Note:** This is a fork of the original [Lavaplayer](https://github.com/sedmelluq/lavaplayer) with some additional +features and fixes from [Walkyst's fork](https://github.com/Walkyst/lavaplayer-fork). **Please read the [FAQ](FAQ.md) in case of issues.** #### Maven package -Replace `x.y.z` with the latest version number: [![Maven Central](https://img.shields.io/maven-central/v/dev.arbjerg/lavaplayer?versionPrefix=2)](https://central.sonatype.com/artifact/dev.arbjerg/lavaplayer) +Replace `x.y.z` with the latest version +number: [![Maven Central](https://img.shields.io/maven-central/v/dev.arbjerg/lavaplayer?versionPrefix=2)](https://central.sonatype.com/artifact/dev.arbjerg/lavaplayer) * Repository: mavenCentral * Artifact: **dev.arbjerg:lavaplayer:x.y.z** -Snapshots are published to https://maven.arbjerg.dev/snapshots & https://s01.oss.sonatype.org/content/repositories/snapshots +Snapshots are published +to https://maven.arbjerg.dev/snapshots & https://s01.oss.sonatype.org/content/repositories/snapshots Using in Gradle: + ```gradle repositories { mavenCentral() @@ -27,6 +33,7 @@ dependencies { ``` Using in Maven: + ```xml @@ -37,10 +44,10 @@ Using in Maven: ``` - ## Supported formats -The set of sources where LavaPlayer can load tracks from is easily extensible, but the ones currently included by default are: +The set of sources where LavaPlayer can load tracks from is easily extensible, but the ones currently included by +default are: * YouTube * SoundCloud @@ -63,35 +70,56 @@ The file formats that LavaPlayer can currently handle are (relevant for file/url ## Resource usage -What makes LavaPlayer unique is that it handles everything in the same process. Different sources and container formats are handled in Java, while decoding and encoding of audio are handled by embedded native libraries. This gives it a very fine-grained control over the resources that it uses, which means a low memory footprint as well as the chance to skip decoding and encoding steps altogether when the input format matches the output format. Some key things to remember: +What makes LavaPlayer unique is that it handles everything in the same process. Different sources and container formats +are handled in Java, while decoding and encoding of audio are handled by embedded native libraries. This gives it a very +fine-grained control over the resources that it uses, which means a low memory footprint as well as the chance to skip +decoding and encoding steps altogether when the input format matches the output format. Some key things to remember: -* Memory usage is both predictable and low. The amount of memory used per track when testing with YouTube was at most 350 kilobytes per track plus the off-heap memory for the thread stack, since there is one thread per playing track. -* The most common format used in YouTube is Opus, which matches the exact output format required for Discord. When no volume adjustment is applied, the packets from YouTube are directly passed to output, which saves CPU cycles. -* Resource leaks are unlikely because there are no additional processes launched and only one thread per playing track. When an audio player is not queried for an user-configured amount of time, then the playing track is aborted and the thread cleaned up. This avoids thread leaks even when the audio player is not shut down as it is supposed to. +* Memory usage is both predictable and low. The amount of memory used per track when testing with YouTube was at most + 350 kilobytes per track plus the off-heap memory for the thread stack, since there is one thread per playing track. +* The most common format used in YouTube is Opus, which matches the exact output format required for Discord. When no + volume adjustment is applied, the packets from YouTube are directly passed to output, which saves CPU cycles. +* Resource leaks are unlikely because there are no additional processes launched and only one thread per playing track. + When an audio player is not queried for an user-configured amount of time, then the playing track is aborted and the + thread cleaned up. This avoids thread leaks even when the audio player is not shut down as it is supposed to. ## Features #### Precise seeking support -Seeking is supported on all non-stream formats and sources. When a seek is performed on a playing track, the previously buffered audio samples will be provided until the seek is finished (this is configurable). When a seek is performed on a track which has not yet started, it will start immediately from the chosen position. +Seeking is supported on all non-stream formats and sources. When a seek is performed on a playing track, the previously +buffered audio samples will be provided until the seek is finished (this is configurable). When a seek is performed on a +track which has not yet started, it will start immediately from the chosen position. -Due to media containers supporting seeking at different resolutions, the position that a media player can start reading data from might be several seconds from the location that the user actually wanted to seek to. LavaPlayer handles it by remembering the position where it was requested to seek to, jumping to the highest position which is not after that and then ignoring the audio until the actual position that was requested. This provides a millisecond accuracy on seeking. +Due to media containers supporting seeking at different resolutions, the position that a media player can start reading +data from might be several seconds from the location that the user actually wanted to seek to. LavaPlayer handles it by +remembering the position where it was requested to seek to, jumping to the highest position which is not after that and +then ignoring the audio until the actual position that was requested. This provides a millisecond accuracy on seeking. #### Easy track loading -When creating an instance of an `AudioPlayerManager`, sources where the tracks should be loaded from with it must be manually registered. When loading tracks, you pass the manager an identifier and a handler which will get asynchronously called when the result has arrived. The handler has separate methods for receiving resolved tracks, resolved playlists, exceptions or being notified when nothing was found for the specified identifier. +When creating an instance of an `AudioPlayerManager`, sources where the tracks should be loaded from with it must be +manually registered. When loading tracks, you pass the manager an identifier and a handler which will get asynchronously +called when the result has arrived. The handler has separate methods for receiving resolved tracks, resolved playlists, +exceptions or being notified when nothing was found for the specified identifier. -Since the tracks hold only minimal meta-information (title, author, duration and identifier), loading playlists does not usually require the library to check the page of each individual track for sources such as YouTube or SoundCloud. This makes loading playlists pretty fast. +Since the tracks hold only minimal meta-information (title, author, duration and identifier), loading playlists does not +usually require the library to check the page of each individual track for sources such as YouTube or SoundCloud. This +makes loading playlists pretty fast. #### Extensibility -Any source that implements the `AudioSourceManager` interface can be registered to the player manager. These can be custom sources using either some of the supported containers and codecs or defining a totally new way the tracks are actually executed, such as delegating it to another process, should the set of formats supported by LavaPlayer by default not be enough. +Any source that implements the `AudioSourceManager` interface can be registered to the player manager. These can be +custom sources using either some of the supported containers and codecs or defining a totally new way the tracks are +actually executed, such as delegating it to another process, should the set of formats supported by LavaPlayer by +default not be enough. ## Usage #### Creating an audio player manager -First thing you have to do when using the library is to create a `DefaultAudioPlayerManager` and then configure it to use the settings and sources you want. Here is a sample: +First thing you have to do when using the library is to create a `DefaultAudioPlayerManager` and then configure it to +use the settings and sources you want. Here is a sample: ```java AudioPlayerManager playerManager = new DefaultAudioPlayerManager(); @@ -99,17 +127,23 @@ AudioSourceManagers.registerRemoteSources(playerManager); ``` There are various configuration settings that can be modified: + * Opus encoding and resampling quality settings. * Frame buffer duration: how much of audio is buffered in advance. * Stuck track threshold: when no data from a playing track comes in the specified time, an event is sent. * Abandoned player cleanup threshold: when the player is not queried in the specified amount of time, it is stopped. -* Garbage collection monitoring: logs statistics of garbage collection pauses every 2 minutes. If the pauses are long enough to cause a stutter in the audio, it will be logged with a warning level, so you could take action to optimize your GC settings. +* Garbage collection monitoring: logs statistics of garbage collection pauses every 2 minutes. If the pauses are long + enough to cause a stutter in the audio, it will be logged with a warning level, so you could take action to optimize + your GC settings. -If possible, you should use a single instance of a player manager for your whole application. A player manager manages several thread pools which make no sense to duplicate. +If possible, you should use a single instance of a player manager for your whole application. A player manager manages +several thread pools which make no sense to duplicate. #### Creating an audio player -Once you have a player manager, you can create players from it. Generally you would want to create a player per every different target you might want to separately stream audio to. It is totally fine to create them even if they are unlikely to be used, as they do not use any resources on their own without an active track. +Once you have a player manager, you can create players from it. Generally you would want to create a player per every +different target you might want to separately stream audio to. It is totally fine to create them even if they are +unlikely to be used, as they do not use any resources on their own without an active track. Creating a player is rather simple: @@ -117,18 +151,27 @@ Creating a player is rather simple: AudioPlayer player = playerManager.createPlayer(); ``` -Once you have an instance of an audio player, you need some way to receive events from it. For that you should register a listener to it which either extends the `AudioEventAdapter` class or implements `AudioEventListener`. Since that listener receives the events for starting and ending tracks, it makes sense to also make it responsible for scheduling tracks. Assuming `TrackScheduler` is a class that implements `AudioEventListener`: +Once you have an instance of an audio player, you need some way to receive events from it. For that you should register +a listener to it which either extends the `AudioEventAdapter` class or implements `AudioEventListener`. Since that +listener receives the events for starting and ending tracks, it makes sense to also make it responsible for scheduling +tracks. Assuming `TrackScheduler` is a class that implements `AudioEventListener`: ```java TrackScheduler trackScheduler = new TrackScheduler(player); player.addListener(trackScheduler); ``` -Now you have an audio player capable of playing instances of `AudioTrack`. However, what you don't have is audio tracks, which are the next things you have to obtain. +Now you have an audio player capable of playing instances of `AudioTrack`. However, what you don't have is audio tracks, +which are the next things you have to obtain. #### Loading audio tracks -To load a track, you have to call either the `loadItem` or `loadItemOrdered` method of an `AudioPlayerManager`. `loadItem` takes an identifier parameter and a load handler parameter. The identifier is a piece of text that should identify the track for some source. For example if it is a YouTube video ID, then YouTube source manager will load it, if it is a file path then the local file source will load it. The handler parameter is an instance of `AudioLoadResultHandler`, which has separate methods for different results of the loading process. You can either have a dedicated class for this or you can simply pass it an anonymous class as in the next example: +To load a track, you have to call either the `loadItem` or `loadItemOrdered` method of +an `AudioPlayerManager`. `loadItem` takes an identifier parameter and a load handler parameter. The identifier is a +piece of text that should identify the track for some source. For example if it is a YouTube video ID, then YouTube +source manager will load it, if it is a file path then the local file source will load it. The handler parameter is an +instance of `AudioLoadResultHandler`, which has separate methods for different results of the loading process. You can +either have a dedicated class for this or you can simply pass it an anonymous class as in the next example: ```java playerManager.loadItem(identifier, new AudioLoadResultHandler() { @@ -156,23 +199,34 @@ playerManager.loadItem(identifier, new AudioLoadResultHandler() { } ``` -Most of these methods are rather obvious. In addition to everything exploding, `loadFailed` will also be called for example when a YouTube track is blocked or not available in your area. The `FriendlyException` class has a field called `severity`. If the value of this is `COMMON`, then it means that the reason is definitely not a bug or a network issue, but because the track is not available, such as the YouTube blocked video example. These message in this case can simply be forwarded as is to the user. +Most of these methods are rather obvious. In addition to everything exploding, `loadFailed` will also be called for +example when a YouTube track is blocked or not available in your area. The `FriendlyException` class has a field +called `severity`. If the value of this is `COMMON`, then it means that the reason is definitely not a bug or a network +issue, but because the track is not available, such as the YouTube blocked video example. These message in this case can +simply be forwarded as is to the user. -The other method for loading tracks, `loadItemOrdered` is for cases where you want the tracks to be loaded in order for example within one player. `loadItemOrdered` takes an ordering channel key as the first parameter, which is simply any object which remains the same for all the requests that should be loaded in the same ordered queue. The most common use would probably be to just pass it the `AudioPlayer` instance that the loaded tracks will be queued for. +The other method for loading tracks, `loadItemOrdered` is for cases where you want the tracks to be loaded in order for +example within one player. `loadItemOrdered` takes an ordering channel key as the first parameter, which is simply any +object which remains the same for all the requests that should be loaded in the same ordered queue. The most common use +would probably be to just pass it the `AudioPlayer` instance that the loaded tracks will be queued for. #### Playing audio tracks -In the previous example I did not actually start playing the loaded track yet, but sneakily passed it on to our fictional `TrackScheduler` class instead. Starting the track is however a trivial thing to do: +In the previous example I did not actually start playing the loaded track yet, but sneakily passed it on to our +fictional `TrackScheduler` class instead. Starting the track is however a trivial thing to do: ```java player.playTrack(track); ``` -Now the track should be playing, which means buffered for whoever needs it to poll its frames. However, you would need to somehow react to events, most notably the track finishing, so you could start the next track. +Now the track should be playing, which means buffered for whoever needs it to poll its frames. However, you would need +to somehow react to events, most notably the track finishing, so you could start the next track. #### Handling events -Events are handled by event handlers added to an `AudioPlayer` instance. The simplest way for creating the handler is to extend the `AudioEventAdapter` class. Here is a quick description of each of the methods it has, in the context of using it for a track scheduler: +Events are handled by event handlers added to an `AudioPlayer` instance. The simplest way for creating the handler is to +extend the `AudioEventAdapter` class. Here is a quick description of each of the methods it has, in the context of using +it for a track scheduler: ```java public class TrackScheduler extends AudioEventAdapter { @@ -219,31 +273,32 @@ public class TrackScheduler extends AudioEventAdapter { #### JDA integration -To use it with JDA 4, you would need an instance of `AudioSendHandler`. There is only the slight difference of no separate `canProvide` and `provide` methods in `AudioPlayer`, so the wrapper for this is simple: +To use it with JDA 4, you would need an instance of `AudioSendHandler`. There is only the slight difference of no +separate `canProvide` and `provide` methods in `AudioPlayer`, so the wrapper for this is simple: ```java public class AudioPlayerSendHandler implements AudioSendHandler { - private final AudioPlayer audioPlayer; - private AudioFrame lastFrame; + private final AudioPlayer audioPlayer; + private AudioFrame lastFrame; - public AudioPlayerSendHandler(AudioPlayer audioPlayer) { - this.audioPlayer = audioPlayer; - } + public AudioPlayerSendHandler(AudioPlayer audioPlayer) { + this.audioPlayer = audioPlayer; + } - @Override - public boolean canProvide() { - lastFrame = audioPlayer.provide(); - return lastFrame != null; - } + @Override + public boolean canProvide() { + lastFrame = audioPlayer.provide(); + return lastFrame != null; + } - @Override - public ByteBuffer provide20MsAudio() { - return ByteBuffer.wrap(lastFrame.getData()); - } + @Override + public ByteBuffer provide20MsAudio() { + return ByteBuffer.wrap(lastFrame.getData()); + } - @Override - public boolean isOpus() { - return true; - } + @Override + public boolean isOpus() { + return true; + } } ``` diff --git a/build.gradle.kts b/build.gradle.kts index 50c1643ac..e5d364c95 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,112 +5,112 @@ import com.vanniktech.maven.publish.SonatypeHost import org.ajoberstar.grgit.Grgit plugins { - id("org.ajoberstar.grgit") version "5.2.0" - id("de.undercouch.download") version "5.4.0" - alias(libs.plugins.maven.publish.base) apply false + id("org.ajoberstar.grgit") version "5.2.0" + id("de.undercouch.download") version "5.4.0" + alias(libs.plugins.maven.publish.base) apply false } val (gitVersion, release) = versionFromGit() logger.lifecycle("Version: $gitVersion (release: $release)") allprojects { - group = "dev.arbjerg" - version = gitVersion - - repositories { - mavenLocal() - mavenCentral() - maven("https://jitpack.io") - } + group = "dev.arbjerg" + version = gitVersion + + repositories { + mavenLocal() + mavenCentral() + maven("https://jitpack.io") + } } subprojects { - if (project.name == "natives" || project.name == "extensions-project") { - return@subprojects - } - - apply() - apply() - - configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - } - - 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" - - maven(if (release) releases else snapshots) { - credentials { - password = findProperty("MAVEN_PASSWORD") as String? - username = findProperty("MAVEN_USERNAME") as String? - } - } - } - } else { - logger.lifecycle("Not publishing to maven.arbjerg.dev because credentials are not set") + if (project.name == "natives" || project.name == "extensions-project") { + return@subprojects } - } - - afterEvaluate { - plugins.withId(libs.plugins.maven.publish.base.get().pluginId) { - configure { - coordinates(group.toString(), project.the().archivesName.get(), version.toString()) - - if (findProperty("mavenCentralUsername") != null && findProperty("mavenCentralPassword") != null) { - publishToMavenCentral(SonatypeHost.S01, false) - if (release) { - signAllPublications() - } - } else { - logger.lifecycle("Not publishing to OSSRH due to missing credentials") - } - pom { - name = "lavaplayer" - description = "A Lavaplayer fork maintained by Lavalink" - url = "https://github.com/lavalink-devs/lavaplayer" + apply() + apply() - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://github.com/lavalink-devs/lavaplayer/blob/main/LICENSE" - } - } + configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + } - developers { - developer { - id = "freyacodes" - name = "Freya Arbjerg" - url = "https://www.arbjerg.dev" + 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" + + maven(if (release) releases else snapshots) { + credentials { + password = findProperty("MAVEN_PASSWORD") as String? + username = findProperty("MAVEN_USERNAME") as String? + } + } } - } + } else { + logger.lifecycle("Not publishing to maven.arbjerg.dev because credentials are not set") + } + } - scm { - url = "https://github.com/lavalink-devs/lavaplayer/" - connection = "scm:git:git://github.com/lavalink-devs/lavaplayer.git" - developerConnection = "scm:git:ssh://git@github.com/lavalink-devs/lavaplayer.git" - } + afterEvaluate { + plugins.withId(libs.plugins.maven.publish.base.get().pluginId) { + configure { + coordinates(group.toString(), project.the().archivesName.get(), version.toString()) + + if (findProperty("mavenCentralUsername") != null && findProperty("mavenCentralPassword") != null) { + publishToMavenCentral(SonatypeHost.S01, false) + if (release) { + signAllPublications() + } + } else { + logger.lifecycle("Not publishing to OSSRH due to missing credentials") + } + + pom { + name = "lavaplayer" + description = "A Lavaplayer fork maintained by Lavalink" + url = "https://github.com/lavalink-devs/lavaplayer" + + licenses { + license { + name = "The Apache License, Version 2.0" + url = "https://github.com/lavalink-devs/lavaplayer/blob/main/LICENSE" + } + } + + developers { + developer { + id = "freyacodes" + name = "Freya Arbjerg" + url = "https://www.arbjerg.dev" + } + } + + scm { + url = "https://github.com/lavalink-devs/lavaplayer/" + connection = "scm:git:git://github.com/lavalink-devs/lavaplayer.git" + developerConnection = "scm:git:ssh://git@github.com/lavalink-devs/lavaplayer.git" + } + } + } } - } } - } } @SuppressWarnings("GrMethodMayBeStatic") fun versionFromGit(): Pair { - Grgit.open(mapOf("currentDir" to project.rootDir)).use { git -> - val headTag = git.tag + Grgit.open(mapOf("currentDir" to project.rootDir)).use { git -> + val headTag = git.tag .list() .find { it.commit.id == git.head().id } - val clean = git.status().isClean || System.getenv("CI") != null - if (!clean) { - logger.lifecycle("Git state is dirty, version is a snapshot.") - } + val clean = git.status().isClean || System.getenv("CI") != null + if (!clean) { + logger.lifecycle("Git state is dirty, version is a snapshot.") + } - return if (headTag != null && clean) headTag.name to true else "${git.head().id}-SNAPSHOT" to false - } + return if (headTag != null && clean) headTag.name to true else "${git.head().id}-SNAPSHOT" to false + } } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index dad90dbf7..5df76461d 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -2,19 +2,19 @@ import com.vanniktech.maven.publish.JavaLibrary import com.vanniktech.maven.publish.JavadocJar plugins { - `java-library` - alias(libs.plugins.maven.publish.base) + `java-library` + alias(libs.plugins.maven.publish.base) } base { - archivesName = "lava-common" + archivesName = "lava-common" } dependencies { - implementation(libs.slf4j) - implementation(libs.commons.io) + implementation(libs.slf4j) + implementation(libs.commons.io) } mavenPublishing { - configure(JavaLibrary(JavadocJar.Javadoc())) + configure(JavaLibrary(JavadocJar.Javadoc())) } diff --git a/common/src/main/java/com/sedmelluq/lava/common/natives/NativeLibraryBinaryProvider.java b/common/src/main/java/com/sedmelluq/lava/common/natives/NativeLibraryBinaryProvider.java index 4ba86f98a..726f695af 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/natives/NativeLibraryBinaryProvider.java +++ b/common/src/main/java/com/sedmelluq/lava/common/natives/NativeLibraryBinaryProvider.java @@ -1,13 +1,14 @@ package com.sedmelluq.lava.common.natives; import com.sedmelluq.lava.common.natives.architecture.SystemType; + import java.io.InputStream; public interface NativeLibraryBinaryProvider { - /** - * @param systemType Detected system type. - * @param libraryName Name of the library to load. - * @return Stream to the library binary. null causes failure. - */ - InputStream getLibraryStream(SystemType systemType, String libraryName); + /** + * @param systemType Detected system type. + * @param libraryName Name of the library to load. + * @return Stream to the library binary. null causes failure. + */ + InputStream getLibraryStream(SystemType systemType, String libraryName); } diff --git a/common/src/main/java/com/sedmelluq/lava/common/natives/NativeLibraryLoader.java b/common/src/main/java/com/sedmelluq/lava/common/natives/NativeLibraryLoader.java index 7c444fb77..45d419ef6 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/natives/NativeLibraryLoader.java +++ b/common/src/main/java/com/sedmelluq/lava/common/natives/NativeLibraryLoader.java @@ -1,18 +1,15 @@ package com.sedmelluq.lava.common.natives; import com.sedmelluq.lava.common.natives.architecture.SystemType; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.nio.file.*; import java.util.function.Predicate; -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import static java.nio.file.attribute.PosixFilePermissions.asFileAttribute; import static java.nio.file.attribute.PosixFilePermissions.fromString; @@ -21,200 +18,200 @@ * Loads native libraries by name. Libraries are expected to be in classpath /natives/[arch]/[prefix]name[suffix] */ public class NativeLibraryLoader { - private static final Logger log = LoggerFactory.getLogger(NativeLibraryLoader.class); - - private static final String DEFAULT_PROPERTY_PREFIX = "lava.native."; - private static final String DEFAULT_RESOURCE_ROOT = "/natives/"; - - private final String libraryName; - private final Predicate systemFilter; - private final NativeLibraryProperties properties; - private final NativeLibraryBinaryProvider binaryProvider; - private final Object lock; - private volatile LoadResult previousResult; - - public NativeLibraryLoader(String libraryName, Predicate systemFilter, NativeLibraryProperties properties, - NativeLibraryBinaryProvider binaryProvider) { - - this.libraryName = libraryName; - this.systemFilter = systemFilter; - this.binaryProvider = binaryProvider; - this.properties = properties; - this.lock = new Object(); - } - - public static NativeLibraryLoader create(Class classLoaderSample, String libraryName) { - return createFiltered(classLoaderSample, libraryName, null); - } - - public static NativeLibraryLoader createFiltered(Class classLoaderSample, String libraryName, - Predicate systemFilter) { - - return new NativeLibraryLoader( - libraryName, - systemFilter, - new SystemNativeLibraryProperties(libraryName, DEFAULT_PROPERTY_PREFIX), - new ResourceNativeLibraryBinaryProvider(classLoaderSample, DEFAULT_RESOURCE_ROOT) - ); - } - - public void load() { - LoadResult result = previousResult; - - if (result == null) { - synchronized (lock) { - result = previousResult; + private static final Logger log = LoggerFactory.getLogger(NativeLibraryLoader.class); + + private static final String DEFAULT_PROPERTY_PREFIX = "lava.native."; + private static final String DEFAULT_RESOURCE_ROOT = "/natives/"; + + private final String libraryName; + private final Predicate systemFilter; + private final NativeLibraryProperties properties; + private final NativeLibraryBinaryProvider binaryProvider; + private final Object lock; + private volatile LoadResult previousResult; + + public NativeLibraryLoader(String libraryName, Predicate systemFilter, NativeLibraryProperties properties, + NativeLibraryBinaryProvider binaryProvider) { + + this.libraryName = libraryName; + this.systemFilter = systemFilter; + this.binaryProvider = binaryProvider; + this.properties = properties; + this.lock = new Object(); + } + + public static NativeLibraryLoader create(Class classLoaderSample, String libraryName) { + return createFiltered(classLoaderSample, libraryName, null); + } + + public static NativeLibraryLoader createFiltered(Class classLoaderSample, String libraryName, + Predicate systemFilter) { + + return new NativeLibraryLoader( + libraryName, + systemFilter, + new SystemNativeLibraryProperties(libraryName, DEFAULT_PROPERTY_PREFIX), + new ResourceNativeLibraryBinaryProvider(classLoaderSample, DEFAULT_RESOURCE_ROOT) + ); + } + + public void load() { + LoadResult result = previousResult; if (result == null) { - result = loadWithFailureCheck(); - previousResult = result; + synchronized (lock) { + result = previousResult; + + if (result == null) { + result = loadWithFailureCheck(); + previousResult = result; + } + } } - } - } - if (!result.success) { - throw result.exception; + if (!result.success) { + throw result.exception; + } } - } - private LoadResult loadWithFailureCheck() { - log.info("Native library {}: loading with filter {}", libraryName, systemFilter); + private LoadResult loadWithFailureCheck() { + log.info("Native library {}: loading with filter {}", libraryName, systemFilter); - try { - loadInternal(); - return new LoadResult(true, null); - } catch (Throwable e) { - log.error("Native library {}: loading failed.", libraryName, e); - return new LoadResult(false, new RuntimeException(e)); + try { + loadInternal(); + return new LoadResult(true, null); + } catch (Throwable e) { + log.error("Native library {}: loading failed.", libraryName, e); + return new LoadResult(false, new RuntimeException(e)); + } } - } - private void loadInternal() { - String explicitPath = properties.getLibraryPath(); + private void loadInternal() { + String explicitPath = properties.getLibraryPath(); - if (explicitPath != null) { - log.debug("Native library {}: explicit path provided {}", libraryName, explicitPath); + if (explicitPath != null) { + log.debug("Native library {}: explicit path provided {}", libraryName, explicitPath); - loadFromFile(Paths.get(explicitPath).toAbsolutePath()); - } else { - SystemType systemType = detectMatchingSystemType(); + loadFromFile(Paths.get(explicitPath).toAbsolutePath()); + } else { + SystemType systemType = detectMatchingSystemType(); - if (systemType != null) { - String explicitDirectory = properties.getLibraryDirectory(); + if (systemType != null) { + String explicitDirectory = properties.getLibraryDirectory(); - if (explicitDirectory != null) { - log.debug("Native library {}: explicit directory provided {}", libraryName, explicitDirectory); + if (explicitDirectory != null) { + log.debug("Native library {}: explicit directory provided {}", libraryName, explicitDirectory); - loadFromFile(Paths.get(explicitDirectory, systemType.formatLibraryName(libraryName)).toAbsolutePath()); - } else { - loadFromFile(extractLibraryFromResources(systemType)); + loadFromFile(Paths.get(explicitDirectory, systemType.formatLibraryName(libraryName)).toAbsolutePath()); + } else { + loadFromFile(extractLibraryFromResources(systemType)); + } + } } - } } - } - private void loadFromFile(Path libraryFilePath) { - log.debug("Native library {}: attempting to load library at {}", libraryName, libraryFilePath); - System.load(libraryFilePath.toAbsolutePath().toString()); - log.info("Native library {}: successfully loaded.", libraryName); - } + private void loadFromFile(Path libraryFilePath) { + log.debug("Native library {}: attempting to load library at {}", libraryName, libraryFilePath); + System.load(libraryFilePath.toAbsolutePath().toString()); + log.info("Native library {}: successfully loaded.", libraryName); + } - private Path extractLibraryFromResources(SystemType systemType) { - try (InputStream libraryStream = binaryProvider.getLibraryStream(systemType, libraryName)) { - if (libraryStream == null) { - throw new UnsatisfiedLinkError("Required library was not found"); - } + private Path extractLibraryFromResources(SystemType systemType) { + try (InputStream libraryStream = binaryProvider.getLibraryStream(systemType, libraryName)) { + if (libraryStream == null) { + throw new UnsatisfiedLinkError("Required library was not found"); + } - Path extractedLibraryPath = prepareExtractionDirectory().resolve(systemType.formatLibraryName(libraryName)); + Path extractedLibraryPath = prepareExtractionDirectory().resolve(systemType.formatLibraryName(libraryName)); - try (FileOutputStream fileStream = new FileOutputStream(extractedLibraryPath.toFile())) { - IOUtils.copy(libraryStream, fileStream); - } + try (FileOutputStream fileStream = new FileOutputStream(extractedLibraryPath.toFile())) { + IOUtils.copy(libraryStream, fileStream); + } - return extractedLibraryPath; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private Path prepareExtractionDirectory() throws IOException { - Path extractionDirectory = detectExtractionBaseDirectory().resolve(String.valueOf(System.currentTimeMillis())); - - if (!Files.isDirectory(extractionDirectory)) { - log.debug("Native library {}: extraction directory {} does not exist, creating.", libraryName, - extractionDirectory); - - try { - createDirectoriesWithFullPermissions(extractionDirectory); - } catch (FileAlreadyExistsException ignored) { - // All is well - } catch (IOException e) { - throw new IOException("Failed to create directory for unpacked native library.", e); - } - } else { - log.debug("Native library {}: extraction directory {} already exists, using.", libraryName, extractionDirectory); + return extractedLibraryPath; + } catch (IOException e) { + throw new RuntimeException(e); + } } - return extractionDirectory; - } + private Path prepareExtractionDirectory() throws IOException { + Path extractionDirectory = detectExtractionBaseDirectory().resolve(String.valueOf(System.currentTimeMillis())); - private Path detectExtractionBaseDirectory() { - String explicitExtractionBase = properties.getExtractionPath(); + if (!Files.isDirectory(extractionDirectory)) { + log.debug("Native library {}: extraction directory {} does not exist, creating.", libraryName, + extractionDirectory); - if (explicitExtractionBase != null) { - log.debug("Native library {}: explicit extraction path provided - {}", libraryName, explicitExtractionBase); - return Paths.get(explicitExtractionBase).toAbsolutePath(); - } + try { + createDirectoriesWithFullPermissions(extractionDirectory); + } catch (FileAlreadyExistsException ignored) { + // All is well + } catch (IOException e) { + throw new IOException("Failed to create directory for unpacked native library.", e); + } + } else { + log.debug("Native library {}: extraction directory {} already exists, using.", libraryName, extractionDirectory); + } - Path path = Paths.get(System.getProperty("java.io.tmpdir", "/tmp"), "lava-jni-natives") - .toAbsolutePath(); + return extractionDirectory; + } - log.debug("Native library {}: detected {} as base directory for extraction.", libraryName, path); - return path; - } + private Path detectExtractionBaseDirectory() { + String explicitExtractionBase = properties.getExtractionPath(); - private SystemType detectMatchingSystemType() { - SystemType systemType; + if (explicitExtractionBase != null) { + log.debug("Native library {}: explicit extraction path provided - {}", libraryName, explicitExtractionBase); + return Paths.get(explicitExtractionBase).toAbsolutePath(); + } - try { - systemType = SystemType.detect(properties); - } catch (IllegalArgumentException e) { - if (systemFilter != null) { - log.info("Native library {}: could not detect sytem type, but system filter is {} - assuming it does " + - "not match and skipping library.", libraryName, systemFilter); + Path path = Paths.get(System.getProperty("java.io.tmpdir", "/tmp"), "lava-jni-natives") + .toAbsolutePath(); - return null; - } else { - throw e; - } + log.debug("Native library {}: detected {} as base directory for extraction.", libraryName, path); + return path; } - if (systemFilter != null && !systemFilter.test(systemType)) { - log.debug("Native library {}: system filter does not match detected system {}, skipping", libraryName, - systemType.formatSystemName()); - return null; - } + private SystemType detectMatchingSystemType() { + SystemType systemType; - return systemType; - } + try { + systemType = SystemType.detect(properties); + } catch (IllegalArgumentException e) { + if (systemFilter != null) { + log.info("Native library {}: could not detect sytem type, but system filter is {} - assuming it does " + + "not match and skipping library.", libraryName, systemFilter); - private static void createDirectoriesWithFullPermissions(Path path) throws IOException { - boolean isPosix = FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + return null; + } else { + throw e; + } + } + + if (systemFilter != null && !systemFilter.test(systemType)) { + log.debug("Native library {}: system filter does not match detected system {}, skipping", libraryName, + systemType.formatSystemName()); + return null; + } - if (!isPosix) { - Files.createDirectories(path); - } else { - Files.createDirectories(path, asFileAttribute(fromString("rwxrwxrwx"))); + return systemType; } - } - private static class LoadResult { - public final boolean success; - public final RuntimeException exception; + private static void createDirectoriesWithFullPermissions(Path path) throws IOException { + boolean isPosix = FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); - private LoadResult(boolean success, RuntimeException exception) { - this.success = success; - this.exception = exception; + if (!isPosix) { + Files.createDirectories(path); + } else { + Files.createDirectories(path, asFileAttribute(fromString("rwxrwxrwx"))); + } + } + + private static class LoadResult { + public final boolean success; + public final RuntimeException exception; + + private LoadResult(boolean success, RuntimeException exception) { + this.success = success; + this.exception = exception; + } } - } -} \ No newline at end of file +} diff --git a/common/src/main/java/com/sedmelluq/lava/common/natives/NativeLibraryProperties.java b/common/src/main/java/com/sedmelluq/lava/common/natives/NativeLibraryProperties.java index 80da22f3c..b44f5db1b 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/natives/NativeLibraryProperties.java +++ b/common/src/main/java/com/sedmelluq/lava/common/natives/NativeLibraryProperties.java @@ -3,48 +3,48 @@ import com.sedmelluq.lava.common.natives.architecture.SystemType; public interface NativeLibraryProperties { - /** - * @return Explicit filesystem path for the library. If this is set, this is loaded directly and no resource - * extraction and/or system name detection is performed. If this returns null, library directory - * is checked next. - */ - String getLibraryPath(); - - /** - * @return Explicit directory containing the native library. The specified directory must contain the system name - * directories, thus the library to be loaded is actually located at - * directory/{systemName}/{libPrefix}{libName}{libSuffix}. If this returns null, - * then {@link NativeLibraryBinaryProvider#getLibraryStream(SystemType, String)} is called to obtain the - * stream to the library file, which is then written to disk for loading. - */ - String getLibraryDirectory(); - - /** - * @return Base directory where to write the library if it is obtained through - * {@link NativeLibraryBinaryProvider#getLibraryStream(SystemType, String)}. The library file itself will - * actually be written to a subdirectory with a randomly generated name. The specified directory does not - * have to exist, but in that case the current process must have privileges to create it. If this returns - * null, then {tmpDir}/lava-jni-natives is used. - */ - String getExtractionPath(); - - /** - * @return System name. If this is set, no operating system or architecture detection is performed. - */ - String getSystemName(); - - /** - * @return Library file name prefix to use. Only used when {@link #getSystemName()} is provided. - */ - String getLibraryFileNamePrefix(); - - /** - * @return Library file name suffix to use. Only used when {@link #getSystemName()} is provided. - */ - String getLibraryFileNameSuffix(); - - /** - * @return Architecture name to use. If this is set, operating system detection is still performed. - */ - String getArchitectureName(); + /** + * @return Explicit filesystem path for the library. If this is set, this is loaded directly and no resource + * extraction and/or system name detection is performed. If this returns null, library directory + * is checked next. + */ + String getLibraryPath(); + + /** + * @return Explicit directory containing the native library. The specified directory must contain the system name + * directories, thus the library to be loaded is actually located at + * directory/{systemName}/{libPrefix}{libName}{libSuffix}. If this returns null, + * then {@link NativeLibraryBinaryProvider#getLibraryStream(SystemType, String)} is called to obtain the + * stream to the library file, which is then written to disk for loading. + */ + String getLibraryDirectory(); + + /** + * @return Base directory where to write the library if it is obtained through + * {@link NativeLibraryBinaryProvider#getLibraryStream(SystemType, String)}. The library file itself will + * actually be written to a subdirectory with a randomly generated name. The specified directory does not + * have to exist, but in that case the current process must have privileges to create it. If this returns + * null, then {tmpDir}/lava-jni-natives is used. + */ + String getExtractionPath(); + + /** + * @return System name. If this is set, no operating system or architecture detection is performed. + */ + String getSystemName(); + + /** + * @return Library file name prefix to use. Only used when {@link #getSystemName()} is provided. + */ + String getLibraryFileNamePrefix(); + + /** + * @return Library file name suffix to use. Only used when {@link #getSystemName()} is provided. + */ + String getLibraryFileNameSuffix(); + + /** + * @return Architecture name to use. If this is set, operating system detection is still performed. + */ + String getArchitectureName(); } diff --git a/common/src/main/java/com/sedmelluq/lava/common/natives/NativeResourceHolder.java b/common/src/main/java/com/sedmelluq/lava/common/natives/NativeResourceHolder.java index b307a4808..7a9f7be92 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/natives/NativeResourceHolder.java +++ b/common/src/main/java/com/sedmelluq/lava/common/natives/NativeResourceHolder.java @@ -9,44 +9,44 @@ * Abstract instance of a class which holds native resources that must be freed. */ public abstract class NativeResourceHolder { - private static final Logger log = LoggerFactory.getLogger(NativeResourceHolder.class); + private static final Logger log = LoggerFactory.getLogger(NativeResourceHolder.class); - private final AtomicBoolean released = new AtomicBoolean(); + private final AtomicBoolean released = new AtomicBoolean(); - /** - * Assert that the native resources have not been freed. - */ - protected void checkNotReleased() { - if (released.get()) { - throw new IllegalStateException("Cannot use the decoder after closing it."); + /** + * Assert that the native resources have not been freed. + */ + protected void checkNotReleased() { + if (released.get()) { + throw new IllegalStateException("Cannot use the decoder after closing it."); + } } - } - - /** - * Free up native resources of the decoder. Using other methods after this will throw IllegalStateException. - */ - public void close() { - closeInternal(false); - } - - /** - * Free the native resources. - */ - protected abstract void freeResources(); - - private synchronized void closeInternal(boolean inFinalizer) { - if (released.compareAndSet(false, true)) { - if (inFinalizer) { - log.warn("Should have been closed before finalization ({}).", this.getClass().getName()); - } - - freeResources(); + + /** + * Free up native resources of the decoder. Using other methods after this will throw IllegalStateException. + */ + public void close() { + closeInternal(false); + } + + /** + * Free the native resources. + */ + protected abstract void freeResources(); + + private synchronized void closeInternal(boolean inFinalizer) { + if (released.compareAndSet(false, true)) { + if (inFinalizer) { + log.warn("Should have been closed before finalization ({}).", this.getClass().getName()); + } + + freeResources(); + } } - } - @Override - protected void finalize() throws Throwable { - super.finalize(); - closeInternal(true); - } + @Override + protected void finalize() throws Throwable { + super.finalize(); + closeInternal(true); + } } diff --git a/common/src/main/java/com/sedmelluq/lava/common/natives/ResourceNativeLibraryBinaryProvider.java b/common/src/main/java/com/sedmelluq/lava/common/natives/ResourceNativeLibraryBinaryProvider.java index fac5b9228..03055c237 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/natives/ResourceNativeLibraryBinaryProvider.java +++ b/common/src/main/java/com/sedmelluq/lava/common/natives/ResourceNativeLibraryBinaryProvider.java @@ -1,28 +1,29 @@ package com.sedmelluq.lava.common.natives; import com.sedmelluq.lava.common.natives.architecture.SystemType; -import java.io.InputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.InputStream; + public class ResourceNativeLibraryBinaryProvider implements NativeLibraryBinaryProvider { - private static final Logger log = LoggerFactory.getLogger(ResourceNativeLibraryBinaryProvider.class); + private static final Logger log = LoggerFactory.getLogger(ResourceNativeLibraryBinaryProvider.class); - private final Class classLoaderSample; - private final String nativesRoot; + private final Class classLoaderSample; + private final String nativesRoot; - public ResourceNativeLibraryBinaryProvider(Class classLoaderSample, String nativesRoot) { - this.classLoaderSample = classLoaderSample != null ? classLoaderSample : ResourceNativeLibraryBinaryProvider.class; - this.nativesRoot = nativesRoot; - } + public ResourceNativeLibraryBinaryProvider(Class classLoaderSample, String nativesRoot) { + this.classLoaderSample = classLoaderSample != null ? classLoaderSample : ResourceNativeLibraryBinaryProvider.class; + this.nativesRoot = nativesRoot; + } - @Override - public InputStream getLibraryStream(SystemType systemType, String libraryName) { - String resourcePath = nativesRoot + systemType.formatSystemName() + "/" + systemType.formatLibraryName(libraryName); + @Override + public InputStream getLibraryStream(SystemType systemType, String libraryName) { + String resourcePath = nativesRoot + systemType.formatSystemName() + "/" + systemType.formatLibraryName(libraryName); - log.debug("Native library {}: trying to find from resources at {} with {} as classloader reference", libraryName, - resourcePath, classLoaderSample.getName()); + log.debug("Native library {}: trying to find from resources at {} with {} as classloader reference", libraryName, + resourcePath, classLoaderSample.getName()); - return classLoaderSample.getResourceAsStream(resourcePath); - } + return classLoaderSample.getResourceAsStream(resourcePath); + } } diff --git a/common/src/main/java/com/sedmelluq/lava/common/natives/SystemNativeLibraryProperties.java b/common/src/main/java/com/sedmelluq/lava/common/natives/SystemNativeLibraryProperties.java index 7a1846386..49f6c8ae6 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/natives/SystemNativeLibraryProperties.java +++ b/common/src/main/java/com/sedmelluq/lava/common/natives/SystemNativeLibraryProperties.java @@ -1,53 +1,53 @@ package com.sedmelluq.lava.common.natives; public class SystemNativeLibraryProperties implements NativeLibraryProperties { - private final String libraryName; - private final String propertyPrefix; - - public SystemNativeLibraryProperties(String libraryName, String propertyPrefix) { - this.libraryName = libraryName; - this.propertyPrefix = propertyPrefix; - } - - @Override - public String getLibraryPath() { - return get("path"); - } - - @Override - public String getLibraryDirectory() { - return get("dir"); - } - - @Override - public String getExtractionPath() { - return get("extractPath"); - } - - @Override - public String getSystemName() { - return get("system"); - } - - @Override - public String getArchitectureName() { - return get("arch"); - } - - @Override - public String getLibraryFileNamePrefix() { - return get("libPrefix"); - } - - @Override - public String getLibraryFileNameSuffix() { - return get("libSuffix"); - } - - private String get(String property) { - return System.getProperty( - propertyPrefix + libraryName + "." + property, - System.getProperty(propertyPrefix + property) - ); - } + private final String libraryName; + private final String propertyPrefix; + + public SystemNativeLibraryProperties(String libraryName, String propertyPrefix) { + this.libraryName = libraryName; + this.propertyPrefix = propertyPrefix; + } + + @Override + public String getLibraryPath() { + return get("path"); + } + + @Override + public String getLibraryDirectory() { + return get("dir"); + } + + @Override + public String getExtractionPath() { + return get("extractPath"); + } + + @Override + public String getSystemName() { + return get("system"); + } + + @Override + public String getArchitectureName() { + return get("arch"); + } + + @Override + public String getLibraryFileNamePrefix() { + return get("libPrefix"); + } + + @Override + public String getLibraryFileNameSuffix() { + return get("libSuffix"); + } + + private String get(String property) { + return System.getProperty( + propertyPrefix + libraryName + "." + property, + System.getProperty(propertyPrefix + property) + ); + } } diff --git a/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/ArchitectureType.java b/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/ArchitectureType.java index 756fd0800..d9007b058 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/ArchitectureType.java +++ b/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/ArchitectureType.java @@ -1,8 +1,8 @@ package com.sedmelluq.lava.common.natives.architecture; public interface ArchitectureType { - /** - * @return Identifier used in directory names (combined with OS identifier) for this ABI - */ - String identifier(); + /** + * @return Identifier used in directory names (combined with OS identifier) for this ABI + */ + String identifier(); } diff --git a/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/DefaultArchitectureTypes.java b/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/DefaultArchitectureTypes.java index fe495d2b7..276356848 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/DefaultArchitectureTypes.java +++ b/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/DefaultArchitectureTypes.java @@ -6,59 +6,59 @@ import java.util.Map; public enum DefaultArchitectureTypes implements ArchitectureType { - ARM("arm", Arrays.asList("arm", "armeabi", "armv7b", "armv7l")), - ARM_HF("armhf", Arrays.asList("armeabihf", "armeabi-v7a")), - ARMv8_32("aarch32", Arrays.asList("armv8b", "armv8l")), - ARMv8_64("aarch64", Arrays.asList("arm64", "aarch64", "aarch64_be", "arm64-v8a")), + ARM("arm", Arrays.asList("arm", "armeabi", "armv7b", "armv7l")), + ARM_HF("armhf", Arrays.asList("armeabihf", "armeabi-v7a")), + ARMv8_32("aarch32", Arrays.asList("armv8b", "armv8l")), + ARMv8_64("aarch64", Arrays.asList("arm64", "aarch64", "aarch64_be", "arm64-v8a")), - MIPS_32("mips", Arrays.asList("mips")), - MIPS_32_LE("mipsel", Arrays.asList("mipsel", "mipsle")), - MIPS_64("mips64", Arrays.asList("mips64")), - MIPS_64_LE("mips64el", Arrays.asList("mips64el", "mips64le")), + MIPS_32("mips", Arrays.asList("mips")), + MIPS_32_LE("mipsel", Arrays.asList("mipsel", "mipsle")), + MIPS_64("mips64", Arrays.asList("mips64")), + MIPS_64_LE("mips64el", Arrays.asList("mips64el", "mips64le")), - PPC_32("powerpc", Arrays.asList("ppc", "powerpc")), - PPC_32_LE("powerpcle", Arrays.asList("ppcel", "ppcle")), - PPC_64("ppc64", Arrays.asList("ppc64")), - PPC_64_LE("ppc64le", Arrays.asList("ppc64el", "ppc64le")), + PPC_32("powerpc", Arrays.asList("ppc", "powerpc")), + PPC_32_LE("powerpcle", Arrays.asList("ppcel", "ppcle")), + PPC_64("ppc64", Arrays.asList("ppc64")), + PPC_64_LE("ppc64le", Arrays.asList("ppc64el", "ppc64le")), - X86_32("x86", Arrays.asList("x86", "i386", "i486", "i586", "i686")), - X86_64("x86-64", Arrays.asList("x86_64", "amd64")); + X86_32("x86", Arrays.asList("x86", "i386", "i486", "i586", "i686")), + X86_64("x86-64", Arrays.asList("x86_64", "amd64")); - public final String identifier; - public final List aliases; + public final String identifier; + public final List aliases; - DefaultArchitectureTypes(String identifier, List aliases) { - this.identifier = identifier; - this.aliases = aliases; - } + DefaultArchitectureTypes(String identifier, List aliases) { + this.identifier = identifier; + this.aliases = aliases; + } + + @Override + public String identifier() { + return identifier; + } - @Override - public String identifier() { - return identifier; - } + public static ArchitectureType detect() { + String architectureName = System.getProperty("os.arch"); + ArchitectureType type = aliasMap.get(architectureName); - public static ArchitectureType detect() { - String architectureName = System.getProperty("os.arch"); - ArchitectureType type = aliasMap.get(architectureName); + if (type == null) { + throw new IllegalArgumentException("Unknown architecture: " + architectureName); + } - if (type == null) { - throw new IllegalArgumentException("Unknown architecture: " + architectureName); + return type; } - return type; - } + private static Map aliasMap = createAliasMap(); - private static Map aliasMap = createAliasMap(); + private static Map createAliasMap() { + Map aliases = new HashMap<>(); - private static Map createAliasMap() { - Map aliases = new HashMap<>(); + for (DefaultArchitectureTypes value : values()) { + for (String alias : value.aliases) { + aliases.put(alias, value); + } + } - for (DefaultArchitectureTypes value : values()) { - for (String alias : value.aliases) { - aliases.put(alias, value); - } + return aliases; } - - return aliases; - } } diff --git a/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/DefaultOperatingSystemTypes.java b/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/DefaultOperatingSystemTypes.java index 11aded6b7..e33cd6aeb 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/DefaultOperatingSystemTypes.java +++ b/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/DefaultOperatingSystemTypes.java @@ -4,84 +4,83 @@ import org.slf4j.LoggerFactory; import java.io.BufferedReader; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Paths; public enum DefaultOperatingSystemTypes implements OperatingSystemType { - LINUX("linux", "lib", ".so"), - LINUX_MUSL("linux-musl", "lib", ".so"), - WINDOWS("win", "", ".dll"), - DARWIN("darwin", "lib", ".dylib"), - SOLARIS("solaris", "lib", ".so"); + LINUX("linux", "lib", ".so"), + LINUX_MUSL("linux-musl", "lib", ".so"), + WINDOWS("win", "", ".dll"), + DARWIN("darwin", "lib", ".dylib"), + SOLARIS("solaris", "lib", ".so"); - private static final Logger log = LoggerFactory.getLogger(DefaultOperatingSystemTypes.class); - private static volatile Boolean cachedMusl; + private static final Logger log = LoggerFactory.getLogger(DefaultOperatingSystemTypes.class); + private static volatile Boolean cachedMusl; - private final String identifier; - private final String libraryFilePrefix; - private final String libraryFileSuffix; + private final String identifier; + private final String libraryFilePrefix; + private final String libraryFileSuffix; - DefaultOperatingSystemTypes(String identifier, String libraryFilePrefix, String libraryFileSuffix) { - this.identifier = identifier; - this.libraryFilePrefix = libraryFilePrefix; - this.libraryFileSuffix = libraryFileSuffix; - } + DefaultOperatingSystemTypes(String identifier, String libraryFilePrefix, String libraryFileSuffix) { + this.identifier = identifier; + this.libraryFilePrefix = libraryFilePrefix; + this.libraryFileSuffix = libraryFileSuffix; + } - @Override - public String identifier() { - return identifier; - } + @Override + public String identifier() { + return identifier; + } - @Override - public String libraryFilePrefix() { - return libraryFilePrefix; - } + @Override + public String libraryFilePrefix() { + return libraryFilePrefix; + } - @Override - public String libraryFileSuffix() { - return libraryFileSuffix; - } + @Override + public String libraryFileSuffix() { + return libraryFileSuffix; + } - public static OperatingSystemType detect() { - String osFullName = System.getProperty("os.name"); + public static OperatingSystemType detect() { + String osFullName = System.getProperty("os.name"); - if(osFullName.startsWith("Windows")) { - return WINDOWS; - } else if(osFullName.startsWith("Mac OS X")) { - return DARWIN; - } else if(osFullName.startsWith("Solaris")) { - return SOLARIS; - } else if(osFullName.toLowerCase().startsWith("linux")) { - return checkMusl() ? LINUX_MUSL : LINUX; - } else { - throw new IllegalArgumentException("Unknown operating system: " + osFullName); + if (osFullName.startsWith("Windows")) { + return WINDOWS; + } else if (osFullName.startsWith("Mac OS X")) { + return DARWIN; + } else if (osFullName.startsWith("Solaris")) { + return SOLARIS; + } else if (osFullName.toLowerCase().startsWith("linux")) { + return checkMusl() ? LINUX_MUSL : LINUX; + } else { + throw new IllegalArgumentException("Unknown operating system: " + osFullName); + } } - } - private static boolean checkMusl() { - Boolean b = cachedMusl; - if(b == null) { - synchronized(DefaultOperatingSystemTypes.class) { - boolean check = false; - try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(Paths.get("/proc/self/maps"))))) { - String line; - while ((line = reader.readLine()) != null) { - if (line.contains("-musl-")) { - check = true; - break; + private static boolean checkMusl() { + Boolean b = cachedMusl; + if (b == null) { + synchronized (DefaultOperatingSystemTypes.class) { + boolean check = false; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(Paths.get("/proc/self/maps"))))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains("-musl-")) { + check = true; + break; + } + } + } catch (IOException fail) { + log.error("Failed to detect libc type, assuming glibc", fail); + check = false; + } + log.debug("is musl: {}", check); + b = cachedMusl = check; } - } - } catch(IOException fail) { - log.error("Failed to detect libc type, assuming glibc", fail); - check = false; } - log.debug("is musl: {}", check); - b = cachedMusl = check; - } + return b; } - return b; - } } diff --git a/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/OperatingSystemType.java b/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/OperatingSystemType.java index c9285a2c1..6eee4e02b 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/OperatingSystemType.java +++ b/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/OperatingSystemType.java @@ -1,18 +1,18 @@ package com.sedmelluq.lava.common.natives.architecture; public interface OperatingSystemType { - /** - * @return Identifier used in directory names (combined with architecture) for this OS - */ - String identifier(); + /** + * @return Identifier used in directory names (combined with architecture) for this OS + */ + String identifier(); - /** - * @return Prefix used for library file names. lib on most Unix flavors. - */ - String libraryFilePrefix(); + /** + * @return Prefix used for library file names. lib on most Unix flavors. + */ + String libraryFilePrefix(); - /** - * @return Suffix (extension) used for library file names. - */ - String libraryFileSuffix(); + /** + * @return Suffix (extension) used for library file names. + */ + String libraryFileSuffix(); } diff --git a/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/SystemType.java b/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/SystemType.java index 288524e37..886a152a5 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/SystemType.java +++ b/common/src/main/java/com/sedmelluq/lava/common/natives/architecture/SystemType.java @@ -1,77 +1,78 @@ package com.sedmelluq.lava.common.natives.architecture; import com.sedmelluq.lava.common.natives.NativeLibraryProperties; + import java.util.Optional; public class SystemType { - public final ArchitectureType architectureType; - public final OperatingSystemType osType; - - public SystemType(ArchitectureType architectureType, OperatingSystemType osType) { - this.architectureType = architectureType; - this.osType = osType; - } - - public String formatSystemName() { - if (osType.identifier() != null) { - if (osType == DefaultOperatingSystemTypes.DARWIN) { - return osType.identifier(); - } else { - return osType.identifier() + "-" + architectureType.identifier(); - } - } else { - return architectureType.identifier(); + public final ArchitectureType architectureType; + public final OperatingSystemType osType; + + public SystemType(ArchitectureType architectureType, OperatingSystemType osType) { + this.architectureType = architectureType; + this.osType = osType; } - } - - public String formatLibraryName(String libraryName) { - return osType.libraryFilePrefix() + libraryName + osType.libraryFileSuffix(); - } - - public static SystemType detect(NativeLibraryProperties properties) { - String systemName = properties.getSystemName(); - - if (systemName != null) { - return new SystemType( - () -> systemName, - new UnknownOperatingSystem( - Optional.ofNullable(properties.getLibraryFileNamePrefix()).orElse("lib"), - Optional.ofNullable(properties.getLibraryFileNameSuffix()).orElse(".so") - ) - ); + + public String formatSystemName() { + if (osType.identifier() != null) { + if (osType == DefaultOperatingSystemTypes.DARWIN) { + return osType.identifier(); + } else { + return osType.identifier() + "-" + architectureType.identifier(); + } + } else { + return architectureType.identifier(); + } } - OperatingSystemType osType = DefaultOperatingSystemTypes.detect(); + public String formatLibraryName(String libraryName) { + return osType.libraryFilePrefix() + libraryName + osType.libraryFileSuffix(); + } - String explicitArchitecture = properties.getArchitectureName(); - ArchitectureType architectureType = explicitArchitecture != null ? () -> explicitArchitecture : - DefaultArchitectureTypes.detect(); + public static SystemType detect(NativeLibraryProperties properties) { + String systemName = properties.getSystemName(); - return new SystemType(architectureType, osType); - } + if (systemName != null) { + return new SystemType( + () -> systemName, + new UnknownOperatingSystem( + Optional.ofNullable(properties.getLibraryFileNamePrefix()).orElse("lib"), + Optional.ofNullable(properties.getLibraryFileNameSuffix()).orElse(".so") + ) + ); + } - private static class UnknownOperatingSystem implements OperatingSystemType { - private final String libraryFilePrefix; - private final String libraryFileSuffix; + OperatingSystemType osType = DefaultOperatingSystemTypes.detect(); - private UnknownOperatingSystem(String libraryFilePrefix, String libraryFileSuffix) { - this.libraryFilePrefix = libraryFilePrefix; - this.libraryFileSuffix = libraryFileSuffix; - } + String explicitArchitecture = properties.getArchitectureName(); + ArchitectureType architectureType = explicitArchitecture != null ? () -> explicitArchitecture : + DefaultArchitectureTypes.detect(); - @Override - public String identifier() { - return null; + return new SystemType(architectureType, osType); } - @Override - public String libraryFilePrefix() { - return libraryFilePrefix; - } + private static class UnknownOperatingSystem implements OperatingSystemType { + private final String libraryFilePrefix; + private final String libraryFileSuffix; + + private UnknownOperatingSystem(String libraryFilePrefix, String libraryFileSuffix) { + this.libraryFilePrefix = libraryFilePrefix; + this.libraryFileSuffix = libraryFileSuffix; + } + + @Override + public String identifier() { + return null; + } + + @Override + public String libraryFilePrefix() { + return libraryFilePrefix; + } - @Override - public String libraryFileSuffix() { - return libraryFileSuffix; + @Override + public String libraryFileSuffix() { + return libraryFileSuffix; + } } - } } diff --git a/common/src/main/java/com/sedmelluq/lava/common/tools/DaemonThreadFactory.java b/common/src/main/java/com/sedmelluq/lava/common/tools/DaemonThreadFactory.java index 2c660c9d0..5b2d6a31a 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/tools/DaemonThreadFactory.java +++ b/common/src/main/java/com/sedmelluq/lava/common/tools/DaemonThreadFactory.java @@ -10,79 +10,79 @@ * Thread factory for daemon threads. */ public class DaemonThreadFactory implements ThreadFactory { - private static final Logger log = LoggerFactory.getLogger(DaemonThreadFactory.class); + private static final Logger log = LoggerFactory.getLogger(DaemonThreadFactory.class); - private static final AtomicInteger poolNumber = new AtomicInteger(1); - private final ThreadGroup group; - private final AtomicInteger threadNumber = new AtomicInteger(1); - private final String namePrefix; - private final Runnable exitCallback; + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + private final Runnable exitCallback; - /** - * @param name Name that will be included in thread names. - */ - public DaemonThreadFactory(String name) { - this(name, null); - } + /** + * @param name Name that will be included in thread names. + */ + public DaemonThreadFactory(String name) { + this(name, null); + } - /** - * @param name Name that will be included in thread names. - * @param exitCallback Runnable to be executed when the thread exits. - */ - public DaemonThreadFactory(String name, Runnable exitCallback) { - SecurityManager securityManager = System.getSecurityManager(); + /** + * @param name Name that will be included in thread names. + * @param exitCallback Runnable to be executed when the thread exits. + */ + public DaemonThreadFactory(String name, Runnable exitCallback) { + SecurityManager securityManager = System.getSecurityManager(); - group = (securityManager != null) ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup(); - namePrefix = "lava-daemon-pool-" + name + "-" + poolNumber.getAndIncrement() + "-thread-"; - this.exitCallback = exitCallback; - } + group = (securityManager != null) ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup(); + namePrefix = "lava-daemon-pool-" + name + "-" + poolNumber.getAndIncrement() + "-thread-"; + this.exitCallback = exitCallback; + } - @Override - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(group, getThreadRunnable(runnable), namePrefix + threadNumber.getAndIncrement(), 0); - thread.setDaemon(true); - thread.setPriority(Thread.NORM_PRIORITY); - return thread; - } + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(group, getThreadRunnable(runnable), namePrefix + threadNumber.getAndIncrement(), 0); + thread.setDaemon(true); + thread.setPriority(Thread.NORM_PRIORITY); + return thread; + } - private Runnable getThreadRunnable(Runnable target) { - if (exitCallback == null) { - return target; - } else { - return new ExitCallbackRunnable(target); + private Runnable getThreadRunnable(Runnable target) { + if (exitCallback == null) { + return target; + } else { + return new ExitCallbackRunnable(target); + } } - } - private class ExitCallbackRunnable implements Runnable { - private final Runnable original; + private class ExitCallbackRunnable implements Runnable { + private final Runnable original; - private ExitCallbackRunnable(Runnable original) { - this.original = original; - } + private ExitCallbackRunnable(Runnable original) { + this.original = original; + } - @Override - public void run() { - try { - if (original != null) { - original.run(); + @Override + public void run() { + try { + if (original != null) { + original.run(); + } + } finally { + wrapExitCallback(); + } } - } finally { - wrapExitCallback(); - } - } - private void wrapExitCallback() { - boolean wasInterrupted = Thread.interrupted(); + private void wrapExitCallback() { + boolean wasInterrupted = Thread.interrupted(); - try { - exitCallback.run(); - } catch (Throwable throwable) { - log.error("Thread exit notification threw an exception.", throwable); - } finally { - if (wasInterrupted) { - Thread.currentThread().interrupt(); + try { + exitCallback.run(); + } catch (Throwable throwable) { + log.error("Thread exit notification threw an exception.", throwable); + } finally { + if (wasInterrupted) { + Thread.currentThread().interrupt(); + } + } } - } } - } } diff --git a/common/src/main/java/com/sedmelluq/lava/common/tools/ExecutorTools.java b/common/src/main/java/com/sedmelluq/lava/common/tools/ExecutorTools.java index 4d6ffd402..f683041c0 100644 --- a/common/src/main/java/com/sedmelluq/lava/common/tools/ExecutorTools.java +++ b/common/src/main/java/com/sedmelluq/lava/common/tools/ExecutorTools.java @@ -3,128 +3,119 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; +import java.util.concurrent.*; /** * Utility methods for working with executors. */ public class ExecutorTools { - private static final Logger log = LoggerFactory.getLogger(ExecutorTools.class); - - private static final long WAIT_TIME = 1000L; - - /** - * A completed Future<Void> instance. - */ - public static final CompletedVoidFuture COMPLETED_VOID = new CompletedVoidFuture(); - - /** - * Shut down an executor and log the shutdown result. The executor is given a fixed amount of time to shut down, if it - * does not manage to do it in that time, then this method just returns. - * - * @param executorService Executor service to shut down - * @param description Description of the service to use for logging - */ - public static void shutdownExecutor(ExecutorService executorService, String description) { - if (executorService == null) { - return; + private static final Logger log = LoggerFactory.getLogger(ExecutorTools.class); + + private static final long WAIT_TIME = 1000L; + + /** + * A completed Future<Void> instance. + */ + public static final CompletedVoidFuture COMPLETED_VOID = new CompletedVoidFuture(); + + /** + * Shut down an executor and log the shutdown result. The executor is given a fixed amount of time to shut down, if it + * does not manage to do it in that time, then this method just returns. + * + * @param executorService Executor service to shut down + * @param description Description of the service to use for logging + */ + public static void shutdownExecutor(ExecutorService executorService, String description) { + if (executorService == null) { + return; + } + + log.debug("Shutting down executor {}", description); + + executorService.shutdownNow(); + + try { + if (!executorService.awaitTermination(WAIT_TIME, TimeUnit.MILLISECONDS)) { + log.debug("Executor {} did not shut down in {}", description, WAIT_TIME); + } else { + log.debug("Executor {} successfully shut down", description); + } + } catch (InterruptedException e) { + log.debug("Received an interruption while shutting down executor {}", description); + Thread.currentThread().interrupt(); + } } - log.debug("Shutting down executor {}", description); - - executorService.shutdownNow(); - - try { - if (!executorService.awaitTermination(WAIT_TIME, TimeUnit.MILLISECONDS)) { - log.debug("Executor {} did not shut down in {}", description, WAIT_TIME); - } else { - log.debug("Executor {} successfully shut down", description); - } - } catch (InterruptedException e) { - log.debug("Received an interruption while shutting down executor {}", description); - Thread.currentThread().interrupt(); - } - } - - /** - * Creates an executor which will use the queue only when maximum number of threads has been reached. The core pool - * size here only means the number of threads that are always alive, it is no longer used to check whether a new - * thread should start or not. The maximum size is otherwise pointless unless you have a bounded queue, which in turn - * would cause tasks to be rejected if it is too small. - * - * @param coreSize Number of threads that are always alive - * @param maximumSize The maximum number of threads in the pool - * @param timeout Non-core thread timeout in milliseconds - * @param threadFactory Thread factory to create pool threads with - * @return An eagerly scaling thread pool executor - */ - public static ThreadPoolExecutor createEagerlyScalingExecutor(int coreSize, int maximumSize, long timeout, - int queueCapacity, ThreadFactory threadFactory) { - - ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maximumSize, timeout, TimeUnit.MILLISECONDS, - new EagerlyScalingTaskQueue(queueCapacity), threadFactory); - - executor.setRejectedExecutionHandler(new EagerlyScalingRejectionHandler()); - return executor; - } - - private static class EagerlyScalingTaskQueue extends LinkedBlockingQueue { - public EagerlyScalingTaskQueue(int capacity) { - super(capacity); - } - - @Override - public boolean offer(Runnable runnable) { - return isEmpty() && super.offer(runnable); + /** + * Creates an executor which will use the queue only when maximum number of threads has been reached. The core pool + * size here only means the number of threads that are always alive, it is no longer used to check whether a new + * thread should start or not. The maximum size is otherwise pointless unless you have a bounded queue, which in turn + * would cause tasks to be rejected if it is too small. + * + * @param coreSize Number of threads that are always alive + * @param maximumSize The maximum number of threads in the pool + * @param timeout Non-core thread timeout in milliseconds + * @param threadFactory Thread factory to create pool threads with + * @return An eagerly scaling thread pool executor + */ + public static ThreadPoolExecutor createEagerlyScalingExecutor(int coreSize, int maximumSize, long timeout, + int queueCapacity, ThreadFactory threadFactory) { + + ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maximumSize, timeout, TimeUnit.MILLISECONDS, + new EagerlyScalingTaskQueue(queueCapacity), threadFactory); + + executor.setRejectedExecutionHandler(new EagerlyScalingRejectionHandler()); + return executor; } - public boolean offerDirectly(Runnable runnable) { - return super.offer(runnable); - } - } - - private static class EagerlyScalingRejectionHandler implements RejectedExecutionHandler { - @Override - public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) { - if (!((EagerlyScalingTaskQueue) executor.getQueue()).offerDirectly(runnable)) { - throw new RejectedExecutionException("Task " + runnable.toString() + " rejected from " + runnable.toString()); - } - } - } + private static class EagerlyScalingTaskQueue extends LinkedBlockingQueue { + public EagerlyScalingTaskQueue(int capacity) { + super(capacity); + } - private static class CompletedVoidFuture implements Future { - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - return false; - } - - @Override - public boolean isCancelled() { - return false; - } + @Override + public boolean offer(Runnable runnable) { + return isEmpty() && super.offer(runnable); + } - @Override - public boolean isDone() { - return true; + public boolean offerDirectly(Runnable runnable) { + return super.offer(runnable); + } } - @Override - public Void get() throws InterruptedException, ExecutionException { - return null; + private static class EagerlyScalingRejectionHandler implements RejectedExecutionHandler { + @Override + public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) { + if (!((EagerlyScalingTaskQueue) executor.getQueue()).offerDirectly(runnable)) { + throw new RejectedExecutionException("Task " + runnable.toString() + " rejected from " + runnable.toString()); + } + } } - @Override - public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - return null; + private static class CompletedVoidFuture implements Future { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public Void get() throws InterruptedException, ExecutionException { + return null; + } + + @Override + public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return null; + } } - } } diff --git a/demo-d4j/build.gradle b/demo-d4j/build.gradle index d2efbc944..8f707c91b 100644 --- a/demo-d4j/build.gradle +++ b/demo-d4j/build.gradle @@ -1,22 +1,22 @@ plugins { - id 'java' - id 'application' + id 'java' + id 'application' } repositories { - jcenter() - maven { - url 'https://jitpack.io' - } + jcenter() + maven { + url 'https://jitpack.io' + } } ext.d4jVersion = '3.0.11' dependencies { - compile "com.discord4j:discord4j-core:$d4jVersion" - compile "com.discord4j:discord4j-voice:$d4jVersion" - compile 'com.sedmelluq:lavaplayer:1.3.49' - runtime 'ch.qos.logback:logback-classic:1.2.3' + compile "com.discord4j:discord4j-core:$d4jVersion" + compile "com.discord4j:discord4j-voice:$d4jVersion" + compile 'com.sedmelluq:lavaplayer:1.3.49' + runtime 'ch.qos.logback:logback-classic:1.2.3' } mainClassName = 'com.sedmelluq.discord.lavaplayer.demo.d4j.Main' diff --git a/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/D4jAudioProvider.java b/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/D4jAudioProvider.java index 4c7d2d03d..034664d53 100644 --- a/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/D4jAudioProvider.java +++ b/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/D4jAudioProvider.java @@ -4,6 +4,7 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame; import discord4j.voice.AudioProvider; + import java.nio.ByteBuffer; /** @@ -12,19 +13,19 @@ * provide(). */ public class D4jAudioProvider extends AudioProvider { - private final MutableAudioFrame frame = new MutableAudioFrame(); - private final AudioPlayer player; + private final MutableAudioFrame frame = new MutableAudioFrame(); + private final AudioPlayer player; - public D4jAudioProvider(AudioPlayer player) { - super(ByteBuffer.allocate(StandardAudioDataFormats.DISCORD_OPUS.maximumChunkSize())); - this.player = player; - this.frame.setBuffer(getBuffer()); - } + public D4jAudioProvider(AudioPlayer player) { + super(ByteBuffer.allocate(StandardAudioDataFormats.DISCORD_OPUS.maximumChunkSize())); + this.player = player; + this.frame.setBuffer(getBuffer()); + } - @Override - public boolean provide() { - boolean didProvide = player.provide(frame); - if (didProvide) getBuffer().flip(); - return didProvide; - } -} \ No newline at end of file + @Override + public boolean provide() { + boolean didProvide = player.provide(frame); + if (didProvide) getBuffer().flip(); + return didProvide; + } +} diff --git a/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/GuildMusicManager.java b/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/GuildMusicManager.java index fad1f21d2..0f1db509f 100644 --- a/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/GuildMusicManager.java +++ b/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/GuildMusicManager.java @@ -7,25 +7,26 @@ * Holder for both the player and a track scheduler for one guild. */ public class GuildMusicManager { - /** - * Audio player for the guild. - */ - public final AudioPlayer player; - /** - * Track scheduler for the player. - */ - public final TrackScheduler scheduler; + /** + * Audio player for the guild. + */ + public final AudioPlayer player; + /** + * Track scheduler for the player. + */ + public final TrackScheduler scheduler; - public final D4jAudioProvider provider; + public final D4jAudioProvider provider; - /** - * Creates a player and a track scheduler. - * @param manager Audio player manager to use for creating the player. - */ - public GuildMusicManager(AudioPlayerManager manager) { - player = manager.createPlayer(); - scheduler = new TrackScheduler(player); - player.addListener(scheduler); - provider = new D4jAudioProvider(player); - } + /** + * Creates a player and a track scheduler. + * + * @param manager Audio player manager to use for creating the player. + */ + public GuildMusicManager(AudioPlayerManager manager) { + player = manager.createPlayer(); + scheduler = new TrackScheduler(player); + player.addListener(scheduler); + provider = new D4jAudioProvider(player); + } } diff --git a/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/Main.java b/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/Main.java index eaf6378bd..293cdf308 100644 --- a/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/Main.java +++ b/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/Main.java @@ -16,130 +16,132 @@ import discord4j.core.object.entity.MessageChannel; import discord4j.core.object.entity.TextChannel; import discord4j.core.object.entity.VoiceChannel; + import java.util.Map; import java.util.concurrent.ConcurrentHashMap; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Main { - private static final Logger log = LoggerFactory.getLogger(Main.class); + private static final Logger log = LoggerFactory.getLogger(Main.class); - public static void main(String[] args) { - DiscordClient client = new DiscordClientBuilder(System.getProperty("botToken")).build(); - new Main().registerListeners(client.getEventDispatcher()); - client.login().block(); - } + public static void main(String[] args) { + DiscordClient client = new DiscordClientBuilder(System.getProperty("botToken")).build(); + new Main().registerListeners(client.getEventDispatcher()); + client.login().block(); + } - private final AudioPlayerManager playerManager; - private final Map musicManagers; + private final AudioPlayerManager playerManager; + private final Map musicManagers; - private Main() { - this.musicManagers = new ConcurrentHashMap<>(); + private Main() { + this.musicManagers = new ConcurrentHashMap<>(); - this.playerManager = new DefaultAudioPlayerManager(); - AudioSourceManagers.registerRemoteSources(playerManager); - AudioSourceManagers.registerLocalSource(playerManager); - } + this.playerManager = new DefaultAudioPlayerManager(); + AudioSourceManagers.registerRemoteSources(playerManager); + AudioSourceManagers.registerLocalSource(playerManager); + } - private void registerListeners(EventDispatcher eventDispatcher) { - eventDispatcher.on(MessageCreateEvent.class).subscribe(this::onMessageReceived); - } + private void registerListeners(EventDispatcher eventDispatcher) { + eventDispatcher.on(MessageCreateEvent.class).subscribe(this::onMessageReceived); + } - private synchronized GuildMusicManager getGuildAudioPlayer(Guild guild) { - long guildId = guild.getId().asLong(); - GuildMusicManager musicManager = musicManagers.get(guildId); + private synchronized GuildMusicManager getGuildAudioPlayer(Guild guild) { + long guildId = guild.getId().asLong(); + GuildMusicManager musicManager = musicManagers.get(guildId); - if (musicManager == null) { - musicManager = new GuildMusicManager(playerManager); - musicManagers.put(guildId, musicManager); + if (musicManager == null) { + musicManager = new GuildMusicManager(playerManager); + musicManagers.put(guildId, musicManager); + } + + return musicManager; } - return musicManager; - } + private void onMessageReceived(MessageCreateEvent event) { + Message message = event.getMessage(); - private void onMessageReceived(MessageCreateEvent event) { - Message message = event.getMessage(); + message.getContent().ifPresent(it -> { + MessageChannel channel = message.getChannel().block(); - message.getContent().ifPresent(it -> { - MessageChannel channel = message.getChannel().block(); + if (channel instanceof TextChannel) { + String[] command = it.split(" ", 2); - if (channel instanceof TextChannel) { - String[] command = it.split(" ", 2); + if ("~play".equals(command[0]) && command.length == 2) { + loadAndPlay((TextChannel) channel, command[1]); + } else if ("~skip".equals(command[0])) { + skipTrack((TextChannel) channel); + } + } + }); + } - if ("~play".equals(command[0]) && command.length == 2) { - loadAndPlay((TextChannel) channel, command[1]); - } else if ("~skip".equals(command[0])) { - skipTrack((TextChannel) channel); - } - } - }); - } + private void loadAndPlay(TextChannel channel, final String trackUrl) { + GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild().block()); - private void loadAndPlay(TextChannel channel, final String trackUrl) { - GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild().block()); + playerManager.loadItemOrdered(musicManager, trackUrl, new AudioLoadResultHandler() { + @Override + public void trackLoaded(AudioTrack track) { + sendMessageToChannel(channel, "Adding to queue " + track.getInfo().title); - playerManager.loadItemOrdered(musicManager, trackUrl, new AudioLoadResultHandler() { - @Override - public void trackLoaded(AudioTrack track) { - sendMessageToChannel(channel, "Adding to queue " + track.getInfo().title); + play(channel.getGuild().block(), musicManager, track); + } - play(channel.getGuild().block(), musicManager, track); - } + @Override + public void playlistLoaded(AudioPlaylist playlist) { + AudioTrack firstTrack = playlist.getSelectedTrack(); - @Override - public void playlistLoaded(AudioPlaylist playlist) { - AudioTrack firstTrack = playlist.getSelectedTrack(); + if (firstTrack == null) { + firstTrack = playlist.getTracks().get(0); + } - if (firstTrack == null) { - firstTrack = playlist.getTracks().get(0); - } + sendMessageToChannel(channel, "Adding to queue " + firstTrack.getInfo().title + " (first track of playlist " + playlist.getName() + ")"); + + play(channel.getGuild().block(), musicManager, firstTrack); + } - sendMessageToChannel(channel, "Adding to queue " + firstTrack.getInfo().title + " (first track of playlist " + playlist.getName() + ")"); - - play(channel.getGuild().block(), musicManager, firstTrack); - } - - @Override - public void noMatches() { - sendMessageToChannel(channel, "Nothing found by " + trackUrl); - } - - @Override - public void loadFailed(FriendlyException exception) { - sendMessageToChannel(channel, "Could not play: " + exception.getMessage()); - } - }); - } - - private void play(Guild guild, GuildMusicManager musicManager, AudioTrack track) { - GuildMusicManager manager = getGuildAudioPlayer(guild); - attachToFirstVoiceChannel(guild, manager.provider); - musicManager.scheduler.queue(track); - } - - private void skipTrack(TextChannel channel) { - GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild().block()); - musicManager.scheduler.nextTrack(); - - sendMessageToChannel(channel, "Skipped to next track."); - } - - private void sendMessageToChannel(TextChannel channel, String message) { - try { - channel.createMessage(message).block(); - } catch (Exception e) { - log.warn("Failed to send message {} to {}", message, channel.getName(), e); + @Override + public void noMatches() { + sendMessageToChannel(channel, "Nothing found by " + trackUrl); + } + + @Override + public void loadFailed(FriendlyException exception) { + sendMessageToChannel(channel, "Could not play: " + exception.getMessage()); + } + }); + } + + private void play(Guild guild, GuildMusicManager musicManager, AudioTrack track) { + GuildMusicManager manager = getGuildAudioPlayer(guild); + attachToFirstVoiceChannel(guild, manager.provider); + musicManager.scheduler.queue(track); + } + + private void skipTrack(TextChannel channel) { + GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild().block()); + musicManager.scheduler.nextTrack(); + + sendMessageToChannel(channel, "Skipped to next track."); } - } - private static void attachToFirstVoiceChannel(Guild guild, D4jAudioProvider provider) { - VoiceChannel voiceChannel = guild.getChannels().ofType(VoiceChannel.class).blockFirst(); - boolean inVoiceChannel = guild.getVoiceStates() // Check if any VoiceState for this guild relates to bot - .any(voiceState -> guild.getClient().getSelfId().map(voiceState.getUserId()::equals).orElse(false)) - .block(); + private void sendMessageToChannel(TextChannel channel, String message) { + try { + channel.createMessage(message).block(); + } catch (Exception e) { + log.warn("Failed to send message {} to {}", message, channel.getName(), e); + } + } - if (!inVoiceChannel) { - voiceChannel.join(spec -> spec.setProvider(provider)).block(); + private static void attachToFirstVoiceChannel(Guild guild, D4jAudioProvider provider) { + VoiceChannel voiceChannel = guild.getChannels().ofType(VoiceChannel.class).blockFirst(); + boolean inVoiceChannel = guild.getVoiceStates() // Check if any VoiceState for this guild relates to bot + .any(voiceState -> guild.getClient().getSelfId().map(voiceState.getUserId()::equals).orElse(false)) + .block(); + + if (!inVoiceChannel) { + voiceChannel.join(spec -> spec.setProvider(provider)).block(); + } } - } } diff --git a/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/TrackScheduler.java b/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/TrackScheduler.java index afc989b74..10750457a 100644 --- a/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/TrackScheduler.java +++ b/demo-d4j/src/main/java/com/sedmelluq/discord/lavaplayer/demo/d4j/TrackScheduler.java @@ -12,45 +12,45 @@ * This class schedules tracks for the audio player. It contains the queue of tracks. */ public class TrackScheduler extends AudioEventAdapter { - private final AudioPlayer player; - private final BlockingQueue queue; + private final AudioPlayer player; + private final BlockingQueue queue; - /** - * @param player The audio player this scheduler uses - */ - public TrackScheduler(AudioPlayer player) { - this.player = player; - this.queue = new LinkedBlockingQueue<>(); - } + /** + * @param player The audio player this scheduler uses + */ + public TrackScheduler(AudioPlayer player) { + this.player = player; + this.queue = new LinkedBlockingQueue<>(); + } - /** - * Add the next track to queue or play right away if nothing is in the queue. - * - * @param track The track to play or add to queue. - */ - public void queue(AudioTrack track) { - // Calling startTrack with the noInterrupt set to true will start the track only if nothing is currently playing. If - // something is playing, it returns false and does nothing. In that case the player was already playing so this - // track goes to the queue instead. - if (!player.startTrack(track, true)) { - queue.offer(track); + /** + * Add the next track to queue or play right away if nothing is in the queue. + * + * @param track The track to play or add to queue. + */ + public void queue(AudioTrack track) { + // Calling startTrack with the noInterrupt set to true will start the track only if nothing is currently playing. If + // something is playing, it returns false and does nothing. In that case the player was already playing so this + // track goes to the queue instead. + if (!player.startTrack(track, true)) { + queue.offer(track); + } } - } - /** - * Start the next track, stopping the current one if it is playing. - */ - public void nextTrack() { - // Start the next track, regardless of if something is already playing or not. In case queue was empty, we are - // giving null to startTrack, which is a valid argument and will simply stop the player. - player.startTrack(queue.poll(), false); - } + /** + * Start the next track, stopping the current one if it is playing. + */ + public void nextTrack() { + // Start the next track, regardless of if something is already playing or not. In case queue was empty, we are + // giving null to startTrack, which is a valid argument and will simply stop the player. + player.startTrack(queue.poll(), false); + } - @Override - public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { - // Only start the next track if the end reason is suitable for it (FINISHED or LOAD_FAILED) - if (endReason.mayStartNext) { - nextTrack(); + @Override + public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { + // Only start the next track if the end reason is suitable for it (FINISHED or LOAD_FAILED) + if (endReason.mayStartNext) { + nextTrack(); + } } - } } diff --git a/demo-jda/build.gradle b/demo-jda/build.gradle index cca2d6ee6..3ba5c1f45 100644 --- a/demo-jda/build.gradle +++ b/demo-jda/build.gradle @@ -1,16 +1,16 @@ plugins { - id 'java' - id 'application' + id 'java' + id 'application' } repositories { - jcenter() + jcenter() } dependencies { - compile 'net.dv8tion:JDA:4.0.0_68' - compile 'com.sedmelluq:lavaplayer:1.3.49' - runtime 'ch.qos.logback:logback-classic:1.2.3' + compile 'net.dv8tion:JDA:4.0.0_68' + compile 'com.sedmelluq:lavaplayer:1.3.49' + runtime 'ch.qos.logback:logback-classic:1.2.3' } mainClassName = 'com.sedmelluq.discord.lavaplayer.demo.jda.Main' diff --git a/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/AudioPlayerSendHandler.java b/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/AudioPlayerSendHandler.java index fca2a7a14..eb22c6f57 100644 --- a/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/AudioPlayerSendHandler.java +++ b/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/AudioPlayerSendHandler.java @@ -2,7 +2,9 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame; + import java.nio.Buffer; + import net.dv8tion.jda.api.audio.AudioSendHandler; import java.nio.ByteBuffer; @@ -13,35 +15,35 @@ * provide20MsAudio(). */ public class AudioPlayerSendHandler implements AudioSendHandler { - private final AudioPlayer audioPlayer; - private final ByteBuffer buffer; - private final MutableAudioFrame frame; - - /** - * @param audioPlayer Audio player to wrap. - */ - public AudioPlayerSendHandler(AudioPlayer audioPlayer) { - this.audioPlayer = audioPlayer; - this.buffer = ByteBuffer.allocate(1024); - this.frame = new MutableAudioFrame(); - this.frame.setBuffer(buffer); - } - - @Override - public boolean canProvide() { - // returns true if audio was provided - return audioPlayer.provide(frame); - } - - @Override - public ByteBuffer provide20MsAudio() { - // flip to make it a read buffer - ((Buffer) buffer).flip(); - return buffer; - } - - @Override - public boolean isOpus() { - return true; - } + private final AudioPlayer audioPlayer; + private final ByteBuffer buffer; + private final MutableAudioFrame frame; + + /** + * @param audioPlayer Audio player to wrap. + */ + public AudioPlayerSendHandler(AudioPlayer audioPlayer) { + this.audioPlayer = audioPlayer; + this.buffer = ByteBuffer.allocate(1024); + this.frame = new MutableAudioFrame(); + this.frame.setBuffer(buffer); + } + + @Override + public boolean canProvide() { + // returns true if audio was provided + return audioPlayer.provide(frame); + } + + @Override + public ByteBuffer provide20MsAudio() { + // flip to make it a read buffer + ((Buffer) buffer).flip(); + return buffer; + } + + @Override + public boolean isOpus() { + return true; + } } diff --git a/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/GuildMusicManager.java b/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/GuildMusicManager.java index e7aea0cc1..18ebf7be2 100644 --- a/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/GuildMusicManager.java +++ b/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/GuildMusicManager.java @@ -7,29 +7,30 @@ * Holder for both the player and a track scheduler for one guild. */ public class GuildMusicManager { - /** - * Audio player for the guild. - */ - public final AudioPlayer player; - /** - * Track scheduler for the player. - */ - public final TrackScheduler scheduler; + /** + * Audio player for the guild. + */ + public final AudioPlayer player; + /** + * Track scheduler for the player. + */ + public final TrackScheduler scheduler; - /** - * Creates a player and a track scheduler. - * @param manager Audio player manager to use for creating the player. - */ - public GuildMusicManager(AudioPlayerManager manager) { - player = manager.createPlayer(); - scheduler = new TrackScheduler(player); - player.addListener(scheduler); - } + /** + * Creates a player and a track scheduler. + * + * @param manager Audio player manager to use for creating the player. + */ + public GuildMusicManager(AudioPlayerManager manager) { + player = manager.createPlayer(); + scheduler = new TrackScheduler(player); + player.addListener(scheduler); + } - /** - * @return Wrapper around AudioPlayer to use it as an AudioSendHandler. - */ - public AudioPlayerSendHandler getSendHandler() { - return new AudioPlayerSendHandler(player); - } + /** + * @return Wrapper around AudioPlayer to use it as an AudioSendHandler. + */ + public AudioPlayerSendHandler getSendHandler() { + return new AudioPlayerSendHandler(player); + } } diff --git a/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/Main.java b/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/Main.java index d1f5577db..fae553e89 100644 --- a/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/Main.java +++ b/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/Main.java @@ -20,106 +20,106 @@ import java.util.Map; public class Main extends ListenerAdapter { - public static void main(String[] args) throws Exception { - new JDABuilder(AccountType.BOT) - .setToken(System.getProperty("botToken")) - .addEventListeners(new Main()) - .build(); - } - - private final AudioPlayerManager playerManager; - private final Map musicManagers; - - private Main() { - this.musicManagers = new HashMap<>(); - - this.playerManager = new DefaultAudioPlayerManager(); - AudioSourceManagers.registerRemoteSources(playerManager); - AudioSourceManagers.registerLocalSource(playerManager); - } - - private synchronized GuildMusicManager getGuildAudioPlayer(Guild guild) { - long guildId = Long.parseLong(guild.getId()); - GuildMusicManager musicManager = musicManagers.get(guildId); - - if (musicManager == null) { - musicManager = new GuildMusicManager(playerManager); - musicManagers.put(guildId, musicManager); + public static void main(String[] args) throws Exception { + new JDABuilder(AccountType.BOT) + .setToken(System.getProperty("botToken")) + .addEventListeners(new Main()) + .build(); } - guild.getAudioManager().setSendingHandler(musicManager.getSendHandler()); + private final AudioPlayerManager playerManager; + private final Map musicManagers; - return musicManager; - } + private Main() { + this.musicManagers = new HashMap<>(); - @Override - public void onGuildMessageReceived(GuildMessageReceivedEvent event) { - String[] command = event.getMessage().getContentRaw().split(" ", 2); - - if ("~play".equals(command[0]) && command.length == 2) { - loadAndPlay(event.getChannel(), command[1]); - } else if ("~skip".equals(command[0])) { - skipTrack(event.getChannel()); + this.playerManager = new DefaultAudioPlayerManager(); + AudioSourceManagers.registerRemoteSources(playerManager); + AudioSourceManagers.registerLocalSource(playerManager); } - super.onGuildMessageReceived(event); - } + private synchronized GuildMusicManager getGuildAudioPlayer(Guild guild) { + long guildId = Long.parseLong(guild.getId()); + GuildMusicManager musicManager = musicManagers.get(guildId); - private void loadAndPlay(final TextChannel channel, final String trackUrl) { - GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild()); + if (musicManager == null) { + musicManager = new GuildMusicManager(playerManager); + musicManagers.put(guildId, musicManager); + } - playerManager.loadItemOrdered(musicManager, trackUrl, new AudioLoadResultHandler() { - @Override - public void trackLoaded(AudioTrack track) { - channel.sendMessage("Adding to queue " + track.getInfo().title).queue(); + guild.getAudioManager().setSendingHandler(musicManager.getSendHandler()); - play(channel.getGuild(), musicManager, track); - } + return musicManager; + } - @Override - public void playlistLoaded(AudioPlaylist playlist) { - AudioTrack firstTrack = playlist.getSelectedTrack(); + @Override + public void onGuildMessageReceived(GuildMessageReceivedEvent event) { + String[] command = event.getMessage().getContentRaw().split(" ", 2); - if (firstTrack == null) { - firstTrack = playlist.getTracks().get(0); + if ("~play".equals(command[0]) && command.length == 2) { + loadAndPlay(event.getChannel(), command[1]); + } else if ("~skip".equals(command[0])) { + skipTrack(event.getChannel()); } - channel.sendMessage("Adding to queue " + firstTrack.getInfo().title + " (first track of playlist " + playlist.getName() + ")").queue(); + super.onGuildMessageReceived(event); + } + + private void loadAndPlay(final TextChannel channel, final String trackUrl) { + GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild()); + + playerManager.loadItemOrdered(musicManager, trackUrl, new AudioLoadResultHandler() { + @Override + public void trackLoaded(AudioTrack track) { + channel.sendMessage("Adding to queue " + track.getInfo().title).queue(); + + play(channel.getGuild(), musicManager, track); + } - play(channel.getGuild(), musicManager, firstTrack); - } + @Override + public void playlistLoaded(AudioPlaylist playlist) { + AudioTrack firstTrack = playlist.getSelectedTrack(); - @Override - public void noMatches() { - channel.sendMessage("Nothing found by " + trackUrl).queue(); - } + if (firstTrack == null) { + firstTrack = playlist.getTracks().get(0); + } - @Override - public void loadFailed(FriendlyException exception) { - channel.sendMessage("Could not play: " + exception.getMessage()).queue(); - } - }); - } + channel.sendMessage("Adding to queue " + firstTrack.getInfo().title + " (first track of playlist " + playlist.getName() + ")").queue(); - private void play(Guild guild, GuildMusicManager musicManager, AudioTrack track) { - connectToFirstVoiceChannel(guild.getAudioManager()); + play(channel.getGuild(), musicManager, firstTrack); + } - musicManager.scheduler.queue(track); - } + @Override + public void noMatches() { + channel.sendMessage("Nothing found by " + trackUrl).queue(); + } - private void skipTrack(TextChannel channel) { - GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild()); - musicManager.scheduler.nextTrack(); + @Override + public void loadFailed(FriendlyException exception) { + channel.sendMessage("Could not play: " + exception.getMessage()).queue(); + } + }); + } + + private void play(Guild guild, GuildMusicManager musicManager, AudioTrack track) { + connectToFirstVoiceChannel(guild.getAudioManager()); + + musicManager.scheduler.queue(track); + } - channel.sendMessage("Skipped to next track.").queue(); - } + private void skipTrack(TextChannel channel) { + GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild()); + musicManager.scheduler.nextTrack(); - private static void connectToFirstVoiceChannel(AudioManager audioManager) { - if (!audioManager.isConnected() && !audioManager.isAttemptingToConnect()) { - for (VoiceChannel voiceChannel : audioManager.getGuild().getVoiceChannels()) { - audioManager.openAudioConnection(voiceChannel); - break; - } + channel.sendMessage("Skipped to next track.").queue(); + } + + private static void connectToFirstVoiceChannel(AudioManager audioManager) { + if (!audioManager.isConnected() && !audioManager.isAttemptingToConnect()) { + for (VoiceChannel voiceChannel : audioManager.getGuild().getVoiceChannels()) { + audioManager.openAudioConnection(voiceChannel); + break; + } + } } - } } diff --git a/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/TrackScheduler.java b/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/TrackScheduler.java index 81d0a18c8..d07181ba5 100644 --- a/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/TrackScheduler.java +++ b/demo-jda/src/main/java/com/sedmelluq/discord/lavaplayer/demo/jda/TrackScheduler.java @@ -12,45 +12,45 @@ * This class schedules tracks for the audio player. It contains the queue of tracks. */ public class TrackScheduler extends AudioEventAdapter { - private final AudioPlayer player; - private final BlockingQueue queue; + private final AudioPlayer player; + private final BlockingQueue queue; - /** - * @param player The audio player this scheduler uses - */ - public TrackScheduler(AudioPlayer player) { - this.player = player; - this.queue = new LinkedBlockingQueue<>(); - } + /** + * @param player The audio player this scheduler uses + */ + public TrackScheduler(AudioPlayer player) { + this.player = player; + this.queue = new LinkedBlockingQueue<>(); + } - /** - * Add the next track to queue or play right away if nothing is in the queue. - * - * @param track The track to play or add to queue. - */ - public void queue(AudioTrack track) { - // Calling startTrack with the noInterrupt set to true will start the track only if nothing is currently playing. If - // something is playing, it returns false and does nothing. In that case the player was already playing so this - // track goes to the queue instead. - if (!player.startTrack(track, true)) { - queue.offer(track); + /** + * Add the next track to queue or play right away if nothing is in the queue. + * + * @param track The track to play or add to queue. + */ + public void queue(AudioTrack track) { + // Calling startTrack with the noInterrupt set to true will start the track only if nothing is currently playing. If + // something is playing, it returns false and does nothing. In that case the player was already playing so this + // track goes to the queue instead. + if (!player.startTrack(track, true)) { + queue.offer(track); + } } - } - /** - * Start the next track, stopping the current one if it is playing. - */ - public void nextTrack() { - // Start the next track, regardless of if something is already playing or not. In case queue was empty, we are - // giving null to startTrack, which is a valid argument and will simply stop the player. - player.startTrack(queue.poll(), false); - } + /** + * Start the next track, stopping the current one if it is playing. + */ + public void nextTrack() { + // Start the next track, regardless of if something is already playing or not. In case queue was empty, we are + // giving null to startTrack, which is a valid argument and will simply stop the player. + player.startTrack(queue.poll(), false); + } - @Override - public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { - // Only start the next track if the end reason is suitable for it (FINISHED or LOAD_FAILED) - if (endReason.mayStartNext) { - nextTrack(); + @Override + public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { + // Only start the next track if the end reason is suitable for it (FINISHED or LOAD_FAILED) + if (endReason.mayStartNext) { + nextTrack(); + } } - } } diff --git a/extensions/format-xm/build.gradle.kts b/extensions/format-xm/build.gradle.kts index 5b0c564cb..7228effdd 100644 --- a/extensions/format-xm/build.gradle.kts +++ b/extensions/format-xm/build.gradle.kts @@ -2,19 +2,19 @@ import com.vanniktech.maven.publish.JavaLibrary import com.vanniktech.maven.publish.JavadocJar plugins { - `java-library` - alias(libs.plugins.maven.publish.base) + `java-library` + alias(libs.plugins.maven.publish.base) } base { - archivesName = "lavaplayer-ext-format-xm" + archivesName = "lavaplayer-ext-format-xm" } dependencies { - compileOnly(projects.main) - implementation(libs.ibxm.fork) - implementation(libs.slf4j) + compileOnly(projects.main) + implementation(libs.ibxm.fork) + implementation(libs.slf4j) } mavenPublishing { - configure(JavaLibrary(JavadocJar.Javadoc())) + configure(JavaLibrary(JavadocJar.Javadoc())) } diff --git a/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmAudioTrack.java b/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmAudioTrack.java index c2ecbbdf1..632b174ee 100644 --- a/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmAudioTrack.java +++ b/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmAudioTrack.java @@ -9,29 +9,29 @@ import org.slf4j.LoggerFactory; public class XmAudioTrack extends BaseAudioTrack { - private static final Logger log = LoggerFactory.getLogger(WavAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(WavAudioTrack.class); - private final SeekableInputStream inputStream; + private final SeekableInputStream inputStream; - /** - * @param trackInfo Track info - * @param inputStream Input stream for the WAV file - */ - public XmAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param inputStream Input stream for the WAV file + */ + public XmAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + super(trackInfo); - this.inputStream = inputStream; - } + this.inputStream = inputStream; + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - XmTrackProvider trackProvider = new XmFileLoader(inputStream).loadTrack(localExecutor.getProcessingContext()); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + XmTrackProvider trackProvider = new XmFileLoader(inputStream).loadTrack(localExecutor.getProcessingContext()); - try { - log.debug("Starting to play module {}", getIdentifier()); - localExecutor.executeProcessingLoop(trackProvider::provideFrames, null); - } finally { - trackProvider.close(); + try { + log.debug("Starting to play module {}", getIdentifier()); + localExecutor.executeProcessingLoop(trackProvider::provideFrames, null); + } finally { + trackProvider.close(); + } } - } } diff --git a/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmContainerProbe.java b/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmContainerProbe.java index 2d2a4d855..2b3bedc4f 100644 --- a/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmContainerProbe.java +++ b/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmContainerProbe.java @@ -9,53 +9,54 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; import ibxm.Module; -import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; + import static com.sedmelluq.discord.lavaplayer.container.MediaContainerDetection.UNKNOWN_ARTIST; import static com.sedmelluq.discord.lavaplayer.container.MediaContainerDetectionResult.supportedFormat; public class XmContainerProbe implements MediaContainerProbe { - private static final Logger log = LoggerFactory.getLogger(XmContainerProbe.class); - - @Override - public String getName() { - return "xm"; - } - - @Override - public boolean matchesHints(MediaContainerHints hints) { - return false; - } - - @Override - public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { - Module module; - try { - module = new Module(inputStream); - } catch (IllegalArgumentException e) { - return null; + private static final Logger log = LoggerFactory.getLogger(XmContainerProbe.class); + + @Override + public String getName() { + return "xm"; } - log.debug("Track {} is a module.", reference.identifier); - - inputStream.seek(0); - - return supportedFormat(this, null, new AudioTrackInfo( - module.songName, - UNKNOWN_ARTIST, - Units.DURATION_MS_UNKNOWN, - reference.identifier, - true, - reference.identifier, - null, - null - )); - } - - @Override - public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - return new XmAudioTrack(trackInfo, inputStream); - } + @Override + public boolean matchesHints(MediaContainerHints hints) { + return false; + } + + @Override + public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { + Module module; + try { + module = new Module(inputStream); + } catch (IllegalArgumentException e) { + return null; + } + + log.debug("Track {} is a module.", reference.identifier); + + inputStream.seek(0); + + return supportedFormat(this, null, new AudioTrackInfo( + module.songName, + UNKNOWN_ARTIST, + Units.DURATION_MS_UNKNOWN, + reference.identifier, + true, + reference.identifier, + null, + null + )); + } + + @Override + public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + return new XmAudioTrack(trackInfo, inputStream); + } } diff --git a/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmFileLoader.java b/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmFileLoader.java index fa563accf..1d32f6e1d 100644 --- a/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmFileLoader.java +++ b/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmFileLoader.java @@ -9,16 +9,16 @@ import java.io.IOException; public class XmFileLoader { - private final SeekableInputStream inputStream; + private final SeekableInputStream inputStream; - public XmFileLoader(SeekableInputStream inputStream) { - this.inputStream = inputStream; - } + public XmFileLoader(SeekableInputStream inputStream) { + this.inputStream = inputStream; + } - public XmTrackProvider loadTrack(AudioProcessingContext context) throws IOException { - Module module = new Module(inputStream); - IBXM ibxm = new IBXM(module, context.outputFormat.sampleRate); - ibxm.setInterpolation(Channel.SINC); - return new XmTrackProvider(context, ibxm); - } + public XmTrackProvider loadTrack(AudioProcessingContext context) throws IOException { + Module module = new Module(inputStream); + IBXM ibxm = new IBXM(module, context.outputFormat.sampleRate); + ibxm.setInterpolation(Channel.SINC); + return new XmTrackProvider(context, ibxm); + } } diff --git a/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmTrackProvider.java b/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmTrackProvider.java index 9c3017ef4..6bb9af4ef 100644 --- a/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmTrackProvider.java +++ b/extensions/format-xm/src/main/java/com/sedmelluq/lavaplayer/extensions/format/xm/XmTrackProvider.java @@ -7,31 +7,31 @@ import ibxm.IBXM; public class XmTrackProvider { - private final IBXM ibxm; - private final AudioPipeline downstream; - private final int blocksInBuffer; + private final IBXM ibxm; + private final AudioPipeline downstream; + private final int blocksInBuffer; - public XmTrackProvider(AudioProcessingContext context, IBXM ibxm) { - this.ibxm = ibxm; - this.downstream = AudioPipelineFactory.create(context, new PcmFormat(2, ibxm.getSampleRate())); - this.blocksInBuffer = ibxm.getMixBufferLength(); - } + public XmTrackProvider(AudioProcessingContext context, IBXM ibxm) { + this.ibxm = ibxm; + this.downstream = AudioPipelineFactory.create(context, new PcmFormat(2, ibxm.getSampleRate())); + this.blocksInBuffer = ibxm.getMixBufferLength(); + } - public void provideFrames() throws InterruptedException { - int blockCount; - int[] buffer = new int[blocksInBuffer]; - short[] shortBuffer = new short[blocksInBuffer]; + public void provideFrames() throws InterruptedException { + int blockCount; + int[] buffer = new int[blocksInBuffer]; + short[] shortBuffer = new short[blocksInBuffer]; - while ((blockCount = ibxm.getAudio(buffer)) > 0) { - for (int i = 0; i < blocksInBuffer; i++) { - shortBuffer[i] = (short) Math.max(-32678, Math.min(buffer[i], 32767)); - } + while ((blockCount = ibxm.getAudio(buffer)) > 0) { + for (int i = 0; i < blocksInBuffer; i++) { + shortBuffer[i] = (short) Math.max(-32678, Math.min(buffer[i], 32767)); + } - downstream.process(shortBuffer, 0, blockCount * 2); + downstream.process(shortBuffer, 0, blockCount * 2); + } } - } - public void close() { - downstream.close(); - } + public void close() { + downstream.close(); + } } diff --git a/extensions/youtube-rotator/build.gradle.kts b/extensions/youtube-rotator/build.gradle.kts index 6654944a8..24630162c 100644 --- a/extensions/youtube-rotator/build.gradle.kts +++ b/extensions/youtube-rotator/build.gradle.kts @@ -2,19 +2,19 @@ import com.vanniktech.maven.publish.JavaLibrary import com.vanniktech.maven.publish.JavadocJar plugins { - `java-library` - alias(libs.plugins.maven.publish.base) + `java-library` + alias(libs.plugins.maven.publish.base) } base { - archivesName = "lavaplayer-ext-youtube-rotator" + archivesName = "lavaplayer-ext-youtube-rotator" } dependencies { - compileOnly(projects.main) - implementation(libs.slf4j) + compileOnly(projects.main) + implementation(libs.slf4j) } mavenPublishing { - configure(JavaLibrary(JavadocJar.Javadoc())) + configure(JavaLibrary(JavadocJar.Javadoc())) } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/YoutubeIpRotatorFilter.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/YoutubeIpRotatorFilter.java index 03d79ba36..8f3ce26bf 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/YoutubeIpRotatorFilter.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/YoutubeIpRotatorFilter.java @@ -3,132 +3,133 @@ import com.sedmelluq.discord.lavaplayer.tools.http.HttpContextFilter; import com.sedmelluq.lava.extensions.youtuberotator.planner.AbstractRoutePlanner; import com.sedmelluq.lava.extensions.youtuberotator.tools.RateLimitException; -import java.net.BindException; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.protocol.HttpClientContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.BindException; + public class YoutubeIpRotatorFilter implements HttpContextFilter { - private static final Logger log = LoggerFactory.getLogger(YoutubeIpRotatorFilter.class); - - private static final String RETRY_COUNT_ATTRIBUTE = "yt-retry-counter"; - - private final HttpContextFilter delegate; - private final boolean isSearch; - private final AbstractRoutePlanner routePlanner; - private final int retryLimit; - - public YoutubeIpRotatorFilter( - HttpContextFilter delegate, - boolean isSearch, - AbstractRoutePlanner routePlanner, - int retryLimit - ) { - this.delegate = delegate; - this.isSearch = isSearch; - this.routePlanner = routePlanner; - this.retryLimit = retryLimit; - } - - @Override - public void onContextOpen(HttpClientContext context) { - if (delegate != null) { - delegate.onContextOpen(context); + private static final Logger log = LoggerFactory.getLogger(YoutubeIpRotatorFilter.class); + + private static final String RETRY_COUNT_ATTRIBUTE = "yt-retry-counter"; + + private final HttpContextFilter delegate; + private final boolean isSearch; + private final AbstractRoutePlanner routePlanner; + private final int retryLimit; + + public YoutubeIpRotatorFilter( + HttpContextFilter delegate, + boolean isSearch, + AbstractRoutePlanner routePlanner, + int retryLimit + ) { + this.delegate = delegate; + this.isSearch = isSearch; + this.routePlanner = routePlanner; + this.retryLimit = retryLimit; } - } - @Override - public void onContextClose(HttpClientContext context) { - if (delegate != null) { - delegate.onContextClose(context); - } - } - - @Override - public void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition) { - if (isRepetition) { - setRetryCount(context, getRetryCount(context) + 1); - } else { - setRetryCount(context, 0); + @Override + public void onContextOpen(HttpClientContext context) { + if (delegate != null) { + delegate.onContextOpen(context); + } } - if (delegate != null) { - delegate.onRequest(context, request, isRepetition); + @Override + public void onContextClose(HttpClientContext context) { + if (delegate != null) { + delegate.onContextClose(context); + } } - } - @Override - public boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response) { - int statusCode = response.getStatusLine().getStatusCode(); + @Override + public void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition) { + if (isRepetition) { + setRetryCount(context, getRetryCount(context) + 1); + } else { + setRetryCount(context, 0); + } - if (isSearch) { - if (statusCode == 429) { - if (routePlanner.shouldHandleSearchFailure()) { - log.warn("YouTube search rate limit reached, marking address as failing and retry"); - routePlanner.markAddressFailing(context); + if (delegate != null) { + delegate.onRequest(context, request, isRepetition); } + } - return limitedRetry(context); - } - } else if (statusCode == 429) { - log.warn("YouTube rate limit reached, marking address {} as failing and retry", - routePlanner.getLastAddress(context)); - routePlanner.markAddressFailing(context); + @Override + public boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + + if (isSearch) { + if (statusCode == 429) { + if (routePlanner.shouldHandleSearchFailure()) { + log.warn("YouTube search rate limit reached, marking address as failing and retry"); + routePlanner.markAddressFailing(context); + } + + return limitedRetry(context); + } + } else if (statusCode == 429) { + log.warn("YouTube rate limit reached, marking address {} as failing and retry", + routePlanner.getLastAddress(context)); + routePlanner.markAddressFailing(context); + + return limitedRetry(context); + } - return limitedRetry(context); + if (delegate != null) { + return delegate.onRequestResponse(context, request, response); + } else { + return false; + } } - if (delegate != null) { - return delegate.onRequestResponse(context, request, response); - } else { - return false; - } - } + @Override + public boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error) { + if (error instanceof BindException) { + log.warn("Cannot assign requested address {}, marking address as failing and retry!", + routePlanner.getLastAddress(context)); - @Override - public boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error) { - if (error instanceof BindException) { - log.warn("Cannot assign requested address {}, marking address as failing and retry!", - routePlanner.getLastAddress(context)); + routePlanner.markAddressFailing(context); + return limitedRetry(context); + } - routePlanner.markAddressFailing(context); - return limitedRetry(context); + if (delegate != null) { + return delegate.onRequestException(context, request, error); + } else { + return false; + } } - if (delegate != null) { - return delegate.onRequestException(context, request, error); - } else { - return false; + private boolean limitedRetry(HttpClientContext context) { + if (getRetryCount(context) >= retryLimit) { + throw new RateLimitException("Retry aborted, too many retries on ratelimit."); + } else { + return true; + } } - } - private boolean limitedRetry(HttpClientContext context) { - if (getRetryCount(context) >= retryLimit) { - throw new RateLimitException("Retry aborted, too many retries on ratelimit."); - } else { - return true; - } - } + private void setRetryCount(HttpClientContext context, int value) { + RetryCount count = context.getAttribute(RETRY_COUNT_ATTRIBUTE, RetryCount.class); - private void setRetryCount(HttpClientContext context, int value) { - RetryCount count = context.getAttribute(RETRY_COUNT_ATTRIBUTE, RetryCount.class); + if (count == null) { + count = new RetryCount(); + context.setAttribute(RETRY_COUNT_ATTRIBUTE, count); + } - if (count == null) { - count = new RetryCount(); - context.setAttribute(RETRY_COUNT_ATTRIBUTE, count); + count.value = value; } - count.value = value; - } - - private int getRetryCount(HttpClientContext context) { - RetryCount count = context.getAttribute(RETRY_COUNT_ATTRIBUTE, RetryCount.class); - return count != null ? count.value : 0; - } + private int getRetryCount(HttpClientContext context) { + RetryCount count = context.getAttribute(RETRY_COUNT_ATTRIBUTE, RetryCount.class); + return count != null ? count.value : 0; + } - private static class RetryCount { - private int value; - } + private static class RetryCount { + private int value; + } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/YoutubeIpRotatorRetryHandler.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/YoutubeIpRotatorRetryHandler.java index fca119dab..521684bd5 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/YoutubeIpRotatorRetryHandler.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/YoutubeIpRotatorRetryHandler.java @@ -1,25 +1,26 @@ package com.sedmelluq.lava.extensions.youtuberotator; -import java.io.IOException; -import java.net.BindException; -import java.net.SocketException; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; import org.apache.http.protocol.HttpContext; +import java.io.IOException; +import java.net.BindException; +import java.net.SocketException; + public class YoutubeIpRotatorRetryHandler implements HttpRequestRetryHandler { - @Override - public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { - if (exception instanceof BindException) { - return false; - } else if (exception instanceof SocketException) { - String message = exception.getMessage(); + @Override + public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { + if (exception instanceof BindException) { + return false; + } else if (exception instanceof SocketException) { + String message = exception.getMessage(); - if (message != null && message.contains("Protocol family unavailable")) { - return false; - } - } + if (message != null && message.contains("Protocol family unavailable")) { + return false; + } + } - return DefaultHttpRequestRetryHandler.INSTANCE.retryRequest(exception, executionCount, context); - } + return DefaultHttpRequestRetryHandler.INSTANCE.retryRequest(exception, executionCount, context); + } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/YoutubeIpRotatorSetup.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/YoutubeIpRotatorSetup.java index 92b0356fc..209370f20 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/YoutubeIpRotatorSetup.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/YoutubeIpRotatorSetup.java @@ -8,91 +8,92 @@ import com.sedmelluq.discord.lavaplayer.tools.http.HttpContextFilter; import com.sedmelluq.discord.lavaplayer.tools.http.SimpleHttpClientConnectionManager; import com.sedmelluq.lava.extensions.youtuberotator.planner.AbstractRoutePlanner; + import java.util.ArrayList; import java.util.List; public class YoutubeIpRotatorSetup { - private static final int DEFAULT_RETRY_LIMIT = 10; - private static final YoutubeHttpContextFilter DEFAULT_DELEGATE = new YoutubeHttpContextFilter(); - private static final YoutubeIpRotatorRetryHandler RETRY_HANDLER = new YoutubeIpRotatorRetryHandler(); - - private final AbstractRoutePlanner routePlanner; - private final List mainConfiguration; - private final List searchConfiguration; - private int retryLimit = DEFAULT_RETRY_LIMIT; - private HttpContextFilter mainDelegate = DEFAULT_DELEGATE; - private HttpContextFilter searchDelegate = null; - - public YoutubeIpRotatorSetup(AbstractRoutePlanner routePlanner) { - this.routePlanner = routePlanner; - mainConfiguration = new ArrayList<>(); - searchConfiguration = new ArrayList<>(); - } - - public YoutubeIpRotatorSetup forConfiguration(ExtendedHttpConfigurable configurable, boolean isSearch) { - if (isSearch) { - searchConfiguration.add(configurable); - } else { - mainConfiguration.add(configurable); + private static final int DEFAULT_RETRY_LIMIT = 10; + private static final YoutubeHttpContextFilter DEFAULT_DELEGATE = new YoutubeHttpContextFilter(); + private static final YoutubeIpRotatorRetryHandler RETRY_HANDLER = new YoutubeIpRotatorRetryHandler(); + + private final AbstractRoutePlanner routePlanner; + private final List mainConfiguration; + private final List searchConfiguration; + private int retryLimit = DEFAULT_RETRY_LIMIT; + private HttpContextFilter mainDelegate = DEFAULT_DELEGATE; + private HttpContextFilter searchDelegate = null; + + public YoutubeIpRotatorSetup(AbstractRoutePlanner routePlanner) { + this.routePlanner = routePlanner; + mainConfiguration = new ArrayList<>(); + searchConfiguration = new ArrayList<>(); } - return this; - } + public YoutubeIpRotatorSetup forConfiguration(ExtendedHttpConfigurable configurable, boolean isSearch) { + if (isSearch) { + searchConfiguration.add(configurable); + } else { + mainConfiguration.add(configurable); + } + + return this; + } - public YoutubeIpRotatorSetup forSource(YoutubeAudioSourceManager sourceManager) { - forConfiguration(sourceManager.getMainHttpConfiguration(), false); - forConfiguration(sourceManager.getSearchHttpConfiguration(), true); - forConfiguration(sourceManager.getSearchMusicHttpConfiguration(), true); - DEFAULT_DELEGATE.setTokenTracker(sourceManager.getAccessTokenTracker()); - return this; - } + public YoutubeIpRotatorSetup forSource(YoutubeAudioSourceManager sourceManager) { + forConfiguration(sourceManager.getMainHttpConfiguration(), false); + forConfiguration(sourceManager.getSearchHttpConfiguration(), true); + forConfiguration(sourceManager.getSearchMusicHttpConfiguration(), true); + DEFAULT_DELEGATE.setTokenTracker(sourceManager.getAccessTokenTracker()); + return this; + } - public YoutubeIpRotatorSetup forManager(AudioPlayerManager playerManager) { - YoutubeAudioSourceManager sourceManager = playerManager.source(YoutubeAudioSourceManager.class); + public YoutubeIpRotatorSetup forManager(AudioPlayerManager playerManager) { + YoutubeAudioSourceManager sourceManager = playerManager.source(YoutubeAudioSourceManager.class); + + if (sourceManager != null) { + forSource(sourceManager); + } + + return this; + } + + public YoutubeIpRotatorSetup withRetryLimit(int retryLimit) { + this.retryLimit = retryLimit; + return this; + } + + public YoutubeIpRotatorSetup withMainDelegateFilter(HttpContextFilter filter) { + this.mainDelegate = filter; + return this; + } + + public YoutubeIpRotatorSetup withSearchDelegateFilter(HttpContextFilter filter) { + this.searchDelegate = filter; + return this; + } - if (sourceManager != null) { - forSource(sourceManager); + public void setup() { + apply(mainConfiguration, new YoutubeIpRotatorFilter(mainDelegate, false, routePlanner, retryLimit)); + apply(searchConfiguration, new YoutubeIpRotatorFilter(searchDelegate, true, routePlanner, retryLimit)); } - return this; - } - - public YoutubeIpRotatorSetup withRetryLimit(int retryLimit) { - this.retryLimit = retryLimit; - return this; - } - - public YoutubeIpRotatorSetup withMainDelegateFilter(HttpContextFilter filter) { - this.mainDelegate = filter; - return this; - } - - public YoutubeIpRotatorSetup withSearchDelegateFilter(HttpContextFilter filter) { - this.searchDelegate = filter; - return this; - } - - public void setup() { - apply(mainConfiguration, new YoutubeIpRotatorFilter(mainDelegate, false, routePlanner, retryLimit)); - apply(searchConfiguration, new YoutubeIpRotatorFilter(searchDelegate, true, routePlanner, retryLimit)); - } - - protected void apply(List configurables, YoutubeIpRotatorFilter filter) { - for (ExtendedHttpConfigurable configurable : configurables) { - configurable.configureBuilder(builder -> - ((ExtendedHttpClientBuilder) builder).setConnectionManagerFactory(SimpleHttpClientConnectionManager::new) - ); - - configurable.configureBuilder(it -> { - it.setRoutePlanner(routePlanner); - // No retry for some exceptions we know are hopeless for retry. - it.setRetryHandler(RETRY_HANDLER); - // Regularly cleans up per-route connection pool which gets huge due to many routes caused by - // each request having an unique route. - it.evictExpiredConnections(); - }); - - configurable.setHttpContextFilter(filter); + protected void apply(List configurables, YoutubeIpRotatorFilter filter) { + for (ExtendedHttpConfigurable configurable : configurables) { + configurable.configureBuilder(builder -> + ((ExtendedHttpClientBuilder) builder).setConnectionManagerFactory(SimpleHttpClientConnectionManager::new) + ); + + configurable.configureBuilder(it -> { + it.setRoutePlanner(routePlanner); + // No retry for some exceptions we know are hopeless for retry. + it.setRetryHandler(RETRY_HANDLER); + // Regularly cleans up per-route connection pool which gets huge due to many routes caused by + // each request having an unique route. + it.evictExpiredConnections(); + }); + + configurable.setHttpContextFilter(filter); + } } - } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/AbstractRoutePlanner.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/AbstractRoutePlanner.java index 3f95ad58f..40259ff84 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/AbstractRoutePlanner.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/AbstractRoutePlanner.java @@ -29,131 +29,131 @@ import java.util.concurrent.TimeUnit; public abstract class AbstractRoutePlanner implements HttpRoutePlanner { - private static final String CHOSEN_IP_ATTRIBUTE = "yt-route-ip"; - - private static final long FAILING_TIME = TimeUnit.DAYS.toMillis(7); - private static final Logger log = LoggerFactory.getLogger(AbstractRoutePlanner.class); - - protected final IpBlock ipBlock; - protected final Map failingAddresses; - private final SchemePortResolver schemePortResolver; - private final boolean handleSearchFailure; - - protected AbstractRoutePlanner(final List ipBlocks, final boolean handleSearchFailure) { - this.ipBlock = new CombinedIpBlock(ipBlocks); - this.failingAddresses = new HashMap<>(); - this.schemePortResolver = DefaultSchemePortResolver.INSTANCE; - this.handleSearchFailure = handleSearchFailure; - log.info("Active RoutePlanner: {} using total of {} ips", getClass().getCanonicalName(), this.ipBlock.getSize()); - } - - public IpBlock getIpBlock() { - return ipBlock; - } - - public boolean shouldHandleSearchFailure() { - return this.handleSearchFailure; - } - - public Map getFailingAddresses() { - return failingAddresses; - } - - public final InetAddress getLastAddress(final HttpClientContext context) { - return context.getAttribute(CHOSEN_IP_ATTRIBUTE, InetAddress.class); - } - - public final void markAddressFailing(HttpClientContext context) { - final InetAddress address = getLastAddress(context); - if (address == null) { - log.warn("Call to markAddressFailing() without chosen IP set", - new RuntimeException("Report this to the devs: address is null")); - return; + private static final String CHOSEN_IP_ATTRIBUTE = "yt-route-ip"; + + private static final long FAILING_TIME = TimeUnit.DAYS.toMillis(7); + private static final Logger log = LoggerFactory.getLogger(AbstractRoutePlanner.class); + + protected final IpBlock ipBlock; + protected final Map failingAddresses; + private final SchemePortResolver schemePortResolver; + private final boolean handleSearchFailure; + + protected AbstractRoutePlanner(final List ipBlocks, final boolean handleSearchFailure) { + this.ipBlock = new CombinedIpBlock(ipBlocks); + this.failingAddresses = new HashMap<>(); + this.schemePortResolver = DefaultSchemePortResolver.INSTANCE; + this.handleSearchFailure = handleSearchFailure; + log.info("Active RoutePlanner: {} using total of {} ips", getClass().getCanonicalName(), this.ipBlock.getSize()); } - this.failingAddresses.put(address.toString(), System.currentTimeMillis()); - onAddressFailure(address); - } - - public final void freeAddress(final InetAddress address) { - this.failingAddresses.remove(address.toString()); - } - - public final void freeAllAddresses() { - this.failingAddresses.clear(); - } - - protected final boolean isValidAddress(final InetAddress address) { - final Long failedTimestamp = failingAddresses.get(address.toString()); - if (failedTimestamp == null) { - log.debug("No failing entry for {}", address); - return true; + + public IpBlock getIpBlock() { + return ipBlock; + } + + public boolean shouldHandleSearchFailure() { + return this.handleSearchFailure; + } + + public Map getFailingAddresses() { + return failingAddresses; + } + + public final InetAddress getLastAddress(final HttpClientContext context) { + return context.getAttribute(CHOSEN_IP_ATTRIBUTE, InetAddress.class); + } + + public final void markAddressFailing(HttpClientContext context) { + final InetAddress address = getLastAddress(context); + if (address == null) { + log.warn("Call to markAddressFailing() without chosen IP set", + new RuntimeException("Report this to the devs: address is null")); + return; + } + this.failingAddresses.put(address.toString(), System.currentTimeMillis()); + onAddressFailure(address); + } + + public final void freeAddress(final InetAddress address) { + this.failingAddresses.remove(address.toString()); + } + + public final void freeAllAddresses() { + this.failingAddresses.clear(); + } + + protected final boolean isValidAddress(final InetAddress address) { + final Long failedTimestamp = failingAddresses.get(address.toString()); + if (failedTimestamp == null) { + log.debug("No failing entry for {}", address); + return true; + } + if (failedTimestamp + getFailingIpsCacheDuration() < System.currentTimeMillis()) { + failingAddresses.remove(address.toString()); + log.debug("Removing expired failing entry for {}", address); + return true; + } + log.warn("{} was chosen, but is marked as failing, retrying...", address); + return false; } - if (failedTimestamp + getFailingIpsCacheDuration() < System.currentTimeMillis()) { - failingAddresses.remove(address.toString()); - log.debug("Removing expired failing entry for {}", address); - return true; + + @Override + public HttpRoute determineRoute(final HttpHost host, final HttpRequest request, final HttpContext context) throws HttpException { + Args.notNull(request, "Request"); + if (host == null) { + throw new ProtocolException("Target host is not specified"); + } + final HttpClientContext clientContext = HttpClientContext.adapt(context); + final RequestConfig config = clientContext.getRequestConfig(); + int remotePort; + if (host.getPort() <= 0) { + try { + remotePort = schemePortResolver.resolve(host); + } catch (final UnsupportedSchemeException e) { + throw new HttpException(e.getMessage()); + } + } else + remotePort = host.getPort(); + + final Tuple remoteAddresses = IpAddressTools.getRandomAddressesFromHost(host); + final Tuple addresses = determineAddressPair(remoteAddresses); + + final HttpHost target = new HttpHost(addresses.r, host.getHostName(), remotePort, host.getSchemeName()); + final HttpHost proxy = config.getProxy(); + final boolean secure = target.getSchemeName().equalsIgnoreCase("https"); + clientContext.setAttribute(CHOSEN_IP_ATTRIBUTE, addresses.l); + log.debug("Setting route context attribute to {}", addresses.l); + if (proxy == null) { + return new HttpRoute(target, addresses.l, secure); + } else { + return new HttpRoute(target, addresses.l, proxy, secure); + } } - log.warn("{} was chosen, but is marked as failing, retrying...", address); - return false; - } - - @Override - public HttpRoute determineRoute(final HttpHost host, final HttpRequest request, final HttpContext context) throws HttpException { - Args.notNull(request, "Request"); - if (host == null) { - throw new ProtocolException("Target host is not specified"); + + /** + * Called when an address is marked as failing + * + * @param address the failing address + */ + protected void onAddressFailure(final InetAddress address) { + } - final HttpClientContext clientContext = HttpClientContext.adapt(context); - final RequestConfig config = clientContext.getRequestConfig(); - int remotePort; - if (host.getPort() <= 0) { - try { - remotePort = schemePortResolver.resolve(host); - } catch (final UnsupportedSchemeException e) { - throw new HttpException(e.getMessage()); - } - } else - remotePort = host.getPort(); - - final Tuple remoteAddresses = IpAddressTools.getRandomAddressesFromHost(host); - final Tuple addresses = determineAddressPair(remoteAddresses); - - final HttpHost target = new HttpHost(addresses.r, host.getHostName(), remotePort, host.getSchemeName()); - final HttpHost proxy = config.getProxy(); - final boolean secure = target.getSchemeName().equalsIgnoreCase("https"); - clientContext.setAttribute(CHOSEN_IP_ATTRIBUTE, addresses.l); - log.debug("Setting route context attribute to {}", addresses.l); - if (proxy == null) { - return new HttpRoute(target, addresses.l, secure); - } else { - return new HttpRoute(target, addresses.l, proxy, secure); + + /** + * How long a failing address should not be reused in milliseconds + * + * @return duration in milliseconds + */ + protected long getFailingIpsCacheDuration() { + return FAILING_TIME; } - } - - /** - * Called when an address is marked as failing - * - * @param address the failing address - */ - protected void onAddressFailure(final InetAddress address) { - - } - - /** - * How long a failing address should not be reused in milliseconds - * - * @return duration in milliseconds - */ - protected long getFailingIpsCacheDuration() { - return FAILING_TIME; - } - - /** - * Determines the local and remote address pair to use - * - * @param remoteAddresses The remote address pair containing IPv4 and IPv6 addresses - which can be null - * @return a {@link Tuple} which contains l = localAddress & r = remoteAddress - * @throws HttpException when no route can be determined - */ - protected abstract Tuple determineAddressPair(final Tuple remoteAddresses) throws HttpException; + + /** + * Determines the local and remote address pair to use + * + * @param remoteAddresses The remote address pair containing IPv4 and IPv6 addresses - which can be null + * @return a {@link Tuple} which contains l = localAddress & r = remoteAddress + * @throws HttpException when no route can be determined + */ + protected abstract Tuple determineAddressPair(final Tuple remoteAddresses) throws HttpException; } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/BalancingIpRoutePlanner.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/BalancingIpRoutePlanner.java index c15fdd94f..cb7533abb 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/BalancingIpRoutePlanner.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/BalancingIpRoutePlanner.java @@ -19,72 +19,72 @@ @SuppressWarnings("WeakerAccess") public class BalancingIpRoutePlanner extends AbstractRoutePlanner { - private static final Logger log = LoggerFactory.getLogger(BalancingIpRoutePlanner.class); - private final Predicate ipFilter; + private static final Logger log = LoggerFactory.getLogger(BalancingIpRoutePlanner.class); + private final Predicate ipFilter; - /** - * @param ipBlocks the block to perform balancing over. - */ - public BalancingIpRoutePlanner(List ipBlocks) { - this(ipBlocks, i -> true); - } + /** + * @param ipBlocks the block to perform balancing over. + */ + public BalancingIpRoutePlanner(List ipBlocks) { + this(ipBlocks, i -> true); + } - /** - * @param ipBlocks the block to perform balancing over. - * @param ipFilter function to filter out certain IP addresses picked from the IP block, causing another random to be chosen. - */ - public BalancingIpRoutePlanner(List ipBlocks, Predicate ipFilter) { - this(ipBlocks, ipFilter, true); - } + /** + * @param ipBlocks the block to perform balancing over. + * @param ipFilter function to filter out certain IP addresses picked from the IP block, causing another random to be chosen. + */ + public BalancingIpRoutePlanner(List ipBlocks, Predicate ipFilter) { + this(ipBlocks, ipFilter, true); + } - /** - * @param ipBlocks the block to perform balancing over. - * @param ipFilter function to filter out certain IP addresses picked from the IP block, causing another random to be chosen. - * @param handleSearchFailure whether a search 429 should trigger the ip as failing - */ - public BalancingIpRoutePlanner(List ipBlocks, Predicate ipFilter, boolean handleSearchFailure) { - super(ipBlocks, handleSearchFailure); - this.ipFilter = ipFilter; - } + /** + * @param ipBlocks the block to perform balancing over. + * @param ipFilter function to filter out certain IP addresses picked from the IP block, causing another random to be chosen. + * @param handleSearchFailure whether a search 429 should trigger the ip as failing + */ + public BalancingIpRoutePlanner(List ipBlocks, Predicate ipFilter, boolean handleSearchFailure) { + super(ipBlocks, handleSearchFailure); + this.ipFilter = ipFilter; + } - @Override - protected Tuple determineAddressPair(Tuple remoteAddresses) throws HttpException { - InetAddress localAddress; - final InetAddress remoteAddress; - if (ipBlock.getType() == Inet4Address.class) { - if (remoteAddresses.l != null) { - localAddress = getRandomAddress(ipBlock); - remoteAddress = remoteAddresses.l; - } else { - throw new HttpException("Could not resolve host"); - } - } else if (ipBlock.getType() == Inet6Address.class) { - if (remoteAddresses.r != null) { - localAddress = getRandomAddress(ipBlock); - remoteAddress = remoteAddresses.r; - } else if (remoteAddresses.l != null) { - localAddress = null; - remoteAddress = remoteAddresses.l; - log.warn("Could not look up AAAA record for host. Falling back to unbalanced IPv4."); - } else { - throw new HttpException("Could not resolve host"); - } - } else { - throw new HttpException("Unknown IpBlock type: " + ipBlock.getType().getCanonicalName()); + @Override + protected Tuple determineAddressPair(Tuple remoteAddresses) throws HttpException { + InetAddress localAddress; + final InetAddress remoteAddress; + if (ipBlock.getType() == Inet4Address.class) { + if (remoteAddresses.l != null) { + localAddress = getRandomAddress(ipBlock); + remoteAddress = remoteAddresses.l; + } else { + throw new HttpException("Could not resolve host"); + } + } else if (ipBlock.getType() == Inet6Address.class) { + if (remoteAddresses.r != null) { + localAddress = getRandomAddress(ipBlock); + remoteAddress = remoteAddresses.r; + } else if (remoteAddresses.l != null) { + localAddress = null; + remoteAddress = remoteAddresses.l; + log.warn("Could not look up AAAA record for host. Falling back to unbalanced IPv4."); + } else { + throw new HttpException("Could not resolve host"); + } + } else { + throw new HttpException("Unknown IpBlock type: " + ipBlock.getType().getCanonicalName()); + } + return new Tuple<>(localAddress, remoteAddress); } - return new Tuple<>(localAddress, remoteAddress); - } - private InetAddress getRandomAddress(final IpBlock ipBlock) { - InetAddress localAddress; - BigInteger it = BigInteger.valueOf(0); - do { - if (ipBlock.getSize().multiply(BigInteger.valueOf(2)).compareTo(it) < 0) { - throw new RuntimeException("Can't find a free ip"); - } - it = it.add(BigInteger.ONE); - localAddress = ipBlock.getRandomAddress(); - } while (localAddress == null || !ipFilter.test(localAddress) || !isValidAddress(localAddress)); - return localAddress; - } + private InetAddress getRandomAddress(final IpBlock ipBlock) { + InetAddress localAddress; + BigInteger it = BigInteger.valueOf(0); + do { + if (ipBlock.getSize().multiply(BigInteger.valueOf(2)).compareTo(it) < 0) { + throw new RuntimeException("Can't find a free ip"); + } + it = it.add(BigInteger.ONE); + localAddress = ipBlock.getRandomAddress(); + } while (localAddress == null || !ipFilter.test(localAddress) || !isValidAddress(localAddress)); + return localAddress; + } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/NanoIpRoutePlanner.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/NanoIpRoutePlanner.java index e169c83b3..c7a87c922 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/NanoIpRoutePlanner.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/NanoIpRoutePlanner.java @@ -16,65 +16,66 @@ public final class NanoIpRoutePlanner extends AbstractRoutePlanner { - private static final Logger log = LoggerFactory.getLogger(NanoIpRoutePlanner.class); - private static final BigRandom random = new BigRandom(); + private static final Logger log = LoggerFactory.getLogger(NanoIpRoutePlanner.class); + private static final BigRandom random = new BigRandom(); - private final BigInteger startTime; - private final int maskBits; + private final BigInteger startTime; + private final int maskBits; - public NanoIpRoutePlanner(final List ipBlocks, final boolean handleSearchFailure) { - super(ipBlocks, handleSearchFailure); - if (ipBlock.getSize().compareTo(Ipv6Block.BLOCK64_IPS) < 0) - throw new IllegalArgumentException("Nano IP Route planner requires an IPv6Block which is greater or equal to a /64"); - startTime = BigInteger.valueOf(System.nanoTime()); - maskBits = ipBlock.getMaskBits(); - } + public NanoIpRoutePlanner(final List ipBlocks, final boolean handleSearchFailure) { + super(ipBlocks, handleSearchFailure); + if (ipBlock.getSize().compareTo(Ipv6Block.BLOCK64_IPS) < 0) + throw new IllegalArgumentException("Nano IP Route planner requires an IPv6Block which is greater or equal to a /64"); + startTime = BigInteger.valueOf(System.nanoTime()); + maskBits = ipBlock.getMaskBits(); + } - /** - * Returns the address offset based on the current nano time - * @return address offset as long - */ - public long getCurrentAddress() { - return System.nanoTime() - startTime.longValue(); - } + /** + * Returns the address offset based on the current nano time + * + * @return address offset as long + */ + public long getCurrentAddress() { + return System.nanoTime() - startTime.longValue(); + } - @Override - protected Tuple determineAddressPair(final Tuple remoteAddresses) throws HttpException { - InetAddress currentAddress = null; - InetAddress remoteAddress; - if (ipBlock.getType() == Inet4Address.class) { - if (remoteAddresses.l != null) { - currentAddress = getAddress(); - log.debug("Selected " + currentAddress.toString() + " as new outgoing ip"); - remoteAddress = remoteAddresses.l; - } else { - throw new HttpException("Could not resolve host"); - } - } else if (ipBlock.getType() == Inet6Address.class) { - if (remoteAddresses.r != null) { - currentAddress = getAddress(); - log.debug("Selected " + currentAddress.toString() + " as new outgoing ip"); - remoteAddress = remoteAddresses.r; - } else if (remoteAddresses.l != null) { - remoteAddress = remoteAddresses.l; - log.warn("Could not look up AAAA record for host. Falling back to unbalanced IPv4."); - } else { - throw new HttpException("Could not resolve host"); - } - } else { - throw new HttpException("Unknown IpBlock type: " + ipBlock.getType().getCanonicalName()); + @Override + protected Tuple determineAddressPair(final Tuple remoteAddresses) throws HttpException { + InetAddress currentAddress = null; + InetAddress remoteAddress; + if (ipBlock.getType() == Inet4Address.class) { + if (remoteAddresses.l != null) { + currentAddress = getAddress(); + log.debug("Selected " + currentAddress.toString() + " as new outgoing ip"); + remoteAddress = remoteAddresses.l; + } else { + throw new HttpException("Could not resolve host"); + } + } else if (ipBlock.getType() == Inet6Address.class) { + if (remoteAddresses.r != null) { + currentAddress = getAddress(); + log.debug("Selected " + currentAddress.toString() + " as new outgoing ip"); + remoteAddress = remoteAddresses.r; + } else if (remoteAddresses.l != null) { + remoteAddress = remoteAddresses.l; + log.warn("Could not look up AAAA record for host. Falling back to unbalanced IPv4."); + } else { + throw new HttpException("Could not resolve host"); + } + } else { + throw new HttpException("Unknown IpBlock type: " + ipBlock.getType().getCanonicalName()); + } + return new Tuple<>(currentAddress, remoteAddress); } - return new Tuple<>(currentAddress, remoteAddress); - } - private InetAddress getAddress() { - final BigInteger now = BigInteger.valueOf(System.nanoTime()); - final BigInteger nanoOffset = now.subtract(startTime); // least 64 bit - if(maskBits == 64) { - return ipBlock.getAddressAtIndex(nanoOffset); + private InetAddress getAddress() { + final BigInteger now = BigInteger.valueOf(System.nanoTime()); + final BigInteger nanoOffset = now.subtract(startTime); // least 64 bit + if (maskBits == 64) { + return ipBlock.getAddressAtIndex(nanoOffset); + } + final BigInteger randomOffset = random.nextBigInt(Ipv6Block.IPV6_BIT_SIZE - maskBits).shiftLeft(Ipv6Block.IPV6_BIT_SIZE - maskBits); // most {{maskBits}}-64 bit + return ipBlock.getAddressAtIndex(randomOffset.add(nanoOffset)); } - final BigInteger randomOffset = random.nextBigInt(Ipv6Block.IPV6_BIT_SIZE - maskBits).shiftLeft(Ipv6Block.IPV6_BIT_SIZE - maskBits); // most {{maskBits}}-64 bit - return ipBlock.getAddressAtIndex(randomOffset.add(nanoOffset)); - } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/RotatingIpRoutePlanner.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/RotatingIpRoutePlanner.java index da84ada7b..018fc7f6a 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/RotatingIpRoutePlanner.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/RotatingIpRoutePlanner.java @@ -19,136 +19,136 @@ public final class RotatingIpRoutePlanner extends AbstractRoutePlanner { - private static final Logger log = LoggerFactory.getLogger(RotatingIpRoutePlanner.class); - private static final Random random = new Random(); - private final Predicate ipFilter; - private final AtomicBoolean next; - private final AtomicReference rotateIndex; - private final AtomicReference index; - private volatile InetAddress lastFailingAddress; + private static final Logger log = LoggerFactory.getLogger(RotatingIpRoutePlanner.class); + private static final Random random = new Random(); + private final Predicate ipFilter; + private final AtomicBoolean next; + private final AtomicReference rotateIndex; + private final AtomicReference index; + private volatile InetAddress lastFailingAddress; - /** - * @param ipBlocks the block to perform balancing over. - */ - public RotatingIpRoutePlanner(final List ipBlocks) { - this(ipBlocks, i -> true); - } + /** + * @param ipBlocks the block to perform balancing over. + */ + public RotatingIpRoutePlanner(final List ipBlocks) { + this(ipBlocks, i -> true); + } - /** - * @param ipBlocks the block to perform balancing over. - * @param ipFilter function to filter out certain IP addresses picked from the IP block, causing another random to be chosen. - */ - public RotatingIpRoutePlanner(final List ipBlocks, final Predicate ipFilter) { - this(ipBlocks, ipFilter, true); - } + /** + * @param ipBlocks the block to perform balancing over. + * @param ipFilter function to filter out certain IP addresses picked from the IP block, causing another random to be chosen. + */ + public RotatingIpRoutePlanner(final List ipBlocks, final Predicate ipFilter) { + this(ipBlocks, ipFilter, true); + } - /** - * @param ipBlocks the block to perform balancing over. - * @param ipFilter function to filter out certain IP addresses picked from the IP block, causing another random to be chosen. - * @param handleSearchFailure whether a search 429 should trigger the ip as failing - */ - public RotatingIpRoutePlanner(final List ipBlocks, final Predicate ipFilter, final boolean handleSearchFailure) { - super(ipBlocks, handleSearchFailure); - this.ipFilter = ipFilter; - this.next = new AtomicBoolean(false); - this.rotateIndex = new AtomicReference<>(BigInteger.valueOf(0)); - this.index = new AtomicReference<>(BigInteger.valueOf(0)); - this.lastFailingAddress = null; - } + /** + * @param ipBlocks the block to perform balancing over. + * @param ipFilter function to filter out certain IP addresses picked from the IP block, causing another random to be chosen. + * @param handleSearchFailure whether a search 429 should trigger the ip as failing + */ + public RotatingIpRoutePlanner(final List ipBlocks, final Predicate ipFilter, final boolean handleSearchFailure) { + super(ipBlocks, handleSearchFailure); + this.ipFilter = ipFilter; + this.next = new AtomicBoolean(false); + this.rotateIndex = new AtomicReference<>(BigInteger.valueOf(0)); + this.index = new AtomicReference<>(BigInteger.valueOf(0)); + this.lastFailingAddress = null; + } - public void next() { - rotateIndex.accumulateAndGet(BigInteger.ONE, BigInteger::add); - if (!this.next.compareAndSet(false, true)) { - log.warn("Call to next() even when previous next() hasn't completed yet"); + public void next() { + rotateIndex.accumulateAndGet(BigInteger.ONE, BigInteger::add); + if (!this.next.compareAndSet(false, true)) { + log.warn("Call to next() even when previous next() hasn't completed yet"); + } } - } - public InetAddress getCurrentAddress() { - if (index.get().compareTo(BigInteger.ZERO) == 0) - return null; - return ipBlock.getAddressAtIndex(index.get().subtract(BigInteger.ONE)); - } + public InetAddress getCurrentAddress() { + if (index.get().compareTo(BigInteger.ZERO) == 0) + return null; + return ipBlock.getAddressAtIndex(index.get().subtract(BigInteger.ONE)); + } - public BigInteger getIndex() { - return index.get(); - } + public BigInteger getIndex() { + return index.get(); + } - public BigInteger getRotateIndex() { - return rotateIndex.get(); - } + public BigInteger getRotateIndex() { + return rotateIndex.get(); + } - @Override - protected Tuple determineAddressPair(final Tuple remoteAddresses) throws HttpException { - InetAddress currentAddress = null; - InetAddress remoteAddress; - if (ipBlock.getType() == Inet4Address.class) { - if (remoteAddresses.l != null) { - if (index.get().compareTo(BigInteger.ZERO) == 0 || next.get()) { - currentAddress = extractLocalAddress(); - log.info("Selected " + currentAddress.toString() + " as new outgoing ip"); - } - remoteAddress = remoteAddresses.l; - } else { - throw new HttpException("Could not resolve host"); - } - } else if (ipBlock.getType() == Inet6Address.class) { - if (remoteAddresses.r != null) { - if (index.get().compareTo(BigInteger.ZERO) == 0 || next.get()) { - currentAddress = extractLocalAddress(); - log.info("Selected " + currentAddress.toString() + " as new outgoing ip"); + @Override + protected Tuple determineAddressPair(final Tuple remoteAddresses) throws HttpException { + InetAddress currentAddress = null; + InetAddress remoteAddress; + if (ipBlock.getType() == Inet4Address.class) { + if (remoteAddresses.l != null) { + if (index.get().compareTo(BigInteger.ZERO) == 0 || next.get()) { + currentAddress = extractLocalAddress(); + log.info("Selected " + currentAddress.toString() + " as new outgoing ip"); + } + remoteAddress = remoteAddresses.l; + } else { + throw new HttpException("Could not resolve host"); + } + } else if (ipBlock.getType() == Inet6Address.class) { + if (remoteAddresses.r != null) { + if (index.get().compareTo(BigInteger.ZERO) == 0 || next.get()) { + currentAddress = extractLocalAddress(); + log.info("Selected " + currentAddress.toString() + " as new outgoing ip"); + } + remoteAddress = remoteAddresses.r; + } else if (remoteAddresses.l != null) { + remoteAddress = remoteAddresses.l; + log.warn("Could not look up AAAA record for host. Falling back to unbalanced IPv4."); + } else { + throw new HttpException("Could not resolve host"); + } + } else { + throw new HttpException("Unknown IpBlock type: " + ipBlock.getType().getCanonicalName()); } - remoteAddress = remoteAddresses.r; - } else if (remoteAddresses.l != null) { - remoteAddress = remoteAddresses.l; - log.warn("Could not look up AAAA record for host. Falling back to unbalanced IPv4."); - } else { - throw new HttpException("Could not resolve host"); - } - } else { - throw new HttpException("Unknown IpBlock type: " + ipBlock.getType().getCanonicalName()); - } - if (currentAddress == null && index.get().compareTo(BigInteger.ZERO) > 0) - currentAddress = ipBlock.getAddressAtIndex(index.get().subtract(BigInteger.ONE)); - next.set(false); - return new Tuple<>(currentAddress, remoteAddress); - } + if (currentAddress == null && index.get().compareTo(BigInteger.ZERO) > 0) + currentAddress = ipBlock.getAddressAtIndex(index.get().subtract(BigInteger.ONE)); + next.set(false); + return new Tuple<>(currentAddress, remoteAddress); + } - @Override - protected void onAddressFailure(final InetAddress address) { - if (lastFailingAddress != null && lastFailingAddress.toString().equals(address.toString())) { - log.warn("Address {} was already failing, not triggering next()", address.toString()); - return; + @Override + protected void onAddressFailure(final InetAddress address) { + if (lastFailingAddress != null && lastFailingAddress.toString().equals(address.toString())) { + log.warn("Address {} was already failing, not triggering next()", address.toString()); + return; + } + lastFailingAddress = address; + next(); } - lastFailingAddress = address; - next(); - } - private InetAddress extractLocalAddress() { - InetAddress localAddress; - long triesSinceBlockSkip = 0; - BigInteger it = BigInteger.valueOf(0); - do { - if (ipBlock.getSize().multiply(BigInteger.valueOf(2)).compareTo(it) < 0) { - throw new RuntimeException("Can't find a free ip"); - } - if (ipBlock.getSize().compareTo(BigInteger.valueOf(128)) > 0) - index.accumulateAndGet(BigInteger.valueOf(random.nextInt(10) + 10), BigInteger::add); - else - index.accumulateAndGet(BigInteger.ONE, BigInteger::add); - it = it.add(BigInteger.ONE); - triesSinceBlockSkip++; - if (ipBlock.getSize().compareTo(Ipv6Block.BLOCK64_IPS) > 0 && triesSinceBlockSkip > 128) { - triesSinceBlockSkip = 0; - rotateIndex.accumulateAndGet(Ipv6Block.BLOCK64_IPS.add(BigInteger.valueOf(random.nextLong())), BigInteger::add); - } - try { - localAddress = ipBlock.getAddressAtIndex(index.get().subtract(BigInteger.ONE)); - } catch (final Exception ex) { - index.set(BigInteger.ZERO); - localAddress = null; - } - } while (localAddress == null || !ipFilter.test(localAddress) || !isValidAddress(localAddress)); - return localAddress; - } + private InetAddress extractLocalAddress() { + InetAddress localAddress; + long triesSinceBlockSkip = 0; + BigInteger it = BigInteger.valueOf(0); + do { + if (ipBlock.getSize().multiply(BigInteger.valueOf(2)).compareTo(it) < 0) { + throw new RuntimeException("Can't find a free ip"); + } + if (ipBlock.getSize().compareTo(BigInteger.valueOf(128)) > 0) + index.accumulateAndGet(BigInteger.valueOf(random.nextInt(10) + 10), BigInteger::add); + else + index.accumulateAndGet(BigInteger.ONE, BigInteger::add); + it = it.add(BigInteger.ONE); + triesSinceBlockSkip++; + if (ipBlock.getSize().compareTo(Ipv6Block.BLOCK64_IPS) > 0 && triesSinceBlockSkip > 128) { + triesSinceBlockSkip = 0; + rotateIndex.accumulateAndGet(Ipv6Block.BLOCK64_IPS.add(BigInteger.valueOf(random.nextLong())), BigInteger::add); + } + try { + localAddress = ipBlock.getAddressAtIndex(index.get().subtract(BigInteger.ONE)); + } catch (final Exception ex) { + index.set(BigInteger.ZERO); + localAddress = null; + } + } while (localAddress == null || !ipFilter.test(localAddress) || !isValidAddress(localAddress)); + return localAddress; + } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/RotatingNanoIpRoutePlanner.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/RotatingNanoIpRoutePlanner.java index ba030b32c..5ee65399e 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/RotatingNanoIpRoutePlanner.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/planner/RotatingNanoIpRoutePlanner.java @@ -18,98 +18,100 @@ public final class RotatingNanoIpRoutePlanner extends AbstractRoutePlanner { - private static final Logger log = LoggerFactory.getLogger(RotatingNanoIpRoutePlanner.class); + private static final Logger log = LoggerFactory.getLogger(RotatingNanoIpRoutePlanner.class); - private final Predicate ipFilter; - private final AtomicReference currentBlock; - private final AtomicReference blockNanoStart; - private final AtomicBoolean next; + private final Predicate ipFilter; + private final AtomicReference currentBlock; + private final AtomicReference blockNanoStart; + private final AtomicBoolean next; - public RotatingNanoIpRoutePlanner(final List ipBlocks) { - this(ipBlocks, ip -> true); - } + public RotatingNanoIpRoutePlanner(final List ipBlocks) { + this(ipBlocks, ip -> true); + } - public RotatingNanoIpRoutePlanner(final List ipBlocks, final Predicate ipFilter) { - this(ipBlocks, ipFilter, true); - } + public RotatingNanoIpRoutePlanner(final List ipBlocks, final Predicate ipFilter) { + this(ipBlocks, ipFilter, true); + } - public RotatingNanoIpRoutePlanner(final List ipBlocks, final Predicate ipFilter, final boolean handleSearchFailure) { - super(ipBlocks, handleSearchFailure); - this.ipFilter = ipFilter; - this.currentBlock = new AtomicReference<>(BigInteger.ZERO); - this.blockNanoStart = new AtomicReference<>(BigInteger.valueOf(System.nanoTime())); - this.next = new AtomicBoolean(false); - if (ipBlock.getType() != Inet6Address.class || ipBlock.getSize().compareTo(Ipv6Block.BLOCK64_IPS) < 0) - throw new IllegalArgumentException("Please use a bigger IPv6 Block!"); - } + public RotatingNanoIpRoutePlanner(final List ipBlocks, final Predicate ipFilter, final boolean handleSearchFailure) { + super(ipBlocks, handleSearchFailure); + this.ipFilter = ipFilter; + this.currentBlock = new AtomicReference<>(BigInteger.ZERO); + this.blockNanoStart = new AtomicReference<>(BigInteger.valueOf(System.nanoTime())); + this.next = new AtomicBoolean(false); + if (ipBlock.getType() != Inet6Address.class || ipBlock.getSize().compareTo(Ipv6Block.BLOCK64_IPS) < 0) + throw new IllegalArgumentException("Please use a bigger IPv6 Block!"); + } - /** - * Returns the current block index - * @return block index which is currently used - */ - public BigInteger getCurrentBlock() { - return currentBlock.get(); - } + /** + * Returns the current block index + * + * @return block index which is currently used + */ + public BigInteger getCurrentBlock() { + return currentBlock.get(); + } - /** - * Returns the address offset for the current nano time - * @return address offset as long - */ - public long getAddressIndexInBlock() { - return System.nanoTime() - blockNanoStart.get().longValue(); - } + /** + * Returns the address offset for the current nano time + * + * @return address offset as long + */ + public long getAddressIndexInBlock() { + return System.nanoTime() - blockNanoStart.get().longValue(); + } - @Override - protected Tuple determineAddressPair(final Tuple remoteAddresses) throws HttpException { - InetAddress currentAddress = null; - InetAddress remoteAddress; - if (ipBlock.getType() == Inet6Address.class) { - if (remoteAddresses.r != null) { - currentAddress = extractLocalAddress(); - log.debug("Selected " + currentAddress.toString() + " as new outgoing ip"); - remoteAddress = remoteAddresses.r; - } else if (remoteAddresses.l != null) { - remoteAddress = remoteAddresses.l; - log.warn("Could not look up AAAA record for host. Falling back to unbalanced IPv4."); - } else { - throw new HttpException("Could not resolve host"); - } - } else { - throw new HttpException("Unknown IpBlock type: " + ipBlock.getType().getCanonicalName()); + @Override + protected Tuple determineAddressPair(final Tuple remoteAddresses) throws HttpException { + InetAddress currentAddress = null; + InetAddress remoteAddress; + if (ipBlock.getType() == Inet6Address.class) { + if (remoteAddresses.r != null) { + currentAddress = extractLocalAddress(); + log.debug("Selected " + currentAddress.toString() + " as new outgoing ip"); + remoteAddress = remoteAddresses.r; + } else if (remoteAddresses.l != null) { + remoteAddress = remoteAddresses.l; + log.warn("Could not look up AAAA record for host. Falling back to unbalanced IPv4."); + } else { + throw new HttpException("Could not resolve host"); + } + } else { + throw new HttpException("Unknown IpBlock type: " + ipBlock.getType().getCanonicalName()); + } + next.set(false); + return new Tuple<>(currentAddress, remoteAddress); } - next.set(false); - return new Tuple<>(currentAddress, remoteAddress); - } - @Override - protected void onAddressFailure(final InetAddress address) { - currentBlock.accumulateAndGet(BigInteger.ONE, BigInteger::add); - blockNanoStart.set(BigInteger.valueOf(System.nanoTime())); - } + @Override + protected void onAddressFailure(final InetAddress address) { + currentBlock.accumulateAndGet(BigInteger.ONE, BigInteger::add); + blockNanoStart.set(BigInteger.valueOf(System.nanoTime())); + } - private InetAddress extractLocalAddress() { - InetAddress localAddress; - long triesSinceBlockSkip = 0; - BigInteger it = BigInteger.valueOf(0); - do { - try { - if (ipBlock.getSize().multiply(BigInteger.valueOf(2)).compareTo(it) < 0) { - throw new RuntimeException("Can't find a free ip"); - } - it = it.add(BigInteger.ONE); - triesSinceBlockSkip++; - if (triesSinceBlockSkip > 128) { - this.currentBlock.accumulateAndGet(BigInteger.ONE, BigInteger::add); - } - final BigInteger nanoTime = BigInteger.valueOf(System.nanoTime()); - final BigInteger timeOffset = nanoTime.subtract(blockNanoStart.get()); - final BigInteger blockOffset = currentBlock.get().multiply(Ipv6Block.BLOCK64_IPS); - localAddress = ipBlock.getAddressAtIndex(blockOffset.add(timeOffset)); - } catch (final IllegalArgumentException ex) { - this.currentBlock.set(BigInteger.ZERO); - localAddress = null; - } - } while (localAddress == null || !ipFilter.test(localAddress) || !isValidAddress(localAddress)); - return localAddress; - } + private InetAddress extractLocalAddress() { + InetAddress localAddress; + long triesSinceBlockSkip = 0; + BigInteger it = BigInteger.valueOf(0); + do { + try { + if (ipBlock.getSize().multiply(BigInteger.valueOf(2)).compareTo(it) < 0) { + throw new RuntimeException("Can't find a free ip"); + } + it = it.add(BigInteger.ONE); + triesSinceBlockSkip++; + if (triesSinceBlockSkip > 128) { + this.currentBlock.accumulateAndGet(BigInteger.ONE, BigInteger::add); + } + final BigInteger nanoTime = BigInteger.valueOf(System.nanoTime()); + final BigInteger timeOffset = nanoTime.subtract(blockNanoStart.get()); + final BigInteger blockOffset = currentBlock.get().multiply(Ipv6Block.BLOCK64_IPS); + localAddress = ipBlock.getAddressAtIndex(blockOffset.add(timeOffset)); + } catch (final IllegalArgumentException ex) { + this.currentBlock.set(BigInteger.ZERO); + localAddress = null; + } + } while (localAddress == null || !ipFilter.test(localAddress) || !isValidAddress(localAddress)); + return localAddress; + } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/BigRandom.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/BigRandom.java index 700ef5d54..206c96958 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/BigRandom.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/BigRandom.java @@ -5,19 +5,19 @@ public final class BigRandom extends Random { - public BigInteger nextBigInt(int bits) { - if (bits < 32) { - return BigInteger.valueOf(next(31)); + public BigInteger nextBigInt(int bits) { + if (bits < 32) { + return BigInteger.valueOf(next(31)); + } + BigInteger value = BigInteger.ZERO; + int index = 0; + while (bits >= 32) { + bits -= 32; + value = value.add(BigInteger.valueOf(next(32)).shiftLeft((index++) * 32)); + } + if (bits > 0) + value = value.add(BigInteger.valueOf(next(bits)).shiftLeft(index * 32)); + return value; } - BigInteger value = BigInteger.ZERO; - int index = 0; - while (bits >= 32) { - bits -= 32; - value = value.add(BigInteger.valueOf(next(32)).shiftLeft((index++) * 32)); - } - if (bits > 0) - value = value.add(BigInteger.valueOf(next(bits)).shiftLeft(index * 32)); - return value; - } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/RateLimitException.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/RateLimitException.java index efec1346e..be63b0494 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/RateLimitException.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/RateLimitException.java @@ -2,15 +2,15 @@ public final class RateLimitException extends RuntimeException { - public RateLimitException() { - super(); - } + public RateLimitException() { + super(); + } - public RateLimitException(final String message) { - super(message); - } + public RateLimitException(final String message) { + super(message); + } - public RateLimitException(final String message, final Throwable cause) { - super(message, cause); - } + public RateLimitException(final String message, final Throwable cause) { + super(message, cause); + } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/Tuple.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/Tuple.java index 1cbfed900..77ff0febc 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/Tuple.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/Tuple.java @@ -1,10 +1,11 @@ package com.sedmelluq.lava.extensions.youtuberotator.tools; -public class Tuple { - public final L l; - public final R r; - public Tuple(L l, R r) { - this.l = l; - this.r = r; - } +public class Tuple { + public final L l; + public final R r; + + public Tuple(L l, R r) { + this.l = l; + this.r = r; + } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/CombinedIpBlock.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/CombinedIpBlock.java index f827d40d1..c9de252f9 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/CombinedIpBlock.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/CombinedIpBlock.java @@ -10,115 +10,115 @@ public final class CombinedIpBlock extends IpBlock { - private static final Random random = new Random(); + private static final Random random = new Random(); - private final Class type; - private final List ipBlocks; - private final BigInteger size; - private final int[] hitProbability; - private final ReentrantLock lock; + private final Class type; + private final List ipBlocks; + private final BigInteger size; + private final int[] hitProbability; + private final ReentrantLock lock; - public CombinedIpBlock(final List ipBlocks) { - if (ipBlocks.size() == 0) - throw new IllegalArgumentException("Ip Blocks list size must be greater than zero"); - this.type = ipBlocks.get(0).getType(); - if (ipBlocks.stream().anyMatch(block -> !block.getType().equals(type))) - throw new IllegalArgumentException("All Ip Blocks must have the same type for a combined block"); - this.ipBlocks = ipBlocks; - this.hitProbability = new int[this.ipBlocks.size()]; + public CombinedIpBlock(final List ipBlocks) { + if (ipBlocks.size() == 0) + throw new IllegalArgumentException("Ip Blocks list size must be greater than zero"); + this.type = ipBlocks.get(0).getType(); + if (ipBlocks.stream().anyMatch(block -> !block.getType().equals(type))) + throw new IllegalArgumentException("All Ip Blocks must have the same type for a combined block"); + this.ipBlocks = ipBlocks; + this.hitProbability = new int[this.ipBlocks.size()]; - // Cache size of all blocks - BigInteger count = BigInteger.ZERO; - for (final IpBlock ipBlock : ipBlocks) { - count = count.add(ipBlock.getSize()); + // Cache size of all blocks + BigInteger count = BigInteger.ZERO; + for (final IpBlock ipBlock : ipBlocks) { + count = count.add(ipBlock.getSize()); + } + this.size = count; + this.lock = new ReentrantLock(); + this.calculateHitProbabilities(); } - this.size = count; - this.lock = new ReentrantLock(); - this.calculateHitProbabilities(); - } - private void calculateHitProbabilities() { - final BigDecimal size = new BigDecimal(this.size); - final BigInteger sizeMultiplicator = BigInteger.valueOf(Integer.MAX_VALUE); // 100% target = Integer.MAX_VALUE - for (int i = 0; i < ipBlocks.size(); i++) { - final IpBlock ipBlock = ipBlocks.get(i); - final BigInteger calcSize = ipBlock.getSize().multiply(sizeMultiplicator); - final BigDecimal probability = new BigDecimal(calcSize).divide(size, BigDecimal.ROUND_HALF_UP); - this.hitProbability[i] = probability.intValue(); + private void calculateHitProbabilities() { + final BigDecimal size = new BigDecimal(this.size); + final BigInteger sizeMultiplicator = BigInteger.valueOf(Integer.MAX_VALUE); // 100% target = Integer.MAX_VALUE + for (int i = 0; i < ipBlocks.size(); i++) { + final IpBlock ipBlock = ipBlocks.get(i); + final BigInteger calcSize = ipBlock.getSize().multiply(sizeMultiplicator); + final BigDecimal probability = new BigDecimal(calcSize).divide(size, BigDecimal.ROUND_HALF_UP); + this.hitProbability[i] = probability.intValue(); + } } - } - @Override - public InetAddress getRandomAddress() { - if (ipBlocks.size() == 1) - return ipBlocks.get(0).getRandomAddress(); - final int probability = random.nextInt(Integer.MAX_VALUE); - int probabilitySum = 0; - int matchIndex = 0; - for (int i = 0; i < hitProbability.length; i++) { - if (hitProbability[i] > probability - probabilitySum) { - matchIndex = i; - break; - } - probabilitySum += hitProbability[i]; + @Override + public InetAddress getRandomAddress() { + if (ipBlocks.size() == 1) + return ipBlocks.get(0).getRandomAddress(); + final int probability = random.nextInt(Integer.MAX_VALUE); + int probabilitySum = 0; + int matchIndex = 0; + for (int i = 0; i < hitProbability.length; i++) { + if (hitProbability[i] > probability - probabilitySum) { + matchIndex = i; + break; + } + probabilitySum += hitProbability[i]; + } + return ipBlocks.get(matchIndex).getRandomAddress(); } - return ipBlocks.get(matchIndex).getRandomAddress(); - } - @Override - public InetAddress getAddressAtIndex(BigInteger index) { - int blockIndex = 0; - while (index.compareTo(BigInteger.ZERO) > 0) { - if (ipBlocks.size() <= blockIndex) - break; - final IpBlock ipBlock = ipBlocks.get(blockIndex); - if (ipBlock.getSize().compareTo(index) > 0) - return ipBlock.getAddressAtIndex(index); - index = index.subtract(ipBlock.getSize()); - blockIndex++; + @Override + public InetAddress getAddressAtIndex(BigInteger index) { + int blockIndex = 0; + while (index.compareTo(BigInteger.ZERO) > 0) { + if (ipBlocks.size() <= blockIndex) + break; + final IpBlock ipBlock = ipBlocks.get(blockIndex); + if (ipBlock.getSize().compareTo(index) > 0) + return ipBlock.getAddressAtIndex(index); + index = index.subtract(ipBlock.getSize()); + blockIndex++; + } + throw new IllegalArgumentException("Index out of bounds for the CombinedBlock"); } - throw new IllegalArgumentException("Index out of bounds for the CombinedBlock"); - } - @Override - public Class getType() { - return this.type; - } + @Override + public Class getType() { + return this.type; + } - @Override - public BigInteger getSize() { - return this.size; - } + @Override + public BigInteger getSize() { + return this.size; + } - /** - * Estimates the virtual mask bits of the combined block - * - * @return mask bits - */ - @Override - public int getMaskBits() { - int[] bits = new int[getType().equals(Inet6Address.class) ? 128 : 32]; - int maskBits = bits.length; - try { - lock.lockInterruptibly(); - for (final IpBlock ipBlock : ipBlocks) { - final int blockMaskBits = ipBlock.getMaskBits(); - final int bitsAtIndex = bits[blockMaskBits - 1]; - bits[blockMaskBits - 1] = bitsAtIndex + 1; - } - lock.unlock(); + /** + * Estimates the virtual mask bits of the combined block + * + * @return mask bits + */ + @Override + public int getMaskBits() { + int[] bits = new int[getType().equals(Inet6Address.class) ? 128 : 32]; + int maskBits = bits.length; + try { + lock.lockInterruptibly(); + for (final IpBlock ipBlock : ipBlocks) { + final int blockMaskBits = ipBlock.getMaskBits(); + final int bitsAtIndex = bits[blockMaskBits - 1]; + bits[blockMaskBits - 1] = bitsAtIndex + 1; + } + lock.unlock(); - for (int i = bits.length - 1; i > 0; i--) { - final int bitsAtIndex = bits[i]; - final int nextSize = bitsAtIndex / 2; - bits[i] = bitsAtIndex - nextSize * 2; - bits[i - 1] = bits[i - 1] + nextSize; - if (bits[i - 1] > 0) - maskBits = i; - } - return maskBits; - } catch (final InterruptedException ex) { - throw new RuntimeException("Could not acquire lock", ex); + for (int i = bits.length - 1; i > 0; i--) { + final int bitsAtIndex = bits[i]; + final int nextSize = bitsAtIndex / 2; + bits[i] = bitsAtIndex - nextSize * 2; + bits[i - 1] = bits[i - 1] + nextSize; + if (bits[i - 1] > 0) + maskBits = i; + } + return maskBits; + } catch (final InterruptedException ex) { + throw new RuntimeException("Could not acquire lock", ex); + } } - } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/IpAddressTools.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/IpAddressTools.java index ca64d70cd..ef077c05f 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/IpAddressTools.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/IpAddressTools.java @@ -12,33 +12,33 @@ public final class IpAddressTools { - private static final Random RANDOM = new Random(); + private static final Random RANDOM = new Random(); - public static Tuple getRandomAddressesFromHost(final HttpHost host) throws HttpException { - final List ipList; - try { - ipList = Arrays.asList(InetAddress.getAllByName(host.getHostName())); - } catch (final UnknownHostException e) { - throw new HttpException("Could not resolve " + host.getHostName(), e); - } - final List ip6 = new ArrayList<>(); - final List ip4 = new ArrayList<>(); + public static Tuple getRandomAddressesFromHost(final HttpHost host) throws HttpException { + final List ipList; + try { + ipList = Arrays.asList(InetAddress.getAllByName(host.getHostName())); + } catch (final UnknownHostException e) { + throw new HttpException("Could not resolve " + host.getHostName(), e); + } + final List ip6 = new ArrayList<>(); + final List ip4 = new ArrayList<>(); - Collections.reverse(ipList); - for (final InetAddress ip : ipList) { - if (ip instanceof Inet6Address) - ip6.add((Inet6Address) ip); - else if (ip instanceof Inet4Address) - ip4.add((Inet4Address) ip); + Collections.reverse(ipList); + for (final InetAddress ip : ipList) { + if (ip instanceof Inet6Address) + ip6.add((Inet6Address) ip); + else if (ip instanceof Inet4Address) + ip4.add((Inet4Address) ip); + } + return new Tuple<>(getRandomFromList(ip4), getRandomFromList(ip6)); } - return new Tuple<>(getRandomFromList(ip4), getRandomFromList(ip6)); - } - public static T getRandomFromList(final List list) { - if (list.isEmpty()) - return null; - if (list.size() == 1) - return list.get(0); - return list.get(RANDOM.nextInt(list.size())); - } + public static T getRandomFromList(final List list) { + if (list.isEmpty()) + return null; + if (list.size() == 1) + return list.get(0); + return list.get(RANDOM.nextInt(list.size())); + } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/IpBlock.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/IpBlock.java index 8699ffa71..d0dc18983 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/IpBlock.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/IpBlock.java @@ -5,19 +5,19 @@ public abstract class IpBlock { - public abstract I getRandomAddress(); + public abstract I getRandomAddress(); - public I getAddressAtIndex(long index) { - return getAddressAtIndex(BigInteger.valueOf(index)); - } + public I getAddressAtIndex(long index) { + return getAddressAtIndex(BigInteger.valueOf(index)); + } - public I getAddressAtIndex(BigInteger index) { - return getAddressAtIndex(index.longValue()); - } + public I getAddressAtIndex(BigInteger index) { + return getAddressAtIndex(index.longValue()); + } - public abstract Class getType(); + public abstract Class getType(); - public abstract BigInteger getSize(); + public abstract BigInteger getSize(); - public abstract int getMaskBits(); + public abstract int getMaskBits(); } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/Ipv4Block.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/Ipv4Block.java index 8991d755e..ba89ec327 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/Ipv4Block.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/Ipv4Block.java @@ -12,92 +12,92 @@ public final class Ipv4Block extends IpBlock { - public static boolean isIpv4CidrBlock(String cidr) { - if (!cidr.contains("/")) - cidr += "/32"; - return CIDR_REGEX.matcher(cidr).matches(); - } - - private static final Pattern CIDR_REGEX = Pattern.compile("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"); - private static final int NBITS = 32; - private static final Logger log = LoggerFactory.getLogger(Ipv4Block.class); - private static final Random random = new Random(); - - private static int matchAddress(Matcher matcher) { - int addr = 0; - for (int i = 1; i <= 4; ++i) { - int n = (rangeCheck(Integer.parseInt(matcher.group(i)), 0, 255)); - addr |= ((n & 0xff) << 8 * (4 - i)); + public static boolean isIpv4CidrBlock(String cidr) { + if (!cidr.contains("/")) + cidr += "/32"; + return CIDR_REGEX.matcher(cidr).matches(); } - return addr; - } - private static int rangeCheck(int value, int begin, int end) { - if (value >= begin && value <= end) { // (begin,end] - return value; + private static final Pattern CIDR_REGEX = Pattern.compile("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"); + private static final int NBITS = 32; + private static final Logger log = LoggerFactory.getLogger(Ipv4Block.class); + private static final Random random = new Random(); + + private static int matchAddress(Matcher matcher) { + int addr = 0; + for (int i = 1; i <= 4; ++i) { + int n = (rangeCheck(Integer.parseInt(matcher.group(i)), 0, 255)); + addr |= ((n & 0xff) << 8 * (4 - i)); + } + return addr; } - throw new IllegalArgumentException("Value [" + value + "] not in range [" + begin + "," + end + "]"); - } - - private final int maskBits; - private final int address; - - public Ipv4Block(String cidr) { - if (!cidr.contains("/")) - cidr += "/32"; - final Matcher matcher = CIDR_REGEX.matcher(cidr); - if (matcher.matches()) { - this.address = matchAddress(matcher); - this.maskBits = Integer.parseInt(matcher.group(5)); - } else - throw new IllegalArgumentException("Could not parse [" + cidr + "]"); - log.info("Using Ipv4Block with {} addresses", getSize()); - } - - @Override - public Inet4Address getRandomAddress() { - if (maskBits == NBITS) return intToAddress(address); - - final int randMask = Integer.MAX_VALUE >> maskBits - 1; - final int maskedRandom = random.nextInt() & randMask; - - final Inet4Address inetAddress = intToAddress(address + maskedRandom); - log.debug(inetAddress.toString()); - return inetAddress; - } - - @Override - public Inet4Address getAddressAtIndex(final long index) { - if (index > Math.pow(2, NBITS - maskBits)) - throw new IllegalArgumentException("Index out of bounds for provided CIDR Block"); - return intToAddress(address + (int) index); - } - - @Override - public Class getType() { - return Inet4Address.class; - } - - @Override - public BigInteger getSize() { - return BigInteger.valueOf(2).pow(NBITS - maskBits); - } - - @Override - public int getMaskBits() { - return maskBits; - } - - private Inet4Address intToAddress(final int val) { - byte[] octets = new byte[4]; - for (int j = 3; j >= 0; --j) { - octets[j] |= ((val >>> 8 * (3 - j)) & (0xff)); + + private static int rangeCheck(int value, int begin, int end) { + if (value >= begin && value <= end) { // (begin,end] + return value; + } + throw new IllegalArgumentException("Value [" + value + "] not in range [" + begin + "," + end + "]"); + } + + private final int maskBits; + private final int address; + + public Ipv4Block(String cidr) { + if (!cidr.contains("/")) + cidr += "/32"; + final Matcher matcher = CIDR_REGEX.matcher(cidr); + if (matcher.matches()) { + this.address = matchAddress(matcher); + this.maskBits = Integer.parseInt(matcher.group(5)); + } else + throw new IllegalArgumentException("Could not parse [" + cidr + "]"); + log.info("Using Ipv4Block with {} addresses", getSize()); + } + + @Override + public Inet4Address getRandomAddress() { + if (maskBits == NBITS) return intToAddress(address); + + final int randMask = Integer.MAX_VALUE >> maskBits - 1; + final int maskedRandom = random.nextInt() & randMask; + + final Inet4Address inetAddress = intToAddress(address + maskedRandom); + log.debug(inetAddress.toString()); + return inetAddress; + } + + @Override + public Inet4Address getAddressAtIndex(final long index) { + if (index > Math.pow(2, NBITS - maskBits)) + throw new IllegalArgumentException("Index out of bounds for provided CIDR Block"); + return intToAddress(address + (int) index); + } + + @Override + public Class getType() { + return Inet4Address.class; + } + + @Override + public BigInteger getSize() { + return BigInteger.valueOf(2).pow(NBITS - maskBits); + } + + @Override + public int getMaskBits() { + return maskBits; } - try { - return (Inet4Address) Inet4Address.getByAddress(octets); - } catch (UnknownHostException e) { - throw new RuntimeException(e); + + private Inet4Address intToAddress(final int val) { + byte[] octets = new byte[4]; + for (int j = 3; j >= 0; --j) { + octets[j] |= ((val >>> 8 * (3 - j)) & (0xff)); + } + try { + return (Inet4Address) Inet4Address.getByAddress(octets); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } } - } } diff --git a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/Ipv6Block.java b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/Ipv6Block.java index df6d52089..e7c9d1b56 100644 --- a/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/Ipv6Block.java +++ b/extensions/youtube-rotator/src/main/java/com/sedmelluq/lava/extensions/youtuberotator/tools/ip/Ipv6Block.java @@ -16,125 +16,125 @@ */ public class Ipv6Block extends IpBlock { - public static boolean isIpv6CidrBlock(String cidr) { - if (!cidr.contains("/")) - cidr += "/128"; - return CIDR_REGEX.matcher(cidr).matches(); - } - - private static final BigInteger TWO = BigInteger.valueOf(2); - private static final BigInteger BITS1 = BigInteger.valueOf(-1); - public static final BigInteger BLOCK64_IPS = TWO.pow(64); - public static final int IPV6_BIT_SIZE = 128; - - private static final BigRandom random = new BigRandom(); - - private static final Pattern CIDR_REGEX = Pattern.compile("([\\da-f:]+)/(\\d{1,3})"); - private final String cidr; - private final int maskBits; - private final BigInteger prefix; - private static final Logger log = LoggerFactory.getLogger(Ipv6Block.class); - - public Ipv6Block(String cidr) { - if (!cidr.contains("/")) - cidr += "/128"; - this.cidr = cidr.toLowerCase(); - Matcher matcher = CIDR_REGEX.matcher(this.cidr); - if (!matcher.find()) { - throw new IllegalArgumentException(cidr + " does not appear to be a valid CIDR."); + public static boolean isIpv6CidrBlock(String cidr) { + if (!cidr.contains("/")) + cidr += "/128"; + return CIDR_REGEX.matcher(cidr).matches(); } - BigInteger unboundedPrefix; - try { - InetAddress address = InetAddress.getByName(matcher.group(1)); - unboundedPrefix = addressToLong((Inet6Address) address); - } catch (UnknownHostException e) { - throw new IllegalArgumentException("Invalid IPv6 address", e); + private static final BigInteger TWO = BigInteger.valueOf(2); + private static final BigInteger BITS1 = BigInteger.valueOf(-1); + public static final BigInteger BLOCK64_IPS = TWO.pow(64); + public static final int IPV6_BIT_SIZE = 128; + + private static final BigRandom random = new BigRandom(); + + private static final Pattern CIDR_REGEX = Pattern.compile("([\\da-f:]+)/(\\d{1,3})"); + private final String cidr; + private final int maskBits; + private final BigInteger prefix; + private static final Logger log = LoggerFactory.getLogger(Ipv6Block.class); + + public Ipv6Block(String cidr) { + if (!cidr.contains("/")) + cidr += "/128"; + this.cidr = cidr.toLowerCase(); + Matcher matcher = CIDR_REGEX.matcher(this.cidr); + if (!matcher.find()) { + throw new IllegalArgumentException(cidr + " does not appear to be a valid CIDR."); + } + + BigInteger unboundedPrefix; + try { + InetAddress address = InetAddress.getByName(matcher.group(1)); + unboundedPrefix = addressToLong((Inet6Address) address); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("Invalid IPv6 address", e); + } + maskBits = Integer.parseInt(matcher.group(2)); + + BigInteger prefixMask = BITS1.shiftLeft(IPV6_BIT_SIZE - maskBits - 1); + prefix = unboundedPrefix.and(prefixMask); + log.info("Using Ipv6Block with {} addresses", getSize()); } - maskBits = Integer.parseInt(matcher.group(2)); - - BigInteger prefixMask = BITS1.shiftLeft(IPV6_BIT_SIZE - maskBits - 1); - prefix = unboundedPrefix.and(prefixMask); - log.info("Using Ipv6Block with {} addresses", getSize()); - } - - /** - * @return a random member of this subnet. - */ - @Override - public Inet6Address getRandomAddress() { - if (maskBits == IPV6_BIT_SIZE) return longToAddress(prefix); - - final BigInteger randomAddressOffset = random.nextBigInt(IPV6_BIT_SIZE - (maskBits + 1)).abs(); - Inet6Address inetAddress = longToAddress(prefix.add(randomAddressOffset)); - log.debug(inetAddress.toString()); - return inetAddress; - } - - @Override - public Inet6Address getAddressAtIndex(long index) { - return getAddressAtIndex(BigInteger.valueOf(index)); - } - - @Override - public Inet6Address getAddressAtIndex(final BigInteger index) { - if (index.compareTo(getSize()) > 0) - throw new IllegalArgumentException("Index out of bounds for provided CIDR Block"); - return longToAddress(prefix.add(index)); - } - - @Override - public Class getType() { - return Inet6Address.class; - } - - @Override - public BigInteger getSize() { - return TWO.pow(IPV6_BIT_SIZE - maskBits); - } - - @Override - public String toString() { - return cidr; - } - - public int getMaskBits() { - return maskBits; - } - - private static Inet6Address longToAddress(final BigInteger l) { - final byte[] b = new byte[IPV6_BIT_SIZE / 8]; - final int start = (b.length - 1) * 8; - for (int i = 0; i < b.length; i++) { - int shift = start - i * 8; - if (shift > 0) - b[i] = l.shiftRight(start - i * 8).byteValue(); - else - b[i] = l.byteValue(); + + /** + * @return a random member of this subnet. + */ + @Override + public Inet6Address getRandomAddress() { + if (maskBits == IPV6_BIT_SIZE) return longToAddress(prefix); + + final BigInteger randomAddressOffset = random.nextBigInt(IPV6_BIT_SIZE - (maskBits + 1)).abs(); + Inet6Address inetAddress = longToAddress(prefix.add(randomAddressOffset)); + log.debug(inetAddress.toString()); + return inetAddress; } - try { - return (Inet6Address) Inet6Address.getByAddress(b); - } catch (final UnknownHostException e) { - throw new RuntimeException(e); // This should not happen, as we do not do a DNS lookup + + @Override + public Inet6Address getAddressAtIndex(long index) { + return getAddressAtIndex(BigInteger.valueOf(index)); } - } - - private static BigInteger addressToLong(final Inet6Address address) { - return bytesToLong(address.getAddress()); - } - - private static BigInteger bytesToLong(final byte[] b) { - BigInteger value = BigInteger.valueOf(0); - final int start = (b.length - 1) * 8; - value = value.or(BigInteger.valueOf(b[0]).shiftLeft(start)); - for (int i = 1; i < b.length; i++) { - final int shift = start - i * 8; - if (shift > 0) - value = value.or(BigInteger.valueOf(b[i] & 0xff).shiftLeft(shift)); - else - value = value.or(BigInteger.valueOf(b[i] & 0xff)); + + @Override + public Inet6Address getAddressAtIndex(final BigInteger index) { + if (index.compareTo(getSize()) > 0) + throw new IllegalArgumentException("Index out of bounds for provided CIDR Block"); + return longToAddress(prefix.add(index)); + } + + @Override + public Class getType() { + return Inet6Address.class; + } + + @Override + public BigInteger getSize() { + return TWO.pow(IPV6_BIT_SIZE - maskBits); + } + + @Override + public String toString() { + return cidr; + } + + public int getMaskBits() { + return maskBits; + } + + private static Inet6Address longToAddress(final BigInteger l) { + final byte[] b = new byte[IPV6_BIT_SIZE / 8]; + final int start = (b.length - 1) * 8; + for (int i = 0; i < b.length; i++) { + int shift = start - i * 8; + if (shift > 0) + b[i] = l.shiftRight(start - i * 8).byteValue(); + else + b[i] = l.byteValue(); + } + try { + return (Inet6Address) Inet6Address.getByAddress(b); + } catch (final UnknownHostException e) { + throw new RuntimeException(e); // This should not happen, as we do not do a DNS lookup + } + } + + private static BigInteger addressToLong(final Inet6Address address) { + return bytesToLong(address.getAddress()); + } + + private static BigInteger bytesToLong(final byte[] b) { + BigInteger value = BigInteger.valueOf(0); + final int start = (b.length - 1) * 8; + value = value.or(BigInteger.valueOf(b[0]).shiftLeft(start)); + for (int i = 1; i < b.length; i++) { + final int shift = start - i * 8; + if (shift > 0) + value = value.or(BigInteger.valueOf(b[i] & 0xff).shiftLeft(shift)); + else + value = value.or(BigInteger.valueOf(b[i] & 0xff)); + } + return value; } - return value; - } -} \ No newline at end of file +} diff --git a/jitpack.yml b/jitpack.yml index a18faeb07..2533818b3 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,2 +1,2 @@ install: - - ./gradlew clean build publishToMavenLocal \ No newline at end of file + - ./gradlew clean build publishToMavenLocal diff --git a/main/build.gradle.kts b/main/build.gradle.kts index 2fb4c2fef..63eb60975 100644 --- a/main/build.gradle.kts +++ b/main/build.gradle.kts @@ -5,55 +5,55 @@ import kotlin.io.path.createDirectories import kotlin.io.path.writeText plugins { - `java-library` - alias(libs.plugins.maven.publish.base) + `java-library` + alias(libs.plugins.maven.publish.base) } base { - archivesName = "lavaplayer" + archivesName = "lavaplayer" } dependencies { - api(projects.common) - implementation(projects.nativesPublish) - implementation(libs.jaadec.fork) - implementation(libs.rhino.engine) - implementation(libs.slf4j) + api(projects.common) + implementation(projects.nativesPublish) + implementation(libs.jaadec.fork) + implementation(libs.rhino.engine) + implementation(libs.slf4j) - api(libs.httpclient) - implementation(libs.commons.io) + api(libs.httpclient) + implementation(libs.commons.io) - api(libs.jackson.core) - api(libs.jackson.databind) + api(libs.jackson.core) + api(libs.jackson.databind) - implementation(libs.jsoup) - implementation(libs.base64) - implementation(libs.json) + implementation(libs.jsoup) + implementation(libs.base64) + implementation(libs.json) - testImplementation(libs.groovy) - testImplementation(libs.spock.core) - testImplementation(libs.logback.classic) + testImplementation(libs.groovy) + testImplementation(libs.spock.core) + testImplementation(libs.logback.classic) } tasks { - val updateVersion by registering { - val output = "$buildDir/resources/main/com/sedmelluq/discord/lavaplayer/tools/version.txt" - inputs.property("version", version) - outputs.file(output) - - doLast { - Path(output).let { - it.parent.createDirectories() - it.writeText(version.toString()) - } + val updateVersion by registering { + val output = "$buildDir/resources/main/com/sedmelluq/discord/lavaplayer/tools/version.txt" + inputs.property("version", version) + outputs.file(output) + + doLast { + Path(output).let { + it.parent.createDirectories() + it.writeText(version.toString()) + } + } } - } - classes { - dependsOn(updateVersion) - } + classes { + dependsOn(updateVersion) + } } mavenPublishing { - configure(JavaLibrary(JavadocJar.Javadoc())) + configure(JavaLibrary(JavadocJar.Javadoc())) } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainer.java index 347d8b0d7..d4cde1791 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainer.java @@ -19,34 +19,34 @@ * Lists currently supported containers and their probes. */ public enum MediaContainer { - WAV(new WavContainerProbe()), - MKV(new MatroskaContainerProbe()), - MP4(new MpegContainerProbe()), - FLAC(new FlacContainerProbe()), - OGG(new OggContainerProbe()), - M3U(new M3uPlaylistContainerProbe()), - PLS(new PlsPlaylistContainerProbe()), - PLAIN(new PlainPlaylistContainerProbe()), - MP3(new Mp3ContainerProbe()), - ADTS(new AdtsContainerProbe()), - MPEGADTS(new MpegAdtsContainerProbe()); - - /** - * The probe used to detect files using this container and create the audio tracks for them. - */ - public final MediaContainerProbe probe; - - MediaContainer(MediaContainerProbe probe) { - this.probe = probe; - } - - public static List asList() { - List probes = new ArrayList<>(); - - for (MediaContainer container : MediaContainer.class.getEnumConstants()) { - probes.add(container.probe); + WAV(new WavContainerProbe()), + MKV(new MatroskaContainerProbe()), + MP4(new MpegContainerProbe()), + FLAC(new FlacContainerProbe()), + OGG(new OggContainerProbe()), + M3U(new M3uPlaylistContainerProbe()), + PLS(new PlsPlaylistContainerProbe()), + PLAIN(new PlainPlaylistContainerProbe()), + MP3(new Mp3ContainerProbe()), + ADTS(new AdtsContainerProbe()), + MPEGADTS(new MpegAdtsContainerProbe()); + + /** + * The probe used to detect files using this container and create the audio tracks for them. + */ + public final MediaContainerProbe probe; + + MediaContainer(MediaContainerProbe probe) { + this.probe = probe; } - return probes; - } + public static List asList() { + List probes = new ArrayList<>(); + + for (MediaContainer container : MediaContainer.class.getEnumConstants()) { + probes.add(container.probe); + } + + return probes; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerDescriptor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerDescriptor.java index 3060e616d..90ae7f922 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerDescriptor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerDescriptor.java @@ -5,15 +5,15 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; public class MediaContainerDescriptor { - public final MediaContainerProbe probe; - public final String parameters; + public final MediaContainerProbe probe; + public final String parameters; - public MediaContainerDescriptor(MediaContainerProbe probe, String parameters) { - this.probe = probe; - this.parameters = parameters; - } + public MediaContainerDescriptor(MediaContainerProbe probe, String parameters) { + this.probe = probe; + this.parameters = parameters; + } - public AudioTrack createTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - return probe.createTrack(parameters, trackInfo, inputStream); - } + public AudioTrack createTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + return probe.createTrack(parameters, trackInfo, inputStream); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerDetection.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerDetection.java index b35534159..dc665847f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerDetection.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerDetection.java @@ -5,11 +5,12 @@ import com.sedmelluq.discord.lavaplayer.tools.io.SavedHeadSeekableInputStream; import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; import com.sedmelluq.discord.lavaplayer.track.AudioReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.nio.charset.Charset; import java.util.regex.Pattern; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import static com.sedmelluq.discord.lavaplayer.container.MediaContainerDetectionResult.unknownFormat; import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS; @@ -18,148 +19,148 @@ * Detects the container used by a file and whether the specific file is supported for playback. */ public class MediaContainerDetection { - public static final String UNKNOWN_TITLE = "Unknown title"; - public static final String UNKNOWN_ARTIST = "Unknown artist"; - public static final int STREAM_SCAN_DISTANCE = 1000; - - private static final Logger log = LoggerFactory.getLogger(MediaContainerDetection.class); - - private static final int HEAD_MARK_LIMIT = 1024; - - private final MediaContainerRegistry containerRegistry; - private final AudioReference reference; - private final SeekableInputStream inputStream; - private final MediaContainerHints hints; - - /** - * @param reference Reference to the track with an identifier, used in the AudioTrackInfo in result - * @param inputStream Input stream of the file - * @param hints Hints about the format (mime type, extension) - */ - public MediaContainerDetection(MediaContainerRegistry containerRegistry, AudioReference reference, - SeekableInputStream inputStream, MediaContainerHints hints) { - - this.containerRegistry = containerRegistry; - this.reference = reference; - this.inputStream = inputStream; - this.hints = hints; - } - - /** - * @return Result of detection. - */ - public MediaContainerDetectionResult detectContainer() { - MediaContainerDetectionResult result; - - try { - SavedHeadSeekableInputStream savedHeadInputStream = new SavedHeadSeekableInputStream(inputStream, HEAD_MARK_LIMIT); - savedHeadInputStream.loadHead(); - - result = detectContainer(savedHeadInputStream, true); - - if (result == null) { - result = detectContainer(savedHeadInputStream, false); - } - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions("Could not read the file for detecting file type.", SUSPICIOUS, e); + public static final String UNKNOWN_TITLE = "Unknown title"; + public static final String UNKNOWN_ARTIST = "Unknown artist"; + public static final int STREAM_SCAN_DISTANCE = 1000; + + private static final Logger log = LoggerFactory.getLogger(MediaContainerDetection.class); + + private static final int HEAD_MARK_LIMIT = 1024; + + private final MediaContainerRegistry containerRegistry; + private final AudioReference reference; + private final SeekableInputStream inputStream; + private final MediaContainerHints hints; + + /** + * @param reference Reference to the track with an identifier, used in the AudioTrackInfo in result + * @param inputStream Input stream of the file + * @param hints Hints about the format (mime type, extension) + */ + public MediaContainerDetection(MediaContainerRegistry containerRegistry, AudioReference reference, + SeekableInputStream inputStream, MediaContainerHints hints) { + + this.containerRegistry = containerRegistry; + this.reference = reference; + this.inputStream = inputStream; + this.hints = hints; } - return result != null ? result : unknownFormat(); - } + /** + * @return Result of detection. + */ + public MediaContainerDetectionResult detectContainer() { + MediaContainerDetectionResult result; - private MediaContainerDetectionResult detectContainer(SeekableInputStream innerStream, boolean matchHints) - throws IOException { + try { + SavedHeadSeekableInputStream savedHeadInputStream = new SavedHeadSeekableInputStream(inputStream, HEAD_MARK_LIMIT); + savedHeadInputStream.loadHead(); - for (MediaContainerProbe probe : containerRegistry.getAll()) { - if (matchHints == probe.matchesHints(hints)) { - innerStream.seek(0); - MediaContainerDetectionResult result = checkContainer(probe, reference, innerStream); + result = detectContainer(savedHeadInputStream, true); - if (result != null) { - return result; + if (result == null) { + result = detectContainer(savedHeadInputStream, false); + } + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions("Could not read the file for detecting file type.", SUSPICIOUS, e); } - } + + return result != null ? result : unknownFormat(); } - return null; - } + private MediaContainerDetectionResult detectContainer(SeekableInputStream innerStream, boolean matchHints) + throws IOException { + + for (MediaContainerProbe probe : containerRegistry.getAll()) { + if (matchHints == probe.matchesHints(hints)) { + innerStream.seek(0); + MediaContainerDetectionResult result = checkContainer(probe, reference, innerStream); - private static MediaContainerDetectionResult checkContainer(MediaContainerProbe probe, AudioReference reference, - SeekableInputStream inputStream) { + if (result != null) { + return result; + } + } + } - try { - return probe.probe(reference, inputStream); - } catch (Exception e) { - log.warn("Attempting to detect file with container {} failed.", probe.getName(), e); - return null; + return null; } - } - - /** - * Checks the next bytes in the stream if they match the specified bytes. The input may contain -1 as byte value as - * a wildcard, which means the value of this byte does not matter. The position of the stream is restored on return. - * - * @param stream Input stream to read the bytes from - * @param match Bytes that the next bytes from input stream should match (-1 as wildcard - * @return True if the bytes matched - * @throws IOException On IO error - */ - public static boolean checkNextBytes(SeekableInputStream stream, int[] match) throws IOException { - return checkNextBytes(stream, match, true); - } - - /** - * Checks the next bytes in the stream if they match the specified bytes. The input may contain -1 as byte value as - * a wildcard, which means the value of this byte does not matter. - * - * @param stream Input stream to read the bytes from - * @param match Bytes that the next bytes from input stream should match (-1 as wildcard - * @param rewind If set to true, restores the original position of the stream after checking - * @return True if the bytes matched - * @throws IOException On IO error - */ - public static boolean checkNextBytes(SeekableInputStream stream, int[] match, boolean rewind) throws IOException { - long position = stream.getPosition(); - boolean result = true; - - for (int matchByte : match) { - int inputByte = stream.read(); - - if (inputByte == -1 || (matchByte != -1 && matchByte != inputByte)) { - result = false; - break; - } + + private static MediaContainerDetectionResult checkContainer(MediaContainerProbe probe, AudioReference reference, + SeekableInputStream inputStream) { + + try { + return probe.probe(reference, inputStream); + } catch (Exception e) { + log.warn("Attempting to detect file with container {} failed.", probe.getName(), e); + return null; + } } - if (rewind) { - stream.seek(position); + /** + * Checks the next bytes in the stream if they match the specified bytes. The input may contain -1 as byte value as + * a wildcard, which means the value of this byte does not matter. The position of the stream is restored on return. + * + * @param stream Input stream to read the bytes from + * @param match Bytes that the next bytes from input stream should match (-1 as wildcard + * @return True if the bytes matched + * @throws IOException On IO error + */ + public static boolean checkNextBytes(SeekableInputStream stream, int[] match) throws IOException { + return checkNextBytes(stream, match, true); } - return result; - } - - /** - * Check if the next bytes in the stream match the specified regex pattern. - * - * @param stream Input stream to read the bytes from - * @param distance Maximum number of bytes to read for matching - * @param pattern Pattern to match against - * @param charset Charset to use to decode the bytes - * @return True if the next bytes in the stream are a match - * @throws IOException On read error - */ - public static boolean matchNextBytesAsRegex(SeekableInputStream stream, int distance, Pattern pattern, Charset charset) throws IOException { - long position = stream.getPosition(); - byte[] bytes = new byte[distance]; - - int read = new GreedyInputStream(stream).read(bytes); - stream.seek(position); - - if (read == -1) { - return false; + /** + * Checks the next bytes in the stream if they match the specified bytes. The input may contain -1 as byte value as + * a wildcard, which means the value of this byte does not matter. + * + * @param stream Input stream to read the bytes from + * @param match Bytes that the next bytes from input stream should match (-1 as wildcard + * @param rewind If set to true, restores the original position of the stream after checking + * @return True if the bytes matched + * @throws IOException On IO error + */ + public static boolean checkNextBytes(SeekableInputStream stream, int[] match, boolean rewind) throws IOException { + long position = stream.getPosition(); + boolean result = true; + + for (int matchByte : match) { + int inputByte = stream.read(); + + if (inputByte == -1 || (matchByte != -1 && matchByte != inputByte)) { + result = false; + break; + } + } + + if (rewind) { + stream.seek(position); + } + + return result; } - String text = new String(bytes, 0, read, charset); - return pattern.matcher(text).find(); - } + /** + * Check if the next bytes in the stream match the specified regex pattern. + * + * @param stream Input stream to read the bytes from + * @param distance Maximum number of bytes to read for matching + * @param pattern Pattern to match against + * @param charset Charset to use to decode the bytes + * @return True if the next bytes in the stream are a match + * @throws IOException On read error + */ + public static boolean matchNextBytesAsRegex(SeekableInputStream stream, int distance, Pattern pattern, Charset charset) throws IOException { + long position = stream.getPosition(); + byte[] bytes = new byte[distance]; + + int read = new GreedyInputStream(stream).read(bytes); + stream.seek(position); + + if (read == -1) { + return false; + } + + String text = new String(bytes, 0, read, charset); + return pattern.matcher(text).find(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerDetectionResult.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerDetectionResult.java index 8de285dfb..03ec993c1 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerDetectionResult.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerDetectionResult.java @@ -7,106 +7,106 @@ * Result of audio container detection. */ public class MediaContainerDetectionResult { - private static final MediaContainerDetectionResult UNKNOWN_FORMAT = - new MediaContainerDetectionResult(null, null, null, null, null); - - private final AudioTrackInfo trackInfo; - private final MediaContainerProbe containerProbe; - private final String probeSettings; - private final AudioReference reference; - private final String unsupportedReason; - - private MediaContainerDetectionResult(AudioTrackInfo trackInfo, MediaContainerProbe containerProbe, - String probeSettings, AudioReference reference, String unsupportedReason) { - - this.trackInfo = trackInfo; - this.containerProbe = containerProbe; - this.probeSettings = probeSettings; - this.reference = reference; - this.unsupportedReason = unsupportedReason; - } - - /** - * Creates an unknown format result. - */ - public static MediaContainerDetectionResult unknownFormat() { - return UNKNOWN_FORMAT; - } - - /** - * Creates a result ofr an unsupported file of a known container. - * - * @param probe Probe of the container - * @param reason The reason why this track is not supported - */ - public static MediaContainerDetectionResult unsupportedFormat(MediaContainerProbe probe, String reason) { - return new MediaContainerDetectionResult(null, probe, null, null, reason); - } - - /** - * Creates a load result referring to another item. - * - * @param probe Probe of the container - * @param reference Reference to another item - */ - public static MediaContainerDetectionResult refer(MediaContainerProbe probe, AudioReference reference) { - return new MediaContainerDetectionResult(null, probe, null, reference, null); - } - - - /** - * Creates a load result for supported file. - * - * @param probe Probe of the container - * @param trackInfo Track info for the file - */ - public static MediaContainerDetectionResult supportedFormat(MediaContainerProbe probe, String settings, - AudioTrackInfo trackInfo) { - - return new MediaContainerDetectionResult(trackInfo, probe, settings, null, null); - } - - /** - * @return If the container this file uses was detected. In case this returns true, the container probe is non-null. - */ - public boolean isContainerDetected() { - return containerProbe != null; - } - - /** - * @return The probe for the container of the file - */ - public MediaContainerDescriptor getContainerDescriptor() { - return new MediaContainerDescriptor(containerProbe, probeSettings); - } - - /** - * @return Whether this specific file is supported. If this returns true, the track info is non-null. Otherwise - * the reason why this file is not supported can be retrieved via getUnsupportedReason(). - */ - public boolean isSupportedFile() { - return isContainerDetected() && unsupportedReason == null; - } - - /** - * @return The reason why this track is not supported. - */ - public String getUnsupportedReason() { - return unsupportedReason; - } - - /** - * @return Track info for the detected file. - */ - public AudioTrackInfo getTrackInfo() { - return trackInfo; - } - - public boolean isReference() { - return reference != null; - } - - public AudioReference getReference() { - return reference; - } + private static final MediaContainerDetectionResult UNKNOWN_FORMAT = + new MediaContainerDetectionResult(null, null, null, null, null); + + private final AudioTrackInfo trackInfo; + private final MediaContainerProbe containerProbe; + private final String probeSettings; + private final AudioReference reference; + private final String unsupportedReason; + + private MediaContainerDetectionResult(AudioTrackInfo trackInfo, MediaContainerProbe containerProbe, + String probeSettings, AudioReference reference, String unsupportedReason) { + + this.trackInfo = trackInfo; + this.containerProbe = containerProbe; + this.probeSettings = probeSettings; + this.reference = reference; + this.unsupportedReason = unsupportedReason; + } + + /** + * Creates an unknown format result. + */ + public static MediaContainerDetectionResult unknownFormat() { + return UNKNOWN_FORMAT; + } + + /** + * Creates a result ofr an unsupported file of a known container. + * + * @param probe Probe of the container + * @param reason The reason why this track is not supported + */ + public static MediaContainerDetectionResult unsupportedFormat(MediaContainerProbe probe, String reason) { + return new MediaContainerDetectionResult(null, probe, null, null, reason); + } + + /** + * Creates a load result referring to another item. + * + * @param probe Probe of the container + * @param reference Reference to another item + */ + public static MediaContainerDetectionResult refer(MediaContainerProbe probe, AudioReference reference) { + return new MediaContainerDetectionResult(null, probe, null, reference, null); + } + + + /** + * Creates a load result for supported file. + * + * @param probe Probe of the container + * @param trackInfo Track info for the file + */ + public static MediaContainerDetectionResult supportedFormat(MediaContainerProbe probe, String settings, + AudioTrackInfo trackInfo) { + + return new MediaContainerDetectionResult(trackInfo, probe, settings, null, null); + } + + /** + * @return If the container this file uses was detected. In case this returns true, the container probe is non-null. + */ + public boolean isContainerDetected() { + return containerProbe != null; + } + + /** + * @return The probe for the container of the file + */ + public MediaContainerDescriptor getContainerDescriptor() { + return new MediaContainerDescriptor(containerProbe, probeSettings); + } + + /** + * @return Whether this specific file is supported. If this returns true, the track info is non-null. Otherwise + * the reason why this file is not supported can be retrieved via getUnsupportedReason(). + */ + public boolean isSupportedFile() { + return isContainerDetected() && unsupportedReason == null; + } + + /** + * @return The reason why this track is not supported. + */ + public String getUnsupportedReason() { + return unsupportedReason; + } + + /** + * @return Track info for the detected file. + */ + public AudioTrackInfo getTrackInfo() { + return trackInfo; + } + + public boolean isReference() { + return reference != null; + } + + public AudioReference getReference() { + return reference; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerHints.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerHints.java index b629c70ff..490a4afe0 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerHints.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerHints.java @@ -4,39 +4,39 @@ * Optional meta-information about a stream which may narrow down the list of possible containers. */ public class MediaContainerHints { - private static final MediaContainerHints NO_INFORMATION = new MediaContainerHints(null, null); + private static final MediaContainerHints NO_INFORMATION = new MediaContainerHints(null, null); - /** - * Mime type, null if not known. - */ - public final String mimeType; - /** - * File extension, null if not known. - */ - public final String fileExtension; + /** + * Mime type, null if not known. + */ + public final String mimeType; + /** + * File extension, null if not known. + */ + public final String fileExtension; - private MediaContainerHints(String mimeType, String fileExtension) { - this.mimeType = mimeType; - this.fileExtension = fileExtension; - } + private MediaContainerHints(String mimeType, String fileExtension) { + this.mimeType = mimeType; + this.fileExtension = fileExtension; + } - /** - * @return true if any hint parameters have a value. - */ - public boolean present() { - return mimeType != null || fileExtension != null; - } + /** + * @return true if any hint parameters have a value. + */ + public boolean present() { + return mimeType != null || fileExtension != null; + } - /** - * @param mimeType Mime type - * @param fileExtension File extension - * @return Instance of hints object with the specified parameters - */ - public static MediaContainerHints from(String mimeType, String fileExtension) { - if (mimeType == null && fileExtension == null) { - return NO_INFORMATION; - } else { - return new MediaContainerHints(mimeType, fileExtension); + /** + * @param mimeType Mime type + * @param fileExtension File extension + * @return Instance of hints object with the specified parameters + */ + public static MediaContainerHints from(String mimeType, String fileExtension) { + if (mimeType == null && fileExtension == null) { + return NO_INFORMATION; + } else { + return new MediaContainerHints(mimeType, fileExtension); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerProbe.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerProbe.java index ffafce02e..eaf66059c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerProbe.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerProbe.java @@ -11,38 +11,38 @@ * Track information probe for one media container type and factory for tracks for that container. */ public interface MediaContainerProbe { - /** - * @return The name of this container - */ - String getName(); + /** + * @return The name of this container + */ + String getName(); - /** - * @param hints The available hints about the possible container. - * @return True if the hints match the format this probe detects. Should always return false if all hints are null. - */ - boolean matchesHints(MediaContainerHints hints); + /** + * @param hints The available hints about the possible container. + * @return True if the hints match the format this probe detects. Should always return false if all hints are null. + */ + boolean matchesHints(MediaContainerHints hints); - /** - * Detect whether the file readable from the input stream is using this container and if this specific file uses - * a format and codec that is supported for playback. - * - * @param reference Reference with an identifier to use in the returned audio track info - * @param inputStream Input stream that contains the track file - * @return Returns result with audio track on supported format, result with unsupported reason set if this is the - * container that the file uses, but this specific file uses a format or codec that is not supported. Returns - * null in case this file does not appear to be using this container format. - * @throws IOException On read error. - */ - MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException; + /** + * Detect whether the file readable from the input stream is using this container and if this specific file uses + * a format and codec that is supported for playback. + * + * @param reference Reference with an identifier to use in the returned audio track info + * @param inputStream Input stream that contains the track file + * @return Returns result with audio track on supported format, result with unsupported reason set if this is the + * container that the file uses, but this specific file uses a format or codec that is not supported. Returns + * null in case this file does not appear to be using this container format. + * @throws IOException On read error. + */ + MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException; - /** - * Creates a new track for this container. The audio tracks created here are never used directly, but the playback is - * delegated to them. As such, they do not have to support cloning or have a source manager. - * - * @param parameters Parameters specific to the probe. - * @param trackInfo Track meta information - * @param inputStream Input stream of the track file - * @return A new audio track - */ - AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream); + /** + * Creates a new track for this container. The audio tracks created here are never used directly, but the playback is + * delegated to them. As such, they do not have to support cloning or have a source manager. + * + * @param parameters Parameters specific to the probe. + * @param trackInfo Track meta information + * @param inputStream Input stream of the track file + * @return A new audio track + */ + AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerRegistry.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerRegistry.java index 832b4fb1e..30c001701 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerRegistry.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/MediaContainerRegistry.java @@ -3,35 +3,35 @@ import java.util.List; public class MediaContainerRegistry { - public static final MediaContainerRegistry DEFAULT_REGISTRY = new MediaContainerRegistry(MediaContainer.asList()); + public static final MediaContainerRegistry DEFAULT_REGISTRY = new MediaContainerRegistry(MediaContainer.asList()); - private final List probes; + private final List probes; - public MediaContainerRegistry(List probes) { - this.probes = probes; - } + public MediaContainerRegistry(List probes) { + this.probes = probes; + } + + public MediaContainerProbe find(String name) { + for (MediaContainerProbe probe : probes) { + if (name.equals(probe.getName())) { + return probe; + } + } - public MediaContainerProbe find(String name) { - for (MediaContainerProbe probe : probes) { - if (name.equals(probe.getName())) { - return probe; - } + return null; } - return null; - } + public List getAll() { + return probes; + } - public List getAll() { - return probes; - } + public static MediaContainerRegistry extended(MediaContainerProbe... additional) { + List probes = MediaContainer.asList(); - public static MediaContainerRegistry extended(MediaContainerProbe... additional) { - List probes = MediaContainer.asList(); + for (MediaContainerProbe probe : additional) { + probes.add(probe); + } - for (MediaContainerProbe probe : additional) { - probes.add(probe); + return new MediaContainerRegistry(probes); } - - return new MediaContainerRegistry(probes); - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsAudioTrack.java index fdaab578d..2f6a9624c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsAudioTrack.java @@ -12,30 +12,30 @@ * Audio track that handles an ADTS packet stream */ public class AdtsAudioTrack extends BaseAudioTrack { - private static final Logger log = LoggerFactory.getLogger(AdtsAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(AdtsAudioTrack.class); - private final InputStream inputStream; + private final InputStream inputStream; - /** - * @param trackInfo Track info - * @param inputStream Input stream for the ADTS stream - */ - public AdtsAudioTrack(AudioTrackInfo trackInfo, InputStream inputStream) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param inputStream Input stream for the ADTS stream + */ + public AdtsAudioTrack(AudioTrackInfo trackInfo, InputStream inputStream) { + super(trackInfo); - this.inputStream = inputStream; - } + this.inputStream = inputStream; + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - AdtsStreamProvider provider = new AdtsStreamProvider(inputStream, localExecutor.getProcessingContext()); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + AdtsStreamProvider provider = new AdtsStreamProvider(inputStream, localExecutor.getProcessingContext()); - try { - log.debug("Starting to play ADTS stream {}", getIdentifier()); + try { + log.debug("Starting to play ADTS stream {}", getIdentifier()); - localExecutor.executeProcessingLoop(provider::provideFrames, null); - } finally { - provider.close(); + localExecutor.executeProcessingLoop(provider::provideFrames, null); + } finally { + provider.close(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsContainerProbe.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsContainerProbe.java index 554695ca2..1d6d2933d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsContainerProbe.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsContainerProbe.java @@ -20,35 +20,35 @@ * Container detection probe for ADTS stream format. */ public class AdtsContainerProbe implements MediaContainerProbe { - private static final Logger log = LoggerFactory.getLogger(AdtsContainerProbe.class); - - @Override - public String getName() { - return "adts"; - } - - @Override - public boolean matchesHints(MediaContainerHints hints) { - boolean invalidMimeType = hints.mimeType != null && !"audio/aac".equalsIgnoreCase(hints.mimeType); - boolean invalidFileExtension = hints.fileExtension != null && !"aac".equalsIgnoreCase(hints.fileExtension); - return hints.present() && !invalidMimeType && !invalidFileExtension; - } - - @Override - public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { - AdtsStreamReader reader = new AdtsStreamReader(inputStream); - - if (reader.findPacketHeader(MediaContainerDetection.STREAM_SCAN_DISTANCE) == null) { - return null; + private static final Logger log = LoggerFactory.getLogger(AdtsContainerProbe.class); + + @Override + public String getName() { + return "adts"; + } + + @Override + public boolean matchesHints(MediaContainerHints hints) { + boolean invalidMimeType = hints.mimeType != null && !"audio/aac".equalsIgnoreCase(hints.mimeType); + boolean invalidFileExtension = hints.fileExtension != null && !"aac".equalsIgnoreCase(hints.fileExtension); + return hints.present() && !invalidMimeType && !invalidFileExtension; } - log.debug("Track {} is an ADTS stream.", reference.identifier); + @Override + public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { + AdtsStreamReader reader = new AdtsStreamReader(inputStream); + + if (reader.findPacketHeader(MediaContainerDetection.STREAM_SCAN_DISTANCE) == null) { + return null; + } - return supportedFormat(this, null, AudioTrackInfoBuilder.create(reference, inputStream).build()); - } + log.debug("Track {} is an ADTS stream.", reference.identifier); - @Override - public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - return new AdtsAudioTrack(trackInfo, inputStream); - } + return supportedFormat(this, null, AudioTrackInfoBuilder.create(reference, inputStream).build()); + } + + @Override + public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + return new AdtsAudioTrack(trackInfo, inputStream); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsPacketHeader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsPacketHeader.java index df6004724..c32e6c956 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsPacketHeader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsPacketHeader.java @@ -27,10 +27,10 @@ public class AdtsPacketHeader { /** * @param isProtectionAbsent If this is false, then the packet header is followed by a 2-byte CRC. - * @param profile Decoder profile (2 is AAC-LC). - * @param sampleRate Sample rate. - * @param channels Number of channels. - * @param payloadLength Packet payload length, excluding the CRC after this header. + * @param profile Decoder profile (2 is AAC-LC). + * @param sampleRate Sample rate. + * @param channels Number of channels. + * @param payloadLength Packet payload length, excluding the CRC after this header. */ public AdtsPacketHeader(boolean isProtectionAbsent, int profile, int sampleRate, int channels, int payloadLength) { this.isProtectionAbsent = isProtectionAbsent; @@ -46,8 +46,8 @@ public AdtsPacketHeader(boolean isProtectionAbsent, int profile, int sampleRate, */ public boolean canUseSameDecoder(AdtsPacketHeader packetHeader) { return packetHeader != null && - profile == packetHeader.profile && - sampleRate == packetHeader.sampleRate && - channels == packetHeader.channels; + profile == packetHeader.profile && + sampleRate == packetHeader.sampleRate && + channels == packetHeader.channels; } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsStreamProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsStreamProvider.java index 74d42873d..96854ec5a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsStreamProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsStreamProvider.java @@ -18,126 +18,127 @@ * Provides the frames of an ADTS stream track to the frame consumer. */ public class AdtsStreamProvider { - private final AudioProcessingContext context; - private final AdtsStreamReader streamReader; - private final AacDecoder decoder; - private final ResettableBoundedInputStream packetBoundedStream; - private final DirectBufferStreamBroker directBufferBroker; - private ShortBuffer outputBuffer; - private AdtsPacketHeader previousHeader; - private AudioPipeline downstream; - private Long requestedTimecode; - private Long providedTimecode; - - /** - * @param inputStream Input stream to read from. - * @param context Configuration and output information for processing - */ - public AdtsStreamProvider(InputStream inputStream, AudioProcessingContext context) { - this.context = context; - this.streamReader = new AdtsStreamReader(inputStream); - this.decoder = new AacDecoder(); - this.packetBoundedStream = new ResettableBoundedInputStream(inputStream); - this.directBufferBroker = new DirectBufferStreamBroker(2048); - } - - /** - * Used to pass the initial position of the stream in case it is part of a chain, to keep timecodes of audio frames - * continuous. - * - * @param requestedTimecode The timecode at which the samples from this stream should be outputted. - * @param providedTimecode The timecode at which this stream starts. - */ - public void setInitialSeek(long requestedTimecode, long providedTimecode) { - this.requestedTimecode = requestedTimecode; - this.providedTimecode = providedTimecode; - } - - /** - * Provides frames to the frame consumer. - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - public void provideFrames() throws InterruptedException { - try { - while (true) { - AdtsPacketHeader header = streamReader.findPacketHeader(); - if (header == null) { - // Reached EOF while scanning for header - return; - } - - configureProcessing(header); - - packetBoundedStream.resetLimit(header.payloadLength); - directBufferBroker.consumeNext(packetBoundedStream, Integer.MAX_VALUE, Integer.MAX_VALUE); + private final AudioProcessingContext context; + private final AdtsStreamReader streamReader; + private final AacDecoder decoder; + private final ResettableBoundedInputStream packetBoundedStream; + private final DirectBufferStreamBroker directBufferBroker; + private ShortBuffer outputBuffer; + private AdtsPacketHeader previousHeader; + private AudioPipeline downstream; + private Long requestedTimecode; + private Long providedTimecode; + + /** + * @param inputStream Input stream to read from. + * @param context Configuration and output information for processing + */ + public AdtsStreamProvider(InputStream inputStream, AudioProcessingContext context) { + this.context = context; + this.streamReader = new AdtsStreamReader(inputStream); + this.decoder = new AacDecoder(); + this.packetBoundedStream = new ResettableBoundedInputStream(inputStream); + this.directBufferBroker = new DirectBufferStreamBroker(2048); + } - ByteBuffer buffer = directBufferBroker.getBuffer(); + /** + * Used to pass the initial position of the stream in case it is part of a chain, to keep timecodes of audio frames + * continuous. + * + * @param requestedTimecode The timecode at which the samples from this stream should be outputted. + * @param providedTimecode The timecode at which this stream starts. + */ + public void setInitialSeek(long requestedTimecode, long providedTimecode) { + this.requestedTimecode = requestedTimecode; + this.providedTimecode = providedTimecode; + } - if (buffer.limit() < header.payloadLength) { - // Reached EOF in the middle of a packet - return; + /** + * Provides frames to the frame consumer. + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + public void provideFrames() throws InterruptedException { + try { + while (true) { + AdtsPacketHeader header = streamReader.findPacketHeader(); + if (header == null) { + // Reached EOF while scanning for header + return; + } + + configureProcessing(header); + + packetBoundedStream.resetLimit(header.payloadLength); + directBufferBroker.consumeNext(packetBoundedStream, Integer.MAX_VALUE, Integer.MAX_VALUE); + + ByteBuffer buffer = directBufferBroker.getBuffer(); + + if (buffer.limit() < header.payloadLength) { + // Reached EOF in the middle of a packet + return; + } + + decodeAndSend(buffer); + streamReader.nextPacket(); + } + } catch (IOException e) { + throw new RuntimeException(e); } - - decodeAndSend(buffer); - streamReader.nextPacket(); - } - } catch (IOException e) { - throw new RuntimeException(e); } - } - private void decodeAndSend(ByteBuffer inputBuffer) throws InterruptedException { - decoder.fill(inputBuffer); + private void decodeAndSend(ByteBuffer inputBuffer) throws InterruptedException { + decoder.fill(inputBuffer); - if (downstream == null) { - AacDecoder.StreamInfo streamInfo = decoder.resolveStreamInfo(); - if (streamInfo == null) { - return; - } + if (downstream == null) { + AacDecoder.StreamInfo streamInfo = decoder.resolveStreamInfo(); + if (streamInfo == null) { + return; + } - downstream = AudioPipelineFactory.create(context, new PcmFormat(streamInfo.channels, streamInfo.sampleRate)); - outputBuffer = ByteBuffer.allocateDirect(2 * streamInfo.frameSize * streamInfo.channels) - .order(ByteOrder.nativeOrder()).asShortBuffer(); + downstream = AudioPipelineFactory.create(context, new PcmFormat(streamInfo.channels, streamInfo.sampleRate)); + outputBuffer = ByteBuffer.allocateDirect(2 * streamInfo.frameSize * streamInfo.channels) + .order(ByteOrder.nativeOrder()).asShortBuffer(); - if (requestedTimecode != null) { - downstream.seekPerformed(requestedTimecode, providedTimecode); - requestedTimecode = null; - } - } + if (requestedTimecode != null) { + downstream.seekPerformed(requestedTimecode, providedTimecode); + requestedTimecode = null; + } + } - outputBuffer.clear(); + outputBuffer.clear(); - while (decoder.decode(outputBuffer, false)) { - downstream.process(outputBuffer); - outputBuffer.clear(); + while (decoder.decode(outputBuffer, false)) { + downstream.process(outputBuffer); + outputBuffer.clear(); + } } - } - private void configureProcessing(AdtsPacketHeader header) { - if (!header.canUseSameDecoder(previousHeader)) { - decoder.configure(header.profile, header.sampleRate, header.channels); + private void configureProcessing(AdtsPacketHeader header) { + if (!header.canUseSameDecoder(previousHeader)) { + decoder.configure(header.profile, header.sampleRate, header.channels); - if (downstream != null) { - downstream.close(); - } + if (downstream != null) { + downstream.close(); + } - downstream = null; - outputBuffer = null; + downstream = null; + outputBuffer = null; + } + + previousHeader = header; } - previousHeader = header; - } - - /** - * Free all resources. - */ - public void close() { - try { - if (downstream != null) { - downstream.close(); - } - } finally { - decoder.close(); + /** + * Free all resources. + */ + public void close() { + try { + if (downstream != null) { + downstream.close(); + } + } finally { + decoder.close(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsStreamReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsStreamReader.java index ad84d2b55..4615cc264 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsStreamReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/adts/AdtsStreamReader.java @@ -10,156 +10,156 @@ * Finds and reads ADTS packet headers from an input stream. */ public class AdtsStreamReader { - private static final AdtsPacketHeader EOF_PACKET = new AdtsPacketHeader(false, 0, 0, 0, 0); - - private static final int HEADER_BASE_SIZE = 7; - private static final int INVALID_VALUE = -1; - - private static final int[] sampleRateMapping = new int[] { - 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, - 16000, 12000, 11025, 8000, 7350, INVALID_VALUE, INVALID_VALUE, INVALID_VALUE - }; - - private final InputStream inputStream; - private final byte[] scanBuffer; - private final ByteBuffer scanByteBuffer; - private final BitBufferReader scanBufferReader; - private AdtsPacketHeader currentPacket; - - /** - * @param inputStream The input stream to use. - */ - public AdtsStreamReader(InputStream inputStream) { - this.inputStream = inputStream; - this.scanBuffer = new byte[32]; - this.scanByteBuffer = ByteBuffer.wrap(scanBuffer); - this.scanBufferReader = new BitBufferReader(scanByteBuffer); - } - - /** - * Scan the input stream for an ADTS packet header. Subsequent calls will return the same header until nextPacket() is - * called. - * - * @return The packet header if found before EOF. - * @throws IOException On read error. - */ - public AdtsPacketHeader findPacketHeader() throws IOException { - return findPacketHeader(Integer.MAX_VALUE); - } - - /** - * Scan the input stream for an ADTS packet header. Subsequent calls will return the same header until nextPacket() is - * called. - * - * @param maximumDistance Maximum number of bytes to scan. - * @return The packet header if found before EOF and maximum byte limit. - * @throws IOException On read error. - */ - public AdtsPacketHeader findPacketHeader(int maximumDistance) throws IOException { - if (currentPacket == null) { - currentPacket = scanForPacketHeader(maximumDistance); + private static final AdtsPacketHeader EOF_PACKET = new AdtsPacketHeader(false, 0, 0, 0, 0); + + private static final int HEADER_BASE_SIZE = 7; + private static final int INVALID_VALUE = -1; + + private static final int[] sampleRateMapping = new int[]{ + 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, + 16000, 12000, 11025, 8000, 7350, INVALID_VALUE, INVALID_VALUE, INVALID_VALUE + }; + + private final InputStream inputStream; + private final byte[] scanBuffer; + private final ByteBuffer scanByteBuffer; + private final BitBufferReader scanBufferReader; + private AdtsPacketHeader currentPacket; + + /** + * @param inputStream The input stream to use. + */ + public AdtsStreamReader(InputStream inputStream) { + this.inputStream = inputStream; + this.scanBuffer = new byte[32]; + this.scanByteBuffer = ByteBuffer.wrap(scanBuffer); + this.scanBufferReader = new BitBufferReader(scanByteBuffer); } - return currentPacket == EOF_PACKET ? null : currentPacket; - } + /** + * Scan the input stream for an ADTS packet header. Subsequent calls will return the same header until nextPacket() is + * called. + * + * @return The packet header if found before EOF. + * @throws IOException On read error. + */ + public AdtsPacketHeader findPacketHeader() throws IOException { + return findPacketHeader(Integer.MAX_VALUE); + } - /** - * Resets the current packet, makes next calls to findPacketHeader scan for the next occurrence in the stream. - */ - public void nextPacket() { - currentPacket = null; - } + /** + * Scan the input stream for an ADTS packet header. Subsequent calls will return the same header until nextPacket() is + * called. + * + * @param maximumDistance Maximum number of bytes to scan. + * @return The packet header if found before EOF and maximum byte limit. + * @throws IOException On read error. + */ + public AdtsPacketHeader findPacketHeader(int maximumDistance) throws IOException { + if (currentPacket == null) { + currentPacket = scanForPacketHeader(maximumDistance); + } - private AdtsPacketHeader scanForPacketHeader(int maximumDistance) throws IOException { - int bufferPosition = 0; + return currentPacket == EOF_PACKET ? null : currentPacket; + } + + /** + * Resets the current packet, makes next calls to findPacketHeader scan for the next occurrence in the stream. + */ + public void nextPacket() { + currentPacket = null; + } - for (int i = 0; i < maximumDistance; i++) { - int nextByte = inputStream.read(); + private AdtsPacketHeader scanForPacketHeader(int maximumDistance) throws IOException { + int bufferPosition = 0; - if (nextByte == -1) { - return EOF_PACKET; - } + for (int i = 0; i < maximumDistance; i++) { + int nextByte = inputStream.read(); - scanBuffer[bufferPosition++] = (byte) nextByte; + if (nextByte == -1) { + return EOF_PACKET; + } - if (bufferPosition >= HEADER_BASE_SIZE) { - AdtsPacketHeader header = readHeaderFromBufferTail(bufferPosition); + scanBuffer[bufferPosition++] = (byte) nextByte; - if (header != null) { - return header; + if (bufferPosition >= HEADER_BASE_SIZE) { + AdtsPacketHeader header = readHeaderFromBufferTail(bufferPosition); + + if (header != null) { + return header; + } + } + + if (bufferPosition == scanBuffer.length) { + copyEndToBeginning(scanBuffer, HEADER_BASE_SIZE); + bufferPosition = HEADER_BASE_SIZE; + } } - } - if (bufferPosition == scanBuffer.length) { - copyEndToBeginning(scanBuffer, HEADER_BASE_SIZE); - bufferPosition = HEADER_BASE_SIZE; - } + return null; } - return null; - } + private AdtsPacketHeader readHeaderFromBufferTail(int position) throws IOException { + scanByteBuffer.position(position - HEADER_BASE_SIZE); - private AdtsPacketHeader readHeaderFromBufferTail(int position) throws IOException { - scanByteBuffer.position(position - HEADER_BASE_SIZE); + AdtsPacketHeader header = readHeader(scanBufferReader); + scanBufferReader.readRemainingBits(); - AdtsPacketHeader header = readHeader(scanBufferReader); - scanBufferReader.readRemainingBits(); + if (header == null) { + return null; + } else if (!header.isProtectionAbsent) { + int crcFirst = inputStream.read(); + int crcSecond = inputStream.read(); - if (header == null) { - return null; - } else if (!header.isProtectionAbsent) { - int crcFirst = inputStream.read(); - int crcSecond = inputStream.read(); + if (crcFirst == -1 || crcSecond == -1) { + return EOF_PACKET; + } + } - if (crcFirst == -1 || crcSecond == -1) { - return EOF_PACKET; - } + return header; } - return header; - } - - private static void copyEndToBeginning(byte[] buffer, int chunk) { - for (int i = 0; i < chunk; i++) { - buffer[i] = buffer[buffer.length - chunk + i]; - } - } - - private static AdtsPacketHeader readHeader(BitBufferReader reader) { - if ((reader.asLong(15) & 0x7FFB) != 0x7FF8) { - // Possible reasons: - // 1) Syncword is not present, cannot be an ADTS header - // 2) Layer value is not 0, which must always be 0 for ADTS - return null; + private static void copyEndToBeginning(byte[] buffer, int chunk) { + for (int i = 0; i < chunk; i++) { + buffer[i] = buffer[buffer.length - chunk + i]; + } } - boolean isProtectionAbsent = reader.asLong(1) == 1; - int profile = reader.asInteger(2); - int sampleRate = sampleRateMapping[reader.asInteger(4)]; + private static AdtsPacketHeader readHeader(BitBufferReader reader) { + if ((reader.asLong(15) & 0x7FFB) != 0x7FF8) { + // Possible reasons: + // 1) Syncword is not present, cannot be an ADTS header + // 2) Layer value is not 0, which must always be 0 for ADTS + return null; + } - // Private bit - reader.asLong(1); + boolean isProtectionAbsent = reader.asLong(1) == 1; + int profile = reader.asInteger(2); + int sampleRate = sampleRateMapping[reader.asInteger(4)]; - int channels = reader.asInteger(3); + // Private bit + reader.asLong(1); - if (sampleRate == INVALID_VALUE || channels == 0) { - return null; - } + int channels = reader.asInteger(3); - // 4 boring bits - reader.asLong(4); + if (sampleRate == INVALID_VALUE || channels == 0) { + return null; + } - int frameLength = reader.asInteger(13); - int payloadLength = frameLength - 7 - (isProtectionAbsent ? 0 : 2); + // 4 boring bits + reader.asLong(4); - // More boring bits - reader.asLong(11); + int frameLength = reader.asInteger(13); + int payloadLength = frameLength - 7 - (isProtectionAbsent ? 0 : 2); - if (reader.asLong(2) != 0) { - // Not handling multiple frames per packet - return null; - } + // More boring bits + reader.asLong(11); + + if (reader.asLong(2) != 0) { + // Not handling multiple frames per packet + return null; + } - return new AdtsPacketHeader(isProtectionAbsent, profile + 1, sampleRate, channels, payloadLength); - } + return new AdtsPacketHeader(isProtectionAbsent, profile + 1, sampleRate, channels, payloadLength); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/common/AacPacketRouter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/common/AacPacketRouter.java index a608bdff0..2004655d3 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/common/AacPacketRouter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/common/AacPacketRouter.java @@ -14,102 +14,102 @@ import java.nio.ShortBuffer; public class AacPacketRouter { - private static final Logger log = LoggerFactory.getLogger(AacPacketRouter.class); + private static final Logger log = LoggerFactory.getLogger(AacPacketRouter.class); - private final AudioProcessingContext context; + private final AudioProcessingContext context; - private Long initialRequestedTimecode; - private Long initialProvidedTimecode; - private AudioPipeline downstream; - private ShortBuffer outputBuffer; + private Long initialRequestedTimecode; + private Long initialProvidedTimecode; + private AudioPipeline downstream; + private ShortBuffer outputBuffer; - public AacDecoder nativeDecoder; - public Decoder embeddedDecoder; + public AacDecoder nativeDecoder; + public Decoder embeddedDecoder; - public AacPacketRouter(AudioProcessingContext context) { - this.context = context; - } - - public void processInput(ByteBuffer inputBuffer) throws InterruptedException { - if (embeddedDecoder == null) { - nativeDecoder.fill(inputBuffer); - - if (downstream == null) { - log.debug("Using native decoder"); - AacDecoder.StreamInfo streamInfo = nativeDecoder.resolveStreamInfo(); - - if (streamInfo != null) { - downstream = AudioPipelineFactory.create(context, new PcmFormat(streamInfo.channels, streamInfo.sampleRate)); - outputBuffer = ByteBuffer.allocateDirect(2 * streamInfo.frameSize * streamInfo.channels) - .order(ByteOrder.nativeOrder()).asShortBuffer(); + public AacPacketRouter(AudioProcessingContext context) { + this.context = context; + } - if (initialRequestedTimecode != null) { - downstream.seekPerformed(initialRequestedTimecode, initialProvidedTimecode); - } + public void processInput(ByteBuffer inputBuffer) throws InterruptedException { + if (embeddedDecoder == null) { + nativeDecoder.fill(inputBuffer); + + if (downstream == null) { + log.debug("Using native decoder"); + AacDecoder.StreamInfo streamInfo = nativeDecoder.resolveStreamInfo(); + + if (streamInfo != null) { + downstream = AudioPipelineFactory.create(context, new PcmFormat(streamInfo.channels, streamInfo.sampleRate)); + outputBuffer = ByteBuffer.allocateDirect(2 * streamInfo.frameSize * streamInfo.channels) + .order(ByteOrder.nativeOrder()).asShortBuffer(); + + if (initialRequestedTimecode != null) { + downstream.seekPerformed(initialRequestedTimecode, initialProvidedTimecode); + } + } + } + + if (downstream != null) { + while (nativeDecoder.decode(outputBuffer, false)) { + downstream.process(outputBuffer); + outputBuffer.clear(); + } + } + } else { + if (downstream == null) { + log.debug("Using embedded decoder"); + downstream = AudioPipelineFactory.create(context, new PcmFormat( + embeddedDecoder.getAudioFormat().getChannels(), + (int) embeddedDecoder.getAudioFormat().getSampleRate() + )); + + if (initialRequestedTimecode != null) { + downstream.seekPerformed(initialRequestedTimecode, initialProvidedTimecode); + } + } + + if (downstream != null) { + downstream.process(embeddedDecoder.decodeFrame(inputBuffer.array())); + } } - } + } - if (downstream != null) { - while (nativeDecoder.decode(outputBuffer, false)) { - downstream.process(outputBuffer); - outputBuffer.clear(); - } - } - } else { - if (downstream == null) { - log.debug("Using embedded decoder"); - downstream = AudioPipelineFactory.create(context, new PcmFormat( - embeddedDecoder.getAudioFormat().getChannels(), - (int) embeddedDecoder.getAudioFormat().getSampleRate() - )); - - if (initialRequestedTimecode != null) { - downstream.seekPerformed(initialRequestedTimecode, initialProvidedTimecode); + public void seekPerformed(long requestedTimecode, long providedTimecode) { + if (downstream != null) { + downstream.seekPerformed(requestedTimecode, providedTimecode); + } else { + this.initialRequestedTimecode = requestedTimecode; + this.initialProvidedTimecode = providedTimecode; } - } - if (downstream != null) { - downstream.process(embeddedDecoder.decodeFrame(inputBuffer.array())); - } - } - } - - public void seekPerformed(long requestedTimecode, long providedTimecode) { - if (downstream != null) { - downstream.seekPerformed(requestedTimecode, providedTimecode); - } else { - this.initialRequestedTimecode = requestedTimecode; - this.initialProvidedTimecode = providedTimecode; + if (nativeDecoder != null) { + nativeDecoder.close(); + nativeDecoder = null; + } else if (embeddedDecoder != null) { + embeddedDecoder = null; + } } - if (nativeDecoder != null) { - nativeDecoder.close(); - nativeDecoder = null; - } else if (embeddedDecoder != null) { - embeddedDecoder = null; - } - } - - public void flush() throws InterruptedException { - if (downstream != null) { - while (nativeDecoder.decode(outputBuffer, true)) { - downstream.process(outputBuffer); - outputBuffer.clear(); - } + public void flush() throws InterruptedException { + if (downstream != null) { + while (nativeDecoder.decode(outputBuffer, true)) { + downstream.process(outputBuffer); + outputBuffer.clear(); + } + } } - } - - public void close() { - try { - if (downstream != null) { - downstream.close(); - } - } finally { - if (nativeDecoder != null) { - nativeDecoder.close(); - } else if (embeddedDecoder != null) { - embeddedDecoder = null; - } + + public void close() { + try { + if (downstream != null) { + downstream.close(); + } + } finally { + if (nativeDecoder != null) { + nativeDecoder.close(); + } else if (embeddedDecoder != null) { + embeddedDecoder = null; + } + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/common/OpusPacketRouter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/common/OpusPacketRouter.java index d1ace94a8..19ae96646 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/common/OpusPacketRouter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/common/OpusPacketRouter.java @@ -22,200 +22,202 @@ * pipeline of the output if necessary. */ public class OpusPacketRouter { - private static final Logger log = LoggerFactory.getLogger(OpusPacketRouter.class); - - private final AudioProcessingContext context; - private final int inputFrequency; - private final int inputChannels; - private final byte[] headerBytes; - private final MutableAudioFrame offeredFrame; - - private long currentFrameDuration; - private long currentTimecode; - private long requestedTimecode; - private OpusDecoder opusDecoder; - private AudioPipeline downstream; - private ByteBuffer directInput; - private ShortBuffer frameBuffer; - private AudioDataFormat inputFormat; - private int lastFrameSize; - - /** - * @param context Configuration and output information for processing - * @param inputFrequency Sample rate of the opus track - * @param inputChannels Number of channels in the opus track - */ - public OpusPacketRouter(AudioProcessingContext context, int inputFrequency, int inputChannels) { - this.context = context; - this.inputFrequency = inputFrequency; - this.inputChannels = inputChannels; - this.headerBytes = new byte[2]; - this.offeredFrame = new MutableAudioFrame(); - this.lastFrameSize = 0; - - offeredFrame.setVolume(100); - offeredFrame.setFormat(context.outputFormat); - } - - /** - * Notify downstream handlers about a seek. - * - * @param requestedTimecode Timecode in milliseconds to which the seek was requested to - * @param providedTimecode Timecode in milliseconds to which the seek was actually performed to - */ - public void seekPerformed(long requestedTimecode, long providedTimecode) { - this.requestedTimecode = requestedTimecode; - currentTimecode = providedTimecode; - - if (downstream != null) { - downstream.seekPerformed(requestedTimecode, providedTimecode); + private static final Logger log = LoggerFactory.getLogger(OpusPacketRouter.class); + + private final AudioProcessingContext context; + private final int inputFrequency; + private final int inputChannels; + private final byte[] headerBytes; + private final MutableAudioFrame offeredFrame; + + private long currentFrameDuration; + private long currentTimecode; + private long requestedTimecode; + private OpusDecoder opusDecoder; + private AudioPipeline downstream; + private ByteBuffer directInput; + private ShortBuffer frameBuffer; + private AudioDataFormat inputFormat; + private int lastFrameSize; + + /** + * @param context Configuration and output information for processing + * @param inputFrequency Sample rate of the opus track + * @param inputChannels Number of channels in the opus track + */ + public OpusPacketRouter(AudioProcessingContext context, int inputFrequency, int inputChannels) { + this.context = context; + this.inputFrequency = inputFrequency; + this.inputChannels = inputChannels; + this.headerBytes = new byte[2]; + this.offeredFrame = new MutableAudioFrame(); + this.lastFrameSize = 0; + + offeredFrame.setVolume(100); + offeredFrame.setFormat(context.outputFormat); } - } - - /** - * Indicates that no more input is coming. Flush any buffers to output. - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - public void flush() throws InterruptedException { - if (downstream != null) { - downstream.flush(); + + /** + * Notify downstream handlers about a seek. + * + * @param requestedTimecode Timecode in milliseconds to which the seek was requested to + * @param providedTimecode Timecode in milliseconds to which the seek was actually performed to + */ + public void seekPerformed(long requestedTimecode, long providedTimecode) { + this.requestedTimecode = requestedTimecode; + currentTimecode = providedTimecode; + + if (downstream != null) { + downstream.seekPerformed(requestedTimecode, providedTimecode); + } } - } - - /** - * Process one opus packet. - * @param buffer Byte buffer of the packet - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - public void process(ByteBuffer buffer) throws InterruptedException { - int frameSize = processFrameSize(buffer); - - if (frameSize != 0) { - checkDecoderNecessity(); - - if (opusDecoder != null) { - passDownstream(buffer, frameSize); - } else { - passThrough(buffer); - } + + /** + * Indicates that no more input is coming. Flush any buffers to output. + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + public void flush() throws InterruptedException { + if (downstream != null) { + downstream.flush(); + } } - } - - /** - * Free all resources. - */ - public void close() { - destroyDecoder(); - } - - private int processFrameSize(ByteBuffer buffer) { - int frameSize; - - if (buffer.isDirect()) { - buffer.mark(); - buffer.get(headerBytes); - buffer.reset(); - - frameSize = OpusDecoder.getPacketFrameSize(inputFrequency, headerBytes, 0, headerBytes.length); - } else { - frameSize = OpusDecoder.getPacketFrameSize(inputFrequency, buffer.array(), buffer.position(), buffer.remaining()); + + /** + * Process one opus packet. + * + * @param buffer Byte buffer of the packet + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + public void process(ByteBuffer buffer) throws InterruptedException { + int frameSize = processFrameSize(buffer); + + if (frameSize != 0) { + checkDecoderNecessity(); + + if (opusDecoder != null) { + passDownstream(buffer, frameSize); + } else { + passThrough(buffer); + } + } } - if (frameSize == 0) { - return 0; - } else if (frameSize != lastFrameSize) { - lastFrameSize = frameSize; - inputFormat = new OpusAudioDataFormat(inputChannels, inputFrequency, frameSize); + /** + * Free all resources. + */ + public void close() { + destroyDecoder(); } - currentFrameDuration = frameSize * 1000 / inputFrequency; - currentTimecode += currentFrameDuration; - return frameSize; - } + private int processFrameSize(ByteBuffer buffer) { + int frameSize; + + if (buffer.isDirect()) { + buffer.mark(); + buffer.get(headerBytes); + buffer.reset(); + + frameSize = OpusDecoder.getPacketFrameSize(inputFrequency, headerBytes, 0, headerBytes.length); + } else { + frameSize = OpusDecoder.getPacketFrameSize(inputFrequency, buffer.array(), buffer.position(), buffer.remaining()); + } + + if (frameSize == 0) { + return 0; + } else if (frameSize != lastFrameSize) { + lastFrameSize = frameSize; + inputFormat = new OpusAudioDataFormat(inputChannels, inputFrequency, frameSize); + } + + currentFrameDuration = frameSize * 1000 / inputFrequency; + currentTimecode += currentFrameDuration; + return frameSize; + } - private void passDownstream(ByteBuffer buffer, int frameSize) throws InterruptedException { - ByteBuffer nativeBuffer; + private void passDownstream(ByteBuffer buffer, int frameSize) throws InterruptedException { + ByteBuffer nativeBuffer; - if (!buffer.isDirect()) { - if (directInput == null || directInput.capacity() < buffer.remaining()) { - directInput = ByteBuffer.allocateDirect(buffer.remaining() + 200); - } + if (!buffer.isDirect()) { + if (directInput == null || directInput.capacity() < buffer.remaining()) { + directInput = ByteBuffer.allocateDirect(buffer.remaining() + 200); + } - directInput.clear(); - directInput.put(buffer); - directInput.flip(); + directInput.clear(); + directInput.put(buffer); + directInput.flip(); - nativeBuffer = directInput; - } else { - nativeBuffer = buffer; - } + nativeBuffer = directInput; + } else { + nativeBuffer = buffer; + } - if (frameBuffer == null || frameBuffer.capacity() < frameSize * inputChannels) { - frameBuffer = ByteBuffer.allocateDirect(frameSize * inputChannels * 2).order(ByteOrder.nativeOrder()).asShortBuffer(); - } + if (frameBuffer == null || frameBuffer.capacity() < frameSize * inputChannels) { + frameBuffer = ByteBuffer.allocateDirect(frameSize * inputChannels * 2).order(ByteOrder.nativeOrder()).asShortBuffer(); + } - frameBuffer.clear(); - frameBuffer.limit(frameSize); + frameBuffer.clear(); + frameBuffer.limit(frameSize); - opusDecoder.decode(nativeBuffer, frameBuffer); - downstream.process(frameBuffer); - } + opusDecoder.decode(nativeBuffer, frameBuffer); + downstream.process(frameBuffer); + } - private void passThrough(ByteBuffer buffer) throws InterruptedException { - if (requestedTimecode < currentTimecode) { - offeredFrame.setTimecode(currentTimecode); - offeredFrame.setBuffer(buffer); + private void passThrough(ByteBuffer buffer) throws InterruptedException { + if (requestedTimecode < currentTimecode) { + offeredFrame.setTimecode(currentTimecode); + offeredFrame.setBuffer(buffer); - context.frameBuffer.consume(offeredFrame); + context.frameBuffer.consume(offeredFrame); + } } - } - private void checkDecoderNecessity() { - if (AudioPipelineFactory.isProcessingRequired(context, inputFormat)) { - if (opusDecoder == null) { - log.debug("Enabling reencode mode on opus track."); + private void checkDecoderNecessity() { + if (AudioPipelineFactory.isProcessingRequired(context, inputFormat)) { + if (opusDecoder == null) { + log.debug("Enabling reencode mode on opus track."); - initialiseDecoder(); + initialiseDecoder(); - AudioFrameVolumeChanger.apply(context); - } - } else { - if (opusDecoder != null) { - log.debug("Enabling passthrough mode on opus track."); + AudioFrameVolumeChanger.apply(context); + } + } else { + if (opusDecoder != null) { + log.debug("Enabling passthrough mode on opus track."); - destroyDecoder(); + destroyDecoder(); - AudioFrameVolumeChanger.apply(context); - } + AudioFrameVolumeChanger.apply(context); + } + } } - } - private void initialiseDecoder() { - opusDecoder = new OpusDecoder(inputFrequency, inputChannels); - - try { - downstream = AudioPipelineFactory.create(context, new PcmFormat(inputChannels, inputFrequency)); - downstream.seekPerformed(Math.max(currentTimecode, requestedTimecode), currentTimecode); - } finally { - // When an exception is thrown, do not leave the router in a limbo state with decoder but no downstream. - if (downstream == null) { - destroyDecoder(); - } + private void initialiseDecoder() { + opusDecoder = new OpusDecoder(inputFrequency, inputChannels); + + try { + downstream = AudioPipelineFactory.create(context, new PcmFormat(inputChannels, inputFrequency)); + downstream.seekPerformed(Math.max(currentTimecode, requestedTimecode), currentTimecode); + } finally { + // When an exception is thrown, do not leave the router in a limbo state with decoder but no downstream. + if (downstream == null) { + destroyDecoder(); + } + } } - } - private void destroyDecoder() { - if (opusDecoder != null) { - opusDecoder.close(); - opusDecoder = null; - } + private void destroyDecoder() { + if (opusDecoder != null) { + opusDecoder.close(); + opusDecoder = null; + } - if (downstream != null) { - downstream.close(); - downstream = null; - } + if (downstream != null) { + downstream.close(); + downstream = null; + } - directInput = null; - frameBuffer = null; - } + directInput = null; + frameBuffer = null; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacAudioTrack.java index c480d67c1..032a00367 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacAudioTrack.java @@ -11,30 +11,30 @@ * Audio track that handles a FLAC stream */ public class FlacAudioTrack extends BaseAudioTrack { - private static final Logger log = LoggerFactory.getLogger(FlacAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(FlacAudioTrack.class); - private final SeekableInputStream inputStream; + private final SeekableInputStream inputStream; - /** - * @param trackInfo Track info - * @param inputStream Input stream for the FLAC file - */ - public FlacAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param inputStream Input stream for the FLAC file + */ + public FlacAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + super(trackInfo); - this.inputStream = inputStream; - } + this.inputStream = inputStream; + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - FlacFileLoader file = new FlacFileLoader(inputStream); - FlacTrackProvider trackProvider = file.loadTrack(localExecutor.getProcessingContext()); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + FlacFileLoader file = new FlacFileLoader(inputStream); + FlacTrackProvider trackProvider = file.loadTrack(localExecutor.getProcessingContext()); - try { - log.debug("Starting to play FLAC track {}", getIdentifier()); - localExecutor.executeProcessingLoop(trackProvider::provideFrames, trackProvider::seekToTimecode); - } finally { - trackProvider.close(); + try { + log.debug("Starting to play FLAC track {}", getIdentifier()); + localExecutor.executeProcessingLoop(trackProvider::provideFrames, trackProvider::seekToTimecode); + } finally { + trackProvider.close(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacContainerProbe.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacContainerProbe.java index 39706c8a1..5a921a855 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacContainerProbe.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacContainerProbe.java @@ -20,42 +20,42 @@ * Container detection probe for MP3 format. */ public class FlacContainerProbe implements MediaContainerProbe { - private static final Logger log = LoggerFactory.getLogger(FlacContainerProbe.class); + private static final Logger log = LoggerFactory.getLogger(FlacContainerProbe.class); - private static final String TITLE_TAG = "TITLE"; - private static final String ARTIST_TAG = "ARTIST"; + private static final String TITLE_TAG = "TITLE"; + private static final String ARTIST_TAG = "ARTIST"; - @Override - public String getName() { - return "flac"; - } - - @Override - public boolean matchesHints(MediaContainerHints hints) { - return false; - } + @Override + public String getName() { + return "flac"; + } - @Override - public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { - if (!checkNextBytes(inputStream, FlacFileLoader.FLAC_CC)) { - return null; + @Override + public boolean matchesHints(MediaContainerHints hints) { + return false; } - log.debug("Track {} is a FLAC file.", reference.identifier); + @Override + public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { + if (!checkNextBytes(inputStream, FlacFileLoader.FLAC_CC)) { + return null; + } - FlacTrackInfo fileInfo = new FlacFileLoader(inputStream).parseHeaders(); + log.debug("Track {} is a FLAC file.", reference.identifier); - AudioTrackInfo trackInfo = AudioTrackInfoBuilder.create(reference, inputStream) - .setTitle(fileInfo.tags.get(TITLE_TAG)) - .setAuthor(fileInfo.tags.get(ARTIST_TAG)) - .setLength(fileInfo.duration) - .build(); + FlacTrackInfo fileInfo = new FlacFileLoader(inputStream).parseHeaders(); - return supportedFormat(this, null, trackInfo); - } + AudioTrackInfo trackInfo = AudioTrackInfoBuilder.create(reference, inputStream) + .setTitle(fileInfo.tags.get(TITLE_TAG)) + .setAuthor(fileInfo.tags.get(ARTIST_TAG)) + .setLength(fileInfo.duration) + .build(); - @Override - public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - return new FlacAudioTrack(trackInfo, inputStream); - } + return supportedFormat(this, null, trackInfo); + } + + @Override + public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + return new FlacAudioTrack(trackInfo, inputStream); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacFileLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacFileLoader.java index 27bc05300..5dbc8f846 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacFileLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacFileLoader.java @@ -13,50 +13,52 @@ * Loads either FLAC header information or a FLAC track object from a stream. */ public class FlacFileLoader { - static final int[] FLAC_CC = new int[] { 0x66, 0x4C, 0x61, 0x43 }; - - private final SeekableInputStream inputStream; - private final DataInput dataInput; - - /** - * @param inputStream Input stream to read the FLAC data from. This must be positioned right before FLAC FourCC. - */ - public FlacFileLoader(SeekableInputStream inputStream) { - this.inputStream = inputStream; - this.dataInput = new DataInputStream(inputStream); - } - - /** - * Read all metadata from a FLAC file. Stream position is at the beginning of the first frame after this call. - * @return FLAC track information - * @throws IOException On IO Error - */ - public FlacTrackInfo parseHeaders() throws IOException { - if (!checkNextBytes(inputStream, FLAC_CC, false)) { - throw new IllegalStateException("Not a FLAC file"); + static final int[] FLAC_CC = new int[]{0x66, 0x4C, 0x61, 0x43}; + + private final SeekableInputStream inputStream; + private final DataInput dataInput; + + /** + * @param inputStream Input stream to read the FLAC data from. This must be positioned right before FLAC FourCC. + */ + public FlacFileLoader(SeekableInputStream inputStream) { + this.inputStream = inputStream; + this.dataInput = new DataInputStream(inputStream); + } + + /** + * Read all metadata from a FLAC file. Stream position is at the beginning of the first frame after this call. + * + * @return FLAC track information + * @throws IOException On IO Error + */ + public FlacTrackInfo parseHeaders() throws IOException { + if (!checkNextBytes(inputStream, FLAC_CC, false)) { + throw new IllegalStateException("Not a FLAC file"); + } + + FlacTrackInfoBuilder trackInfoBuilder = new FlacTrackInfoBuilder(FlacMetadataReader.readStreamInfoBlock(dataInput)); + readMetadataBlocks(trackInfoBuilder); + trackInfoBuilder.setFirstFramePosition(inputStream.getPosition()); + return trackInfoBuilder.build(); + } + + /** + * Initialise a FLAC track stream. + * + * @param context Configuration and output information for processing + * @return The FLAC track stream which can produce frames. + * @throws IOException On IO error + */ + public FlacTrackProvider loadTrack(AudioProcessingContext context) throws IOException { + return new FlacTrackProvider(context, parseHeaders(), inputStream); } - FlacTrackInfoBuilder trackInfoBuilder = new FlacTrackInfoBuilder(FlacMetadataReader.readStreamInfoBlock(dataInput)); - readMetadataBlocks(trackInfoBuilder); - trackInfoBuilder.setFirstFramePosition(inputStream.getPosition()); - return trackInfoBuilder.build(); - } - - /** - * Initialise a FLAC track stream. - * @param context Configuration and output information for processing - * @return The FLAC track stream which can produce frames. - * @throws IOException On IO error - */ - public FlacTrackProvider loadTrack(AudioProcessingContext context) throws IOException { - return new FlacTrackProvider(context, parseHeaders(), inputStream); - } - - private void readMetadataBlocks(FlacTrackInfoBuilder trackInfoBuilder) throws IOException { - boolean hasMoreBlocks = trackInfoBuilder.getStreamInfo().hasMetadataBlocks; - - while (hasMoreBlocks) { - hasMoreBlocks = FlacMetadataReader.readMetadataBlock(dataInput, inputStream, trackInfoBuilder); + private void readMetadataBlocks(FlacTrackInfoBuilder trackInfoBuilder) throws IOException { + boolean hasMoreBlocks = trackInfoBuilder.getStreamInfo().hasMetadataBlocks; + + while (hasMoreBlocks) { + hasMoreBlocks = FlacMetadataReader.readMetadataBlock(dataInput, inputStream, trackInfoBuilder); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacMetadataHeader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacMetadataHeader.java index b43689e82..d0bfb6dac 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacMetadataHeader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacMetadataHeader.java @@ -8,34 +8,34 @@ * A header of FLAC metadata. */ public class FlacMetadataHeader { - public static final int LENGTH = 4; - - public static final int BLOCK_SEEKTABLE = 3; - public static final int BLOCK_COMMENT = 4; - - /** - * If this header is for the last metadata block. If this is true, then the current metadata block is followed by - * frames. - */ - public final boolean isLastBlock; - - /** - * Block type, see: https://xiph.org/flac/format.html#metadata_block_header - */ - public final int blockType; - - /** - * Length of the block, current header excluded - */ - public final int blockLength; - - /** - * @param data The raw header data - */ - public FlacMetadataHeader(byte[] data) { - BitBufferReader bitReader = new BitBufferReader(ByteBuffer.wrap(data)); - isLastBlock = bitReader.asInteger(1) == 1; - blockType = bitReader.asInteger(7); - blockLength = bitReader.asInteger(24); - } + public static final int LENGTH = 4; + + public static final int BLOCK_SEEKTABLE = 3; + public static final int BLOCK_COMMENT = 4; + + /** + * If this header is for the last metadata block. If this is true, then the current metadata block is followed by + * frames. + */ + public final boolean isLastBlock; + + /** + * Block type, see: https://xiph.org/flac/format.html#metadata_block_header + */ + public final int blockType; + + /** + * Length of the block, current header excluded + */ + public final int blockLength; + + /** + * @param data The raw header data + */ + public FlacMetadataHeader(byte[] data) { + BitBufferReader bitReader = new BitBufferReader(ByteBuffer.wrap(data)); + isLastBlock = bitReader.asInteger(1) == 1; + blockType = bitReader.asInteger(7); + blockLength = bitReader.asInteger(24); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacMetadataReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacMetadataReader.java index fca824727..15c2b7ad2 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacMetadataReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacMetadataReader.java @@ -15,93 +15,93 @@ * Handles reading one FLAC metadata blocks. */ public class FlacMetadataReader { - private static final Charset CHARSET = StandardCharsets.UTF_8; - - /** - * Reads FLAC stream info metadata block. - * - * @param dataInput Data input where the block is read from - * @return Stream information - * @throws IOException On read error - */ - public static FlacStreamInfo readStreamInfoBlock(DataInput dataInput) throws IOException { - FlacMetadataHeader header = readMetadataHeader(dataInput); - - if (header.blockType != 0) { - throw new IllegalStateException("Wrong metadata block, should be stream info."); - } else if (header.blockLength != FlacStreamInfo.LENGTH) { - throw new IllegalStateException("Invalid stream info block size."); + private static final Charset CHARSET = StandardCharsets.UTF_8; + + /** + * Reads FLAC stream info metadata block. + * + * @param dataInput Data input where the block is read from + * @return Stream information + * @throws IOException On read error + */ + public static FlacStreamInfo readStreamInfoBlock(DataInput dataInput) throws IOException { + FlacMetadataHeader header = readMetadataHeader(dataInput); + + if (header.blockType != 0) { + throw new IllegalStateException("Wrong metadata block, should be stream info."); + } else if (header.blockLength != FlacStreamInfo.LENGTH) { + throw new IllegalStateException("Invalid stream info block size."); + } + + byte[] streamInfoData = new byte[FlacStreamInfo.LENGTH]; + dataInput.readFully(streamInfoData); + return new FlacStreamInfo(streamInfoData, !header.isLastBlock); } - byte[] streamInfoData = new byte[FlacStreamInfo.LENGTH]; - dataInput.readFully(streamInfoData); - return new FlacStreamInfo(streamInfoData, !header.isLastBlock); - } - - private static FlacMetadataHeader readMetadataHeader(DataInput dataInput) throws IOException { - byte[] headerBytes = new byte[FlacMetadataHeader.LENGTH]; - dataInput.readFully(headerBytes); - return new FlacMetadataHeader(headerBytes); - } - - /** - * @param dataInput Data input where the block is read from - * @param inputStream Input stream matching the data input - * @param trackInfoBuilder Track info builder object where detected metadata is stored in - * @return True if there are more metadata blocks available - * @throws IOException On read error - */ - public static boolean readMetadataBlock(DataInput dataInput, InputStream inputStream, FlacTrackInfoBuilder trackInfoBuilder) throws IOException { - FlacMetadataHeader header = readMetadataHeader(dataInput); - - if (header.blockType == BLOCK_SEEKTABLE) { - readSeekTableBlock(dataInput, trackInfoBuilder, header.blockLength); - } else if (header.blockType == BLOCK_COMMENT) { - readCommentBlock(dataInput, inputStream, trackInfoBuilder); - } else { - IOUtils.skipFully(inputStream, header.blockLength); + private static FlacMetadataHeader readMetadataHeader(DataInput dataInput) throws IOException { + byte[] headerBytes = new byte[FlacMetadataHeader.LENGTH]; + dataInput.readFully(headerBytes); + return new FlacMetadataHeader(headerBytes); } - return !header.isLastBlock; - } + /** + * @param dataInput Data input where the block is read from + * @param inputStream Input stream matching the data input + * @param trackInfoBuilder Track info builder object where detected metadata is stored in + * @return True if there are more metadata blocks available + * @throws IOException On read error + */ + public static boolean readMetadataBlock(DataInput dataInput, InputStream inputStream, FlacTrackInfoBuilder trackInfoBuilder) throws IOException { + FlacMetadataHeader header = readMetadataHeader(dataInput); + + if (header.blockType == BLOCK_SEEKTABLE) { + readSeekTableBlock(dataInput, trackInfoBuilder, header.blockLength); + } else if (header.blockType == BLOCK_COMMENT) { + readCommentBlock(dataInput, inputStream, trackInfoBuilder); + } else { + IOUtils.skipFully(inputStream, header.blockLength); + } + + return !header.isLastBlock; + } - private static void readCommentBlock(DataInput dataInput, InputStream inputStream, FlacTrackInfoBuilder trackInfoBuilder) throws IOException { - int vendorLength = Integer.reverseBytes(dataInput.readInt()); - IOUtils.skipFully(inputStream, vendorLength); + private static void readCommentBlock(DataInput dataInput, InputStream inputStream, FlacTrackInfoBuilder trackInfoBuilder) throws IOException { + int vendorLength = Integer.reverseBytes(dataInput.readInt()); + IOUtils.skipFully(inputStream, vendorLength); - int listLength = Integer.reverseBytes(dataInput.readInt()); + int listLength = Integer.reverseBytes(dataInput.readInt()); - for (int i = 0; i < listLength; i++) { - int itemLength = Integer.reverseBytes(dataInput.readInt()); + for (int i = 0; i < listLength; i++) { + int itemLength = Integer.reverseBytes(dataInput.readInt()); - byte[] textBytes = new byte[itemLength]; - dataInput.readFully(textBytes); + byte[] textBytes = new byte[itemLength]; + dataInput.readFully(textBytes); - String text = new String(textBytes, 0, textBytes.length, CHARSET); - String[] keyAndValue = text.split("=", 2); + String text = new String(textBytes, 0, textBytes.length, CHARSET); + String[] keyAndValue = text.split("=", 2); - if (keyAndValue.length > 1) { - trackInfoBuilder.addTag(keyAndValue[0].toUpperCase(), keyAndValue[1]); - } + if (keyAndValue.length > 1) { + trackInfoBuilder.addTag(keyAndValue[0].toUpperCase(), keyAndValue[1]); + } + } } - } - private static void readSeekTableBlock(DataInput dataInput, FlacTrackInfoBuilder trackInfoBuilder, int length) throws IOException { - FlacSeekPoint[] seekPoints = new FlacSeekPoint[length / FlacSeekPoint.LENGTH]; - int seekPointCount = 0; + private static void readSeekTableBlock(DataInput dataInput, FlacTrackInfoBuilder trackInfoBuilder, int length) throws IOException { + FlacSeekPoint[] seekPoints = new FlacSeekPoint[length / FlacSeekPoint.LENGTH]; + int seekPointCount = 0; - for (int i = 0; i < seekPoints.length; i++) { - long sampleIndex = dataInput.readLong(); - long byteOffset = dataInput.readLong(); - int sampleCount = dataInput.readUnsignedShort(); + for (int i = 0; i < seekPoints.length; i++) { + long sampleIndex = dataInput.readLong(); + long byteOffset = dataInput.readLong(); + int sampleCount = dataInput.readUnsignedShort(); - seekPoints[i] = new FlacSeekPoint(sampleIndex, byteOffset, sampleCount); + seekPoints[i] = new FlacSeekPoint(sampleIndex, byteOffset, sampleCount); - if (sampleIndex != -1) { - seekPointCount = i + 1; - } - } + if (sampleIndex != -1) { + seekPointCount = i + 1; + } + } - trackInfoBuilder.setSeekPoints(seekPoints, seekPointCount); - } + trackInfoBuilder.setSeekPoints(seekPoints, seekPointCount); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacSeekPoint.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacSeekPoint.java index a3b2d52df..50091d0ad 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacSeekPoint.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacSeekPoint.java @@ -3,12 +3,12 @@ /** * FLAC seek point info. Field descriptions are from: * https://xiph.org/flac/format.html#seekpoint - * + *

* - For placeholder points, the second and third field values are undefined. * - Seek points within a table must be sorted in ascending order by sample number. * - Seek points within a table must be unique by sample number, with the exception of placeholder points. * - The previous two notes imply that there may be any number of placeholder points, but they must all occur at the end - * of the table. + * of the table. */ public class FlacSeekPoint { public static final int LENGTH = 18; @@ -30,7 +30,7 @@ public class FlacSeekPoint { /** * @param sampleIndex Index of the first sample in the frame - * @param byteOffset Offset in bytes from first frame start to target frame start + * @param byteOffset Offset in bytes from first frame start to target frame start * @param sampleCount Number of samples in the frame */ public FlacSeekPoint(long sampleIndex, long byteOffset, int sampleCount) { diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacStreamInfo.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacStreamInfo.java index 280261213..5ac7cf652 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacStreamInfo.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacStreamInfo.java @@ -7,77 +7,77 @@ /** * FLAC stream info metadata block contents. Field descriptions are from: * https://xiph.org/flac/format.html#metadata_block_streaminfo - * + *

* FLAC specifies a minimum block size of 16 and a maximum block size of 65535, meaning the bit patterns corresponding * to the numbers 0-15 in the minimum blocksize and maximum blocksize fields are invalid. */ public class FlacStreamInfo { - public static final int LENGTH = 34; + public static final int LENGTH = 34; - /** - * The minimum block size (in samples) used in the stream. - */ - public final int minimumBlockSize; - /** - * The maximum block size (in samples) used in the stream. (Minimum blocksize == maximum blocksize) implies a - * fixed-blocksize stream. - */ - public final int maximumBlockSize; - /** - * The minimum frame size (in bytes) used in the stream. May be 0 to imply the value is not known. - */ - public final int minimumFrameSize; - /** - * The maximum frame size (in bytes) used in the stream. May be 0 to imply the value is not known. - */ - public final int maximumFrameSize; - /** - * Sample rate in Hz. Though 20 bits are available, the maximum sample rate is limited by the structure of frame - * headers to 655350Hz. Also, a value of 0 is invalid. - */ - public final int sampleRate; - /** - * FLAC supports from 1 to 8 channels - */ - public final int channelCount; - /** - * FLAC supports from 4 to 32 bits per sample. Currently the reference encoder and decoders only support up to 24 bits - * per sample. - */ - public final int bitsPerSample; - /** - * Total samples in stream. 'Samples' means inter-channel sample, i.e. one second of 44.1Khz audio will have 44100 - * samples regardless of the number of channels. A value of zero here means the number of total samples is unknown. - */ - public final long sampleCount; - /** - * MD5 signature of the unencoded audio data. This allows the decoder to determine if an error exists in the audio - * data even when the error does not result in an invalid bitstream. - */ - public final byte[] md5Signature; - /** - * Whether the file has any metadata blocks after the stream info. - */ - public final boolean hasMetadataBlocks; + /** + * The minimum block size (in samples) used in the stream. + */ + public final int minimumBlockSize; + /** + * The maximum block size (in samples) used in the stream. (Minimum blocksize == maximum blocksize) implies a + * fixed-blocksize stream. + */ + public final int maximumBlockSize; + /** + * The minimum frame size (in bytes) used in the stream. May be 0 to imply the value is not known. + */ + public final int minimumFrameSize; + /** + * The maximum frame size (in bytes) used in the stream. May be 0 to imply the value is not known. + */ + public final int maximumFrameSize; + /** + * Sample rate in Hz. Though 20 bits are available, the maximum sample rate is limited by the structure of frame + * headers to 655350Hz. Also, a value of 0 is invalid. + */ + public final int sampleRate; + /** + * FLAC supports from 1 to 8 channels + */ + public final int channelCount; + /** + * FLAC supports from 4 to 32 bits per sample. Currently the reference encoder and decoders only support up to 24 bits + * per sample. + */ + public final int bitsPerSample; + /** + * Total samples in stream. 'Samples' means inter-channel sample, i.e. one second of 44.1Khz audio will have 44100 + * samples regardless of the number of channels. A value of zero here means the number of total samples is unknown. + */ + public final long sampleCount; + /** + * MD5 signature of the unencoded audio data. This allows the decoder to determine if an error exists in the audio + * data even when the error does not result in an invalid bitstream. + */ + public final byte[] md5Signature; + /** + * Whether the file has any metadata blocks after the stream info. + */ + public final boolean hasMetadataBlocks; - /** - * @param blockData The raw block data. - * @param hasMetadataBlocks Whether the file has any metadata blocks after the stream info. - */ - public FlacStreamInfo(byte[] blockData, boolean hasMetadataBlocks) { - BitBufferReader bitReader = new BitBufferReader(ByteBuffer.wrap(blockData)); - minimumBlockSize = bitReader.asInteger(16); - maximumBlockSize = bitReader.asInteger(16); - minimumFrameSize = bitReader.asInteger(24); - maximumFrameSize = bitReader.asInteger(24); - sampleRate = bitReader.asInteger(20); - channelCount = bitReader.asInteger(3) + 1; - bitsPerSample = bitReader.asInteger(5) + 1; - sampleCount = bitReader.asLong(36); + /** + * @param blockData The raw block data. + * @param hasMetadataBlocks Whether the file has any metadata blocks after the stream info. + */ + public FlacStreamInfo(byte[] blockData, boolean hasMetadataBlocks) { + BitBufferReader bitReader = new BitBufferReader(ByteBuffer.wrap(blockData)); + minimumBlockSize = bitReader.asInteger(16); + maximumBlockSize = bitReader.asInteger(16); + minimumFrameSize = bitReader.asInteger(24); + maximumFrameSize = bitReader.asInteger(24); + sampleRate = bitReader.asInteger(20); + channelCount = bitReader.asInteger(3) + 1; + bitsPerSample = bitReader.asInteger(5) + 1; + sampleCount = bitReader.asLong(36); - md5Signature = new byte[16]; - System.arraycopy(blockData, 18, md5Signature, 0, 16); + md5Signature = new byte[16]; + System.arraycopy(blockData, 18, md5Signature, 0, 16); - this.hasMetadataBlocks = hasMetadataBlocks; - } + this.hasMetadataBlocks = hasMetadataBlocks; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacTrackInfo.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacTrackInfo.java index 3b4dff505..02f948592 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacTrackInfo.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacTrackInfo.java @@ -6,47 +6,47 @@ * All relevant information about a FLAC track from its metadata. */ public class FlacTrackInfo { - /** - * FLAC stream information. - */ - public final FlacStreamInfo stream; - /** - * An array of seek points. - */ - public final FlacSeekPoint[] seekPoints; - /** - * The actual number of seek points that are not placeholders. The end of the array may contain empty seek points, - * which is why this value should be used to determine how far into the array to look. - */ - public final int seekPointCount; - /** - * The map of tag values from comment metadata block. - */ - public final Map tags; - /** - * The position in the stream where the first frame starts. - */ - public final long firstFramePosition; - /** - * The duration of the track in milliseconds - */ - public final long duration; + /** + * FLAC stream information. + */ + public final FlacStreamInfo stream; + /** + * An array of seek points. + */ + public final FlacSeekPoint[] seekPoints; + /** + * The actual number of seek points that are not placeholders. The end of the array may contain empty seek points, + * which is why this value should be used to determine how far into the array to look. + */ + public final int seekPointCount; + /** + * The map of tag values from comment metadata block. + */ + public final Map tags; + /** + * The position in the stream where the first frame starts. + */ + public final long firstFramePosition; + /** + * The duration of the track in milliseconds + */ + public final long duration; - /** - * @param stream FLAC stream information. - * @param seekPoints An array of seek points. - * @param seekPointCount The actual number of seek points that are not placeholders. - * @param tags The map of tag values from comment metadata block. - * @param firstFramePosition The position in the stream where the first frame starts. - */ - public FlacTrackInfo(FlacStreamInfo stream, FlacSeekPoint[] seekPoints, int seekPointCount, Map tags, - long firstFramePosition) { + /** + * @param stream FLAC stream information. + * @param seekPoints An array of seek points. + * @param seekPointCount The actual number of seek points that are not placeholders. + * @param tags The map of tag values from comment metadata block. + * @param firstFramePosition The position in the stream where the first frame starts. + */ + public FlacTrackInfo(FlacStreamInfo stream, FlacSeekPoint[] seekPoints, int seekPointCount, Map tags, + long firstFramePosition) { - this.stream = stream; - this.seekPoints = seekPoints; - this.seekPointCount = seekPointCount; - this.tags = tags; - this.firstFramePosition = firstFramePosition; - this.duration = stream.sampleCount * 1000L / stream.sampleRate; - } + this.stream = stream; + this.seekPoints = seekPoints; + this.seekPointCount = seekPointCount; + this.tags = tags; + this.firstFramePosition = firstFramePosition; + this.duration = stream.sampleCount * 1000L / stream.sampleRate; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacTrackInfoBuilder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacTrackInfoBuilder.java index eb42c8d10..a294295ef 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacTrackInfoBuilder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacTrackInfoBuilder.java @@ -7,55 +7,55 @@ * Builder for FLAC track info. */ public class FlacTrackInfoBuilder { - private final FlacStreamInfo streamInfo; - private final Map tags; - private FlacSeekPoint[] seekPoints; - private int seekPointCount; - private long firstFramePosition; - - /** - * @param streamInfo Stream info metadata block. - */ - public FlacTrackInfoBuilder(FlacStreamInfo streamInfo) { - this.streamInfo = streamInfo; - this.tags = new HashMap<>(); - } - - /** - * @return Stream info metadata block. - */ - public FlacStreamInfo getStreamInfo() { - return streamInfo; - } - - /** - * @param seekPoints Seek point array. - * @param seekPointCount The number of seek points which are not placeholders. - */ - public void setSeekPoints(FlacSeekPoint[] seekPoints, int seekPointCount) { - this.seekPoints = seekPoints; - this.seekPointCount = seekPointCount; - } - - /** - * @param key Name of the tag - * @param value Value of the tag - */ - public void addTag(String key, String value) { - tags.put(key, value); - } - - /** - * @param firstFramePosition File position of the first frame - */ - public void setFirstFramePosition(long firstFramePosition) { - this.firstFramePosition = firstFramePosition; - } - - /** - * @return Track info object. - */ - public FlacTrackInfo build() { - return new FlacTrackInfo(streamInfo, seekPoints, seekPointCount, tags, firstFramePosition); - } + private final FlacStreamInfo streamInfo; + private final Map tags; + private FlacSeekPoint[] seekPoints; + private int seekPointCount; + private long firstFramePosition; + + /** + * @param streamInfo Stream info metadata block. + */ + public FlacTrackInfoBuilder(FlacStreamInfo streamInfo) { + this.streamInfo = streamInfo; + this.tags = new HashMap<>(); + } + + /** + * @return Stream info metadata block. + */ + public FlacStreamInfo getStreamInfo() { + return streamInfo; + } + + /** + * @param seekPoints Seek point array. + * @param seekPointCount The number of seek points which are not placeholders. + */ + public void setSeekPoints(FlacSeekPoint[] seekPoints, int seekPointCount) { + this.seekPoints = seekPoints; + this.seekPointCount = seekPointCount; + } + + /** + * @param key Name of the tag + * @param value Value of the tag + */ + public void addTag(String key, String value) { + tags.put(key, value); + } + + /** + * @param firstFramePosition File position of the first frame + */ + public void setFirstFramePosition(long firstFramePosition) { + this.firstFramePosition = firstFramePosition; + } + + /** + * @return Track info object. + */ + public FlacTrackInfo build() { + return new FlacTrackInfo(streamInfo, seekPoints, seekPointCount, tags, firstFramePosition); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacTrackProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacTrackProvider.java index c13fe445e..9a07a5bda 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacTrackProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/FlacTrackProvider.java @@ -14,99 +14,101 @@ * A provider of audio frames from a FLAC track. */ public class FlacTrackProvider { - private final FlacTrackInfo info; - private final SeekableInputStream inputStream; - private final AudioPipeline downstream; - private final BitStreamReader bitStreamReader; - private final int[] decodingBuffer; - private final int[][] rawSampleBuffers; - private final short[][] sampleBuffers; - - /** - * @param context Configuration and output information for processing - * @param info Track information from FLAC metadata - * @param inputStream Input stream to use - */ - public FlacTrackProvider(AudioProcessingContext context, FlacTrackInfo info, SeekableInputStream inputStream) { - this.info = info; - this.inputStream = inputStream; - this.downstream = AudioPipelineFactory.create(context, - new PcmFormat(info.stream.channelCount, info.stream.sampleRate)); - this.bitStreamReader = new BitStreamReader(inputStream); - this.decodingBuffer = new int[FlacFrameReader.TEMPORARY_BUFFER_SIZE]; - this.rawSampleBuffers = new int[info.stream.channelCount][]; - this.sampleBuffers = new short[info.stream.channelCount][]; - - for (int i = 0; i < rawSampleBuffers.length; i++) { - rawSampleBuffers[i] = new int[info.stream.maximumBlockSize]; - sampleBuffers[i] = new short[info.stream.maximumBlockSize]; + private final FlacTrackInfo info; + private final SeekableInputStream inputStream; + private final AudioPipeline downstream; + private final BitStreamReader bitStreamReader; + private final int[] decodingBuffer; + private final int[][] rawSampleBuffers; + private final short[][] sampleBuffers; + + /** + * @param context Configuration and output information for processing + * @param info Track information from FLAC metadata + * @param inputStream Input stream to use + */ + public FlacTrackProvider(AudioProcessingContext context, FlacTrackInfo info, SeekableInputStream inputStream) { + this.info = info; + this.inputStream = inputStream; + this.downstream = AudioPipelineFactory.create(context, + new PcmFormat(info.stream.channelCount, info.stream.sampleRate)); + this.bitStreamReader = new BitStreamReader(inputStream); + this.decodingBuffer = new int[FlacFrameReader.TEMPORARY_BUFFER_SIZE]; + this.rawSampleBuffers = new int[info.stream.channelCount][]; + this.sampleBuffers = new short[info.stream.channelCount][]; + + for (int i = 0; i < rawSampleBuffers.length; i++) { + rawSampleBuffers[i] = new int[info.stream.maximumBlockSize]; + sampleBuffers[i] = new short[info.stream.maximumBlockSize]; + } } - } - - /** - * Decodes audio frames and sends them to frame consumer - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - public void provideFrames() throws InterruptedException { - try { - int sampleCount; - - while ((sampleCount = readFlacFrame()) != 0) { - downstream.process(sampleBuffers, 0, sampleCount); - } - } catch (IOException e) { - throw new RuntimeException(e); + + /** + * Decodes audio frames and sends them to frame consumer + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + public void provideFrames() throws InterruptedException { + try { + int sampleCount; + + while ((sampleCount = readFlacFrame()) != 0) { + downstream.process(sampleBuffers, 0, sampleCount); + } + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - - private int readFlacFrame() throws IOException { - return FlacFrameReader.readFlacFrame(inputStream, bitStreamReader, info.stream, rawSampleBuffers, sampleBuffers, decodingBuffer); - } - - /** - * Seeks to the specified timecode. - * @param timecode The timecode in milliseconds - */ - public void seekToTimecode(long timecode) { - try { - FlacSeekPoint seekPoint = findSeekPointForTime(timecode); - inputStream.seek(info.firstFramePosition + seekPoint.byteOffset); - downstream.seekPerformed(timecode, seekPoint.sampleIndex * 1000 / info.stream.sampleRate); - } catch (IOException e) { - throw new RuntimeException(e); + + private int readFlacFrame() throws IOException { + return FlacFrameReader.readFlacFrame(inputStream, bitStreamReader, info.stream, rawSampleBuffers, sampleBuffers, decodingBuffer); } - } - private FlacSeekPoint findSeekPointForTime(long timecode) { - if (info.seekPointCount == 0) { - return new FlacSeekPoint(0, 0, 0); + /** + * Seeks to the specified timecode. + * + * @param timecode The timecode in milliseconds + */ + public void seekToTimecode(long timecode) { + try { + FlacSeekPoint seekPoint = findSeekPointForTime(timecode); + inputStream.seek(info.firstFramePosition + seekPoint.byteOffset); + downstream.seekPerformed(timecode, seekPoint.sampleIndex * 1000 / info.stream.sampleRate); + } catch (IOException e) { + throw new RuntimeException(e); + } } - long targetSampleIndex = timecode * info.stream.sampleRate / 1000L; - return binarySearchSeekPoints(info.seekPoints, info.seekPointCount, targetSampleIndex); - } + private FlacSeekPoint findSeekPointForTime(long timecode) { + if (info.seekPointCount == 0) { + return new FlacSeekPoint(0, 0, 0); + } - private FlacSeekPoint binarySearchSeekPoints(FlacSeekPoint[] seekPoints, int length, long targetSampleIndex) { - int low = 0; - int high = length - 1; + long targetSampleIndex = timecode * info.stream.sampleRate / 1000L; + return binarySearchSeekPoints(info.seekPoints, info.seekPointCount, targetSampleIndex); + } - while (high > low) { - int mid = (low + high + 1) / 2; + private FlacSeekPoint binarySearchSeekPoints(FlacSeekPoint[] seekPoints, int length, long targetSampleIndex) { + int low = 0; + int high = length - 1; - if (info.seekPoints[mid].sampleIndex > targetSampleIndex) { - high = mid - 1; - } else { - low = mid; - } - } + while (high > low) { + int mid = (low + high + 1) / 2; - return seekPoints[low]; - } + if (info.seekPoints[mid].sampleIndex > targetSampleIndex) { + high = mid - 1; + } else { + low = mid; + } + } - /** - * Free all resources associated to processing the track. - */ - public void close() { - downstream.close(); - } + return seekPoints[low]; + } + + /** + * Free all resources associated to processing the track. + */ + public void close() { + downstream.close(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacFrameHeaderReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacFrameHeaderReader.java index 29c691646..552aff9fb 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacFrameHeaderReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacFrameHeaderReader.java @@ -1,7 +1,7 @@ package com.sedmelluq.discord.lavaplayer.container.flac.frame; -import com.sedmelluq.discord.lavaplayer.container.flac.frame.FlacFrameInfo.ChannelDelta; import com.sedmelluq.discord.lavaplayer.container.flac.FlacStreamInfo; +import com.sedmelluq.discord.lavaplayer.container.flac.frame.FlacFrameInfo.ChannelDelta; import com.sedmelluq.discord.lavaplayer.tools.io.BitStreamReader; import java.io.IOException; @@ -15,125 +15,125 @@ * Contains methods for reading a frame header. */ public class FlacFrameHeaderReader { - private static final int VALUE_INVALID = Integer.MIN_VALUE; - private static final int VALUE_INHERITED = -1024; - - private static final int BLOCK_SIZE_EXPLICIT_8_BIT = -2; - private static final int BLOCK_SIZE_EXPLICIT_16_BIT = -1; - - private static final int SAMPLE_RATE_EXPLICIT_8_BIT = -3; - private static final int SAMPLE_RATE_EXPLICIT_16_BIT = -2; - private static final int SAMPLE_RATE_EXPLICIT_10X_16_BIT = -1; - - private static final int[] blockSizeMapping = new int[] { - VALUE_INVALID, 192, 576, 1152, 2304, 4608, BLOCK_SIZE_EXPLICIT_8_BIT, BLOCK_SIZE_EXPLICIT_16_BIT, - 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 - }; - - private static final int[] sampleRateMapping = new int[] { - VALUE_INHERITED, 88200, 176400, 192000, 8000, 16000, 22050, 24000, - 32000, 44100, 48000, 96000, SAMPLE_RATE_EXPLICIT_8_BIT, SAMPLE_RATE_EXPLICIT_16_BIT, SAMPLE_RATE_EXPLICIT_10X_16_BIT, VALUE_INVALID - }; - - private static final int[] channelCountMapping = new int[] { - 1, 2, 3, 4, 5, 6, 7, 8, - 2, 2, 2, VALUE_INVALID, VALUE_INVALID, VALUE_INVALID, VALUE_INVALID, VALUE_INVALID - }; - - private static final ChannelDelta[] channelDeltaMapping = new ChannelDelta[] { - NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE, - LEFT_SIDE, RIGHT_SIDE, MID_SIDE, NONE, NONE, NONE, NONE, NONE - }; - - private static final int[] sampleSizeMapping = new int[] { VALUE_INHERITED, 8, 12, VALUE_INVALID, 16, 20, 24, VALUE_INVALID }; - - /** - * Reads a frame header. At this point the first two bytes of the frame have actually been read during the frame sync - * scanning already. This means that this method expects there to be no EOF in the middle of the header. The frame - * information must match that of the stream, as changing sample rates, channel counts and sample sizes are not - * supported. - * - * @param bitStreamReader Bit stream reader for input - * @param streamInfo Information about the stream from metadata headers - * @param variableBlock If this is a variable block header. This information was included in the frame sync bytes - * consumed before calling this method. - * @return The frame information. - * @throws IOException On read error. - */ - public static FlacFrameInfo readFrameHeader(BitStreamReader bitStreamReader, FlacStreamInfo streamInfo, - boolean variableBlock) throws IOException { - - int blockSize = blockSizeMapping[bitStreamReader.asInteger(4)]; - int sampleRate = sampleRateMapping[bitStreamReader.asInteger(4)]; - int channelAssignment = bitStreamReader.asInteger(4); - int channelCount = channelCountMapping[channelAssignment]; - ChannelDelta channelDelta = channelDeltaMapping[channelAssignment]; - int sampleSize = sampleSizeMapping[bitStreamReader.asInteger(3)]; - - bitStreamReader.asInteger(1); - - readUtf8Value(variableBlock, bitStreamReader); - - if (blockSize == BLOCK_SIZE_EXPLICIT_8_BIT) { - blockSize = bitStreamReader.asInteger(8) + 1; - } else if (blockSize == BLOCK_SIZE_EXPLICIT_16_BIT) { - blockSize = bitStreamReader.asInteger(16) + 1; + private static final int VALUE_INVALID = Integer.MIN_VALUE; + private static final int VALUE_INHERITED = -1024; + + private static final int BLOCK_SIZE_EXPLICIT_8_BIT = -2; + private static final int BLOCK_SIZE_EXPLICIT_16_BIT = -1; + + private static final int SAMPLE_RATE_EXPLICIT_8_BIT = -3; + private static final int SAMPLE_RATE_EXPLICIT_16_BIT = -2; + private static final int SAMPLE_RATE_EXPLICIT_10X_16_BIT = -1; + + private static final int[] blockSizeMapping = new int[]{ + VALUE_INVALID, 192, 576, 1152, 2304, 4608, BLOCK_SIZE_EXPLICIT_8_BIT, BLOCK_SIZE_EXPLICIT_16_BIT, + 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 + }; + + private static final int[] sampleRateMapping = new int[]{ + VALUE_INHERITED, 88200, 176400, 192000, 8000, 16000, 22050, 24000, + 32000, 44100, 48000, 96000, SAMPLE_RATE_EXPLICIT_8_BIT, SAMPLE_RATE_EXPLICIT_16_BIT, SAMPLE_RATE_EXPLICIT_10X_16_BIT, VALUE_INVALID + }; + + private static final int[] channelCountMapping = new int[]{ + 1, 2, 3, 4, 5, 6, 7, 8, + 2, 2, 2, VALUE_INVALID, VALUE_INVALID, VALUE_INVALID, VALUE_INVALID, VALUE_INVALID + }; + + private static final ChannelDelta[] channelDeltaMapping = new ChannelDelta[]{ + NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE, + LEFT_SIDE, RIGHT_SIDE, MID_SIDE, NONE, NONE, NONE, NONE, NONE + }; + + private static final int[] sampleSizeMapping = new int[]{VALUE_INHERITED, 8, 12, VALUE_INVALID, 16, 20, 24, VALUE_INVALID}; + + /** + * Reads a frame header. At this point the first two bytes of the frame have actually been read during the frame sync + * scanning already. This means that this method expects there to be no EOF in the middle of the header. The frame + * information must match that of the stream, as changing sample rates, channel counts and sample sizes are not + * supported. + * + * @param bitStreamReader Bit stream reader for input + * @param streamInfo Information about the stream from metadata headers + * @param variableBlock If this is a variable block header. This information was included in the frame sync bytes + * consumed before calling this method. + * @return The frame information. + * @throws IOException On read error. + */ + public static FlacFrameInfo readFrameHeader(BitStreamReader bitStreamReader, FlacStreamInfo streamInfo, + boolean variableBlock) throws IOException { + + int blockSize = blockSizeMapping[bitStreamReader.asInteger(4)]; + int sampleRate = sampleRateMapping[bitStreamReader.asInteger(4)]; + int channelAssignment = bitStreamReader.asInteger(4); + int channelCount = channelCountMapping[channelAssignment]; + ChannelDelta channelDelta = channelDeltaMapping[channelAssignment]; + int sampleSize = sampleSizeMapping[bitStreamReader.asInteger(3)]; + + bitStreamReader.asInteger(1); + + readUtf8Value(variableBlock, bitStreamReader); + + if (blockSize == BLOCK_SIZE_EXPLICIT_8_BIT) { + blockSize = bitStreamReader.asInteger(8) + 1; + } else if (blockSize == BLOCK_SIZE_EXPLICIT_16_BIT) { + blockSize = bitStreamReader.asInteger(16) + 1; + } + + verifyNotInvalid(blockSize, "block size"); + + if (blockSize == SAMPLE_RATE_EXPLICIT_8_BIT) { + sampleRate = bitStreamReader.asInteger(8); + } else if (blockSize == SAMPLE_RATE_EXPLICIT_16_BIT) { + sampleRate = bitStreamReader.asInteger(16); + } else if (blockSize == SAMPLE_RATE_EXPLICIT_10X_16_BIT) { + sampleRate = bitStreamReader.asInteger(16) * 10; + } + + verifyMatchesExpected(sampleRate, streamInfo.sampleRate, "sample rate"); + verifyMatchesExpected(channelCount, streamInfo.channelCount, "channel count"); + verifyMatchesExpected(sampleSize, streamInfo.bitsPerSample, "bits per sample"); + + // Ignore CRC for now + bitStreamReader.asInteger(8); + + return new FlacFrameInfo(blockSize, channelDelta); } - verifyNotInvalid(blockSize, "block size"); - - if (blockSize == SAMPLE_RATE_EXPLICIT_8_BIT) { - sampleRate = bitStreamReader.asInteger(8); - } else if (blockSize == SAMPLE_RATE_EXPLICIT_16_BIT) { - sampleRate = bitStreamReader.asInteger(16); - } else if (blockSize == SAMPLE_RATE_EXPLICIT_10X_16_BIT) { - sampleRate = bitStreamReader.asInteger(16) * 10; + private static void verifyNotInvalid(int value, String description) { + if (value < 0) { + throw new IllegalStateException("Invalid value " + value + " for " + description); + } } - verifyMatchesExpected(sampleRate, streamInfo.sampleRate, "sample rate"); - verifyMatchesExpected(channelCount, streamInfo.channelCount, "channel count"); - verifyMatchesExpected(sampleSize, streamInfo.bitsPerSample, "bits per sample"); - - // Ignore CRC for now - bitStreamReader.asInteger(8); - - return new FlacFrameInfo(blockSize, channelDelta); - } - - private static void verifyNotInvalid(int value, String description) { - if (value < 0) { - throw new IllegalStateException("Invalid value " + value + " for " + description); + private static void verifyMatchesExpected(int value, int expected, String description) { + if (value != VALUE_INHERITED && value != expected) { + throw new IllegalStateException("Invalid value " + value + " for " + description + ", should match value " + expected + " in stream."); + } } - } - private static void verifyMatchesExpected(int value, int expected, String description) { - if (value != VALUE_INHERITED && value != expected) { - throw new IllegalStateException("Invalid value " + value + " for " + description + ", should match value " + expected + " in stream."); - } - } + private static long readUtf8Value(boolean isLong, BitStreamReader bitStreamReader) throws IOException { + int maximumSize = isLong ? 7 : 6; + int firstByte = bitStreamReader.asInteger(8); + int leadingOnes = Integer.numberOfLeadingZeros((~firstByte) & 0xFF) - 24; - private static long readUtf8Value(boolean isLong, BitStreamReader bitStreamReader) throws IOException { - int maximumSize = isLong ? 7 : 6; - int firstByte = bitStreamReader.asInteger(8); - int leadingOnes = Integer.numberOfLeadingZeros((~firstByte) & 0xFF) - 24; + if (leadingOnes > maximumSize || leadingOnes == 1) { + throw new IllegalStateException("Invalid number of leading ones in UTF encoded integer"); + } else if (leadingOnes == 0) { + return firstByte; + } - if (leadingOnes > maximumSize || leadingOnes == 1) { - throw new IllegalStateException("Invalid number of leading ones in UTF encoded integer"); - } else if (leadingOnes == 0) { - return firstByte; - } + long value = firstByte - (1L << (7 - leadingOnes)) - 1L; - long value = firstByte - (1L << (7 - leadingOnes)) - 1L; + for (int i = 0; i < leadingOnes - 1; i++) { + int currentByte = bitStreamReader.asInteger(8); + if ((currentByte & 0xC0) != 0x80) { + throw new IllegalStateException("Invalid content of payload byte, first bits must be 1 and 0."); + } - for (int i = 0; i < leadingOnes - 1; i++) { - int currentByte = bitStreamReader.asInteger(8); - if ((currentByte & 0xC0) != 0x80) { - throw new IllegalStateException("Invalid content of payload byte, first bits must be 1 and 0."); - } + value = (value << 6) | (currentByte & 0x3F); + } - value = (value << 6) | (currentByte & 0x3F); + return value; } - - return value; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacFrameInfo.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacFrameInfo.java index aea94f1e8..b14c9781a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacFrameInfo.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacFrameInfo.java @@ -17,7 +17,7 @@ public class FlacFrameInfo { public final ChannelDelta channelDelta; /** - * @param sampleCount Number of samples in each subframe of this frame + * @param sampleCount Number of samples in each subframe of this frame * @param channelDelta Channel data delta setting */ public FlacFrameInfo(int sampleCount, ChannelDelta channelDelta) { diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacFrameReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacFrameReader.java index 1282f9ab8..6770745fe 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacFrameReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacFrameReader.java @@ -1,7 +1,7 @@ package com.sedmelluq.discord.lavaplayer.container.flac.frame; -import com.sedmelluq.discord.lavaplayer.container.flac.frame.FlacFrameInfo.ChannelDelta; import com.sedmelluq.discord.lavaplayer.container.flac.FlacStreamInfo; +import com.sedmelluq.discord.lavaplayer.container.flac.frame.FlacFrameInfo.ChannelDelta; import com.sedmelluq.discord.lavaplayer.tools.io.BitStreamReader; import java.io.IOException; @@ -11,140 +11,140 @@ * Handles reading one FLAC audio frame. */ public class FlacFrameReader { - public static final int TEMPORARY_BUFFER_SIZE = 32; - - /** - * Reads one frame, returning the number of samples written to sampleBuffers. A return value of 0 indicates that EOF - * was reached in the frame, which happens when the track ends. - * - * @param inputStream Input stream for reading the track - * @param reader Bit stream reader for the same underlying stream as inputStream - * @param streamInfo Global stream information - * @param rawSampleBuffers Intermediate sample decoding buffers. FlacStreamInfo#channelCount integer buffers of size - * at least FlacStreamInfo#maximumBlockSize. - * @param sampleBuffers The sample buffers where the final decoding result is written to. FlacStreamInfo#channelCount - * short buffers of size at least FlacStreamInfo#maximumBlockSize. - * @param temporaryBuffer Temporary working buffer of size at least TEMPORARY_BUFFER_SIZE. No state is held in this - * between separate calls. - * @return The number of samples read, zero on EOF - * @throws IOException On read error - */ - public static int readFlacFrame(InputStream inputStream, BitStreamReader reader, FlacStreamInfo streamInfo, - int[][] rawSampleBuffers, short[][] sampleBuffers, int[] temporaryBuffer) throws IOException { - FlacFrameInfo frameInfo = findAndParseFrameHeader(inputStream, reader, streamInfo); - - if (frameInfo == null) { - return 0; - } + public static final int TEMPORARY_BUFFER_SIZE = 32; + + /** + * Reads one frame, returning the number of samples written to sampleBuffers. A return value of 0 indicates that EOF + * was reached in the frame, which happens when the track ends. + * + * @param inputStream Input stream for reading the track + * @param reader Bit stream reader for the same underlying stream as inputStream + * @param streamInfo Global stream information + * @param rawSampleBuffers Intermediate sample decoding buffers. FlacStreamInfo#channelCount integer buffers of size + * at least FlacStreamInfo#maximumBlockSize. + * @param sampleBuffers The sample buffers where the final decoding result is written to. FlacStreamInfo#channelCount + * short buffers of size at least FlacStreamInfo#maximumBlockSize. + * @param temporaryBuffer Temporary working buffer of size at least TEMPORARY_BUFFER_SIZE. No state is held in this + * between separate calls. + * @return The number of samples read, zero on EOF + * @throws IOException On read error + */ + public static int readFlacFrame(InputStream inputStream, BitStreamReader reader, FlacStreamInfo streamInfo, + int[][] rawSampleBuffers, short[][] sampleBuffers, int[] temporaryBuffer) throws IOException { + FlacFrameInfo frameInfo = findAndParseFrameHeader(inputStream, reader, streamInfo); + + if (frameInfo == null) { + return 0; + } - for (int i = 0; i < streamInfo.channelCount; i++) { - FlacSubFrameReader.readSubFrame(reader, streamInfo, frameInfo, rawSampleBuffers[i], i, temporaryBuffer); - } + for (int i = 0; i < streamInfo.channelCount; i++) { + FlacSubFrameReader.readSubFrame(reader, streamInfo, frameInfo, rawSampleBuffers[i], i, temporaryBuffer); + } - reader.readRemainingBits(); - reader.asInteger(16); + reader.readRemainingBits(); + reader.asInteger(16); - applyChannelDelta(frameInfo.channelDelta, rawSampleBuffers, frameInfo.sampleCount); - convertToShortPcm(streamInfo, frameInfo.sampleCount, rawSampleBuffers, sampleBuffers); + applyChannelDelta(frameInfo.channelDelta, rawSampleBuffers, frameInfo.sampleCount); + convertToShortPcm(streamInfo, frameInfo.sampleCount, rawSampleBuffers, sampleBuffers); - return frameInfo.sampleCount; - } + return frameInfo.sampleCount; + } + + private static FlacFrameInfo findAndParseFrameHeader(InputStream inputStream, BitStreamReader reader, + FlacStreamInfo streamInfo) throws IOException { + int blockingStrategy; - private static FlacFrameInfo findAndParseFrameHeader(InputStream inputStream, BitStreamReader reader, - FlacStreamInfo streamInfo) throws IOException { - int blockingStrategy; + if ((blockingStrategy = skipToFrameSync(inputStream)) == -1) { + return null; + } - if ((blockingStrategy = skipToFrameSync(inputStream)) == -1) { - return null; + return FlacFrameHeaderReader.readFrameHeader(reader, streamInfo, blockingStrategy == 1); } - return FlacFrameHeaderReader.readFrameHeader(reader, streamInfo, blockingStrategy == 1); - } + private static int skipToFrameSync(InputStream inputStream) throws IOException { + int lastByte = -1; + int currentByte; - private static int skipToFrameSync(InputStream inputStream) throws IOException { - int lastByte = -1; - int currentByte; + while ((currentByte = inputStream.read()) != -1) { + if (lastByte == 0xFF && (currentByte & 0xFE) == 0xF8) { + return currentByte & 0x01; + } + lastByte = currentByte; + } - while ((currentByte = inputStream.read()) != -1) { - if (lastByte == 0xFF && (currentByte & 0xFE) == 0xF8) { - return currentByte & 0x01; - } - lastByte = currentByte; + return -1; } - return -1; - } - - private static void applyChannelDelta(ChannelDelta channelDelta, int[][] rawSampleBuffers, int sampleCount) { - switch (channelDelta) { - case LEFT_SIDE: - applyLeftSideDelta(rawSampleBuffers, sampleCount); - break; - case RIGHT_SIDE: - applyRightSideDelta(rawSampleBuffers, sampleCount); - break; - case MID_SIDE: - applyMidDelta(rawSampleBuffers, sampleCount); - break; - case NONE: - default: - break; + private static void applyChannelDelta(ChannelDelta channelDelta, int[][] rawSampleBuffers, int sampleCount) { + switch (channelDelta) { + case LEFT_SIDE: + applyLeftSideDelta(rawSampleBuffers, sampleCount); + break; + case RIGHT_SIDE: + applyRightSideDelta(rawSampleBuffers, sampleCount); + break; + case MID_SIDE: + applyMidDelta(rawSampleBuffers, sampleCount); + break; + case NONE: + default: + break; + } } - } - private static void applyLeftSideDelta(int[][] rawSampleBuffers, int sampleCount) { - for (int i = 0; i < sampleCount; i++) { - rawSampleBuffers[1][i] = rawSampleBuffers[0][i] - rawSampleBuffers[1][i]; + private static void applyLeftSideDelta(int[][] rawSampleBuffers, int sampleCount) { + for (int i = 0; i < sampleCount; i++) { + rawSampleBuffers[1][i] = rawSampleBuffers[0][i] - rawSampleBuffers[1][i]; + } } - } - private static void applyRightSideDelta(int[][] rawSampleBuffers, int sampleCount) { - for (int i = 0; i < sampleCount; i++) { - rawSampleBuffers[0][i] += rawSampleBuffers[1][i]; + private static void applyRightSideDelta(int[][] rawSampleBuffers, int sampleCount) { + for (int i = 0; i < sampleCount; i++) { + rawSampleBuffers[0][i] += rawSampleBuffers[1][i]; + } } - } - private static void applyMidDelta(int[][] rawSampleBuffers, int sampleCount) { - for (int i = 0; i < sampleCount; i++) { - int delta = rawSampleBuffers[1][i]; - int middle = (rawSampleBuffers[0][i] << 1) + (delta & 1); + private static void applyMidDelta(int[][] rawSampleBuffers, int sampleCount) { + for (int i = 0; i < sampleCount; i++) { + int delta = rawSampleBuffers[1][i]; + int middle = (rawSampleBuffers[0][i] << 1) + (delta & 1); - rawSampleBuffers[0][i] = (middle + delta) >> 1; - rawSampleBuffers[1][i] = (middle - delta) >> 1; + rawSampleBuffers[0][i] = (middle + delta) >> 1; + rawSampleBuffers[1][i] = (middle - delta) >> 1; + } } - } - - private static void convertToShortPcm(FlacStreamInfo streamInfo, int sampleCount, int[][] rawSampleBuffers, short[][] sampleBuffers) { - if (streamInfo.bitsPerSample < 16) { - increaseSampleSize(streamInfo, sampleCount, rawSampleBuffers, sampleBuffers); - } else if (streamInfo.bitsPerSample > 16) { - decreaseSampleSize(streamInfo, sampleCount, rawSampleBuffers, sampleBuffers); - } else { - for (int channel = 0; channel < streamInfo.channelCount; channel++) { - for (int i = 0; i < sampleCount; i++) { - sampleBuffers[channel][i] = (short) rawSampleBuffers[channel][i]; + + private static void convertToShortPcm(FlacStreamInfo streamInfo, int sampleCount, int[][] rawSampleBuffers, short[][] sampleBuffers) { + if (streamInfo.bitsPerSample < 16) { + increaseSampleSize(streamInfo, sampleCount, rawSampleBuffers, sampleBuffers); + } else if (streamInfo.bitsPerSample > 16) { + decreaseSampleSize(streamInfo, sampleCount, rawSampleBuffers, sampleBuffers); + } else { + for (int channel = 0; channel < streamInfo.channelCount; channel++) { + for (int i = 0; i < sampleCount; i++) { + sampleBuffers[channel][i] = (short) rawSampleBuffers[channel][i]; + } + } } - } } - } - private static void increaseSampleSize(FlacStreamInfo streamInfo, int sampleCount, int[][] rawSampleBuffers, short[][] sampleBuffers) { - int shiftLeft = 16 - streamInfo.bitsPerSample; + private static void increaseSampleSize(FlacStreamInfo streamInfo, int sampleCount, int[][] rawSampleBuffers, short[][] sampleBuffers) { + int shiftLeft = 16 - streamInfo.bitsPerSample; - for (int channel = 0; channel < streamInfo.channelCount; channel++) { - for (int i = 0; i < sampleCount; i++) { - sampleBuffers[channel][i] = (short) (rawSampleBuffers[channel][i] << shiftLeft); - } + for (int channel = 0; channel < streamInfo.channelCount; channel++) { + for (int i = 0; i < sampleCount; i++) { + sampleBuffers[channel][i] = (short) (rawSampleBuffers[channel][i] << shiftLeft); + } + } } - } - private static void decreaseSampleSize(FlacStreamInfo streamInfo, int sampleCount, int[][] rawSampleBuffers, short[][] sampleBuffers) { - int shiftRight = streamInfo.bitsPerSample - 16; + private static void decreaseSampleSize(FlacStreamInfo streamInfo, int sampleCount, int[][] rawSampleBuffers, short[][] sampleBuffers) { + int shiftRight = streamInfo.bitsPerSample - 16; - for (int channel = 0; channel < streamInfo.channelCount; channel++) { - for (int i = 0; i < sampleCount; i++) { - sampleBuffers[channel][i] = (short) (rawSampleBuffers[channel][i] >> shiftRight); - } + for (int channel = 0; channel < streamInfo.channelCount; channel++) { + for (int i = 0; i < sampleCount; i++) { + sampleBuffers[channel][i] = (short) (rawSampleBuffers[channel][i] >> shiftRight); + } + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacSubFrameReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacSubFrameReader.java index 28a47d0f6..e1183b645 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacSubFrameReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/flac/frame/FlacSubFrameReader.java @@ -9,189 +9,189 @@ * Contains methods for reading a FLAC subframe */ public class FlacSubFrameReader { - private static final Encoding[] encodingMapping = new Encoding[] { - Encoding.LPC, null, Encoding.FIXED, null, null, Encoding.VERBATIM, Encoding.CONSTANT - }; - - /** - * Reads and decodes one subframe (a channel of a frame) - * - * @param reader Bit stream reader - * @param streamInfo Stream global info - * @param frameInfo Current frame info - * @param sampleBuffer Output buffer for the (possibly delta) decoded samples of this subframe - * @param channel The index of the current channel - * @param temporaryBuffer Temporary working buffer of size at least 32 - * @throws IOException On read error - */ - public static void readSubFrame(BitStreamReader reader, FlacStreamInfo streamInfo, FlacFrameInfo frameInfo, - int[] sampleBuffer, int channel, int[] temporaryBuffer) throws IOException { - - if (reader.asInteger(1) == 1) { - throw new IllegalStateException("Subframe header must start with 0 bit."); - } + private static final Encoding[] encodingMapping = new Encoding[]{ + Encoding.LPC, null, Encoding.FIXED, null, null, Encoding.VERBATIM, Encoding.CONSTANT + }; + + /** + * Reads and decodes one subframe (a channel of a frame) + * + * @param reader Bit stream reader + * @param streamInfo Stream global info + * @param frameInfo Current frame info + * @param sampleBuffer Output buffer for the (possibly delta) decoded samples of this subframe + * @param channel The index of the current channel + * @param temporaryBuffer Temporary working buffer of size at least 32 + * @throws IOException On read error + */ + public static void readSubFrame(BitStreamReader reader, FlacStreamInfo streamInfo, FlacFrameInfo frameInfo, + int[] sampleBuffer, int channel, int[] temporaryBuffer) throws IOException { + + if (reader.asInteger(1) == 1) { + throw new IllegalStateException("Subframe header must start with 0 bit."); + } - boolean isDeltaChannel = frameInfo.channelDelta.deltaChannel == channel; + boolean isDeltaChannel = frameInfo.channelDelta.deltaChannel == channel; - int subFrameDescriptor = reader.asInteger(6); + int subFrameDescriptor = reader.asInteger(6); - int wastedBitCount = reader.asInteger(1) == 1 ? reader.readAllZeroes() + 1 : 0; - int bitsPerSample = streamInfo.bitsPerSample - wastedBitCount + (isDeltaChannel ? 1 : 0); + int wastedBitCount = reader.asInteger(1) == 1 ? reader.readAllZeroes() + 1 : 0; + int bitsPerSample = streamInfo.bitsPerSample - wastedBitCount + (isDeltaChannel ? 1 : 0); - readSubFrameSamples(reader, subFrameDescriptor, bitsPerSample, sampleBuffer, frameInfo.sampleCount, temporaryBuffer); + readSubFrameSamples(reader, subFrameDescriptor, bitsPerSample, sampleBuffer, frameInfo.sampleCount, temporaryBuffer); - if (wastedBitCount > 0) { - for (int i = 0; i < frameInfo.sampleCount; i++) { - sampleBuffer[i] <<= wastedBitCount; - } - } - } - - private static void readSubFrameSamples(BitStreamReader reader, int subFrameDescriptor, int bitsPerSample, int[] sampleBuffer, - int sampleCount, int[] temporaryBuffer) throws IOException { - - Encoding subframeEncoding = encodingMapping[Integer.numberOfLeadingZeros(subFrameDescriptor) - 26]; - - if (subframeEncoding == null) { - throw new RuntimeException("Invalid subframe type."); - } else if (subframeEncoding == Encoding.LPC) { - readSubFrameLpcData(reader, (subFrameDescriptor & 0x1F) + 1, bitsPerSample, sampleBuffer, sampleCount, temporaryBuffer); - } else if (subframeEncoding == Encoding.FIXED) { - readSubFrameFixedData(reader, subFrameDescriptor & 0x07, bitsPerSample, sampleBuffer, sampleCount); - } else if (subframeEncoding == Encoding.VERBATIM) { - readSubFrameVerbatimData(reader, bitsPerSample, sampleBuffer, sampleCount); - } else if (subframeEncoding == Encoding.CONSTANT) { - readSubFrameConstantData(reader, bitsPerSample, sampleBuffer, sampleCount); + if (wastedBitCount > 0) { + for (int i = 0; i < frameInfo.sampleCount; i++) { + sampleBuffer[i] <<= wastedBitCount; + } + } } - } - private static void readSubFrameConstantData(BitStreamReader reader, int bitsPerSample, int[] sampleBuffer, - int sampleCount) throws IOException { + private static void readSubFrameSamples(BitStreamReader reader, int subFrameDescriptor, int bitsPerSample, int[] sampleBuffer, + int sampleCount, int[] temporaryBuffer) throws IOException { + + Encoding subframeEncoding = encodingMapping[Integer.numberOfLeadingZeros(subFrameDescriptor) - 26]; + + if (subframeEncoding == null) { + throw new RuntimeException("Invalid subframe type."); + } else if (subframeEncoding == Encoding.LPC) { + readSubFrameLpcData(reader, (subFrameDescriptor & 0x1F) + 1, bitsPerSample, sampleBuffer, sampleCount, temporaryBuffer); + } else if (subframeEncoding == Encoding.FIXED) { + readSubFrameFixedData(reader, subFrameDescriptor & 0x07, bitsPerSample, sampleBuffer, sampleCount); + } else if (subframeEncoding == Encoding.VERBATIM) { + readSubFrameVerbatimData(reader, bitsPerSample, sampleBuffer, sampleCount); + } else if (subframeEncoding == Encoding.CONSTANT) { + readSubFrameConstantData(reader, bitsPerSample, sampleBuffer, sampleCount); + } + } - int value = reader.asSignedInteger(bitsPerSample); + private static void readSubFrameConstantData(BitStreamReader reader, int bitsPerSample, int[] sampleBuffer, + int sampleCount) throws IOException { - for (int i = 0; i < sampleCount; i++) { - sampleBuffer[i] = value; - } - } + int value = reader.asSignedInteger(bitsPerSample); - private static void readSubFrameVerbatimData(BitStreamReader reader, int bitsPerSample, int[] sampleBuffer, - int sampleCount) throws IOException { - for (int i = 0; i < sampleCount; i++) { - sampleBuffer[i] = reader.asSignedInteger(bitsPerSample); + for (int i = 0; i < sampleCount; i++) { + sampleBuffer[i] = value; + } } - } - private static void readSubFrameFixedData(BitStreamReader reader, int order, int bitsPerSample, int[] sampleBuffer, - int sampleCount) throws IOException { - for (int i = 0; i < order; i++) { - sampleBuffer[i] = reader.asSignedInteger(bitsPerSample); + private static void readSubFrameVerbatimData(BitStreamReader reader, int bitsPerSample, int[] sampleBuffer, + int sampleCount) throws IOException { + for (int i = 0; i < sampleCount; i++) { + sampleBuffer[i] = reader.asSignedInteger(bitsPerSample); + } } - readResidual(reader, order, sampleBuffer, order, sampleCount); - restoreFixedSignal(sampleBuffer, sampleCount, order); - } - - private static void restoreFixedSignal(int[] buffer, int sampleCount, int order) { - switch (order) { - case 1: - for (int i = order; i < sampleCount; i++) { - buffer[i] += buffer[i - 1]; - } - break; - case 2: - for (int i = order; i < sampleCount; i++) { - buffer[i] += (buffer[i - 1] << 1) - buffer[i - 2]; - } - break; - case 3: - for (int i = order; i < sampleCount; i++) { - buffer[i] += (((buffer[i - 1] - buffer[i - 2]) << 1) + (buffer[i - 1] - buffer[i - 2])) + buffer[i - 3]; + private static void readSubFrameFixedData(BitStreamReader reader, int order, int bitsPerSample, int[] sampleBuffer, + int sampleCount) throws IOException { + for (int i = 0; i < order; i++) { + sampleBuffer[i] = reader.asSignedInteger(bitsPerSample); } - break; - case 4: - for (int i = order; i < sampleCount; i++) { - buffer[i] += ((buffer[i - 1] + buffer[i - 3]) << 2) - ((buffer[i - 2] << 2) + (buffer[i - 2] << 1)) - buffer[i - 4]; - } - break; - default: - break; - } - } - private static void readSubFrameLpcData(BitStreamReader reader, int order, int bitsPerSample, int[] sampleBuffer, - int sampleCount, int[] coefficients) throws IOException { - for (int i = 0; i < order; i++) { - sampleBuffer[i] = reader.asSignedInteger(bitsPerSample); + readResidual(reader, order, sampleBuffer, order, sampleCount); + restoreFixedSignal(sampleBuffer, sampleCount, order); } - int precision = reader.asInteger(4) + 1; - int shift = reader.asInteger(5); - - for (int i = 0; i < order; i++) { - coefficients[i] = reader.asSignedInteger(precision); + private static void restoreFixedSignal(int[] buffer, int sampleCount, int order) { + switch (order) { + case 1: + for (int i = order; i < sampleCount; i++) { + buffer[i] += buffer[i - 1]; + } + break; + case 2: + for (int i = order; i < sampleCount; i++) { + buffer[i] += (buffer[i - 1] << 1) - buffer[i - 2]; + } + break; + case 3: + for (int i = order; i < sampleCount; i++) { + buffer[i] += (((buffer[i - 1] - buffer[i - 2]) << 1) + (buffer[i - 1] - buffer[i - 2])) + buffer[i - 3]; + } + break; + case 4: + for (int i = order; i < sampleCount; i++) { + buffer[i] += ((buffer[i - 1] + buffer[i - 3]) << 2) - ((buffer[i - 2] << 2) + (buffer[i - 2] << 1)) - buffer[i - 4]; + } + break; + default: + break; + } } - readResidual(reader, order, sampleBuffer, order, sampleCount); - restoreLpcSignal(sampleBuffer, sampleCount, order, shift, coefficients); - } + private static void readSubFrameLpcData(BitStreamReader reader, int order, int bitsPerSample, int[] sampleBuffer, + int sampleCount, int[] coefficients) throws IOException { + for (int i = 0; i < order; i++) { + sampleBuffer[i] = reader.asSignedInteger(bitsPerSample); + } - private static void restoreLpcSignal(int[] buffer, int sampleCount, int order, int shift, int[] coefficients) { - for (int i = order; i < sampleCount; i++) { - long sum = 0; + int precision = reader.asInteger(4) + 1; + int shift = reader.asInteger(5); - for (int j = 0; j < order; j++) { - sum += (long) coefficients[j] * buffer[i - j - 1]; - } + for (int i = 0; i < order; i++) { + coefficients[i] = reader.asSignedInteger(precision); + } - buffer[i] += (int) (sum >> shift); + readResidual(reader, order, sampleBuffer, order, sampleCount); + restoreLpcSignal(sampleBuffer, sampleCount, order, shift, coefficients); } - } - private static void readResidual(BitStreamReader reader, int order, int[] buffer, int startOffset, int endOffset) throws IOException { - int method = reader.asInteger(2); + private static void restoreLpcSignal(int[] buffer, int sampleCount, int order, int shift, int[] coefficients) { + for (int i = order; i < sampleCount; i++) { + long sum = 0; - if (method > 1) { - throw new RuntimeException("Invalid residual coding method " + method); - } + for (int j = 0; j < order; j++) { + sum += (long) coefficients[j] * buffer[i - j - 1]; + } - int partitionOrder = reader.asInteger(4); - int partitions = 1 << partitionOrder; - int partitionSamples = partitionOrder > 0 ? endOffset >> partitionOrder : endOffset - order; - int parameterLength = method == 0 ? 4 : 5; - int parameterMaximum = (1 << parameterLength) - 1; + buffer[i] += (int) (sum >> shift); + } + } - int sample = startOffset; + private static void readResidual(BitStreamReader reader, int order, int[] buffer, int startOffset, int endOffset) throws IOException { + int method = reader.asInteger(2); - for (int partition = 0; partition < partitions; partition++) { - int parameter = reader.asInteger(parameterLength); - int value = (partitionOrder == 0 || partition > 0) ? 0 : order; + if (method > 1) { + throw new RuntimeException("Invalid residual coding method " + method); + } - if (parameter < parameterMaximum) { - value = partitionSamples - value; - readResidualBlock(reader, buffer, sample, sample + value, parameter); - sample += value; - } else { - parameter = reader.asInteger(5); + int partitionOrder = reader.asInteger(4); + int partitions = 1 << partitionOrder; + int partitionSamples = partitionOrder > 0 ? endOffset >> partitionOrder : endOffset - order; + int parameterLength = method == 0 ? 4 : 5; + int parameterMaximum = (1 << parameterLength) - 1; + + int sample = startOffset; + + for (int partition = 0; partition < partitions; partition++) { + int parameter = reader.asInteger(parameterLength); + int value = (partitionOrder == 0 || partition > 0) ? 0 : order; + + if (parameter < parameterMaximum) { + value = partitionSamples - value; + readResidualBlock(reader, buffer, sample, sample + value, parameter); + sample += value; + } else { + parameter = reader.asInteger(5); + + for (int i = value; i < partitionSamples; i++, sample++) { + buffer[sample] = reader.asSignedInteger(parameter); + } + } + } + } - for (int i = value ; i < partitionSamples; i++, sample++) { - buffer[sample] = reader.asSignedInteger(parameter); + private static void readResidualBlock(BitStreamReader reader, int[] buffer, int offset, int endOffset, int parameter) throws IOException { + for (int i = offset; i < endOffset; i++) { + int lowOrderSigned = (reader.readAllZeroes() << parameter) | reader.asInteger(parameter); + buffer[i] = (lowOrderSigned & 1) == 0 ? lowOrderSigned >> 1 : -(lowOrderSigned >> 1) - 1; } - } } - } - private static void readResidualBlock(BitStreamReader reader, int[] buffer, int offset, int endOffset, int parameter) throws IOException { - for (int i = offset; i < endOffset; i++) { - int lowOrderSigned = (reader.readAllZeroes() << parameter) | reader.asInteger(parameter); - buffer[i] = (lowOrderSigned & 1) == 0 ? lowOrderSigned >> 1 : -(lowOrderSigned >> 1) - 1; + private enum Encoding { + CONSTANT, + VERBATIM, + FIXED, + LPC } - } - - private enum Encoding { - CONSTANT, - VERBATIM, - FIXED, - LPC - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaAacTrackConsumer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaAacTrackConsumer.java index dbe8ee02b..b21d3ab3a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaAacTrackConsumer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaAacTrackConsumer.java @@ -15,89 +15,89 @@ * Consumes AAC track data from a matroska file. */ public class MatroskaAacTrackConsumer implements MatroskaTrackConsumer { - private static final Logger log = LoggerFactory.getLogger(MpegAacTrackConsumer.class); - - private final MatroskaFileTrack track; - private final AacPacketRouter packetRouter; - - private ByteBuffer inputBuffer; - private boolean configured; - - /** - * @param context Configuration and output information for processing - * @param track The MP4 audio track descriptor - */ - public MatroskaAacTrackConsumer(AudioProcessingContext context, MatroskaFileTrack track) { - this.track = track; - this.packetRouter = new AacPacketRouter(context); - } - - @Override - public void initialise() { - log.debug("Initialising AAC track with expected frequency {} and channel count {}.", - track.audio.samplingFrequency, track.audio.channels); - } - - @Override - public MatroskaFileTrack getTrack() { - return track; - } - - @Override - public void seekPerformed(long requestedTimecode, long providedTimecode) { - packetRouter.seekPerformed(requestedTimecode, providedTimecode); - } - - @Override - public void flush() throws InterruptedException { - packetRouter.flush(); - } - - @Override - public void consume(ByteBuffer data) throws InterruptedException { - if (packetRouter.nativeDecoder == null) { - packetRouter.nativeDecoder = new AacDecoder(); - configured = configureDecoder(packetRouter.nativeDecoder); + private static final Logger log = LoggerFactory.getLogger(MpegAacTrackConsumer.class); + + private final MatroskaFileTrack track; + private final AacPacketRouter packetRouter; + + private ByteBuffer inputBuffer; + private boolean configured; + + /** + * @param context Configuration and output information for processing + * @param track The MP4 audio track descriptor + */ + public MatroskaAacTrackConsumer(AudioProcessingContext context, MatroskaFileTrack track) { + this.track = track; + this.packetRouter = new AacPacketRouter(context); } - if (configured) { - if (inputBuffer == null) { - inputBuffer = ByteBuffer.allocateDirect(4096); - } + @Override + public void initialise() { + log.debug("Initialising AAC track with expected frequency {} and channel count {}.", + track.audio.samplingFrequency, track.audio.channels); + } - processInput(data); - } else { - if (packetRouter.embeddedDecoder == null) { - packetRouter.embeddedDecoder = Decoder.create(track.codecPrivate); - inputBuffer = ByteBuffer.allocate(4096); - } + @Override + public MatroskaFileTrack getTrack() { + return track; + } - processInput(data); + @Override + public void seekPerformed(long requestedTimecode, long providedTimecode) { + packetRouter.seekPerformed(requestedTimecode, providedTimecode); } - } - private void processInput(ByteBuffer data) throws InterruptedException { - while (data.hasRemaining()) { - int chunk = Math.min(data.remaining(), inputBuffer.capacity()); - ByteBuffer chunkBuffer = data.duplicate(); - chunkBuffer.limit(chunkBuffer.position() + chunk); + @Override + public void flush() throws InterruptedException { + packetRouter.flush(); + } - inputBuffer.clear(); - inputBuffer.put(chunkBuffer); - inputBuffer.flip(); + @Override + public void consume(ByteBuffer data) throws InterruptedException { + if (packetRouter.nativeDecoder == null) { + packetRouter.nativeDecoder = new AacDecoder(); + configured = configureDecoder(packetRouter.nativeDecoder); + } + + if (configured) { + if (inputBuffer == null) { + inputBuffer = ByteBuffer.allocateDirect(4096); + } + + processInput(data); + } else { + if (packetRouter.embeddedDecoder == null) { + packetRouter.embeddedDecoder = Decoder.create(track.codecPrivate); + inputBuffer = ByteBuffer.allocate(4096); + } + + processInput(data); + } + } + + private void processInput(ByteBuffer data) throws InterruptedException { + while (data.hasRemaining()) { + int chunk = Math.min(data.remaining(), inputBuffer.capacity()); + ByteBuffer chunkBuffer = data.duplicate(); + chunkBuffer.limit(chunkBuffer.position() + chunk); + + inputBuffer.clear(); + inputBuffer.put(chunkBuffer); + inputBuffer.flip(); - packetRouter.processInput(inputBuffer); + packetRouter.processInput(inputBuffer); - data.position(chunkBuffer.position()); + data.position(chunkBuffer.position()); + } } - } - @Override - public void close() { - packetRouter.close(); - } + @Override + public void close() { + packetRouter.close(); + } - private boolean configureDecoder(AacDecoder decoder) { - return (decoder.configure(track.codecPrivate) == 0); - } + private boolean configureDecoder(AacDecoder decoder) { + return (decoder.configure(track.codecPrivate) == 0); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaAudioTrack.java index ad0b5a77b..4d7257365 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaAudioTrack.java @@ -16,88 +16,88 @@ * Audio track that handles the processing of MKV and WEBM formats */ public class MatroskaAudioTrack extends BaseAudioTrack { - private static final Logger log = LoggerFactory.getLogger(MatroskaAudioTrack.class); - - private final SeekableInputStream inputStream; - - /** - * @param trackInfo Track info - * @param inputStream Input stream for the file - */ - public MatroskaAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - super(trackInfo); - - this.inputStream = inputStream; - } - - @Override - public void process(LocalAudioTrackExecutor localExecutor) { - MatroskaStreamingFile file = loadMatroskaFile(); - MatroskaTrackConsumer trackConsumer = loadAudioTrack(file, localExecutor.getProcessingContext()); - - try { - localExecutor.executeProcessingLoop(() -> { - file.provideFrames(trackConsumer); - }, position -> { - file.seekToTimecode(trackConsumer.getTrack().index, position); - }); - } finally { - ExceptionTools.closeWithWarnings(trackConsumer); - } - } + private static final Logger log = LoggerFactory.getLogger(MatroskaAudioTrack.class); + + private final SeekableInputStream inputStream; + + /** + * @param trackInfo Track info + * @param inputStream Input stream for the file + */ + public MatroskaAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + super(trackInfo); - private MatroskaStreamingFile loadMatroskaFile() { - try { - MatroskaStreamingFile file = new MatroskaStreamingFile(inputStream); - file.readFile(); + this.inputStream = inputStream; + } - accurateDuration.set((int) file.getDuration()); - return file; - } catch (IOException e) { - throw new RuntimeException(e); + @Override + public void process(LocalAudioTrackExecutor localExecutor) { + MatroskaStreamingFile file = loadMatroskaFile(); + MatroskaTrackConsumer trackConsumer = loadAudioTrack(file, localExecutor.getProcessingContext()); + + try { + localExecutor.executeProcessingLoop(() -> { + file.provideFrames(trackConsumer); + }, position -> { + file.seekToTimecode(trackConsumer.getTrack().index, position); + }); + } finally { + ExceptionTools.closeWithWarnings(trackConsumer); + } } - } - - private MatroskaTrackConsumer loadAudioTrack(MatroskaStreamingFile file, AudioProcessingContext context) { - MatroskaTrackConsumer trackConsumer = null; - boolean success = false; - - try { - trackConsumer = selectAudioTrack(file.getTrackList(), context); - - if (trackConsumer == null) { - throw new IllegalStateException("No supported audio tracks in the file."); - } else { - log.debug("Starting to play track with codec {}", trackConsumer.getTrack().codecId); - } - - trackConsumer.initialise(); - success = true; - } finally { - if (!success && trackConsumer != null) { - ExceptionTools.closeWithWarnings(trackConsumer); - } + + private MatroskaStreamingFile loadMatroskaFile() { + try { + MatroskaStreamingFile file = new MatroskaStreamingFile(inputStream); + file.readFile(); + + accurateDuration.set((int) file.getDuration()); + return file; + } catch (IOException e) { + throw new RuntimeException(e); + } } - return trackConsumer; - } - - private MatroskaTrackConsumer selectAudioTrack(MatroskaFileTrack[] tracks, AudioProcessingContext context) { - MatroskaTrackConsumer trackConsumer = null; - - for (MatroskaFileTrack track : tracks) { - if (track.type == MatroskaFileTrack.Type.AUDIO) { - if (MatroskaContainerProbe.OPUS_CODEC.equals(track.codecId)) { - trackConsumer = new MatroskaOpusTrackConsumer(context, track); - break; - } else if (MatroskaContainerProbe.VORBIS_CODEC.equals(track.codecId)) { - trackConsumer = new MatroskaVorbisTrackConsumer(context, track); - } else if (MatroskaContainerProbe.AAC_CODEC.equals(track.codecId)) { - trackConsumer = new MatroskaAacTrackConsumer(context, track); + private MatroskaTrackConsumer loadAudioTrack(MatroskaStreamingFile file, AudioProcessingContext context) { + MatroskaTrackConsumer trackConsumer = null; + boolean success = false; + + try { + trackConsumer = selectAudioTrack(file.getTrackList(), context); + + if (trackConsumer == null) { + throw new IllegalStateException("No supported audio tracks in the file."); + } else { + log.debug("Starting to play track with codec {}", trackConsumer.getTrack().codecId); + } + + trackConsumer.initialise(); + success = true; + } finally { + if (!success && trackConsumer != null) { + ExceptionTools.closeWithWarnings(trackConsumer); + } } - } + + return trackConsumer; } - return trackConsumer; - } + private MatroskaTrackConsumer selectAudioTrack(MatroskaFileTrack[] tracks, AudioProcessingContext context) { + MatroskaTrackConsumer trackConsumer = null; + + for (MatroskaFileTrack track : tracks) { + if (track.type == MatroskaFileTrack.Type.AUDIO) { + if (MatroskaContainerProbe.OPUS_CODEC.equals(track.codecId)) { + trackConsumer = new MatroskaOpusTrackConsumer(context, track); + break; + } else if (MatroskaContainerProbe.VORBIS_CODEC.equals(track.codecId)) { + trackConsumer = new MatroskaVorbisTrackConsumer(context, track); + } else if (MatroskaContainerProbe.AAC_CODEC.equals(track.codecId)) { + trackConsumer = new MatroskaAacTrackConsumer(context, track); + } + } + } + + return trackConsumer; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaContainerProbe.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaContainerProbe.java index e4c616e80..776b7d7bb 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaContainerProbe.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaContainerProbe.java @@ -25,56 +25,56 @@ * Container detection probe for matroska format. */ public class MatroskaContainerProbe implements MediaContainerProbe { - private static final Logger log = LoggerFactory.getLogger(MatroskaContainerProbe.class); + private static final Logger log = LoggerFactory.getLogger(MatroskaContainerProbe.class); - public static final String OPUS_CODEC = "A_OPUS"; - public static final String VORBIS_CODEC = "A_VORBIS"; - public static final String AAC_CODEC = "A_AAC"; + public static final String OPUS_CODEC = "A_OPUS"; + public static final String VORBIS_CODEC = "A_VORBIS"; + public static final String AAC_CODEC = "A_AAC"; - private static final int[] EBML_TAG = new int[] { 0x1A, 0x45, 0xDF, 0xA3 }; - private static final List supportedCodecs = Arrays.asList(OPUS_CODEC, VORBIS_CODEC, AAC_CODEC); + private static final int[] EBML_TAG = new int[]{0x1A, 0x45, 0xDF, 0xA3}; + private static final List supportedCodecs = Arrays.asList(OPUS_CODEC, VORBIS_CODEC, AAC_CODEC); - @Override - public String getName() { - return "matroska/webm"; - } - - @Override - public boolean matchesHints(MediaContainerHints hints) { - return false; - } + @Override + public String getName() { + return "matroska/webm"; + } - @Override - public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { - if (!checkNextBytes(inputStream, EBML_TAG)) { - return null; + @Override + public boolean matchesHints(MediaContainerHints hints) { + return false; } - log.debug("Track {} is a matroska file.", reference.identifier); + @Override + public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { + if (!checkNextBytes(inputStream, EBML_TAG)) { + return null; + } - MatroskaStreamingFile file = new MatroskaStreamingFile(inputStream); - file.readFile(); + log.debug("Track {} is a matroska file.", reference.identifier); - if (!hasSupportedAudioTrack(file)) { - return unsupportedFormat(this, "No supported audio tracks present in the file."); - } + MatroskaStreamingFile file = new MatroskaStreamingFile(inputStream); + file.readFile(); - return supportedFormat(this, null, new AudioTrackInfo(UNKNOWN_TITLE, UNKNOWN_ARTIST, - (long) file.getDuration(), reference.identifier, false, reference.identifier, null, null)); - } + if (!hasSupportedAudioTrack(file)) { + return unsupportedFormat(this, "No supported audio tracks present in the file."); + } - private boolean hasSupportedAudioTrack(MatroskaStreamingFile file) { - for (MatroskaFileTrack track : file.getTrackList()) { - if (track.type == MatroskaFileTrack.Type.AUDIO && supportedCodecs.contains(track.codecId)) { - return true; - } + return supportedFormat(this, null, new AudioTrackInfo(UNKNOWN_TITLE, UNKNOWN_ARTIST, + (long) file.getDuration(), reference.identifier, false, reference.identifier, null, null)); } - return false; - } + private boolean hasSupportedAudioTrack(MatroskaStreamingFile file) { + for (MatroskaFileTrack track : file.getTrackList()) { + if (track.type == MatroskaFileTrack.Type.AUDIO && supportedCodecs.contains(track.codecId)) { + return true; + } + } - @Override - public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - return new MatroskaAudioTrack(trackInfo, inputStream); - } + return false; + } + + @Override + public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + return new MatroskaAudioTrack(trackInfo, inputStream); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaOpusTrackConsumer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaOpusTrackConsumer.java index 80c599119..cfb2870ef 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaOpusTrackConsumer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaOpusTrackConsumer.java @@ -10,45 +10,45 @@ * Consumes OPUS track data from a matroska file. */ public class MatroskaOpusTrackConsumer implements MatroskaTrackConsumer { - private final MatroskaFileTrack track; - private final OpusPacketRouter opusPacketRouter; - - /** - * @param context Configuration and output information for processing - * @param track The associated matroska track - */ - public MatroskaOpusTrackConsumer(AudioProcessingContext context, MatroskaFileTrack track) { - this.track = track; - this.opusPacketRouter = new OpusPacketRouter(context, (int) track.audio.samplingFrequency, track.audio.channels); - } - - @Override - public MatroskaFileTrack getTrack() { - return track; - } - - @Override - public void initialise() { - // Nothing to do here - } - - @Override - public void seekPerformed(long requestedTimecode, long providedTimecode) { - opusPacketRouter.seekPerformed(requestedTimecode, providedTimecode); - } - - @Override - public void flush() throws InterruptedException { - opusPacketRouter.flush(); - } - - @Override - public void consume(ByteBuffer data) throws InterruptedException { - opusPacketRouter.process(data); - } - - @Override - public void close() { - opusPacketRouter.close(); - } + private final MatroskaFileTrack track; + private final OpusPacketRouter opusPacketRouter; + + /** + * @param context Configuration and output information for processing + * @param track The associated matroska track + */ + public MatroskaOpusTrackConsumer(AudioProcessingContext context, MatroskaFileTrack track) { + this.track = track; + this.opusPacketRouter = new OpusPacketRouter(context, (int) track.audio.samplingFrequency, track.audio.channels); + } + + @Override + public MatroskaFileTrack getTrack() { + return track; + } + + @Override + public void initialise() { + // Nothing to do here + } + + @Override + public void seekPerformed(long requestedTimecode, long providedTimecode) { + opusPacketRouter.seekPerformed(requestedTimecode, providedTimecode); + } + + @Override + public void flush() throws InterruptedException { + opusPacketRouter.flush(); + } + + @Override + public void consume(ByteBuffer data) throws InterruptedException { + opusPacketRouter.process(data); + } + + @Override + public void close() { + opusPacketRouter.close(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaStreamingFile.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaStreamingFile.java index 8a89be173..e9b555b3b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaStreamingFile.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaStreamingFile.java @@ -1,402 +1,398 @@ package com.sedmelluq.discord.lavaplayer.container.matroska; +import com.sedmelluq.discord.lavaplayer.container.matroska.format.*; +import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; + import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import com.sedmelluq.discord.lavaplayer.container.matroska.format.MatroskaBlock; -import com.sedmelluq.discord.lavaplayer.container.matroska.format.MatroskaCuePoint; -import com.sedmelluq.discord.lavaplayer.container.matroska.format.MatroskaElement; -import com.sedmelluq.discord.lavaplayer.container.matroska.format.MatroskaElementType; -import com.sedmelluq.discord.lavaplayer.container.matroska.format.MatroskaFileReader; -import com.sedmelluq.discord.lavaplayer.container.matroska.format.MatroskaFileTrack; -import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; - /** * Handles processing an MKV/WEBM file for the purpose of streaming one specific track from it. Only performs seeks when * absolutely necessary, as the stream may be a network connection, in which case each seek may require a new connection. */ public class MatroskaStreamingFile { - private final MatroskaFileReader reader; - - private long timecodeScale = 1000000; - private double duration; - private final ArrayList trackList = new ArrayList<>(); - private MatroskaElement segmentElement = null; - private MatroskaElement firstClusterElement = null; - - private long minimumTimecode = 0; - private boolean seeking = false; - - private Long cueElementPosition = null; - private List cuePoints = null; - - /** - * @param inputStream The input stream for the file - */ - public MatroskaStreamingFile(SeekableInputStream inputStream) { - this.reader = new MatroskaFileReader(inputStream); - } - - /** - * @return Timescale for the durations used in this file - */ - public long getTimecodeScale() { - return timecodeScale; - } - - /** - * @return Total duration of the file - */ - public double getDuration() { - return duration; - } - - /** - * @return Array of tracks in this file - */ - public MatroskaFileTrack[] getTrackList() { - if (!trackList.isEmpty()) { - MatroskaFileTrack[] tracks = new MatroskaFileTrack[trackList.size()]; - - for (int t = 0; t < trackList.size(); t++) { - tracks[t] = trackList.get(t); - } - return tracks; - } else { - return new MatroskaFileTrack[0]; - } - } - - /** - * Read the headers and tracks from the file. - * - * @throws IOException On read error. - */ - public void readFile() throws IOException { - MatroskaElement ebmlElement = reader.readNextElement(null); - if (ebmlElement == null) { - throw new RuntimeException("Unable to scan for EBML elements"); - } + private final MatroskaFileReader reader; + + private long timecodeScale = 1000000; + private double duration; + private final ArrayList trackList = new ArrayList<>(); + private MatroskaElement segmentElement = null; + private MatroskaElement firstClusterElement = null; - if (ebmlElement.is(MatroskaElementType.Ebml)) { - parseEbmlElement(ebmlElement); - } else { - throw new RuntimeException("EBML Header not the first element in the file"); + private long minimumTimecode = 0; + private boolean seeking = false; + + private Long cueElementPosition = null; + private List cuePoints = null; + + /** + * @param inputStream The input stream for the file + */ + public MatroskaStreamingFile(SeekableInputStream inputStream) { + this.reader = new MatroskaFileReader(inputStream); } - segmentElement = reader.readNextElement(null).frozen(); + /** + * @return Timescale for the durations used in this file + */ + public long getTimecodeScale() { + return timecodeScale; + } - if (segmentElement.is(MatroskaElementType.Segment)) { - parseSegmentElement(segmentElement); + /** + * @return Total duration of the file + */ + public double getDuration() { + return duration; } - else { - throw new RuntimeException(String.format("Segment not the second element in the file: was %s (%d) instead", - segmentElement.getType().name(), segmentElement.getId())); + + /** + * @return Array of tracks in this file + */ + public MatroskaFileTrack[] getTrackList() { + if (!trackList.isEmpty()) { + MatroskaFileTrack[] tracks = new MatroskaFileTrack[trackList.size()]; + + for (int t = 0; t < trackList.size(); t++) { + tracks[t] = trackList.get(t); + } + return tracks; + } else { + return new MatroskaFileTrack[0]; + } } - } - private void parseEbmlElement(MatroskaElement ebmlElement) throws IOException { - MatroskaElement child; + /** + * Read the headers and tracks from the file. + * + * @throws IOException On read error. + */ + public void readFile() throws IOException { + MatroskaElement ebmlElement = reader.readNextElement(null); + if (ebmlElement == null) { + throw new RuntimeException("Unable to scan for EBML elements"); + } - while ((child = reader.readNextElement(ebmlElement)) != null) { - if (child.is(MatroskaElementType.DocType)) { - String docType = reader.asString(child); + if (ebmlElement.is(MatroskaElementType.Ebml)) { + parseEbmlElement(ebmlElement); + } else { + throw new RuntimeException("EBML Header not the first element in the file"); + } + + segmentElement = reader.readNextElement(null).frozen(); - if (docType.compareTo("matroska") != 0 && docType.compareTo("webm") != 0) { - throw new RuntimeException("Error: DocType is not matroska, \"" + docType + "\""); + if (segmentElement.is(MatroskaElementType.Segment)) { + parseSegmentElement(segmentElement); + } else { + throw new RuntimeException(String.format("Segment not the second element in the file: was %s (%d) instead", + segmentElement.getType().name(), segmentElement.getId())); } - } + } + + private void parseEbmlElement(MatroskaElement ebmlElement) throws IOException { + MatroskaElement child; + + while ((child = reader.readNextElement(ebmlElement)) != null) { + if (child.is(MatroskaElementType.DocType)) { + String docType = reader.asString(child); + + if (docType.compareTo("matroska") != 0 && docType.compareTo("webm") != 0) { + throw new RuntimeException("Error: DocType is not matroska, \"" + docType + "\""); + } + } - reader.skip(child); + reader.skip(child); + } } - } - - private void parseSegmentElement(MatroskaElement segmentElement) throws IOException { - MatroskaElement child; - - while ((child = reader.readNextElement(segmentElement)) != null) { - if (child.is(MatroskaElementType.Info)) { - parseSegmentInfo(child); - } else if (child.is(MatroskaElementType.Tracks)) { - parseTracks(child); - } else if (child.is(MatroskaElementType.Cluster)) { - firstClusterElement = child.frozen(); - break; - } else if (child.is(MatroskaElementType.SeekHead)) { - parseSeekInfoForCuePosition(child); - } else if (child.is(MatroskaElementType.Cues)) { - cuePoints = parseCues(child); - } - - reader.skip(child); + + private void parseSegmentElement(MatroskaElement segmentElement) throws IOException { + MatroskaElement child; + + while ((child = reader.readNextElement(segmentElement)) != null) { + if (child.is(MatroskaElementType.Info)) { + parseSegmentInfo(child); + } else if (child.is(MatroskaElementType.Tracks)) { + parseTracks(child); + } else if (child.is(MatroskaElementType.Cluster)) { + firstClusterElement = child.frozen(); + break; + } else if (child.is(MatroskaElementType.SeekHead)) { + parseSeekInfoForCuePosition(child); + } else if (child.is(MatroskaElementType.Cues)) { + cuePoints = parseCues(child); + } + + reader.skip(child); + } } - } - private void parseSeekInfoForCuePosition(MatroskaElement seekHeadElement) throws IOException { - MatroskaElement child; + private void parseSeekInfoForCuePosition(MatroskaElement seekHeadElement) throws IOException { + MatroskaElement child; - while ((child = reader.readNextElement(seekHeadElement)) != null) { - if (child.is(MatroskaElementType.Seek)) { - parseSeekElement(child); - } + while ((child = reader.readNextElement(seekHeadElement)) != null) { + if (child.is(MatroskaElementType.Seek)) { + parseSeekElement(child); + } - reader.skip(child); + reader.skip(child); + } } - } - private void parseSeekElement(MatroskaElement seekElement) throws IOException { - MatroskaElement child; - boolean isCueElement = false; + private void parseSeekElement(MatroskaElement seekElement) throws IOException { + MatroskaElement child; + boolean isCueElement = false; - while ((child = reader.readNextElement(seekElement)) != null) { - if (child.is(MatroskaElementType.SeekId)) { - isCueElement = ByteBuffer.wrap(reader.asBytes(child)).equals(ByteBuffer.wrap(MatroskaElementType.Cues.bytes)); - } else if (child.is(MatroskaElementType.SeekPosition) && isCueElement) { - cueElementPosition = reader.asLong(child); - } + while ((child = reader.readNextElement(seekElement)) != null) { + if (child.is(MatroskaElementType.SeekId)) { + isCueElement = ByteBuffer.wrap(reader.asBytes(child)).equals(ByteBuffer.wrap(MatroskaElementType.Cues.bytes)); + } else if (child.is(MatroskaElementType.SeekPosition) && isCueElement) { + cueElementPosition = reader.asLong(child); + } - reader.skip(child); + reader.skip(child); + } } - } - private List parseCues(MatroskaElement cuesElement) throws IOException { - List parsedCuePoints = new ArrayList<>(); - MatroskaElement child; + private List parseCues(MatroskaElement cuesElement) throws IOException { + List parsedCuePoints = new ArrayList<>(); + MatroskaElement child; + + while ((child = reader.readNextElement(cuesElement)) != null) { + if (child.is(MatroskaElementType.CuePoint)) { + MatroskaCuePoint cuePoint = parseCuePoint(child); - while ((child = reader.readNextElement(cuesElement)) != null) { - if (child.is(MatroskaElementType.CuePoint)) { - MatroskaCuePoint cuePoint = parseCuePoint(child); + if (cuePoint != null) { + parsedCuePoints.add(cuePoint); + } + } - if (cuePoint != null) { - parsedCuePoints.add(cuePoint); + reader.skip(child); } - } - reader.skip(child); + return parsedCuePoints.isEmpty() ? null : parsedCuePoints; } - return parsedCuePoints.isEmpty() ? null : parsedCuePoints; - } + private MatroskaCuePoint parseCuePoint(MatroskaElement cuePointElement) throws IOException { + MatroskaElement child; - private MatroskaCuePoint parseCuePoint(MatroskaElement cuePointElement) throws IOException { - MatroskaElement child; + Long cueTime = null; + long[] positions = null; - Long cueTime = null; - long[] positions = null; + while ((child = reader.readNextElement(cuePointElement)) != null) { + if (child.is(MatroskaElementType.CueTime)) { + cueTime = reader.asLong(child); + } else if (child.is(MatroskaElementType.CueTrackPositions)) { + positions = parseCueTrackPositions(child); + } - while ((child = reader.readNextElement(cuePointElement)) != null) { - if (child.is(MatroskaElementType.CueTime)) { - cueTime = reader.asLong(child); - } else if (child.is(MatroskaElementType.CueTrackPositions)) { - positions = parseCueTrackPositions(child); - } + reader.skip(child); + } - reader.skip(child); + if (cueTime != null && positions != null) { + return new MatroskaCuePoint(cueTime, positions); + } else { + return null; + } } - if (cueTime != null && positions != null) { - return new MatroskaCuePoint(cueTime, positions); - } else { - return null; - } - } + private long[] parseCueTrackPositions(MatroskaElement positionsElement) throws IOException { + Long currentTrackId = null; + MatroskaElement child; - private long[] parseCueTrackPositions(MatroskaElement positionsElement) throws IOException { - Long currentTrackId = null; - MatroskaElement child; + long[] positions = new long[trackList.size() + 1]; + Arrays.fill(positions, -1); - long[] positions = new long[trackList.size() + 1]; - Arrays.fill(positions, -1); + while ((child = reader.readNextElement(positionsElement)) != null) { + if (child.is(MatroskaElementType.CueTrack)) { + currentTrackId = reader.asLong(child); + } else if (child.is(MatroskaElementType.CueClusterPosition) && currentTrackId != null) { + positions[currentTrackId.intValue()] = reader.asLong(child); + } - while ((child = reader.readNextElement(positionsElement)) != null) { - if (child.is(MatroskaElementType.CueTrack)) { - currentTrackId = reader.asLong(child); - } else if (child.is(MatroskaElementType.CueClusterPosition) && currentTrackId != null) { - positions[currentTrackId.intValue()] = reader.asLong(child); - } + reader.skip(child); + } - reader.skip(child); + return positions; } - return positions; - } - - /** - * Perform a seek to a specified timecode - * @param trackId ID of the reference track - * @param timecode Timecode using the timescale of the file - */ - public void seekToTimecode(int trackId, long timecode) { - try { - seekToTimecodeInternal(trackId, timecode); - } catch (IOException e) { - throw new RuntimeException(e); + /** + * Perform a seek to a specified timecode + * + * @param trackId ID of the reference track + * @param timecode Timecode using the timescale of the file + */ + public void seekToTimecode(int trackId, long timecode) { + try { + seekToTimecodeInternal(trackId, timecode); + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - private void seekToTimecodeInternal(int trackId, long timecode) throws IOException { - minimumTimecode = timecode; - seeking = true; + private void seekToTimecodeInternal(int trackId, long timecode) throws IOException { + minimumTimecode = timecode; + seeking = true; - if (cuePoints == null && cueElementPosition != null) { - reader.seek(segmentElement.getDataPosition() + cueElementPosition); + if (cuePoints == null && cueElementPosition != null) { + reader.seek(segmentElement.getDataPosition() + cueElementPosition); - MatroskaElement cuesElement = reader.readNextElement(segmentElement); - if (!cuesElement.is(MatroskaElementType.Cues)) { - throw new IllegalStateException("The element here should be cue."); - } + MatroskaElement cuesElement = reader.readNextElement(segmentElement); + if (!cuesElement.is(MatroskaElementType.Cues)) { + throw new IllegalStateException("The element here should be cue."); + } - cuePoints = parseCues(cuesElement); - } + cuePoints = parseCues(cuesElement); + } + + if (cuePoints != null) { + MatroskaCuePoint cuePoint = lastCueNotAfterTimecode(timecode); - if (cuePoints != null) { - MatroskaCuePoint cuePoint = lastCueNotAfterTimecode(timecode); + if (cuePoint != null && cuePoint.trackClusterOffsets[trackId] >= 0) { + reader.seek(segmentElement.getDataPosition() + cuePoint.trackClusterOffsets[trackId]); + return; + } + } - if (cuePoint != null && cuePoint.trackClusterOffsets[trackId] >= 0) { - reader.seek(segmentElement.getDataPosition() + cuePoint.trackClusterOffsets[trackId]); - return; - } + // If there were no cues available, just seek to the beginning and discard until the right timecode + reader.seek(firstClusterElement.getPosition()); } - // If there were no cues available, just seek to the beginning and discard until the right timecode - reader.seek(firstClusterElement.getPosition()); - } + private MatroskaCuePoint lastCueNotAfterTimecode(long timecode) { + int largerTimecodeIndex; - private MatroskaCuePoint lastCueNotAfterTimecode(long timecode) { - int largerTimecodeIndex; + for (largerTimecodeIndex = 0; largerTimecodeIndex < cuePoints.size(); largerTimecodeIndex++) { + if (cuePoints.get(largerTimecodeIndex).timecode > timecode) { + break; + } + } - for (largerTimecodeIndex = 0; largerTimecodeIndex < cuePoints.size(); largerTimecodeIndex++) { - if (cuePoints.get(largerTimecodeIndex).timecode > timecode) { - break; - } + if (largerTimecodeIndex > 0) { + return cuePoints.get(largerTimecodeIndex - 1); + } else { + return null; + } } - if (largerTimecodeIndex > 0) { - return cuePoints.get(largerTimecodeIndex - 1); - } else { - return null; - } - } - - /** - * Provide data chunks for the specified track consumer - * @param consumer Track data consumer - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - public void provideFrames(MatroskaTrackConsumer consumer) throws InterruptedException { - try { - long position = reader.getPosition(); - MatroskaElement child = position == firstClusterElement.getDataPosition() - ? firstClusterElement : reader.readNextElement(segmentElement); - - while (child != null) { - if (child.is(MatroskaElementType.Cluster)) { - parseNextCluster(child, consumer); + /** + * Provide data chunks for the specified track consumer + * + * @param consumer Track data consumer + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + public void provideFrames(MatroskaTrackConsumer consumer) throws InterruptedException { + try { + long position = reader.getPosition(); + MatroskaElement child = position == firstClusterElement.getDataPosition() + ? firstClusterElement : reader.readNextElement(segmentElement); + + while (child != null) { + if (child.is(MatroskaElementType.Cluster)) { + parseNextCluster(child, consumer); + } + + reader.skip(child); + + if (segmentElement.getRemaining(reader.getPosition()) <= 0) { + break; + } + + child = reader.readNextElement(segmentElement); + } + } catch (IOException e) { + throw new RuntimeException(e); } + } - reader.skip(child); + private void parseNextCluster(MatroskaElement clusterElement, MatroskaTrackConsumer consumer) throws InterruptedException, IOException { + MatroskaElement child; + long clusterTimecode = 0; - if (segmentElement.getRemaining(reader.getPosition()) <= 0) { - break; - } + while ((child = reader.readNextElement(clusterElement)) != null) { + if (child.is(MatroskaElementType.Timecode)) { + clusterTimecode = reader.asLong(child); + } else if (child.is(MatroskaElementType.SimpleBlock)) { + parseClusterSimpleBlock(child, consumer, clusterTimecode); + } else if (child.is(MatroskaElementType.BlockGroup)) { + parseClusterBlockGroup(child, consumer, clusterTimecode); + } - child = reader.readNextElement(segmentElement); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private void parseNextCluster(MatroskaElement clusterElement, MatroskaTrackConsumer consumer) throws InterruptedException, IOException { - MatroskaElement child; - long clusterTimecode = 0; - - while ((child = reader.readNextElement(clusterElement)) != null) { - if (child.is(MatroskaElementType.Timecode)) { - clusterTimecode = reader.asLong(child); - } else if (child.is(MatroskaElementType.SimpleBlock)) { - parseClusterSimpleBlock(child, consumer, clusterTimecode); - } else if (child.is(MatroskaElementType.BlockGroup)) { - parseClusterBlockGroup(child, consumer, clusterTimecode); - } - - reader.skip(child); + reader.skip(child); + } } - } - private void parseClusterSimpleBlock(MatroskaElement simpleBlock, MatroskaTrackConsumer consumer, long clusterTimecode) - throws InterruptedException, IOException { + private void parseClusterSimpleBlock(MatroskaElement simpleBlock, MatroskaTrackConsumer consumer, long clusterTimecode) + throws InterruptedException, IOException { - MatroskaBlock block = reader.readBlockHeader(simpleBlock, consumer.getTrack().index); + MatroskaBlock block = reader.readBlockHeader(simpleBlock, consumer.getTrack().index); - if (block != null) { - processFrameInBlock(block, consumer, clusterTimecode); + if (block != null) { + processFrameInBlock(block, consumer, clusterTimecode); + } } - } - private void parseClusterBlockGroup(MatroskaElement blockGroup, MatroskaTrackConsumer consumer, long clusterTimecode) - throws InterruptedException, IOException { + private void parseClusterBlockGroup(MatroskaElement blockGroup, MatroskaTrackConsumer consumer, long clusterTimecode) + throws InterruptedException, IOException { - MatroskaElement child; + MatroskaElement child; - while ((child = reader.readNextElement(blockGroup)) != null) { - if (child.is(MatroskaElementType.Block)) { - MatroskaBlock block = reader.readBlockHeader(child, consumer.getTrack().index); + while ((child = reader.readNextElement(blockGroup)) != null) { + if (child.is(MatroskaElementType.Block)) { + MatroskaBlock block = reader.readBlockHeader(child, consumer.getTrack().index); - if (block != null) { - processFrameInBlock(block, consumer, clusterTimecode); - } - } + if (block != null) { + processFrameInBlock(block, consumer, clusterTimecode); + } + } - reader.skip(child); + reader.skip(child); + } } - } - private void processFrameInBlock(MatroskaBlock block, MatroskaTrackConsumer consumer, long clusterTimecode) - throws InterruptedException, IOException { + private void processFrameInBlock(MatroskaBlock block, MatroskaTrackConsumer consumer, long clusterTimecode) + throws InterruptedException, IOException { - long timecode = clusterTimecode + block.getTimecode(); + long timecode = clusterTimecode + block.getTimecode(); - if (timecode >= minimumTimecode) { - int frameCount = block.getFrameCount(); + if (timecode >= minimumTimecode) { + int frameCount = block.getFrameCount(); - if (seeking) { - consumer.seekPerformed(minimumTimecode, timecode); - seeking = false; - } + if (seeking) { + consumer.seekPerformed(minimumTimecode, timecode); + seeking = false; + } - for (int i = 0; i < frameCount; i++) { - consumer.consume(block.getNextFrameBuffer(reader, i)); - } + for (int i = 0; i < frameCount; i++) { + consumer.consume(block.getNextFrameBuffer(reader, i)); + } + } } - } - private void parseSegmentInfo(MatroskaElement infoElement) throws IOException { - MatroskaElement child; + private void parseSegmentInfo(MatroskaElement infoElement) throws IOException { + MatroskaElement child; - while ((child = reader.readNextElement(infoElement)) != null) { - if (child.is(MatroskaElementType.Duration)) { - duration = reader.asDouble(child); - } else if (child.is(MatroskaElementType.TimecodeScale)) { - timecodeScale = reader.asLong(child); - } + while ((child = reader.readNextElement(infoElement)) != null) { + if (child.is(MatroskaElementType.Duration)) { + duration = reader.asDouble(child); + } else if (child.is(MatroskaElementType.TimecodeScale)) { + timecodeScale = reader.asLong(child); + } - reader.skip(child); + reader.skip(child); + } } - } - private void parseTracks(MatroskaElement tracksElement) throws IOException { - MatroskaElement child; + private void parseTracks(MatroskaElement tracksElement) throws IOException { + MatroskaElement child; - while ((child = reader.readNextElement(tracksElement)) != null) { - if (child.is(MatroskaElementType.TrackEntry)) { - trackList.add(MatroskaFileTrack.parse(child, reader)); - } + while ((child = reader.readNextElement(tracksElement)) != null) { + if (child.is(MatroskaElementType.TrackEntry)) { + trackList.add(MatroskaFileTrack.parse(child, reader)); + } - reader.skip(child); + reader.skip(child); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaTrackConsumer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaTrackConsumer.java index ebd46fb3a..0e8796199 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaTrackConsumer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaTrackConsumer.java @@ -8,42 +8,42 @@ * Consumer for the file frames of a specific matroska file track */ public interface MatroskaTrackConsumer extends AutoCloseable { - /** - * @return The associated matroska file track - */ - MatroskaFileTrack getTrack(); - - /** - * Initialise the consumer, called before first consume() - */ - void initialise(); - - /** - * Indicates that the next frame is not a direct continuation of the previous one - * - * @param requestedTimecode Timecode in milliseconds to which the seek was requested to - * @param providedTimecode Timecode in milliseconds to which the seek was actually performed to - */ - void seekPerformed(long requestedTimecode, long providedTimecode); - - /** - * Indicates that no more input will come, all remaining buffers should be flushed - * - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void flush() throws InterruptedException; - - /** - * Consume one frame from the track - * - * @param data The data of the frame - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void consume(ByteBuffer data) throws InterruptedException; - - /** - * Already flushed, no more input coming. Free all resources. - */ - @Override - void close() throws Exception; + /** + * @return The associated matroska file track + */ + MatroskaFileTrack getTrack(); + + /** + * Initialise the consumer, called before first consume() + */ + void initialise(); + + /** + * Indicates that the next frame is not a direct continuation of the previous one + * + * @param requestedTimecode Timecode in milliseconds to which the seek was requested to + * @param providedTimecode Timecode in milliseconds to which the seek was actually performed to + */ + void seekPerformed(long requestedTimecode, long providedTimecode); + + /** + * Indicates that no more input will come, all remaining buffers should be flushed + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void flush() throws InterruptedException; + + /** + * Consume one frame from the track + * + * @param data The data of the frame + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void consume(ByteBuffer data) throws InterruptedException; + + /** + * Already flushed, no more input coming. Free all resources. + */ + @Override + void close() throws Exception; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaVorbisTrackConsumer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaVorbisTrackConsumer.java index 147dedfa5..9a9c7b131 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaVorbisTrackConsumer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/MatroskaVorbisTrackConsumer.java @@ -14,148 +14,148 @@ * Consumes Vorbis track data from a matroska file. */ public class MatroskaVorbisTrackConsumer implements MatroskaTrackConsumer { - private static final int PCM_BUFFER_SIZE = 4096; - private static final int COPY_BUFFER_SIZE = 256; - - private final MatroskaFileTrack track; - private final VorbisDecoder decoder; - private final byte[] copyBuffer; - private final AudioPipeline downstream; - private ByteBuffer inputBuffer; - private float[][] channelPcmBuffers; + private static final int PCM_BUFFER_SIZE = 4096; + private static final int COPY_BUFFER_SIZE = 256; + + private final MatroskaFileTrack track; + private final VorbisDecoder decoder; + private final byte[] copyBuffer; + private final AudioPipeline downstream; + private ByteBuffer inputBuffer; + private float[][] channelPcmBuffers; + + /** + * @param context Configuration and output information for processing + * @param track The associated matroska track + */ + public MatroskaVorbisTrackConsumer(AudioProcessingContext context, MatroskaFileTrack track) { + + this.track = track; + this.decoder = new VorbisDecoder(); + this.copyBuffer = new byte[COPY_BUFFER_SIZE]; + + AudioDetails audioTrack = fillMissingDetails(track.audio, track.codecPrivate); + this.downstream = AudioPipelineFactory.create(context, + new PcmFormat(audioTrack.channels, (int) audioTrack.samplingFrequency)); + } - /** - * @param context Configuration and output information for processing - * @param track The associated matroska track - */ - public MatroskaVorbisTrackConsumer(AudioProcessingContext context, MatroskaFileTrack track) { + @Override + public MatroskaFileTrack getTrack() { + return track; + } - this.track = track; - this.decoder = new VorbisDecoder(); - this.copyBuffer = new byte[COPY_BUFFER_SIZE]; + @Override + public void initialise() { + ByteBuffer directPrivateData = ByteBuffer.allocateDirect(track.codecPrivate.length); - AudioDetails audioTrack = fillMissingDetails(track.audio, track.codecPrivate); - this.downstream = AudioPipelineFactory.create(context, - new PcmFormat(audioTrack.channels, (int) audioTrack.samplingFrequency)); - } + directPrivateData.put(track.codecPrivate); + directPrivateData.flip(); - @Override - public MatroskaFileTrack getTrack() { - return track; - } + try { + int lengthInfoSize = directPrivateData.get(); - @Override - public void initialise() { - ByteBuffer directPrivateData = ByteBuffer.allocateDirect(track.codecPrivate.length); + if (lengthInfoSize != 2) { + throw new IllegalStateException("Unexpected lacing count."); + } - directPrivateData.put(track.codecPrivate); - directPrivateData.flip(); + int firstHeaderSize = readLacingValue(directPrivateData); + int secondHeaderSize = readLacingValue(directPrivateData); - try { - int lengthInfoSize = directPrivateData.get(); + ByteBuffer infoBuffer = directPrivateData.duplicate(); + infoBuffer.limit(infoBuffer.position() + firstHeaderSize); - if (lengthInfoSize != 2) { - throw new IllegalStateException("Unexpected lacing count."); - } + directPrivateData.position(directPrivateData.position() + firstHeaderSize + secondHeaderSize); - int firstHeaderSize = readLacingValue(directPrivateData); - int secondHeaderSize = readLacingValue(directPrivateData); + decoder.initialise(infoBuffer, directPrivateData); - ByteBuffer infoBuffer = directPrivateData.duplicate(); - infoBuffer.limit(infoBuffer.position() + firstHeaderSize); + channelPcmBuffers = new float[decoder.getChannelCount()][]; - directPrivateData.position(directPrivateData.position() + firstHeaderSize + secondHeaderSize); + for (int i = 0; i < channelPcmBuffers.length; i++) { + channelPcmBuffers[i] = new float[PCM_BUFFER_SIZE]; + } + } catch (Exception e) { + throw new RuntimeException("Reading Vorbis header failed.", e); + } + } - decoder.initialise(infoBuffer, directPrivateData); + private static int readLacingValue(ByteBuffer buffer) { + int value = 0; + int current; - channelPcmBuffers = new float[decoder.getChannelCount()][]; + do { + current = buffer.get() & 0xFF; + value += current; + } while (current == 255); - for (int i = 0; i < channelPcmBuffers.length; i++) { - channelPcmBuffers[i] = new float[PCM_BUFFER_SIZE]; - } - } catch (Exception e) { - throw new RuntimeException("Reading Vorbis header failed.", e); + return value; } - } - private static int readLacingValue(ByteBuffer buffer) { - int value = 0; - int current; + private static AudioDetails fillMissingDetails(AudioDetails details, byte[] headers) { + if (details.channels != 0) { + return details; + } - do { - current = buffer.get() & 0xFF; - value += current; - } while (current == 255); + ByteBuffer buffer = ByteBuffer.wrap(headers); + readLacingValue(buffer); // first header size + readLacingValue(buffer); // second header size - return value; - } + buffer.getInt(); // vorbis version + int channelCount = buffer.get() & 0xFF; - private static AudioDetails fillMissingDetails(AudioDetails details, byte[] headers) { - if (details.channels != 0) { - return details; + return new AudioDetails(details.samplingFrequency, details.outputSamplingFrequency, channelCount, details.bitDepth); } - ByteBuffer buffer = ByteBuffer.wrap(headers); - readLacingValue(buffer); // first header size - readLacingValue(buffer); // second header size - - buffer.getInt(); // vorbis version - int channelCount = buffer.get() & 0xFF; - - return new AudioDetails(details.samplingFrequency, details.outputSamplingFrequency, channelCount, details.bitDepth); - } + @Override + public void seekPerformed(long requestedTimecode, long providedTimecode) { + downstream.seekPerformed(requestedTimecode, providedTimecode); + } - @Override - public void seekPerformed(long requestedTimecode, long providedTimecode) { - downstream.seekPerformed(requestedTimecode, providedTimecode); - } + @Override + public void flush() throws InterruptedException { + downstream.flush(); + } - @Override - public void flush() throws InterruptedException { - downstream.flush(); - } + private ByteBuffer getDirectBuffer(int size) { + if (inputBuffer == null || inputBuffer.capacity() < size) { + inputBuffer = ByteBuffer.allocateDirect(size * 3 / 2); + } - private ByteBuffer getDirectBuffer(int size) { - if (inputBuffer == null || inputBuffer.capacity() < size) { - inputBuffer = ByteBuffer.allocateDirect(size * 3 / 2); + inputBuffer.clear(); + return inputBuffer; } - inputBuffer.clear(); - return inputBuffer; - } + private ByteBuffer getAsDirectBuffer(ByteBuffer data) { + ByteBuffer buffer = getDirectBuffer(data.remaining()); - private ByteBuffer getAsDirectBuffer(ByteBuffer data) { - ByteBuffer buffer = getDirectBuffer(data.remaining()); + while (data.remaining() > 0) { + int chunk = Math.min(copyBuffer.length, data.remaining()); + data.get(copyBuffer, 0, chunk); + buffer.put(copyBuffer, 0, chunk); + } - while (data.remaining() > 0) { - int chunk = Math.min(copyBuffer.length, data.remaining()); - data.get(copyBuffer, 0, chunk); - buffer.put(copyBuffer, 0, chunk); + buffer.flip(); + return buffer; } - buffer.flip(); - return buffer; - } - - @Override - public void consume(ByteBuffer data) throws InterruptedException { - ByteBuffer directBuffer = getAsDirectBuffer(data); - decoder.input(directBuffer); + @Override + public void consume(ByteBuffer data) throws InterruptedException { + ByteBuffer directBuffer = getAsDirectBuffer(data); + decoder.input(directBuffer); - int output; + int output; - do { - output = decoder.output(channelPcmBuffers); + do { + output = decoder.output(channelPcmBuffers); - if (output > 0) { - downstream.process(channelPcmBuffers, 0, output); - } - } while (output == PCM_BUFFER_SIZE); - } + if (output > 0) { + downstream.process(channelPcmBuffers, 0, output); + } + } while (output == PCM_BUFFER_SIZE); + } - @Override - public void close() { - downstream.close(); - decoder.close(); - } + @Override + public void close() { + downstream.close(); + decoder.close(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaBlock.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaBlock.java index 2ee38393c..55089ec14 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaBlock.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaBlock.java @@ -7,34 +7,34 @@ * Provides information and buffer to read from for a Matroska block. */ public interface MatroskaBlock { - /** - * @return The timecode of this block relative to its cluster - */ - int getTimecode(); + /** + * @return The timecode of this block relative to its cluster + */ + int getTimecode(); - /** - * @return The track number which this block is for - */ - int getTrackNumber(); + /** + * @return The track number which this block is for + */ + int getTrackNumber(); - /** - * @return Whether this block is a keyframe - */ - boolean isKeyFrame(); + /** + * @return Whether this block is a keyframe + */ + boolean isKeyFrame(); - /** - * @return The number of frames in this block - */ - int getFrameCount(); + /** + * @return The number of frames in this block + */ + int getFrameCount(); - /** - * The reader must already be positioned at the frame that is to be read next. - * - * @param reader The reader to use to read the block contents into a buffer. - * @param index The index of the frame to get the buffer for. - * @return A buffer where the range between position and limit contains the data for the specific frame. The contents - * of this buffer are only valid until the next call to this method. - * @throws IOException On read error. - */ - ByteBuffer getNextFrameBuffer(MatroskaFileReader reader, int index) throws IOException; + /** + * The reader must already be positioned at the frame that is to be read next. + * + * @param reader The reader to use to read the block contents into a buffer. + * @param index The index of the frame to get the buffer for. + * @return A buffer where the range between position and limit contains the data for the specific frame. The contents + * of this buffer are only valid until the next call to this method. + * @throws IOException On read error. + */ + ByteBuffer getNextFrameBuffer(MatroskaFileReader reader, int index) throws IOException; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaCuePoint.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaCuePoint.java index b8a5ab197..5e341aac4 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaCuePoint.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaCuePoint.java @@ -14,7 +14,7 @@ public class MatroskaCuePoint { public final long[] trackClusterOffsets; /** - * @param timecode Timecode using the file timescale + * @param timecode Timecode using the file timescale * @param trackClusterOffsets Absolute offset to the cluster */ public MatroskaCuePoint(long timecode, long[] trackClusterOffsets) { diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaEbmlReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaEbmlReader.java index 15bf794a9..db82a9fa6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaEbmlReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaEbmlReader.java @@ -8,148 +8,162 @@ * Handles reading various different EBML code formats. */ public class MatroskaEbmlReader { - /** - * Read an EBML code from data input with fixed size - no size encoded in the data. - * - * @param input Data input to read bytes from - * @param codeLength Length of the code in bytes - * @param type Method of sign handling (null is unsigned) - * @return Read EBML code - * @throws IOException On read error - */ - public static long readFixedSizeEbmlInteger(DataInput input, int codeLength, Type type) throws IOException { - long code = 0; - - for (int i = 1; i <= codeLength; i++) { - code |= applyNextByte(codeLength, input.readByte() & 0xFF, i); + /** + * Read an EBML code from data input with fixed size - no size encoded in the data. + * + * @param input Data input to read bytes from + * @param codeLength Length of the code in bytes + * @param type Method of sign handling (null is unsigned) + * @return Read EBML code + * @throws IOException On read error + */ + public static long readFixedSizeEbmlInteger(DataInput input, int codeLength, Type type) throws IOException { + long code = 0; + + for (int i = 1; i <= codeLength; i++) { + code |= applyNextByte(codeLength, input.readByte() & 0xFF, i); + } + + return applyType(code, codeLength, type); } - return applyType(code, codeLength, type); - } - - /** - * Read an EBML code from data input. - * - * @param input Data input to read bytes from - * @param type Method of sign handling (null is unsigned) - * @return Read EBML code - * @throws IOException On read error - */ - public static long readEbmlInteger(DataInput input, Type type) throws IOException { - int firstByte = input.readByte() & 0xFF; - int codeLength = getCodeLength(firstByte); - - long code = applyFirstByte(firstByte, codeLength); - - for (int i = 2; i <= codeLength; i++) { - code |= applyNextByte(codeLength, input.readByte() & 0xFF, i); + /** + * Read an EBML code from data input. + * + * @param input Data input to read bytes from + * @param type Method of sign handling (null is unsigned) + * @return Read EBML code + * @throws IOException On read error + */ + public static long readEbmlInteger(DataInput input, Type type) throws IOException { + int firstByte = input.readByte() & 0xFF; + int codeLength = getCodeLength(firstByte); + + long code = applyFirstByte(firstByte, codeLength); + + for (int i = 2; i <= codeLength; i++) { + code |= applyNextByte(codeLength, input.readByte() & 0xFF, i); + } + + return applyType(code, codeLength, type); } - return applyType(code, codeLength, type); - } + /** + * Read an EBML code from byte buffer. + * + * @param buffer Buffer to read bytes from + * @param type Method of sign handling (null is unsigned) + * @return Read EBML code + */ + public static long readEbmlInteger(ByteBuffer buffer, Type type) { + int firstByte = buffer.get() & 0xFF; + int codeLength = getCodeLength(firstByte); + + long code = applyFirstByte(firstByte, codeLength); + + for (int i = 2; i <= codeLength; i++) { + code |= applyNextByte(codeLength, buffer.get() & 0xFF, i); + } - /** - * Read an EBML code from byte buffer. - * - * @param buffer Buffer to read bytes from - * @param type Method of sign handling (null is unsigned) - * @return Read EBML code - */ - public static long readEbmlInteger(ByteBuffer buffer, Type type) { - int firstByte = buffer.get() & 0xFF; - int codeLength = getCodeLength(firstByte); + return applyType(code, codeLength, type); + } - long code = applyFirstByte(firstByte, codeLength); + private static int getCodeLength(int firstByte) { + int codeLength = Integer.numberOfLeadingZeros(firstByte) - 23; + if (codeLength > 8) { + throw new IllegalStateException("More than 4 bytes for length, probably invalid data"); + } - for (int i = 2; i <= codeLength; i++) { - code |= applyNextByte(codeLength, buffer.get() & 0xFF, i); + return codeLength; } - return applyType(code, codeLength, type); - } + private static long applyFirstByte(long firstByte, int codeLength) { + return (firstByte & (0xFFL >> codeLength)) << ((codeLength - 1) << 3); + } - private static int getCodeLength(int firstByte) { - int codeLength = Integer.numberOfLeadingZeros(firstByte) - 23; - if (codeLength > 8) { - throw new IllegalStateException("More than 4 bytes for length, probably invalid data"); + private static long applyNextByte(int codeLength, int value, int index) { + return value << ((codeLength - index) << 3); } - return codeLength; - } - - private static long applyFirstByte(long firstByte, int codeLength) { - return (firstByte & (0xFFL >> codeLength)) << ((codeLength - 1) << 3); - } - - private static long applyNextByte(int codeLength, int value, int index) { - return value << ((codeLength - index) << 3); - } - - private static long applyType(long code, int codeLength, Type type) { - if (type != null) { - switch (type) { - case SIGNED: - return signEbmlInteger(code, codeLength); - case LACE_SIGNED: - return laceSignEbmlInteger(code, codeLength); - case UNSIGNED: - default: - return code; - } - } else { - return code; + private static long applyType(long code, int codeLength, Type type) { + if (type != null) { + switch (type) { + case SIGNED: + return signEbmlInteger(code, codeLength); + case LACE_SIGNED: + return laceSignEbmlInteger(code, codeLength); + case UNSIGNED: + default: + return code; + } + } else { + return code; + } } - } - - private static long laceSignEbmlInteger(long code, int codeLength) { - switch (codeLength) { - case 1: return code - 63; - case 2: return code - 8191; - case 3: return code - 1048575; - case 4: return code - 134217727; - default: throw new IllegalStateException("Code length out of bounds."); + + private static long laceSignEbmlInteger(long code, int codeLength) { + switch (codeLength) { + case 1: + return code - 63; + case 2: + return code - 8191; + case 3: + return code - 1048575; + case 4: + return code - 134217727; + default: + throw new IllegalStateException("Code length out of bounds."); + } } - } - private static long signEbmlInteger(long code, int codeLength) { - long mask = getSignMask(codeLength); + private static long signEbmlInteger(long code, int codeLength) { + long mask = getSignMask(codeLength); - if ((code & mask) != 0) { - return code | mask; - } else { - return code; + if ((code & mask) != 0) { + return code | mask; + } else { + return code; + } } - } - - private static long getSignMask(int codeLength) { - switch (codeLength) { - case 1: return ~0x000000000000003FL; - case 2: return ~0x0000000000001FFFL; - case 3: return ~0x00000000000FFFFFL; - case 4: return ~0x0000000007FFFFFFL; - case 5: return ~0x00000003FFFFFFFFL; - case 6: return ~0x000001FFFFFFFFFFL; - case 7: return ~0x0000FFFFFFFFFFFFL; - case 8: return ~0x007FFFFFFFFFFFFFL; - default: throw new IllegalStateException("Code length out of bounds."); + + private static long getSignMask(int codeLength) { + switch (codeLength) { + case 1: + return ~0x000000000000003FL; + case 2: + return ~0x0000000000001FFFL; + case 3: + return ~0x00000000000FFFFFL; + case 4: + return ~0x0000000007FFFFFFL; + case 5: + return ~0x00000003FFFFFFFFL; + case 6: + return ~0x000001FFFFFFFFFFL; + case 7: + return ~0x0000FFFFFFFFFFFFL; + case 8: + return ~0x007FFFFFFFFFFFFFL; + default: + throw new IllegalStateException("Code length out of bounds."); + } } - } - /** - * EBML code type (sign handling method). - */ - public enum Type { /** - * Signed value with first bit marking the sign. + * EBML code type (sign handling method). */ - SIGNED, - /** - * Signed value where where sign is applied via subtraction. - */ - LACE_SIGNED, - /** - * Unsigned value. - */ - UNSIGNED - } + public enum Type { + /** + * Signed value with first bit marking the sign. + */ + SIGNED, + /** + * Signed value where where sign is applied via subtraction. + */ + LACE_SIGNED, + /** + * Unsigned value. + */ + UNSIGNED + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaElementType.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaElementType.java index b84a75b14..7fa4e6fd2 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaElementType.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaElementType.java @@ -9,134 +9,134 @@ */ @SuppressWarnings("squid:S00115") public enum MatroskaElementType { - Ebml(DataType.MASTER, new int[] { 0x1A, 0x45, 0xDF, 0xA3 }), - DocType(DataType.STRING, new int[] { 0x42, 0x82 }), - Segment(DataType.MASTER, new int[] { 0x18, 0x53, 0x80, 0x67 }), - SeekHead(DataType.MASTER, new int[] { 0x11, 0x4D, 0x9B, 0x74 }), - Seek(DataType.MASTER, new int[] { 0x4D, 0xBB }), - SeekId(DataType.BINARY, new int[] { 0x53, 0xAB }), - SeekPosition(DataType.UNSIGNED_INTEGER, new int[] { 0x53, 0xAC }), - Info(DataType.MASTER, new int[] { 0x15, 0x49, 0xA9, 0x66 }), - Duration(DataType.FLOAT, new int[] { 0x44, 0x89 }), - TimecodeScale(DataType.UNSIGNED_INTEGER, new int[] { 0x2A, 0xD7, 0xB1 }), - Cluster(DataType.MASTER, new int[] { 0x1F, 0x43, 0xB6, 0x75 }), - Timecode(DataType.UNSIGNED_INTEGER, new int[] { 0xE7 }), - SimpleBlock(DataType.BINARY, new int[] { 0xA3 }), - BlockGroup(DataType.MASTER, new int[] { 0xA0 }), - Block(DataType.BINARY, new int[] { 0xA1 }), - BlockDuration(DataType.UNSIGNED_INTEGER, new int[] { 0x9B }), - ReferenceBlock(DataType.SIGNED_INTEGER, new int[] { 0xFB }), - Tracks(DataType.MASTER, new int[] { 0x16, 0x54, 0xAE, 0x6B }), - TrackEntry(DataType.MASTER, new int[] { 0xAE }), - TrackNumber(DataType.UNSIGNED_INTEGER, new int[] { 0xD7 }), - TrackUid(DataType.UNSIGNED_INTEGER, new int[] { 0x73, 0xC5 }), - TrackType(DataType.UNSIGNED_INTEGER, new int[] { 0x83 }), - Name(DataType.UTF8_STRING, new int[] { 0x53, 0x6E }), - CodecId(DataType.STRING, new int[] { 0x86 }), - CodecPrivate(DataType.BINARY, new int[] { 0x63, 0xA2 }), - Audio(DataType.MASTER, new int[] { 0xE1 }), - SamplingFrequency(DataType.FLOAT, new int[] { 0xB5 }), - OutputSamplingFrequency(DataType.FLOAT, new int[] { 0x78, 0xB5 }), - Channels(DataType.UNSIGNED_INTEGER, new int[] { 0x9F }), - BitDepth(DataType.UNSIGNED_INTEGER, new int[] { 0x62, 0x64 }), - Cues(DataType.MASTER, new int[] { 0x1C, 0x53, 0xBB, 0x6B }), - CuePoint(DataType.MASTER, new int[] { 0xBB }), - CueTime(DataType.UNSIGNED_INTEGER, new int[] { 0xB3 }), - CueTrackPositions(DataType.MASTER, new int[] { 0xB7 }), - CueTrack(DataType.UNSIGNED_INTEGER, new int[] { 0xF7 }), - CueClusterPosition(DataType.UNSIGNED_INTEGER, new int[] { 0xF1 }), - Unknown(DataType.BINARY, new int[] { }); + Ebml(DataType.MASTER, new int[]{0x1A, 0x45, 0xDF, 0xA3}), + DocType(DataType.STRING, new int[]{0x42, 0x82}), + Segment(DataType.MASTER, new int[]{0x18, 0x53, 0x80, 0x67}), + SeekHead(DataType.MASTER, new int[]{0x11, 0x4D, 0x9B, 0x74}), + Seek(DataType.MASTER, new int[]{0x4D, 0xBB}), + SeekId(DataType.BINARY, new int[]{0x53, 0xAB}), + SeekPosition(DataType.UNSIGNED_INTEGER, new int[]{0x53, 0xAC}), + Info(DataType.MASTER, new int[]{0x15, 0x49, 0xA9, 0x66}), + Duration(DataType.FLOAT, new int[]{0x44, 0x89}), + TimecodeScale(DataType.UNSIGNED_INTEGER, new int[]{0x2A, 0xD7, 0xB1}), + Cluster(DataType.MASTER, new int[]{0x1F, 0x43, 0xB6, 0x75}), + Timecode(DataType.UNSIGNED_INTEGER, new int[]{0xE7}), + SimpleBlock(DataType.BINARY, new int[]{0xA3}), + BlockGroup(DataType.MASTER, new int[]{0xA0}), + Block(DataType.BINARY, new int[]{0xA1}), + BlockDuration(DataType.UNSIGNED_INTEGER, new int[]{0x9B}), + ReferenceBlock(DataType.SIGNED_INTEGER, new int[]{0xFB}), + Tracks(DataType.MASTER, new int[]{0x16, 0x54, 0xAE, 0x6B}), + TrackEntry(DataType.MASTER, new int[]{0xAE}), + TrackNumber(DataType.UNSIGNED_INTEGER, new int[]{0xD7}), + TrackUid(DataType.UNSIGNED_INTEGER, new int[]{0x73, 0xC5}), + TrackType(DataType.UNSIGNED_INTEGER, new int[]{0x83}), + Name(DataType.UTF8_STRING, new int[]{0x53, 0x6E}), + CodecId(DataType.STRING, new int[]{0x86}), + CodecPrivate(DataType.BINARY, new int[]{0x63, 0xA2}), + Audio(DataType.MASTER, new int[]{0xE1}), + SamplingFrequency(DataType.FLOAT, new int[]{0xB5}), + OutputSamplingFrequency(DataType.FLOAT, new int[]{0x78, 0xB5}), + Channels(DataType.UNSIGNED_INTEGER, new int[]{0x9F}), + BitDepth(DataType.UNSIGNED_INTEGER, new int[]{0x62, 0x64}), + Cues(DataType.MASTER, new int[]{0x1C, 0x53, 0xBB, 0x6B}), + CuePoint(DataType.MASTER, new int[]{0xBB}), + CueTime(DataType.UNSIGNED_INTEGER, new int[]{0xB3}), + CueTrackPositions(DataType.MASTER, new int[]{0xB7}), + CueTrack(DataType.UNSIGNED_INTEGER, new int[]{0xF7}), + CueClusterPosition(DataType.UNSIGNED_INTEGER, new int[]{0xF1}), + Unknown(DataType.BINARY, new int[]{}); - private static Map mapping; + private static Map mapping; - /** - * The ID as EBML code bytes. - */ - public final byte[] bytes; - /** - * The ID of the element type. - */ - public final long id; - /** - * Data type of the element type. - */ - public final DataType dataType; - - static { - Map codeMapping = new HashMap<>(); - - for (MatroskaElementType code : MatroskaElementType.class.getEnumConstants()) { - if (code != Unknown) { - codeMapping.put(code.id, code); - } - } - - mapping = codeMapping; - } - - MatroskaElementType(DataType dataType, int[] integers) { - this.dataType = dataType; - this.bytes = asByteArray(integers); - this.id = bytes.length > 0 ? MatroskaEbmlReader.readEbmlInteger(ByteBuffer.wrap(bytes), null) : -1; - } - - /** - * Data type of an element. - */ - public enum DataType { - /** - * Contains child elements. - */ - MASTER, - /** - * Unsigned EBML integer. - */ - UNSIGNED_INTEGER, - /** - * Signed EBML integer. - */ - SIGNED_INTEGER, - /** - * ASCII-encoded string. - */ - STRING, /** - * UTF8-encoded string. + * The ID as EBML code bytes. */ - UTF8_STRING, + public final byte[] bytes; /** - * Raw binary data. + * The ID of the element type. */ - BINARY, + public final long id; /** - * Float (either 4 or 8 byte) + * Data type of the element type. */ - FLOAT, + public final DataType dataType; + + static { + Map codeMapping = new HashMap<>(); + + for (MatroskaElementType code : MatroskaElementType.class.getEnumConstants()) { + if (code != Unknown) { + codeMapping.put(code.id, code); + } + } + + mapping = codeMapping; + } + + MatroskaElementType(DataType dataType, int[] integers) { + this.dataType = dataType; + this.bytes = asByteArray(integers); + this.id = bytes.length > 0 ? MatroskaEbmlReader.readEbmlInteger(ByteBuffer.wrap(bytes), null) : -1; + } + /** - * Nanosecond timestamp since 2001. + * Data type of an element. */ - DATE - } + public enum DataType { + /** + * Contains child elements. + */ + MASTER, + /** + * Unsigned EBML integer. + */ + UNSIGNED_INTEGER, + /** + * Signed EBML integer. + */ + SIGNED_INTEGER, + /** + * ASCII-encoded string. + */ + STRING, + /** + * UTF8-encoded string. + */ + UTF8_STRING, + /** + * Raw binary data. + */ + BINARY, + /** + * Float (either 4 or 8 byte) + */ + FLOAT, + /** + * Nanosecond timestamp since 2001. + */ + DATE + } - private static byte[] asByteArray(int[] integers) { - byte[] bytes = new byte[integers.length]; + private static byte[] asByteArray(int[] integers) { + byte[] bytes = new byte[integers.length]; - for (int i = 0; i < integers.length; i++) { - bytes[i] = (byte) integers[i]; - } + for (int i = 0; i < integers.length; i++) { + bytes[i] = (byte) integers[i]; + } - return bytes; - } + return bytes; + } - /** - * @param id Code of the element type to find - * @return The element type, Unknown if not present. - */ - public static MatroskaElementType find(long id) { - MatroskaElementType code = mapping.get(id); - if (code == null) { - code = Unknown; + /** + * @param id Code of the element type to find + * @return The element type, Unknown if not present. + */ + public static MatroskaElementType find(long id) { + MatroskaElementType code = mapping.get(id); + if (code == null) { + code = Unknown; + } + return code; } - return code; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaFileReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaFileReader.java index da28379d0..4eabe158f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaFileReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaFileReader.java @@ -11,205 +11,206 @@ * Handles reading of elements and their content from an MKV file. */ public class MatroskaFileReader { - private final SeekableInputStream inputStream; - private final DataInput dataInput; - private final MutableMatroskaElement[] levels; - private final MutableMatroskaBlock mutableBlock; - - /** - * @param inputStream Input stream to read from. - */ - public MatroskaFileReader(SeekableInputStream inputStream) { - this.inputStream = inputStream; - this.dataInput = new DataInputStream(inputStream); - this.levels = new MutableMatroskaElement[8]; - this.mutableBlock = new MutableMatroskaBlock(); - } - - /** - * @param parent The parent element to use for bounds checking, null is valid. - * @return The element whose header was read. Null if the parent/file has ended. The contents of this element are only - * valid until the next element at the same level is read, use {@link MatroskaElement#frozen()} to get a - * permanent instance. - * @throws IOException On read error - */ - public MatroskaElement readNextElement(MatroskaElement parent) throws IOException { - long position = inputStream.getPosition(); - long remaining = parent != null ? parent.getRemaining(position) : inputStream.getContentLength() - position; - - if (remaining == 0) { - return null; - } else if (remaining < 0) { - throw new IllegalStateException("Current position is beyond this element"); + private final SeekableInputStream inputStream; + private final DataInput dataInput; + private final MutableMatroskaElement[] levels; + private final MutableMatroskaBlock mutableBlock; + + /** + * @param inputStream Input stream to read from. + */ + public MatroskaFileReader(SeekableInputStream inputStream) { + this.inputStream = inputStream; + this.dataInput = new DataInputStream(inputStream); + this.levels = new MutableMatroskaElement[8]; + this.mutableBlock = new MutableMatroskaBlock(); } - long id = MatroskaEbmlReader.readEbmlInteger(dataInput, null); - long dataSize = MatroskaEbmlReader.readEbmlInteger(dataInput, null); - long dataPosition = inputStream.getPosition(); + /** + * @param parent The parent element to use for bounds checking, null is valid. + * @return The element whose header was read. Null if the parent/file has ended. The contents of this element are only + * valid until the next element at the same level is read, use {@link MatroskaElement#frozen()} to get a + * permanent instance. + * @throws IOException On read error + */ + public MatroskaElement readNextElement(MatroskaElement parent) throws IOException { + long position = inputStream.getPosition(); + long remaining = parent != null ? parent.getRemaining(position) : inputStream.getContentLength() - position; + + if (remaining == 0) { + return null; + } else if (remaining < 0) { + throw new IllegalStateException("Current position is beyond this element"); + } + + long id = MatroskaEbmlReader.readEbmlInteger(dataInput, null); + long dataSize = MatroskaEbmlReader.readEbmlInteger(dataInput, null); + long dataPosition = inputStream.getPosition(); + + int level = parent == null ? 0 : parent.getLevel() + 1; + MutableMatroskaElement element = levels[level]; + + if (element == null) { + element = levels[level] = new MutableMatroskaElement(level); + } + + element.setId(id); + element.setType(MatroskaElementType.find(id)); + element.setPosition(position); + element.setHeaderSize((int) (dataPosition - position)); + element.setDataSize((int) dataSize); + return element; + } - int level = parent == null ? 0 : parent.getLevel() + 1; - MutableMatroskaElement element = levels[level]; + /** + * Reads one Matroska block header. The data is of the block is not read, but can be read frame by frame using + * {@link MatroskaBlock#getNextFrameBuffer(MatroskaFileReader, int)}. + * + * @param parent The block parent element. + * @param trackFilter The ID of the track to read data for from the block. + * @return An instance of a block if it contains data for the requested track, null otherwise. + * @throws IOException On read error. + */ + public MatroskaBlock readBlockHeader(MatroskaElement parent, int trackFilter) throws IOException { + if (!mutableBlock.parseHeader(this, parent, trackFilter)) { + return null; + } + + return mutableBlock; + } - if (element == null) { - element = levels[level] = new MutableMatroskaElement(level); + /** + * @param element Element to read from + * @return The contents of the element as an integer + * @throws IOException On read error + */ + public int asInteger(MatroskaElement element) throws IOException { + if (element.is(MatroskaElementType.DataType.UNSIGNED_INTEGER)) { + long value = MatroskaEbmlReader.readFixedSizeEbmlInteger(dataInput, element.dataSize, null); + + if (value < 0 || value > Integer.MAX_VALUE) { + throw new ArithmeticException("Cannot convert unsigned value to integer."); + } else { + return (int) value; + } + } else if (element.is(MatroskaElementType.DataType.SIGNED_INTEGER)) { + return Math.toIntExact(MatroskaEbmlReader.readFixedSizeEbmlInteger(dataInput, element.dataSize, MatroskaEbmlReader.Type.SIGNED)); + } else { + throw new IllegalArgumentException("Not an integer element."); + } } - element.setId(id); - element.setType(MatroskaElementType.find(id)); - element.setPosition(position); - element.setHeaderSize((int) (dataPosition - position)); - element.setDataSize((int) dataSize); - return element; - } - - /** - * Reads one Matroska block header. The data is of the block is not read, but can be read frame by frame using - * {@link MatroskaBlock#getNextFrameBuffer(MatroskaFileReader, int)}. - * - * @param parent The block parent element. - * @param trackFilter The ID of the track to read data for from the block. - * @return An instance of a block if it contains data for the requested track, null otherwise. - * @throws IOException On read error. - */ - public MatroskaBlock readBlockHeader(MatroskaElement parent, int trackFilter) throws IOException { - if (!mutableBlock.parseHeader(this, parent, trackFilter)) { - return null; + /** + * @param element Element to read from + * @return The contents of the element as a long + * @throws IOException On read error + */ + public long asLong(MatroskaElement element) throws IOException { + if (element.is(MatroskaElementType.DataType.UNSIGNED_INTEGER)) { + return MatroskaEbmlReader.readFixedSizeEbmlInteger(dataInput, element.dataSize, null); + } else if (element.is(MatroskaElementType.DataType.SIGNED_INTEGER)) { + return MatroskaEbmlReader.readFixedSizeEbmlInteger(dataInput, element.dataSize, MatroskaEbmlReader.Type.SIGNED); + } else { + throw new IllegalArgumentException("Not an integer element."); + } } - return mutableBlock; - } - - /** - * @param element Element to read from - * @return The contents of the element as an integer - * @throws IOException On read error - */ - public int asInteger(MatroskaElement element) throws IOException { - if (element.is(MatroskaElementType.DataType.UNSIGNED_INTEGER)) { - long value = MatroskaEbmlReader.readFixedSizeEbmlInteger(dataInput, element.dataSize, null); - - if (value < 0 || value > Integer.MAX_VALUE) { - throw new ArithmeticException("Cannot convert unsigned value to integer."); - } else { - return (int) value; - } - } else if (element.is(MatroskaElementType.DataType.SIGNED_INTEGER)) { - return Math.toIntExact(MatroskaEbmlReader.readFixedSizeEbmlInteger(dataInput, element.dataSize, MatroskaEbmlReader.Type.SIGNED)); - } else { - throw new IllegalArgumentException("Not an integer element."); + /** + * @param element Element to read from + * @return The contents of the element as a float + * @throws IOException On read error + */ + public float asFloat(MatroskaElement element) throws IOException { + if (element.is(MatroskaElementType.DataType.FLOAT)) { + if (element.dataSize == 4) { + return dataInput.readFloat(); + } else if (element.dataSize == 8) { + return (float) dataInput.readDouble(); + } else { + throw new IllegalStateException("Float element has invalid size."); + } + } else { + throw new IllegalArgumentException("Not a float element."); + } } - } - - /** - * @param element Element to read from - * @return The contents of the element as a long - * @throws IOException On read error - */ - public long asLong(MatroskaElement element) throws IOException { - if (element.is(MatroskaElementType.DataType.UNSIGNED_INTEGER)) { - return MatroskaEbmlReader.readFixedSizeEbmlInteger(dataInput, element.dataSize, null); - } else if (element.is(MatroskaElementType.DataType.SIGNED_INTEGER)) { - return MatroskaEbmlReader.readFixedSizeEbmlInteger(dataInput, element.dataSize, MatroskaEbmlReader.Type.SIGNED); - } else { - throw new IllegalArgumentException("Not an integer element."); + + /** + * @param element Element to read from + * @return The contents of the element as a double + * @throws IOException On read error + */ + public double asDouble(MatroskaElement element) throws IOException { + if (element.is(MatroskaElementType.DataType.FLOAT)) { + if (element.dataSize == 4) { + return dataInput.readFloat(); + } else if (element.dataSize == 8) { + return dataInput.readDouble(); + } else { + throw new IllegalStateException("Float element has invalid size."); + } + } else { + throw new IllegalArgumentException("Not a float element."); + } } - } - - /** - * @param element Element to read from - * @return The contents of the element as a float - * @throws IOException On read error - */ - public float asFloat(MatroskaElement element) throws IOException { - if (element.is(MatroskaElementType.DataType.FLOAT)) { - if (element.dataSize == 4) { - return dataInput.readFloat(); - } else if (element.dataSize == 8) { - return (float) dataInput.readDouble(); - } else { - throw new IllegalStateException("Float element has invalid size."); - } - } else { - throw new IllegalArgumentException("Not a float element."); + + /** + * @param element Element to read from + * @return The contents of the element as a string + * @throws IOException On read error + */ + public String asString(MatroskaElement element) throws IOException { + if (element.is(MatroskaElementType.DataType.STRING)) { + return new String(asBytes(element), StandardCharsets.US_ASCII); + } else if (element.is(MatroskaElementType.DataType.UTF8_STRING)) { + return new String(asBytes(element), StandardCharsets.UTF_8); + } else { + throw new IllegalArgumentException("Not a string element."); + } } - } - - /** - * @param element Element to read from - * @return The contents of the element as a double - * @throws IOException On read error - */ - public double asDouble(MatroskaElement element) throws IOException { - if (element.is(MatroskaElementType.DataType.FLOAT)) { - if (element.dataSize == 4) { - return dataInput.readFloat(); - } else if (element.dataSize == 8) { - return dataInput.readDouble(); - } else { - throw new IllegalStateException("Float element has invalid size."); - } - } else { - throw new IllegalArgumentException("Not a float element."); + + /** + * @param element Element to read from + * @return The contents of the element as a byte array + * @throws IOException On read error + */ + public byte[] asBytes(MatroskaElement element) throws IOException { + byte[] bytes = new byte[element.dataSize]; + dataInput.readFully(bytes); + return bytes; } - } - - /** - * @param element Element to read from - * @return The contents of the element as a string - * @throws IOException On read error - */ - public String asString(MatroskaElement element) throws IOException { - if (element.is(MatroskaElementType.DataType.STRING)) { - return new String(asBytes(element), StandardCharsets.US_ASCII); - } else if (element.is(MatroskaElementType.DataType.UTF8_STRING)) { - return new String(asBytes(element), StandardCharsets.UTF_8); - } else { - throw new IllegalArgumentException("Not a string element."); + + /** + * @param element Element to skip over + * @throws IOException On read error + */ + public void skip(MatroskaElement element) throws IOException { + long remaining = element.getRemaining(inputStream.getPosition()); + + if (remaining > 0) { + inputStream.skipFully(remaining); + } else if (remaining < 0) { + throw new IllegalStateException("Current position is beyond this element"); + } } - } - - /** - * @param element Element to read from - * @return The contents of the element as a byte array - * @throws IOException On read error - */ - public byte[] asBytes(MatroskaElement element) throws IOException { - byte[] bytes = new byte[element.dataSize]; - dataInput.readFully(bytes); - return bytes; - } - - /** - * @param element Element to skip over - * @throws IOException On read error - */ - public void skip(MatroskaElement element) throws IOException { - long remaining = element.getRemaining(inputStream.getPosition()); - - if (remaining > 0) { - inputStream.skipFully(remaining); - } else if (remaining < 0) { - throw new IllegalStateException("Current position is beyond this element"); + + /** + * @return Returns the current absolute position of the file. + */ + public long getPosition() { + return inputStream.getPosition(); + } + + /** + * Seeks to the specified position. + * + * @param position The position in bytes. + * @throws IOException On read error + */ + public void seek(long position) throws IOException { + inputStream.seek(position); + } + + public DataInput getDataInput() { + return dataInput; } - } - - /** - * @return Returns the current absolute position of the file. - */ - public long getPosition() { - return inputStream.getPosition(); - } - - /** - * Seeks to the specified position. - * @param position The position in bytes. - * @throws IOException On read error - */ - public void seek(long position) throws IOException { - inputStream.seek(position); - } - - public DataInput getDataInput() { - return dataInput; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaFileTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaFileTrack.java index ee6e46405..e270c4eec 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaFileTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MatroskaFileTrack.java @@ -6,202 +6,202 @@ * Describes one track in a matroska file. */ public class MatroskaFileTrack { - /** - * Track index/number. - */ - public final int index; - /** - * Type of the track. - */ - public final Type type; - /** - * The unique track UID. - */ - public final long trackUid; - /** - * Name of the track. - */ - public final String name; - /** - * ID of the codec. - */ - public final String codecId; - /** - * Custom data for the codec (header). - */ - public final byte[] codecPrivate; - /** - * Information specific to audio tracks (null for non-audio tracks). - */ - public final AudioDetails audio; - - /** - * @param index Track index/number. - * @param type Type of the track. - * @param trackUid The unique track UID. - * @param name Name of the track. - * @param codecId ID of the codec. - * @param codecPrivate Custom data for the codec (header). - * @param audio Information specific to audio tracks (null for non-audio tracks). - */ - public MatroskaFileTrack(int index, Type type, long trackUid, String name, String codecId, byte[] codecPrivate, AudioDetails audio) { - this.index = index; - this.type = type; - this.trackUid = trackUid; - this.name = name; - this.codecId = codecId; - this.codecPrivate = codecPrivate; - this.audio = audio; - } - - /** - * Track type list. - */ - public enum Type { - VIDEO(1), - AUDIO(2), - COMPLEX(3), - LOGO(0x10), - SUBTITLE(0x11), - BUTTONS(0x12), - CONTROL(0x20); - /** - * ID which is used in the track type field in the file. + * Track index/number. */ - public final long id; - - Type(long id) { - this.id = id; - } - + public final int index; /** - * @param id ID to look up. - * @return Track type for that ID, null if not found. + * Type of the track. */ - public static Type fromId(long id) { - for (Type entry : Type.class.getEnumConstants()) { - if (entry.id == id) { - return entry; - } - } - - return null; - } - } - - /** - * Fields specific to an audio track. - */ - public static class AudioDetails { + public final Type type; /** - * Sampling frequency in Hz. + * The unique track UID. */ - public final float samplingFrequency; + public final long trackUid; /** - * Real output sampling frequency in Hz. + * Name of the track. */ - public final float outputSamplingFrequency; + public final String name; /** - * Number of channels in the track. + * ID of the codec. */ - public final int channels; + public final String codecId; /** - * Number of bits per sample. + * Custom data for the codec (header). */ - public final int bitDepth; + public final byte[] codecPrivate; + /** + * Information specific to audio tracks (null for non-audio tracks). + */ + public final AudioDetails audio; /** - * @param samplingFrequency Sampling frequency in Hz. - * @param outputSamplingFrequency Real output sampling frequency in Hz. - * @param channels Number of channels in the track. - * @param bitDepth Number of bits per sample. + * @param index Track index/number. + * @param type Type of the track. + * @param trackUid The unique track UID. + * @param name Name of the track. + * @param codecId ID of the codec. + * @param codecPrivate Custom data for the codec (header). + * @param audio Information specific to audio tracks (null for non-audio tracks). */ - public AudioDetails(float samplingFrequency, float outputSamplingFrequency, int channels, int bitDepth) { - this.samplingFrequency = samplingFrequency; - this.outputSamplingFrequency = outputSamplingFrequency; - this.channels = channels; - this.bitDepth = bitDepth; + public MatroskaFileTrack(int index, Type type, long trackUid, String name, String codecId, byte[] codecPrivate, AudioDetails audio) { + this.index = index; + this.type = type; + this.trackUid = trackUid; + this.name = name; + this.codecId = codecId; + this.codecPrivate = codecPrivate; + this.audio = audio; } - } - - /** - * @param trackElement The track element - * @param reader Matroska file reader - * @return The parsed track - * @throws IOException On read error - */ - public static MatroskaFileTrack parse(MatroskaElement trackElement, MatroskaFileReader reader) throws IOException { - Builder builder = new Builder(); - MatroskaElement child; - - while ((child = reader.readNextElement(trackElement)) != null) { - if (child.is(MatroskaElementType.TrackNumber)) { - builder.index = reader.asInteger(child); - } else if (child.is(MatroskaElementType.TrackUid)) { - builder.trackUid = reader.asLong(child); - } else if (child.is(MatroskaElementType.TrackType)) { - builder.type = Type.fromId(reader.asInteger(child)); - } else if (child.is(MatroskaElementType.Name)) { - builder.name = reader.asString(child); - } else if (child.is(MatroskaElementType.CodecId)) { - builder.codecId = reader.asString(child); - } else if (child.is(MatroskaElementType.CodecPrivate)) { - builder.codecPrivate = reader.asBytes(child); - } else if (child.is(MatroskaElementType.Audio)) { - builder.audio = parseAudioElement(child, reader); - } - - // Unused fields: DefaultDuration, Language, Video, etc - reader.skip(child); + + /** + * Track type list. + */ + public enum Type { + VIDEO(1), + AUDIO(2), + COMPLEX(3), + LOGO(0x10), + SUBTITLE(0x11), + BUTTONS(0x12), + CONTROL(0x20); + + /** + * ID which is used in the track type field in the file. + */ + public final long id; + + Type(long id) { + this.id = id; + } + + /** + * @param id ID to look up. + * @return Track type for that ID, null if not found. + */ + public static Type fromId(long id) { + for (Type entry : Type.class.getEnumConstants()) { + if (entry.id == id) { + return entry; + } + } + + return null; + } } - return builder.build(); - } - - private static AudioDetails parseAudioElement(MatroskaElement audioElement, MatroskaFileReader reader) throws IOException { - AudioBuilder builder = new AudioBuilder(); - MatroskaElement child; - - while ((child = reader.readNextElement(audioElement)) != null) { - if (child.is(MatroskaElementType.SamplingFrequency)) { - builder.samplingFrequency = reader.asFloat(child); - } else if (child.is(MatroskaElementType.OutputSamplingFrequency)) { - builder.outputSamplingFrequency = reader.asFloat(child); - } else if (child.is(MatroskaElementType.Channels)) { - builder.channels = reader.asInteger(child); - } else if (child.is(MatroskaElementType.BitDepth)) { - builder.bitDepth = reader.asInteger(child); - } - - reader.skip(child); + /** + * Fields specific to an audio track. + */ + public static class AudioDetails { + /** + * Sampling frequency in Hz. + */ + public final float samplingFrequency; + /** + * Real output sampling frequency in Hz. + */ + public final float outputSamplingFrequency; + /** + * Number of channels in the track. + */ + public final int channels; + /** + * Number of bits per sample. + */ + public final int bitDepth; + + /** + * @param samplingFrequency Sampling frequency in Hz. + * @param outputSamplingFrequency Real output sampling frequency in Hz. + * @param channels Number of channels in the track. + * @param bitDepth Number of bits per sample. + */ + public AudioDetails(float samplingFrequency, float outputSamplingFrequency, int channels, int bitDepth) { + this.samplingFrequency = samplingFrequency; + this.outputSamplingFrequency = outputSamplingFrequency; + this.channels = channels; + this.bitDepth = bitDepth; + } + } + + /** + * @param trackElement The track element + * @param reader Matroska file reader + * @return The parsed track + * @throws IOException On read error + */ + public static MatroskaFileTrack parse(MatroskaElement trackElement, MatroskaFileReader reader) throws IOException { + Builder builder = new Builder(); + MatroskaElement child; + + while ((child = reader.readNextElement(trackElement)) != null) { + if (child.is(MatroskaElementType.TrackNumber)) { + builder.index = reader.asInteger(child); + } else if (child.is(MatroskaElementType.TrackUid)) { + builder.trackUid = reader.asLong(child); + } else if (child.is(MatroskaElementType.TrackType)) { + builder.type = Type.fromId(reader.asInteger(child)); + } else if (child.is(MatroskaElementType.Name)) { + builder.name = reader.asString(child); + } else if (child.is(MatroskaElementType.CodecId)) { + builder.codecId = reader.asString(child); + } else if (child.is(MatroskaElementType.CodecPrivate)) { + builder.codecPrivate = reader.asBytes(child); + } else if (child.is(MatroskaElementType.Audio)) { + builder.audio = parseAudioElement(child, reader); + } + + // Unused fields: DefaultDuration, Language, Video, etc + reader.skip(child); + } + + return builder.build(); } - return builder.build(); - } + private static AudioDetails parseAudioElement(MatroskaElement audioElement, MatroskaFileReader reader) throws IOException { + AudioBuilder builder = new AudioBuilder(); + MatroskaElement child; + + while ((child = reader.readNextElement(audioElement)) != null) { + if (child.is(MatroskaElementType.SamplingFrequency)) { + builder.samplingFrequency = reader.asFloat(child); + } else if (child.is(MatroskaElementType.OutputSamplingFrequency)) { + builder.outputSamplingFrequency = reader.asFloat(child); + } else if (child.is(MatroskaElementType.Channels)) { + builder.channels = reader.asInteger(child); + } else if (child.is(MatroskaElementType.BitDepth)) { + builder.bitDepth = reader.asInteger(child); + } + + reader.skip(child); + } - private static class Builder { - private int index; - private Type type; - private long trackUid; - private String name; - private String codecId; - private byte[] codecPrivate; - private AudioDetails audio; + return builder.build(); + } - private MatroskaFileTrack build() { - return new MatroskaFileTrack(index, type, trackUid, name, codecId, codecPrivate, audio); + private static class Builder { + private int index; + private Type type; + private long trackUid; + private String name; + private String codecId; + private byte[] codecPrivate; + private AudioDetails audio; + + private MatroskaFileTrack build() { + return new MatroskaFileTrack(index, type, trackUid, name, codecId, codecPrivate, audio); + } } - } - private static class AudioBuilder { - private float samplingFrequency; - private float outputSamplingFrequency; - private int channels; - private int bitDepth; + private static class AudioBuilder { + private float samplingFrequency; + private float outputSamplingFrequency; + private int channels; + private int bitDepth; - private AudioDetails build() { - return new AudioDetails(samplingFrequency, outputSamplingFrequency, channels, bitDepth); + private AudioDetails build() { + return new AudioDetails(samplingFrequency, outputSamplingFrequency, channels, bitDepth); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MutableMatroskaBlock.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MutableMatroskaBlock.java index df2c963e2..01abd2911 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MutableMatroskaBlock.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MutableMatroskaBlock.java @@ -10,152 +10,152 @@ * a block with more than twice as many frames as seen before, or a frame more than twice as long as before. */ public class MutableMatroskaBlock implements MatroskaBlock { - private int timecode; - private int trackNumber; - private boolean keyFrame; - private int[] frameSizes; - private int frameCount; - private ByteBuffer buffer; - private byte[] bufferArray; - - @Override - public int getTimecode() { - return timecode; - } - - @Override - public int getTrackNumber() { - return trackNumber; - } - - @Override - public boolean isKeyFrame() { - return keyFrame; - } - - @Override - public int getFrameCount() { - return frameCount; - } - - @Override - public ByteBuffer getNextFrameBuffer(MatroskaFileReader reader, int index) throws IOException { - if (index >= frameCount) { - throw new IllegalArgumentException("Frame index out of bounds."); + private int timecode; + private int trackNumber; + private boolean keyFrame; + private int[] frameSizes; + private int frameCount; + private ByteBuffer buffer; + private byte[] bufferArray; + + @Override + public int getTimecode() { + return timecode; } - int frameSize = frameSizes[index]; + @Override + public int getTrackNumber() { + return trackNumber; + } - if (buffer == null || frameSize > buffer.capacity()) { - buffer = ByteBuffer.allocate(frameSizes[index] * 2); - bufferArray = buffer.array(); + @Override + public boolean isKeyFrame() { + return keyFrame; } - reader.getDataInput().readFully(bufferArray, 0, frameSize); - - buffer.position(0); - buffer.limit(frameSize); - return buffer; - } - - /** - * Parses the Matroska block header data into the fields of this instance. On success of this method, this instance - * effectively represents that block. - * - * @param reader The reader to use. - * @param element The block EBML element. - * @param trackFilter The ID of the track to read data for from the block. - * @return true of a block if it contains data for the requested track, false otherwise. - * @throws IOException On read error. - */ - public boolean parseHeader(MatroskaFileReader reader, MatroskaElement element, int trackFilter) throws IOException { - DataInput input = reader.getDataInput(); - trackNumber = (int) MatroskaEbmlReader.readEbmlInteger(input, null); - - if (trackFilter >= 0 && trackNumber != trackFilter) { - return false; + @Override + public int getFrameCount() { + return frameCount; } - timecode = input.readShort(); + @Override + public ByteBuffer getNextFrameBuffer(MatroskaFileReader reader, int index) throws IOException { + if (index >= frameCount) { + throw new IllegalArgumentException("Frame index out of bounds."); + } + + int frameSize = frameSizes[index]; - int flags = input.readByte() & 0xFF; - keyFrame = (flags & 0x80) != 0; + if (buffer == null || frameSize > buffer.capacity()) { + buffer = ByteBuffer.allocate(frameSizes[index] * 2); + bufferArray = buffer.array(); + } - int laceType = (flags & 0x06) >> 1; + reader.getDataInput().readFully(bufferArray, 0, frameSize); - if (laceType != 0) { - setFrameCount((input.readByte() & 0xFF) + 1); - parseLacing(reader, element, laceType); - } else { - setFrameCount(1); - frameSizes[0] = (int) element.getRemaining(reader.getPosition()); + buffer.position(0); + buffer.limit(frameSize); + return buffer; } - return true; - } - - private void parseLacing(MatroskaFileReader reader, MatroskaElement element, int laceType) throws IOException { - setFrameCount(frameCount); - - switch (laceType) { - case 1: - parseXiphLaceSizes(reader, element); - break; - case 2: - parseFixedLaceSizes(reader, element); - break; - case 3: - default: - parseEbmlLaceSizes(reader, element); + /** + * Parses the Matroska block header data into the fields of this instance. On success of this method, this instance + * effectively represents that block. + * + * @param reader The reader to use. + * @param element The block EBML element. + * @param trackFilter The ID of the track to read data for from the block. + * @return true of a block if it contains data for the requested track, false otherwise. + * @throws IOException On read error. + */ + public boolean parseHeader(MatroskaFileReader reader, MatroskaElement element, int trackFilter) throws IOException { + DataInput input = reader.getDataInput(); + trackNumber = (int) MatroskaEbmlReader.readEbmlInteger(input, null); + + if (trackFilter >= 0 && trackNumber != trackFilter) { + return false; + } + + timecode = input.readShort(); + + int flags = input.readByte() & 0xFF; + keyFrame = (flags & 0x80) != 0; + + int laceType = (flags & 0x06) >> 1; + + if (laceType != 0) { + setFrameCount((input.readByte() & 0xFF) + 1); + parseLacing(reader, element, laceType); + } else { + setFrameCount(1); + frameSizes[0] = (int) element.getRemaining(reader.getPosition()); + } + + return true; } - } - private void setFrameCount(int frameCount) { - if (frameSizes == null || frameSizes.length < frameCount) { - frameSizes = new int[frameCount * 2]; + private void parseLacing(MatroskaFileReader reader, MatroskaElement element, int laceType) throws IOException { + setFrameCount(frameCount); + + switch (laceType) { + case 1: + parseXiphLaceSizes(reader, element); + break; + case 2: + parseFixedLaceSizes(reader, element); + break; + case 3: + default: + parseEbmlLaceSizes(reader, element); + } } - this.frameCount = frameCount; - } + private void setFrameCount(int frameCount) { + if (frameSizes == null || frameSizes.length < frameCount) { + frameSizes = new int[frameCount * 2]; + } - private void parseXiphLaceSizes(MatroskaFileReader reader, MatroskaElement element) throws IOException { - int sizeTotal = 0; - DataInput input = reader.getDataInput(); + this.frameCount = frameCount; + } - for (int i = 0; i < frameCount - 1; i++) { - int value = 0; + private void parseXiphLaceSizes(MatroskaFileReader reader, MatroskaElement element) throws IOException { + int sizeTotal = 0; + DataInput input = reader.getDataInput(); - do { - value += input.readByte() & 0xFF; - } while (value == 255); + for (int i = 0; i < frameCount - 1; i++) { + int value = 0; - frameSizes[i] = value; - sizeTotal += value; - } + do { + value += input.readByte() & 0xFF; + } while (value == 255); - long remaining = element.getRemaining(reader.getPosition()); - frameSizes[frameCount - 1] = (int) remaining - sizeTotal; - } + frameSizes[i] = value; + sizeTotal += value; + } - private void parseFixedLaceSizes(MatroskaFileReader reader, MatroskaElement element) { - int size = (int) element.getRemaining(reader.getPosition()) / frameCount; + long remaining = element.getRemaining(reader.getPosition()); + frameSizes[frameCount - 1] = (int) remaining - sizeTotal; + } + + private void parseFixedLaceSizes(MatroskaFileReader reader, MatroskaElement element) { + int size = (int) element.getRemaining(reader.getPosition()) / frameCount; - for (int i = 0; i < frameCount; i++) { - frameSizes[i] = size; + for (int i = 0; i < frameCount; i++) { + frameSizes[i] = size; + } } - } - private void parseEbmlLaceSizes(MatroskaFileReader reader, MatroskaElement element) throws IOException { - DataInput input = reader.getDataInput(); + private void parseEbmlLaceSizes(MatroskaFileReader reader, MatroskaElement element) throws IOException { + DataInput input = reader.getDataInput(); - frameSizes[0] = (int) MatroskaEbmlReader.readEbmlInteger(input, null); - int sizeTotal = frameSizes[0]; + frameSizes[0] = (int) MatroskaEbmlReader.readEbmlInteger(input, null); + int sizeTotal = frameSizes[0]; - for (int i = 1; i < frameCount - 1; i++) { - frameSizes[i] = frameSizes[i - 1] + (int) MatroskaEbmlReader.readEbmlInteger(input, MatroskaEbmlReader.Type.LACE_SIGNED); - sizeTotal += frameSizes[i]; - } + for (int i = 1; i < frameCount - 1; i++) { + frameSizes[i] = frameSizes[i - 1] + (int) MatroskaEbmlReader.readEbmlInteger(input, MatroskaEbmlReader.Type.LACE_SIGNED); + sizeTotal += frameSizes[i]; + } - frameSizes[frameCount - 1] = (int) element.getRemaining(reader.getPosition()) - sizeTotal; - } + frameSizes[frameCount - 1] = (int) element.getRemaining(reader.getPosition()) - sizeTotal; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MutableMatroskaElement.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MutableMatroskaElement.java index f2360d2d0..1408219ce 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MutableMatroskaElement.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/matroska/format/MutableMatroskaElement.java @@ -7,7 +7,7 @@ public class MutableMatroskaElement extends MatroskaElement { protected MutableMatroskaElement(int level) { super(level); } - + public void setId(long id) { this.id = id; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3AudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3AudioTrack.java index b8879aa4a..2afa61b4f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3AudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3AudioTrack.java @@ -11,31 +11,31 @@ * Audio track that handles an MP3 stream */ public class Mp3AudioTrack extends BaseAudioTrack { - private static final Logger log = LoggerFactory.getLogger(Mp3AudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(Mp3AudioTrack.class); - private final SeekableInputStream inputStream; + private final SeekableInputStream inputStream; - /** - * @param trackInfo Track info - * @param inputStream Input stream for the MP3 file - */ - public Mp3AudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param inputStream Input stream for the MP3 file + */ + public Mp3AudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + super(trackInfo); - this.inputStream = inputStream; - } + this.inputStream = inputStream; + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - Mp3TrackProvider provider = new Mp3TrackProvider(localExecutor.getProcessingContext(), inputStream); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + Mp3TrackProvider provider = new Mp3TrackProvider(localExecutor.getProcessingContext(), inputStream); - try { - provider.parseHeaders(); + try { + provider.parseHeaders(); - log.debug("Starting to play MP3 track {}", getIdentifier()); - localExecutor.executeProcessingLoop(provider::provideFrames, provider::seekToTimecode); - } finally { - provider.close(); + log.debug("Starting to play MP3 track {}", getIdentifier()); + localExecutor.executeProcessingLoop(provider::provideFrames, provider::seekToTimecode); + } finally { + provider.close(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3ConstantRateSeeker.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3ConstantRateSeeker.java index ca272d648..f21116924 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3ConstantRateSeeker.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3ConstantRateSeeker.java @@ -3,6 +3,7 @@ import com.sedmelluq.discord.lavaplayer.natives.mp3.Mp3Decoder; import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; + import java.io.IOException; import static com.sedmelluq.discord.lavaplayer.natives.mp3.Mp3Decoder.MPEG1_SAMPLES_PER_FRAME; @@ -12,71 +13,71 @@ * supported. In case the file is not actually CBR, this being used as a fallback may cause inaccurate seeking. */ public class Mp3ConstantRateSeeker implements Mp3Seeker { - private static final int META_TAG_OFFSET = 36; - private static final byte[][] META_TAGS = new byte[][] { - new byte[] {'I', 'n', 'f', 'o'}, - new byte[] { 'L', 'A', 'M', 'E' } - }; - - private final double averageFrameSize; - private final int sampleRate; - private final long firstFramePosition; - private final long contentLength; - - private Mp3ConstantRateSeeker(double averageFrameSize, int sampleRate, long firstFramePosition, long contentLength) { - this.averageFrameSize = averageFrameSize; - this.sampleRate = sampleRate; - this.firstFramePosition = firstFramePosition; - this.contentLength = contentLength; - } - - /** - * @param firstFramePosition Position of the first frame in the file - * @param contentLength Total length of the file - * @param frameBuffer Buffer of the first frame - * @return Constant rate seeker, will always succeed, never null. - */ - public static Mp3ConstantRateSeeker createFromFrame(long firstFramePosition, long contentLength, byte[] frameBuffer) { - int sampleRate = Mp3Decoder.getFrameSampleRate(frameBuffer, 0); - double averageFrameSize = Mp3Decoder.getAverageFrameSize(frameBuffer, 0); - - return new Mp3ConstantRateSeeker(averageFrameSize, sampleRate, firstFramePosition, contentLength); - } - - public static boolean isMetaFrame(byte[] frameBuffer) { - for (byte[] metaTag : META_TAGS) { - if (DataFormatTools.arrayRangeEquals(frameBuffer, META_TAG_OFFSET, metaTag)) { - return true; - } + private static final int META_TAG_OFFSET = 36; + private static final byte[][] META_TAGS = new byte[][]{ + new byte[]{'I', 'n', 'f', 'o'}, + new byte[]{'L', 'A', 'M', 'E'} + }; + + private final double averageFrameSize; + private final int sampleRate; + private final long firstFramePosition; + private final long contentLength; + + private Mp3ConstantRateSeeker(double averageFrameSize, int sampleRate, long firstFramePosition, long contentLength) { + this.averageFrameSize = averageFrameSize; + this.sampleRate = sampleRate; + this.firstFramePosition = firstFramePosition; + this.contentLength = contentLength; } - return false; - } + /** + * @param firstFramePosition Position of the first frame in the file + * @param contentLength Total length of the file + * @param frameBuffer Buffer of the first frame + * @return Constant rate seeker, will always succeed, never null. + */ + public static Mp3ConstantRateSeeker createFromFrame(long firstFramePosition, long contentLength, byte[] frameBuffer) { + int sampleRate = Mp3Decoder.getFrameSampleRate(frameBuffer, 0); + double averageFrameSize = Mp3Decoder.getAverageFrameSize(frameBuffer, 0); + + return new Mp3ConstantRateSeeker(averageFrameSize, sampleRate, firstFramePosition, contentLength); + } - @Override - public long getDuration() { - return getMaximumFrameCount() * MPEG1_SAMPLES_PER_FRAME * 1000 / sampleRate; - } + public static boolean isMetaFrame(byte[] frameBuffer) { + for (byte[] metaTag : META_TAGS) { + if (DataFormatTools.arrayRangeEquals(frameBuffer, META_TAG_OFFSET, metaTag)) { + return true; + } + } - @Override - public boolean isSeekable() { - return true; - } + return false; + } - @Override - public long seekAndGetFrameIndex(long timecode, SeekableInputStream inputStream) throws IOException { - long maximumFrameCount = getMaximumFrameCount(); + @Override + public long getDuration() { + return getMaximumFrameCount() * MPEG1_SAMPLES_PER_FRAME * 1000 / sampleRate; + } - long sampleIndex = timecode * sampleRate / 1000; - long frameIndex = Math.min(sampleIndex / MPEG1_SAMPLES_PER_FRAME, maximumFrameCount); + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long seekAndGetFrameIndex(long timecode, SeekableInputStream inputStream) throws IOException { + long maximumFrameCount = getMaximumFrameCount(); - long seekPosition = (long) (frameIndex * averageFrameSize) - 8; - inputStream.seek(firstFramePosition + seekPosition); + long sampleIndex = timecode * sampleRate / 1000; + long frameIndex = Math.min(sampleIndex / MPEG1_SAMPLES_PER_FRAME, maximumFrameCount); - return frameIndex; - } + long seekPosition = (long) (frameIndex * averageFrameSize) - 8; + inputStream.seek(firstFramePosition + seekPosition); - private long getMaximumFrameCount() { - return (long) ((contentLength - firstFramePosition + 8) / averageFrameSize); - } + return frameIndex; + } + + private long getMaximumFrameCount() { + return (long) ((contentLength - firstFramePosition + 8) / averageFrameSize); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3ContainerProbe.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3ContainerProbe.java index 8b0187776..c2d2c18ac 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3ContainerProbe.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3ContainerProbe.java @@ -21,50 +21,50 @@ * Container detection probe for MP3 format. */ public class Mp3ContainerProbe implements MediaContainerProbe { - private static final Logger log = LoggerFactory.getLogger(Mp3ContainerProbe.class); + private static final Logger log = LoggerFactory.getLogger(Mp3ContainerProbe.class); - private static final int[] ID3_TAG = new int[] { 0x49, 0x44, 0x33 }; + private static final int[] ID3_TAG = new int[]{0x49, 0x44, 0x33}; - @Override - public String getName() { - return "mp3"; - } + @Override + public String getName() { + return "mp3"; + } - @Override - public boolean matchesHints(MediaContainerHints hints) { - boolean invalidMimeType = hints.mimeType != null && !"audio/mpeg".equalsIgnoreCase(hints.mimeType); - boolean invalidFileExtension = hints.fileExtension != null && !"mp3".equalsIgnoreCase(hints.fileExtension); - return hints.present() && !invalidMimeType && !invalidFileExtension; - } + @Override + public boolean matchesHints(MediaContainerHints hints) { + boolean invalidMimeType = hints.mimeType != null && !"audio/mpeg".equalsIgnoreCase(hints.mimeType); + boolean invalidFileExtension = hints.fileExtension != null && !"mp3".equalsIgnoreCase(hints.fileExtension); + return hints.present() && !invalidMimeType && !invalidFileExtension; + } - @Override - public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { - if (!checkNextBytes(inputStream, ID3_TAG)) { - byte[] frameHeader = new byte[4]; - Mp3FrameReader frameReader = new Mp3FrameReader(inputStream, frameHeader); - if (!frameReader.scanForFrame(STREAM_SCAN_DISTANCE, false)) { - return null; - } + @Override + public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { + if (!checkNextBytes(inputStream, ID3_TAG)) { + byte[] frameHeader = new byte[4]; + Mp3FrameReader frameReader = new Mp3FrameReader(inputStream, frameHeader); + if (!frameReader.scanForFrame(STREAM_SCAN_DISTANCE, false)) { + return null; + } - inputStream.seek(0); - } + inputStream.seek(0); + } - log.debug("Track {} is an MP3 file.", reference.identifier); + log.debug("Track {} is an MP3 file.", reference.identifier); - Mp3TrackProvider file = new Mp3TrackProvider(null, inputStream); + Mp3TrackProvider file = new Mp3TrackProvider(null, inputStream); - try { - file.parseHeaders(); + try { + file.parseHeaders(); - return supportedFormat(this, null, AudioTrackInfoBuilder.create(reference, inputStream) - .apply(file).setIsStream(!file.isSeekable()).build()); - } finally { - file.close(); + return supportedFormat(this, null, AudioTrackInfoBuilder.create(reference, inputStream) + .apply(file).setIsStream(!file.isSeekable()).build()); + } finally { + file.close(); + } } - } - @Override - public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - return new Mp3AudioTrack(trackInfo, inputStream); - } + @Override + public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + return new Mp3AudioTrack(trackInfo, inputStream); + } } 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 d2aa8cb8f..31e2930e5 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 @@ -13,141 +13,142 @@ * Handles reading MP3 frames from a stream. */ public class Mp3FrameReader { - private final SeekableInputStream inputStream; - private final DataInput dataInput; - private final byte[] scanBuffer; - private final byte[] frameBuffer; - private int frameSize; - private int frameBufferPosition; - private int scanBufferPosition; - private boolean frameHeaderRead; - - /** - * @param inputStream Input buffer to read from - * @param frameBuffer Array to store the frame data in - */ - public Mp3FrameReader(SeekableInputStream inputStream, byte[] frameBuffer) { - this.inputStream = inputStream; - this.dataInput = new DataInputStream(inputStream); - this.scanBuffer = new byte[16]; - this.frameBuffer = frameBuffer; - } - - /** - * @param bytesToCheck The maximum number of bytes to check before throwing an IllegalStateException - * @param throwOnLimit Whether to throw an exception when maximum number of bytes is reached, but no frame has been - * found and EOF has not been reached. - * @return True if a frame was found, false if EOF was encountered. - * @throws IOException On IO error - * @throws IllegalStateException If the maximum number of bytes to check was reached before a frame was found - */ - public boolean scanForFrame(int bytesToCheck, boolean throwOnLimit) throws IOException { - int bytesInBuffer = scanBufferPosition; - scanBufferPosition = 0; - - if (parseFrameAt(bytesInBuffer)) { - frameHeaderRead = true; - return true; + private final SeekableInputStream inputStream; + private final DataInput dataInput; + private final byte[] scanBuffer; + private final byte[] frameBuffer; + private int frameSize; + private int frameBufferPosition; + private int scanBufferPosition; + private boolean frameHeaderRead; + + /** + * @param inputStream Input buffer to read from + * @param frameBuffer Array to store the frame data in + */ + public Mp3FrameReader(SeekableInputStream inputStream, byte[] frameBuffer) { + this.inputStream = inputStream; + this.dataInput = new DataInputStream(inputStream); + this.scanBuffer = new byte[16]; + this.frameBuffer = frameBuffer; } - return runFrameScanLoop(bytesToCheck - bytesInBuffer, bytesInBuffer, throwOnLimit); - } - - private boolean runFrameScanLoop(int bytesToCheck, int bytesInBuffer, boolean throwOnLimit) throws IOException { - while (bytesToCheck > 0) { - for (int i = bytesInBuffer; i < scanBuffer.length && bytesToCheck > 0; i++, bytesToCheck--) { - int next = inputStream.read(); - if (next == -1) { - return false; + /** + * @param bytesToCheck The maximum number of bytes to check before throwing an IllegalStateException + * @param throwOnLimit Whether to throw an exception when maximum number of bytes is reached, but no frame has been + * found and EOF has not been reached. + * @return True if a frame was found, false if EOF was encountered. + * @throws IOException On IO error + * @throws IllegalStateException If the maximum number of bytes to check was reached before a frame was found + */ + public boolean scanForFrame(int bytesToCheck, boolean throwOnLimit) throws IOException { + int bytesInBuffer = scanBufferPosition; + scanBufferPosition = 0; + + if (parseFrameAt(bytesInBuffer)) { + frameHeaderRead = true; + return true; } - scanBuffer[i] = (byte) (next & 0xFF); + return runFrameScanLoop(bytesToCheck - bytesInBuffer, bytesInBuffer, throwOnLimit); + } + + private boolean runFrameScanLoop(int bytesToCheck, int bytesInBuffer, boolean throwOnLimit) throws IOException { + while (bytesToCheck > 0) { + for (int i = bytesInBuffer; i < scanBuffer.length && bytesToCheck > 0; i++, bytesToCheck--) { + int next = inputStream.read(); + if (next == -1) { + return false; + } + + scanBuffer[i] = (byte) (next & 0xFF); - if (parseFrameAt(i + 1)) { - frameHeaderRead = true; - return true; + if (parseFrameAt(i + 1)) { + frameHeaderRead = true; + return true; + } + } + + bytesInBuffer = copyScanBufferEndToBeginning(); + } + + if (throwOnLimit) { + throw new IllegalStateException("Mp3 frame not found."); } - } - bytesInBuffer = copyScanBufferEndToBeginning(); + return false; } - if (throwOnLimit) { - throw new IllegalStateException("Mp3 frame not found."); + private int copyScanBufferEndToBeginning() { + for (int i = 0; i < HEADER_SIZE - 1; i++) { + scanBuffer[i] = scanBuffer[scanBuffer.length - HEADER_SIZE + i + 1]; + } + + return HEADER_SIZE - 1; } - return false; - } + 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]; + } + + frameBufferPosition = HEADER_SIZE; + return true; + } - private int copyScanBufferEndToBeginning() { - for (int i = 0; i < HEADER_SIZE - 1; i++) { - scanBuffer[i] = scanBuffer[scanBuffer.length - HEADER_SIZE + i + 1]; + return false; } - return HEADER_SIZE - 1; - } + /** + * Fills the buffer for the current frame. If no frame header has been read previously, it will first scan for the + * sync bytes of the next frame in the stream. + * + * @return False if EOF was encountered while looking for the next frame, true otherwise + * @throws IOException On IO error + */ + public boolean fillFrameBuffer() throws IOException { + if (!frameHeaderRead && !scanForFrame(Integer.MAX_VALUE, true)) { + return false; + } + + dataInput.readFully(frameBuffer, frameBufferPosition, frameSize - frameBufferPosition); + frameBufferPosition = frameSize; + return true; + } - 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]; - } + /** + * Forget the current frame and make next calls look for the next frame. + */ + public void nextFrame() { + frameHeaderRead = false; + frameBufferPosition = 0; + } - frameBufferPosition = HEADER_SIZE; - return true; + /** + * @return The start position of the current frame in the stream. + */ + public long getFrameStartPosition() { + return inputStream.getPosition() - frameBufferPosition; } - return false; - } - - /** - * Fills the buffer for the current frame. If no frame header has been read previously, it will first scan for the - * sync bytes of the next frame in the stream. - * @return False if EOF was encountered while looking for the next frame, true otherwise - * @throws IOException On IO error - */ - public boolean fillFrameBuffer() throws IOException { - if (!frameHeaderRead && !scanForFrame(Integer.MAX_VALUE, true)) { - return false; + /** + * @return Size of the current frame in bytes. + */ + public int getFrameSize() { + return frameSize; } - dataInput.readFully(frameBuffer, frameBufferPosition, frameSize - frameBufferPosition); - frameBufferPosition = frameSize; - return true; - } - - /** - * Forget the current frame and make next calls look for the next frame. - */ - public void nextFrame() { - frameHeaderRead = false; - frameBufferPosition = 0; - } - - /** - * @return The start position of the current frame in the stream. - */ - public long getFrameStartPosition() { - return inputStream.getPosition() - frameBufferPosition; - } - - /** - * @return Size of the current frame in bytes. - */ - public int getFrameSize() { - return frameSize; - } - - /** - * Append some bytes to the frame sync scan buffer. This must be called when some bytes have been read externally that - * may actually be part of the next frame header. - * - * @param data The buffer to copy from - * @param offset The offset in the buffer - * @param length The length of the region to copy - */ - public void appendToScanBuffer(byte[] data, int offset, int length) { - System.arraycopy(data, offset, scanBuffer, 0, length); - scanBufferPosition = length; - } + /** + * Append some bytes to the frame sync scan buffer. This must be called when some bytes have been read externally that + * may actually be part of the next frame header. + * + * @param data The buffer to copy from + * @param offset The offset in the buffer + * @param length The length of the region to copy + */ + public void appendToScanBuffer(byte[] data, int offset, int length) { + System.arraycopy(data, offset, scanBuffer, 0, length); + scanBufferPosition = length; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3Seeker.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3Seeker.java index 0351d2eec..f1b89faca 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3Seeker.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3Seeker.java @@ -8,21 +8,21 @@ * A seeking handler for MP3 files. */ public interface Mp3Seeker { - /** - * @return The duration of the file in milliseconds. May be an estimate. - */ - long getDuration(); + /** + * @return The duration of the file in milliseconds. May be an estimate. + */ + long getDuration(); - /** - * @return True if the track is seekable. - */ - boolean isSeekable(); + /** + * @return True if the track is seekable. + */ + boolean isSeekable(); - /** - * @param timecode The timecode that the seek is requested to - * @param inputStream The input stream to perform the seek on - * @return The index of the frame that the seek was performed to - * @throws IOException On IO error - */ - long seekAndGetFrameIndex(long timecode, SeekableInputStream inputStream) throws IOException; + /** + * @param timecode The timecode that the seek is requested to + * @param inputStream The input stream to perform the seek on + * @return The index of the frame that the seek was performed to + * @throws IOException On IO error + */ + long seekAndGetFrameIndex(long timecode, SeekableInputStream inputStream) throws IOException; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3StreamSeeker.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3StreamSeeker.java index df4aaff42..ee7ad8025 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3StreamSeeker.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3StreamSeeker.java @@ -10,18 +10,18 @@ * duration. */ public class Mp3StreamSeeker implements Mp3Seeker { - @Override - public long getDuration() { - return Units.DURATION_MS_UNKNOWN; - } + @Override + public long getDuration() { + return Units.DURATION_MS_UNKNOWN; + } - @Override - public boolean isSeekable() { - return false; - } + @Override + public boolean isSeekable() { + return false; + } - @Override - public long seekAndGetFrameIndex(long timecode, SeekableInputStream inputStream) throws IOException { - throw new UnsupportedOperationException("Cannot seek on a stream."); - } + @Override + public long seekAndGetFrameIndex(long timecode, SeekableInputStream inputStream) throws IOException { + throw new UnsupportedOperationException("Cannot seek on a stream."); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3TrackProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3TrackProvider.java index f1988ab21..bd46b586e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3TrackProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3TrackProvider.java @@ -26,378 +26,381 @@ * Handles parsing MP3 files, seeking and sending the decoded frames to the specified frame consumer. */ public class Mp3TrackProvider implements AudioTrackInfoProvider { - private static final byte[] IDV3_TAG = new byte[] { 0x49, 0x44, 0x33 }; - private static final int IDV3_FLAG_EXTENDED = 0x40; - - private static final String TITLE_TAG = "TIT2"; - private static final String ARTIST_TAG = "TPE1"; - - private static final List knownTextExtensions = Arrays.asList(TITLE_TAG, ARTIST_TAG); - - private final AudioProcessingContext context; - private final SeekableInputStream inputStream; - private final DataInputStream dataInput; - private final Mp3Decoder mp3Decoder; - private final ShortBuffer outputBuffer; - private final ByteBuffer inputBuffer; - private final byte[] frameBuffer; - private final byte[] tagHeaderBuffer; - private final Mp3FrameReader frameReader; - private final Map tags; - - private int sampleRate; - private int channelCount; - private AudioPipeline downstream; - private Mp3Seeker seeker; - - /** - * @param context Configuration and output information for processing. May be null in case no frames are read and this - * instance is only used to retrieve information about the track. - * @param inputStream Stream to read the file from - */ - public Mp3TrackProvider(AudioProcessingContext context, SeekableInputStream inputStream) { - this.context = context; - this.inputStream = inputStream; - this.dataInput = new DataInputStream(inputStream); - this.outputBuffer = ByteBuffer.allocateDirect((int) MPEG1_SAMPLES_PER_FRAME * 4).order(ByteOrder.nativeOrder()).asShortBuffer(); - this.inputBuffer = ByteBuffer.allocateDirect(Mp3Decoder.getMaximumFrameSize()); - this.frameBuffer = new byte[Mp3Decoder.getMaximumFrameSize()]; - this.tagHeaderBuffer = new byte[4]; - this.frameReader = new Mp3FrameReader(inputStream, frameBuffer); - this.mp3Decoder = new Mp3Decoder(); - this.tags = new HashMap<>(); - } - - /** - * Parses file headers to find the first MP3 frame and to get the settings for initialising the filter chain. - * @throws IOException On read error - */ - public void parseHeaders() throws IOException { - skipIdv3Tags(); - - if (!frameReader.scanForFrame(2048, true)) { - throw new IllegalStateException("File ended before the first frame was found."); + private static final byte[] IDV3_TAG = new byte[]{0x49, 0x44, 0x33}; + private static final int IDV3_FLAG_EXTENDED = 0x40; + + private static final String TITLE_TAG = "TIT2"; + private static final String ARTIST_TAG = "TPE1"; + + private static final List knownTextExtensions = Arrays.asList(TITLE_TAG, ARTIST_TAG); + + private final AudioProcessingContext context; + private final SeekableInputStream inputStream; + private final DataInputStream dataInput; + private final Mp3Decoder mp3Decoder; + private final ShortBuffer outputBuffer; + private final ByteBuffer inputBuffer; + private final byte[] frameBuffer; + private final byte[] tagHeaderBuffer; + private final Mp3FrameReader frameReader; + private final Map tags; + + private int sampleRate; + private int channelCount; + private AudioPipeline downstream; + private Mp3Seeker seeker; + + /** + * @param context Configuration and output information for processing. May be null in case no frames are read and this + * instance is only used to retrieve information about the track. + * @param inputStream Stream to read the file from + */ + public Mp3TrackProvider(AudioProcessingContext context, SeekableInputStream inputStream) { + this.context = context; + this.inputStream = inputStream; + this.dataInput = new DataInputStream(inputStream); + this.outputBuffer = ByteBuffer.allocateDirect((int) MPEG1_SAMPLES_PER_FRAME * 4).order(ByteOrder.nativeOrder()).asShortBuffer(); + this.inputBuffer = ByteBuffer.allocateDirect(Mp3Decoder.getMaximumFrameSize()); + this.frameBuffer = new byte[Mp3Decoder.getMaximumFrameSize()]; + this.tagHeaderBuffer = new byte[4]; + this.frameReader = new Mp3FrameReader(inputStream, frameBuffer); + this.mp3Decoder = new Mp3Decoder(); + this.tags = new HashMap<>(); } - sampleRate = Mp3Decoder.getFrameSampleRate(frameBuffer, 0); - channelCount = Mp3Decoder.getFrameChannelCount(frameBuffer, 0); - downstream = context != null ? AudioPipelineFactory.create(context, new PcmFormat(channelCount, sampleRate)) : null; + /** + * Parses file headers to find the first MP3 frame and to get the settings for initialising the filter chain. + * + * @throws IOException On read error + */ + public void parseHeaders() throws IOException { + skipIdv3Tags(); - initialiseSeeker(); - } + if (!frameReader.scanForFrame(2048, true)) { + throw new IllegalStateException("File ended before the first frame was found."); + } - private void initialiseSeeker() throws IOException { - long startPosition = frameReader.getFrameStartPosition(); - frameReader.fillFrameBuffer(); + sampleRate = Mp3Decoder.getFrameSampleRate(frameBuffer, 0); + channelCount = Mp3Decoder.getFrameChannelCount(frameBuffer, 0); + downstream = context != null ? AudioPipelineFactory.create(context, new PcmFormat(channelCount, sampleRate)) : null; - seeker = Mp3XingSeeker.createFromFrame(startPosition, inputStream.getContentLength(), frameBuffer); + initialiseSeeker(); + } - if (seeker == null) { - if (inputStream.getContentLength() == Units.CONTENT_LENGTH_UNKNOWN) { - seeker = new Mp3StreamSeeker(); - } else { - if (context == null) { - // Skip meta frames if this provider is created only for reading metadata. - for (int i = 0; Mp3ConstantRateSeeker.isMetaFrame(frameBuffer) && i < 2; i++) { - frameReader.nextFrame(); - frameReader.fillFrameBuffer(); - } + private void initialiseSeeker() throws IOException { + long startPosition = frameReader.getFrameStartPosition(); + frameReader.fillFrameBuffer(); + + seeker = Mp3XingSeeker.createFromFrame(startPosition, inputStream.getContentLength(), frameBuffer); + + if (seeker == null) { + if (inputStream.getContentLength() == Units.CONTENT_LENGTH_UNKNOWN) { + seeker = new Mp3StreamSeeker(); + } else { + if (context == null) { + // Skip meta frames if this provider is created only for reading metadata. + for (int i = 0; Mp3ConstantRateSeeker.isMetaFrame(frameBuffer) && i < 2; i++) { + frameReader.nextFrame(); + frameReader.fillFrameBuffer(); + } + } + + seeker = Mp3ConstantRateSeeker.createFromFrame(startPosition, inputStream.getContentLength(), frameBuffer); + } } + } - seeker = Mp3ConstantRateSeeker.createFromFrame(startPosition, inputStream.getContentLength(), frameBuffer); - } + /** + * Decodes audio frames and sends them to frame consumer + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + public void provideFrames() throws InterruptedException { + try { + while (true) { + if (!frameReader.fillFrameBuffer()) { + break; + } + + inputBuffer.clear(); + inputBuffer.put(frameBuffer, 0, frameReader.getFrameSize()); + inputBuffer.flip(); + + outputBuffer.clear(); + outputBuffer.limit(channelCount * (int) Mp3Decoder.getSamplesPerFrame(frameBuffer, 0)); + + int produced = mp3Decoder.decode(inputBuffer, outputBuffer); + + if (produced > 0) { + downstream.process(outputBuffer); + } + + frameReader.nextFrame(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - - /** - * Decodes audio frames and sends them to frame consumer - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - public void provideFrames() throws InterruptedException { - try { - while (true) { - if (!frameReader.fillFrameBuffer()) { - break; + + /** + * Seeks to the specified timecode. + * + * @param timecode The timecode in milliseconds + */ + public void seekToTimecode(long timecode) { + try { + long frameIndex = seeker.seekAndGetFrameIndex(timecode, inputStream); + long actualTimecode = frameIndex * MPEG1_SAMPLES_PER_FRAME * 1000 / sampleRate; + downstream.seekPerformed(timecode, actualTimecode); + + frameReader.nextFrame(); + } catch (IOException e) { + throw new RuntimeException(e); } + } - inputBuffer.clear(); - inputBuffer.put(frameBuffer, 0, frameReader.getFrameSize()); - inputBuffer.flip(); + /** + * @return True if the track is seekable (false for streams for example). + */ + public boolean isSeekable() { + return seeker.isSeekable(); + } - outputBuffer.clear(); - outputBuffer.limit(channelCount * (int) Mp3Decoder.getSamplesPerFrame(frameBuffer, 0)); + /** + * @return An estimated duration of the file in milliseconds + */ + public long getDuration() { + return seeker.getDuration(); + } - int produced = mp3Decoder.decode(inputBuffer, outputBuffer); + /** + * Gets an ID3 tag. These are loaded when parsing headers and only for a fixed list of tags. + * + * @param tagId The FourCC of the tag + * @return The value of the tag if present, otherwise null + */ + public String getIdv3Tag(String tagId) { + return tags.get(tagId); + } - if (produced > 0) { - downstream.process(outputBuffer); + /** + * Closes resources. + */ + public void close() { + if (downstream != null) { + downstream.close(); } - frameReader.nextFrame(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Seeks to the specified timecode. - * @param timecode The timecode in milliseconds - */ - public void seekToTimecode(long timecode) { - try { - long frameIndex = seeker.seekAndGetFrameIndex(timecode, inputStream); - long actualTimecode = frameIndex * MPEG1_SAMPLES_PER_FRAME * 1000 / sampleRate; - downstream.seekPerformed(timecode, actualTimecode); - - frameReader.nextFrame(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * @return True if the track is seekable (false for streams for example). - */ - public boolean isSeekable() { - return seeker.isSeekable(); - } - - /** - * @return An estimated duration of the file in milliseconds - */ - public long getDuration() { - return seeker.getDuration(); - } - - /** - * Gets an ID3 tag. These are loaded when parsing headers and only for a fixed list of tags. - * - * @param tagId The FourCC of the tag - * @return The value of the tag if present, otherwise null - */ - public String getIdv3Tag(String tagId) { - return tags.get(tagId); - } - - /** - * Closes resources. - */ - public void close() { - if (downstream != null) { - downstream.close(); + mp3Decoder.close(); } - mp3Decoder.close(); - } + private void skipIdv3Tags() throws IOException { + dataInput.readFully(tagHeaderBuffer, 0, 3); - private void skipIdv3Tags() throws IOException { - dataInput.readFully(tagHeaderBuffer, 0, 3); + for (int i = 0; i < 3; i++) { + if (tagHeaderBuffer[i] != IDV3_TAG[i]) { + frameReader.appendToScanBuffer(tagHeaderBuffer, 0, 3); + return; + } + } - for (int i = 0; i < 3; i++) { - if (tagHeaderBuffer[i] != IDV3_TAG[i]) { - frameReader.appendToScanBuffer(tagHeaderBuffer, 0, 3); - return; - } - } + int majorVersion = dataInput.readByte() & 0xFF; + // Minor version + dataInput.readByte(); - int majorVersion = dataInput.readByte() & 0xFF; - // Minor version - dataInput.readByte(); + if (majorVersion < 2 || majorVersion > 5) { + return; + } - if (majorVersion < 2 || majorVersion > 5) { - return; - } + int flags = dataInput.readByte() & 0xFF; + int tagsSize = readSyncProofInteger(); - int flags = dataInput.readByte() & 0xFF; - int tagsSize = readSyncProofInteger(); + long tagsEndPosition = inputStream.getPosition() + tagsSize; - long tagsEndPosition = inputStream.getPosition() + tagsSize; + skipExtendedHeader(flags); - skipExtendedHeader(flags); + if (majorVersion < 5) { + parseIdv3Frames(majorVersion, tagsEndPosition); + } - if (majorVersion < 5) { - parseIdv3Frames(majorVersion, tagsEndPosition); + inputStream.seek(tagsEndPosition); } - inputStream.seek(tagsEndPosition); - } - - private int readSyncProofInteger() throws IOException { - return (dataInput.readByte() & 0xFF) << 21 - | (dataInput.readByte() & 0xFF) << 14 - | (dataInput.readByte() & 0xFF) << 7 - | (dataInput.readByte() & 0xFF); - } + private int readSyncProofInteger() throws IOException { + return (dataInput.readByte() & 0xFF) << 21 + | (dataInput.readByte() & 0xFF) << 14 + | (dataInput.readByte() & 0xFF) << 7 + | (dataInput.readByte() & 0xFF); + } - private int readSyncProof3ByteInteger() throws IOException { - return (dataInput.readByte() & 0xFF) << 14 - | (dataInput.readByte() & 0xFF) << 7 - | (dataInput.readByte() & 0xFF); - } + private int readSyncProof3ByteInteger() throws IOException { + return (dataInput.readByte() & 0xFF) << 14 + | (dataInput.readByte() & 0xFF) << 7 + | (dataInput.readByte() & 0xFF); + } - private void skipExtendedHeader(int flags) throws IOException { - if ((flags & IDV3_FLAG_EXTENDED) != 0) { - int size = readSyncProofInteger(); + private void skipExtendedHeader(int flags) throws IOException { + if ((flags & IDV3_FLAG_EXTENDED) != 0) { + int size = readSyncProofInteger(); - inputStream.seek(inputStream.getPosition() + size - 4); + inputStream.seek(inputStream.getPosition() + size - 4); + } } - } - private void parseIdv3Frames(int version, long tagsEndPosition) throws IOException { - FrameHeader header; + private void parseIdv3Frames(int version, long tagsEndPosition) throws IOException { + FrameHeader header; + + while (inputStream.getPosition() + 10 <= tagsEndPosition && (header = readFrameHeader(version)) != null) { + long nextTagPosition = inputStream.getPosition() + header.size; - while (inputStream.getPosition() + 10 <= tagsEndPosition && (header = readFrameHeader(version)) != null) { - long nextTagPosition = inputStream.getPosition() + header.size; + if (header.hasRawFormat() && knownTextExtensions.contains(header.id)) { + String text = parseIdv3TextContent(header.size); - if (header.hasRawFormat() && knownTextExtensions.contains(header.id)) { - String text = parseIdv3TextContent(header.size); + if (text != null) { + tags.put(header.id, text); + } + } - if (text != null) { - tags.put(header.id, text); + inputStream.seek(nextTagPosition); } - } + } - inputStream.seek(nextTagPosition); + private String parseIdv3TextContent(int size) throws IOException { + int encoding = dataInput.readByte() & 0xFF; + + byte[] data = new byte[size - 1]; + dataInput.readFully(data); + + boolean shortTerminator = data.length > 0 && data[data.length - 1] == 0; + boolean wideTerminator = data.length > 1 && data[data.length - 2] == 0 && shortTerminator; + + switch (encoding) { + case 0: + return new String(data, 0, size - (shortTerminator ? 2 : 1), "ISO-8859-1"); + case 1: + return new String(data, 0, size - (wideTerminator ? 3 : 1), "UTF-16"); + case 2: + return new String(data, 0, size - (wideTerminator ? 3 : 1), "UTF-16BE"); + case 3: + return new String(data, 0, size - (shortTerminator ? 2 : 1), "UTF-8"); + default: + return null; + } } - } - - private String parseIdv3TextContent(int size) throws IOException { - int encoding = dataInput.readByte() & 0xFF; - - byte[] data = new byte[size - 1]; - dataInput.readFully(data); - - boolean shortTerminator = data.length > 0 && data[data.length - 1] == 0; - boolean wideTerminator = data.length > 1 && data[data.length - 2] == 0 && shortTerminator; - - switch (encoding) { - case 0: - return new String(data, 0, size - (shortTerminator ? 2 : 1), "ISO-8859-1"); - case 1: - return new String(data, 0, size - (wideTerminator ? 3 : 1), "UTF-16"); - case 2: - return new String(data, 0, size - (wideTerminator ? 3 : 1), "UTF-16BE"); - case 3: - return new String(data, 0, size - (shortTerminator ? 2 : 1), "UTF-8"); - default: - return null; + + private String readId3v22TagName() throws IOException { + dataInput.readFully(tagHeaderBuffer, 0, 3); + + if (tagHeaderBuffer[0] == 0) { + return null; + } + + String shortName = new String(tagHeaderBuffer, 0, 3, StandardCharsets.ISO_8859_1); + + if ("TT2".equals(shortName)) { + return "TIT2"; + } else if ("TP1".equals(shortName)) { + return "TPE1"; + } else { + return shortName; + } } - } - private String readId3v22TagName() throws IOException { - dataInput.readFully(tagHeaderBuffer, 0, 3); + private String readTagName() throws IOException { + dataInput.readFully(tagHeaderBuffer, 0, 4); + + if (tagHeaderBuffer[0] == 0) { + return null; + } - if (tagHeaderBuffer[0] == 0) { - return null; + return new String(tagHeaderBuffer, 0, 4, StandardCharsets.ISO_8859_1); } - String shortName = new String(tagHeaderBuffer, 0, 3, StandardCharsets.ISO_8859_1); + private FrameHeader readFrameHeader(int version) throws IOException { + if (version == 2) { + String tagName = readId3v22TagName(); + + if (tagName != null) { + return new FrameHeader(tagName, readSyncProof3ByteInteger(), 0); + } + } else { + String tagName = readTagName(); + + if (tagName != null) { + int size = version == 3 ? dataInput.readInt() : readSyncProofInteger(); + return new FrameHeader(tagName, size, dataInput.readUnsignedShort()); + } + } - if ("TT2".equals(shortName)) { - return "TIT2"; - } else if ("TP1".equals(shortName)) { - return "TPE1"; - } else { - return shortName; + return null; } - } - private String readTagName() throws IOException { - dataInput.readFully(tagHeaderBuffer, 0, 4); + @Override + public String getTitle() { + return getIdv3Tag(TITLE_TAG); + } - if (tagHeaderBuffer[0] == 0) { - return null; + @Override + public String getAuthor() { + return getIdv3Tag(ARTIST_TAG); } - return new String(tagHeaderBuffer, 0, 4, StandardCharsets.ISO_8859_1); - } + @Override + public Long getLength() { + return getDuration(); + } - private FrameHeader readFrameHeader(int version) throws IOException { - if (version == 2) { - String tagName = readId3v22TagName(); + @Override + public String getIdentifier() { + return null; + } - if (tagName != null) { - return new FrameHeader(tagName, readSyncProof3ByteInteger(), 0); - } - } else { - String tagName = readTagName(); + @Override + public String getUri() { + return null; + } - if (tagName != null) { - int size = version == 3 ? dataInput.readInt() : readSyncProofInteger(); - return new FrameHeader(tagName, size, dataInput.readUnsignedShort()); - } + @Override + public String getArtworkUrl() { + return null; } - return null; - } - - @Override - public String getTitle() { - return getIdv3Tag(TITLE_TAG); - } - - @Override - public String getAuthor() { - return getIdv3Tag(ARTIST_TAG); - } - - @Override - public Long getLength() { - return getDuration(); - } - - @Override - public String getIdentifier() { - return null; - } - - @Override - public String getUri() { - return null; - } - - @Override - public String getArtworkUrl() { - return null; - } - - @Override - public String getISRC() { - return null; - } - - private static class FrameHeader { - private final String id; - private final int size; - @SuppressWarnings("unused") - private final boolean tagAlterPreservation; - @SuppressWarnings("unused") - private final boolean fileAlterPreservation; - @SuppressWarnings("unused") - private final boolean readOnly; - @SuppressWarnings("unused") - private final boolean groupingIdentity; - private final boolean compression; - private final boolean encryption; - private final boolean unsynchronization; - private final boolean dataLengthIndicator; - - private FrameHeader(String id, int size, int flags) { - this.id = id; - this.size = size; - this.tagAlterPreservation = (flags & 0x4000) != 0; - this.fileAlterPreservation = (flags & 0x2000) != 0; - this.readOnly = (flags & 0x1000) != 0; - this.groupingIdentity = (flags & 0x0040) != 0; - this.compression = (flags & 0x0008) != 0; - this.encryption = (flags & 0x0004) != 0; - this.unsynchronization = (flags & 0x0002) != 0; - this.dataLengthIndicator = (flags & 0x0001) != 0; + @Override + public String getISRC() { + return null; } - private boolean hasRawFormat() { - return !compression && !encryption && !unsynchronization && !dataLengthIndicator; + private static class FrameHeader { + private final String id; + private final int size; + @SuppressWarnings("unused") + private final boolean tagAlterPreservation; + @SuppressWarnings("unused") + private final boolean fileAlterPreservation; + @SuppressWarnings("unused") + private final boolean readOnly; + @SuppressWarnings("unused") + private final boolean groupingIdentity; + private final boolean compression; + private final boolean encryption; + private final boolean unsynchronization; + private final boolean dataLengthIndicator; + + private FrameHeader(String id, int size, int flags) { + this.id = id; + this.size = size; + this.tagAlterPreservation = (flags & 0x4000) != 0; + this.fileAlterPreservation = (flags & 0x2000) != 0; + this.readOnly = (flags & 0x1000) != 0; + this.groupingIdentity = (flags & 0x0040) != 0; + this.compression = (flags & 0x0008) != 0; + this.encryption = (flags & 0x0004) != 0; + this.unsynchronization = (flags & 0x0002) != 0; + this.dataLengthIndicator = (flags & 0x0001) != 0; + } + + private boolean hasRawFormat() { + return !compression && !encryption && !unsynchronization && !dataLengthIndicator; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3XingSeeker.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3XingSeeker.java index 97914d29f..42e338a69 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3XingSeeker.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mp3/Mp3XingSeeker.java @@ -14,73 +14,73 @@ * Seeking support for VBR files with Xing header. */ public class Mp3XingSeeker implements Mp3Seeker { - private static final Logger log = LoggerFactory.getLogger(Mp3XingSeeker.class); - - private static final int XING_OFFSET = 36; - private static final int ALL_FLAGS = 0x7; - private static final ByteBuffer xingTagBuffer = ByteBuffer.wrap(new byte[] { 0x58, 0x69, 0x6E, 0x67 }); - - private final long firstFramePosition; - private final long contentLength; - private final long frameCount; - private final long dataSize; - private final byte[] seekMapping; - private final long duration; - - private Mp3XingSeeker(int sampleRate, long firstFramePosition, long contentLength, long frameCount, long dataSize, byte[] seekMapping) { - this.firstFramePosition = firstFramePosition; - this.contentLength = contentLength; - this.frameCount = frameCount; - this.dataSize = dataSize; - this.seekMapping = seekMapping; - this.duration = frameCount * MPEG1_SAMPLES_PER_FRAME * 1000L / sampleRate; - } - - /** - * @param firstFramePosition Position of the first frame in the file - * @param contentLength Total length of the file - * @param frameBuffer Buffer of the first frame - * @return Xing seeker, if its header is found in the first frame and has all the necessary fields - */ - public static Mp3XingSeeker createFromFrame(long firstFramePosition, long contentLength, byte[] frameBuffer) { - ByteBuffer frame = ByteBuffer.wrap(frameBuffer); - - if (frame.getInt(XING_OFFSET) != xingTagBuffer.getInt(0)) { - return null; - } else if ((frame.getInt(XING_OFFSET + 4) & ALL_FLAGS) != ALL_FLAGS) { - log.debug("Xing tag is present, but is missing some required fields."); - return null; + private static final Logger log = LoggerFactory.getLogger(Mp3XingSeeker.class); + + private static final int XING_OFFSET = 36; + private static final int ALL_FLAGS = 0x7; + private static final ByteBuffer xingTagBuffer = ByteBuffer.wrap(new byte[]{0x58, 0x69, 0x6E, 0x67}); + + private final long firstFramePosition; + private final long contentLength; + private final long frameCount; + private final long dataSize; + private final byte[] seekMapping; + private final long duration; + + private Mp3XingSeeker(int sampleRate, long firstFramePosition, long contentLength, long frameCount, long dataSize, byte[] seekMapping) { + this.firstFramePosition = firstFramePosition; + this.contentLength = contentLength; + this.frameCount = frameCount; + this.dataSize = dataSize; + this.seekMapping = seekMapping; + this.duration = frameCount * MPEG1_SAMPLES_PER_FRAME * 1000L / sampleRate; } - int sampleRate = Mp3Decoder.getFrameSampleRate(frameBuffer, 0); - long frameCount = frame.getInt(XING_OFFSET + 8); - long dataSize = frame.getInt(XING_OFFSET + 12); - - byte[] seekMapping = new byte[100]; - frame.position(XING_OFFSET + 16); - frame.get(seekMapping); - - return new Mp3XingSeeker(sampleRate, firstFramePosition, contentLength, frameCount, dataSize, seekMapping); - } + /** + * @param firstFramePosition Position of the first frame in the file + * @param contentLength Total length of the file + * @param frameBuffer Buffer of the first frame + * @return Xing seeker, if its header is found in the first frame and has all the necessary fields + */ + public static Mp3XingSeeker createFromFrame(long firstFramePosition, long contentLength, byte[] frameBuffer) { + ByteBuffer frame = ByteBuffer.wrap(frameBuffer); + + if (frame.getInt(XING_OFFSET) != xingTagBuffer.getInt(0)) { + return null; + } else if ((frame.getInt(XING_OFFSET + 4) & ALL_FLAGS) != ALL_FLAGS) { + log.debug("Xing tag is present, but is missing some required fields."); + return null; + } + + int sampleRate = Mp3Decoder.getFrameSampleRate(frameBuffer, 0); + long frameCount = frame.getInt(XING_OFFSET + 8); + long dataSize = frame.getInt(XING_OFFSET + 12); + + byte[] seekMapping = new byte[100]; + frame.position(XING_OFFSET + 16); + frame.get(seekMapping); + + return new Mp3XingSeeker(sampleRate, firstFramePosition, contentLength, frameCount, dataSize, seekMapping); + } - @Override - public long getDuration() { - return duration; - } + @Override + public long getDuration() { + return duration; + } - @Override - public boolean isSeekable() { - return true; - } + @Override + public boolean isSeekable() { + return true; + } - @Override - public long seekAndGetFrameIndex(long timecode, SeekableInputStream inputStream) throws IOException { - int percentile = (int) (timecode * 100L / duration); - long frameIndex = frameCount * percentile / 100L; + @Override + public long seekAndGetFrameIndex(long timecode, SeekableInputStream inputStream) throws IOException { + int percentile = (int) (timecode * 100L / duration); + long frameIndex = frameCount * percentile / 100L; - long seekPosition = Math.min(firstFramePosition + dataSize * (seekMapping[percentile] & 0xFF) / 256, contentLength); - inputStream.seek(seekPosition); + long seekPosition = Math.min(firstFramePosition + dataSize * (seekMapping[percentile] & 0xFF) / 256, contentLength); + inputStream.seek(seekPosition); - return frameIndex; - } + return frameIndex; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegAacTrackConsumer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegAacTrackConsumer.java index a8e23172e..0d6a57edb 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegAacTrackConsumer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegAacTrackConsumer.java @@ -18,108 +18,108 @@ * format is supported, although the underlying decoder can handler other types as well. */ public class MpegAacTrackConsumer implements MpegTrackConsumer { - private static final Logger log = LoggerFactory.getLogger(MpegAacTrackConsumer.class); - - private final MpegTrackInfo track; - private final AacPacketRouter packetRouter; - - private ByteBuffer inputBuffer; - private boolean configured; - - /** - * @param context Configuration and output information for processing - * @param track The MP4 audio track descriptor - */ - public MpegAacTrackConsumer(AudioProcessingContext context, MpegTrackInfo track) { - this.track = track; - this.packetRouter = new AacPacketRouter(context); - } - - @Override - public void initialise() { - log.debug("Initialising AAC track with expected frequency {} and channel count {}.", - track.sampleRate, track.channelCount); - } - - @Override - public MpegTrackInfo getTrack() { - return track; - } - - @Override - public void seekPerformed(long requestedTimecode, long providedTimecode) { - packetRouter.seekPerformed(requestedTimecode, providedTimecode); - } - - @Override - public void flush() throws InterruptedException { - packetRouter.flush(); - } - - @Override - public void consume(ReadableByteChannel channel, int length) throws InterruptedException { - if (packetRouter.nativeDecoder == null) { - packetRouter.nativeDecoder = new AacDecoder(); - configured = configureDecoder(packetRouter.nativeDecoder); + private static final Logger log = LoggerFactory.getLogger(MpegAacTrackConsumer.class); + + private final MpegTrackInfo track; + private final AacPacketRouter packetRouter; + + private ByteBuffer inputBuffer; + private boolean configured; + + /** + * @param context Configuration and output information for processing + * @param track The MP4 audio track descriptor + */ + public MpegAacTrackConsumer(AudioProcessingContext context, MpegTrackInfo track) { + this.track = track; + this.packetRouter = new AacPacketRouter(context); } - if (configured) { - if (inputBuffer == null) { - inputBuffer = ByteBuffer.allocateDirect(4096); - } + @Override + public void initialise() { + log.debug("Initialising AAC track with expected frequency {} and channel count {}.", + track.sampleRate, track.channelCount); + } - processInput(channel, length); - } else { - if (packetRouter.embeddedDecoder == null) { - if (track.decoderConfig != null) { - packetRouter.embeddedDecoder = Decoder.create(track.decoderConfig); - } else { - packetRouter.embeddedDecoder = Decoder.create(AacDecoder.AAC_LC, track.sampleRate, track.channelCount); + @Override + public MpegTrackInfo getTrack() { + return track; + } + + @Override + public void seekPerformed(long requestedTimecode, long providedTimecode) { + packetRouter.seekPerformed(requestedTimecode, providedTimecode); + } + + @Override + public void flush() throws InterruptedException { + packetRouter.flush(); + } + + @Override + public void consume(ReadableByteChannel channel, int length) throws InterruptedException { + if (packetRouter.nativeDecoder == null) { + packetRouter.nativeDecoder = new AacDecoder(); + configured = configureDecoder(packetRouter.nativeDecoder); } - inputBuffer = ByteBuffer.allocate(4096); - } - processInput(channel, length); + if (configured) { + if (inputBuffer == null) { + inputBuffer = ByteBuffer.allocateDirect(4096); + } + + processInput(channel, length); + } else { + if (packetRouter.embeddedDecoder == null) { + if (track.decoderConfig != null) { + packetRouter.embeddedDecoder = Decoder.create(track.decoderConfig); + } else { + packetRouter.embeddedDecoder = Decoder.create(AacDecoder.AAC_LC, track.sampleRate, track.channelCount); + } + inputBuffer = ByteBuffer.allocate(4096); + } + + processInput(channel, length); + } } - } - private void processInput(ReadableByteChannel channel, int length) throws InterruptedException { - int remaining = length; + private void processInput(ReadableByteChannel channel, int length) throws InterruptedException { + int remaining = length; - while (remaining > 0) { - int chunk = Math.min(remaining, inputBuffer.capacity()); + while (remaining > 0) { + int chunk = Math.min(remaining, inputBuffer.capacity()); - inputBuffer.clear(); - inputBuffer.limit(chunk); + inputBuffer.clear(); + inputBuffer.limit(chunk); - try { - IOUtils.readFully(channel, inputBuffer); - } catch (ClosedByInterruptException e) { - log.trace("Interrupt received while reading channel", e); + try { + IOUtils.readFully(channel, inputBuffer); + } catch (ClosedByInterruptException e) { + log.trace("Interrupt received while reading channel", e); - Thread.currentThread().interrupt(); - throw new InterruptedException(); - } catch (IOException e) { - throw new RuntimeException(e); - } + Thread.currentThread().interrupt(); + throw new InterruptedException(); + } catch (IOException e) { + throw new RuntimeException(e); + } - inputBuffer.flip(); - packetRouter.processInput(inputBuffer); + inputBuffer.flip(); + packetRouter.processInput(inputBuffer); - remaining -= chunk; + remaining -= chunk; + } } - } - - @Override - public void close() { - packetRouter.close(); - } - - private boolean configureDecoder(AacDecoder decoder) { - if (track.decoderConfig != null) { - return (decoder.configure(track.decoderConfig) == 0); - } else { - return (decoder.configure(AacDecoder.AAC_LC, track.sampleRate, track.channelCount) == 0); + + @Override + public void close() { + packetRouter.close(); + } + + private boolean configureDecoder(AacDecoder decoder) { + if (track.decoderConfig != null) { + return (decoder.configure(track.decoderConfig) == 0); + } else { + return (decoder.configure(AacDecoder.AAC_LC, track.sampleRate, track.channelCount) == 0); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegAudioTrack.java index c4bd69452..83fb9f375 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegAudioTrack.java @@ -20,75 +20,75 @@ * Audio track that handles the processing of MP4 format */ public class MpegAudioTrack extends BaseAudioTrack { - private static final Logger log = LoggerFactory.getLogger(MpegAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(MpegAudioTrack.class); - private final SeekableInputStream inputStream; + private final SeekableInputStream inputStream; - /** - * @param trackInfo Track info - * @param inputStream Input stream for the MP4 file - */ - public MpegAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param inputStream Input stream for the MP4 file + */ + public MpegAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + super(trackInfo); - this.inputStream = inputStream; - } + this.inputStream = inputStream; + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) { - MpegFileLoader file = new MpegFileLoader(inputStream); - file.parseHeaders(); + @Override + public void process(LocalAudioTrackExecutor localExecutor) { + MpegFileLoader file = new MpegFileLoader(inputStream); + file.parseHeaders(); - MpegTrackConsumer trackConsumer = loadAudioTrack(file, localExecutor.getProcessingContext()); + MpegTrackConsumer trackConsumer = loadAudioTrack(file, localExecutor.getProcessingContext()); - try { - MpegFileTrackProvider fileReader = file.loadReader(trackConsumer); - if (fileReader == null) { - throw new FriendlyException("Unknown MP4 format.", SUSPICIOUS, null); - } + try { + MpegFileTrackProvider fileReader = file.loadReader(trackConsumer); + if (fileReader == null) { + throw new FriendlyException("Unknown MP4 format.", SUSPICIOUS, null); + } - accurateDuration.set(fileReader.getDuration()); + accurateDuration.set(fileReader.getDuration()); - localExecutor.executeProcessingLoop(fileReader::provideFrames, fileReader::seekToTimecode); - } finally { - trackConsumer.close(); + localExecutor.executeProcessingLoop(fileReader::provideFrames, fileReader::seekToTimecode); + } finally { + trackConsumer.close(); + } } - } - - protected MpegTrackConsumer loadAudioTrack(MpegFileLoader file, AudioProcessingContext context) { - MpegTrackConsumer trackConsumer = null; - boolean success = false; - - try { - trackConsumer = selectAudioTrack(file.getTrackList(), context); - - if (trackConsumer == null) { - StringBuilder error = new StringBuilder(); - error.append("The audio codec used in the track is not supported, options:\n"); - file.getTrackList().forEach(track -> error.append(track.handler).append("|").append(track.codecName).append("\n")); - throw new FriendlyException(error.toString(), SUSPICIOUS, null); - } else { - log.debug("Starting to play track with codec {}", trackConsumer.getTrack().codecName); - } - - trackConsumer.initialise(); - success = true; - return trackConsumer; - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions("Something went wrong when loading an MP4 format track.", FAULT, e); - } finally { - if (!success && trackConsumer != null) { - trackConsumer.close(); - } + + protected MpegTrackConsumer loadAudioTrack(MpegFileLoader file, AudioProcessingContext context) { + MpegTrackConsumer trackConsumer = null; + boolean success = false; + + try { + trackConsumer = selectAudioTrack(file.getTrackList(), context); + + if (trackConsumer == null) { + StringBuilder error = new StringBuilder(); + error.append("The audio codec used in the track is not supported, options:\n"); + file.getTrackList().forEach(track -> error.append(track.handler).append("|").append(track.codecName).append("\n")); + throw new FriendlyException(error.toString(), SUSPICIOUS, null); + } else { + log.debug("Starting to play track with codec {}", trackConsumer.getTrack().codecName); + } + + trackConsumer.initialise(); + success = true; + return trackConsumer; + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions("Something went wrong when loading an MP4 format track.", FAULT, e); + } finally { + if (!success && trackConsumer != null) { + trackConsumer.close(); + } + } } - } - private MpegTrackConsumer selectAudioTrack(List tracks, AudioProcessingContext context) { - for (MpegTrackInfo track : tracks) { - if ("soun".equals(track.handler) && "mp4a".equals(track.codecName)) { - return new MpegAacTrackConsumer(context, track); - } + private MpegTrackConsumer selectAudioTrack(List tracks, AudioProcessingContext context) { + for (MpegTrackInfo track : tracks) { + if ("soun".equals(track.handler) && "mp4a".equals(track.codecName)) { + return new MpegAacTrackConsumer(context, track); + } + } + return null; } - return null; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegContainerProbe.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegContainerProbe.java index 6610501fc..f2bc72b28 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegContainerProbe.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegContainerProbe.java @@ -22,65 +22,65 @@ * Container detection probe for MP4 format. */ public class MpegContainerProbe implements MediaContainerProbe { - private static final Logger log = LoggerFactory.getLogger(MpegContainerProbe.class); + private static final Logger log = LoggerFactory.getLogger(MpegContainerProbe.class); - private static final int[] ISO_TAG = new int[] { 0x00, 0x00, 0x00, -1, 0x66, 0x74, 0x79, 0x70 }; + private static final int[] ISO_TAG = new int[]{0x00, 0x00, 0x00, -1, 0x66, 0x74, 0x79, 0x70}; - @Override - public String getName() { - return "mp4"; - } - - @Override - public boolean matchesHints(MediaContainerHints hints) { - return false; - } + @Override + public String getName() { + return "mp4"; + } - @Override - public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { - if (!checkNextBytes(inputStream, ISO_TAG)) { - return null; + @Override + public boolean matchesHints(MediaContainerHints hints) { + return false; } - log.debug("Track {} is an MP4 file.", reference.identifier); + @Override + public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { + if (!checkNextBytes(inputStream, ISO_TAG)) { + return null; + } - MpegFileLoader file = new MpegFileLoader(inputStream); - file.parseHeaders(); + log.debug("Track {} is an MP4 file.", reference.identifier); - MpegTrackInfo audioTrack = getSupportedAudioTrack(file); + MpegFileLoader file = new MpegFileLoader(inputStream); + file.parseHeaders(); - if (audioTrack == null) { - return unsupportedFormat(this, "No supported audio format in the MP4 file."); - } + MpegTrackInfo audioTrack = getSupportedAudioTrack(file); + + if (audioTrack == null) { + return unsupportedFormat(this, "No supported audio format in the MP4 file."); + } - MpegTrackConsumer trackConsumer = new MpegNoopTrackConsumer(audioTrack); - MpegFileTrackProvider fileReader = file.loadReader(trackConsumer); + MpegTrackConsumer trackConsumer = new MpegNoopTrackConsumer(audioTrack); + MpegFileTrackProvider fileReader = file.loadReader(trackConsumer); - if (fileReader == null) { - return unsupportedFormat(this, "MP4 file uses an unsupported format."); + if (fileReader == null) { + return unsupportedFormat(this, "MP4 file uses an unsupported format."); + } + + AudioTrackInfo trackInfo = AudioTrackInfoBuilder.create(reference, inputStream) + .setTitle(file.getTextMetadata("Title")) + .setAuthor(file.getTextMetadata("Artist")) + .setLength(fileReader.getDuration()) + .build(); + + return supportedFormat(this, null, trackInfo); } - AudioTrackInfo trackInfo = AudioTrackInfoBuilder.create(reference, inputStream) - .setTitle(file.getTextMetadata("Title")) - .setAuthor(file.getTextMetadata("Artist")) - .setLength(fileReader.getDuration()) - .build(); - - return supportedFormat(this, null, trackInfo); - } - - @Override - public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - return new MpegAudioTrack(trackInfo, inputStream); - } - - private MpegTrackInfo getSupportedAudioTrack(MpegFileLoader file) { - for (MpegTrackInfo track : file.getTrackList()) { - if ("soun".equals(track.handler) && "mp4a".equals(track.codecName)) { - return track; - } + @Override + public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + return new MpegAudioTrack(trackInfo, inputStream); } - return null; - } + private MpegTrackInfo getSupportedAudioTrack(MpegFileLoader file) { + for (MpegTrackInfo track : file.getTrackList()) { + if ("soun".equals(track.handler) && "mp4a".equals(track.codecName)) { + return track; + } + } + + return null; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegFileLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegFileLoader.java index b531ae21c..982d007bd 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegFileLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegFileLoader.java @@ -1,10 +1,6 @@ package com.sedmelluq.discord.lavaplayer.container.mpeg; -import com.sedmelluq.discord.lavaplayer.container.mpeg.reader.MpegFileTrackProvider; -import com.sedmelluq.discord.lavaplayer.container.mpeg.reader.MpegParseStopChecker; -import com.sedmelluq.discord.lavaplayer.container.mpeg.reader.MpegReader; -import com.sedmelluq.discord.lavaplayer.container.mpeg.reader.MpegSectionInfo; -import com.sedmelluq.discord.lavaplayer.container.mpeg.reader.MpegVersionedSectionInfo; +import com.sedmelluq.discord.lavaplayer.container.mpeg.reader.*; import com.sedmelluq.discord.lavaplayer.container.mpeg.reader.fragmented.MpegFragmentedFileTrackProvider; import com.sedmelluq.discord.lavaplayer.container.mpeg.reader.standard.MpegStandardFileTrackProvider; import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; @@ -21,286 +17,289 @@ * absolutely necessary, as the stream may be a network connection, in which case each seek may require a new connection. */ public class MpegFileLoader { - private final List tracks; - private final MpegFragmentedFileTrackProvider fragmentedFileReader; - private final MpegStandardFileTrackProvider standardFileReader; - private final MpegReader reader; - private final MpegSectionInfo root; - private final Map metadata; - private byte[] lastEventMessage; - - /** - * @param inputStream Stream to read the file from - */ - public MpegFileLoader(SeekableInputStream inputStream) { - this.tracks = new ArrayList<>(); - this.reader = new MpegReader(inputStream); - this.root = new MpegSectionInfo(0, inputStream.getContentLength(), "root"); - this.fragmentedFileReader = new MpegFragmentedFileTrackProvider(reader, root); - this.standardFileReader = new MpegStandardFileTrackProvider(reader); - this.metadata = new HashMap<>(); - } - - /** - * @return List of tracks found in the file - */ - public List getTrackList() { - return tracks; - } - - /** - * Read the headers of the file to get the list of tracks and data required for seeking. - */ - public void parseHeaders() { - try { - final AtomicBoolean movieBoxSeen = new AtomicBoolean(); - - reader.in(root).handle("moov", moov -> { - movieBoxSeen.set(true); - - reader.in(moov).handle("trak", - this::parseTrackInfo - ).handle("mvex", - fragmentedFileReader::parseMovieExtended - ).handle("udta", - this::parseMetadata - ).run(); - }).handleVersioned("emsg", - this::parseEventMessage - ).handleVersioned("sidx", true, - fragmentedFileReader::parseSegmentIndex - ).stopChecker(getRootStopChecker(movieBoxSeen)).run(); - } catch (IOException e) { - throw new RuntimeException(e); + private final List tracks; + private final MpegFragmentedFileTrackProvider fragmentedFileReader; + private final MpegStandardFileTrackProvider standardFileReader; + private final MpegReader reader; + private final MpegSectionInfo root; + private final Map metadata; + private byte[] lastEventMessage; + + /** + * @param inputStream Stream to read the file from + */ + public MpegFileLoader(SeekableInputStream inputStream) { + this.tracks = new ArrayList<>(); + this.reader = new MpegReader(inputStream); + this.root = new MpegSectionInfo(0, inputStream.getContentLength(), "root"); + this.fragmentedFileReader = new MpegFragmentedFileTrackProvider(reader, root); + this.standardFileReader = new MpegStandardFileTrackProvider(reader); + this.metadata = new HashMap<>(); } - } - - /** - * @param name Name of the text metadata field. - * @return Value of the metadata field, or null if no value or not a string. - */ - public String getTextMetadata(String name) { - Object data = metadata.get(name); - return data instanceof String ? (String) data : null; - } - - /** - * @return Payload from the last emsg message encountered. - */ - public byte[] getLastEventMessage() { - return lastEventMessage; - } - - private void parseMetadata(MpegSectionInfo udta) throws IOException { - reader.in(udta).handleVersioned("meta", meta -> { - reader.in(meta).handle("ilst", ilst -> { - MpegSectionInfo entry; - - while ((entry = reader.nextChild(ilst)) != null) { - parseMetadataEntry(entry); + + /** + * @return List of tracks found in the file + */ + public List getTrackList() { + return tracks; + } + + /** + * Read the headers of the file to get the list of tracks and data required for seeking. + */ + public void parseHeaders() { + try { + final AtomicBoolean movieBoxSeen = new AtomicBoolean(); + + reader.in(root).handle("moov", moov -> { + movieBoxSeen.set(true); + + reader.in(moov).handle("trak", + this::parseTrackInfo + ).handle("mvex", + fragmentedFileReader::parseMovieExtended + ).handle("udta", + this::parseMetadata + ).run(); + }).handleVersioned("emsg", + this::parseEventMessage + ).handleVersioned("sidx", true, + fragmentedFileReader::parseSegmentIndex + ).stopChecker(getRootStopChecker(movieBoxSeen)).run(); + } catch (IOException e) { + throw new RuntimeException(e); } - }).run(); - }).run(); - } + } - private void parseMetadataEntry(MpegSectionInfo entry) throws IOException { - MpegSectionInfo dataHeader = reader.nextChild(entry); + /** + * @param name Name of the text metadata field. + * @return Value of the metadata field, or null if no value or not a string. + */ + public String getTextMetadata(String name) { + Object data = metadata.get(name); + return data instanceof String ? (String) data : null; + } - if (dataHeader != null && "data".equals(dataHeader.type)) { - MpegVersionedSectionInfo data = reader.parseFlags(dataHeader); + /** + * @return Payload from the last emsg message encountered. + */ + public byte[] getLastEventMessage() { + return lastEventMessage; + } - // Skip next 4 bytes - reader.data.readInt(); + private void parseMetadata(MpegSectionInfo udta) throws IOException { + reader.in(udta).handleVersioned("meta", meta -> { + reader.in(meta).handle("ilst", ilst -> { + MpegSectionInfo entry; - if (data.flags == 1) { - storeMetadata(entry.type, reader.readUtfString((int) data.length - 16)); - } + while ((entry = reader.nextChild(ilst)) != null) { + parseMetadataEntry(entry); + } + }).run(); + }).run(); } - reader.skip(entry); - } + private void parseMetadataEntry(MpegSectionInfo entry) throws IOException { + MpegSectionInfo dataHeader = reader.nextChild(entry); - private void storeMetadata(String code, Object value) { - String name = getMetadataName(code); + if (dataHeader != null && "data".equals(dataHeader.type)) { + MpegVersionedSectionInfo data = reader.parseFlags(dataHeader); - if (name != null && value != null) { - metadata.put(name, value); - } - } + // Skip next 4 bytes + reader.data.readInt(); + + if (data.flags == 1) { + storeMetadata(entry.type, reader.readUtfString((int) data.length - 16)); + } + } - private static String getMetadataName(String code) { - switch (code.toLowerCase()) { - case "\u00a9art": return "Artist"; - case "\u00a9nam": return "Title"; - default: return null; + reader.skip(entry); } - } - - private MpegParseStopChecker getRootStopChecker(final AtomicBoolean movieBoxSeen) { - return (child, start) -> { - if (!start && "sidx".equals(child.type)) { - return true; - } else if (!start && "emsg".equals(child.type)) { - return movieBoxSeen.get(); - } else if (start && ("mdat".equals(child.type) || "free".equals(child.type))) { - return movieBoxSeen.get(); - } else { - return false; - } - }; - } - - /** - * @param consumer Track information consumer that the track provider passes the raw packets to. - * @return Track audio provider. - */ - public MpegFileTrackProvider loadReader(MpegTrackConsumer consumer) { - if (fragmentedFileReader.initialise(consumer)) { - return fragmentedFileReader; - } else if (standardFileReader.initialise(consumer)) { - return standardFileReader; - } else { - return null; + + private void storeMetadata(String code, Object value) { + String name = getMetadataName(code); + + if (name != null && value != null) { + metadata.put(name, value); + } } - } - private void parseTrackInfo(MpegSectionInfo trak) throws IOException { - final MpegTrackInfo.Builder trackInfo = new MpegTrackInfo.Builder(); + private static String getMetadataName(String code) { + switch (code.toLowerCase()) { + case "\u00a9art": + return "Artist"; + case "\u00a9nam": + return "Title"; + default: + return null; + } + } - reader.in(trak).handleVersioned("tkhd", tkhd -> { - reader.data.skipBytes(tkhd.version == 1 ? 16 : 8); + private MpegParseStopChecker getRootStopChecker(final AtomicBoolean movieBoxSeen) { + return (child, start) -> { + if (!start && "sidx".equals(child.type)) { + return true; + } else if (!start && "emsg".equals(child.type)) { + return movieBoxSeen.get(); + } else if (start && ("mdat".equals(child.type) || "free".equals(child.type))) { + return movieBoxSeen.get(); + } else { + return false; + } + }; + } - trackInfo.setTrackId(reader.data.readInt()); - }).handle("mdia", mdia -> { - reader.in(mdia).handleVersioned("hdlr", hdlr -> { - reader.data.skipBytes(4); + /** + * @param consumer Track information consumer that the track provider passes the raw packets to. + * @return Track audio provider. + */ + public MpegFileTrackProvider loadReader(MpegTrackConsumer consumer) { + if (fragmentedFileReader.initialise(consumer)) { + return fragmentedFileReader; + } else if (standardFileReader.initialise(consumer)) { + return standardFileReader; + } else { + return null; + } + } - trackInfo.setHandler(reader.readFourCC()); - }).handleVersioned("mdhd", mdhd -> - standardFileReader.readMediaHeaders(mdhd, trackInfo.getTrackId()) - ).handle("minf", minf -> { - reader.in(minf).handle("stbl", stbl -> { - MpegReader.Chain chain = reader.in(stbl); - parseTrackCodecInfo(chain, trackInfo); - standardFileReader.attachSampleTableParsers(chain, trackInfo.getTrackId()); - chain.run(); + private void parseTrackInfo(MpegSectionInfo trak) throws IOException { + final MpegTrackInfo.Builder trackInfo = new MpegTrackInfo.Builder(); + + reader.in(trak).handleVersioned("tkhd", tkhd -> { + reader.data.skipBytes(tkhd.version == 1 ? 16 : 8); + + trackInfo.setTrackId(reader.data.readInt()); + }).handle("mdia", mdia -> { + reader.in(mdia).handleVersioned("hdlr", hdlr -> { + reader.data.skipBytes(4); + + trackInfo.setHandler(reader.readFourCC()); + }).handleVersioned("mdhd", mdhd -> + standardFileReader.readMediaHeaders(mdhd, trackInfo.getTrackId()) + ).handle("minf", minf -> { + reader.in(minf).handle("stbl", stbl -> { + MpegReader.Chain chain = reader.in(stbl); + parseTrackCodecInfo(chain, trackInfo); + standardFileReader.attachSampleTableParsers(chain, trackInfo.getTrackId()); + chain.run(); + }).run(); + }).run(); }).run(); - }).run(); - }).run(); - tracks.add(trackInfo.build()); - } - - private void parseTrackCodecInfo(MpegReader.Chain chain, MpegTrackInfo.Builder trackInfo) { - chain.handleVersioned("stsd", stsd -> { - int entryCount = reader.data.readInt(); - if (entryCount > 0) { - MpegSectionInfo codec = reader.nextChild(stsd); - trackInfo.setCodecName(codec.type); + tracks.add(trackInfo.build()); + } - if ("soun".equals(trackInfo.getHandler())) { - parseSoundTrackCodec(codec, trackInfo); - } - } - }); - } + private void parseTrackCodecInfo(MpegReader.Chain chain, MpegTrackInfo.Builder trackInfo) { + chain.handleVersioned("stsd", stsd -> { + int entryCount = reader.data.readInt(); + if (entryCount > 0) { + MpegSectionInfo codec = reader.nextChild(stsd); + trackInfo.setCodecName(codec.type); + + if ("soun".equals(trackInfo.getHandler())) { + parseSoundTrackCodec(codec, trackInfo); + } + } + }); + } - private void parseSoundTrackCodec(MpegSectionInfo codec, MpegTrackInfo.Builder trackInfo) throws IOException { - reader.parseFlags(codec); + private void parseSoundTrackCodec(MpegSectionInfo codec, MpegTrackInfo.Builder trackInfo) throws IOException { + reader.parseFlags(codec); - reader.data.skipBytes(4); - int version = reader.data.readUnsignedShort(); + reader.data.skipBytes(4); + int version = reader.data.readUnsignedShort(); - switch (version) { - case 0: - case 1: { - reader.data.skipBytes(6); + switch (version) { + case 0: + case 1: { + reader.data.skipBytes(6); - trackInfo.setChannelCount(reader.data.readUnsignedShort()); + trackInfo.setChannelCount(reader.data.readUnsignedShort()); - reader.data.readUnsignedShort(); // sample_size - reader.data.readUnsignedShort(); // apple stuff + reader.data.readUnsignedShort(); // sample_size + reader.data.readUnsignedShort(); // apple stuff - trackInfo.setSampleRate(reader.data.readInt()); - reader.data.readUnsignedShort(); - break; - } - case 2: { - reader.data.skipBytes(6); + trackInfo.setSampleRate(reader.data.readInt()); + reader.data.readUnsignedShort(); + break; + } + case 2: { + reader.data.skipBytes(6); - reader.data.readUnsignedShort(); // Always3 - reader.data.readUnsignedShort(); // Always16 - reader.data.readShort(); // AlwaysMinus2 - reader.data.readUnsignedShort(); // Always0 - reader.data.readInt(); // Always65536 + reader.data.readUnsignedShort(); // Always3 + reader.data.readUnsignedShort(); // Always16 + reader.data.readShort(); // AlwaysMinus2 + reader.data.readUnsignedShort(); // Always0 + reader.data.readInt(); // Always65536 - reader.data.skipBytes(2); + reader.data.skipBytes(2); - reader.data.readUnsignedShort(); // sizeOfStructOnly + reader.data.readUnsignedShort(); // sizeOfStructOnly - trackInfo.setSampleRate((int) reader.data.readDouble()); - trackInfo.setChannelCount(reader.data.readInt()); - break; - } - } + trackInfo.setSampleRate((int) reader.data.readDouble()); + trackInfo.setChannelCount(reader.data.readInt()); + break; + } + } - MpegSectionInfo esds = reader.nextChild(codec); + MpegSectionInfo esds = reader.nextChild(codec); - if (esds != null && "esds".equals(esds.type)) { - trackInfo.setDecoderConfig(parseDecoderConfig(esds)); + if (esds != null && "esds".equals(esds.type)) { + trackInfo.setDecoderConfig(parseDecoderConfig(esds)); + } } - } - private byte[] parseDecoderConfig(MpegSectionInfo esds) throws IOException { - reader.parseFlags(esds); + private byte[] parseDecoderConfig(MpegSectionInfo esds) throws IOException { + reader.parseFlags(esds); - int descriptorTag = reader.data.readUnsignedByte(); + int descriptorTag = reader.data.readUnsignedByte(); - // ES_DescrTag - if (descriptorTag == 0x03) { - if (reader.readCompressedInt() < 5 + 15) { - return null; - } + // ES_DescrTag + if (descriptorTag == 0x03) { + if (reader.readCompressedInt() < 5 + 15) { + return null; + } - reader.data.skipBytes(3); - } else { - reader.data.skipBytes(2); - } + reader.data.skipBytes(3); + } else { + reader.data.skipBytes(2); + } - // DecoderConfigDescrTab - if (reader.data.readUnsignedByte() != 0x04 || reader.readCompressedInt() < 15) { - return null; - } + // DecoderConfigDescrTab + if (reader.data.readUnsignedByte() != 0x04 || reader.readCompressedInt() < 15) { + return null; + } - reader.data.skipBytes(13); + reader.data.skipBytes(13); - // DecSpecificInfoTag - if (reader.data.readUnsignedByte() != 0x05) { - return null; - } + // DecSpecificInfoTag + if (reader.data.readUnsignedByte() != 0x05) { + return null; + } - int decoderConfigLength = reader.readCompressedInt(); + int decoderConfigLength = reader.readCompressedInt(); - if (decoderConfigLength > 8) { - // Longer decoder config than 8 bytes should not be possible with supported formats. - return null; - } + if (decoderConfigLength > 8) { + // Longer decoder config than 8 bytes should not be possible with supported formats. + return null; + } - byte[] decoderConfig = new byte[decoderConfigLength]; - reader.data.readFully(decoderConfig); - return decoderConfig; - } + byte[] decoderConfig = new byte[decoderConfigLength]; + reader.data.readFully(decoderConfig); + return decoderConfig; + } - private void parseEventMessage(MpegSectionInfo emsg) throws IOException { - reader.readTerminatedString(); // scheme_id_uri - reader.readTerminatedString(); // value - reader.data.readInt(); // timescale - reader.data.readInt(); // presentation_time_delta - reader.data.readInt(); // event_duration + private void parseEventMessage(MpegSectionInfo emsg) throws IOException { + reader.readTerminatedString(); // scheme_id_uri + reader.readTerminatedString(); // value + reader.data.readInt(); // timescale + reader.data.readInt(); // presentation_time_delta + reader.data.readInt(); // event_duration - int remaining = (int) ((emsg.offset + emsg.length) - reader.seek.getPosition()); + int remaining = (int) ((emsg.offset + emsg.length) - reader.seek.getPosition()); - lastEventMessage = new byte[remaining]; - reader.data.readFully(lastEventMessage); - } + lastEventMessage = new byte[remaining]; + reader.data.readFully(lastEventMessage); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegNoopTrackConsumer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegNoopTrackConsumer.java index 81bdc1133..bcd614b11 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegNoopTrackConsumer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegNoopTrackConsumer.java @@ -6,42 +6,42 @@ * No-op MP4 track consumer, for probing purposes. */ public class MpegNoopTrackConsumer implements MpegTrackConsumer { - private final MpegTrackInfo trackInfo; - - /** - * @param trackInfo Track info. - */ - public MpegNoopTrackConsumer(MpegTrackInfo trackInfo) { - this.trackInfo = trackInfo; - } - - @Override - public MpegTrackInfo getTrack() { - return trackInfo; - } - - @Override - public void initialise() { - // Nothing to do - } - - @Override - public void seekPerformed(long requestedTimecode, long providedTimecode) { - // Nothing to do - } - - @Override - public void flush() throws InterruptedException { - // Nothing to do - } - - @Override - public void consume(ReadableByteChannel channel, int length) throws InterruptedException { - // Nothing to do - } - - @Override - public void close() { - // Nothing to do - } + private final MpegTrackInfo trackInfo; + + /** + * @param trackInfo Track info. + */ + public MpegNoopTrackConsumer(MpegTrackInfo trackInfo) { + this.trackInfo = trackInfo; + } + + @Override + public MpegTrackInfo getTrack() { + return trackInfo; + } + + @Override + public void initialise() { + // Nothing to do + } + + @Override + public void seekPerformed(long requestedTimecode, long providedTimecode) { + // Nothing to do + } + + @Override + public void flush() throws InterruptedException { + // Nothing to do + } + + @Override + public void consume(ReadableByteChannel channel, int length) throws InterruptedException { + // Nothing to do + } + + @Override + public void close() { + // Nothing to do + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegTrackConsumer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegTrackConsumer.java index 24c148bc9..963fbc940 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegTrackConsumer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegTrackConsumer.java @@ -6,40 +6,42 @@ * Consumer for the data of one MP4 track */ public interface MpegTrackConsumer { - /** - * @return The associated MP4 track - */ - MpegTrackInfo getTrack(); + /** + * @return The associated MP4 track + */ + MpegTrackInfo getTrack(); - /** - * Initialise the consumer, called before first consume() - */ - void initialise(); + /** + * Initialise the consumer, called before first consume() + */ + void initialise(); - /** - * Indicates that the next frame is not a direct continuation of the previous one - * - * @param requestedTimecode Timecode in milliseconds to which the seek was requested to - * @param providedTimecode Timecode in milliseconds to which the seek was actually performed to - */ - void seekPerformed(long requestedTimecode, long providedTimecode); + /** + * Indicates that the next frame is not a direct continuation of the previous one + * + * @param requestedTimecode Timecode in milliseconds to which the seek was requested to + * @param providedTimecode Timecode in milliseconds to which the seek was actually performed to + */ + void seekPerformed(long requestedTimecode, long providedTimecode); - /** - * Indicates that no more input is coming. Flush any buffers to output. - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void flush() throws InterruptedException; + /** + * Indicates that no more input is coming. Flush any buffers to output. + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void flush() throws InterruptedException; - /** - * Consume one chunk from the track - * @param channel Byte channel to consume from - * @param length Lenth of the chunk in bytes - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void consume(ReadableByteChannel channel, int length) throws InterruptedException; + /** + * Consume one chunk from the track + * + * @param channel Byte channel to consume from + * @param length Lenth of the chunk in bytes + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void consume(ReadableByteChannel channel, int length) throws InterruptedException; - /** - * Free all resources - */ - void close(); + /** + * Free all resources + */ + void close(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegTrackInfo.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegTrackInfo.java index c73615433..7c4b169d1 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegTrackInfo.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/MpegTrackInfo.java @@ -4,93 +4,93 @@ * Codec information for an MP4 track */ public class MpegTrackInfo { - /** - * ID of the track - */ - public final int trackId; - /** - * Handler type (soun for audio) - */ - public final String handler; - /** - * Name of the codec - */ - public final String codecName; - /** - * Number of audio channels - */ - public final int channelCount; - /** - * Sample rate for audio - */ - public final int sampleRate; - public final byte[] decoderConfig; + /** + * ID of the track + */ + public final int trackId; + /** + * Handler type (soun for audio) + */ + public final String handler; + /** + * Name of the codec + */ + public final String codecName; + /** + * Number of audio channels + */ + public final int channelCount; + /** + * Sample rate for audio + */ + public final int sampleRate; + public final byte[] decoderConfig; - /** - * @param trackId ID of the track - * @param handler Handler type (soun for audio) - * @param codecName Name of the codec - * @param channelCount Number of audio channels - * @param sampleRate Sample rate for audio - * @param decoderConfig - */ - public MpegTrackInfo(int trackId, String handler, String codecName, int channelCount, int sampleRate, byte[] decoderConfig) { - this.trackId = trackId; - this.handler = handler; - this.codecName = codecName; - this.channelCount = channelCount; - this.sampleRate = sampleRate; - this.decoderConfig = decoderConfig; - } + /** + * @param trackId ID of the track + * @param handler Handler type (soun for audio) + * @param codecName Name of the codec + * @param channelCount Number of audio channels + * @param sampleRate Sample rate for audio + * @param decoderConfig + */ + public MpegTrackInfo(int trackId, String handler, String codecName, int channelCount, int sampleRate, byte[] decoderConfig) { + this.trackId = trackId; + this.handler = handler; + this.codecName = codecName; + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.decoderConfig = decoderConfig; + } - /** - * Helper class for constructing a track info instance. - */ - public static class Builder { - private int trackId; - private String handler; - private String codecName; - private int channelCount; - private int sampleRate; - private byte[] decoderConfig; + /** + * Helper class for constructing a track info instance. + */ + public static class Builder { + private int trackId; + private String handler; + private String codecName; + private int channelCount; + private int sampleRate; + private byte[] decoderConfig; - public void setTrackId(int trackId) { - this.trackId = trackId; - } + public void setTrackId(int trackId) { + this.trackId = trackId; + } - public int getTrackId() { - return trackId; - } + public int getTrackId() { + return trackId; + } - public String getHandler() { - return handler; - } + public String getHandler() { + return handler; + } - public void setHandler(String handler) { - this.handler = handler; - } + public void setHandler(String handler) { + this.handler = handler; + } - public void setCodecName(String codecName) { - this.codecName = codecName; - } + public void setCodecName(String codecName) { + this.codecName = codecName; + } - public void setChannelCount(int channelCount) { - this.channelCount = channelCount; - } + public void setChannelCount(int channelCount) { + this.channelCount = channelCount; + } - public void setSampleRate(int sampleRate) { - this.sampleRate = sampleRate; - } + public void setSampleRate(int sampleRate) { + this.sampleRate = sampleRate; + } - public void setDecoderConfig(byte[] decoderConfig) { - this.decoderConfig = decoderConfig; - } + public void setDecoderConfig(byte[] decoderConfig) { + this.decoderConfig = decoderConfig; + } - /** - * @return The final track info - */ - public MpegTrackInfo build() { - return new MpegTrackInfo(trackId, handler, codecName, channelCount, sampleRate, decoderConfig); + /** + * @return The final track info + */ + public MpegTrackInfo build() { + return new MpegTrackInfo(trackId, handler, codecName, channelCount, sampleRate, decoderConfig); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegFileTrackProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegFileTrackProvider.java index 3e952fe4a..17da19f91 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegFileTrackProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegFileTrackProvider.java @@ -8,31 +8,31 @@ * Track provider for a type of MP4 file. */ public interface MpegFileTrackProvider { - /** - * @param trackConsumer Track consumer which defines the track this will provide and the consumer for packets. - * @return Returns true if it had enough information for initialisation. - */ - boolean initialise(MpegTrackConsumer trackConsumer); + /** + * @param trackConsumer Track consumer which defines the track this will provide and the consumer for packets. + * @return Returns true if it had enough information for initialisation. + */ + boolean initialise(MpegTrackConsumer trackConsumer); - /** - * @return Total duration of the file in milliseconds - */ - long getDuration(); + /** + * @return Total duration of the file in milliseconds + */ + long getDuration(); - /** - * Provide audio frames to the frame consumer until the end of the track or interruption. - * - * @throws InterruptedException When interrupted externally (or for seek/stop). - * @throws IOException When network exception is happened, currently only throw from MpegFragmentedFileTrackProvider. - */ - void provideFrames() throws InterruptedException, IOException; + /** + * Provide audio frames to the frame consumer until the end of the track or interruption. + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + * @throws IOException When network exception is happened, currently only throw from MpegFragmentedFileTrackProvider. + */ + void provideFrames() throws InterruptedException, IOException; - /** - * Perform a seek to the given timecode (ms). On the next call to provideFrames, the seekPerformed method of frame - * consumer is called with the position where it actually seeked to and the position where the seek was requested to - * as arguments. - * - * @param timecode The timecode to seek to in milliseconds - */ - void seekToTimecode(long timecode); + /** + * Perform a seek to the given timecode (ms). On the next call to provideFrames, the seekPerformed method of frame + * consumer is called with the position where it actually seeked to and the position where the seek was requested to + * as arguments. + * + * @param timecode The timecode to seek to in milliseconds + */ + void seekToTimecode(long timecode); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegParseStopChecker.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegParseStopChecker.java index c31046574..fb71fbe90 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegParseStopChecker.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegParseStopChecker.java @@ -5,10 +5,10 @@ * stopped. */ public interface MpegParseStopChecker { - /** - * @param child Section before or after which this is called. - * @param start Whether this is called before (true) or after (false). - * @return True to stop, false to continue. - */ - boolean check(MpegSectionInfo child, boolean start); + /** + * @param child Section before or after which this is called. + * @param start Whether this is called before (true) or after (false). + * @return True to stop, false to continue. + */ + boolean check(MpegSectionInfo child, boolean start); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegReader.java index 67e8f6809..83b942b29 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegReader.java @@ -15,275 +15,284 @@ * Handles reading parts of an MP4 file */ public class MpegReader { - /** - * The input as a DataInput - */ - public final DataInput data; - - /** - * The input as a seekable stream - */ - public final SeekableInputStream seek; - - private final byte[] fourCcBuffer; - private final ByteBuffer readAttemptBuffer; - - /** - * @param inputStream Input as a seekable stream - */ - public MpegReader(SeekableInputStream inputStream) { - seek = inputStream; - data = new DataInputStream(inputStream); - fourCcBuffer = new byte[4]; - readAttemptBuffer = ByteBuffer.allocate(4); - } - - /** - * Reads the header of the next child element. Assumes position is at the start of a header or at the end of the section. - * @param parent The section from which to read child sections from - * @return The element if there were any more child elements - * @throws IOException When network exception is happened - */ - public MpegSectionInfo nextChild(MpegSectionInfo parent) throws IOException { - if (parent.offset + parent.length <= seek.getPosition() + 8) { - return null; - } + /** + * The input as a DataInput + */ + public final DataInput data; - long offset = seek.getPosition(); - Integer lengthField = tryReadInt(); + /** + * The input as a seekable stream + */ + public final SeekableInputStream seek; + + private final byte[] fourCcBuffer; + private final ByteBuffer readAttemptBuffer; - if (lengthField == null) { - return null; + /** + * @param inputStream Input as a seekable stream + */ + public MpegReader(SeekableInputStream inputStream) { + seek = inputStream; + data = new DataInputStream(inputStream); + fourCcBuffer = new byte[4]; + readAttemptBuffer = ByteBuffer.allocate(4); } - long length = Integer.toUnsignedLong(lengthField); - String type = readFourCC(); + /** + * Reads the header of the next child element. Assumes position is at the start of a header or at the end of the section. + * + * @param parent The section from which to read child sections from + * @return The element if there were any more child elements + * @throws IOException When network exception is happened + */ + public MpegSectionInfo nextChild(MpegSectionInfo parent) throws IOException { + if (parent.offset + parent.length <= seek.getPosition() + 8) { + return null; + } - if (length == 1) { - length = data.readLong(); - } + long offset = seek.getPosition(); + Integer lengthField = tryReadInt(); - return new MpegSectionInfo(offset, length, type); - } - - /** - * Skip to the end of a section. - * @param section The section to skip - */ - public void skip(MpegSectionInfo section) { - try { - seek.seek(section.offset + section.length); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Read a FourCC as a string - * @return The FourCC string - * @throws IOException When reading the bytes from input fails - */ - public String readFourCC() throws IOException { - data.readFully(fourCcBuffer); - return new String(fourCcBuffer, StandardCharsets.ISO_8859_1); - } - - /** - * Read an UTF string with a specified size. - * @param size Size in bytes. - * @return The string read from the stream - * @throws IOException On read error - */ - public String readUtfString(int size) throws IOException { - byte[] bytes = new byte[size]; - data.readFully(bytes); - - return new String(bytes, StandardCharsets.UTF_8); - } - - /** - * Read a null-terminated UTF string. - * @return The string read from the stream - * @throws IOException On read error - */ - public String readTerminatedString() throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - byte nextByte; - - while ((nextByte = data.readByte()) != 0) { - bytes.write(nextByte); - } + if (lengthField == null) { + return null; + } + + long length = Integer.toUnsignedLong(lengthField); + String type = readFourCC(); + + if (length == 1) { + length = data.readLong(); + } - return new String(bytes.toByteArray(), StandardCharsets.UTF_8); - } - - public int readCompressedInt() throws IOException { - int byteCount = 0; - int value = 0; - int currentByte; - - do { - currentByte = data.readUnsignedByte(); - byteCount++; - value = (value << 7) | (currentByte & 0x7F); - } while ((currentByte & 0x80) == 0x80 && byteCount < 4); - - return value; - } - - /** - * Parse the flags and version for the specified section - * @param section The section where the flags and version should be parsed - * @return The section info with version info - * @throws IOException On a read error - */ - public MpegVersionedSectionInfo parseFlags(MpegSectionInfo section) throws IOException { - return parseFlagsForSection(data, section); - } - - private static MpegVersionedSectionInfo parseFlagsForSection(DataInput in, MpegSectionInfo section) throws IOException { - int versionAndFlags = in.readInt(); - return new MpegVersionedSectionInfo(section, versionAndFlags >>> 24, versionAndFlags & 0xffffff); - } - - private Integer tryReadInt() throws IOException { - int firstByte = seek.read(); - - if (firstByte == -1) { - return null; + return new MpegSectionInfo(offset, length, type); } - readAttemptBuffer.put(0, (byte) firstByte); - data.readFully(readAttemptBuffer.array(), 1, 3); - return readAttemptBuffer.getInt(0); - } - - /** - * Start a child element handling chain - * @param parent The parent chain - * @return The chain - */ - public Chain in(MpegSectionInfo parent) { - return new Chain(parent, this); - } - - /** - * Child element processing helper class. - */ - public static class Chain { - private final MpegSectionInfo parent; - private final List handlers; - private final MpegReader reader; - private MpegParseStopChecker stopChecker; - - private Chain(MpegSectionInfo parent, MpegReader reader) { - this.parent = parent; - this.reader = reader; - handlers = new ArrayList<>(); + /** + * Skip to the end of a section. + * + * @param section The section to skip + */ + public void skip(MpegSectionInfo section) { + try { + seek.seek(section.offset + section.length); + } catch (IOException e) { + throw new RuntimeException(e); + } } /** - * @param type The FourCC of the section for which a handler is specified - * @param handler The handler - * @return this + * Read a FourCC as a string + * + * @return The FourCC string + * @throws IOException When reading the bytes from input fails */ - public Chain handle(String type, MpegSectionHandler handler) { - handle(type, false, handler); - return this; + public String readFourCC() throws IOException { + data.readFully(fourCcBuffer); + return new String(fourCcBuffer, StandardCharsets.ISO_8859_1); } /** - * @param type The FourCC of the section for which a handler is specified - * @param finish Whether to stop reading after this section - * @param handler The handler - * @return this + * Read an UTF string with a specified size. + * + * @param size Size in bytes. + * @return The string read from the stream + * @throws IOException On read error */ - public Chain handle(String type, boolean finish, MpegSectionHandler handler) { - handlers.add(new Handler(type, finish, handler)); - return this; + public String readUtfString(int size) throws IOException { + byte[] bytes = new byte[size]; + data.readFully(bytes); + + return new String(bytes, StandardCharsets.UTF_8); } /** - * @param type The FourCC of the section for which a handler is specified - * @param handler The handler which expects versioned section info - * @return this + * Read a null-terminated UTF string. + * + * @return The string read from the stream + * @throws IOException On read error */ - public Chain handleVersioned(String type, MpegVersionedSectionHandler handler) { - handlers.add(new Handler(type, false, handler)); - return this; + public String readTerminatedString() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte nextByte; + + while ((nextByte = data.readByte()) != 0) { + bytes.write(nextByte); + } + + return new String(bytes.toByteArray(), StandardCharsets.UTF_8); + } + + public int readCompressedInt() throws IOException { + int byteCount = 0; + int value = 0; + int currentByte; + + do { + currentByte = data.readUnsignedByte(); + byteCount++; + value = (value << 7) | (currentByte & 0x7F); + } while ((currentByte & 0x80) == 0x80 && byteCount < 4); + + return value; } /** - * @param type The FourCC of the section for which a handler is specified - * @param finish Whether to stop reading after this section - * @param handler The handler which expects versioned section info - * @return this + * Parse the flags and version for the specified section + * + * @param section The section where the flags and version should be parsed + * @return The section info with version info + * @throws IOException On a read error */ - public Chain handleVersioned(String type, boolean finish, MpegVersionedSectionHandler handler) { - handlers.add(new Handler(type, finish, handler)); - return this; + public MpegVersionedSectionInfo parseFlags(MpegSectionInfo section) throws IOException { + return parseFlagsForSection(data, section); + } + + private static MpegVersionedSectionInfo parseFlagsForSection(DataInput in, MpegSectionInfo section) throws IOException { + int versionAndFlags = in.readInt(); + return new MpegVersionedSectionInfo(section, versionAndFlags >>> 24, versionAndFlags & 0xffffff); + } + + private Integer tryReadInt() throws IOException { + int firstByte = seek.read(); + + if (firstByte == -1) { + return null; + } + + readAttemptBuffer.put(0, (byte) firstByte); + data.readFully(readAttemptBuffer.array(), 1, 3); + return readAttemptBuffer.getInt(0); } /** - * Assign a parsing stop checker to this chain. - * @param stopChecker Stop checker. - * @return this + * Start a child element handling chain + * + * @param parent The parent chain + * @return The chain */ - public Chain stopChecker(MpegParseStopChecker stopChecker) { - this.stopChecker = stopChecker; - return this; + public Chain in(MpegSectionInfo parent) { + return new Chain(parent, this); } /** - * Process the current section with all the handlers specified so far - * @throws IOException On read error + * Child element processing helper class. */ - public void run() throws IOException { - MpegSectionInfo child; - boolean finished = false; + public static class Chain { + private final MpegSectionInfo parent; + private final List handlers; + private final MpegReader reader; + private MpegParseStopChecker stopChecker; + + private Chain(MpegSectionInfo parent, MpegReader reader) { + this.parent = parent; + this.reader = reader; + handlers = new ArrayList<>(); + } - while (!finished && (child = reader.nextChild(parent)) != null) { - finished = stopChecker != null && stopChecker.check(child, true); + /** + * @param type The FourCC of the section for which a handler is specified + * @param handler The handler + * @return this + */ + public Chain handle(String type, MpegSectionHandler handler) { + handle(type, false, handler); + return this; + } - if (!finished) { - processHandlers(child); + /** + * @param type The FourCC of the section for which a handler is specified + * @param finish Whether to stop reading after this section + * @param handler The handler + * @return this + */ + public Chain handle(String type, boolean finish, MpegSectionHandler handler) { + handlers.add(new Handler(type, finish, handler)); + return this; + } - finished = stopChecker != null && stopChecker.check(child, false); + /** + * @param type The FourCC of the section for which a handler is specified + * @param handler The handler which expects versioned section info + * @return this + */ + public Chain handleVersioned(String type, MpegVersionedSectionHandler handler) { + handlers.add(new Handler(type, false, handler)); + return this; } - reader.skip(child); - } - } + /** + * @param type The FourCC of the section for which a handler is specified + * @param finish Whether to stop reading after this section + * @param handler The handler which expects versioned section info + * @return this + */ + public Chain handleVersioned(String type, boolean finish, MpegVersionedSectionHandler handler) { + handlers.add(new Handler(type, finish, handler)); + return this; + } - private void processHandlers(MpegSectionInfo child) throws IOException { - for (Handler handler : handlers) { - if (handler.type.equals(child.type)) { - handleSection(child, handler); + /** + * Assign a parsing stop checker to this chain. + * + * @param stopChecker Stop checker. + * @return this + */ + public Chain stopChecker(MpegParseStopChecker stopChecker) { + this.stopChecker = stopChecker; + return this; } - } - } - private boolean handleSection(MpegSectionInfo child, Handler handler) throws IOException { - if (handler.sectionHandler instanceof MpegVersionedSectionHandler) { - MpegVersionedSectionInfo versioned = parseFlagsForSection(reader.data, child); - ((MpegVersionedSectionHandler) handler.sectionHandler).handle(versioned); - } else { - ((MpegSectionHandler) handler.sectionHandler).handle(child); - } + /** + * Process the current section with all the handlers specified so far + * + * @throws IOException On read error + */ + public void run() throws IOException { + MpegSectionInfo child; + boolean finished = false; + + while (!finished && (child = reader.nextChild(parent)) != null) { + finished = stopChecker != null && stopChecker.check(child, true); - return !handler.terminator; + if (!finished) { + processHandlers(child); + + finished = stopChecker != null && stopChecker.check(child, false); + } + + reader.skip(child); + } + } + + private void processHandlers(MpegSectionInfo child) throws IOException { + for (Handler handler : handlers) { + if (handler.type.equals(child.type)) { + handleSection(child, handler); + } + } + } + + private boolean handleSection(MpegSectionInfo child, Handler handler) throws IOException { + if (handler.sectionHandler instanceof MpegVersionedSectionHandler) { + MpegVersionedSectionInfo versioned = parseFlagsForSection(reader.data, child); + ((MpegVersionedSectionHandler) handler.sectionHandler).handle(versioned); + } else { + ((MpegSectionHandler) handler.sectionHandler).handle(child); + } + + return !handler.terminator; + } } - } - private static class Handler { - private final String type; - private final boolean terminator; - private final Object sectionHandler; + private static class Handler { + private final String type; + private final boolean terminator; + private final Object sectionHandler; - private Handler(String type, boolean terminator, Object sectionHandler) { - this.type = type; - this.terminator = terminator; - this.sectionHandler = sectionHandler; + private Handler(String type, boolean terminator, Object sectionHandler) { + this.type = type; + this.terminator = terminator; + this.sectionHandler = sectionHandler; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegSectionHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegSectionHandler.java index 7def39a2d..11e5b947c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegSectionHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegSectionHandler.java @@ -6,9 +6,9 @@ * Handles one MPEG section which has no version info */ public interface MpegSectionHandler { - /** - * @param child The section - * @throws IOException On read error - */ - void handle(MpegSectionInfo child) throws IOException; + /** + * @param child The section + * @throws IOException On read error + */ + void handle(MpegSectionInfo child) throws IOException; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegSectionInfo.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegSectionInfo.java index 2d7b6f2ed..10227bdfe 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegSectionInfo.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegSectionInfo.java @@ -4,27 +4,27 @@ * Information for one MP4 section (aka box) */ public class MpegSectionInfo { - /** - * Absolute offset of the section - */ - public final long offset; - /** - * Length of the section - */ - public final long length; - /** - * Type (fourCC) of the section - */ - public final String type; + /** + * Absolute offset of the section + */ + public final long offset; + /** + * Length of the section + */ + public final long length; + /** + * Type (fourCC) of the section + */ + public final String type; - /** - * @param offset Absolute offset of the section - * @param length Length of the section - * @param type Type (fourCC) of the section - */ - public MpegSectionInfo(long offset, long length, String type) { - this.offset = offset; - this.length = length; - this.type = type; - } + /** + * @param offset Absolute offset of the section + * @param length Length of the section + * @param type Type (fourCC) of the section + */ + public MpegSectionInfo(long offset, long length, String type) { + this.offset = offset; + this.length = length; + this.type = type; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegVersionedSectionHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegVersionedSectionHandler.java index 6330ae9d5..95966168a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegVersionedSectionHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegVersionedSectionHandler.java @@ -6,9 +6,9 @@ * Handles one MPEG section which has version info */ public interface MpegVersionedSectionHandler { - /** - * @param child The versioned section - * @throws IOException On read error - */ - void handle(MpegVersionedSectionInfo child) throws IOException; + /** + * @param child The versioned section + * @throws IOException On read error + */ + void handle(MpegVersionedSectionInfo child) throws IOException; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegVersionedSectionInfo.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegVersionedSectionInfo.java index e1ece1106..fe8488d9d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegVersionedSectionInfo.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/MpegVersionedSectionInfo.java @@ -4,24 +4,24 @@ * Information for one MP4 section (aka box) including version and flags */ public class MpegVersionedSectionInfo extends MpegSectionInfo { - /** - * Version of the section - */ - public final int version; - /** - * Flags of the section - */ - public final int flags; + /** + * Version of the section + */ + public final int version; + /** + * Flags of the section + */ + public final int flags; - /** - * @param sectionInfo Basic info for the section - * @param version Version of the section - * @param flags Flags of the section - */ - public MpegVersionedSectionInfo(MpegSectionInfo sectionInfo, int version, int flags) { - super(sectionInfo.offset, sectionInfo.length, sectionInfo.type); + /** + * @param sectionInfo Basic info for the section + * @param version Version of the section + * @param flags Flags of the section + */ + public MpegVersionedSectionInfo(MpegSectionInfo sectionInfo, int version, int flags) { + super(sectionInfo.offset, sectionInfo.length, sectionInfo.type); - this.version = version; - this.flags = flags; - } + this.version = version; + this.flags = flags; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegFragmentedFileTrackProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegFragmentedFileTrackProvider.java index 2d980063e..3061fadab 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegFragmentedFileTrackProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegFragmentedFileTrackProvider.java @@ -17,224 +17,226 @@ * Track provider for fragmented MP4 file format. */ public class MpegFragmentedFileTrackProvider implements MpegFileTrackProvider { - private final MpegReader reader; - private final MpegSectionInfo root; - - private MpegTrackConsumer consumer; - private boolean isFragmented; - private long totalDuration; - private MpegGlobalSeekInfo globalSeekInfo; - private boolean seeking; - private long minimumTimecode; - - /** - * @param reader MP4-specific reader - * @param root Root section info (synthetic section wrapping the entire file) - */ - public MpegFragmentedFileTrackProvider(MpegReader reader, MpegSectionInfo root) { - this.reader = reader; - this.root = root; - } - - @Override - public boolean initialise(MpegTrackConsumer consumer) { - if (!isFragmented) { - return false; + private final MpegReader reader; + private final MpegSectionInfo root; + + private MpegTrackConsumer consumer; + private boolean isFragmented; + private long totalDuration; + private MpegGlobalSeekInfo globalSeekInfo; + private boolean seeking; + private long minimumTimecode; + + /** + * @param reader MP4-specific reader + * @param root Root section info (synthetic section wrapping the entire file) + */ + public MpegFragmentedFileTrackProvider(MpegReader reader, MpegSectionInfo root) { + this.reader = reader; + this.root = root; } - this.consumer = consumer; - return true; - } + @Override + public boolean initialise(MpegTrackConsumer consumer) { + if (!isFragmented) { + return false; + } - @Override - public void provideFrames() throws InterruptedException, IOException { - MpegSectionInfo moof; + this.consumer = consumer; + return true; + } - ReadableByteChannel channel = new DetachedByteChannel(Channels.newChannel(reader.seek)); - while ((moof = reader.nextChild(root)) != null) { - if (!"moof".equals(moof.type)) { - reader.skip(moof); - continue; - } + @Override + public void provideFrames() throws InterruptedException, IOException { + MpegSectionInfo moof; - MpegTrackFragmentHeader fragment = parseTrackMovieFragment(moof, consumer.getTrack().trackId); - MpegSectionInfo mdat = reader.nextChild(root); + ReadableByteChannel channel = new DetachedByteChannel(Channels.newChannel(reader.seek)); + while ((moof = reader.nextChild(root)) != null) { + if (!"moof".equals(moof.type)) { + reader.skip(moof); + continue; + } - long timecode = fragment.baseTimecode; - reader.seek.seek(moof.offset + fragment.dataOffset); + MpegTrackFragmentHeader fragment = parseTrackMovieFragment(moof, consumer.getTrack().trackId); + MpegSectionInfo mdat = reader.nextChild(root); - for (int i = 0; i < fragment.sampleSizes.length; i++) { - handleSeeking(consumer, timecode); + long timecode = fragment.baseTimecode; + reader.seek.seek(moof.offset + fragment.dataOffset); - consumer.consume(channel, fragment.sampleSizes[i]); - } + for (int i = 0; i < fragment.sampleSizes.length; i++) { + handleSeeking(consumer, timecode); - reader.skip(mdat); - } - } + consumer.consume(channel, fragment.sampleSizes[i]); + } - @Override - public void seekToTimecode(long timecode) { - if (globalSeekInfo == null) { - // Not seekable - return; + reader.skip(mdat); + } } - minimumTimecode = timecode * globalSeekInfo.timescale / 1000; - seeking = true; + @Override + public void seekToTimecode(long timecode) { + if (globalSeekInfo == null) { + // Not seekable + return; + } - int segmentIndex; + minimumTimecode = timecode * globalSeekInfo.timescale / 1000; + seeking = true; - for (segmentIndex = 0; segmentIndex < globalSeekInfo.entries.length - 1; segmentIndex++) { - if (globalSeekInfo.timeOffsets[segmentIndex + 1] > minimumTimecode) { - break; - } - } + int segmentIndex; - try { - reader.seek.seek(globalSeekInfo.fileOffsets[segmentIndex]); - } catch (IOException e) { - throw new RuntimeException(e); + for (segmentIndex = 0; segmentIndex < globalSeekInfo.entries.length - 1; segmentIndex++) { + if (globalSeekInfo.timeOffsets[segmentIndex + 1] > minimumTimecode) { + break; + } + } + + try { + reader.seek.seek(globalSeekInfo.fileOffsets[segmentIndex]); + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - @Override - public long getDuration() { - if (globalSeekInfo == null) { - return Units.DURATION_MS_UNKNOWN; + @Override + public long getDuration() { + if (globalSeekInfo == null) { + return Units.DURATION_MS_UNKNOWN; + } + + return totalDuration * 1000 / globalSeekInfo.timescale; } - return totalDuration * 1000 / globalSeekInfo.timescale; - } - - /** - * Handle mvex section. - * @param mvex Section header. - * @throws IOException On read error - */ - public void parseMovieExtended(MpegSectionInfo mvex) throws IOException { - reader.in(mvex).handleVersioned("trex", trex -> { - isFragmented = true; - }).run(); - } - - /** - * Handle segment index section. - * @param sbix Section header. - * @throws IOException On read error - */ - public void parseSegmentIndex(MpegVersionedSectionInfo sbix) throws IOException { - reader.data.readInt(); // referenceId - int timescale = reader.data.readInt(); - - if (sbix.version == 0) { - reader.data.readInt(); // earliestPresentationTime - reader.data.readInt(); // firstOffset - } else { - reader.data.readLong(); // earliestPresentationTime - reader.data.readLong(); // firstOffset + /** + * Handle mvex section. + * + * @param mvex Section header. + * @throws IOException On read error + */ + public void parseMovieExtended(MpegSectionInfo mvex) throws IOException { + reader.in(mvex).handleVersioned("trex", trex -> { + isFragmented = true; + }).run(); } - reader.data.readShort(); // reserved + /** + * Handle segment index section. + * + * @param sbix Section header. + * @throws IOException On read error + */ + public void parseSegmentIndex(MpegVersionedSectionInfo sbix) throws IOException { + reader.data.readInt(); // referenceId + int timescale = reader.data.readInt(); + + if (sbix.version == 0) { + reader.data.readInt(); // earliestPresentationTime + reader.data.readInt(); // firstOffset + } else { + reader.data.readLong(); // earliestPresentationTime + reader.data.readLong(); // firstOffset + } - MpegSegmentEntry[] entries = new MpegSegmentEntry[reader.data.readUnsignedShort()]; + reader.data.readShort(); // reserved - for (int i = 0; i < entries.length; i++) { - int typeAndSize = reader.data.readInt(); - int duration = reader.data.readInt(); - reader.data.readInt(); // startsWithSap + sapType + sapDeltaTime + MpegSegmentEntry[] entries = new MpegSegmentEntry[reader.data.readUnsignedShort()]; - entries[i] = new MpegSegmentEntry(typeAndSize >>> 31, typeAndSize & 0x7fffffff, duration); + for (int i = 0; i < entries.length; i++) { + int typeAndSize = reader.data.readInt(); + int duration = reader.data.readInt(); + reader.data.readInt(); // startsWithSap + sapType + sapDeltaTime - totalDuration += duration; - } + entries[i] = new MpegSegmentEntry(typeAndSize >>> 31, typeAndSize & 0x7fffffff, duration); - globalSeekInfo = new MpegGlobalSeekInfo(timescale, sbix.offset + sbix.length, entries); - } + totalDuration += duration; + } - private void handleSeeking(MpegTrackConsumer consumer, long timecode) { - if (seeking) { - // Even though sample durations may be available, decoding doesn't work if we don't start from the beginning - // of a fragment. Therefore skipping within the fragment is handled by skipping decoded samples later. - consumer.seekPerformed(minimumTimecode * 1000 / globalSeekInfo.timescale, timecode * 1000 / globalSeekInfo.timescale); - seeking = false; + globalSeekInfo = new MpegGlobalSeekInfo(timescale, sbix.offset + sbix.length, entries); } - } - private MpegTrackFragmentHeader parseTrackMovieFragment(MpegSectionInfo moof, int trackId) throws IOException { - final AtomicReference header = new AtomicReference<>(); + private void handleSeeking(MpegTrackConsumer consumer, long timecode) { + if (seeking) { + // Even though sample durations may be available, decoding doesn't work if we don't start from the beginning + // of a fragment. Therefore skipping within the fragment is handled by skipping decoded samples later. + consumer.seekPerformed(minimumTimecode * 1000 / globalSeekInfo.timescale, timecode * 1000 / globalSeekInfo.timescale); + seeking = false; + } + } - reader.in(moof).handle("traf", traf -> { - final MpegTrackFragmentHeader.Builder builder = new MpegTrackFragmentHeader.Builder(); + private MpegTrackFragmentHeader parseTrackMovieFragment(MpegSectionInfo moof, int trackId) throws IOException { + final AtomicReference header = new AtomicReference<>(); - reader.in(traf).handleVersioned("tfhd", tfhd -> { - parseTrackFragmentHeader(tfhd, builder); - }).handleVersioned("tfdt", tfdt -> { - builder.setBaseTimecode((tfdt.version == 1) ? reader.data.readLong() : reader.data.readInt()); - }).handleVersioned("trun", trun -> { - if (builder.getTrackId() == trackId) { - parseTrackRunInfo(trun, builder); - } - }).run(); + reader.in(moof).handle("traf", traf -> { + final MpegTrackFragmentHeader.Builder builder = new MpegTrackFragmentHeader.Builder(); - if (builder.getTrackId() == trackId) { - header.set(builder.build()); - } - }).run(); + reader.in(traf).handleVersioned("tfhd", tfhd -> { + parseTrackFragmentHeader(tfhd, builder); + }).handleVersioned("tfdt", tfdt -> { + builder.setBaseTimecode((tfdt.version == 1) ? reader.data.readLong() : reader.data.readInt()); + }).handleVersioned("trun", trun -> { + if (builder.getTrackId() == trackId) { + parseTrackRunInfo(trun, builder); + } + }).run(); - return header.get(); - } + if (builder.getTrackId() == trackId) { + header.set(builder.build()); + } + }).run(); - private void parseTrackFragmentHeader(MpegVersionedSectionInfo tfhd, MpegTrackFragmentHeader.Builder builder) throws IOException { - builder.setTrackId(reader.data.readInt()); + return header.get(); + } - if ((tfhd.flags & 0x000010) != 0) { - // Need to read default sample size, but first must skip the fields before it - if ((tfhd.flags & 0x000001) != 0) { - // Skip baseDataOffset - reader.data.readLong(); - } + private void parseTrackFragmentHeader(MpegVersionedSectionInfo tfhd, MpegTrackFragmentHeader.Builder builder) throws IOException { + builder.setTrackId(reader.data.readInt()); - if ((tfhd.flags & 0x000002) != 0) { - // Skip sampleDescriptionIndex - reader.data.readInt(); - } + if ((tfhd.flags & 0x000010) != 0) { + // Need to read default sample size, but first must skip the fields before it + if ((tfhd.flags & 0x000001) != 0) { + // Skip baseDataOffset + reader.data.readLong(); + } - if ((tfhd.flags & 0x000008) != 0) { - // Skip defaultSampleDuration - reader.data.readInt(); - } + if ((tfhd.flags & 0x000002) != 0) { + // Skip sampleDescriptionIndex + reader.data.readInt(); + } - builder.setDefaultSampleSize(reader.data.readInt()); + if ((tfhd.flags & 0x000008) != 0) { + // Skip defaultSampleDuration + reader.data.readInt(); + } + + builder.setDefaultSampleSize(reader.data.readInt()); + } } - } - private void parseTrackRunInfo(MpegVersionedSectionInfo trun, MpegTrackFragmentHeader.Builder builder) throws IOException { - int sampleCount = reader.data.readInt(); - builder.setDataOffset(((trun.flags & 0x01) != 0) ? reader.data.readInt() : -1); + private void parseTrackRunInfo(MpegVersionedSectionInfo trun, MpegTrackFragmentHeader.Builder builder) throws IOException { + int sampleCount = reader.data.readInt(); + builder.setDataOffset(((trun.flags & 0x01) != 0) ? reader.data.readInt() : -1); - if ((trun.flags & 0x04) != 0) { - reader.data.skipBytes(4); // first sample flags - } + if ((trun.flags & 0x04) != 0) { + reader.data.skipBytes(4); // first sample flags + } - boolean hasDurations = (trun.flags & 0x100) != 0; - boolean hasSizes = (trun.flags & 0x200) != 0; - - builder.createSampleArrays(hasDurations, hasSizes, sampleCount); - - for (int i = 0; i < sampleCount; i++) { - if (hasDurations) { - builder.setDuration(i, reader.data.readInt()); - } - if (hasSizes) { - builder.setSize(i, reader.data.readInt()); - } - if ((trun.flags & 0x400) != 0) { - reader.data.skipBytes(4); - } - if ((trun.flags & 0x800) != 0) { - reader.data.skipBytes(4); - } + boolean hasDurations = (trun.flags & 0x100) != 0; + boolean hasSizes = (trun.flags & 0x200) != 0; + + builder.createSampleArrays(hasDurations, hasSizes, sampleCount); + + for (int i = 0; i < sampleCount; i++) { + if (hasDurations) { + builder.setDuration(i, reader.data.readInt()); + } + if (hasSizes) { + builder.setSize(i, reader.data.readInt()); + } + if ((trun.flags & 0x400) != 0) { + reader.data.skipBytes(4); + } + if ((trun.flags & 0x800) != 0) { + reader.data.skipBytes(4); + } + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegGlobalSeekInfo.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegGlobalSeekInfo.java index b3971515b..2a7d8a83d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegGlobalSeekInfo.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegGlobalSeekInfo.java @@ -4,39 +4,39 @@ * Describes the seek info for a fragmented MP4 file */ public class MpegGlobalSeekInfo { - /** - * The value of the internal timecodes that corresponds to one second - */ - public final int timescale; - /** - * Size and duration information for each segment - */ - public final MpegSegmentEntry[] entries; - /** - * Absolute timecode offset of each segment - */ - public final long[] timeOffsets; - /** - * Absolute file offset of each segment - */ - public final long[] fileOffsets; + /** + * The value of the internal timecodes that corresponds to one second + */ + public final int timescale; + /** + * Size and duration information for each segment + */ + public final MpegSegmentEntry[] entries; + /** + * Absolute timecode offset of each segment + */ + public final long[] timeOffsets; + /** + * Absolute file offset of each segment + */ + public final long[] fileOffsets; - /** - * @param timescale The value of the internal timecodes that corresponds to one second - * @param baseOffset The file offset of the first segment - * @param entries Size and duration information for each segment - */ - public MpegGlobalSeekInfo(int timescale, long baseOffset, MpegSegmentEntry[] entries) { - this.timescale = timescale; - this.entries = entries; + /** + * @param timescale The value of the internal timecodes that corresponds to one second + * @param baseOffset The file offset of the first segment + * @param entries Size and duration information for each segment + */ + public MpegGlobalSeekInfo(int timescale, long baseOffset, MpegSegmentEntry[] entries) { + this.timescale = timescale; + this.entries = entries; - timeOffsets = new long[entries.length]; - fileOffsets = new long[entries.length]; - fileOffsets[0] = baseOffset; + timeOffsets = new long[entries.length]; + fileOffsets = new long[entries.length]; + fileOffsets[0] = baseOffset; - for (int i = 1; i < entries.length; i++) { - timeOffsets[i] = timeOffsets[i-1] + entries[i-1].duration; - fileOffsets[i] = fileOffsets[i-1] + entries[i-1].size; + for (int i = 1; i < entries.length; i++) { + timeOffsets[i] = timeOffsets[i - 1] + entries[i - 1].duration; + fileOffsets[i] = fileOffsets[i - 1] + entries[i - 1].size; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegSegmentEntry.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegSegmentEntry.java index 843833fc9..5f4c346c4 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegSegmentEntry.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegSegmentEntry.java @@ -4,27 +4,27 @@ * Information about one MP4 segment aka fragment */ public class MpegSegmentEntry { - /** - * Type of the segment - */ - public final int type; - /** - * Size in bytes - */ - public final int size; - /** - * Duration using the timescale of the file - */ - public final int duration; + /** + * Type of the segment + */ + public final int type; + /** + * Size in bytes + */ + public final int size; + /** + * Duration using the timescale of the file + */ + public final int duration; - /** - * @param type Type of the segment - * @param size Size in bytes - * @param duration Duration using the timescale of the file - */ - public MpegSegmentEntry(int type, int size, int duration) { - this.type = type; - this.size = size; - this.duration = duration; - } + /** + * @param type Type of the segment + * @param size Size in bytes + * @param duration Duration using the timescale of the file + */ + public MpegSegmentEntry(int type, int size, int duration) { + this.type = type; + this.size = size; + this.duration = duration; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegTrackFragmentHeader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegTrackFragmentHeader.java index 4514bf592..9fad0ab68 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegTrackFragmentHeader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/fragmented/MpegTrackFragmentHeader.java @@ -4,135 +4,138 @@ * Header for an MP4 track in a fragment. */ public class MpegTrackFragmentHeader { - /** - * Track ID which this header is for - */ - public final int trackId; - /** - * The timecode at which this track is at the start of this fragment - */ - public final long baseTimecode; - /** - * The offset of the data for this track in this fragment - */ - public final int dataOffset; - /** - * Duration of each sample for this track in this fragment using file timescale - */ - public final int[] sampleDurations; - /** - * Size of each sample for this track in this fragment - */ - public final int[] sampleSizes; - - /** - * @param trackId Track ID which this header is for - * @param baseTimecode The timecode at which this track is at the start of this fragment - * @param dataOffset The offset of the data for this track in this fragment - * @param sampleDurations Duration of each sample for this track in this fragment using file timescale - * @param sampleSizes Size of each sample for this track in this fragment - */ - public MpegTrackFragmentHeader(int trackId, long baseTimecode, int dataOffset, int[] sampleDurations, int[] sampleSizes) { - this.trackId = trackId; - this.baseTimecode = baseTimecode; - this.dataOffset = dataOffset; - this.sampleDurations = sampleDurations; - this.sampleSizes = sampleSizes; - } - - /** - * A helper for building an instance of this class. - */ - public static class Builder { - private int trackId; - private long baseTimecode; - private int dataOffset; - private int defaultSampleSize; - private int sampleCount; - private int[] sampleDurations; - private int[] sampleSizes; + /** + * Track ID which this header is for + */ + public final int trackId; + /** + * The timecode at which this track is at the start of this fragment + */ + public final long baseTimecode; + /** + * The offset of the data for this track in this fragment + */ + public final int dataOffset; + /** + * Duration of each sample for this track in this fragment using file timescale + */ + public final int[] sampleDurations; + /** + * Size of each sample for this track in this fragment + */ + public final int[] sampleSizes; /** - * Create an empty builder. + * @param trackId Track ID which this header is for + * @param baseTimecode The timecode at which this track is at the start of this fragment + * @param dataOffset The offset of the data for this track in this fragment + * @param sampleDurations Duration of each sample for this track in this fragment using file timescale + * @param sampleSizes Size of each sample for this track in this fragment */ - public Builder() { - trackId = -1; + public MpegTrackFragmentHeader(int trackId, long baseTimecode, int dataOffset, int[] sampleDurations, int[] sampleSizes) { + this.trackId = trackId; + this.baseTimecode = baseTimecode; + this.dataOffset = dataOffset; + this.sampleDurations = sampleDurations; + this.sampleSizes = sampleSizes; } /** - * @return Previously assigned track ID, or -1 if not assigned + * A helper for building an instance of this class. */ - public int getTrackId() { - return trackId; - } + public static class Builder { + private int trackId; + private long baseTimecode; + private int dataOffset; + private int defaultSampleSize; + private int sampleCount; + private int[] sampleDurations; + private int[] sampleSizes; + + /** + * Create an empty builder. + */ + public Builder() { + trackId = -1; + } - public void setTrackId(int trackId) { - this.trackId = trackId; - } + /** + * @return Previously assigned track ID, or -1 if not assigned + */ + public int getTrackId() { + return trackId; + } - public void setBaseTimecode(long baseTimecode) { - this.baseTimecode = baseTimecode; - } + public void setTrackId(int trackId) { + this.trackId = trackId; + } - public void setDataOffset(int dataOffset) { - this.dataOffset = dataOffset; - } + public void setBaseTimecode(long baseTimecode) { + this.baseTimecode = baseTimecode; + } - public void setDefaultSampleSize(int defaultSampleSize) { - this.defaultSampleSize = defaultSampleSize; - } + public void setDataOffset(int dataOffset) { + this.dataOffset = dataOffset; + } - /** - * Create sample duration and size arrays - * @param hasDurations If duration data is present - * @param hasSizes If size data is present - * @param sampleCount Number of samples - */ - public void createSampleArrays(boolean hasDurations, boolean hasSizes, int sampleCount) { - this.sampleCount = sampleCount; + public void setDefaultSampleSize(int defaultSampleSize) { + this.defaultSampleSize = defaultSampleSize; + } - if (hasDurations) { - sampleDurations = new int[sampleCount]; - } + /** + * Create sample duration and size arrays + * + * @param hasDurations If duration data is present + * @param hasSizes If size data is present + * @param sampleCount Number of samples + */ + public void createSampleArrays(boolean hasDurations, boolean hasSizes, int sampleCount) { + this.sampleCount = sampleCount; + + if (hasDurations) { + sampleDurations = new int[sampleCount]; + } + + if (hasSizes) { + sampleSizes = new int[sampleCount]; + } + } - if (hasSizes) { - sampleSizes = new int[sampleCount]; - } - } + /** + * Set the duration of a specific sample + * + * @param i Sample index + * @param value Duration using the file timescale + */ + public void setDuration(int i, int value) { + sampleDurations[i] = value; + } - /** - * Set the duration of a specific sample - * @param i Sample index - * @param value Duration using the file timescale - */ - public void setDuration(int i, int value) { - sampleDurations[i] = value; - } + /** + * Set the size of a specific sample + * + * @param i Sample index + * @param value Size + */ + public void setSize(int i, int value) { + sampleSizes[i] = value; + } - /** - * Set the size of a specific sample - * @param i Sample index - * @param value Size - */ - public void setSize(int i, int value) { - sampleSizes[i] = value; - } + /** + * @return The final header + */ + public MpegTrackFragmentHeader build() { + int[] finalSampleSizes = sampleSizes; - /** - * @return The final header - */ - public MpegTrackFragmentHeader build() { - int[] finalSampleSizes = sampleSizes; + if (defaultSampleSize != 0) { + finalSampleSizes = new int[sampleCount]; - if (defaultSampleSize != 0) { - finalSampleSizes = new int[sampleCount]; + for (int i = 0; i < sampleCount; i++) { + finalSampleSizes[i] = defaultSampleSize; + } + } - for (int i = 0; i < sampleCount; i++) { - finalSampleSizes[i] = defaultSampleSize; + return new MpegTrackFragmentHeader(trackId, baseTimecode, dataOffset, sampleDurations, finalSampleSizes); } - } - - return new MpegTrackFragmentHeader(trackId, baseTimecode, dataOffset, sampleDurations, finalSampleSizes); } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/standard/MpegStandardFileTrackProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/standard/MpegStandardFileTrackProvider.java index c832808c3..4c036a869 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/standard/MpegStandardFileTrackProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpeg/reader/standard/MpegStandardFileTrackProvider.java @@ -1,8 +1,8 @@ package com.sedmelluq.discord.lavaplayer.container.mpeg.reader.standard; +import com.sedmelluq.discord.lavaplayer.container.mpeg.MpegTrackConsumer; import com.sedmelluq.discord.lavaplayer.container.mpeg.reader.MpegFileTrackProvider; import com.sedmelluq.discord.lavaplayer.container.mpeg.reader.MpegReader; -import com.sedmelluq.discord.lavaplayer.container.mpeg.MpegTrackConsumer; import com.sedmelluq.discord.lavaplayer.container.mpeg.reader.MpegVersionedSectionInfo; import com.sedmelluq.discord.lavaplayer.tools.io.DetachedByteChannel; @@ -18,316 +18,318 @@ * Track provider for the standard (non-fragmented) MP4 file format. */ public class MpegStandardFileTrackProvider implements MpegFileTrackProvider { - private final MpegReader reader; - private final List builders = new ArrayList<>(); - private final Map trackTimescales = new HashMap<>(); - private int timescale; - private int currentChunk; - private MpegTrackConsumer consumer; - private TrackSeekInfo seekInfo; - - /** - * @param reader MP4-specific reader - */ - public MpegStandardFileTrackProvider(MpegReader reader) { - this.reader = reader; - this.currentChunk = 0; - } - - @Override - public boolean initialise(MpegTrackConsumer consumer) { - this.consumer = consumer; - - int trackId = consumer.getTrack().trackId; - - if (!trackTimescales.containsKey(trackId)) { - return false; + private final MpegReader reader; + private final List builders = new ArrayList<>(); + private final Map trackTimescales = new HashMap<>(); + private int timescale; + private int currentChunk; + private MpegTrackConsumer consumer; + private TrackSeekInfo seekInfo; + + /** + * @param reader MP4-specific reader + */ + public MpegStandardFileTrackProvider(MpegReader reader) { + this.reader = reader; + this.currentChunk = 0; } - try { - for (TrackSeekInfoBuilder builder : builders) { - if (builder.trackId == trackId) { - seekInfo = builder.build(); - timescale = trackTimescales.get(trackId); - return true; + @Override + public boolean initialise(MpegTrackConsumer consumer) { + this.consumer = consumer; + + int trackId = consumer.getTrack().trackId; + + if (!trackTimescales.containsKey(trackId)) { + return false; } - } - } finally { - builders.clear(); - } - return false; - } + try { + for (TrackSeekInfoBuilder builder : builders) { + if (builder.trackId == trackId) { + seekInfo = builder.build(); + timescale = trackTimescales.get(trackId); + return true; + } + } + } finally { + builders.clear(); + } - @Override - public long getDuration() { - return seekInfo.totalDuration * 1000L / timescale; - } + return false; + } - @Override - public void provideFrames() throws InterruptedException { - try (ReadableByteChannel channel = new DetachedByteChannel(Channels.newChannel(reader.seek))) { - while (currentChunk < seekInfo.chunkOffsets.length) { - reader.seek.seek(seekInfo.chunkOffsets[currentChunk]); + @Override + public long getDuration() { + return seekInfo.totalDuration * 1000L / timescale; + } - int[] samples = seekInfo.chunkSamples[currentChunk]; - for (int i = 0; i < samples.length; i++) { - consumer.consume(channel, samples[i]); + @Override + public void provideFrames() throws InterruptedException { + try (ReadableByteChannel channel = new DetachedByteChannel(Channels.newChannel(reader.seek))) { + while (currentChunk < seekInfo.chunkOffsets.length) { + reader.seek.seek(seekInfo.chunkOffsets[currentChunk]); + + int[] samples = seekInfo.chunkSamples[currentChunk]; + for (int i = 0; i < samples.length; i++) { + consumer.consume(channel, samples[i]); + } + + currentChunk++; + } + } catch (IOException e) { + throw new RuntimeException(e); } + } - currentChunk++; - } - } catch (IOException e) { - throw new RuntimeException(e); + @Override + public void seekToTimecode(long timecode) { + long scaledTimecode = timecode * timescale / 1000; + int length = seekInfo.chunkOffsets.length; + + if (scaledTimecode >= seekInfo.totalDuration) { + currentChunk = length; + consumer.seekPerformed(timecode, seekInfo.totalDuration * 1000 / timescale); + } else { + for (int i = 0; i < length; i++) { + long nextTimecode = i < length - 1 ? seekInfo.chunkTimecodes[i + 1] : seekInfo.totalDuration; + + if (scaledTimecode < nextTimecode) { + consumer.seekPerformed(timecode, seekInfo.chunkTimecodes[i] * 1000 / timescale); + currentChunk = i; + break; + } + } + } } - } - - @Override - public void seekToTimecode(long timecode) { - long scaledTimecode = timecode * timescale / 1000; - int length = seekInfo.chunkOffsets.length; - - if (scaledTimecode >= seekInfo.totalDuration) { - currentChunk = length; - consumer.seekPerformed(timecode, seekInfo.totalDuration * 1000 / timescale); - } else { - for (int i = 0; i < length; i++) { - long nextTimecode = i < length - 1 ? seekInfo.chunkTimecodes[i + 1] : seekInfo.totalDuration; - - if (scaledTimecode < nextTimecode) { - consumer.seekPerformed(timecode, seekInfo.chunkTimecodes[i] * 1000 / timescale); - currentChunk = i; - break; + + /** + * Read the mdhd section for a track. + * + * @param mdhd The section header + * @param trackId Track ID + * @throws IOException On read error. + */ + public void readMediaHeaders(MpegVersionedSectionInfo mdhd, int trackId) throws IOException { + int trackTimescale; + + if (mdhd.version == 1) { + reader.data.readLong(); // creation time + reader.data.readLong(); // modification time + trackTimescale = reader.data.readInt(); + reader.data.readLong(); // duration + } else { + reader.data.readInt(); // creation time + reader.data.readInt(); // modification time + trackTimescale = reader.data.readInt(); + reader.data.readInt(); // duration } - } + + trackTimescales.put(trackId, trackTimescale); } - } - - /** - * Read the mdhd section for a track. - * @param mdhd The section header - * @param trackId Track ID - * @throws IOException On read error. - */ - public void readMediaHeaders(MpegVersionedSectionInfo mdhd, int trackId) throws IOException { - int trackTimescale; - - if (mdhd.version == 1) { - reader.data.readLong(); // creation time - reader.data.readLong(); // modification time - trackTimescale = reader.data.readInt(); - reader.data.readLong(); // duration - } else { - reader.data.readInt(); // creation time - reader.data.readInt(); // modification time - trackTimescale = reader.data.readInt(); - reader.data.readInt(); // duration + + /** + * Attaches standard format specific handlers to sample table section handle chain. + * + * @param sampleTableChain Sample table child section handler chain. + * @param trackId Track ID + */ + public void attachSampleTableParsers(MpegReader.Chain sampleTableChain, int trackId) { + final TrackSeekInfoBuilder seekInfoBuilder = new TrackSeekInfoBuilder(trackId); + + sampleTableChain + .handleVersioned("stts", stts -> parseTimeToSample(seekInfoBuilder)) + .handleVersioned("stsc", stsc -> parseSampleToChunk(seekInfoBuilder)) + .handleVersioned("stsz", stsz -> parseSampleSizes(seekInfoBuilder)) + .handleVersioned("stco", stco -> parseChunkOffsets32(seekInfoBuilder)) + .handleVersioned("co64", co64 -> parseChunkOffsets64(seekInfoBuilder)); + + builders.add(seekInfoBuilder); } - trackTimescales.put(trackId, trackTimescale); - } - - /** - * Attaches standard format specific handlers to sample table section handle chain. - * @param sampleTableChain Sample table child section handler chain. - * @param trackId Track ID - */ - public void attachSampleTableParsers(MpegReader.Chain sampleTableChain, int trackId) { - final TrackSeekInfoBuilder seekInfoBuilder = new TrackSeekInfoBuilder(trackId); - - sampleTableChain - .handleVersioned("stts", stts -> parseTimeToSample(seekInfoBuilder)) - .handleVersioned("stsc", stsc -> parseSampleToChunk(seekInfoBuilder)) - .handleVersioned("stsz", stsz -> parseSampleSizes(seekInfoBuilder)) - .handleVersioned("stco", stco -> parseChunkOffsets32(seekInfoBuilder)) - .handleVersioned("co64", co64 -> parseChunkOffsets64(seekInfoBuilder)); - - builders.add(seekInfoBuilder); - } - - private void parseTimeToSample(TrackSeekInfoBuilder seekInfoBuilder) throws IOException { - int entries = reader.data.readInt(); - seekInfoBuilder.sampleTimeCounts = new int[entries]; - seekInfoBuilder.sampleTimeDeltas = new int[entries]; - seekInfoBuilder.presence |= 1; - - for (int i = 0; i < entries; i++) { - seekInfoBuilder.sampleTimeCounts[i] = reader.data.readInt(); - seekInfoBuilder.sampleTimeDeltas[i] = reader.data.readInt(); + private void parseTimeToSample(TrackSeekInfoBuilder seekInfoBuilder) throws IOException { + int entries = reader.data.readInt(); + seekInfoBuilder.sampleTimeCounts = new int[entries]; + seekInfoBuilder.sampleTimeDeltas = new int[entries]; + seekInfoBuilder.presence |= 1; + + for (int i = 0; i < entries; i++) { + seekInfoBuilder.sampleTimeCounts[i] = reader.data.readInt(); + seekInfoBuilder.sampleTimeDeltas[i] = reader.data.readInt(); + } } - } - - private void parseSampleToChunk(TrackSeekInfoBuilder seekInfoBuilder) throws IOException { - int entries = reader.data.readInt(); - seekInfoBuilder.sampleChunkingFirst = new int[entries]; - seekInfoBuilder.sampleChunkingCount = new int[entries]; - seekInfoBuilder.presence |= 2; - - for (int i = 0; i < entries; i++) { - seekInfoBuilder.sampleChunkingFirst[i] = reader.data.readInt(); - seekInfoBuilder.sampleChunkingCount[i] = reader.data.readInt(); - reader.data.readInt(); + + private void parseSampleToChunk(TrackSeekInfoBuilder seekInfoBuilder) throws IOException { + int entries = reader.data.readInt(); + seekInfoBuilder.sampleChunkingFirst = new int[entries]; + seekInfoBuilder.sampleChunkingCount = new int[entries]; + seekInfoBuilder.presence |= 2; + + for (int i = 0; i < entries; i++) { + seekInfoBuilder.sampleChunkingFirst[i] = reader.data.readInt(); + seekInfoBuilder.sampleChunkingCount[i] = reader.data.readInt(); + reader.data.readInt(); + } } - } - private void parseSampleSizes(TrackSeekInfoBuilder seekInfoBuilder) throws IOException { - seekInfoBuilder.sampleSize = reader.data.readInt(); - seekInfoBuilder.sampleCount = reader.data.readInt(); - seekInfoBuilder.presence |= 4; + private void parseSampleSizes(TrackSeekInfoBuilder seekInfoBuilder) throws IOException { + seekInfoBuilder.sampleSize = reader.data.readInt(); + seekInfoBuilder.sampleCount = reader.data.readInt(); + seekInfoBuilder.presence |= 4; - if (seekInfoBuilder.sampleSize == 0) { - seekInfoBuilder.sampleSizes = new int[seekInfoBuilder.sampleCount]; + if (seekInfoBuilder.sampleSize == 0) { + seekInfoBuilder.sampleSizes = new int[seekInfoBuilder.sampleCount]; - for (int i = 0; i < seekInfoBuilder.sampleCount; i++) { - seekInfoBuilder.sampleSizes[i] = reader.data.readInt(); - } + for (int i = 0; i < seekInfoBuilder.sampleCount; i++) { + seekInfoBuilder.sampleSizes[i] = reader.data.readInt(); + } + } } - } - private void parseChunkOffsets32(TrackSeekInfoBuilder seekInfoBuilder) throws IOException { - int chunks = reader.data.readInt(); - seekInfoBuilder.chunkOffsets = new long[chunks]; - seekInfoBuilder.presence |= 8; + private void parseChunkOffsets32(TrackSeekInfoBuilder seekInfoBuilder) throws IOException { + int chunks = reader.data.readInt(); + seekInfoBuilder.chunkOffsets = new long[chunks]; + seekInfoBuilder.presence |= 8; - for (int i = 0; i < chunks; i++) { - seekInfoBuilder.chunkOffsets[i] = reader.data.readInt(); + for (int i = 0; i < chunks; i++) { + seekInfoBuilder.chunkOffsets[i] = reader.data.readInt(); + } } - } - private void parseChunkOffsets64(TrackSeekInfoBuilder seekInfoBuilder) throws IOException { - int chunks = reader.data.readInt(); - seekInfoBuilder.chunkOffsets = new long[chunks]; - seekInfoBuilder.presence |= 8; + private void parseChunkOffsets64(TrackSeekInfoBuilder seekInfoBuilder) throws IOException { + int chunks = reader.data.readInt(); + seekInfoBuilder.chunkOffsets = new long[chunks]; + seekInfoBuilder.presence |= 8; - for (int i = 0; i < chunks; i++) { - seekInfoBuilder.chunkOffsets[i] = reader.data.readLong(); - } - } - - private static class TrackSeekInfo { - private final long totalDuration; - private final long[] chunkOffsets; - private final long[] chunkTimecodes; - private final int[][] chunkSamples; - - private TrackSeekInfo(long totalDuration, long[] chunkOffsets, long[] chunkTimecodes, int[][] chunkSamples) { - this.totalDuration = totalDuration; - this.chunkOffsets = chunkOffsets; - this.chunkTimecodes = chunkTimecodes; - this.chunkSamples = chunkSamples; - } - } - - private static class TrackSeekInfoBuilder { - private final int trackId; - private int presence; - private int[] sampleTimeCounts; - private int[] sampleTimeDeltas; - private int[] sampleChunkingFirst; - private int[] sampleChunkingCount; - private long[] chunkOffsets; - private int sampleSize; - private int sampleCount; - private int[] sampleSizes; - - private TrackSeekInfoBuilder(int trackId) { - this.trackId = trackId; + for (int i = 0; i < chunks; i++) { + seekInfoBuilder.chunkOffsets[i] = reader.data.readLong(); + } } - private TrackSeekInfo build() { - if (presence != 15) { - return null; - } + private static class TrackSeekInfo { + private final long totalDuration; + private final long[] chunkOffsets; + private final long[] chunkTimecodes; + private final int[][] chunkSamples; + + private TrackSeekInfo(long totalDuration, long[] chunkOffsets, long[] chunkTimecodes, int[][] chunkSamples) { + this.totalDuration = totalDuration; + this.chunkOffsets = chunkOffsets; + this.chunkTimecodes = chunkTimecodes; + this.chunkSamples = chunkSamples; + } + } - long[] chunkTimecodes = new long[chunkOffsets.length]; - int[][] chunkSamples = new int[chunkOffsets.length][]; + private static class TrackSeekInfoBuilder { + private final int trackId; + private int presence; + private int[] sampleTimeCounts; + private int[] sampleTimeDeltas; + private int[] sampleChunkingFirst; + private int[] sampleChunkingCount; + private long[] chunkOffsets; + private int sampleSize; + private int sampleCount; + private int[] sampleSizes; + + private TrackSeekInfoBuilder(int trackId) { + this.trackId = trackId; + } - SampleChunkingIterator chunkingIterator = new SampleChunkingIterator(sampleChunkingFirst, sampleChunkingCount); - SampleDurationIterator durationIterator = new SampleDurationIterator(sampleTimeCounts, sampleTimeDeltas); + private TrackSeekInfo build() { + if (presence != 15) { + return null; + } - int sampleOffset = 0; - long timeOffset = 0; + long[] chunkTimecodes = new long[chunkOffsets.length]; + int[][] chunkSamples = new int[chunkOffsets.length][]; - for (int chunk = 0; chunk < chunkOffsets.length; chunk++) { - int chunkSampleCount = chunkingIterator.nextSampleCount(); + SampleChunkingIterator chunkingIterator = new SampleChunkingIterator(sampleChunkingFirst, sampleChunkingCount); + SampleDurationIterator durationIterator = new SampleDurationIterator(sampleTimeCounts, sampleTimeDeltas); - chunkSamples[chunk] = buildChunkSampleSizes(chunkSampleCount, sampleOffset, sampleSize, sampleSizes); - chunkTimecodes[chunk] = timeOffset; + int sampleOffset = 0; + long timeOffset = 0; - timeOffset += calculateChunkDuration(chunkSampleCount, durationIterator); - sampleOffset += chunkSampleCount; - } + for (int chunk = 0; chunk < chunkOffsets.length; chunk++) { + int chunkSampleCount = chunkingIterator.nextSampleCount(); - return new TrackSeekInfo(timeOffset, chunkOffsets, chunkTimecodes, chunkSamples); - } + chunkSamples[chunk] = buildChunkSampleSizes(chunkSampleCount, sampleOffset, sampleSize, sampleSizes); + chunkTimecodes[chunk] = timeOffset; - private static int[] buildChunkSampleSizes(int sampleCount, int sampleOffset, int sampleSize, int[] sampleSizes) { - int[] chunkSampleSizes = new int[sampleCount]; + timeOffset += calculateChunkDuration(chunkSampleCount, durationIterator); + sampleOffset += chunkSampleCount; + } - if (sampleSize != 0) { - for (int i = 0; i < sampleCount; i++) { - chunkSampleSizes[i] = sampleSize; + return new TrackSeekInfo(timeOffset, chunkOffsets, chunkTimecodes, chunkSamples); } - } else { - System.arraycopy(sampleSizes, sampleOffset, chunkSampleSizes, 0, sampleCount); - } - return chunkSampleSizes; - } + private static int[] buildChunkSampleSizes(int sampleCount, int sampleOffset, int sampleSize, int[] sampleSizes) { + int[] chunkSampleSizes = new int[sampleCount]; - private static int calculateChunkDuration(int sampleCount, SampleDurationIterator durationIterator) { - int duration = 0; + if (sampleSize != 0) { + for (int i = 0; i < sampleCount; i++) { + chunkSampleSizes[i] = sampleSize; + } + } else { + System.arraycopy(sampleSizes, sampleOffset, chunkSampleSizes, 0, sampleCount); + } - for (int i = 0; i < sampleCount; i++) { - duration += durationIterator.nextSampleDuration(); - } + return chunkSampleSizes; + } - return duration; - } - } + private static int calculateChunkDuration(int sampleCount, SampleDurationIterator durationIterator) { + int duration = 0; - private static class SampleChunkingIterator { - private final int[] sampleChunkingFirst; - private final int[] sampleChunkingCount; - private int chunkIndex = 1; - private int entryIndex = 0; + for (int i = 0; i < sampleCount; i++) { + duration += durationIterator.nextSampleDuration(); + } - private SampleChunkingIterator(int[] sampleChunkingFirst, int[] sampleChunkingCount) { - this.sampleChunkingFirst = sampleChunkingFirst; - this.sampleChunkingCount = sampleChunkingCount; + return duration; + } } - private int nextSampleCount() { - int result = sampleChunkingCount[entryIndex]; - chunkIndex++; + private static class SampleChunkingIterator { + private final int[] sampleChunkingFirst; + private final int[] sampleChunkingCount; + private int chunkIndex = 1; + private int entryIndex = 0; - if (entryIndex + 1 < sampleChunkingFirst.length && chunkIndex == sampleChunkingFirst[entryIndex + 1]) { - entryIndex++; - } + private SampleChunkingIterator(int[] sampleChunkingFirst, int[] sampleChunkingCount) { + this.sampleChunkingFirst = sampleChunkingFirst; + this.sampleChunkingCount = sampleChunkingCount; + } - return result; - } - } + private int nextSampleCount() { + int result = sampleChunkingCount[entryIndex]; + chunkIndex++; - private static class SampleDurationIterator { - private final int[] sampleTimeCounts; - private final int[] sampleTimeDeltas; - private int relativeSampleIndex = 0; - private int entryIndex = 0; + if (entryIndex + 1 < sampleChunkingFirst.length && chunkIndex == sampleChunkingFirst[entryIndex + 1]) { + entryIndex++; + } - private SampleDurationIterator(int[] sampleTimeCounts, int[] sampleTimeDeltas) { - this.sampleTimeCounts = sampleTimeCounts; - this.sampleTimeDeltas = sampleTimeDeltas; + return result; + } } - private int nextSampleDuration() { - int result = sampleTimeDeltas[entryIndex]; + private static class SampleDurationIterator { + private final int[] sampleTimeCounts; + private final int[] sampleTimeDeltas; + private int relativeSampleIndex = 0; + private int entryIndex = 0; - if (entryIndex + 1 < sampleTimeCounts.length && ++relativeSampleIndex >= sampleTimeCounts[entryIndex]) { - entryIndex++; - } + private SampleDurationIterator(int[] sampleTimeCounts, int[] sampleTimeDeltas) { + this.sampleTimeCounts = sampleTimeCounts; + this.sampleTimeDeltas = sampleTimeDeltas; + } + + private int nextSampleDuration() { + int result = sampleTimeDeltas[entryIndex]; - return result; + if (entryIndex + 1 < sampleTimeCounts.length && ++relativeSampleIndex >= sampleTimeCounts[entryIndex]) { + entryIndex++; + } + + return result; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/MpegAdtsAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/MpegAdtsAudioTrack.java index 1390a6aa9..18732fe3d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/MpegAdtsAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/MpegAdtsAudioTrack.java @@ -10,21 +10,21 @@ import static com.sedmelluq.discord.lavaplayer.container.mpegts.MpegTsElementaryInputStream.ADTS_ELEMENTARY_STREAM; public class MpegAdtsAudioTrack extends DelegatedAudioTrack { - private final InputStream inputStream; + private final InputStream inputStream; - /** - * @param trackInfo Track info - */ - public MpegAdtsAudioTrack(AudioTrackInfo trackInfo, InputStream inputStream) { - super(trackInfo); + /** + * @param trackInfo Track info + */ + public MpegAdtsAudioTrack(AudioTrackInfo trackInfo, InputStream inputStream) { + super(trackInfo); - this.inputStream = inputStream; - } + this.inputStream = inputStream; + } - @Override - public void process(LocalAudioTrackExecutor executor) throws Exception { - MpegTsElementaryInputStream elementaryInputStream = new MpegTsElementaryInputStream(inputStream, ADTS_ELEMENTARY_STREAM); - PesPacketInputStream pesPacketInputStream = new PesPacketInputStream(elementaryInputStream); - processDelegate(new AdtsAudioTrack(trackInfo, pesPacketInputStream), executor); - } + @Override + public void process(LocalAudioTrackExecutor executor) throws Exception { + MpegTsElementaryInputStream elementaryInputStream = new MpegTsElementaryInputStream(inputStream, ADTS_ELEMENTARY_STREAM); + PesPacketInputStream pesPacketInputStream = new PesPacketInputStream(elementaryInputStream); + processDelegate(new AdtsAudioTrack(trackInfo, pesPacketInputStream), executor); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/MpegAdtsContainerProbe.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/MpegAdtsContainerProbe.java index 1a7289fb5..7e0d188c4 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/MpegAdtsContainerProbe.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/MpegAdtsContainerProbe.java @@ -19,57 +19,57 @@ import static com.sedmelluq.discord.lavaplayer.container.mpegts.MpegTsElementaryInputStream.ADTS_ELEMENTARY_STREAM; public class MpegAdtsContainerProbe implements MediaContainerProbe { - private static final Logger log = LoggerFactory.getLogger(MpegAdtsContainerProbe.class); + private static final Logger log = LoggerFactory.getLogger(MpegAdtsContainerProbe.class); - @Override - public String getName() { - return "mpegts-adts"; - } + @Override + public String getName() { + return "mpegts-adts"; + } - @Override - public boolean matchesHints(MediaContainerHints hints) { - return "ts".equalsIgnoreCase(hints.fileExtension); - } + @Override + public boolean matchesHints(MediaContainerHints hints) { + return "ts".equalsIgnoreCase(hints.fileExtension); + } - @Override - public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) - throws IOException { + @Override + public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) + throws IOException { - SavedHeadSeekableInputStream head = inputStream instanceof SavedHeadSeekableInputStream ? - (SavedHeadSeekableInputStream) inputStream : null; + SavedHeadSeekableInputStream head = inputStream instanceof SavedHeadSeekableInputStream ? + (SavedHeadSeekableInputStream) inputStream : null; - if (head != null) { - head.setAllowDirectReads(false); - } + if (head != null) { + head.setAllowDirectReads(false); + } - MpegTsElementaryInputStream tsStream = new MpegTsElementaryInputStream(inputStream, ADTS_ELEMENTARY_STREAM); - PesPacketInputStream pesStream = new PesPacketInputStream(tsStream); - AdtsStreamReader reader = new AdtsStreamReader(pesStream); + MpegTsElementaryInputStream tsStream = new MpegTsElementaryInputStream(inputStream, ADTS_ELEMENTARY_STREAM); + PesPacketInputStream pesStream = new PesPacketInputStream(tsStream); + AdtsStreamReader reader = new AdtsStreamReader(pesStream); - try { - if (reader.findPacketHeader() != null) { - log.debug("Track {} is an MPEG-TS stream with an ADTS track.", reference.identifier); + try { + if (reader.findPacketHeader() != null) { + log.debug("Track {} is an MPEG-TS stream with an ADTS track.", reference.identifier); - return supportedFormat(this, null, - AudioTrackInfoBuilder.create(reference, inputStream) - .apply(tsStream.getLoadedMetadata()) - .build() - ); - } - } catch (IndexOutOfBoundsException ignored) { - // TS stream read too far and still did not find required elementary stream - SavedHeadSeekableInputStream throws - // this because we disabled reads past the loaded "head". - } finally { - if (head != null) { - head.setAllowDirectReads(true); - } - } + return supportedFormat(this, null, + AudioTrackInfoBuilder.create(reference, inputStream) + .apply(tsStream.getLoadedMetadata()) + .build() + ); + } + } catch (IndexOutOfBoundsException ignored) { + // TS stream read too far and still did not find required elementary stream - SavedHeadSeekableInputStream throws + // this because we disabled reads past the loaded "head". + } finally { + if (head != null) { + head.setAllowDirectReads(true); + } + } - return null; - } + return null; + } - @Override - public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - return new MpegAdtsAudioTrack(trackInfo, inputStream); - } + @Override + public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + return new MpegAdtsAudioTrack(trackInfo, inputStream); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/MpegTsElementaryInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/MpegTsElementaryInputStream.java index 5f9c087bd..768196ff3 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/MpegTsElementaryInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/MpegTsElementaryInputStream.java @@ -2,7 +2,6 @@ import com.sedmelluq.discord.lavaplayer.tools.io.BitBufferReader; import com.sedmelluq.discord.lavaplayer.tools.io.GreedyInputStream; -import com.sedmelluq.discord.lavaplayer.track.info.AudioTrackInfoBuilder; import com.sedmelluq.discord.lavaplayer.track.info.AudioTrackInfoProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,327 +16,327 @@ * elementary data type. */ public class MpegTsElementaryInputStream extends InputStream { - private static final Logger log = LoggerFactory.getLogger(MpegTsElementaryInputStream.class); - - public static final int ADTS_ELEMENTARY_STREAM = 0x0F; - - private static final int PID_UNKNOWN = -1; - private static final int PID_NOT_PRESENT = -2; - - private static final int PACKET_IDENTIFIER_SDT = 0x11; - - private static final int TS_PACKET_SIZE = 188; - - private final InputStream inputStream; - private final int elementaryDataType; - private final byte[] packet; - private final ByteBuffer packetBuffer; - private final BitBufferReader bufferReader; - private int elementaryStreamIdentifier; - private int programMapIdentifier; - private boolean elementaryDataInPacket; - private boolean streamEndReached; - - private String serviceProviderName; - private String serviceName; - - /** - * @param inputStream Underlying input stream - * @param elementaryDataType ID of the media type to pass upstream - */ - public MpegTsElementaryInputStream(InputStream inputStream, int elementaryDataType) { - this.inputStream = new GreedyInputStream(inputStream); - this.elementaryDataType = elementaryDataType; - this.packet = new byte[TS_PACKET_SIZE]; - this.packetBuffer = ByteBuffer.wrap(packet); - this.bufferReader = new BitBufferReader(packetBuffer); - this.elementaryStreamIdentifier = PID_UNKNOWN; - this.programMapIdentifier = PID_UNKNOWN; - } - - public AudioTrackInfoProvider getLoadedMetadata() { - return new AudioTrackInfoProvider() { - @Override - public String getTitle() { - return serviceName; - } - - @Override - public String getAuthor() { - return serviceProviderName; - } - - @Override - public Long getLength() { - return null; - } - - @Override - public String getIdentifier() { - return null; - } - - @Override - public String getUri() { - return null; - } - - @Override - public String getArtworkUrl() { - return null; - } - - @Override - public String getISRC() { - return null; - } - }; - } - - @Override - public int read() throws IOException { - if (!findElementaryData()) { - return -1; + private static final Logger log = LoggerFactory.getLogger(MpegTsElementaryInputStream.class); + + public static final int ADTS_ELEMENTARY_STREAM = 0x0F; + + private static final int PID_UNKNOWN = -1; + private static final int PID_NOT_PRESENT = -2; + + private static final int PACKET_IDENTIFIER_SDT = 0x11; + + private static final int TS_PACKET_SIZE = 188; + + private final InputStream inputStream; + private final int elementaryDataType; + private final byte[] packet; + private final ByteBuffer packetBuffer; + private final BitBufferReader bufferReader; + private int elementaryStreamIdentifier; + private int programMapIdentifier; + private boolean elementaryDataInPacket; + private boolean streamEndReached; + + private String serviceProviderName; + private String serviceName; + + /** + * @param inputStream Underlying input stream + * @param elementaryDataType ID of the media type to pass upstream + */ + public MpegTsElementaryInputStream(InputStream inputStream, int elementaryDataType) { + this.inputStream = new GreedyInputStream(inputStream); + this.elementaryDataType = elementaryDataType; + this.packet = new byte[TS_PACKET_SIZE]; + this.packetBuffer = ByteBuffer.wrap(packet); + this.bufferReader = new BitBufferReader(packetBuffer); + this.elementaryStreamIdentifier = PID_UNKNOWN; + this.programMapIdentifier = PID_UNKNOWN; } - int result = packetBuffer.get() & 0xFF; + public AudioTrackInfoProvider getLoadedMetadata() { + return new AudioTrackInfoProvider() { + @Override + public String getTitle() { + return serviceName; + } + + @Override + public String getAuthor() { + return serviceProviderName; + } + + @Override + public Long getLength() { + return null; + } + + @Override + public String getIdentifier() { + return null; + } + + @Override + public String getUri() { + return null; + } + + @Override + public String getArtworkUrl() { + return null; + } + + @Override + public String getISRC() { + return null; + } + }; + } + + @Override + public int read() throws IOException { + if (!findElementaryData()) { + return -1; + } - checkElementaryDataEnd(); - return result; - } + int result = packetBuffer.get() & 0xFF; - @Override - public int read(byte[] buffer, int offset, int length) throws IOException { - if (!findElementaryData()) { - return -1; + checkElementaryDataEnd(); + return result; } - int chunk = Math.min(length, packetBuffer.remaining()); - packetBuffer.get(buffer, offset, chunk); + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + if (!findElementaryData()) { + return -1; + } - checkElementaryDataEnd(); + int chunk = Math.min(length, packetBuffer.remaining()); + packetBuffer.get(buffer, offset, chunk); - return chunk; - } + checkElementaryDataEnd(); - private boolean findElementaryData() throws IOException { - if (!elementaryDataInPacket) { - while (processPacket()) { - if (elementaryDataInPacket) { - return true; - } - } + return chunk; } - return elementaryDataInPacket; - } + private boolean findElementaryData() throws IOException { + if (!elementaryDataInPacket) { + while (processPacket()) { + if (elementaryDataInPacket) { + return true; + } + } + } - private void checkElementaryDataEnd() { - if (packetBuffer.remaining() == 0) { - elementaryDataInPacket = false; + return elementaryDataInPacket; } - } - - private boolean processPacket() throws IOException { - if (!isContinuable()) { - return false; - } else if (inputStream.read(packet) < packet.length) { - streamEndReached = true; - return false; + + private void checkElementaryDataEnd() { + if (packetBuffer.remaining() == 0) { + elementaryDataInPacket = false; + } } - packetBuffer.clear(); - bufferReader.readRemainingBits(); + private boolean processPacket() throws IOException { + if (!isContinuable()) { + return false; + } else if (inputStream.read(packet) < packet.length) { + streamEndReached = true; + return false; + } + + packetBuffer.clear(); + bufferReader.readRemainingBits(); + + int identifier = verifyPacket(bufferReader, packetBuffer); + if (identifier == -1) { + return false; + } - int identifier = verifyPacket(bufferReader, packetBuffer); - if (identifier == -1) { - return false; + processPacketContent(identifier); + return isContinuable(); } - processPacketContent(identifier); - return isContinuable(); - } - - private void processPacketContent(int identifier) { - if (identifier == 0 || identifier == programMapIdentifier) { - if (identifier == 0) { - programMapIdentifier = PID_NOT_PRESENT; - } - - processProgramPacket(); - } else if (identifier == elementaryStreamIdentifier) { - elementaryDataInPacket = true; - } else if (identifier == PACKET_IDENTIFIER_SDT) { - try { - parseSdtTable(); - } catch (RuntimeException e) { - log.warn("Exception when parsing MPEG-TS SDT table.", e); - } + private void processPacketContent(int identifier) { + if (identifier == 0 || identifier == programMapIdentifier) { + if (identifier == 0) { + programMapIdentifier = PID_NOT_PRESENT; + } + + processProgramPacket(); + } else if (identifier == elementaryStreamIdentifier) { + elementaryDataInPacket = true; + } else if (identifier == PACKET_IDENTIFIER_SDT) { + try { + parseSdtTable(); + } catch (RuntimeException e) { + log.warn("Exception when parsing MPEG-TS SDT table.", e); + } + } } - } - private void parseSdtTable() { - bufferReader.asLong(20); - int sectionLength = bufferReader.asInteger(12); - bufferReader.asLong(64); + private void parseSdtTable() { + bufferReader.asLong(20); + int sectionLength = bufferReader.asInteger(12); + bufferReader.asLong(64); - if (sectionLength > 0) { - bufferReader.asLong(28); - int loopLength = bufferReader.asInteger(12); + if (sectionLength > 0) { + bufferReader.asLong(28); + int loopLength = bufferReader.asInteger(12); - if (loopLength > 0) { - int descriptorTag = bufferReader.asInteger(8); + if (loopLength > 0) { + int descriptorTag = bufferReader.asInteger(8); - if (descriptorTag == 0x48) { - bufferReader.asLong(16); + if (descriptorTag == 0x48) { + bufferReader.asLong(16); - serviceProviderName = parseSdtAsciiString(); - serviceName = parseSdtAsciiString(); + serviceProviderName = parseSdtAsciiString(); + serviceName = parseSdtAsciiString(); + } + } } - } } - } - private String parseSdtAsciiString() { - int length = packetBuffer.get() & 0xFF; + private String parseSdtAsciiString() { + int length = packetBuffer.get() & 0xFF; - if (length > 0) { - byte[] textBytes = new byte[length]; - packetBuffer.get(textBytes); + if (length > 0) { + byte[] textBytes = new byte[length]; + packetBuffer.get(textBytes); - return new String(textBytes, 0, textBytes.length, StandardCharsets.US_ASCII); - } else { - return null; + return new String(textBytes, 0, textBytes.length, StandardCharsets.US_ASCII); + } else { + return null; + } } - } - private boolean isContinuable() { - return !streamEndReached || programMapIdentifier != PID_NOT_PRESENT && elementaryStreamIdentifier != PID_NOT_PRESENT; - } - - private void processProgramPacket() { - discardPointerField(); + private boolean isContinuable() { + return !streamEndReached || programMapIdentifier != PID_NOT_PRESENT && elementaryStreamIdentifier != PID_NOT_PRESENT; + } - while (packetBuffer.hasRemaining()) { - int tableIdentifier = packetBuffer.get() & 0xFF; - if (tableIdentifier == 0xFF) { - break; - } + private void processProgramPacket() { + discardPointerField(); - int sectionInfo = bufferReader.asInteger(6); - int sectionLength = bufferReader.asInteger(10); - int position = packetBuffer.position(); - bufferReader.readRemainingBits(); + while (packetBuffer.hasRemaining()) { + int tableIdentifier = packetBuffer.get() & 0xFF; + if (tableIdentifier == 0xFF) { + break; + } - if (tableIdentifier == 0) { - processPatTable(sectionInfo); - } else if (tableIdentifier == 2) { - processPmtTable(sectionInfo, sectionLength); - } + int sectionInfo = bufferReader.asInteger(6); + int sectionLength = bufferReader.asInteger(10); + int position = packetBuffer.position(); + bufferReader.readRemainingBits(); - packetBuffer.position(position + sectionLength); - } - } + if (tableIdentifier == 0) { + processPatTable(sectionInfo); + } else if (tableIdentifier == 2) { + processPmtTable(sectionInfo, sectionLength); + } - private boolean processPatPmtCommon(int sectionInfo) { - if (sectionInfo != 0x2C) { - return false; + packetBuffer.position(position + sectionLength); + } } - // Table syntax section, boring. - bufferReader.asLong(40); - return true; - } + private boolean processPatPmtCommon(int sectionInfo) { + if (sectionInfo != 0x2C) { + return false; + } - private void processPatTable(int sectionInfo) { - if (!processPatPmtCommon(sectionInfo)) { - return; + // Table syntax section, boring. + bufferReader.asLong(40); + return true; } - // Program number - bufferReader.asLong(16); + private void processPatTable(int sectionInfo) { + if (!processPatPmtCommon(sectionInfo)) { + return; + } + + // Program number + bufferReader.asLong(16); - if (bufferReader.asLong(3) == 0x07) { - programMapIdentifier = bufferReader.asInteger(13); + if (bufferReader.asLong(3) == 0x07) { + programMapIdentifier = bufferReader.asInteger(13); + } } - } - private void processPmtTable(int sectionInfo, int sectionLength) { - int endPosition = packetBuffer.position() + sectionLength; + private void processPmtTable(int sectionInfo, int sectionLength) { + int endPosition = packetBuffer.position() + sectionLength; - if (!processPatPmtCommon(sectionInfo) || bufferReader.asInteger(3) != 0x07) { - return; - } + if (!processPatPmtCommon(sectionInfo) || bufferReader.asInteger(3) != 0x07) { + return; + } - // Clock packet identifier (PCR PID) - bufferReader.asLong(13); - // Reserved bits (must be 1111) and program info length unused bits (must be 00) - if (bufferReader.asLong(6) != 0x3C) { - return; - } + // Clock packet identifier (PCR PID) + bufferReader.asLong(13); + // Reserved bits (must be 1111) and program info length unused bits (must be 00) + if (bufferReader.asLong(6) != 0x3C) { + return; + } - // Skip program descriptors - int programInfoLength = bufferReader.asInteger(10); - packetBuffer.position(packetBuffer.position() + programInfoLength); + // Skip program descriptors + int programInfoLength = bufferReader.asInteger(10); + packetBuffer.position(packetBuffer.position() + programInfoLength); - processElementaryStreams(endPosition); - } + processElementaryStreams(endPosition); + } - private void processElementaryStreams(int endPosition) { - elementaryStreamIdentifier = PID_NOT_PRESENT; + private void processElementaryStreams(int endPosition) { + elementaryStreamIdentifier = PID_NOT_PRESENT; - while (packetBuffer.position() < endPosition - 4) { - int streamType = bufferReader.asInteger(8); - // Reserved bits (must be 111) - bufferReader.asInteger(3); + while (packetBuffer.position() < endPosition - 4) { + int streamType = bufferReader.asInteger(8); + // Reserved bits (must be 111) + bufferReader.asInteger(3); - int streamPid = bufferReader.asInteger(13); - // 4 reserved bits (1111) and 2 ES Info length unused bits (00) - bufferReader.asLong(6); + int streamPid = bufferReader.asInteger(13); + // 4 reserved bits (1111) and 2 ES Info length unused bits (00) + bufferReader.asLong(6); - int infoLength = bufferReader.asInteger(10); - packetBuffer.position(packetBuffer.position() + infoLength); + int infoLength = bufferReader.asInteger(10); + packetBuffer.position(packetBuffer.position() + infoLength); - if (streamType == elementaryDataType) { - elementaryStreamIdentifier = streamPid; - } + if (streamType == elementaryDataType) { + elementaryStreamIdentifier = streamPid; + } + } } - } - private void discardPointerField() { - int pointerField = packetBuffer.get(); + private void discardPointerField() { + int pointerField = packetBuffer.get(); - for (int i = 0; i < pointerField; i++) { - packetBuffer.get(); + for (int i = 0; i < pointerField; i++) { + packetBuffer.get(); + } } - } - private static int verifyPacket(BitBufferReader reader, ByteBuffer rawBuffer) { - if (reader.asLong(8) != 'G') { - return -1; - } + private static int verifyPacket(BitBufferReader reader, ByteBuffer rawBuffer) { + if (reader.asLong(8) != 'G') { + return -1; + } - // Not important for this case - reader.asLong(3); + // Not important for this case + reader.asLong(3); - int identifier = reader.asInteger(13); - long scrambling = reader.asLong(2); + int identifier = reader.asInteger(13); + long scrambling = reader.asLong(2); - // Adaptation - long adaptation = reader.asLong(2); + // Adaptation + long adaptation = reader.asLong(2); - if (scrambling != 0) { - return -1; - } + if (scrambling != 0) { + return -1; + } - // Continuity counter - reader.asLong(4); + // Continuity counter + reader.asLong(4); - if (adaptation == 2 || adaptation == 3) { - int adaptationSize = reader.asInteger(8); - rawBuffer.position(rawBuffer.position() + adaptationSize); - } + if (adaptation == 2 || adaptation == 3) { + int adaptationSize = reader.asInteger(8); + rawBuffer.position(rawBuffer.position() + adaptationSize); + } - return identifier; - } + return identifier; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/PesPacketInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/PesPacketInputStream.java index 0536bb322..1c7ec8bf7 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/PesPacketInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/mpegts/PesPacketInputStream.java @@ -11,96 +11,96 @@ * upstream. */ public class PesPacketInputStream extends InputStream { - private static final byte[] SYNC_BYTES = new byte[] { 0x00, 0x00, 0x01 }; - - private final InputStream inputStream; - private final byte[] lengthBufferBytes; - private final ByteBuffer lengthBuffer; - private int packetBytesLeft; - - /** - * @param inputStream Underlying input stream. - */ - public PesPacketInputStream(InputStream inputStream) { - this.inputStream = new GreedyInputStream(inputStream); - this.lengthBufferBytes = new byte[2]; - this.lengthBuffer = ByteBuffer.wrap(lengthBufferBytes); - } - - private boolean makeBytesAvailable() throws IOException { - if (packetBytesLeft > 0) { - return true; + private static final byte[] SYNC_BYTES = new byte[]{0x00, 0x00, 0x01}; + + private final InputStream inputStream; + private final byte[] lengthBufferBytes; + private final ByteBuffer lengthBuffer; + private int packetBytesLeft; + + /** + * @param inputStream Underlying input stream. + */ + public PesPacketInputStream(InputStream inputStream) { + this.inputStream = new GreedyInputStream(inputStream); + this.lengthBufferBytes = new byte[2]; + this.lengthBuffer = ByteBuffer.wrap(lengthBufferBytes); } - int streamByte; - int matched = 0; - boolean packetFound = false; + private boolean makeBytesAvailable() throws IOException { + if (packetBytesLeft > 0) { + return true; + } - while (!packetFound && (streamByte = inputStream.read()) != -1) { - if (streamByte == SYNC_BYTES[matched]) { - if (++matched == SYNC_BYTES.length) { - matched = 0; - packetFound = processPacketHeader(); + int streamByte; + int matched = 0; + boolean packetFound = false; + + while (!packetFound && (streamByte = inputStream.read()) != -1) { + if (streamByte == SYNC_BYTES[matched]) { + if (++matched == SYNC_BYTES.length) { + matched = 0; + packetFound = processPacketHeader(); + } + } else { + matched = 0; + } } - } else { - matched = 0; - } + + return packetFound; } - return packetFound; - } + private boolean processPacketHeader() throws IOException { + // No need to check stream ID value + if (inputStream.read() == -1 || inputStream.read(lengthBufferBytes) != lengthBufferBytes.length) { + return false; + } - private boolean processPacketHeader() throws IOException { - // No need to check stream ID value - if (inputStream.read() == -1 || inputStream.read(lengthBufferBytes) != lengthBufferBytes.length) { - return false; - } + int length = lengthBuffer.getShort(0); + if (inputStream.skip(2) != 2) { + return false; + } - int length = lengthBuffer.getShort(0); - if (inputStream.skip(2) != 2) { - return false; - } + int headerLength = inputStream.read(); + if (headerLength == -1 || inputStream.skip(headerLength) != headerLength) { + return false; + } - int headerLength = inputStream.read(); - if (headerLength == -1 || inputStream.skip(headerLength) != headerLength) { - return false; + packetBytesLeft = length - 3 - headerLength; + return packetBytesLeft > 0; } - packetBytesLeft = length - 3 - headerLength; - return packetBytesLeft > 0; - } + @Override + public int read() throws IOException { + if (!makeBytesAvailable()) { + return -1; + } - @Override - public int read() throws IOException { - if (!makeBytesAvailable()) { - return -1; - } + int result = inputStream.read(); + if (result >= 0) { + packetBytesLeft--; + } - int result = inputStream.read(); - if (result >= 0) { - packetBytesLeft--; + return result; } - return result; - } + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + if (!makeBytesAvailable()) { + return -1; + } - @Override - public int read(byte[] buffer, int offset, int length) throws IOException { - if (!makeBytesAvailable()) { - return -1; - } + int chunk = Math.min(packetBytesLeft, length); + int result = inputStream.read(buffer, offset, chunk); + if (result > 0) { + packetBytesLeft -= result; + } - int chunk = Math.min(packetBytesLeft, length); - int result = inputStream.read(buffer, offset, chunk); - if (result > 0) { - packetBytesLeft -= result; + return result; } - return result; - } - - @Override - public int available() throws IOException { - return packetBytesLeft; - } + @Override + public int available() throws IOException { + return packetBytesLeft; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggAudioTrack.java index cb240d04c..f3926de07 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggAudioTrack.java @@ -17,46 +17,46 @@ * Audio track which handles an OGG stream. */ public class OggAudioTrack extends BaseAudioTrack { - private static final Logger log = LoggerFactory.getLogger(OggAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(OggAudioTrack.class); - private final SeekableInputStream inputStream; + private final SeekableInputStream inputStream; - /** - * @param trackInfo Track info - * @param inputStream Input stream for the OGG stream - */ - public OggAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param inputStream Input stream for the OGG stream + */ + public OggAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + super(trackInfo); - this.inputStream = inputStream; - } - - @Override - public void process(final LocalAudioTrackExecutor localExecutor) throws IOException { - OggPacketInputStream packetInputStream = new OggPacketInputStream(inputStream, false); - OggTrackBlueprint blueprint = OggTrackLoader.loadTrackBlueprint(packetInputStream); - - log.debug("Starting to play an OGG track {}", getIdentifier()); + this.inputStream = inputStream; + } - if (blueprint == null) { - throw new IOException("Stream terminated before the first packet."); + @Override + public void process(final LocalAudioTrackExecutor localExecutor) throws IOException { + OggPacketInputStream packetInputStream = new OggPacketInputStream(inputStream, false); + OggTrackBlueprint blueprint = OggTrackLoader.loadTrackBlueprint(packetInputStream); + + log.debug("Starting to play an OGG track {}", getIdentifier()); + + if (blueprint == null) { + throw new IOException("Stream terminated before the first packet."); + } + + OggTrackHandler handler = blueprint.loadTrackHandler(packetInputStream); + localExecutor.executeProcessingLoop(() -> { + try { + processTrackLoop(packetInputStream, localExecutor.getProcessingContext(), handler, blueprint); + } catch (IOException e) { + throw new FriendlyException("Stream broke when playing OGG track.", SUSPICIOUS, e); + } + }, handler::seekToTimecode, true); } - OggTrackHandler handler = blueprint.loadTrackHandler(packetInputStream); - localExecutor.executeProcessingLoop(() -> { - try { - processTrackLoop(packetInputStream, localExecutor.getProcessingContext(), handler, blueprint); - } catch (IOException e) { - throw new FriendlyException("Stream broke when playing OGG track.", SUSPICIOUS, e); - } - }, handler::seekToTimecode, true); - } - - private void processTrackLoop(OggPacketInputStream packetInputStream, AudioProcessingContext context, OggTrackHandler handler, OggTrackBlueprint blueprint) throws IOException, InterruptedException { - while (blueprint != null) { - handler.initialise(context, 0, 0); - handler.provideFrames(); - blueprint = OggTrackLoader.loadTrackBlueprint(packetInputStream); + private void processTrackLoop(OggPacketInputStream packetInputStream, AudioProcessingContext context, OggTrackHandler handler, OggTrackBlueprint blueprint) throws IOException, InterruptedException { + while (blueprint != null) { + handler.initialise(context, 0, 0); + handler.provideFrames(); + blueprint = OggTrackLoader.loadTrackBlueprint(packetInputStream); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggCodecHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggCodecHandler.java index 6af5ef198..77fce2e5a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggCodecHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggCodecHandler.java @@ -5,11 +5,11 @@ import java.io.IOException; public interface OggCodecHandler { - boolean isMatchingIdentifier(int identifier); + boolean isMatchingIdentifier(int identifier); - int getMaximumFirstPacketLength(); + int getMaximumFirstPacketLength(); - OggTrackBlueprint loadBlueprint(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException; + OggTrackBlueprint loadBlueprint(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException; - OggMetadata loadMetadata(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException; + OggMetadata loadMetadata(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggContainerProbe.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggContainerProbe.java index 56c175a85..99630c3a2 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggContainerProbe.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggContainerProbe.java @@ -21,48 +21,48 @@ * Container detection probe for OGG stream. */ public class OggContainerProbe implements MediaContainerProbe { - private static final Logger log = LoggerFactory.getLogger(OggContainerProbe.class); + private static final Logger log = LoggerFactory.getLogger(OggContainerProbe.class); - @Override - public String getName() { - return "ogg"; - } - - @Override - public boolean matchesHints(MediaContainerHints hints) { - return false; - } + @Override + public String getName() { + return "ogg"; + } - @Override - public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream stream) throws IOException { - if (!checkNextBytes(stream, OGG_PAGE_HEADER)) { - return null; + @Override + public boolean matchesHints(MediaContainerHints hints) { + return false; } - log.debug("Track {} is an OGG file.", reference.identifier); + @Override + public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream stream) throws IOException { + if (!checkNextBytes(stream, OGG_PAGE_HEADER)) { + return null; + } - AudioTrackInfoBuilder infoBuilder = AudioTrackInfoBuilder.create(reference, stream); + log.debug("Track {} is an OGG file.", reference.identifier); - try { - collectStreamInformation(stream, infoBuilder); - } catch (Exception e) { - log.warn("Failed to collect additional information on OGG stream.", e); - } + AudioTrackInfoBuilder infoBuilder = AudioTrackInfoBuilder.create(reference, stream); - return supportedFormat(this, null, infoBuilder.build()); - } + try { + collectStreamInformation(stream, infoBuilder); + } catch (Exception e) { + log.warn("Failed to collect additional information on OGG stream.", e); + } - @Override - public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - return new OggAudioTrack(trackInfo, inputStream); - } + return supportedFormat(this, null, infoBuilder.build()); + } + + @Override + public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + return new OggAudioTrack(trackInfo, inputStream); + } - private void collectStreamInformation(SeekableInputStream stream, AudioTrackInfoBuilder infoBuilder) throws IOException { - OggPacketInputStream packetInputStream = new OggPacketInputStream(stream, false); - OggMetadata metadata = OggTrackLoader.loadMetadata(packetInputStream); + private void collectStreamInformation(SeekableInputStream stream, AudioTrackInfoBuilder infoBuilder) throws IOException { + OggPacketInputStream packetInputStream = new OggPacketInputStream(stream, false); + OggMetadata metadata = OggTrackLoader.loadMetadata(packetInputStream); - if (metadata != null) { - infoBuilder.apply(metadata); + if (metadata != null) { + infoBuilder.apply(metadata); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggMetadata.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggMetadata.java index 6dcaaf984..790aa5f46 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggMetadata.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggMetadata.java @@ -10,54 +10,54 @@ * Audio track info provider based on OGG metadata map. */ public class OggMetadata implements AudioTrackInfoProvider { - public static final OggMetadata EMPTY = new OggMetadata(Collections.emptyMap(), Units.DURATION_MS_UNKNOWN); - - private static final String TITLE_FIELD = "TITLE"; - private static final String ARTIST_FIELD = "ARTIST"; - - private final Map tags; - private final long length; - - /** - * @param tags Map of OGG metadata with OGG-specific keys. - */ - public OggMetadata(Map tags, Long length) { - this.tags = tags; - this.length = length; - } - - @Override - public String getTitle() { - return tags.get(TITLE_FIELD); - } - - @Override - public String getAuthor() { - return tags.get(ARTIST_FIELD); - } - - @Override - public Long getLength() { - return length; - } - - @Override - public String getIdentifier() { - return null; - } - - @Override - public String getUri() { - return null; - } - - @Override - public String getArtworkUrl() { - return null; - } - - @Override - public String getISRC() { - return null; - } + public static final OggMetadata EMPTY = new OggMetadata(Collections.emptyMap(), Units.DURATION_MS_UNKNOWN); + + private static final String TITLE_FIELD = "TITLE"; + private static final String ARTIST_FIELD = "ARTIST"; + + private final Map tags; + private final long length; + + /** + * @param tags Map of OGG metadata with OGG-specific keys. + */ + public OggMetadata(Map tags, Long length) { + this.tags = tags; + this.length = length; + } + + @Override + public String getTitle() { + return tags.get(TITLE_FIELD); + } + + @Override + public String getAuthor() { + return tags.get(ARTIST_FIELD); + } + + @Override + public Long getLength() { + return length; + } + + @Override + public String getIdentifier() { + return null; + } + + @Override + public String getUri() { + return null; + } + + @Override + public String getArtworkUrl() { + return null; + } + + @Override + public String getISRC() { + return null; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggPacketInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggPacketInputStream.java index 5c1e42636..f3b8228a3 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggPacketInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggPacketInputStream.java @@ -3,12 +3,7 @@ import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; import com.sedmelluq.discord.lavaplayer.tools.io.StreamTools; -import java.io.DataInput; -import java.io.DataInputStream; -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; +import java.io.*; import java.util.List; import static com.sedmelluq.discord.lavaplayer.container.MediaContainerDetection.checkNextBytes; @@ -19,394 +14,397 @@ * with startNewTrack() when the previous one has ended (startNewPacket() has returned false). */ public class OggPacketInputStream extends InputStream { - static final int[] OGG_PAGE_HEADER = new int[] { 0x4F, 0x67, 0x67, 0x53 }; - - private static final int SHORT_SCAN = 10240; - private static final int LONG_SCAN = 65307; - - private final SeekableInputStream inputStream; - private final boolean closeDelegated; - private final DataInput dataInput; - private final int[] segmentSizes; - - private List seekPoints; - private OggPageHeader pageHeader; - private int bytesLeftInPacket; - private boolean packetContinues; - private int nextPacketSegmentIndex; - private State state; - - /** - * @param inputStream Input stream to read in as OGG packets - * @param closeDelegated Whether closing this stream should close the inputStream as well - */ - public OggPacketInputStream(SeekableInputStream inputStream, boolean closeDelegated) { - this.inputStream = inputStream; - this.closeDelegated = closeDelegated; - this.dataInput = new DataInputStream(inputStream); - this.segmentSizes = new int[256]; - this.state = State.TRACK_BOUNDARY; - } - - public void setSeekPoints(List seekPoints) { - this.seekPoints = seekPoints; - } - - /** - * Load the next track from the stream. This is only valid when the stream is in a track boundary state. - * @return True if next track is present in the stream, false if the stream has terminated. - */ - public boolean startNewTrack() { - if (state == State.TERMINATED) { - return false; - } else if (state != State.TRACK_BOUNDARY) { - throw new IllegalStateException("Cannot load the next track while the previous one has not been consumed."); + static final int[] OGG_PAGE_HEADER = new int[]{0x4F, 0x67, 0x67, 0x53}; + + private static final int SHORT_SCAN = 10240; + private static final int LONG_SCAN = 65307; + + private final SeekableInputStream inputStream; + private final boolean closeDelegated; + private final DataInput dataInput; + private final int[] segmentSizes; + + private List seekPoints; + private OggPageHeader pageHeader; + private int bytesLeftInPacket; + private boolean packetContinues; + private int nextPacketSegmentIndex; + private State state; + + /** + * @param inputStream Input stream to read in as OGG packets + * @param closeDelegated Whether closing this stream should close the inputStream as well + */ + public OggPacketInputStream(SeekableInputStream inputStream, boolean closeDelegated) { + this.inputStream = inputStream; + this.closeDelegated = closeDelegated; + this.dataInput = new DataInputStream(inputStream); + this.segmentSizes = new int[256]; + this.state = State.TRACK_BOUNDARY; } - pageHeader = null; - state = State.PACKET_BOUNDARY; - return true; - } - - /** - * Load the next packet from the stream. This is only valid when the stream is in a packet boundary state. - * @return True if next packet is present in the track. State is PACKET_READ. - * False if the track is finished. State is either TRACK_BOUNDARY or TERMINATED. - * @throws IOException On read error. - */ - public boolean startNewPacket() throws IOException { - if (state == State.TRACK_BOUNDARY) { - return false; - } else if (state == State.TRACK_SEEKING) { - loadNextNonEmptyPage(); - } else if (state != State.PACKET_BOUNDARY) { - throw new IllegalStateException("Cannot start a new packet while the previous one has not been consumed."); + public void setSeekPoints(List seekPoints) { + this.seekPoints = seekPoints; } - if ((pageHeader == null || nextPacketSegmentIndex == pageHeader.segmentCount) && !loadNextNonEmptyPage()) { - return false; - } + /** + * Load the next track from the stream. This is only valid when the stream is in a track boundary state. + * + * @return True if next track is present in the stream, false if the stream has terminated. + */ + public boolean startNewTrack() { + if (state == State.TERMINATED) { + return false; + } else if (state != State.TRACK_BOUNDARY) { + throw new IllegalStateException("Cannot load the next track while the previous one has not been consumed."); + } - if (!initialisePacket()) { - return loadNextNonEmptyPage(); + pageHeader = null; + state = State.PACKET_BOUNDARY; + return true; } - return true; - } + /** + * Load the next packet from the stream. This is only valid when the stream is in a packet boundary state. + * + * @return True if next packet is present in the track. State is PACKET_READ. + * False if the track is finished. State is either TRACK_BOUNDARY or TERMINATED. + * @throws IOException On read error. + */ + public boolean startNewPacket() throws IOException { + if (state == State.TRACK_BOUNDARY) { + return false; + } else if (state == State.TRACK_SEEKING) { + loadNextNonEmptyPage(); + } else if (state != State.PACKET_BOUNDARY) { + throw new IllegalStateException("Cannot start a new packet while the previous one has not been consumed."); + } - public boolean isPacketComplete() { - return state == State.PACKET_READ; - } + if ((pageHeader == null || nextPacketSegmentIndex == pageHeader.segmentCount) && !loadNextNonEmptyPage()) { + return false; + } - private boolean readPageHeader() throws IOException { - if (!checkNextBytes(inputStream, OGG_PAGE_HEADER, false)) { - if (inputStream.read() == -1) { - return false; - } + if (!initialisePacket()) { + return loadNextNonEmptyPage(); + } - throw new IllegalStateException("Stream is not positioned at a page header."); - } else if ((dataInput.readByte() & 0xFF) != 0) { - throw new IllegalStateException("Unknown OGG stream version."); + return true; } - int flags = dataInput.readByte() & 0xFF; - long position = Long.reverseBytes(dataInput.readLong()); - int streamIdentifier = Integer.reverseBytes(dataInput.readInt()); - int pageSequence = Integer.reverseBytes(dataInput.readInt()); - int checksum = Integer.reverseBytes(dataInput.readInt()); - int segmentCount = dataInput.readByte() & 0xFF; - long byteStreamPosition = inputStream.getPosition() - 27; + public boolean isPacketComplete() { + return state == State.PACKET_READ; + } - pageHeader = new OggPageHeader(flags, position, streamIdentifier, pageSequence, checksum, segmentCount, - byteStreamPosition); + private boolean readPageHeader() throws IOException { + if (!checkNextBytes(inputStream, OGG_PAGE_HEADER, false)) { + if (inputStream.read() == -1) { + return false; + } - for (int i = 0; i < segmentCount; i++) { - segmentSizes[i] = dataInput.readByte() & 0xFF; - } + throw new IllegalStateException("Stream is not positioned at a page header."); + } else if ((dataInput.readByte() & 0xFF) != 0) { + throw new IllegalStateException("Unknown OGG stream version."); + } - return true; - } - - /** - * Load pages until a non-empty page is reached. Valid to call in states PACKET_BOUNDARY (page starts a new packet) or - * PACKET_READ (page starts with a continuation). - * - * @return True if a page belonging to the same track was loaded, state is PACKET_READ. - * False if the next page cannot be loaded because the current one ended the track, state is TRACK_BOUNDARY - * or TERMINATED. - * @throws IOException On read error. - */ - private boolean loadNextNonEmptyPage() throws IOException { - do { - if (!loadNextPage()) { - return false; - } - } while (pageHeader.segmentCount == 0); - - return true; - } - - /** - * Load the next page from the stream. Valid to call in states PACKET_BOUNDARY (page starts a new packet) or - * PACKET_READ (page starts with a continuation). - * - * @return True if a page belonging to the same track was loaded, state is PACKET_READ. - * False if the next page cannot be loaded because the current one ended the track, state is TRACK_BOUNDARY - * or TERMINATED. - * @throws IOException On read error. - */ - private boolean loadNextPage() throws IOException { - if (pageHeader != null && pageHeader.isLastPage) { - if (packetContinues) { - throw new IllegalStateException("Track finished in the middle of a packet."); - } - - state = State.TRACK_BOUNDARY; - return false; - } + int flags = dataInput.readByte() & 0xFF; + long position = Long.reverseBytes(dataInput.readLong()); + int streamIdentifier = Integer.reverseBytes(dataInput.readInt()); + int pageSequence = Integer.reverseBytes(dataInput.readInt()); + int checksum = Integer.reverseBytes(dataInput.readInt()); + int segmentCount = dataInput.readByte() & 0xFF; + long byteStreamPosition = inputStream.getPosition() - 27; - if (!readPageHeader()) { - if (packetContinues) { - throw new IllegalStateException("Stream ended in the middle of a packet."); - } - return false; - } + pageHeader = new OggPageHeader(flags, position, streamIdentifier, pageSequence, checksum, segmentCount, + byteStreamPosition); - nextPacketSegmentIndex = 0; - state = State.PACKET_READ; - return true; - } - - /** - * Initialise the (remainder of the) current packet in the stream. This may be called either to initialise a new - * packet or a continuation of the previous one. Call only in state PACKET_READ. - * - * @return Returns false if the remaining size of the packet was zero, state is PACKET_BOUNDARY. - * Returns true if the initialised packet has any bytes in it, state is PACKET_READ. - */ - private boolean initialisePacket() { - while (nextPacketSegmentIndex < pageHeader.segmentCount) { - int size = segmentSizes[nextPacketSegmentIndex++]; - bytesLeftInPacket += size; - - if (size < 255) { - // Anything below 255 is also a packet end marker. - if (bytesLeftInPacket == 0) { - // We reached packet end without getting any additional bytes, set state to packet boundary - state = State.PACKET_BOUNDARY; - return false; + for (int i = 0; i < segmentCount; i++) { + segmentSizes[i] = dataInput.readByte() & 0xFF; } - // We reached packet end and got some more bytes. - packetContinues = false; return true; - } } - // Packet does not end within this page. - packetContinues = true; - return true; - } + /** + * Load pages until a non-empty page is reached. Valid to call in states PACKET_BOUNDARY (page starts a new packet) or + * PACKET_READ (page starts with a continuation). + * + * @return True if a page belonging to the same track was loaded, state is PACKET_READ. + * False if the next page cannot be loaded because the current one ended the track, state is TRACK_BOUNDARY + * or TERMINATED. + * @throws IOException On read error. + */ + private boolean loadNextNonEmptyPage() throws IOException { + do { + if (!loadNextPage()) { + return false; + } + } while (pageHeader.segmentCount == 0); - @Override - public int read() throws IOException { - if (bytesLeftInPacket == 0) { - return -1; + return true; } - int value = inputStream.read(); - if (value == -1) { - return -1; - } + /** + * Load the next page from the stream. Valid to call in states PACKET_BOUNDARY (page starts a new packet) or + * PACKET_READ (page starts with a continuation). + * + * @return True if a page belonging to the same track was loaded, state is PACKET_READ. + * False if the next page cannot be loaded because the current one ended the track, state is TRACK_BOUNDARY + * or TERMINATED. + * @throws IOException On read error. + */ + private boolean loadNextPage() throws IOException { + if (pageHeader != null && pageHeader.isLastPage) { + if (packetContinues) { + throw new IllegalStateException("Track finished in the middle of a packet."); + } + + state = State.TRACK_BOUNDARY; + return false; + } + + if (!readPageHeader()) { + if (packetContinues) { + throw new IllegalStateException("Stream ended in the middle of a packet."); + } + return false; + } - if (--bytesLeftInPacket == 0) { - continuePacket(); + nextPacketSegmentIndex = 0; + state = State.PACKET_READ; + return true; } - return value; - } - - @Override - public int read(byte[] buffer, int initialOffset, int length) throws IOException { - int currentOffset = initialOffset; - int maximumOffset = initialOffset + length; - - // Terminates when we have read as much as we needed - while (currentOffset < maximumOffset) { - // If there is nothing left in the current packet, stream is in EOF state - if (bytesLeftInPacket == 0) { - return -1; - } - - // Limit the read size to the number of bytes that are definitely still left in the packet - int chunk = Math.min(maximumOffset - currentOffset, bytesLeftInPacket); - int read = inputStream.read(buffer, currentOffset, chunk); - - if (read == -1) { - // EOF in the underlying stream before the end of a packet. Throw an exception, the consumer should not need - // to check for partial packets. - throw new EOFException("Underlying stream ended before the end of a packet."); - } - - currentOffset += read; - bytesLeftInPacket -= read; - - if (bytesLeftInPacket == 0) { - // We got everything from our chunk of size min(leftInPacket, requested) and also exhausted the bytes that we - // know the packet had left. Check if the packet continues so we could continue fetching from the same packet. - // Otherwise, bugger out. - - if (!continuePacket()) { - break; + /** + * Initialise the (remainder of the) current packet in the stream. This may be called either to initialise a new + * packet or a continuation of the previous one. Call only in state PACKET_READ. + * + * @return Returns false if the remaining size of the packet was zero, state is PACKET_BOUNDARY. + * Returns true if the initialised packet has any bytes in it, state is PACKET_READ. + */ + private boolean initialisePacket() { + while (nextPacketSegmentIndex < pageHeader.segmentCount) { + int size = segmentSizes[nextPacketSegmentIndex++]; + bytesLeftInPacket += size; + + if (size < 255) { + // Anything below 255 is also a packet end marker. + if (bytesLeftInPacket == 0) { + // We reached packet end without getting any additional bytes, set state to packet boundary + state = State.PACKET_BOUNDARY; + return false; + } + + // We reached packet end and got some more bytes. + packetContinues = false; + return true; + } } - } else if (read < chunk) { - // The underlying stream cannot provide more right now. Let it rest. - return currentOffset - initialOffset; - } + + // Packet does not end within this page. + packetContinues = true; + return true; } - return currentOffset - initialOffset; - } + @Override + public int read() throws IOException { + if (bytesLeftInPacket == 0) { + return -1; + } - @Override - public int available() throws IOException { - if (state != State.PACKET_READ) { - return 0; - } + int value = inputStream.read(); + if (value == -1) { + return -1; + } - return Math.min(inputStream.available(), bytesLeftInPacket); - } + if (--bytesLeftInPacket == 0) { + continuePacket(); + } - @Override - public void close() throws IOException { - if (closeDelegated) { - inputStream.close(); + return value; } - } - - /** - * Seeks the stream to the specified timecode. - * @param timecode Timecode in milliseconds to seek to. - * @return The actual timecode in milliseconds to which the stream was seeked. - * @throws IOException On read error. - */ - public long seek(long timecode) throws IOException { - if (seekPoints == null) { - throw new IllegalStateException("Seek points have not been set."); + + @Override + public int read(byte[] buffer, int initialOffset, int length) throws IOException { + int currentOffset = initialOffset; + int maximumOffset = initialOffset + length; + + // Terminates when we have read as much as we needed + while (currentOffset < maximumOffset) { + // If there is nothing left in the current packet, stream is in EOF state + if (bytesLeftInPacket == 0) { + return -1; + } + + // Limit the read size to the number of bytes that are definitely still left in the packet + int chunk = Math.min(maximumOffset - currentOffset, bytesLeftInPacket); + int read = inputStream.read(buffer, currentOffset, chunk); + + if (read == -1) { + // EOF in the underlying stream before the end of a packet. Throw an exception, the consumer should not need + // to check for partial packets. + throw new EOFException("Underlying stream ended before the end of a packet."); + } + + currentOffset += read; + bytesLeftInPacket -= read; + + if (bytesLeftInPacket == 0) { + // We got everything from our chunk of size min(leftInPacket, requested) and also exhausted the bytes that we + // know the packet had left. Check if the packet continues so we could continue fetching from the same packet. + // Otherwise, bugger out. + + if (!continuePacket()) { + break; + } + } else if (read < chunk) { + // The underlying stream cannot provide more right now. Let it rest. + return currentOffset - initialOffset; + } + } + + return currentOffset - initialOffset; } - // Binary search for the seek point with the largest timecode that is smaller than or equal to the target timecode - int low = 0; - int mid = 0; - int high = seekPoints.size() - 1; - while (low <= high) { - mid = (low + high) / 2; - if (seekPoints.get(mid).getTimecode() <= timecode) { - low = mid + 1; - } else { - high = mid - 1; - } + @Override + public int available() throws IOException { + if (state != State.PACKET_READ) { + return 0; + } + + return Math.min(inputStream.available(), bytesLeftInPacket); } - if (mid == 0) { - mid++; + @Override + public void close() throws IOException { + if (closeDelegated) { + inputStream.close(); + } } - OggSeekPoint seekPoint = seekPoints.get(mid); - inputStream.seek(seekPoint.getPosition()); - state = State.TRACK_SEEKING; + /** + * Seeks the stream to the specified timecode. + * + * @param timecode Timecode in milliseconds to seek to. + * @return The actual timecode in milliseconds to which the stream was seeked. + * @throws IOException On read error. + */ + public long seek(long timecode) throws IOException { + if (seekPoints == null) { + throw new IllegalStateException("Seek points have not been set."); + } + + // Binary search for the seek point with the largest timecode that is smaller than or equal to the target timecode + int low = 0; + int mid = 0; + int high = seekPoints.size() - 1; + while (low <= high) { + mid = (low + high) / 2; + if (seekPoints.get(mid).getTimecode() <= timecode) { + low = mid + 1; + } else { + high = mid - 1; + } + } - return seekPoint.getTimecode(); - } + if (mid == 0) { + mid++; + } - public List createSeekTable(int sampleRate) throws IOException { - if (!inputStream.canSeekHard()) { - return null; + OggSeekPoint seekPoint = seekPoints.get(mid); + inputStream.seek(seekPoint.getPosition()); + state = State.TRACK_SEEKING; + + return seekPoint.getTimecode(); } - long savedPosition = inputStream.getPosition(); + public List createSeekTable(int sampleRate) throws IOException { + if (!inputStream.canSeekHard()) { + return null; + } - long absoluteOffset = pageHeader.byteStreamPosition; - inputStream.seek(absoluteOffset); + long savedPosition = inputStream.getPosition(); - byte[] data = new byte[(int) inputStream.getContentLength()]; - int dataLength = StreamTools.readUntilEnd(inputStream, data, 0, data.length); + long absoluteOffset = pageHeader.byteStreamPosition; + inputStream.seek(absoluteOffset); - List seekPoints = new OggPageScanner(absoluteOffset, data, dataLength).createSeekTable(sampleRate); + byte[] data = new byte[(int) inputStream.getContentLength()]; + int dataLength = StreamTools.readUntilEnd(inputStream, data, 0, data.length); - inputStream.seek(savedPosition); - return seekPoints; - } + List seekPoints = new OggPageScanner(absoluteOffset, data, dataLength).createSeekTable(sampleRate); - /** - * If it is possible to seek backwards on this stream, and the length of the stream is known, seeks to the end of the - * track to determine the stream length both in bytes and samples. - * - * @param sampleRate Sample rate of the track in this stream. - * @return OGG stream size information. - * @throws IOException On read error. - */ - public OggStreamSizeInfo seekForSizeInfo(int sampleRate) throws IOException { - if (!inputStream.canSeekHard()) { - return null; + inputStream.seek(savedPosition); + return seekPoints; } - long savedPosition = inputStream.getPosition(); + /** + * If it is possible to seek backwards on this stream, and the length of the stream is known, seeks to the end of the + * track to determine the stream length both in bytes and samples. + * + * @param sampleRate Sample rate of the track in this stream. + * @return OGG stream size information. + * @throws IOException On read error. + */ + public OggStreamSizeInfo seekForSizeInfo(int sampleRate) throws IOException { + if (!inputStream.canSeekHard()) { + return null; + } - OggStreamSizeInfo sizeInfo = scanForSizeInfo(SHORT_SCAN, sampleRate); + long savedPosition = inputStream.getPosition(); - if (sizeInfo == null) { - sizeInfo = scanForSizeInfo(LONG_SCAN, sampleRate); - } + OggStreamSizeInfo sizeInfo = scanForSizeInfo(SHORT_SCAN, sampleRate); - inputStream.seek(savedPosition); - return sizeInfo; - } + if (sizeInfo == null) { + sizeInfo = scanForSizeInfo(LONG_SCAN, sampleRate); + } - private OggStreamSizeInfo scanForSizeInfo(int tailLength, int sampleRate) throws IOException { - if (pageHeader == null) { - return null; + inputStream.seek(savedPosition); + return sizeInfo; } - long absoluteOffset = Math.max(pageHeader.byteStreamPosition, inputStream.getContentLength() - tailLength); - inputStream.seek(absoluteOffset); - - byte[] data = new byte[tailLength]; - int dataLength = StreamTools.readUntilEnd(inputStream, data, 0, data.length); - - return new OggPageScanner(absoluteOffset, data, dataLength) - .scanForSizeInfo(pageHeader.byteStreamPosition, sampleRate); - } - - /** - * Process request for more bytes for the packet. Call only when the state is PACKET_READ. - * - * @return Returns false if no more bytes for the packet are available, state is PACKET_BOUNDARY. - * Returns true if more bytes were fetched for this packet, state is PACKET_READ. - * @throws IOException On read error. - */ - private boolean continuePacket() throws IOException { - if (!packetContinues) { - // We have reached the end of the packet. - state = State.PACKET_BOUNDARY; - return false; - } + private OggStreamSizeInfo scanForSizeInfo(int tailLength, int sampleRate) throws IOException { + if (pageHeader == null) { + return null; + } - // Load more segments for this packet from the next page. - if (!loadNextNonEmptyPage()) { - throw new IllegalStateException("Track or stream end reached within an incomplete packet."); - } else if (!initialisePacket()) { - return false; + long absoluteOffset = Math.max(pageHeader.byteStreamPosition, inputStream.getContentLength() - tailLength); + inputStream.seek(absoluteOffset); + + byte[] data = new byte[tailLength]; + int dataLength = StreamTools.readUntilEnd(inputStream, data, 0, data.length); + + return new OggPageScanner(absoluteOffset, data, dataLength) + .scanForSizeInfo(pageHeader.byteStreamPosition, sampleRate); } - return true; - } + /** + * Process request for more bytes for the packet. Call only when the state is PACKET_READ. + * + * @return Returns false if no more bytes for the packet are available, state is PACKET_BOUNDARY. + * Returns true if more bytes were fetched for this packet, state is PACKET_READ. + * @throws IOException On read error. + */ + private boolean continuePacket() throws IOException { + if (!packetContinues) { + // We have reached the end of the packet. + state = State.PACKET_BOUNDARY; + return false; + } - private enum State { - TRACK_SEEKING, - TRACK_BOUNDARY, - PACKET_BOUNDARY, - PACKET_READ, - TERMINATED - } + // Load more segments for this packet from the next page. + if (!loadNextNonEmptyPage()) { + throw new IllegalStateException("Track or stream end reached within an incomplete packet."); + } else if (!initialisePacket()) { + return false; + } + + return true; + } + + private enum State { + TRACK_SEEKING, + TRACK_BOUNDARY, + PACKET_BOUNDARY, + PACKET_READ, + TERMINATED + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggPageHeader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggPageHeader.java index 61edd1c95..22bc091f2 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggPageHeader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggPageHeader.java @@ -4,67 +4,67 @@ * Header of an OGG stream page. */ public class OggPageHeader { - public static final int FLAG_CONTINUATION = 0x01; - public static final int FLAG_FIRST_PAGE = 0x02; - public static final int FLAG_LAST_PAGE = 0x04; + public static final int FLAG_CONTINUATION = 0x01; + public static final int FLAG_FIRST_PAGE = 0x02; + public static final int FLAG_LAST_PAGE = 0x04; - /** - * If this page starts in the middle of a packet that was left incomplete in the previous page. - */ - public final boolean isContinuation; - /** - * If this is the first page of the track. - */ - public final boolean isFirstPage; - /** - * If this is the last page of the track. - */ - public final boolean isLastPage; - /** - * The absolute position in the number of samples of this packet relative to the track start. - */ - public final long absolutePosition; - /** - * Unique identifier of this track in the stream. - */ - public final int streamIdentifier; - /** - * The index of the page within a track. - */ - public final int pageSequence; - /** - * The checksum of the page. - */ - public final int pageChecksum; - /** - * Number of segments in the page. - */ - public final int segmentCount; - /** - * The absolute position of the start of this page in the underlying bytestream. - */ - public final long byteStreamPosition; + /** + * If this page starts in the middle of a packet that was left incomplete in the previous page. + */ + public final boolean isContinuation; + /** + * If this is the first page of the track. + */ + public final boolean isFirstPage; + /** + * If this is the last page of the track. + */ + public final boolean isLastPage; + /** + * The absolute position in the number of samples of this packet relative to the track start. + */ + public final long absolutePosition; + /** + * Unique identifier of this track in the stream. + */ + public final int streamIdentifier; + /** + * The index of the page within a track. + */ + public final int pageSequence; + /** + * The checksum of the page. + */ + public final int pageChecksum; + /** + * Number of segments in the page. + */ + public final int segmentCount; + /** + * The absolute position of the start of this page in the underlying bytestream. + */ + public final long byteStreamPosition; - /** - * @param flags Page flags. - * @param absolutePosition The absolute position in the number of samples of this packet relative to the track start. - * @param streamIdentifier Unique identifier of this track in the stream. - * @param pageSequence The index of the page within a track. - * @param checksum The checksum of the page. - * @param segmentCount Number of segments in the page. - * @param byteStreamPosition The absolute position in bytes of this page in the stream. - */ - public OggPageHeader(int flags, long absolutePosition, int streamIdentifier, int pageSequence, int checksum, - int segmentCount, long byteStreamPosition) { + /** + * @param flags Page flags. + * @param absolutePosition The absolute position in the number of samples of this packet relative to the track start. + * @param streamIdentifier Unique identifier of this track in the stream. + * @param pageSequence The index of the page within a track. + * @param checksum The checksum of the page. + * @param segmentCount Number of segments in the page. + * @param byteStreamPosition The absolute position in bytes of this page in the stream. + */ + public OggPageHeader(int flags, long absolutePosition, int streamIdentifier, int pageSequence, int checksum, + int segmentCount, long byteStreamPosition) { - this.isContinuation = (flags & FLAG_CONTINUATION) != 0; - this.isFirstPage = (flags & FLAG_FIRST_PAGE) != 0; - this.isLastPage = (flags & FLAG_LAST_PAGE) != 0; - this.absolutePosition = absolutePosition; - this.streamIdentifier = streamIdentifier; - this.pageSequence = pageSequence; - this.pageChecksum = checksum; - this.segmentCount = segmentCount; - this.byteStreamPosition = byteStreamPosition; - } + this.isContinuation = (flags & FLAG_CONTINUATION) != 0; + this.isFirstPage = (flags & FLAG_FIRST_PAGE) != 0; + this.isLastPage = (flags & FLAG_LAST_PAGE) != 0; + this.absolutePosition = absolutePosition; + this.streamIdentifier = streamIdentifier; + this.pageSequence = pageSequence; + this.pageChecksum = checksum; + this.segmentCount = segmentCount; + this.byteStreamPosition = byteStreamPosition; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggPageScanner.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggPageScanner.java index 01e0769a9..1326a925c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggPageScanner.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggPageScanner.java @@ -8,126 +8,126 @@ * Scanner for determining OGG stream information by seeking around in it. */ public class OggPageScanner { - private static final int OGG_PAGE_HEADER_INT = ByteBuffer.wrap(new byte[] { 0x4F, 0x67, 0x67, 0x53 }).getInt(0); - - private final long absoluteOffset; - private final byte[] data; - private final int dataLength; - - private int flags; - private long reversedPosition; - private int pageSize; - private long byteStreamPosition; - private int pageSequence; - - /** - * @param absoluteOffset Current position of the stream in bytes. - * @param data Byte array with data starting at that position. - * @param dataLength Length of data. - */ - public OggPageScanner(long absoluteOffset, byte[] data, int dataLength) { - this.absoluteOffset = absoluteOffset; - this.data = data; - this.dataLength = dataLength; - } - - /** - * @param firstPageOffset Absolute position of the first page in the stream. - * @param sampleRate Sample rate of the track in the stream. - * @return If the data contains the header of the last page in the OGG stream, then stream size information, - * otherwise null. - */ - public OggStreamSizeInfo scanForSizeInfo(long firstPageOffset, int sampleRate) { - ByteBuffer buffer = ByteBuffer.wrap(data, 0, dataLength); - int head = buffer.getInt(0); - - for (int i = 0; i < dataLength - 27; i++) { - if (head == OGG_PAGE_HEADER_INT) { - buffer.position(i); - - if (attemptReadHeader(buffer)) { - do { - if ((flags & OggPageHeader.FLAG_LAST_PAGE) != 0) { - return new OggStreamSizeInfo((byteStreamPosition - firstPageOffset) + pageSize, - Long.reverseBytes(reversedPosition), firstPageOffset, byteStreamPosition, sampleRate); + private static final int OGG_PAGE_HEADER_INT = ByteBuffer.wrap(new byte[]{0x4F, 0x67, 0x67, 0x53}).getInt(0); + + private final long absoluteOffset; + private final byte[] data; + private final int dataLength; + + private int flags; + private long reversedPosition; + private int pageSize; + private long byteStreamPosition; + private int pageSequence; + + /** + * @param absoluteOffset Current position of the stream in bytes. + * @param data Byte array with data starting at that position. + * @param dataLength Length of data. + */ + public OggPageScanner(long absoluteOffset, byte[] data, int dataLength) { + this.absoluteOffset = absoluteOffset; + this.data = data; + this.dataLength = dataLength; + } + + /** + * @param firstPageOffset Absolute position of the first page in the stream. + * @param sampleRate Sample rate of the track in the stream. + * @return If the data contains the header of the last page in the OGG stream, then stream size information, + * otherwise null. + */ + public OggStreamSizeInfo scanForSizeInfo(long firstPageOffset, int sampleRate) { + ByteBuffer buffer = ByteBuffer.wrap(data, 0, dataLength); + int head = buffer.getInt(0); + + for (int i = 0; i < dataLength - 27; i++) { + if (head == OGG_PAGE_HEADER_INT) { + buffer.position(i); + + if (attemptReadHeader(buffer)) { + do { + if ((flags & OggPageHeader.FLAG_LAST_PAGE) != 0) { + return new OggStreamSizeInfo((byteStreamPosition - firstPageOffset) + pageSize, + Long.reverseBytes(reversedPosition), firstPageOffset, byteStreamPosition, sampleRate); + } + } while (attemptReadHeader(buffer)); + } } - } while (attemptReadHeader(buffer)); + + head <<= 8; + head |= data[i + 4] & 0xFF; } - } - head <<= 8; - head |= data[i + 4] & 0xFF; + return null; } - return null; - } - - /** - * Creates a seek table for the OGG stream. - * - * @param sampleRate Sample rate of the track in the stream. - * @return A list of OggSeekPoint objects representing the seek points in the stream. - */ - public List createSeekTable(int sampleRate) { - List seekPoints = new ArrayList<>(); - - ByteBuffer buffer = ByteBuffer.wrap(data, 0, dataLength); - int head = buffer.getInt(0); - - for (int i = 0; i < dataLength - 27; i++) { - if (head == OGG_PAGE_HEADER_INT) { - buffer.position(i); - - if (attemptReadHeader(buffer)) { - long position = byteStreamPosition; - long granulePosition = Long.reverseBytes(reversedPosition); - long timecode = granulePosition / (sampleRate / 1000); - pageSequence++; - seekPoints.add(new OggSeekPoint(position, granulePosition, timecode, pageSequence)); + /** + * Creates a seek table for the OGG stream. + * + * @param sampleRate Sample rate of the track in the stream. + * @return A list of OggSeekPoint objects representing the seek points in the stream. + */ + public List createSeekTable(int sampleRate) { + List seekPoints = new ArrayList<>(); + + ByteBuffer buffer = ByteBuffer.wrap(data, 0, dataLength); + int head = buffer.getInt(0); + + for (int i = 0; i < dataLength - 27; i++) { + if (head == OGG_PAGE_HEADER_INT) { + buffer.position(i); + + if (attemptReadHeader(buffer)) { + long position = byteStreamPosition; + long granulePosition = Long.reverseBytes(reversedPosition); + long timecode = granulePosition / (sampleRate / 1000); + pageSequence++; + seekPoints.add(new OggSeekPoint(position, granulePosition, timecode, pageSequence)); + } + } + + head <<= 8; + head |= data[i + 4] & 0xFF; } - } - head <<= 8; - head |= data[i + 4] & 0xFF; + return seekPoints; } - return seekPoints; - } - - private boolean attemptReadHeader(ByteBuffer buffer) { - int start = buffer.position(); + private boolean attemptReadHeader(ByteBuffer buffer) { + int start = buffer.position(); - if (buffer.limit() < start + 27) { - return false; - } else if (buffer.getInt(start) != OGG_PAGE_HEADER_INT) { - return false; - } else if (buffer.get(start + 4) != 0) { - return false; - } + if (buffer.limit() < start + 27) { + return false; + } else if (buffer.getInt(start) != OGG_PAGE_HEADER_INT) { + return false; + } else if (buffer.get(start + 4) != 0) { + return false; + } - int segmentCount = buffer.get(start + 26) & 0xFF; - int minimumCapacity = start + segmentCount + 27; + int segmentCount = buffer.get(start + 26) & 0xFF; + int minimumCapacity = start + segmentCount + 27; - if (buffer.limit() < minimumCapacity) { - return false; - } + if (buffer.limit() < minimumCapacity) { + return false; + } - int segmentBase = start + 27; + int segmentBase = start + 27; - for (int i = 0; i < segmentCount; i++) { - minimumCapacity += buffer.get(segmentBase + i) & 0xFF; - } + for (int i = 0; i < segmentCount; i++) { + minimumCapacity += buffer.get(segmentBase + i) & 0xFF; + } - if (buffer.limit() < minimumCapacity) { - return false; - } + if (buffer.limit() < minimumCapacity) { + return false; + } - flags = buffer.get(start + 5) & 0xFF; - reversedPosition = buffer.getLong(start + 6); - byteStreamPosition = absoluteOffset + start; - pageSize = minimumCapacity; + flags = buffer.get(start + 5) & 0xFF; + reversedPosition = buffer.getLong(start + 6); + byteStreamPosition = absoluteOffset + start; + pageSize = minimumCapacity; - buffer.position(minimumCapacity); - return true; - } + buffer.position(minimumCapacity); + return true; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggSeekPoint.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggSeekPoint.java index 80f1a3af9..464c9ac54 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggSeekPoint.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggSeekPoint.java @@ -1,49 +1,49 @@ package com.sedmelluq.discord.lavaplayer.container.ogg; public class OggSeekPoint { - private final long position; - private final long granulePosition; - private final long timecode; - private final long pageSequence; + private final long position; + private final long granulePosition; + private final long timecode; + private final long pageSequence; - /** - * @param position The position of the seek point in the stream, in bytes. - * @param granulePosition The granule position of the seek point in the stream. - * @param timecode The time of the seek point in the stream, in milliseconds. - * @param pageSequence The page to what this seek point belong. - */ - public OggSeekPoint(long position, long granulePosition, long timecode, long pageSequence) { - this.position = position; - this.granulePosition = granulePosition; - this.timecode = timecode; - this.pageSequence = pageSequence; - } + /** + * @param position The position of the seek point in the stream, in bytes. + * @param granulePosition The granule position of the seek point in the stream. + * @param timecode The time of the seek point in the stream, in milliseconds. + * @param pageSequence The page to what this seek point belong. + */ + public OggSeekPoint(long position, long granulePosition, long timecode, long pageSequence) { + this.position = position; + this.granulePosition = granulePosition; + this.timecode = timecode; + this.pageSequence = pageSequence; + } - /** - * @return The position of the seek point in the stream, in bytes. - */ - public long getPosition() { - return position; - } + /** + * @return The position of the seek point in the stream, in bytes. + */ + public long getPosition() { + return position; + } - /** - * @return The granule position of the seek point in the stream. - */ - public long getGranulePosition() { - return granulePosition; - } + /** + * @return The granule position of the seek point in the stream. + */ + public long getGranulePosition() { + return granulePosition; + } - /** - * @return The timecode of the seek point in the stream, in milliseconds. - */ - public long getTimecode() { - return timecode; - } + /** + * @return The timecode of the seek point in the stream, in milliseconds. + */ + public long getTimecode() { + return timecode; + } - /** - * @return The page to what this seek point belong. - */ - public long getPageSequence() { - return pageSequence; - } + /** + * @return The page to what this seek point belong. + */ + public long getPageSequence() { + return pageSequence; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggStreamSizeInfo.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggStreamSizeInfo.java index 333a915ce..817dffc7e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggStreamSizeInfo.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggStreamSizeInfo.java @@ -26,11 +26,11 @@ public class OggStreamSizeInfo { public final int sampleRate; /** - * @param totalBytes See {@link #totalBytes}. - * @param totalSamples See {@link #totalSamples}. + * @param totalBytes See {@link #totalBytes}. + * @param totalSamples See {@link #totalSamples}. * @param firstPageOffset See {@link #firstPageOffset}. - * @param lastPageOffset See {@link #lastPageOffset}. - * @param sampleRate See {@link #sampleRate}. + * @param lastPageOffset See {@link #lastPageOffset}. + * @param sampleRate See {@link #sampleRate}. */ public OggStreamSizeInfo(long totalBytes, long totalSamples, long firstPageOffset, long lastPageOffset, int sampleRate) { diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggTrackBlueprint.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggTrackBlueprint.java index ba6b37354..1b57b75c3 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggTrackBlueprint.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggTrackBlueprint.java @@ -2,5 +2,6 @@ public interface OggTrackBlueprint { OggTrackHandler loadTrackHandler(OggPacketInputStream stream); + int getSampleRate(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggTrackHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggTrackHandler.java index ea24e5472..a95ccfb50 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggTrackHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggTrackHandler.java @@ -1,6 +1,7 @@ package com.sedmelluq.discord.lavaplayer.container.ogg; import com.sedmelluq.discord.lavaplayer.track.playback.AudioProcessingContext; + import java.io.Closeable; import java.io.IOException; @@ -8,25 +9,25 @@ * A handler for a specific codec for an OGG stream. */ public interface OggTrackHandler extends Closeable { - /** - * Initialises the track stream. - * - * @param context Configuration and output information for processing - * @throws IOException On read error. - */ - void initialise(AudioProcessingContext context, long timecode, long desiredTimecode) throws IOException; + /** + * Initialises the track stream. + * + * @param context Configuration and output information for processing + * @throws IOException On read error. + */ + void initialise(AudioProcessingContext context, long timecode, long desiredTimecode) throws IOException; - /** - * Decodes audio frames and sends them to frame consumer. - * - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void provideFrames() throws InterruptedException; + /** + * Decodes audio frames and sends them to frame consumer. + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void provideFrames() throws InterruptedException; - /** - * Seeks to the specified timecode. - * - * @param timecode The timecode in milliseconds - */ - void seekToTimecode(long timecode); + /** + * Seeks to the specified timecode. + * + * @param timecode The timecode in milliseconds + */ + void seekToTimecode(long timecode); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggTrackLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggTrackLoader.java index b7e26c52f..a2ef9d22b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggTrackLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/OggTrackLoader.java @@ -12,61 +12,61 @@ * Track loader for an OGG packet stream. Automatically detects the track codec and loads the specific track handler. */ public class OggTrackLoader { - private static final OggCodecHandler[] TRACK_PROVIDERS = new OggCodecHandler[] { - new OggOpusCodecHandler(), - new OggFlacCodecHandler(), - new OggVorbisCodecHandler() - }; + private static final OggCodecHandler[] TRACK_PROVIDERS = new OggCodecHandler[]{ + new OggOpusCodecHandler(), + new OggFlacCodecHandler(), + new OggVorbisCodecHandler() + }; - private static final int MAXIMUM_FIRST_PACKET_LENGTH = Stream.of(TRACK_PROVIDERS) - .mapToInt(OggCodecHandler::getMaximumFirstPacketLength).max().getAsInt(); + private static final int MAXIMUM_FIRST_PACKET_LENGTH = Stream.of(TRACK_PROVIDERS) + .mapToInt(OggCodecHandler::getMaximumFirstPacketLength).max().getAsInt(); - /** - * @param packetInputStream OGG packet input stream - * @return The track handler detected from this packet input stream. Returns null if the stream ended. - * @throws IOException On read error - * @throws IllegalStateException If the track uses an unknown codec. - */ - public static OggTrackBlueprint loadTrackBlueprint(OggPacketInputStream packetInputStream) throws IOException { - CodecDetection result = detectCodec(packetInputStream); - return result != null ? result.provider.loadBlueprint(packetInputStream, result.broker) : null; - } - - public static OggMetadata loadMetadata(OggPacketInputStream packetInputStream) throws IOException { - CodecDetection result = detectCodec(packetInputStream); - return result != null ? result.provider.loadMetadata(packetInputStream, result.broker) : null; - } + /** + * @param packetInputStream OGG packet input stream + * @return The track handler detected from this packet input stream. Returns null if the stream ended. + * @throws IOException On read error + * @throws IllegalStateException If the track uses an unknown codec. + */ + public static OggTrackBlueprint loadTrackBlueprint(OggPacketInputStream packetInputStream) throws IOException { + CodecDetection result = detectCodec(packetInputStream); + return result != null ? result.provider.loadBlueprint(packetInputStream, result.broker) : null; + } - private static CodecDetection detectCodec(OggPacketInputStream stream) throws IOException { - if (!stream.startNewTrack() || !stream.startNewPacket()) { - return null; + public static OggMetadata loadMetadata(OggPacketInputStream packetInputStream) throws IOException { + CodecDetection result = detectCodec(packetInputStream); + return result != null ? result.provider.loadMetadata(packetInputStream, result.broker) : null; } - DirectBufferStreamBroker broker = new DirectBufferStreamBroker(1024); - int maximumLength = MAXIMUM_FIRST_PACKET_LENGTH + 1; + private static CodecDetection detectCodec(OggPacketInputStream stream) throws IOException { + if (!stream.startNewTrack() || !stream.startNewPacket()) { + return null; + } - if (!broker.consumeNext(stream, maximumLength, maximumLength)) { - throw new IOException("First packet is too large for any known OGG codec."); - } + DirectBufferStreamBroker broker = new DirectBufferStreamBroker(1024); + int maximumLength = MAXIMUM_FIRST_PACKET_LENGTH + 1; - int headerIdentifier = broker.getBuffer().getInt(); + if (!broker.consumeNext(stream, maximumLength, maximumLength)) { + throw new IOException("First packet is too large for any known OGG codec."); + } - for (OggCodecHandler trackProvider : TRACK_PROVIDERS) { - if (trackProvider.isMatchingIdentifier(headerIdentifier)) { - return new CodecDetection(trackProvider, broker); - } - } + int headerIdentifier = broker.getBuffer().getInt(); + + for (OggCodecHandler trackProvider : TRACK_PROVIDERS) { + if (trackProvider.isMatchingIdentifier(headerIdentifier)) { + return new CodecDetection(trackProvider, broker); + } + } - throw new IllegalStateException("Unsupported track in OGG stream."); - } + throw new IllegalStateException("Unsupported track in OGG stream."); + } - private static class CodecDetection { - private final OggCodecHandler provider; - private final DirectBufferStreamBroker broker; + private static class CodecDetection { + private final OggCodecHandler provider; + private final DirectBufferStreamBroker broker; - private CodecDetection(OggCodecHandler provider, DirectBufferStreamBroker broker) { - this.provider = provider; - this.broker = broker; + private CodecDetection(OggCodecHandler provider, DirectBufferStreamBroker broker) { + this.provider = provider; + this.broker = broker; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/flac/OggFlacCodecHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/flac/OggFlacCodecHandler.java index 427370353..6f37c3257 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/flac/OggFlacCodecHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/flac/OggFlacCodecHandler.java @@ -1,16 +1,7 @@ package com.sedmelluq.discord.lavaplayer.container.ogg.flac; -import com.sedmelluq.discord.lavaplayer.container.flac.FlacMetadataHeader; -import com.sedmelluq.discord.lavaplayer.container.flac.FlacMetadataReader; -import com.sedmelluq.discord.lavaplayer.container.flac.FlacStreamInfo; -import com.sedmelluq.discord.lavaplayer.container.flac.FlacTrackInfo; -import com.sedmelluq.discord.lavaplayer.container.flac.FlacTrackInfoBuilder; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggCodecHandler; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggMetadata; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggPacketInputStream; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggStreamSizeInfo; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggTrackBlueprint; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggTrackHandler; +import com.sedmelluq.discord.lavaplayer.container.flac.*; +import com.sedmelluq.discord.lavaplayer.container.ogg.*; import com.sedmelluq.discord.lavaplayer.tools.io.ByteBufferInputStream; import com.sedmelluq.discord.lavaplayer.tools.io.DirectBufferStreamBroker; @@ -22,92 +13,92 @@ * Loader for an OGG FLAC track from an OGG packet stream. */ public class OggFlacCodecHandler implements OggCodecHandler { - private static final int FLAC_IDENTIFIER = ByteBuffer.wrap(new byte[] { 0x7F, 'F', 'L', 'A' }).getInt(); - - private static final int NATIVE_FLAC_HEADER_OFFSET = 9; - private static final int NATIVE_FLAC_HEADER = ByteBuffer.wrap(new byte[] { 'f', 'L', 'a', 'C' }).getInt(); - - @Override - public boolean isMatchingIdentifier(int identifier) { - return identifier == FLAC_IDENTIFIER; - } - - @Override - public int getMaximumFirstPacketLength() { - return NATIVE_FLAC_HEADER_OFFSET + 4 + FlacMetadataHeader.LENGTH + FlacStreamInfo.LENGTH; - } - - @Override - public OggTrackBlueprint loadBlueprint(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { - FlacTrackInfo info = load(stream, broker); - stream.setSeekPoints(stream.createSeekTable(info.stream.sampleRate)); - return new Blueprint(info); - } - - @Override - public OggMetadata loadMetadata(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { - FlacTrackInfo info = load(stream, broker); - return new OggMetadata(info.tags, detectLength(info, stream)); - } - - private FlacTrackInfo load(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { - ByteBuffer buffer = broker.getBuffer(); - - if (buffer.getInt(NATIVE_FLAC_HEADER_OFFSET) != NATIVE_FLAC_HEADER) { - throw new IllegalStateException("Native flac header not found."); - } - - buffer.position(NATIVE_FLAC_HEADER_OFFSET + 4); + private static final int FLAC_IDENTIFIER = ByteBuffer.wrap(new byte[]{0x7F, 'F', 'L', 'A'}).getInt(); - return readHeaders(buffer, stream); - } + private static final int NATIVE_FLAC_HEADER_OFFSET = 9; + private static final int NATIVE_FLAC_HEADER = ByteBuffer.wrap(new byte[]{'f', 'L', 'a', 'C'}).getInt(); - private Long detectLength(FlacTrackInfo info, OggPacketInputStream stream) throws IOException { - OggStreamSizeInfo sizeInfo; + @Override + public boolean isMatchingIdentifier(int identifier) { + return identifier == FLAC_IDENTIFIER; + } - if (info.stream.sampleCount > 0) { - sizeInfo = new OggStreamSizeInfo(0, info.stream.sampleCount, 0, 0, info.stream.sampleRate); - } else { - sizeInfo = stream.seekForSizeInfo(info.stream.sampleRate); + @Override + public int getMaximumFirstPacketLength() { + return NATIVE_FLAC_HEADER_OFFSET + 4 + FlacMetadataHeader.LENGTH + FlacStreamInfo.LENGTH; } - return sizeInfo != null ? sizeInfo.getDuration() : null; - } + @Override + public OggTrackBlueprint loadBlueprint(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { + FlacTrackInfo info = load(stream, broker); + stream.setSeekPoints(stream.createSeekTable(info.stream.sampleRate)); + return new Blueprint(info); + } - private FlacTrackInfo readHeaders(ByteBuffer firstPacketBuffer, OggPacketInputStream packetInputStream) throws IOException { - FlacStreamInfo streamInfo = FlacMetadataReader.readStreamInfoBlock(new DataInputStream(new ByteBufferInputStream(firstPacketBuffer))); - FlacTrackInfoBuilder trackInfoBuilder = new FlacTrackInfoBuilder(streamInfo); + @Override + public OggMetadata loadMetadata(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { + FlacTrackInfo info = load(stream, broker); + return new OggMetadata(info.tags, detectLength(info, stream)); + } - DataInputStream dataInputStream = new DataInputStream(packetInputStream); + private FlacTrackInfo load(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { + ByteBuffer buffer = broker.getBuffer(); - boolean hasMoreMetadata = trackInfoBuilder.getStreamInfo().hasMetadataBlocks; + if (buffer.getInt(NATIVE_FLAC_HEADER_OFFSET) != NATIVE_FLAC_HEADER) { + throw new IllegalStateException("Native flac header not found."); + } - while (hasMoreMetadata) { - if (!packetInputStream.startNewPacket()) { - throw new IllegalStateException("Track ended when more metadata was expected."); - } + buffer.position(NATIVE_FLAC_HEADER_OFFSET + 4); - hasMoreMetadata = FlacMetadataReader.readMetadataBlock(dataInputStream, packetInputStream, trackInfoBuilder); + return readHeaders(buffer, stream); } - return trackInfoBuilder.build(); - } + private Long detectLength(FlacTrackInfo info, OggPacketInputStream stream) throws IOException { + OggStreamSizeInfo sizeInfo; - private static class Blueprint implements OggTrackBlueprint { - private final FlacTrackInfo info; + if (info.stream.sampleCount > 0) { + sizeInfo = new OggStreamSizeInfo(0, info.stream.sampleCount, 0, 0, info.stream.sampleRate); + } else { + sizeInfo = stream.seekForSizeInfo(info.stream.sampleRate); + } - private Blueprint(FlacTrackInfo info) { - this.info = info; + return sizeInfo != null ? sizeInfo.getDuration() : null; } - @Override - public OggTrackHandler loadTrackHandler(OggPacketInputStream stream) { - return new OggFlacTrackHandler(info, stream); + private FlacTrackInfo readHeaders(ByteBuffer firstPacketBuffer, OggPacketInputStream packetInputStream) throws IOException { + FlacStreamInfo streamInfo = FlacMetadataReader.readStreamInfoBlock(new DataInputStream(new ByteBufferInputStream(firstPacketBuffer))); + FlacTrackInfoBuilder trackInfoBuilder = new FlacTrackInfoBuilder(streamInfo); + + DataInputStream dataInputStream = new DataInputStream(packetInputStream); + + boolean hasMoreMetadata = trackInfoBuilder.getStreamInfo().hasMetadataBlocks; + + while (hasMoreMetadata) { + if (!packetInputStream.startNewPacket()) { + throw new IllegalStateException("Track ended when more metadata was expected."); + } + + hasMoreMetadata = FlacMetadataReader.readMetadataBlock(dataInputStream, packetInputStream, trackInfoBuilder); + } + + return trackInfoBuilder.build(); } - @Override - public int getSampleRate() { - return info.stream.sampleRate; + private static class Blueprint implements OggTrackBlueprint { + private final FlacTrackInfo info; + + private Blueprint(FlacTrackInfo info) { + this.info = info; + } + + @Override + public OggTrackHandler loadTrackHandler(OggPacketInputStream stream) { + return new OggFlacTrackHandler(info, stream); + } + + @Override + public int getSampleRate() { + return info.stream.sampleRate; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/flac/OggFlacTrackHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/flac/OggFlacTrackHandler.java index ed2e9ce3d..e6cdabcc7 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/flac/OggFlacTrackHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/flac/OggFlacTrackHandler.java @@ -9,79 +9,80 @@ import com.sedmelluq.discord.lavaplayer.filter.PcmFormat; import com.sedmelluq.discord.lavaplayer.tools.io.BitStreamReader; import com.sedmelluq.discord.lavaplayer.track.playback.AudioProcessingContext; + import java.io.IOException; /** * OGG stream handler for FLAC codec. */ public class OggFlacTrackHandler implements OggTrackHandler { - private final FlacTrackInfo info; - private final OggPacketInputStream packetInputStream; - private final BitStreamReader bitStreamReader; - private final int[] decodingBuffer; - private final int[][] rawSampleBuffers; - private final short[][] sampleBuffers; - private AudioPipeline downstream; + private final FlacTrackInfo info; + private final OggPacketInputStream packetInputStream; + private final BitStreamReader bitStreamReader; + private final int[] decodingBuffer; + private final int[][] rawSampleBuffers; + private final short[][] sampleBuffers; + private AudioPipeline downstream; - /** - * @param info FLAC track info - * @param packetInputStream OGG packet input stream - */ - public OggFlacTrackHandler(FlacTrackInfo info, OggPacketInputStream packetInputStream) { - this.info = info; - this.packetInputStream = packetInputStream; - this.bitStreamReader = new BitStreamReader(packetInputStream); - this.decodingBuffer = new int[FlacFrameReader.TEMPORARY_BUFFER_SIZE]; - this.rawSampleBuffers = new int[info.stream.channelCount][]; - this.sampleBuffers = new short[info.stream.channelCount][]; + /** + * @param info FLAC track info + * @param packetInputStream OGG packet input stream + */ + public OggFlacTrackHandler(FlacTrackInfo info, OggPacketInputStream packetInputStream) { + this.info = info; + this.packetInputStream = packetInputStream; + this.bitStreamReader = new BitStreamReader(packetInputStream); + this.decodingBuffer = new int[FlacFrameReader.TEMPORARY_BUFFER_SIZE]; + this.rawSampleBuffers = new int[info.stream.channelCount][]; + this.sampleBuffers = new short[info.stream.channelCount][]; - for (int i = 0; i < rawSampleBuffers.length; i++) { - rawSampleBuffers[i] = new int[info.stream.maximumBlockSize]; - sampleBuffers[i] = new short[info.stream.maximumBlockSize]; + for (int i = 0; i < rawSampleBuffers.length; i++) { + rawSampleBuffers[i] = new int[info.stream.maximumBlockSize]; + sampleBuffers[i] = new short[info.stream.maximumBlockSize]; + } } - } - @Override - public void initialise(AudioProcessingContext context, long timecode, long desiredTimecode) { - downstream = AudioPipelineFactory.create(context, - new PcmFormat(info.stream.channelCount, info.stream.sampleRate)); - downstream.seekPerformed(desiredTimecode, timecode); - } + @Override + public void initialise(AudioProcessingContext context, long timecode, long desiredTimecode) { + downstream = AudioPipelineFactory.create(context, + new PcmFormat(info.stream.channelCount, info.stream.sampleRate)); + downstream.seekPerformed(desiredTimecode, timecode); + } - @Override - public void provideFrames() throws InterruptedException { - try { - while (packetInputStream.startNewPacket()) { - int sampleCount = readFlacFrame(); + @Override + public void provideFrames() throws InterruptedException { + try { + while (packetInputStream.startNewPacket()) { + int sampleCount = readFlacFrame(); - if (sampleCount == 0) { - throw new IllegalStateException("Not enough bytes in packet."); - } + if (sampleCount == 0) { + throw new IllegalStateException("Not enough bytes in packet."); + } - downstream.process(sampleBuffers, 0, sampleCount); - } - } catch (IOException e) { - throw new RuntimeException(e); + downstream.process(sampleBuffers, 0, sampleCount); + } + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - private int readFlacFrame() throws IOException { - return FlacFrameReader.readFlacFrame(packetInputStream, bitStreamReader, info.stream, rawSampleBuffers, sampleBuffers, decodingBuffer); - } + private int readFlacFrame() throws IOException { + return FlacFrameReader.readFlacFrame(packetInputStream, bitStreamReader, info.stream, rawSampleBuffers, sampleBuffers, decodingBuffer); + } - @Override - public void seekToTimecode(long timecode) { - try { - downstream.seekPerformed(timecode, packetInputStream.seek(timecode)); - } catch (IOException e) { - throw new RuntimeException(e); + @Override + public void seekToTimecode(long timecode) { + try { + downstream.seekPerformed(timecode, packetInputStream.seek(timecode)); + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - @Override - public void close() { - if (downstream != null) { - downstream.close(); + @Override + public void close() { + if (downstream != null) { + downstream.close(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/opus/OggOpusCodecHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/opus/OggOpusCodecHandler.java index 13295bd3b..51cb0c26d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/opus/OggOpusCodecHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/opus/OggOpusCodecHandler.java @@ -1,11 +1,6 @@ package com.sedmelluq.discord.lavaplayer.container.ogg.opus; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggCodecHandler; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggMetadata; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggPacketInputStream; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggStreamSizeInfo; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggTrackBlueprint; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggTrackHandler; +import com.sedmelluq.discord.lavaplayer.container.ogg.*; import com.sedmelluq.discord.lavaplayer.container.ogg.vorbis.VorbisCommentParser; import com.sedmelluq.discord.lavaplayer.tools.io.DirectBufferStreamBroker; @@ -18,109 +13,109 @@ * Loader for Opus track providers from an OGG stream. */ public class OggOpusCodecHandler implements OggCodecHandler { - private static final int OPUS_IDENTIFIER = ByteBuffer.wrap(new byte[] { 'O', 'p', 'u', 's' }).getInt(); - private static final int HEAD_TAG_HALF = ByteBuffer.wrap(new byte[] { 'H', 'e', 'a', 'd' }).getInt(); - - private static final int OPUS_TAG_HALF = ByteBuffer.wrap(new byte[] { 'O', 'p', 'u', 's' }).getInt(); - private static final int TAGS_TAG_HALF = ByteBuffer.wrap(new byte[] { 'T', 'a', 'g', 's' }).getInt(); - - private static final int MAX_COMMENTS_SAVED_LENGTH = 1024 * 60; // 60 KB - private static final int MAX_COMMENTS_READ_LENGTH = 1024 * 1024 * 120; // 120 MB - - @Override - public boolean isMatchingIdentifier(int identifier) { - return identifier == OPUS_IDENTIFIER; - } - - @Override - public int getMaximumFirstPacketLength() { - return 276; - } - - @Override - public OggTrackBlueprint loadBlueprint(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { - ByteBuffer firstPacket = broker.getBuffer(); - int sampleRate = getSampleRate(firstPacket); - verifyFirstPacket(firstPacket); - loadCommentsHeader(stream, broker, true); - stream.setSeekPoints(stream.createSeekTable(sampleRate)); - int channelCount = firstPacket.get(9) & 0xFF; - return new Blueprint(broker, channelCount, sampleRate); - } - - @Override - public OggMetadata loadMetadata(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { - ByteBuffer firstPacket = broker.getBuffer(); - verifyFirstPacket(firstPacket); - - loadCommentsHeader(stream, broker, false); - - return new OggMetadata( - parseTags(broker.getBuffer(), broker.isTruncated()), - detectLength(stream, getSampleRate(firstPacket)) - ); - } - - private Map parseTags(ByteBuffer tagBuffer, boolean truncated) { - if (tagBuffer.getInt() != OPUS_TAG_HALF || tagBuffer.getInt() != TAGS_TAG_HALF) { - return Collections.emptyMap(); - } + private static final int OPUS_IDENTIFIER = ByteBuffer.wrap(new byte[]{'O', 'p', 'u', 's'}).getInt(); + private static final int HEAD_TAG_HALF = ByteBuffer.wrap(new byte[]{'H', 'e', 'a', 'd'}).getInt(); - return VorbisCommentParser.parse(tagBuffer, truncated); - } + private static final int OPUS_TAG_HALF = ByteBuffer.wrap(new byte[]{'O', 'p', 'u', 's'}).getInt(); + private static final int TAGS_TAG_HALF = ByteBuffer.wrap(new byte[]{'T', 'a', 'g', 's'}).getInt(); - private Long detectLength(OggPacketInputStream stream, int sampleRate) throws IOException { - OggStreamSizeInfo sizeInfo = stream.seekForSizeInfo(sampleRate); + private static final int MAX_COMMENTS_SAVED_LENGTH = 1024 * 60; // 60 KB + private static final int MAX_COMMENTS_READ_LENGTH = 1024 * 1024 * 120; // 120 MB - if (sizeInfo != null) { - return sizeInfo.totalSamples * 1000 / sizeInfo.sampleRate; - } else { - return null; + @Override + public boolean isMatchingIdentifier(int identifier) { + return identifier == OPUS_IDENTIFIER; } - } - private void verifyFirstPacket(ByteBuffer firstPacket) { - if (firstPacket.getInt(4) != HEAD_TAG_HALF) { - throw new IllegalStateException("First packet is not an OpusHead."); + @Override + public int getMaximumFirstPacketLength() { + return 276; } - } - private int getSampleRate(ByteBuffer firstPacket) { - return Integer.reverseBytes(firstPacket.getInt(12)); - } + @Override + public OggTrackBlueprint loadBlueprint(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { + ByteBuffer firstPacket = broker.getBuffer(); + int sampleRate = getSampleRate(firstPacket); + verifyFirstPacket(firstPacket); + loadCommentsHeader(stream, broker, true); + stream.setSeekPoints(stream.createSeekTable(sampleRate)); + int channelCount = firstPacket.get(9) & 0xFF; + return new Blueprint(broker, channelCount, sampleRate); + } - private void loadCommentsHeader(OggPacketInputStream stream, DirectBufferStreamBroker broker, boolean skip) - throws IOException { + @Override + public OggMetadata loadMetadata(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { + ByteBuffer firstPacket = broker.getBuffer(); + verifyFirstPacket(firstPacket); + + loadCommentsHeader(stream, broker, false); - if (!stream.startNewPacket()) { - throw new IllegalStateException("No OpusTags packet in track."); - } else if (!broker.consumeNext(stream, skip ? 0 : MAX_COMMENTS_SAVED_LENGTH, MAX_COMMENTS_READ_LENGTH)) { - if (!stream.isPacketComplete()) { - throw new IllegalStateException("Opus comments header packet longer than allowed."); - } + return new OggMetadata( + parseTags(broker.getBuffer(), broker.isTruncated()), + detectLength(stream, getSampleRate(firstPacket)) + ); } - } - private static class Blueprint implements OggTrackBlueprint { - private final DirectBufferStreamBroker broker; - private final int channelCount; - private final int sampleRate; + private Map parseTags(ByteBuffer tagBuffer, boolean truncated) { + if (tagBuffer.getInt() != OPUS_TAG_HALF || tagBuffer.getInt() != TAGS_TAG_HALF) { + return Collections.emptyMap(); + } - private Blueprint(DirectBufferStreamBroker broker, int channelCount, int sampleRate) { - this.broker = broker; - this.channelCount = channelCount; - this.sampleRate = sampleRate; + return VorbisCommentParser.parse(tagBuffer, truncated); } - @Override - public OggTrackHandler loadTrackHandler(OggPacketInputStream stream) { - broker.clear(); - return new OggOpusTrackHandler(stream, broker, channelCount, sampleRate); + private Long detectLength(OggPacketInputStream stream, int sampleRate) throws IOException { + OggStreamSizeInfo sizeInfo = stream.seekForSizeInfo(sampleRate); + + if (sizeInfo != null) { + return sizeInfo.totalSamples * 1000 / sizeInfo.sampleRate; + } else { + return null; + } } - @Override - public int getSampleRate() { - return sampleRate; + private void verifyFirstPacket(ByteBuffer firstPacket) { + if (firstPacket.getInt(4) != HEAD_TAG_HALF) { + throw new IllegalStateException("First packet is not an OpusHead."); + } + } + + private int getSampleRate(ByteBuffer firstPacket) { + return Integer.reverseBytes(firstPacket.getInt(12)); + } + + private void loadCommentsHeader(OggPacketInputStream stream, DirectBufferStreamBroker broker, boolean skip) + throws IOException { + + if (!stream.startNewPacket()) { + throw new IllegalStateException("No OpusTags packet in track."); + } else if (!broker.consumeNext(stream, skip ? 0 : MAX_COMMENTS_SAVED_LENGTH, MAX_COMMENTS_READ_LENGTH)) { + if (!stream.isPacketComplete()) { + throw new IllegalStateException("Opus comments header packet longer than allowed."); + } + } + } + + private static class Blueprint implements OggTrackBlueprint { + private final DirectBufferStreamBroker broker; + private final int channelCount; + private final int sampleRate; + + private Blueprint(DirectBufferStreamBroker broker, int channelCount, int sampleRate) { + this.broker = broker; + this.channelCount = channelCount; + this.sampleRate = sampleRate; + } + + @Override + public OggTrackHandler loadTrackHandler(OggPacketInputStream stream) { + broker.clear(); + return new OggOpusTrackHandler(stream, broker, channelCount, sampleRate); + } + + @Override + public int getSampleRate() { + return sampleRate; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/opus/OggOpusTrackHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/opus/OggOpusTrackHandler.java index 3591c487f..c64e4c76c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/opus/OggOpusTrackHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/opus/OggOpusTrackHandler.java @@ -5,6 +5,7 @@ import com.sedmelluq.discord.lavaplayer.container.ogg.OggTrackHandler; import com.sedmelluq.discord.lavaplayer.tools.io.DirectBufferStreamBroker; import com.sedmelluq.discord.lavaplayer.track.playback.AudioProcessingContext; + import java.io.IOException; import java.nio.ByteBuffer; @@ -12,63 +13,63 @@ * OGG stream handler for Opus codec. */ public class OggOpusTrackHandler implements OggTrackHandler { - private final OggPacketInputStream packetInputStream; - private final DirectBufferStreamBroker broker; - private final int channelCount; - private final int sampleRate; - private OpusPacketRouter opusPacketRouter; + private final OggPacketInputStream packetInputStream; + private final DirectBufferStreamBroker broker; + private final int channelCount; + private final int sampleRate; + private OpusPacketRouter opusPacketRouter; - /** - * @param packetInputStream OGG packet input stream - * @param broker Broker for loading stream data into direct byte buffer. - * @param channelCount Number of channels in the track. - * @param sampleRate Sample rate of the track. - */ - public OggOpusTrackHandler(OggPacketInputStream packetInputStream, DirectBufferStreamBroker broker, int channelCount, - int sampleRate) { + /** + * @param packetInputStream OGG packet input stream + * @param broker Broker for loading stream data into direct byte buffer. + * @param channelCount Number of channels in the track. + * @param sampleRate Sample rate of the track. + */ + public OggOpusTrackHandler(OggPacketInputStream packetInputStream, DirectBufferStreamBroker broker, int channelCount, + int sampleRate) { - this.packetInputStream = packetInputStream; - this.broker = broker; - this.channelCount = channelCount; - this.sampleRate = sampleRate; - } + this.packetInputStream = packetInputStream; + this.broker = broker; + this.channelCount = channelCount; + this.sampleRate = sampleRate; + } - @Override - public void initialise(AudioProcessingContext context, long timecode, long desiredTimecode) { - opusPacketRouter = new OpusPacketRouter(context, sampleRate, channelCount); - opusPacketRouter.seekPerformed(desiredTimecode, timecode); - } + @Override + public void initialise(AudioProcessingContext context, long timecode, long desiredTimecode) { + opusPacketRouter = new OpusPacketRouter(context, sampleRate, channelCount); + opusPacketRouter.seekPerformed(desiredTimecode, timecode); + } - @Override - public void provideFrames() throws InterruptedException { - try { - while (packetInputStream.startNewPacket()) { - broker.consumeNext(packetInputStream, Integer.MAX_VALUE, Integer.MAX_VALUE); + @Override + public void provideFrames() throws InterruptedException { + try { + while (packetInputStream.startNewPacket()) { + broker.consumeNext(packetInputStream, Integer.MAX_VALUE, Integer.MAX_VALUE); - ByteBuffer buffer = broker.getBuffer(); + ByteBuffer buffer = broker.getBuffer(); - if (buffer.remaining() > 0) { - opusPacketRouter.process(buffer); + if (buffer.remaining() > 0) { + opusPacketRouter.process(buffer); + } + } + } catch (IOException e) { + throw new RuntimeException(e); } - } - } catch (IOException e) { - throw new RuntimeException(e); } - } - @Override - public void seekToTimecode(long timecode) { - try { - opusPacketRouter.seekPerformed(timecode, packetInputStream.seek(timecode)); - } catch (IOException e) { - throw new RuntimeException(e); + @Override + public void seekToTimecode(long timecode) { + try { + opusPacketRouter.seekPerformed(timecode, packetInputStream.seek(timecode)); + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - @Override - public void close() { - if (opusPacketRouter != null) { - opusPacketRouter.close(); + @Override + public void close() { + if (opusPacketRouter != null) { + opusPacketRouter.close(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/vorbis/OggVorbisCodecHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/vorbis/OggVorbisCodecHandler.java index 91481425b..b28f8b499 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/vorbis/OggVorbisCodecHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/vorbis/OggVorbisCodecHandler.java @@ -1,12 +1,6 @@ package com.sedmelluq.discord.lavaplayer.container.ogg.vorbis; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggCodecHandler; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggMetadata; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggPacketInputStream; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggSeekPoint; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggStreamSizeInfo; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggTrackBlueprint; -import com.sedmelluq.discord.lavaplayer.container.ogg.OggTrackHandler; +import com.sedmelluq.discord.lavaplayer.container.ogg.*; import com.sedmelluq.discord.lavaplayer.tools.Units; import com.sedmelluq.discord.lavaplayer.tools.io.DirectBufferStreamBroker; @@ -16,89 +10,89 @@ import java.util.List; public class OggVorbisCodecHandler implements OggCodecHandler { - private static final int VORBIS_IDENTIFIER = ByteBuffer.wrap(new byte[] { 0x01, 'v', 'o', 'r' }).getInt(); - - // These are arbitrary - there is no limit specified in Vorbis specification, Opus limit used as reference. - private static final int MAX_COMMENTS_SAVED_LENGTH = 1024 * 128; // 128 KB - private static final int MAX_COMMENTS_READ_LENGTH = 1024 * 1024 * 120; // 120 MB - - private static final byte[] COMMENT_PACKET_START = new byte[] { 0x03, 'v', 'o', 'r', 'b', 'i', 's' }; - - @Override - public boolean isMatchingIdentifier(int identifier) { - return identifier == VORBIS_IDENTIFIER; - } - - @Override - public int getMaximumFirstPacketLength() { - return 64; - } - - @Override - public OggTrackBlueprint loadBlueprint(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { - byte[] infoPacket = broker.extractBytes(); - loadCommentsHeader(stream, broker, true); - ByteBuffer infoBuffer = ByteBuffer.wrap(infoPacket); - int sampleRate = Integer.reverseBytes(infoBuffer.getInt(12)); - List seekPointList = stream.createSeekTable(sampleRate); - if (seekPointList != null) stream.setSeekPoints(seekPointList); - return new Blueprint(sampleRate, infoPacket, broker); - } - - @Override - public OggMetadata loadMetadata(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { - byte[] infoPacket = broker.extractBytes(); - loadCommentsHeader(stream, broker, false); - - ByteBuffer commentsPacket = broker.getBuffer(); - byte[] packetStart = new byte[COMMENT_PACKET_START.length]; - commentsPacket.get(packetStart); - - if (!Arrays.equals(packetStart, COMMENT_PACKET_START)) { - return OggMetadata.EMPTY; - } + private static final int VORBIS_IDENTIFIER = ByteBuffer.wrap(new byte[]{0x01, 'v', 'o', 'r'}).getInt(); - ByteBuffer infoBuffer = ByteBuffer.wrap(infoPacket); - int sampleRate = Integer.reverseBytes(infoBuffer.getInt(12)); - OggStreamSizeInfo sizeInfo = stream.seekForSizeInfo(sampleRate); - - return new OggMetadata( - VorbisCommentParser.parse(commentsPacket, broker.isTruncated()), - sizeInfo != null ? sizeInfo.getDuration() : Units.DURATION_MS_UNKNOWN - ); - } - - private void loadCommentsHeader(OggPacketInputStream stream, DirectBufferStreamBroker broker, boolean skip) - throws IOException { - - if (!stream.startNewPacket()) { - throw new IllegalStateException("No comments packet in track."); - } else if (!broker.consumeNext(stream, skip ? 0 : MAX_COMMENTS_SAVED_LENGTH, MAX_COMMENTS_READ_LENGTH)) { - if (!stream.isPacketComplete()) { - throw new IllegalStateException("Vorbis comments header packet longer than allowed."); - } - } - } + // These are arbitrary - there is no limit specified in Vorbis specification, Opus limit used as reference. + private static final int MAX_COMMENTS_SAVED_LENGTH = 1024 * 128; // 128 KB + private static final int MAX_COMMENTS_READ_LENGTH = 1024 * 1024 * 120; // 120 MB + + private static final byte[] COMMENT_PACKET_START = new byte[]{0x03, 'v', 'o', 'r', 'b', 'i', 's'}; - private static class Blueprint implements OggTrackBlueprint { - private final int sampleRate; - private final byte[] infoPacket; - private final DirectBufferStreamBroker broker; + @Override + public boolean isMatchingIdentifier(int identifier) { + return identifier == VORBIS_IDENTIFIER; + } - private Blueprint(int sampleRate, byte[] infoPacket, DirectBufferStreamBroker broker) { - this.sampleRate = sampleRate; - this.infoPacket = infoPacket; - this.broker = broker; + @Override + public int getMaximumFirstPacketLength() { + return 64; } @Override - public OggTrackHandler loadTrackHandler(OggPacketInputStream stream) { - return new OggVorbisTrackHandler(infoPacket, stream, broker); + public OggTrackBlueprint loadBlueprint(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { + byte[] infoPacket = broker.extractBytes(); + loadCommentsHeader(stream, broker, true); + ByteBuffer infoBuffer = ByteBuffer.wrap(infoPacket); + int sampleRate = Integer.reverseBytes(infoBuffer.getInt(12)); + List seekPointList = stream.createSeekTable(sampleRate); + if (seekPointList != null) stream.setSeekPoints(seekPointList); + return new Blueprint(sampleRate, infoPacket, broker); } @Override - public int getSampleRate() { - return sampleRate; + public OggMetadata loadMetadata(OggPacketInputStream stream, DirectBufferStreamBroker broker) throws IOException { + byte[] infoPacket = broker.extractBytes(); + loadCommentsHeader(stream, broker, false); + + ByteBuffer commentsPacket = broker.getBuffer(); + byte[] packetStart = new byte[COMMENT_PACKET_START.length]; + commentsPacket.get(packetStart); + + if (!Arrays.equals(packetStart, COMMENT_PACKET_START)) { + return OggMetadata.EMPTY; + } + + ByteBuffer infoBuffer = ByteBuffer.wrap(infoPacket); + int sampleRate = Integer.reverseBytes(infoBuffer.getInt(12)); + OggStreamSizeInfo sizeInfo = stream.seekForSizeInfo(sampleRate); + + return new OggMetadata( + VorbisCommentParser.parse(commentsPacket, broker.isTruncated()), + sizeInfo != null ? sizeInfo.getDuration() : Units.DURATION_MS_UNKNOWN + ); + } + + private void loadCommentsHeader(OggPacketInputStream stream, DirectBufferStreamBroker broker, boolean skip) + throws IOException { + + if (!stream.startNewPacket()) { + throw new IllegalStateException("No comments packet in track."); + } else if (!broker.consumeNext(stream, skip ? 0 : MAX_COMMENTS_SAVED_LENGTH, MAX_COMMENTS_READ_LENGTH)) { + if (!stream.isPacketComplete()) { + throw new IllegalStateException("Vorbis comments header packet longer than allowed."); + } + } + } + + private static class Blueprint implements OggTrackBlueprint { + private final int sampleRate; + private final byte[] infoPacket; + private final DirectBufferStreamBroker broker; + + private Blueprint(int sampleRate, byte[] infoPacket, DirectBufferStreamBroker broker) { + this.sampleRate = sampleRate; + this.infoPacket = infoPacket; + this.broker = broker; + } + + @Override + public OggTrackHandler loadTrackHandler(OggPacketInputStream stream) { + return new OggVorbisTrackHandler(infoPacket, stream, broker); + } + + @Override + public int getSampleRate() { + return sampleRate; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/vorbis/OggVorbisTrackHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/vorbis/OggVorbisTrackHandler.java index de1679a8b..41a48cd83 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/vorbis/OggVorbisTrackHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/vorbis/OggVorbisTrackHandler.java @@ -8,6 +8,7 @@ import com.sedmelluq.discord.lavaplayer.natives.vorbis.VorbisDecoder; import com.sedmelluq.discord.lavaplayer.tools.io.DirectBufferStreamBroker; import com.sedmelluq.discord.lavaplayer.track.playback.AudioProcessingContext; + import java.io.IOException; import java.nio.ByteBuffer; @@ -15,99 +16,99 @@ * OGG stream handler for Vorbis codec. */ public class OggVorbisTrackHandler implements OggTrackHandler { - private static final int PCM_BUFFER_SIZE = 4096; - - private final byte[] infoPacket; - private final OggPacketInputStream packetInputStream; - private final DirectBufferStreamBroker broker; - private final VorbisDecoder decoder; - private final int sampleRate; - private float[][] channelPcmBuffers; - private AudioPipeline downstream; - - /** - * @param packetInputStream OGG packet input stream - * @param broker Broker for loading stream data into direct byte buffer, it has already loaded the first two packets - * (info and comments) and should be in the state where we should request the next - the setup packet. - */ - public OggVorbisTrackHandler(byte[] infoPacket, OggPacketInputStream packetInputStream, - DirectBufferStreamBroker broker) { - - this.infoPacket = infoPacket; - this.packetInputStream = packetInputStream; - this.broker = broker; - this.decoder = new VorbisDecoder(); - - ByteBuffer infoBuffer = ByteBuffer.wrap(infoPacket); - this.sampleRate = Integer.reverseBytes(infoBuffer.getInt(12)); - - int channelCount = infoBuffer.get(11) & 0xFF; - channelPcmBuffers = new float[channelCount][]; - - for (int i = 0; i < channelPcmBuffers.length; i++) { - channelPcmBuffers[i] = new float[PCM_BUFFER_SIZE]; + private static final int PCM_BUFFER_SIZE = 4096; + + private final byte[] infoPacket; + private final OggPacketInputStream packetInputStream; + private final DirectBufferStreamBroker broker; + private final VorbisDecoder decoder; + private final int sampleRate; + private float[][] channelPcmBuffers; + private AudioPipeline downstream; + + /** + * @param packetInputStream OGG packet input stream + * @param broker Broker for loading stream data into direct byte buffer, it has already loaded the first two packets + * (info and comments) and should be in the state where we should request the next - the setup packet. + */ + public OggVorbisTrackHandler(byte[] infoPacket, OggPacketInputStream packetInputStream, + DirectBufferStreamBroker broker) { + + this.infoPacket = infoPacket; + this.packetInputStream = packetInputStream; + this.broker = broker; + this.decoder = new VorbisDecoder(); + + ByteBuffer infoBuffer = ByteBuffer.wrap(infoPacket); + this.sampleRate = Integer.reverseBytes(infoBuffer.getInt(12)); + + int channelCount = infoBuffer.get(11) & 0xFF; + channelPcmBuffers = new float[channelCount][]; + + for (int i = 0; i < channelPcmBuffers.length; i++) { + channelPcmBuffers[i] = new float[PCM_BUFFER_SIZE]; + } } - } - @Override - public void initialise(AudioProcessingContext context, long timecode, long desiredTimecode) throws IOException { - ByteBuffer infoBuffer = ByteBuffer.allocateDirect(infoPacket.length); - infoBuffer.put(infoPacket); - infoBuffer.flip(); + @Override + public void initialise(AudioProcessingContext context, long timecode, long desiredTimecode) throws IOException { + ByteBuffer infoBuffer = ByteBuffer.allocateDirect(infoPacket.length); + infoBuffer.put(infoPacket); + infoBuffer.flip(); - if (!packetInputStream.startNewPacket()) { - throw new IllegalStateException("End of track before header setup header."); - } + if (!packetInputStream.startNewPacket()) { + throw new IllegalStateException("End of track before header setup header."); + } - broker.consumeNext(packetInputStream, Integer.MAX_VALUE, Integer.MAX_VALUE); - decoder.initialise(infoBuffer, broker.getBuffer()); + broker.consumeNext(packetInputStream, Integer.MAX_VALUE, Integer.MAX_VALUE); + decoder.initialise(infoBuffer, broker.getBuffer()); - broker.resetAndCompact(); + broker.resetAndCompact(); - downstream = AudioPipelineFactory.create(context, new PcmFormat(decoder.getChannelCount(), sampleRate)); - downstream.seekPerformed(desiredTimecode, timecode); - } + downstream = AudioPipelineFactory.create(context, new PcmFormat(decoder.getChannelCount(), sampleRate)); + downstream.seekPerformed(desiredTimecode, timecode); + } - @Override - public void provideFrames() throws InterruptedException { - try { - while (packetInputStream.startNewPacket()) { - broker.consumeNext(packetInputStream, Integer.MAX_VALUE, Integer.MAX_VALUE); - provideFromBuffer(broker.getBuffer()); - } - } catch (IOException e) { - throw new RuntimeException(e); + @Override + public void provideFrames() throws InterruptedException { + try { + while (packetInputStream.startNewPacket()) { + broker.consumeNext(packetInputStream, Integer.MAX_VALUE, Integer.MAX_VALUE); + provideFromBuffer(broker.getBuffer()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - - private void provideFromBuffer(ByteBuffer buffer) throws InterruptedException { - decoder.input(buffer); - int output; - - do { - output = decoder.output(channelPcmBuffers); - - if (output > 0) { - downstream.process(channelPcmBuffers, 0, output); - } - } while (output == PCM_BUFFER_SIZE); - } - - @Override - public void seekToTimecode(long timecode) { - try { - downstream.seekPerformed(timecode, packetInputStream.seek(timecode)); - } catch (IOException e) { - throw new RuntimeException(e); + + private void provideFromBuffer(ByteBuffer buffer) throws InterruptedException { + decoder.input(buffer); + int output; + + do { + output = decoder.output(channelPcmBuffers); + + if (output > 0) { + downstream.process(channelPcmBuffers, 0, output); + } + } while (output == PCM_BUFFER_SIZE); } - } - @Override - public void close() { - if (downstream != null) { - downstream.close(); + @Override + public void seekToTimecode(long timecode) { + try { + downstream.seekPerformed(timecode, packetInputStream.seek(timecode)); + } catch (IOException e) { + throw new RuntimeException(e); + } } - decoder.close(); - } + @Override + public void close() { + if (downstream != null) { + downstream.close(); + } + + decoder.close(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/vorbis/VorbisCommentParser.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/vorbis/VorbisCommentParser.java index 06037432f..6de3a07fa 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/vorbis/VorbisCommentParser.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/ogg/vorbis/VorbisCommentParser.java @@ -7,56 +7,56 @@ import static java.nio.charset.StandardCharsets.UTF_8; public class VorbisCommentParser { - public static Map parse(ByteBuffer tagBuffer, boolean truncated) { - Map tags = new HashMap<>(); + public static Map parse(ByteBuffer tagBuffer, boolean truncated) { + Map tags = new HashMap<>(); - int vendorLength = Integer.reverseBytes(tagBuffer.getInt()); - if (vendorLength < 0) { - throw new IllegalStateException("Ogg comments vendor length is negative."); - } - - tagBuffer.position(tagBuffer.position() + vendorLength); - - int itemCount = Integer.reverseBytes(tagBuffer.getInt()); - - for (int itemIndex = 0; itemIndex < itemCount; itemIndex++) { - if (tagBuffer.remaining() < Integer.BYTES) { - if (!truncated) { - throw new IllegalArgumentException("Invalid tag buffer - tag size field out of bounds."); - } else { - // The buffer is truncated, it may cut off at an arbitrary point. - break; + int vendorLength = Integer.reverseBytes(tagBuffer.getInt()); + if (vendorLength < 0) { + throw new IllegalStateException("Ogg comments vendor length is negative."); } - } - - int itemLength = Integer.reverseBytes(tagBuffer.getInt()); - - if (itemLength < 0) { - throw new IllegalStateException("Ogg comments tag item length is negative."); - } else if (tagBuffer.remaining() < itemLength) { - if (!truncated) { - throw new IllegalArgumentException("Invalid tag buffer - tag size field out of bounds."); - } else { - // The buffer is truncated, it may cut off at an arbitrary point. - break; - } - } - byte[] data = new byte[itemLength]; - tagBuffer.get(data); + tagBuffer.position(tagBuffer.position() + vendorLength); + + int itemCount = Integer.reverseBytes(tagBuffer.getInt()); + + for (int itemIndex = 0; itemIndex < itemCount; itemIndex++) { + if (tagBuffer.remaining() < Integer.BYTES) { + if (!truncated) { + throw new IllegalArgumentException("Invalid tag buffer - tag size field out of bounds."); + } else { + // The buffer is truncated, it may cut off at an arbitrary point. + break; + } + } + + int itemLength = Integer.reverseBytes(tagBuffer.getInt()); + + if (itemLength < 0) { + throw new IllegalStateException("Ogg comments tag item length is negative."); + } else if (tagBuffer.remaining() < itemLength) { + if (!truncated) { + throw new IllegalArgumentException("Invalid tag buffer - tag size field out of bounds."); + } else { + // The buffer is truncated, it may cut off at an arbitrary point. + break; + } + } + + byte[] data = new byte[itemLength]; + tagBuffer.get(data); + + storeTagToMap(tags, data); + } - storeTagToMap(tags, data); + return tags; } - return tags; - } - - private static void storeTagToMap(Map tags, byte[] data) { - for (int i = 0; i < data.length; i++) { - if (data[i] == '=') { - tags.put(new String(data, 0, i, UTF_8), new String(data, i + 1, data.length - i - 1, UTF_8)); - break; - } + private static void storeTagToMap(Map tags, byte[] data) { + for (int i = 0; i < data.length; i++) { + if (data[i] == '=') { + tags.put(new String(data, 0, i, UTF_8), new String(data, i + 1, data.length - i - 1, UTF_8)); + break; + } + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/ExtendedM3uParser.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/ExtendedM3uParser.java index 960ba598d..b515ea45a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/ExtendedM3uParser.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/ExtendedM3uParser.java @@ -13,85 +13,86 @@ * #SOMETHING:FOO="thing",BAR=4 */ public class ExtendedM3uParser { - private static final Pattern directiveArgumentPattern = Pattern.compile("([A-Z-]+)=(?:\"([^\"]*)\"|([^,]*))(?:,|\\z)"); + private static final Pattern directiveArgumentPattern = Pattern.compile("([A-Z-]+)=(?:\"([^\"]*)\"|([^,]*))(?:,|\\z)"); - /** - * Parses one line. - * @param line Line. - * @return Line object describing the directive or data on the line. - */ - public static Line parseLine(String line) { - String trimmed = line.trim(); + /** + * Parses one line. + * + * @param line Line. + * @return Line object describing the directive or data on the line. + */ + public static Line parseLine(String line) { + String trimmed = line.trim(); - if (trimmed.isEmpty()) { - return Line.EMPTY_LINE; - } else if (!trimmed.startsWith("#")) { - return new Line(trimmed, null, Collections.emptyMap(), null); - } else { - return parseDirectiveLine(trimmed); + if (trimmed.isEmpty()) { + return Line.EMPTY_LINE; + } else if (!trimmed.startsWith("#")) { + return new Line(trimmed, null, Collections.emptyMap(), null); + } else { + return parseDirectiveLine(trimmed); + } } - } - private static Line parseDirectiveLine(String line) { - String[] parts = line.split(":", 2); + private static Line parseDirectiveLine(String line) { + String[] parts = line.split(":", 2); - if (parts.length == 1) { - return new Line(null, line.substring(1), Collections.emptyMap(), ""); - } + if (parts.length == 1) { + return new Line(null, line.substring(1), Collections.emptyMap(), ""); + } - Matcher matcher = directiveArgumentPattern.matcher(parts[1]); - Map arguments = new HashMap<>(); - - while (matcher.find()) { - arguments.put(matcher.group(1), DataFormatTools.defaultOnNull(matcher.group(2), matcher.group(3))); - } + Matcher matcher = directiveArgumentPattern.matcher(parts[1]); + Map arguments = new HashMap<>(); - return new Line(null, parts[0].substring(1), arguments, parts[1]); - } + while (matcher.find()) { + arguments.put(matcher.group(1), DataFormatTools.defaultOnNull(matcher.group(2), matcher.group(3))); + } - /** - * Parsed extended M3U line info. May be either an empty line (isDirective() and isData() both false), a directive - * or a data line. - */ - public static class Line { - private static final Line EMPTY_LINE = new Line(null, null, null, null); + return new Line(null, parts[0].substring(1), arguments, parts[1]); + } /** - * The data of a data line. - */ - public final String lineData; - /** - * Directive name of a directive line. - */ - public final String directiveName; - /** - * Directive arguments of a directive line. + * Parsed extended M3U line info. May be either an empty line (isDirective() and isData() both false), a directive + * or a data line. */ - public final Map directiveArguments; - /** - * Raw unprocessed directive extra data (where arguments are parsed from). - */ - public final String extraData; + public static class Line { + private static final Line EMPTY_LINE = new Line(null, null, null, null); - private Line(String lineData, String directiveName, Map directiveArguments, String extraData) { - this.lineData = lineData; - this.directiveName = directiveName; - this.directiveArguments = directiveArguments; - this.extraData = extraData; - } + /** + * The data of a data line. + */ + public final String lineData; + /** + * Directive name of a directive line. + */ + public final String directiveName; + /** + * Directive arguments of a directive line. + */ + public final Map directiveArguments; + /** + * Raw unprocessed directive extra data (where arguments are parsed from). + */ + public final String extraData; - /** - * @return True if it is a directive line. - */ - public boolean isDirective() { - return directiveName != null; - } + private Line(String lineData, String directiveName, Map directiveArguments, String extraData) { + this.lineData = lineData; + this.directiveName = directiveName; + this.directiveArguments = directiveArguments; + this.extraData = extraData; + } - /** - * @return True if it is a data line. - */ - public boolean isData() { - return lineData != null; + /** + * @return True if it is a directive line. + */ + public boolean isDirective() { + return directiveName != null; + } + + /** + * @return True if it is a data line. + */ + public boolean isData() { + return lineData != null; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamSegment.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamSegment.java index d4f9ec5b2..f4d09ac8d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamSegment.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamSegment.java @@ -1,22 +1,22 @@ package com.sedmelluq.discord.lavaplayer.container.playlists; public class HlsStreamSegment { - /** - * URL of the segment. - */ - public final String url; - /** - * Duration of the segment in milliseconds. null if unknown. - */ - public final Long duration; - /** - * Name of the segment. null if unknown. - */ - public final String name; + /** + * URL of the segment. + */ + public final String url; + /** + * Duration of the segment in milliseconds. null if unknown. + */ + public final Long duration; + /** + * Name of the segment. null if unknown. + */ + public final String name; - public HlsStreamSegment(String url, Long duration, String name) { - this.url = url; - this.duration = duration; - this.name = name; - } + public HlsStreamSegment(String url, Long duration, String name) { + this.url = url; + this.duration = duration; + this.name = name; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamSegmentParser.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamSegmentParser.java index 7c0ed0a0e..1e47bdbb7 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamSegmentParser.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamSegmentParser.java @@ -1,48 +1,49 @@ package com.sedmelluq.discord.lavaplayer.container.playlists; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; +import org.apache.http.client.methods.HttpGet; + import java.io.IOException; import java.util.ArrayList; import java.util.List; -import org.apache.http.client.methods.HttpGet; import static com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools.fetchResponseLines; public class HlsStreamSegmentParser { - public static List parseFromUrl(HttpInterface httpInterface, String url) throws IOException { - return parseFromLines(fetchResponseLines(httpInterface, new HttpGet(url), "stream segments list")); - } - - public static List parseFromLines(String[] lines) { - List segments = new ArrayList<>(); - ExtendedM3uParser.Line segmentInfo = null; - - for (String lineText : lines) { - ExtendedM3uParser.Line line = ExtendedM3uParser.parseLine(lineText); - - if (line.isDirective() && "EXTINF".equals(line.directiveName)) { - segmentInfo = line; - } - - if (line.isData()) { - if (segmentInfo != null && segmentInfo.extraData.contains(",")) { - String[] fields = segmentInfo.extraData.split(",", 2); - segments.add(new HlsStreamSegment(line.lineData, parseSecondDuration(fields[0]), fields[1])); - } else { - segments.add(new HlsStreamSegment(line.lineData, null, null)); - } - } + public static List parseFromUrl(HttpInterface httpInterface, String url) throws IOException { + return parseFromLines(fetchResponseLines(httpInterface, new HttpGet(url), "stream segments list")); } - return segments; - } + public static List parseFromLines(String[] lines) { + List segments = new ArrayList<>(); + ExtendedM3uParser.Line segmentInfo = null; + + for (String lineText : lines) { + ExtendedM3uParser.Line line = ExtendedM3uParser.parseLine(lineText); + + if (line.isDirective() && "EXTINF".equals(line.directiveName)) { + segmentInfo = line; + } + + if (line.isData()) { + if (segmentInfo != null && segmentInfo.extraData.contains(",")) { + String[] fields = segmentInfo.extraData.split(",", 2); + segments.add(new HlsStreamSegment(line.lineData, parseSecondDuration(fields[0]), fields[1])); + } else { + segments.add(new HlsStreamSegment(line.lineData, null, null)); + } + } + } + + return segments; + } - private static Long parseSecondDuration(String value) { - try { - double asDouble = Double.parseDouble(value); - return (long) (asDouble * 1000.0); - } catch (NumberFormatException ignored) { - return null; + private static Long parseSecondDuration(String value) { + try { + double asDouble = Double.parseDouble(value); + return (long) (asDouble * 1000.0); + } catch (NumberFormatException ignored) { + return null; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamSegmentUrlProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamSegmentUrlProvider.java index 6b76909f4..225ab919e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamSegmentUrlProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamSegmentUrlProvider.java @@ -13,51 +13,51 @@ import static com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools.fetchResponseLines; public class HlsStreamSegmentUrlProvider extends M3uStreamSegmentUrlProvider { - private static final Logger log = LoggerFactory.getLogger(HlsStreamSegmentUrlProvider.class); + private static final Logger log = LoggerFactory.getLogger(HlsStreamSegmentUrlProvider.class); - private final String streamListUrl; - private volatile String segmentPlaylistUrl; + private final String streamListUrl; + private volatile String segmentPlaylistUrl; - public HlsStreamSegmentUrlProvider(String streamListUrl, String segmentPlaylistUrl) { - this.streamListUrl = streamListUrl; - this.segmentPlaylistUrl = segmentPlaylistUrl; - } - - @Override - protected String getQualityFromM3uDirective(ExtendedM3uParser.Line directiveLine) { - return "default"; - } + public HlsStreamSegmentUrlProvider(String streamListUrl, String segmentPlaylistUrl) { + this.streamListUrl = streamListUrl; + this.segmentPlaylistUrl = segmentPlaylistUrl; + } - @Override - protected String fetchSegmentPlaylistUrl(HttpInterface httpInterface) throws IOException { - if (segmentPlaylistUrl != null) { - return segmentPlaylistUrl; + @Override + protected String getQualityFromM3uDirective(ExtendedM3uParser.Line directiveLine) { + return "default"; } - HttpUriRequest request = new HttpGet(streamListUrl); - List streams = loadChannelStreamsList(fetchResponseLines(httpInterface, request, - "HLS stream list")); + @Override + protected String fetchSegmentPlaylistUrl(HttpInterface httpInterface) throws IOException { + if (segmentPlaylistUrl != null) { + return segmentPlaylistUrl; + } - if (streams.isEmpty()) { - throw new IllegalStateException("No streams listed in HLS stream list."); - } + HttpUriRequest request = new HttpGet(streamListUrl); + List streams = loadChannelStreamsList(fetchResponseLines(httpInterface, request, + "HLS stream list")); - ChannelStreamInfo stream = streams.get(0); + if (streams.isEmpty()) { + throw new IllegalStateException("No streams listed in HLS stream list."); + } - log.debug("Chose stream with quality {} and url {}", stream.quality, stream.url); - segmentPlaylistUrl = stream.url; - return segmentPlaylistUrl; - } + ChannelStreamInfo stream = streams.get(0); - @Override - protected HttpUriRequest createSegmentGetRequest(String url) { - return new HttpGet(url); - } + log.debug("Chose stream with quality {} and url {}", stream.quality, stream.url); + segmentPlaylistUrl = stream.url; + return segmentPlaylistUrl; + } - public static String findHlsEntryUrl(String[] lines) { - List streams = new HlsStreamSegmentUrlProvider(null, null) - .loadChannelStreamsList(lines); + @Override + protected HttpUriRequest createSegmentGetRequest(String url) { + return new HttpGet(url); + } + + public static String findHlsEntryUrl(String[] lines) { + List streams = new HlsStreamSegmentUrlProvider(null, null) + .loadChannelStreamsList(lines); - return streams.isEmpty() ? null : streams.get(0).url; - } + return streams.isEmpty() ? null : streams.get(0).url; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamTrack.java index 453ec2b68..a95e080c0 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/HlsStreamTrack.java @@ -7,32 +7,32 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; public class HlsStreamTrack extends MpegTsM3uStreamAudioTrack { - private final HlsStreamSegmentUrlProvider segmentUrlProvider; - private final HttpInterfaceManager httpInterfaceManager; - - /** - * @param trackInfo Track info - * @param httpInterfaceManager - */ - public HlsStreamTrack(AudioTrackInfo trackInfo, String streamUrl, HttpInterfaceManager httpInterfaceManager, - boolean isInnerUrl) { - - super(trackInfo); - - segmentUrlProvider = isInnerUrl ? - new HlsStreamSegmentUrlProvider(null, streamUrl) : - new HlsStreamSegmentUrlProvider(streamUrl, null); - - this.httpInterfaceManager = httpInterfaceManager; - } - - @Override - protected M3uStreamSegmentUrlProvider getSegmentUrlProvider() { - return segmentUrlProvider; - } - - @Override - protected HttpInterface getHttpInterface() { - return httpInterfaceManager.getInterface(); - } + private final HlsStreamSegmentUrlProvider segmentUrlProvider; + private final HttpInterfaceManager httpInterfaceManager; + + /** + * @param trackInfo Track info + * @param httpInterfaceManager + */ + public HlsStreamTrack(AudioTrackInfo trackInfo, String streamUrl, HttpInterfaceManager httpInterfaceManager, + boolean isInnerUrl) { + + super(trackInfo); + + segmentUrlProvider = isInnerUrl ? + new HlsStreamSegmentUrlProvider(null, streamUrl) : + new HlsStreamSegmentUrlProvider(streamUrl, null); + + this.httpInterfaceManager = httpInterfaceManager; + } + + @Override + protected M3uStreamSegmentUrlProvider getSegmentUrlProvider() { + return segmentUrlProvider; + } + + @Override + protected HttpInterface getHttpInterface() { + return httpInterfaceManager.getInterface(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/M3uPlaylistContainerProbe.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/M3uPlaylistContainerProbe.java index 0a46045c4..562a7ae7e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/M3uPlaylistContainerProbe.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/M3uPlaylistContainerProbe.java @@ -29,93 +29,93 @@ * Probe for M3U playlist. */ public class M3uPlaylistContainerProbe implements MediaContainerProbe { - private static final Logger log = LoggerFactory.getLogger(M3uPlaylistContainerProbe.class); - - private static final String TYPE_HLS_OUTER = "hls-outer"; - private static final String TYPE_HLS_INNER = "hls-inner"; - - private static final int[] M3U_HEADER_TAG = new int[] { '#', 'E', 'X', 'T', 'M', '3', 'U' }; - private static final int[] M3U_ENTRY_TAG = new int[] { '#', 'E', 'X', 'T', 'I', 'N', 'F' }; - - private final HttpInterfaceManager httpInterfaceManager = new ThreadLocalHttpInterfaceManager( - HttpClientTools - .createSharedCookiesHttpBuilder() - .setRedirectStrategy(new HttpClientTools.NoRedirectsStrategy()), - HttpClientTools.DEFAULT_REQUEST_CONFIG - ); - - @Override - public String getName() { - return "m3u"; - } - - @Override - public boolean matchesHints(MediaContainerHints hints) { - return false; - } - - @Override - public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { - if (!checkNextBytes(inputStream, M3U_HEADER_TAG) && !checkNextBytes(inputStream, M3U_ENTRY_TAG)) { - return null; - } + private static final Logger log = LoggerFactory.getLogger(M3uPlaylistContainerProbe.class); - log.debug("Track {} is an M3U playlist file.", reference.identifier); - String[] lines = DataFormatTools.streamToLines(inputStream, StandardCharsets.UTF_8); + private static final String TYPE_HLS_OUTER = "hls-outer"; + private static final String TYPE_HLS_INNER = "hls-inner"; - String hlsStreamUrl = HlsStreamSegmentUrlProvider.findHlsEntryUrl(lines); + private static final int[] M3U_HEADER_TAG = new int[]{'#', 'E', 'X', 'T', 'M', '3', 'U'}; + private static final int[] M3U_ENTRY_TAG = new int[]{'#', 'E', 'X', 'T', 'I', 'N', 'F'}; - if (hlsStreamUrl != null) { - AudioTrackInfoBuilder infoBuilder = AudioTrackInfoBuilder.create(reference, inputStream); - AudioReference httpReference = HttpAudioSourceManager.getAsHttpReference(reference); + private final HttpInterfaceManager httpInterfaceManager = new ThreadLocalHttpInterfaceManager( + HttpClientTools + .createSharedCookiesHttpBuilder() + .setRedirectStrategy(new HttpClientTools.NoRedirectsStrategy()), + HttpClientTools.DEFAULT_REQUEST_CONFIG + ); - if (httpReference != null) { - return supportedFormat(this, TYPE_HLS_OUTER, infoBuilder.setIdentifier(httpReference.identifier).build()); - } else { - return refer(this, new AudioReference(hlsStreamUrl, infoBuilder.getTitle(), - new MediaContainerDescriptor(this, TYPE_HLS_INNER))); - } + @Override + public String getName() { + return "m3u"; } - MediaContainerDetectionResult result = loadSingleItemPlaylist(lines); - if (result != null) { - return result; + @Override + public boolean matchesHints(MediaContainerHints hints) { + return false; } - return unsupportedFormat(this, "The playlist file contains no links."); - } + @Override + public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { + if (!checkNextBytes(inputStream, M3U_HEADER_TAG) && !checkNextBytes(inputStream, M3U_ENTRY_TAG)) { + return null; + } + + log.debug("Track {} is an M3U playlist file.", reference.identifier); + String[] lines = DataFormatTools.streamToLines(inputStream, StandardCharsets.UTF_8); + + String hlsStreamUrl = HlsStreamSegmentUrlProvider.findHlsEntryUrl(lines); - private MediaContainerDetectionResult loadSingleItemPlaylist(String[] lines) { - String trackTitle = null; + if (hlsStreamUrl != null) { + AudioTrackInfoBuilder infoBuilder = AudioTrackInfoBuilder.create(reference, inputStream); + AudioReference httpReference = HttpAudioSourceManager.getAsHttpReference(reference); - for (String line : lines) { - if (line.startsWith("#EXTINF")) { - trackTitle = extractTitleFromInfo(line); - } else if (!line.startsWith("#") && line.length() > 0) { - if (line.startsWith("http://") || line.startsWith("https://") || line.startsWith("icy://")) { - return refer(this, new AudioReference(line.trim(), trackTitle)); + if (httpReference != null) { + return supportedFormat(this, TYPE_HLS_OUTER, infoBuilder.setIdentifier(httpReference.identifier).build()); + } else { + return refer(this, new AudioReference(hlsStreamUrl, infoBuilder.getTitle(), + new MediaContainerDescriptor(this, TYPE_HLS_INNER))); + } } - trackTitle = null; - } + MediaContainerDetectionResult result = loadSingleItemPlaylist(lines); + if (result != null) { + return result; + } + + return unsupportedFormat(this, "The playlist file contains no links."); } - return null; - } - - private String extractTitleFromInfo(String infoLine) { - String[] splitInfo = infoLine.split(",", 2); - return splitInfo.length == 2 ? splitInfo[1] : null; - } - - @Override - public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - if (parameters.equals(TYPE_HLS_INNER)) { - return new HlsStreamTrack(trackInfo, trackInfo.identifier, httpInterfaceManager, true); - } else if (parameters.equals(TYPE_HLS_OUTER)) { - return new HlsStreamTrack(trackInfo, trackInfo.identifier, httpInterfaceManager, false); - } else { - throw new IllegalArgumentException("Unsupported parameters: " + parameters); + private MediaContainerDetectionResult loadSingleItemPlaylist(String[] lines) { + String trackTitle = null; + + for (String line : lines) { + if (line.startsWith("#EXTINF")) { + trackTitle = extractTitleFromInfo(line); + } else if (!line.startsWith("#") && line.length() > 0) { + if (line.startsWith("http://") || line.startsWith("https://") || line.startsWith("icy://")) { + return refer(this, new AudioReference(line.trim(), trackTitle)); + } + + trackTitle = null; + } + } + + return null; + } + + private String extractTitleFromInfo(String infoLine) { + String[] splitInfo = infoLine.split(",", 2); + return splitInfo.length == 2 ? splitInfo[1] : null; + } + + @Override + public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + if (parameters.equals(TYPE_HLS_INNER)) { + return new HlsStreamTrack(trackInfo, trackInfo.identifier, httpInterfaceManager, true); + } else if (parameters.equals(TYPE_HLS_OUTER)) { + return new HlsStreamTrack(trackInfo, trackInfo.identifier, httpInterfaceManager, false); + } else { + throw new IllegalArgumentException("Unsupported parameters: " + parameters); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/PlainPlaylistContainerProbe.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/PlainPlaylistContainerProbe.java index d32eeb5e7..c5e6092fe 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/PlainPlaylistContainerProbe.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/PlainPlaylistContainerProbe.java @@ -25,44 +25,44 @@ * Probe for a playlist containing the raw link without any format. */ public class PlainPlaylistContainerProbe implements MediaContainerProbe { - private static final Logger log = LoggerFactory.getLogger(PlainPlaylistContainerProbe.class); + private static final Logger log = LoggerFactory.getLogger(PlainPlaylistContainerProbe.class); - private static final Pattern linkPattern = Pattern.compile("^(?:https?|icy)://.*"); + private static final Pattern linkPattern = Pattern.compile("^(?:https?|icy)://.*"); - @Override - public String getName() { - return "plain"; - } + @Override + public String getName() { + return "plain"; + } + + @Override + public boolean matchesHints(MediaContainerHints hints) { + return false; + } - @Override - public boolean matchesHints(MediaContainerHints hints) { - return false; - } + @Override + public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { + if (!matchNextBytesAsRegex(inputStream, STREAM_SCAN_DISTANCE, linkPattern, StandardCharsets.UTF_8)) { + return null; + } - @Override - public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { - if (!matchNextBytesAsRegex(inputStream, STREAM_SCAN_DISTANCE, linkPattern, StandardCharsets.UTF_8)) { - return null; + log.debug("Track {} is a plain playlist file.", reference.identifier); + return loadFromLines(DataFormatTools.streamToLines(inputStream, StandardCharsets.UTF_8)); } - log.debug("Track {} is a plain playlist file.", reference.identifier); - return loadFromLines(DataFormatTools.streamToLines(inputStream, StandardCharsets.UTF_8)); - } + private MediaContainerDetectionResult loadFromLines(String[] lines) { + for (String line : lines) { + Matcher matcher = linkPattern.matcher(line); - private MediaContainerDetectionResult loadFromLines(String[] lines) { - for (String line : lines) { - Matcher matcher = linkPattern.matcher(line); + if (matcher.matches()) { + return refer(this, new AudioReference(matcher.group(0), null)); + } + } - if (matcher.matches()) { - return refer(this, new AudioReference(matcher.group(0), null)); - } + return unsupportedFormat(this, "The playlist file contains no links."); } - return unsupportedFormat(this, "The playlist file contains no links."); - } - - @Override - public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - throw new UnsupportedOperationException(); - } + @Override + public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + throw new UnsupportedOperationException(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/PlsPlaylistContainerProbe.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/PlsPlaylistContainerProbe.java index 253efc1cf..05bca86ff 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/PlsPlaylistContainerProbe.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/playlists/PlsPlaylistContainerProbe.java @@ -27,61 +27,61 @@ * Probe for PLS playlist. */ public class PlsPlaylistContainerProbe implements MediaContainerProbe { - private static final Logger log = LoggerFactory.getLogger(PlsPlaylistContainerProbe.class); + private static final Logger log = LoggerFactory.getLogger(PlsPlaylistContainerProbe.class); - private static final int[] PLS_HEADER = new int[] { '[', -1, 'l', 'a', 'y', 'l', 'i', 's', 't', ']' }; + private static final int[] PLS_HEADER = new int[]{'[', -1, 'l', 'a', 'y', 'l', 'i', 's', 't', ']'}; - private static Pattern filePattern = Pattern.compile("\\s*File([0-9]+)=((?:https?|icy)://.*)\\s*"); - private static Pattern titlePattern = Pattern.compile("\\s*Title([0-9]+)=(.*)\\s*"); + private static Pattern filePattern = Pattern.compile("\\s*File([0-9]+)=((?:https?|icy)://.*)\\s*"); + private static Pattern titlePattern = Pattern.compile("\\s*Title([0-9]+)=(.*)\\s*"); - @Override - public String getName() { - return "pls"; - } + @Override + public String getName() { + return "pls"; + } + + @Override + public boolean matchesHints(MediaContainerHints hints) { + return false; + } - @Override - public boolean matchesHints(MediaContainerHints hints) { - return false; - } + @Override + public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { + if (!checkNextBytes(inputStream, PLS_HEADER)) { + return null; + } - @Override - public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { - if (!checkNextBytes(inputStream, PLS_HEADER)) { - return null; + log.debug("Track {} is a PLS playlist file.", reference.identifier); + return loadFromLines(DataFormatTools.streamToLines(inputStream, StandardCharsets.UTF_8)); } - log.debug("Track {} is a PLS playlist file.", reference.identifier); - return loadFromLines(DataFormatTools.streamToLines(inputStream, StandardCharsets.UTF_8)); - } + private MediaContainerDetectionResult loadFromLines(String[] lines) { + Map trackFiles = new HashMap<>(); + Map trackTitles = new HashMap<>(); - private MediaContainerDetectionResult loadFromLines(String[] lines) { - Map trackFiles = new HashMap<>(); - Map trackTitles = new HashMap<>(); + for (String line : lines) { + Matcher fileMatcher = filePattern.matcher(line); - for (String line : lines) { - Matcher fileMatcher = filePattern.matcher(line); + if (fileMatcher.matches()) { + trackFiles.put(fileMatcher.group(1), fileMatcher.group(2)); + continue; + } - if (fileMatcher.matches()) { - trackFiles.put(fileMatcher.group(1), fileMatcher.group(2)); - continue; - } + Matcher titleMatcher = titlePattern.matcher(line); + if (titleMatcher.matches()) { + trackTitles.put(titleMatcher.group(1), titleMatcher.group(2)); + } + } - Matcher titleMatcher = titlePattern.matcher(line); - if (titleMatcher.matches()) { - trackTitles.put(titleMatcher.group(1), titleMatcher.group(2)); - } - } + for (Map.Entry entry : trackFiles.entrySet()) { + String title = trackTitles.get(entry.getKey()); + return refer(this, new AudioReference(entry.getValue(), title != null ? title : UNKNOWN_TITLE)); + } - for (Map.Entry entry : trackFiles.entrySet()) { - String title = trackTitles.get(entry.getKey()); - return refer(this, new AudioReference(entry.getValue(), title != null ? title : UNKNOWN_TITLE)); + return unsupportedFormat(this, "The playlist file contains no links."); } - return unsupportedFormat(this, "The playlist file contains no links."); - } - - @Override - public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - throw new UnsupportedOperationException(); - } + @Override + public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + throw new UnsupportedOperationException(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavAudioTrack.java index 806c35ed7..8766ccfd0 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavAudioTrack.java @@ -11,29 +11,29 @@ * Audio track that handles a WAV stream */ public class WavAudioTrack extends BaseAudioTrack { - private static final Logger log = LoggerFactory.getLogger(WavAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(WavAudioTrack.class); - private final SeekableInputStream inputStream; + private final SeekableInputStream inputStream; - /** - * @param trackInfo Track info - * @param inputStream Input stream for the WAV file - */ - public WavAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param inputStream Input stream for the WAV file + */ + public WavAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + super(trackInfo); - this.inputStream = inputStream; - } + this.inputStream = inputStream; + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - WavTrackProvider trackProvider = new WavFileLoader(inputStream).loadTrack(localExecutor.getProcessingContext()); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + WavTrackProvider trackProvider = new WavFileLoader(inputStream).loadTrack(localExecutor.getProcessingContext()); - try { - log.debug("Starting to play WAV track {}", getIdentifier()); - localExecutor.executeProcessingLoop(trackProvider::provideFrames, trackProvider::seekToTimecode); - } finally { - trackProvider.close(); + try { + log.debug("Starting to play WAV track {}", getIdentifier()); + localExecutor.executeProcessingLoop(trackProvider::provideFrames, trackProvider::seekToTimecode); + } finally { + trackProvider.close(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavContainerProbe.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavContainerProbe.java index 8d3b1c008..a9d202ec7 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavContainerProbe.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavContainerProbe.java @@ -23,42 +23,42 @@ * Container detection probe for WAV format. */ public class WavContainerProbe implements MediaContainerProbe { - private static final Logger log = LoggerFactory.getLogger(WavContainerProbe.class); + private static final Logger log = LoggerFactory.getLogger(WavContainerProbe.class); - @Override - public String getName() { - return "wav"; - } - - @Override - public boolean matchesHints(MediaContainerHints hints) { - return false; - } + @Override + public String getName() { + return "wav"; + } - @Override - public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { - if (!checkNextBytes(inputStream, WAV_RIFF_HEADER)) { - return null; + @Override + public boolean matchesHints(MediaContainerHints hints) { + return false; } - log.debug("Track {} is a WAV file.", reference.identifier); + @Override + public MediaContainerDetectionResult probe(AudioReference reference, SeekableInputStream inputStream) throws IOException { + if (!checkNextBytes(inputStream, WAV_RIFF_HEADER)) { + return null; + } - WavFileInfo fileInfo = new WavFileLoader(inputStream).parseHeaders(); + log.debug("Track {} is a WAV file.", reference.identifier); - return supportedFormat(this, null, new AudioTrackInfo( - defaultOnNull(reference.title, UNKNOWN_TITLE), - UNKNOWN_ARTIST, - fileInfo.getDuration(), - reference.identifier, - false, - reference.identifier, - null, - null - )); - } + WavFileInfo fileInfo = new WavFileLoader(inputStream).parseHeaders(); - @Override - public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { - return new WavAudioTrack(trackInfo, inputStream); - } + return supportedFormat(this, null, new AudioTrackInfo( + defaultOnNull(reference.title, UNKNOWN_TITLE), + UNKNOWN_ARTIST, + fileInfo.getDuration(), + reference.identifier, + false, + reference.identifier, + null, + null + )); + } + + @Override + public AudioTrack createTrack(String parameters, AudioTrackInfo trackInfo, SeekableInputStream inputStream) { + return new WavAudioTrack(trackInfo, inputStream); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavFileInfo.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavFileInfo.java index a259f6ad7..f0b8c6a2a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavFileInfo.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavFileInfo.java @@ -30,12 +30,12 @@ public class WavFileInfo { public final long startOffset; /** - * @param channelCount Number of channels. - * @param sampleRate Sample rate. + * @param channelCount Number of channels. + * @param sampleRate Sample rate. * @param bitsPerSample Bits per sample (currently only 16 supported). - * @param blockAlign Size of a block (one sample for each channel + padding). - * @param blockCount Number of blocks in the file. - * @param startOffset Starting position of the raw PCM samples in the file. + * @param blockAlign Size of a block (one sample for each channel + padding). + * @param blockCount Number of blocks in the file. + * @param startOffset Starting position of the raw PCM samples in the file. */ public WavFileInfo(int channelCount, int sampleRate, int bitsPerSample, int blockAlign, long blockCount, long startOffset) { this.channelCount = channelCount; diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavFileLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavFileLoader.java index a665705e0..edde9fad4 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavFileLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavFileLoader.java @@ -14,116 +14,118 @@ * Loads either WAV header information or a WAV track provider from a stream. */ public class WavFileLoader { - static final int[] WAV_RIFF_HEADER = new int[] { 0x52, 0x49, 0x46, 0x46, -1, -1, -1, -1, 0x57, 0x41, 0x56, 0x45 }; - - private final SeekableInputStream inputStream; - - /** - * @param inputStream Input stream to read the WAV data from. This must be positioned right before WAV RIFF header. - */ - public WavFileLoader(SeekableInputStream inputStream) { - this.inputStream = inputStream; - } - - /** - * Parses the headers of the file. - * @return Format description of the WAV file - * @throws IOException On read error - */ - public WavFileInfo parseHeaders() throws IOException { - if (!checkNextBytes(inputStream, WAV_RIFF_HEADER, false)) { - throw new IllegalStateException("Not a WAV header."); - } + static final int[] WAV_RIFF_HEADER = new int[]{0x52, 0x49, 0x46, 0x46, -1, -1, -1, -1, 0x57, 0x41, 0x56, 0x45}; - InfoBuilder builder = new InfoBuilder(); - DataInput dataInput = new DataInputStream(inputStream); + private final SeekableInputStream inputStream; - while (true) { - String chunkName = readChunkName(dataInput); - long chunkSize = Integer.toUnsignedLong(Integer.reverseBytes(dataInput.readInt())); + /** + * @param inputStream Input stream to read the WAV data from. This must be positioned right before WAV RIFF header. + */ + public WavFileLoader(SeekableInputStream inputStream) { + this.inputStream = inputStream; + } - if ("fmt ".equals(chunkName)) { - readFormatChunk(builder, dataInput); + /** + * Parses the headers of the file. + * + * @return Format description of the WAV file + * @throws IOException On read error + */ + public WavFileInfo parseHeaders() throws IOException { + if (!checkNextBytes(inputStream, WAV_RIFF_HEADER, false)) { + throw new IllegalStateException("Not a WAV header."); + } - if (chunkSize > 16) { - inputStream.skipFully(chunkSize - 16); + InfoBuilder builder = new InfoBuilder(); + DataInput dataInput = new DataInputStream(inputStream); + + while (true) { + String chunkName = readChunkName(dataInput); + long chunkSize = Integer.toUnsignedLong(Integer.reverseBytes(dataInput.readInt())); + + if ("fmt ".equals(chunkName)) { + readFormatChunk(builder, dataInput); + + if (chunkSize > 16) { + inputStream.skipFully(chunkSize - 16); + } + } else if ("data".equals(chunkName)) { + builder.sampleAreaSize = chunkSize; + builder.startOffset = inputStream.getPosition(); + return builder.build(); + } else { + inputStream.skipFully(chunkSize); + } } - } else if ("data".equals(chunkName)) { - builder.sampleAreaSize = chunkSize; - builder.startOffset = inputStream.getPosition(); - return builder.build(); - } else { - inputStream.skipFully(chunkSize); - } } - } - - private String readChunkName(DataInput dataInput) throws IOException { - byte[] buffer = new byte[4]; - dataInput.readFully(buffer); - return new String(buffer, StandardCharsets.US_ASCII); - } - - private void readFormatChunk(InfoBuilder builder, DataInput dataInput) throws IOException { - builder.audioFormat = Short.reverseBytes(dataInput.readShort()) & 0xFFFF; - builder.channelCount = Short.reverseBytes(dataInput.readShort()) & 0xFFFF; - builder.sampleRate = Integer.reverseBytes(dataInput.readInt()); - - // Skip bitrate - dataInput.readInt(); - - builder.blockAlign = Short.reverseBytes(dataInput.readShort()) & 0xFFFF; - builder.bitsPerSample = Short.reverseBytes(dataInput.readShort()) & 0xFFFF; - } - - /** - * Initialise a WAV track stream. - * @param context Configuration and output information for processing - * @return The WAV track stream which can produce frames. - * @throws IOException On read error - */ - public WavTrackProvider loadTrack(AudioProcessingContext context) throws IOException { - return new WavTrackProvider(context, inputStream, parseHeaders()); - } - - private static class InfoBuilder { - private int audioFormat; - private int channelCount; - private int sampleRate; - private int bitsPerSample; - private int blockAlign; - private long sampleAreaSize; - private long startOffset; - - private WavFileInfo build() { - validateFormat(); - validateAlignment(); - - return new WavFileInfo(channelCount, sampleRate, bitsPerSample, blockAlign, sampleAreaSize / blockAlign, startOffset); + + private String readChunkName(DataInput dataInput) throws IOException { + byte[] buffer = new byte[4]; + dataInput.readFully(buffer); + return new String(buffer, StandardCharsets.US_ASCII); + } + + private void readFormatChunk(InfoBuilder builder, DataInput dataInput) throws IOException { + builder.audioFormat = Short.reverseBytes(dataInput.readShort()) & 0xFFFF; + builder.channelCount = Short.reverseBytes(dataInput.readShort()) & 0xFFFF; + builder.sampleRate = Integer.reverseBytes(dataInput.readInt()); + + // Skip bitrate + dataInput.readInt(); + + builder.blockAlign = Short.reverseBytes(dataInput.readShort()) & 0xFFFF; + builder.bitsPerSample = Short.reverseBytes(dataInput.readShort()) & 0xFFFF; } - private void validateFormat() { - if (audioFormat != 1) { - throw new IllegalStateException("Invalid audio format " + audioFormat + ", must be 1 (PCM)"); - } else if (channelCount < 1 || channelCount > 16) { - throw new IllegalStateException("Invalid channel count: " + channelCount); - } else if (sampleRate < 100 || sampleRate > 384000) { - throw new IllegalStateException("Invalid sample rate: " + sampleRate); - } else if (bitsPerSample != 16 && bitsPerSample != 24) { - throw new IllegalStateException("Unsupported bits per sample: " + bitsPerSample); - } + /** + * Initialise a WAV track stream. + * + * @param context Configuration and output information for processing + * @return The WAV track stream which can produce frames. + * @throws IOException On read error + */ + public WavTrackProvider loadTrack(AudioProcessingContext context) throws IOException { + return new WavTrackProvider(context, inputStream, parseHeaders()); } - private void validateAlignment() { - int minimumBlockAlign = channelCount * (bitsPerSample >> 3); + private static class InfoBuilder { + private int audioFormat; + private int channelCount; + private int sampleRate; + private int bitsPerSample; + private int blockAlign; + private long sampleAreaSize; + private long startOffset; - if (blockAlign < minimumBlockAlign || blockAlign > minimumBlockAlign + 32) { - throw new IllegalStateException("Block align is not valid: " + blockAlign); - } else if (blockAlign % (bitsPerSample >> 3) != 0) { - throw new IllegalStateException("Block align is not a multiple of bits per sample: " + blockAlign); - } else if (sampleAreaSize < 0) { - throw new IllegalStateException("Negative sample area size: " + sampleAreaSize); - } + private WavFileInfo build() { + validateFormat(); + validateAlignment(); + + return new WavFileInfo(channelCount, sampleRate, bitsPerSample, blockAlign, sampleAreaSize / blockAlign, startOffset); + } + + private void validateFormat() { + if (audioFormat != 1) { + throw new IllegalStateException("Invalid audio format " + audioFormat + ", must be 1 (PCM)"); + } else if (channelCount < 1 || channelCount > 16) { + throw new IllegalStateException("Invalid channel count: " + channelCount); + } else if (sampleRate < 100 || sampleRate > 384000) { + throw new IllegalStateException("Invalid sample rate: " + sampleRate); + } else if (bitsPerSample != 16 && bitsPerSample != 24) { + throw new IllegalStateException("Unsupported bits per sample: " + bitsPerSample); + } + } + + private void validateAlignment() { + int minimumBlockAlign = channelCount * (bitsPerSample >> 3); + + if (blockAlign < minimumBlockAlign || blockAlign > minimumBlockAlign + 32) { + throw new IllegalStateException("Block align is not valid: " + blockAlign); + } else if (blockAlign % (bitsPerSample >> 3) != 0) { + throw new IllegalStateException("Block align is not a multiple of bits per sample: " + blockAlign); + } else if (sampleAreaSize < 0) { + throw new IllegalStateException("Negative sample area size: " + sampleAreaSize); + } + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavTrackProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavTrackProvider.java index a2c116e46..e81520057 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavTrackProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/container/wav/WavTrackProvider.java @@ -18,128 +18,130 @@ * A provider of audio frames from a WAV track. */ public class WavTrackProvider { - private static final int BLOCKS_IN_BUFFER = 4096; - - private final SeekableInputStream inputStream; - private final DataInput dataInput; - private final WavFileInfo info; - private final AudioPipeline downstream; - private final short[] buffer; - private final byte[] rawBuffer; - private final ByteBuffer byteBuffer; - private final ShortBuffer nioBuffer; - - /** - * @param context Configuration and output information for processing - * @param inputStream Input stream to use - * @param info Information about the WAV file - */ - public WavTrackProvider(AudioProcessingContext context, SeekableInputStream inputStream, WavFileInfo info) { - this.inputStream = inputStream; - this.dataInput = new DataInputStream(inputStream); - this.info = info; - this.downstream = AudioPipelineFactory.create(context, new PcmFormat(info.channelCount, info.sampleRate)); - this.buffer = info.getPadding() > 0 ? new short[info.channelCount * BLOCKS_IN_BUFFER] : null; - - this.byteBuffer = ByteBuffer.allocate(info.blockAlign * BLOCKS_IN_BUFFER).order(LITTLE_ENDIAN); - this.rawBuffer = byteBuffer.array(); - this.nioBuffer = byteBuffer.asShortBuffer(); - } - - /** - * Seeks to the specified timecode. - * @param timecode The timecode in milliseconds - */ - public void seekToTimecode(long timecode) { - try { - long fileOffset = (timecode * info.sampleRate / 1000L) * info.blockAlign + info.startOffset; - inputStream.seek(fileOffset); - downstream.seekPerformed(timecode, timecode); - } catch (IOException e) { - throw new RuntimeException(e); + private static final int BLOCKS_IN_BUFFER = 4096; + + private final SeekableInputStream inputStream; + private final DataInput dataInput; + private final WavFileInfo info; + private final AudioPipeline downstream; + private final short[] buffer; + private final byte[] rawBuffer; + private final ByteBuffer byteBuffer; + private final ShortBuffer nioBuffer; + + /** + * @param context Configuration and output information for processing + * @param inputStream Input stream to use + * @param info Information about the WAV file + */ + public WavTrackProvider(AudioProcessingContext context, SeekableInputStream inputStream, WavFileInfo info) { + this.inputStream = inputStream; + this.dataInput = new DataInputStream(inputStream); + this.info = info; + this.downstream = AudioPipelineFactory.create(context, new PcmFormat(info.channelCount, info.sampleRate)); + this.buffer = info.getPadding() > 0 ? new short[info.channelCount * BLOCKS_IN_BUFFER] : null; + + this.byteBuffer = ByteBuffer.allocate(info.blockAlign * BLOCKS_IN_BUFFER).order(LITTLE_ENDIAN); + this.rawBuffer = byteBuffer.array(); + this.nioBuffer = byteBuffer.asShortBuffer(); } - } - - /** - * Reads audio frames and sends them to frame consumer - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - public void provideFrames() throws InterruptedException { - try { - int blockCount; - - while ((blockCount = getNextChunkBlocks()) > 0) { - if (buffer != null) { - processChunkWithPadding(blockCount); - } else { - processChunk(blockCount); + + /** + * Seeks to the specified timecode. + * + * @param timecode The timecode in milliseconds + */ + public void seekToTimecode(long timecode) { + try { + long fileOffset = (timecode * info.sampleRate / 1000L) * info.blockAlign + info.startOffset; + inputStream.seek(fileOffset); + downstream.seekPerformed(timecode, timecode); + } catch (IOException e) { + throw new RuntimeException(e); } - } - } catch (IOException e) { - throw new RuntimeException(e); } - } - - /** - * Free all resources associated to processing the track. - */ - public void close() { - downstream.close(); - } - - private void processChunkWithPadding(int blockCount) throws IOException, InterruptedException { - if (info.bitsPerSample != 16) { - throw new IllegalStateException("Cannot process " + info.bitsPerSample + "-bit PCM with padding!"); + + /** + * Reads audio frames and sends them to frame consumer + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + public void provideFrames() throws InterruptedException { + try { + int blockCount; + + while ((blockCount = getNextChunkBlocks()) > 0) { + if (buffer != null) { + processChunkWithPadding(blockCount); + } else { + processChunk(blockCount); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } } - readChunkToBuffer(blockCount); + /** + * Free all resources associated to processing the track. + */ + public void close() { + downstream.close(); + } - int padding = info.getPadding() / 2; - int sampleCount = blockCount * info.channelCount; - int indexInBlock = 0; + private void processChunkWithPadding(int blockCount) throws IOException, InterruptedException { + if (info.bitsPerSample != 16) { + throw new IllegalStateException("Cannot process " + info.bitsPerSample + "-bit PCM with padding!"); + } - for (int i = 0; i < sampleCount; i++) { - buffer[i] = nioBuffer.get(); + readChunkToBuffer(blockCount); - if (++indexInBlock == info.channelCount) { - nioBuffer.position(nioBuffer.position() + padding); - indexInBlock = 0; - } - } + int padding = info.getPadding() / 2; + int sampleCount = blockCount * info.channelCount; + int indexInBlock = 0; - downstream.process(buffer, 0, blockCount * info.channelCount); - } + for (int i = 0; i < sampleCount; i++) { + buffer[i] = nioBuffer.get(); - private void processChunk(int blockCount) throws IOException, InterruptedException { - int sampleCount = readChunkToBuffer(blockCount); + if (++indexInBlock == info.channelCount) { + nioBuffer.position(nioBuffer.position() + padding); + indexInBlock = 0; + } + } - if (info.bitsPerSample == 16) { - downstream.process(nioBuffer); - } else if (info.bitsPerSample == 24) { - short[] samples = new short[sampleCount]; + downstream.process(buffer, 0, blockCount * info.channelCount); + } + + private void processChunk(int blockCount) throws IOException, InterruptedException { + int sampleCount = readChunkToBuffer(blockCount); + + if (info.bitsPerSample == 16) { + downstream.process(nioBuffer); + } else if (info.bitsPerSample == 24) { + short[] samples = new short[sampleCount]; - for (int i = 0; i < sampleCount; i++) { - samples[i] = (short) (byteBuffer.get((i * 3) + 2) << 8 | byteBuffer.get((i * 3) + 1) & 0xFF); - } + for (int i = 0; i < sampleCount; i++) { + samples[i] = (short) (byteBuffer.get((i * 3) + 2) << 8 | byteBuffer.get((i * 3) + 1) & 0xFF); + } - downstream.process(samples, 0, sampleCount); + downstream.process(samples, 0, sampleCount); + } } - } - private int readChunkToBuffer(int blockCount) throws IOException { - int bytesPerSample = info.bitsPerSample >> 3; - int bytesToRead = blockCount * info.blockAlign; - dataInput.readFully(rawBuffer, 0, bytesToRead); + private int readChunkToBuffer(int blockCount) throws IOException { + int bytesPerSample = info.bitsPerSample >> 3; + int bytesToRead = blockCount * info.blockAlign; + dataInput.readFully(rawBuffer, 0, bytesToRead); - byteBuffer.position(0); - nioBuffer.position(0); - nioBuffer.limit(bytesToRead / bytesPerSample); + byteBuffer.position(0); + nioBuffer.position(0); + nioBuffer.limit(bytesToRead / bytesPerSample); - return bytesToRead / bytesPerSample; - } + return bytesToRead / bytesPerSample; + } - private int getNextChunkBlocks() { - long endOffset = info.startOffset + info.blockAlign * info.blockCount; - return (int) Math.min((endOffset - inputStream.getPosition()) / info.blockAlign, BLOCKS_IN_BUFFER); - } + private int getNextChunkBlocks() { + long endOffset = info.startOffset + info.blockAlign * info.blockCount; + return (int) Math.min((endOffset - inputStream.getPosition()) / info.blockAlign, BLOCKS_IN_BUFFER); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioFilter.java index f85ff0c50..c3169913a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioFilter.java @@ -4,24 +4,24 @@ * A filter for audio samples */ public interface AudioFilter { - /** - * Indicates that the next samples are not a continuation from the previous ones and gives the timecode for the - * next incoming sample. - * - * @param requestedTime Timecode in milliseconds to which the seek was requested to - * @param providedTime Timecode in milliseconds to which the seek was actually performed to - */ - void seekPerformed(long requestedTime, long providedTime); + /** + * Indicates that the next samples are not a continuation from the previous ones and gives the timecode for the + * next incoming sample. + * + * @param requestedTime Timecode in milliseconds to which the seek was requested to + * @param providedTime Timecode in milliseconds to which the seek was actually performed to + */ + void seekPerformed(long requestedTime, long providedTime); - /** - * Flush everything to output. - * - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void flush() throws InterruptedException; + /** + * Flush everything to output. + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void flush() throws InterruptedException; - /** - * Free all resources. No more input is expected. - */ - void close(); + /** + * Free all resources. No more input is expected. + */ + void close(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioFilterChain.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioFilterChain.java index 5764e8da1..d20785b45 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioFilterChain.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioFilterChain.java @@ -6,30 +6,30 @@ * A chain of audio filters. */ public class AudioFilterChain { - /** - * The first filter in the stream. Separate field as unlike other filters, this must be an instance of - * {@link UniversalPcmAudioFilter} as the input data may be in any representation. - */ - public final UniversalPcmAudioFilter input; + /** + * The first filter in the stream. Separate field as unlike other filters, this must be an instance of + * {@link UniversalPcmAudioFilter} as the input data may be in any representation. + */ + public final UniversalPcmAudioFilter input; - /** - * All filters in this chain. - */ - public final List filters; + /** + * All filters in this chain. + */ + public final List filters; - /** - * Immutable context/configuration instance that this filter was generated from. May be null. - */ - public final Object context; + /** + * Immutable context/configuration instance that this filter was generated from. May be null. + */ + public final Object context; - /** - * @param input See {@link #input}. - * @param filters See {@link #filters}. - * @param context See {@link #context}. - */ - public AudioFilterChain(UniversalPcmAudioFilter input, List filters, Object context) { - this.input = input; - this.filters = filters; - this.context = context; - } + /** + * @param input See {@link #input}. + * @param filters See {@link #filters}. + * @param context See {@link #context}. + */ + public AudioFilterChain(UniversalPcmAudioFilter input, List filters, Object context) { + this.input = input; + this.filters = filters; + this.context = context; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioPipeline.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioPipeline.java index fb45fb03a..3ca10888b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioPipeline.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioPipeline.java @@ -7,39 +7,39 @@ * Represents an audio pipeline (top-level audio filter chain). */ public class AudioPipeline extends CompositeAudioFilter { - private final List filters; - private final UniversalPcmAudioFilter first; - - /** - * @param chain The top-level filter chain. - */ - public AudioPipeline(AudioFilterChain chain) { - this.filters = chain.filters; - this.first = chain.input; - } - - @Override - public void process(float[][] input, int offset, int length) throws InterruptedException { - first.process(input, offset, length); - } - - @Override - public void process(short[] input, int offset, int length) throws InterruptedException { - first.process(input, offset, length); - } - - @Override - public void process(ShortBuffer buffer) throws InterruptedException { - first.process(buffer); - } - - @Override - public void process(short[][] input, int offset, int length) throws InterruptedException { - first.process(input, offset, length); - } - - @Override - protected List getFilters() { - return filters; - } + private final List filters; + private final UniversalPcmAudioFilter first; + + /** + * @param chain The top-level filter chain. + */ + public AudioPipeline(AudioFilterChain chain) { + this.filters = chain.filters; + this.first = chain.input; + } + + @Override + public void process(float[][] input, int offset, int length) throws InterruptedException { + first.process(input, offset, length); + } + + @Override + public void process(short[] input, int offset, int length) throws InterruptedException { + first.process(input, offset, length); + } + + @Override + public void process(ShortBuffer buffer) throws InterruptedException { + first.process(buffer); + } + + @Override + public void process(short[][] input, int offset, int length) throws InterruptedException { + first.process(input, offset, length); + } + + @Override + protected List getFilters() { + return filters; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioPipelineFactory.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioPipelineFactory.java index 29b0cae3f..d2de8b4f0 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioPipelineFactory.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioPipelineFactory.java @@ -12,55 +12,55 @@ * Factory for audio pipelines. Contains helper methods to determine whether an audio pipeline is even required. */ public class AudioPipelineFactory { - /** - * @param context Audio processing context to check output format from - * @param inputFormat Input format of the audio - * @return True if no audio processing is currently required with this context and input format combination. - */ - public static boolean isProcessingRequired(AudioProcessingContext context, AudioDataFormat inputFormat) { - return !context.outputFormat.equals(inputFormat) || context.playerOptions.volumeLevel.get() != 100 || - context.playerOptions.filterFactory.get() != null; - } + /** + * @param context Audio processing context to check output format from + * @param inputFormat Input format of the audio + * @return True if no audio processing is currently required with this context and input format combination. + */ + public static boolean isProcessingRequired(AudioProcessingContext context, AudioDataFormat inputFormat) { + return !context.outputFormat.equals(inputFormat) || context.playerOptions.volumeLevel.get() != 100 || + context.playerOptions.filterFactory.get() != null; + } - /** - * Creates an audio pipeline instance based on provided settings. - * - * @param context Configuration and output information for processing - * @param inputFormat The parameters of the PCM input. - * @return A pipeline which delivers the input to the final frame destination. - */ - public static AudioPipeline create(AudioProcessingContext context, PcmFormat inputFormat) { - int inputChannels = inputFormat.channelCount; - int outputChannels = context.outputFormat.channelCount; + /** + * Creates an audio pipeline instance based on provided settings. + * + * @param context Configuration and output information for processing + * @param inputFormat The parameters of the PCM input. + * @return A pipeline which delivers the input to the final frame destination. + */ + public static AudioPipeline create(AudioProcessingContext context, PcmFormat inputFormat) { + int inputChannels = inputFormat.channelCount; + int outputChannels = context.outputFormat.channelCount; - UniversalPcmAudioFilter end = new FinalPcmAudioFilter(context, createPostProcessors(context)); - FilterChainBuilder builder = new FilterChainBuilder(); - builder.addFirst(end); + UniversalPcmAudioFilter end = new FinalPcmAudioFilter(context, createPostProcessors(context)); + FilterChainBuilder builder = new FilterChainBuilder(); + builder.addFirst(end); - if (context.filterHotSwapEnabled || context.playerOptions.filterFactory.get() != null) { - UserProvidedAudioFilters userFilters = new UserProvidedAudioFilters(context, end); - builder.addFirst(userFilters); - } + if (context.filterHotSwapEnabled || context.playerOptions.filterFactory.get() != null) { + UserProvidedAudioFilters userFilters = new UserProvidedAudioFilters(context, end); + builder.addFirst(userFilters); + } - if (inputFormat.sampleRate != context.outputFormat.sampleRate) { - builder.addFirst(new ResamplingPcmAudioFilter(context.configuration, outputChannels, - builder.makeFirstFloat(outputChannels), inputFormat.sampleRate, context.outputFormat.sampleRate)); - } + if (inputFormat.sampleRate != context.outputFormat.sampleRate) { + builder.addFirst(new ResamplingPcmAudioFilter(context.configuration, outputChannels, + builder.makeFirstFloat(outputChannels), inputFormat.sampleRate, context.outputFormat.sampleRate)); + } - if (inputChannels != outputChannels) { - builder.addFirst(new ChannelCountPcmAudioFilter(inputChannels, outputChannels, - builder.makeFirstUniversal(outputChannels))); - } + if (inputChannels != outputChannels) { + builder.addFirst(new ChannelCountPcmAudioFilter(inputChannels, outputChannels, + builder.makeFirstUniversal(outputChannels))); + } - return new AudioPipeline(builder.build(null, inputChannels)); - } + return new AudioPipeline(builder.build(null, inputChannels)); + } - private static Collection createPostProcessors(AudioProcessingContext context) { - AudioChunkEncoder chunkEncoder = context.outputFormat.createEncoder(context.configuration); + private static Collection createPostProcessors(AudioProcessingContext context) { + AudioChunkEncoder chunkEncoder = context.outputFormat.createEncoder(context.configuration); - return Arrays.asList( - new VolumePostProcessor(context), - new BufferingPostProcessor(context, chunkEncoder) - ); - } + return Arrays.asList( + new VolumePostProcessor(context), + new BufferingPostProcessor(context, chunkEncoder) + ); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioPostProcessor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioPostProcessor.java index 75ecab501..32e496804 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioPostProcessor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/AudioPostProcessor.java @@ -6,18 +6,18 @@ * Audio chunk post processor. */ public interface AudioPostProcessor { - /** - * Receives chunk buffer in its final PCM format with the sample count, sample rate and channel count matching that of - * the output format. - * - * @param timecode Absolute starting timecode of the chunk in milliseconds - * @param buffer PCM buffer of samples in the chunk - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void process(long timecode, ShortBuffer buffer) throws InterruptedException; + /** + * Receives chunk buffer in its final PCM format with the sample count, sample rate and channel count matching that of + * the output format. + * + * @param timecode Absolute starting timecode of the chunk in milliseconds + * @param buffer PCM buffer of samples in the chunk + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void process(long timecode, ShortBuffer buffer) throws InterruptedException; - /** - * Frees up all resources this processor is holding internally. - */ - void close(); + /** + * Frees up all resources this processor is holding internally. + */ + void close(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/BufferingPostProcessor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/BufferingPostProcessor.java index 893eb8eb1..a2fbf4577 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/BufferingPostProcessor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/BufferingPostProcessor.java @@ -11,38 +11,38 @@ * Post processor which encodes audio chunks and passes them as audio frames to the frame buffer. */ public class BufferingPostProcessor implements AudioPostProcessor { - private final AudioProcessingContext context; - private final AudioChunkEncoder encoder; - private final MutableAudioFrame offeredFrame; - private final ByteBuffer outputBuffer; - - /** - * @param context Processing context to determine the destination buffer from. - * @param encoder Encoder to encode the chunk with. - */ - public BufferingPostProcessor(AudioProcessingContext context, AudioChunkEncoder encoder) { - this.encoder = encoder; - this.context = context; - this.offeredFrame = new MutableAudioFrame(); - this.outputBuffer = ByteBuffer.allocateDirect(context.outputFormat.maximumChunkSize()); - - offeredFrame.setFormat(context.outputFormat); - } - - @Override - public void process(long timecode, ShortBuffer buffer) throws InterruptedException { - outputBuffer.clear(); - encoder.encode(buffer, outputBuffer); - - offeredFrame.setTimecode(timecode); - offeredFrame.setVolume(context.playerOptions.volumeLevel.get()); - - offeredFrame.setBuffer(outputBuffer); - context.frameBuffer.consume(offeredFrame); - } - - @Override - public void close() { - encoder.close(); - } + private final AudioProcessingContext context; + private final AudioChunkEncoder encoder; + private final MutableAudioFrame offeredFrame; + private final ByteBuffer outputBuffer; + + /** + * @param context Processing context to determine the destination buffer from. + * @param encoder Encoder to encode the chunk with. + */ + public BufferingPostProcessor(AudioProcessingContext context, AudioChunkEncoder encoder) { + this.encoder = encoder; + this.context = context; + this.offeredFrame = new MutableAudioFrame(); + this.outputBuffer = ByteBuffer.allocateDirect(context.outputFormat.maximumChunkSize()); + + offeredFrame.setFormat(context.outputFormat); + } + + @Override + public void process(long timecode, ShortBuffer buffer) throws InterruptedException { + outputBuffer.clear(); + encoder.encode(buffer, outputBuffer); + + offeredFrame.setTimecode(timecode); + offeredFrame.setVolume(context.playerOptions.volumeLevel.get()); + + offeredFrame.setBuffer(outputBuffer); + context.frameBuffer.consume(offeredFrame); + } + + @Override + public void close() { + encoder.close(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/ChannelCountPcmAudioFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/ChannelCountPcmAudioFilter.java index 2b22a744e..5ad3823b2 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/ChannelCountPcmAudioFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/ChannelCountPcmAudioFilter.java @@ -5,145 +5,145 @@ /** * For short PCM buffers, guarantees that the output has the required number of channels and that no outgoing * buffer contains any partial frames. - * + *

* For example if the input is three channels, and output is two channels, then: * in [0, 1, 2, 0, 1, 2, 0, 1] out [0, 1, 0, 1] saved [0, 1] * in [2, 0, 1, 2] out [0, 1, 0, 1] saved [] */ public class ChannelCountPcmAudioFilter implements UniversalPcmAudioFilter { - private final UniversalPcmAudioFilter downstream; - private final int outputChannels; - private final ShortBuffer outputBuffer; - private final int inputChannels; - private final int commonChannels; - private final int channelsToAdd; - private final short[] inputSet; - private final float[][] splitFloatOutput; - private final short[][] splitShortOutput; - private int inputIndex; - - /** - * @param inputChannels Number of input channels - * @param outputChannels Number of output channels - * @param downstream The next filter in line - */ - public ChannelCountPcmAudioFilter(int inputChannels, int outputChannels, UniversalPcmAudioFilter downstream) { - this.downstream = downstream; - this.inputChannels = inputChannels; - this.outputChannels = outputChannels; - this.outputBuffer = ShortBuffer.allocate(2048 * inputChannels); - this.commonChannels = Math.min(outputChannels, inputChannels); - this.channelsToAdd = outputChannels - commonChannels; - this.inputSet = new short[inputChannels]; - this.splitFloatOutput = new float[outputChannels][]; - this.splitShortOutput = new short[outputChannels][]; - this.inputIndex = 0; - } - - @Override - public void process(short[] input, int offset, int length) throws InterruptedException { - if (canPassThrough(length)) { - downstream.process(input, offset, length); - } else { - if (inputChannels == 1 && outputChannels == 2) { - processMonoToStereo(ShortBuffer.wrap(input, offset, length)); - } else { - processNormalizer(ShortBuffer.wrap(input, offset, length)); - } + private final UniversalPcmAudioFilter downstream; + private final int outputChannels; + private final ShortBuffer outputBuffer; + private final int inputChannels; + private final int commonChannels; + private final int channelsToAdd; + private final short[] inputSet; + private final float[][] splitFloatOutput; + private final short[][] splitShortOutput; + private int inputIndex; + + /** + * @param inputChannels Number of input channels + * @param outputChannels Number of output channels + * @param downstream The next filter in line + */ + public ChannelCountPcmAudioFilter(int inputChannels, int outputChannels, UniversalPcmAudioFilter downstream) { + this.downstream = downstream; + this.inputChannels = inputChannels; + this.outputChannels = outputChannels; + this.outputBuffer = ShortBuffer.allocate(2048 * inputChannels); + this.commonChannels = Math.min(outputChannels, inputChannels); + this.channelsToAdd = outputChannels - commonChannels; + this.inputSet = new short[inputChannels]; + this.splitFloatOutput = new float[outputChannels][]; + this.splitShortOutput = new short[outputChannels][]; + this.inputIndex = 0; } - } - - @Override - public void process(ShortBuffer buffer) throws InterruptedException { - if (canPassThrough(buffer.remaining())) { - downstream.process(buffer); - } else { - if (inputChannels == 1 && outputChannels == 2) { - processMonoToStereo(buffer); - } else { - processNormalizer(buffer); - } + + @Override + public void process(short[] input, int offset, int length) throws InterruptedException { + if (canPassThrough(length)) { + downstream.process(input, offset, length); + } else { + if (inputChannels == 1 && outputChannels == 2) { + processMonoToStereo(ShortBuffer.wrap(input, offset, length)); + } else { + processNormalizer(ShortBuffer.wrap(input, offset, length)); + } + } } - } - private void processNormalizer(ShortBuffer buffer) throws InterruptedException { - while (buffer.hasRemaining()) { - inputSet[inputIndex++] = buffer.get(); + @Override + public void process(ShortBuffer buffer) throws InterruptedException { + if (canPassThrough(buffer.remaining())) { + downstream.process(buffer); + } else { + if (inputChannels == 1 && outputChannels == 2) { + processMonoToStereo(buffer); + } else { + processNormalizer(buffer); + } + } + } - if (inputIndex == inputChannels) { - outputBuffer.put(inputSet, 0, commonChannels); + private void processNormalizer(ShortBuffer buffer) throws InterruptedException { + while (buffer.hasRemaining()) { + inputSet[inputIndex++] = buffer.get(); - for (int i = 0; i < channelsToAdd; i++) { - outputBuffer.put(inputSet[0]); - } + if (inputIndex == inputChannels) { + outputBuffer.put(inputSet, 0, commonChannels); - if (!outputBuffer.hasRemaining()) { - outputBuffer.flip(); - downstream.process(outputBuffer); - outputBuffer.clear(); - } + for (int i = 0; i < channelsToAdd; i++) { + outputBuffer.put(inputSet[0]); + } - inputIndex = 0; - } + if (!outputBuffer.hasRemaining()) { + outputBuffer.flip(); + downstream.process(outputBuffer); + outputBuffer.clear(); + } + + inputIndex = 0; + } + } } - } - private void processMonoToStereo(ShortBuffer buffer) throws InterruptedException { - while (buffer.hasRemaining()) { - short sample = buffer.get(); - outputBuffer.put(sample); - outputBuffer.put(sample); + private void processMonoToStereo(ShortBuffer buffer) throws InterruptedException { + while (buffer.hasRemaining()) { + short sample = buffer.get(); + outputBuffer.put(sample); + outputBuffer.put(sample); + + if (!outputBuffer.hasRemaining()) { + outputBuffer.flip(); + downstream.process(outputBuffer); + outputBuffer.clear(); + } + } + } - if (!outputBuffer.hasRemaining()) { - outputBuffer.flip(); - downstream.process(outputBuffer); - outputBuffer.clear(); - } + private boolean canPassThrough(int length) { + return inputIndex == 0 && inputChannels == outputChannels && (length % inputChannels) == 0; } - } - private boolean canPassThrough(int length) { - return inputIndex == 0 && inputChannels == outputChannels && (length % inputChannels) == 0; - } + @Override + public void process(float[][] input, int offset, int length) throws InterruptedException { + for (int i = 0; i < commonChannels; i++) { + splitFloatOutput[i] = input[i]; + } - @Override - public void process(float[][] input, int offset, int length) throws InterruptedException { - for (int i = 0; i < commonChannels; i++) { - splitFloatOutput[i] = input[i]; - } + for (int i = commonChannels; i < outputChannels; i++) { + splitFloatOutput[i] = input[0]; + } - for (int i = commonChannels; i < outputChannels; i++) { - splitFloatOutput[i] = input[0]; + downstream.process(splitFloatOutput, offset, length); } - downstream.process(splitFloatOutput, offset, length); - } + @Override + public void process(short[][] input, int offset, int length) throws InterruptedException { + for (int i = 0; i < commonChannels; i++) { + splitShortOutput[i] = input[i]; + } - @Override - public void process(short[][] input, int offset, int length) throws InterruptedException { - for (int i = 0; i < commonChannels; i++) { - splitShortOutput[i] = input[i]; - } + for (int i = commonChannels; i < outputChannels; i++) { + splitShortOutput[i] = input[0]; + } - for (int i = commonChannels; i < outputChannels; i++) { - splitShortOutput[i] = input[0]; + downstream.process(splitShortOutput, offset, length); } - downstream.process(splitShortOutput, offset, length); - } - - @Override - public void seekPerformed(long requestedTime, long providedTime) { - outputBuffer.clear(); - } + @Override + public void seekPerformed(long requestedTime, long providedTime) { + outputBuffer.clear(); + } - @Override - public void flush() throws InterruptedException { - // Nothing to do. - } + @Override + public void flush() throws InterruptedException { + // Nothing to do. + } - @Override - public void close() { - // Nothing to do. - } + @Override + public void close() { + // Nothing to do. + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/CompositeAudioFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/CompositeAudioFilter.java index b5e438fa3..7ee80b63a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/CompositeAudioFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/CompositeAudioFilter.java @@ -9,40 +9,40 @@ * An audio filter which may consist of a number of other filters. */ public abstract class CompositeAudioFilter implements UniversalPcmAudioFilter { - private static final Logger log = LoggerFactory.getLogger(CompositeAudioFilter.class); + private static final Logger log = LoggerFactory.getLogger(CompositeAudioFilter.class); - @Override - public void seekPerformed(long requestedTime, long providedTime) { - for (AudioFilter filter : getFilters()) { - try { - filter.seekPerformed(requestedTime, providedTime); - } catch (Exception e) { - log.error("Notifying filter {} of seek failed with exception.", filter.getClass(), e); - } + @Override + public void seekPerformed(long requestedTime, long providedTime) { + for (AudioFilter filter : getFilters()) { + try { + filter.seekPerformed(requestedTime, providedTime); + } catch (Exception e) { + log.error("Notifying filter {} of seek failed with exception.", filter.getClass(), e); + } + } } - } - @Override - public void flush() throws InterruptedException { - for (AudioFilter filter : getFilters()) { - try { - filter.flush(); - } catch (Exception e) { - log.error("Flushing filter {} failed with exception.", filter.getClass(), e); - } + @Override + public void flush() throws InterruptedException { + for (AudioFilter filter : getFilters()) { + try { + filter.flush(); + } catch (Exception e) { + log.error("Flushing filter {} failed with exception.", filter.getClass(), e); + } + } } - } - @Override - public void close() { - for (AudioFilter filter : getFilters()) { - try { - filter.close(); - } catch (Exception e) { - log.error("Closing filter {} failed with exception.", filter.getClass(), e); - } + @Override + public void close() { + for (AudioFilter filter : getFilters()) { + try { + filter.close(); + } catch (Exception e) { + log.error("Closing filter {} failed with exception.", filter.getClass(), e); + } + } } - } - protected abstract List getFilters(); + protected abstract List getFilters(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/FilterChainBuilder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/FilterChainBuilder.java index 985de84d4..4452c2a8d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/FilterChainBuilder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/FilterChainBuilder.java @@ -4,82 +4,83 @@ import com.sedmelluq.discord.lavaplayer.filter.converter.ToShortAudioFilter; import com.sedmelluq.discord.lavaplayer.filter.converter.ToSplitShortAudioFilter; -import java.util.*; +import java.util.ArrayList; +import java.util.List; /** * Builder for audio filter chains. */ public class FilterChainBuilder { - private final List filters = new ArrayList<>(); + private final List filters = new ArrayList<>(); - /** - * @param filter The filter to add as the first one in the chain. - */ - public void addFirst(AudioFilter filter) { - filters.add(filter); - } + /** + * @param filter The filter to add as the first one in the chain. + */ + public void addFirst(AudioFilter filter) { + filters.add(filter); + } - /** - * @return The first chain in the filter. - */ - public AudioFilter first() { - return filters.get(filters.size() - 1); - } + /** + * @return The first chain in the filter. + */ + public AudioFilter first() { + return filters.get(filters.size() - 1); + } - /** - * @param channelCount Number of input channels expected by the current head of the chain. - * @return The first chain in the filter as a float PCM filter, or if it is not, then adds an adapter filter to the - * beginning and returns that. - */ - public FloatPcmAudioFilter makeFirstFloat(int channelCount) { - AudioFilter first = first(); + /** + * @param channelCount Number of input channels expected by the current head of the chain. + * @return The first chain in the filter as a float PCM filter, or if it is not, then adds an adapter filter to the + * beginning and returns that. + */ + public FloatPcmAudioFilter makeFirstFloat(int channelCount) { + AudioFilter first = first(); - if (first instanceof FloatPcmAudioFilter) { - return (FloatPcmAudioFilter) first; - } else { - return prependUniversalFilter(first, channelCount); + if (first instanceof FloatPcmAudioFilter) { + return (FloatPcmAudioFilter) first; + } else { + return prependUniversalFilter(first, channelCount); + } } - } - /** - * @param channelCount Number of input channels expected by the current head of the chain. - * @return The first chain in the filter as an universal PCM filter, or if it is not, then adds an adapter filter to - * the beginning and returns that. - */ - public UniversalPcmAudioFilter makeFirstUniversal(int channelCount) { - AudioFilter first = first(); + /** + * @param channelCount Number of input channels expected by the current head of the chain. + * @return The first chain in the filter as an universal PCM filter, or if it is not, then adds an adapter filter to + * the beginning and returns that. + */ + public UniversalPcmAudioFilter makeFirstUniversal(int channelCount) { + AudioFilter first = first(); - if (first instanceof UniversalPcmAudioFilter) { - return (UniversalPcmAudioFilter) first; - } else { - return prependUniversalFilter(first, channelCount); + if (first instanceof UniversalPcmAudioFilter) { + return (UniversalPcmAudioFilter) first; + } else { + return prependUniversalFilter(first, channelCount); + } } - } - /** - * @param context See {@link AudioFilterChain#context}. - * @param channelCount Number of input channels expected by the current head of the chain. - * @return The built filter chain. Adds an adapter to the beginning of the chain if the first filter is not universal. - */ - public AudioFilterChain build(Object context, int channelCount) { - UniversalPcmAudioFilter firstFilter = makeFirstUniversal(channelCount); - return new AudioFilterChain(firstFilter, filters, context); - } + /** + * @param context See {@link AudioFilterChain#context}. + * @param channelCount Number of input channels expected by the current head of the chain. + * @return The built filter chain. Adds an adapter to the beginning of the chain if the first filter is not universal. + */ + public AudioFilterChain build(Object context, int channelCount) { + UniversalPcmAudioFilter firstFilter = makeFirstUniversal(channelCount); + return new AudioFilterChain(firstFilter, filters, context); + } - private UniversalPcmAudioFilter prependUniversalFilter(AudioFilter first, int channelCount) { - UniversalPcmAudioFilter universalInput; + private UniversalPcmAudioFilter prependUniversalFilter(AudioFilter first, int channelCount) { + UniversalPcmAudioFilter universalInput; - if (first instanceof SplitShortPcmAudioFilter) { - universalInput = new ToSplitShortAudioFilter((SplitShortPcmAudioFilter) first, channelCount); - } else if (first instanceof FloatPcmAudioFilter) { - universalInput = new ToFloatAudioFilter((FloatPcmAudioFilter) first, channelCount); - } else if (first instanceof ShortPcmAudioFilter) { - universalInput = new ToShortAudioFilter((ShortPcmAudioFilter) first, channelCount); - } else { - throw new RuntimeException("Filter must implement at least one data type."); - } + if (first instanceof SplitShortPcmAudioFilter) { + universalInput = new ToSplitShortAudioFilter((SplitShortPcmAudioFilter) first, channelCount); + } else if (first instanceof FloatPcmAudioFilter) { + universalInput = new ToFloatAudioFilter((FloatPcmAudioFilter) first, channelCount); + } else if (first instanceof ShortPcmAudioFilter) { + universalInput = new ToShortAudioFilter((ShortPcmAudioFilter) first, channelCount); + } else { + throw new RuntimeException("Filter must implement at least one data type."); + } - addFirst(universalInput); - return universalInput; - } + addFirst(universalInput); + return universalInput; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/FinalPcmAudioFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/FinalPcmAudioFilter.java index ad90814bc..a3f3375dc 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/FinalPcmAudioFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/FinalPcmAudioFilter.java @@ -14,153 +14,153 @@ * Collects buffers of the required chunk size and passes them on to audio post processors. */ public class FinalPcmAudioFilter implements UniversalPcmAudioFilter { - private static final Logger log = LoggerFactory.getLogger(FinalPcmAudioFilter.class); - private static final short[] zeroPadding = new short[128]; - - private final AudioDataFormat format; - private final ShortBuffer frameBuffer; - private final Collection postProcessors; - - private long ignoredFrames; - private long timecodeBase; - private long timecodeSampleOffset; - - /** - * @param context Configuration and output information for processing - * @param postProcessors Post processors to pass the final audio buffers to - */ - public FinalPcmAudioFilter(AudioProcessingContext context, Collection postProcessors) { - this.format = context.outputFormat; - this.frameBuffer = ByteBuffer - .allocateDirect(format.totalSampleCount() * 2) - .order(ByteOrder.nativeOrder()) - .asShortBuffer(); - this.postProcessors = postProcessors; - - timecodeBase = 0; - timecodeSampleOffset = 0; - } - - private short decodeSample(float sample) { - return (short) Math.min(Math.max((int)(sample * 32768.f), -32768), 32767); - } - - @Override - public void seekPerformed(long requestedTime, long providedTime) { - frameBuffer.clear(); - ignoredFrames = requestedTime > providedTime ? (requestedTime - providedTime) * format.channelCount * format.sampleRate / 1000L : 0; - timecodeBase = Math.max(requestedTime, providedTime); - timecodeSampleOffset = 0; - - if (ignoredFrames > 0) { - log.debug("Ignoring {} frames due to inaccurate seek (requested {}, provided {}).", ignoredFrames, requestedTime, providedTime); + private static final Logger log = LoggerFactory.getLogger(FinalPcmAudioFilter.class); + private static final short[] zeroPadding = new short[128]; + + private final AudioDataFormat format; + private final ShortBuffer frameBuffer; + private final Collection postProcessors; + + private long ignoredFrames; + private long timecodeBase; + private long timecodeSampleOffset; + + /** + * @param context Configuration and output information for processing + * @param postProcessors Post processors to pass the final audio buffers to + */ + public FinalPcmAudioFilter(AudioProcessingContext context, Collection postProcessors) { + this.format = context.outputFormat; + this.frameBuffer = ByteBuffer + .allocateDirect(format.totalSampleCount() * 2) + .order(ByteOrder.nativeOrder()) + .asShortBuffer(); + this.postProcessors = postProcessors; + + timecodeBase = 0; + timecodeSampleOffset = 0; } - } - @Override - public void flush() throws InterruptedException { - if (frameBuffer.position() > 0) { - fillFrameBuffer(); - dispatch(); + private short decodeSample(float sample) { + return (short) Math.min(Math.max((int) (sample * 32768.f), -32768), 32767); } - } - @Override - public void close() { - for (AudioPostProcessor postProcessor : postProcessors) { - postProcessor.close(); - } - } + @Override + public void seekPerformed(long requestedTime, long providedTime) { + frameBuffer.clear(); + ignoredFrames = requestedTime > providedTime ? (requestedTime - providedTime) * format.channelCount * format.sampleRate / 1000L : 0; + timecodeBase = Math.max(requestedTime, providedTime); + timecodeSampleOffset = 0; - private void fillFrameBuffer() { - while (frameBuffer.remaining() >= zeroPadding.length) { - frameBuffer.put(zeroPadding); + if (ignoredFrames > 0) { + log.debug("Ignoring {} frames due to inaccurate seek (requested {}, provided {}).", ignoredFrames, requestedTime, providedTime); + } } - while (frameBuffer.remaining() > 0) { - frameBuffer.put((short) 0); + @Override + public void flush() throws InterruptedException { + if (frameBuffer.position() > 0) { + fillFrameBuffer(); + dispatch(); + } } - } - - @Override - public void process(short[] input, int offset, int length) throws InterruptedException { - for (int i = 0; i < length; i++) { - if (ignoredFrames > 0) { - ignoredFrames--; - } else { - frameBuffer.put(input[offset + i]); - - dispatch(); - } + + @Override + public void close() { + for (AudioPostProcessor postProcessor : postProcessors) { + postProcessor.close(); + } } - } - @Override - public void process(short[][] input, int offset, int length) throws InterruptedException { - int secondChannelIndex = Math.min(1, input.length - 1); + private void fillFrameBuffer() { + while (frameBuffer.remaining() >= zeroPadding.length) { + frameBuffer.put(zeroPadding); + } - for (int i = 0; i < length; i++) { - if (ignoredFrames > 0) { - ignoredFrames -= format.channelCount; - } else { - frameBuffer.put(input[0][offset + i]); - frameBuffer.put(input[secondChannelIndex][offset + i]); + while (frameBuffer.remaining() > 0) { + frameBuffer.put((short) 0); + } + } - dispatch(); - } + @Override + public void process(short[] input, int offset, int length) throws InterruptedException { + for (int i = 0; i < length; i++) { + if (ignoredFrames > 0) { + ignoredFrames--; + } else { + frameBuffer.put(input[offset + i]); + + dispatch(); + } + } } - } - - @Override - public void process(ShortBuffer buffer) throws InterruptedException { - if (ignoredFrames > 0) { - long skipped = Math.min(buffer.remaining(), ignoredFrames); - buffer.position(buffer.position() + (int) skipped); - ignoredFrames -= skipped; + + @Override + public void process(short[][] input, int offset, int length) throws InterruptedException { + int secondChannelIndex = Math.min(1, input.length - 1); + + for (int i = 0; i < length; i++) { + if (ignoredFrames > 0) { + ignoredFrames -= format.channelCount; + } else { + frameBuffer.put(input[0][offset + i]); + frameBuffer.put(input[secondChannelIndex][offset + i]); + + dispatch(); + } + } } - ShortBuffer local = buffer.duplicate(); + @Override + public void process(ShortBuffer buffer) throws InterruptedException { + if (ignoredFrames > 0) { + long skipped = Math.min(buffer.remaining(), ignoredFrames); + buffer.position(buffer.position() + (int) skipped); + ignoredFrames -= skipped; + } + + ShortBuffer local = buffer.duplicate(); - while (buffer.remaining() > 0) { - int chunk = Math.min(buffer.remaining(), frameBuffer.remaining()); - local.position(buffer.position()); - local.limit(local.position() + chunk); + while (buffer.remaining() > 0) { + int chunk = Math.min(buffer.remaining(), frameBuffer.remaining()); + local.position(buffer.position()); + local.limit(local.position() + chunk); - frameBuffer.put(local); - dispatch(); + frameBuffer.put(local); + dispatch(); - buffer.position(buffer.position() + chunk); + buffer.position(buffer.position() + chunk); + } } - } - @Override - public void process(float[][] buffer, int offset, int length) throws InterruptedException { - int secondChannelIndex = Math.min(1, buffer.length - 1); + @Override + public void process(float[][] buffer, int offset, int length) throws InterruptedException { + int secondChannelIndex = Math.min(1, buffer.length - 1); - for (int i = 0; i < length; i++) { - if (ignoredFrames > 0) { - ignoredFrames -= 2; - } else { - frameBuffer.put(decodeSample(buffer[0][offset + i])); - frameBuffer.put(decodeSample(buffer[secondChannelIndex][offset + i])); + for (int i = 0; i < length; i++) { + if (ignoredFrames > 0) { + ignoredFrames -= 2; + } else { + frameBuffer.put(decodeSample(buffer[0][offset + i])); + frameBuffer.put(decodeSample(buffer[secondChannelIndex][offset + i])); - dispatch(); - } + dispatch(); + } + } } - } - private void dispatch() throws InterruptedException { - if (!frameBuffer.hasRemaining()) { - long timecode = timecodeBase + timecodeSampleOffset * 1000 / format.sampleRate; - frameBuffer.clear(); + private void dispatch() throws InterruptedException { + if (!frameBuffer.hasRemaining()) { + long timecode = timecodeBase + timecodeSampleOffset * 1000 / format.sampleRate; + frameBuffer.clear(); - for (AudioPostProcessor postProcessor : postProcessors) { - postProcessor.process(timecode, frameBuffer); - } + for (AudioPostProcessor postProcessor : postProcessors) { + postProcessor.process(timecode, frameBuffer); + } - frameBuffer.clear(); + frameBuffer.clear(); - timecodeSampleOffset += format.chunkSampleCount; + timecodeSampleOffset += format.chunkSampleCount; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/FloatPcmAudioFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/FloatPcmAudioFilter.java index e8cb8afd3..68dfcf1e5 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/FloatPcmAudioFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/FloatPcmAudioFilter.java @@ -4,11 +4,11 @@ * Audio filter which accepts floating point PCM samples. */ public interface FloatPcmAudioFilter extends AudioFilter { - /** - * @param input An array of samples for each channel - * @param offset Offset in the arrays to start at - * @param length Length of the target sequence in arrays - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void process(float[][] input, int offset, int length) throws InterruptedException; + /** + * @param input An array of samples for each channel + * @param offset Offset in the arrays to start at + * @param length Length of the target sequence in arrays + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void process(float[][] input, int offset, int length) throws InterruptedException; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/PcmFilterFactory.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/PcmFilterFactory.java index f6a07c82b..fb7d22f7d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/PcmFilterFactory.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/PcmFilterFactory.java @@ -9,17 +9,17 @@ * Factory for custom PCM filters. */ public interface PcmFilterFactory { - /** - * Builds a filter chain for processing a track. Note that this may be called several times during the playback of a - * single track. All filters should send the output from the filter either to the next filter in the list, or to the - * output filter if it is the last one in the list. Only the process and flush methods should call the next filter, - * all other methods are called individually for each filter anyway. - * - * @param track The track that this chain is built for. - * @param format The output format of the track. At the point where these filters are called, the number of channels - * and the sample rate already matches that of the output format. - * @param output The filter that the last filter in this chain should send its data to. - * @return The list of filters in the built chain. May be empty, but not null. - */ - List buildChain(AudioTrack track, AudioDataFormat format, UniversalPcmAudioFilter output); + /** + * Builds a filter chain for processing a track. Note that this may be called several times during the playback of a + * single track. All filters should send the output from the filter either to the next filter in the list, or to the + * output filter if it is the last one in the list. Only the process and flush methods should call the next filter, + * all other methods are called individually for each filter anyway. + * + * @param track The track that this chain is built for. + * @param format The output format of the track. At the point where these filters are called, the number of channels + * and the sample rate already matches that of the output format. + * @param output The filter that the last filter in this chain should send its data to. + * @return The list of filters in the built chain. May be empty, but not null. + */ + List buildChain(AudioTrack track, AudioDataFormat format, UniversalPcmAudioFilter output); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/PcmFormat.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/PcmFormat.java index 563998217..8574241c7 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/PcmFormat.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/PcmFormat.java @@ -15,7 +15,7 @@ public class PcmFormat { /** * @param channelCount See {@link #channelCount}. - * @param sampleRate See {@link #sampleRate}. + * @param sampleRate See {@link #sampleRate}. */ public PcmFormat(int channelCount, int sampleRate) { this.channelCount = channelCount; diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/ResamplingPcmAudioFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/ResamplingPcmAudioFilter.java index 727d90e82..cc7f29508 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/ResamplingPcmAudioFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/ResamplingPcmAudioFilter.java @@ -7,79 +7,79 @@ * Filter which resamples audio to the specified sample rate */ public class ResamplingPcmAudioFilter implements FloatPcmAudioFilter { - private static final int BUFFER_SIZE = 4096; + private static final int BUFFER_SIZE = 4096; - private final FloatPcmAudioFilter downstream; - private final SampleRateConverter[] converters; - private final SampleRateConverter.Progress progress = new SampleRateConverter.Progress(); - private final float[][] outputSegments; + private final FloatPcmAudioFilter downstream; + private final SampleRateConverter[] converters; + private final SampleRateConverter.Progress progress = new SampleRateConverter.Progress(); + private final float[][] outputSegments; - /** - * @param configuration Configuration to use - * @param channels Number of channels in input data - * @param downstream Next filter in chain - * @param sourceRate Source sample rate - * @param targetRate Target sample rate - */ - public ResamplingPcmAudioFilter(AudioConfiguration configuration, int channels, FloatPcmAudioFilter downstream, - int sourceRate, int targetRate) { + /** + * @param configuration Configuration to use + * @param channels Number of channels in input data + * @param downstream Next filter in chain + * @param sourceRate Source sample rate + * @param targetRate Target sample rate + */ + public ResamplingPcmAudioFilter(AudioConfiguration configuration, int channels, FloatPcmAudioFilter downstream, + int sourceRate, int targetRate) { - this.downstream = downstream; - converters = new SampleRateConverter[channels]; - outputSegments = new float[channels][]; + this.downstream = downstream; + converters = new SampleRateConverter[channels]; + outputSegments = new float[channels][]; - SampleRateConverter.ResamplingType type = getResamplingType(configuration.getResamplingQuality()); + SampleRateConverter.ResamplingType type = getResamplingType(configuration.getResamplingQuality()); - for (int i = 0; i < channels; i++) { - outputSegments[i] = new float[BUFFER_SIZE]; - converters[i] = new SampleRateConverter(type, 1, sourceRate, targetRate); + for (int i = 0; i < channels; i++) { + outputSegments[i] = new float[BUFFER_SIZE]; + converters[i] = new SampleRateConverter(type, 1, sourceRate, targetRate); + } } - } - @Override - public void seekPerformed(long requestedTime, long providedTime) { - for (SampleRateConverter converter : converters) { - converter.reset(); + @Override + public void seekPerformed(long requestedTime, long providedTime) { + for (SampleRateConverter converter : converters) { + converter.reset(); + } } - } - @Override - public void flush() throws InterruptedException { - // Nothing to do. - } + @Override + public void flush() throws InterruptedException { + // Nothing to do. + } - @Override - public void close() { - for (SampleRateConverter converter : converters) { - converter.close(); + @Override + public void close() { + for (SampleRateConverter converter : converters) { + converter.close(); + } } - } - @Override - public void process(float[][] input, int offset, int length) throws InterruptedException { - do { - for (int i = 0; i < input.length; i++) { - converters[i].process(input[i], offset, length, outputSegments[i], 0, BUFFER_SIZE, false, progress); - } + @Override + public void process(float[][] input, int offset, int length) throws InterruptedException { + do { + for (int i = 0; i < input.length; i++) { + converters[i].process(input[i], offset, length, outputSegments[i], 0, BUFFER_SIZE, false, progress); + } - offset += progress.getInputUsed(); - length -= progress.getInputUsed(); + offset += progress.getInputUsed(); + length -= progress.getInputUsed(); - if (progress.getOutputGenerated() > 0) { - downstream.process(outputSegments, 0, progress.getOutputGenerated()); - } - } while (length > 0 || progress.getOutputGenerated() == BUFFER_SIZE); - } + if (progress.getOutputGenerated() > 0) { + downstream.process(outputSegments, 0, progress.getOutputGenerated()); + } + } while (length > 0 || progress.getOutputGenerated() == BUFFER_SIZE); + } - private static SampleRateConverter.ResamplingType getResamplingType(AudioConfiguration.ResamplingQuality quality) { - switch (quality) { - case HIGH: - return SampleRateConverter.ResamplingType.SINC_MEDIUM_QUALITY; - case MEDIUM: - return SampleRateConverter.ResamplingType.SINC_FASTEST; - case LOW: - default: - return SampleRateConverter.ResamplingType.LINEAR; + private static SampleRateConverter.ResamplingType getResamplingType(AudioConfiguration.ResamplingQuality quality) { + switch (quality) { + case HIGH: + return SampleRateConverter.ResamplingType.SINC_MEDIUM_QUALITY; + case MEDIUM: + return SampleRateConverter.ResamplingType.SINC_FASTEST; + case LOW: + default: + return SampleRateConverter.ResamplingType.LINEAR; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/ShortPcmAudioFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/ShortPcmAudioFilter.java index 7da1f8170..469c9c719 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/ShortPcmAudioFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/ShortPcmAudioFilter.java @@ -6,17 +6,17 @@ * Audio filter which accepts 16-bit signed PCM samples. */ public interface ShortPcmAudioFilter extends AudioFilter { - /** - * @param input Array of samples - * @param offset Offset in the array - * @param length Length of the sequence in the array - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void process(short[] input, int offset, int length) throws InterruptedException; + /** + * @param input Array of samples + * @param offset Offset in the array + * @param length Length of the sequence in the array + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void process(short[] input, int offset, int length) throws InterruptedException; - /** - * @param buffer The buffer of samples - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void process(ShortBuffer buffer) throws InterruptedException; + /** + * @param buffer The buffer of samples + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void process(ShortBuffer buffer) throws InterruptedException; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/SplitShortPcmAudioFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/SplitShortPcmAudioFilter.java index b0a69b125..299fa3b0b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/SplitShortPcmAudioFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/SplitShortPcmAudioFilter.java @@ -4,11 +4,11 @@ * Audio filter which accepts 16-bit signed PCM samples, with an array per . */ public interface SplitShortPcmAudioFilter extends AudioFilter { - /** - * @param input An array of samples for each channel - * @param offset Offset in the array - * @param length Length of the sequence in the array - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void process(short[][] input, int offset, int length) throws InterruptedException; + /** + * @param input An array of samples for each channel + * @param offset Offset in the array + * @param length Length of the sequence in the array + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void process(short[][] input, int offset, int length) throws InterruptedException; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/UserProvidedAudioFilters.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/UserProvidedAudioFilters.java index b8b093a79..8e6979764 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/UserProvidedAudioFilters.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/UserProvidedAudioFilters.java @@ -12,82 +12,82 @@ * whenever the filter factory is changed. */ public class UserProvidedAudioFilters extends CompositeAudioFilter { - private final AudioProcessingContext context; - private final UniversalPcmAudioFilter nextFilter; - private final boolean hotSwapEnabled; - private AudioFilterChain chain; + private final AudioProcessingContext context; + private final UniversalPcmAudioFilter nextFilter; + private final boolean hotSwapEnabled; + private AudioFilterChain chain; + + /** + * @param context Configuration and output information for processing + * @param nextFilter The next filter that should be processed after this one. + */ + public UserProvidedAudioFilters(AudioProcessingContext context, UniversalPcmAudioFilter nextFilter) { + this.context = context; + this.nextFilter = nextFilter; + this.hotSwapEnabled = context.filterHotSwapEnabled; + this.chain = buildFragment(context, nextFilter); + } + + private static AudioFilterChain buildFragment(AudioProcessingContext context, + UniversalPcmAudioFilter nextFilter) { + + PcmFilterFactory factory = context.playerOptions.filterFactory.get(); - /** - * @param context Configuration and output information for processing - * @param nextFilter The next filter that should be processed after this one. - */ - public UserProvidedAudioFilters(AudioProcessingContext context, UniversalPcmAudioFilter nextFilter) { - this.context = context; - this.nextFilter = nextFilter; - this.hotSwapEnabled = context.filterHotSwapEnabled; - this.chain = buildFragment(context, nextFilter); - } + if (factory == null) { + return new AudioFilterChain(nextFilter, Collections.emptyList(), null); + } else { + FilterChainBuilder builder = new FilterChainBuilder(); - private static AudioFilterChain buildFragment(AudioProcessingContext context, - UniversalPcmAudioFilter nextFilter) { + List filters = new ArrayList<>(factory.buildChain(null, context.outputFormat, nextFilter)); - PcmFilterFactory factory = context.playerOptions.filterFactory.get(); + if (filters.isEmpty()) { + return new AudioFilterChain(nextFilter, Collections.emptyList(), null); + } - if (factory == null) { - return new AudioFilterChain(nextFilter, Collections.emptyList(), null); - } else { - FilterChainBuilder builder = new FilterChainBuilder(); + Collections.reverse(filters); - List filters = new ArrayList<>(factory.buildChain(null, context.outputFormat, nextFilter)); + for (AudioFilter filter : filters) { + builder.addFirst(filter); + } - if (filters.isEmpty()) { - return new AudioFilterChain(nextFilter, Collections.emptyList(), null); - } + return builder.build(factory, context.outputFormat.channelCount); + } + } - Collections.reverse(filters); + @Override + protected List getFilters() { + return chain.filters; + } - for (AudioFilter filter : filters) { - builder.addFirst(filter); - } + @Override + public void process(float[][] input, int offset, int length) throws InterruptedException { + checkRebuild(); + chain.input.process(input, offset, length); + } - return builder.build(factory, context.outputFormat.channelCount); + @Override + public void process(short[] input, int offset, int length) throws InterruptedException { + checkRebuild(); + chain.input.process(input, offset, length); } - } - - @Override - protected List getFilters() { - return chain.filters; - } - - @Override - public void process(float[][] input, int offset, int length) throws InterruptedException { - checkRebuild(); - chain.input.process(input, offset, length); - } - - @Override - public void process(short[] input, int offset, int length) throws InterruptedException { - checkRebuild(); - chain.input.process(input, offset, length); - } - - @Override - public void process(ShortBuffer buffer) throws InterruptedException { - checkRebuild(); - chain.input.process(buffer); - } - - @Override - public void process(short[][] input, int offset, int length) throws InterruptedException { - checkRebuild(); - chain.input.process(input, offset, length); - } - - private void checkRebuild() throws InterruptedException { - if (hotSwapEnabled && context.playerOptions.filterFactory.get() != chain.context) { - flush(); - close(); - chain = buildFragment(context, nextFilter); + + @Override + public void process(ShortBuffer buffer) throws InterruptedException { + checkRebuild(); + chain.input.process(buffer); + } + + @Override + public void process(short[][] input, int offset, int length) throws InterruptedException { + checkRebuild(); + chain.input.process(input, offset, length); + } + + private void checkRebuild() throws InterruptedException { + if (hotSwapEnabled && context.playerOptions.filterFactory.get() != chain.context) { + flush(); + close(); + chain = buildFragment(context, nextFilter); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ConverterAudioFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ConverterAudioFilter.java index 1584e299b..9abca80d6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ConverterAudioFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ConverterAudioFilter.java @@ -6,24 +6,24 @@ * Base class for converter filters which have no internal state. */ public abstract class ConverterAudioFilter implements UniversalPcmAudioFilter { - protected static final int BUFFER_SIZE = 4096; + protected static final int BUFFER_SIZE = 4096; - @Override - public void seekPerformed(long requestedTime, long providedTime) { - // Nothing to do. - } + @Override + public void seekPerformed(long requestedTime, long providedTime) { + // Nothing to do. + } - @Override - public void flush() throws InterruptedException { - // Nothing to do. - } + @Override + public void flush() throws InterruptedException { + // Nothing to do. + } - @Override - public void close() { - // Nothing to do. - } + @Override + public void close() { + // Nothing to do. + } - protected static short floatToShort(float value) { - return (short) (value * 32768.0f); - } + protected static short floatToShort(float value) { + return (short) (value * 32768.0f); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ToFloatAudioFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ToFloatAudioFilter.java index 053ed1629..9da01c471 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ToFloatAudioFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ToFloatAudioFilter.java @@ -8,84 +8,84 @@ * Filter which takes in PCM data in any representation and outputs it as float PCM. */ public class ToFloatAudioFilter extends ConverterAudioFilter { - private final FloatPcmAudioFilter downstream; - private final int channelCount; - private final float[][] buffers; - - /** - * @param downstream The float PCM filter to pass the output to. - * @param channelCount Number of channels in the PCM data. - */ - public ToFloatAudioFilter(FloatPcmAudioFilter downstream, int channelCount) { - this.downstream = downstream; - this.channelCount = channelCount; - this.buffers = new float[channelCount][]; - - for (int i = 0; i < channelCount; i++) { - this.buffers[i] = new float[BUFFER_SIZE]; + private final FloatPcmAudioFilter downstream; + private final int channelCount; + private final float[][] buffers; + + /** + * @param downstream The float PCM filter to pass the output to. + * @param channelCount Number of channels in the PCM data. + */ + public ToFloatAudioFilter(FloatPcmAudioFilter downstream, int channelCount) { + this.downstream = downstream; + this.channelCount = channelCount; + this.buffers = new float[channelCount][]; + + for (int i = 0; i < channelCount; i++) { + this.buffers[i] = new float[BUFFER_SIZE]; + } } - } - @Override - public void process(float[][] input, int offset, int length) throws InterruptedException { - downstream.process(input, offset, length); - } + @Override + public void process(float[][] input, int offset, int length) throws InterruptedException { + downstream.process(input, offset, length); + } - @Override - public void process(short[] input, int offset, int length) throws InterruptedException { - int end = offset + length; + @Override + public void process(short[] input, int offset, int length) throws InterruptedException { + int end = offset + length; - while (end - offset >= channelCount) { - int chunkLength = Math.min((end - offset) / channelCount, BUFFER_SIZE); + while (end - offset >= channelCount) { + int chunkLength = Math.min((end - offset) / channelCount, BUFFER_SIZE); - for (int chunkPosition = 0; chunkPosition < chunkLength; chunkPosition++) { - for (int channel = 0; channel < channelCount; channel++) { - buffers[channel][chunkPosition] = shortToFloat(input[offset++]); - } - } + for (int chunkPosition = 0; chunkPosition < chunkLength; chunkPosition++) { + for (int channel = 0; channel < channelCount; channel++) { + buffers[channel][chunkPosition] = shortToFloat(input[offset++]); + } + } - downstream.process(buffers, 0, chunkLength); + downstream.process(buffers, 0, chunkLength); + } } - } - @Override - public void process(ShortBuffer buffer) throws InterruptedException { - while (buffer.hasRemaining()) { - int chunkLength = Math.min(buffer.remaining() / channelCount, BUFFER_SIZE); + @Override + public void process(ShortBuffer buffer) throws InterruptedException { + while (buffer.hasRemaining()) { + int chunkLength = Math.min(buffer.remaining() / channelCount, BUFFER_SIZE); - if (chunkLength == 0) { - break; - } + if (chunkLength == 0) { + break; + } - for (int chunkPosition = 0; chunkPosition < chunkLength; chunkPosition++) { - for (int channel = 0; channel < buffers.length; channel++) { - buffers[channel][chunkPosition] = shortToFloat(buffer.get()); - } - } + for (int chunkPosition = 0; chunkPosition < chunkLength; chunkPosition++) { + for (int channel = 0; channel < buffers.length; channel++) { + buffers[channel][chunkPosition] = shortToFloat(buffer.get()); + } + } - downstream.process(buffers, 0, chunkLength); + downstream.process(buffers, 0, chunkLength); + } } - } - @Override - public void process(short[][] input, int offset, int length) throws InterruptedException { - int end = offset + length; + @Override + public void process(short[][] input, int offset, int length) throws InterruptedException { + int end = offset + length; - while (offset < end) { - int chunkLength = Math.min(end - offset, BUFFER_SIZE); + while (offset < end) { + int chunkLength = Math.min(end - offset, BUFFER_SIZE); - for (int channel = 0; channel < buffers.length; channel++) { - for (int chunkPosition = 0; chunkPosition < chunkLength; chunkPosition++) { - buffers[channel][chunkPosition] = shortToFloat(input[channel][offset + chunkPosition]); - } - } + for (int channel = 0; channel < buffers.length; channel++) { + for (int chunkPosition = 0; chunkPosition < chunkLength; chunkPosition++) { + buffers[channel][chunkPosition] = shortToFloat(input[channel][offset + chunkPosition]); + } + } - offset += chunkLength; - downstream.process(buffers, 0, chunkLength); + offset += chunkLength; + downstream.process(buffers, 0, chunkLength); + } } - } - private static float shortToFloat(short value) { - return value / 32768.0f; - } + private static float shortToFloat(short value) { + return value / 32768.0f; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ToShortAudioFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ToShortAudioFilter.java index 5441a72ee..7933d2810 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ToShortAudioFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ToShortAudioFilter.java @@ -8,65 +8,65 @@ * Filter which takes in PCM data in any representation and outputs it as short PCM. */ public class ToShortAudioFilter extends ConverterAudioFilter { - private final ShortPcmAudioFilter downstream; - private final int channelCount; - private final short[] outputBuffer; + private final ShortPcmAudioFilter downstream; + private final int channelCount; + private final short[] outputBuffer; - /** - * @param downstream The short PCM filter to pass the output to. - * @param channelCount Number of channels in the PCM data. - */ - public ToShortAudioFilter(ShortPcmAudioFilter downstream, int channelCount) { - this.downstream = downstream; - this.channelCount = channelCount; - this.outputBuffer = new short[BUFFER_SIZE * channelCount]; - } + /** + * @param downstream The short PCM filter to pass the output to. + * @param channelCount Number of channels in the PCM data. + */ + public ToShortAudioFilter(ShortPcmAudioFilter downstream, int channelCount) { + this.downstream = downstream; + this.channelCount = channelCount; + this.outputBuffer = new short[BUFFER_SIZE * channelCount]; + } + + @Override + public void process(float[][] input, int offset, int length) throws InterruptedException { + int end = offset + length; - @Override - public void process(float[][] input, int offset, int length) throws InterruptedException { - int end = offset + length; + while (offset < end) { + int chunkSize = Math.min(BUFFER_SIZE, end - offset); + int writePosition = 0; - while (offset < end) { - int chunkSize = Math.min(BUFFER_SIZE, end - offset); - int writePosition = 0; + for (int chunkPosition = 0; chunkPosition < chunkSize; chunkPosition++) { + for (int channel = 0; channel < channelCount; channel++) { + outputBuffer[writePosition++] = floatToShort(input[channel][chunkPosition]); + } + } - for (int chunkPosition = 0; chunkPosition < chunkSize; chunkPosition++) { - for (int channel = 0; channel < channelCount; channel++) { - outputBuffer[writePosition++] = floatToShort(input[channel][chunkPosition]); + offset += chunkSize; + downstream.process(outputBuffer, 0, chunkSize); } - } + } - offset += chunkSize; - downstream.process(outputBuffer, 0, chunkSize); + @Override + public void process(short[] input, int offset, int length) throws InterruptedException { + downstream.process(input, offset, length); } - } - @Override - public void process(short[] input, int offset, int length) throws InterruptedException { - downstream.process(input, offset, length); - } + @Override + public void process(ShortBuffer buffer) throws InterruptedException { + downstream.process(buffer); + } - @Override - public void process(ShortBuffer buffer) throws InterruptedException { - downstream.process(buffer); - } + @Override + public void process(short[][] input, int offset, int length) throws InterruptedException { + int end = offset + length; - @Override - public void process(short[][] input, int offset, int length) throws InterruptedException { - int end = offset + length; + while (offset < end) { + int chunkSize = Math.min(BUFFER_SIZE, end - offset); + int writePosition = 0; - while (offset < end) { - int chunkSize = Math.min(BUFFER_SIZE, end - offset); - int writePosition = 0; + for (int chunkPosition = 0; chunkPosition < chunkSize; chunkPosition++) { + for (int channel = 0; channel < channelCount; channel++) { + outputBuffer[writePosition++] = input[channel][chunkPosition]; + } + } - for (int chunkPosition = 0; chunkPosition < chunkSize; chunkPosition++) { - for (int channel = 0; channel < channelCount; channel++) { - outputBuffer[writePosition++] = input[channel][chunkPosition]; + offset += chunkSize; + downstream.process(outputBuffer, 0, chunkSize); } - } - - offset += chunkSize; - downstream.process(outputBuffer, 0, chunkSize); } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ToSplitShortAudioFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ToSplitShortAudioFilter.java index bd7b5aabb..a47c6b84c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ToSplitShortAudioFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/converter/ToSplitShortAudioFilter.java @@ -8,75 +8,75 @@ * Filter which takes in PCM data in any representation and outputs it as split short PCM. */ public class ToSplitShortAudioFilter extends ConverterAudioFilter { - private final SplitShortPcmAudioFilter downstream; - private final int channelCount; - private final short[][] buffers; - - /** - * @param downstream The split short PCM filter to pass the output to. - * @param channelCount Number of channels in the PCM data. - */ - public ToSplitShortAudioFilter(SplitShortPcmAudioFilter downstream, int channelCount) { - this.downstream = downstream; - this.channelCount = channelCount; - this.buffers = new short[channelCount][]; - - for (int i = 0; i < channelCount; i++) { - this.buffers[i] = new short[BUFFER_SIZE]; + private final SplitShortPcmAudioFilter downstream; + private final int channelCount; + private final short[][] buffers; + + /** + * @param downstream The split short PCM filter to pass the output to. + * @param channelCount Number of channels in the PCM data. + */ + public ToSplitShortAudioFilter(SplitShortPcmAudioFilter downstream, int channelCount) { + this.downstream = downstream; + this.channelCount = channelCount; + this.buffers = new short[channelCount][]; + + for (int i = 0; i < channelCount; i++) { + this.buffers[i] = new short[BUFFER_SIZE]; + } } - } - @Override - public void process(float[][] input, int offset, int length) throws InterruptedException { - int end = offset + length; + @Override + public void process(float[][] input, int offset, int length) throws InterruptedException { + int end = offset + length; - while (offset < end) { - int chunkLength = Math.min(end - offset, BUFFER_SIZE); + while (offset < end) { + int chunkLength = Math.min(end - offset, BUFFER_SIZE); - for (int channel = 0; channel < channelCount; channel++) { - for (int chunkPosition = 0; chunkPosition < chunkLength; chunkPosition++) { - buffers[channel][chunkPosition] = floatToShort(input[channel][offset + chunkPosition]); - } - } + for (int channel = 0; channel < channelCount; channel++) { + for (int chunkPosition = 0; chunkPosition < chunkLength; chunkPosition++) { + buffers[channel][chunkPosition] = floatToShort(input[channel][offset + chunkPosition]); + } + } - downstream.process(buffers, 0, chunkLength); + downstream.process(buffers, 0, chunkLength); + } } - } - @Override - public void process(short[] input, int offset, int length) throws InterruptedException { - int end = offset + length; + @Override + public void process(short[] input, int offset, int length) throws InterruptedException { + int end = offset + length; - while (end - offset >= channelCount) { - int chunkLength = Math.min(end - offset, BUFFER_SIZE * channelCount); + while (end - offset >= channelCount) { + int chunkLength = Math.min(end - offset, BUFFER_SIZE * channelCount); - for (int chunkPosition = 0; chunkPosition < chunkLength; chunkPosition++) { - for (int channel = 0; channel < buffers.length; channel++) { - buffers[channel][chunkPosition] = floatToShort(input[offset++]); - } - } + for (int chunkPosition = 0; chunkPosition < chunkLength; chunkPosition++) { + for (int channel = 0; channel < buffers.length; channel++) { + buffers[channel][chunkPosition] = floatToShort(input[offset++]); + } + } - downstream.process(buffers, 0, chunkLength); + downstream.process(buffers, 0, chunkLength); + } } - } - @Override - public void process(ShortBuffer buffer) throws InterruptedException { - while (buffer.hasRemaining()) { - int chunkLength = Math.min(buffer.remaining(), BUFFER_SIZE * channelCount); + @Override + public void process(ShortBuffer buffer) throws InterruptedException { + while (buffer.hasRemaining()) { + int chunkLength = Math.min(buffer.remaining(), BUFFER_SIZE * channelCount); - for (int chunkPosition = 0; chunkPosition < chunkLength; chunkPosition++) { - for (int channel = 0; channel < buffers.length; channel++) { - buffers[channel][chunkPosition] = floatToShort(buffer.get()); - } - } + for (int chunkPosition = 0; chunkPosition < chunkLength; chunkPosition++) { + for (int channel = 0; channel < buffers.length; channel++) { + buffers[channel][chunkPosition] = floatToShort(buffer.get()); + } + } - downstream.process(buffers, 0, chunkLength); + downstream.process(buffers, 0, chunkLength); + } } - } - @Override - public void process(short[][] input, int offset, int length) throws InterruptedException { - downstream.process(input, offset, length); - } + @Override + public void process(short[][] input, int offset, int length) throws InterruptedException { + downstream.process(input, offset, length); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/equalizer/Equalizer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/equalizer/Equalizer.java index e042249fe..ad2df63e1 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/equalizer/Equalizer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/equalizer/Equalizer.java @@ -10,165 +10,165 @@ * externally or using {@link #setGain(int, float)}). */ public class Equalizer extends EqualizerConfiguration implements FloatPcmAudioFilter { - /** - * Number of bands in the equalizer. - */ - public static final int BAND_COUNT = 15; - - private static final int SAMPLE_RATE = 48000; - - private final ChannelProcessor[] channels; - private final FloatPcmAudioFilter next; - - private static final Coefficients[] coefficients48000 = { - new Coefficients(9.9847546664e-01f, 7.6226668143e-04f, 1.9984647656e+00f), - new Coefficients(9.9756184654e-01f, 1.2190767289e-03f, 1.9975344645e+00f), - new Coefficients(9.9616261379e-01f, 1.9186931041e-03f, 1.9960947369e+00f), - new Coefficients(9.9391578543e-01f, 3.0421072865e-03f, 1.9937449618e+00f), - new Coefficients(9.9028307215e-01f, 4.8584639242e-03f, 1.9898465702e+00f), - new Coefficients(9.8485897264e-01f, 7.5705136795e-03f, 1.9837962543e+00f), - new Coefficients(9.7588512657e-01f, 1.2057436715e-02f, 1.9731772447e+00f), - new Coefficients(9.6228521814e-01f, 1.8857390928e-02f, 1.9556164694e+00f), - new Coefficients(9.4080933132e-01f, 2.9595334338e-02f, 1.9242054384e+00f), - new Coefficients(9.0702059196e-01f, 4.6489704022e-02f, 1.8653476166e+00f), - new Coefficients(8.5868004289e-01f, 7.0659978553e-02f, 1.7600401337e+00f), - new Coefficients(7.8409610788e-01f, 1.0795194606e-01f, 1.5450725522e+00f), - new Coefficients(6.8332861002e-01f, 1.5833569499e-01f, 1.1426447155e+00f), - new Coefficients(5.5267518228e-01f, 2.2366240886e-01f, 4.0186190803e-01f), - new Coefficients(4.1811888447e-01f, 2.9094055777e-01f, -7.0905944223e-01f) - }; - - /** - * @param channelCount Number of channels in the input. - * @param next The next filter in the chain. - * @param bandMultipliers The band multiplier values. Keeps using this array internally, so the values can be changed - * externally. - */ - public Equalizer(int channelCount, FloatPcmAudioFilter next, float[] bandMultipliers) { - super(bandMultipliers); - this.channels = createProcessors(channelCount, bandMultipliers); - this.next = next; - } - - /** - * @param channelCount Number of channels in the input. - * @param next The next filter in the chain. - */ - public Equalizer(int channelCount, FloatPcmAudioFilter next) { - this(channelCount, next, new float[BAND_COUNT]); - } - - /** - * @param format Audio output format. - * @return true if the output format is compatible for the equalizer (based on sample rate). - */ - public static boolean isCompatible(AudioDataFormat format) { - return format.sampleRate == SAMPLE_RATE; - } - - @Override - public void process(float[][] input, int offset, int length) throws InterruptedException { - for (int channelIndex = 0; channelIndex < channels.length; channelIndex++) { - channels[channelIndex].process(input[channelIndex], offset, offset + length); + /** + * Number of bands in the equalizer. + */ + public static final int BAND_COUNT = 15; + + private static final int SAMPLE_RATE = 48000; + + private final ChannelProcessor[] channels; + private final FloatPcmAudioFilter next; + + private static final Coefficients[] coefficients48000 = { + new Coefficients(9.9847546664e-01f, 7.6226668143e-04f, 1.9984647656e+00f), + new Coefficients(9.9756184654e-01f, 1.2190767289e-03f, 1.9975344645e+00f), + new Coefficients(9.9616261379e-01f, 1.9186931041e-03f, 1.9960947369e+00f), + new Coefficients(9.9391578543e-01f, 3.0421072865e-03f, 1.9937449618e+00f), + new Coefficients(9.9028307215e-01f, 4.8584639242e-03f, 1.9898465702e+00f), + new Coefficients(9.8485897264e-01f, 7.5705136795e-03f, 1.9837962543e+00f), + new Coefficients(9.7588512657e-01f, 1.2057436715e-02f, 1.9731772447e+00f), + new Coefficients(9.6228521814e-01f, 1.8857390928e-02f, 1.9556164694e+00f), + new Coefficients(9.4080933132e-01f, 2.9595334338e-02f, 1.9242054384e+00f), + new Coefficients(9.0702059196e-01f, 4.6489704022e-02f, 1.8653476166e+00f), + new Coefficients(8.5868004289e-01f, 7.0659978553e-02f, 1.7600401337e+00f), + new Coefficients(7.8409610788e-01f, 1.0795194606e-01f, 1.5450725522e+00f), + new Coefficients(6.8332861002e-01f, 1.5833569499e-01f, 1.1426447155e+00f), + new Coefficients(5.5267518228e-01f, 2.2366240886e-01f, 4.0186190803e-01f), + new Coefficients(4.1811888447e-01f, 2.9094055777e-01f, -7.0905944223e-01f) + }; + + /** + * @param channelCount Number of channels in the input. + * @param next The next filter in the chain. + * @param bandMultipliers The band multiplier values. Keeps using this array internally, so the values can be changed + * externally. + */ + public Equalizer(int channelCount, FloatPcmAudioFilter next, float[] bandMultipliers) { + super(bandMultipliers); + this.channels = createProcessors(channelCount, bandMultipliers); + this.next = next; } - next.process(input, offset, length); - } + /** + * @param channelCount Number of channels in the input. + * @param next The next filter in the chain. + */ + public Equalizer(int channelCount, FloatPcmAudioFilter next) { + this(channelCount, next, new float[BAND_COUNT]); + } - @Override - public void seekPerformed(long requestedTime, long providedTime) { - for (int channelIndex = 0; channelIndex < channels.length; channelIndex++) { - channels[channelIndex].reset(); + /** + * @param format Audio output format. + * @return true if the output format is compatible for the equalizer (based on sample rate). + */ + public static boolean isCompatible(AudioDataFormat format) { + return format.sampleRate == SAMPLE_RATE; } - } - @Override - public void flush() throws InterruptedException { - // Nothing to do here. - } + @Override + public void process(float[][] input, int offset, int length) throws InterruptedException { + for (int channelIndex = 0; channelIndex < channels.length; channelIndex++) { + channels[channelIndex].process(input[channelIndex], offset, offset + length); + } - @Override - public void close() { - // Nothing to do here. - } + next.process(input, offset, length); + } - private static ChannelProcessor[] createProcessors(int channelCount, float[] bandMultipliers) { - ChannelProcessor[] processors = new ChannelProcessor[channelCount]; + @Override + public void seekPerformed(long requestedTime, long providedTime) { + for (int channelIndex = 0; channelIndex < channels.length; channelIndex++) { + channels[channelIndex].reset(); + } + } - for (int i = 0; i < channelCount; i++) { - processors[i] = new ChannelProcessor(bandMultipliers); + @Override + public void flush() throws InterruptedException { + // Nothing to do here. } - return processors; - } + @Override + public void close() { + // Nothing to do here. + } - private static class ChannelProcessor { - private final float[] history; - private final float[] bandMultipliers; + private static ChannelProcessor[] createProcessors(int channelCount, float[] bandMultipliers) { + ChannelProcessor[] processors = new ChannelProcessor[channelCount]; - private int current; - private int minusOne; - private int minusTwo; + for (int i = 0; i < channelCount; i++) { + processors[i] = new ChannelProcessor(bandMultipliers); + } - private ChannelProcessor(float[] bandMultipliers) { - this.history = new float[BAND_COUNT * 6]; - this.bandMultipliers = bandMultipliers; - this.current = 0; - this.minusOne = 2; - this.minusTwo = 1; + return processors; } - private void process(float[] samples, int startIndex, int endIndex) { - for (int sampleIndex = startIndex; sampleIndex < endIndex; sampleIndex++) { - float sample = samples[sampleIndex]; - float result = sample * 0.25f; + private static class ChannelProcessor { + private final float[] history; + private final float[] bandMultipliers; - for (int bandIndex = 0; bandIndex < BAND_COUNT; bandIndex++) { - int x = bandIndex * 6; - int y = x + 3; + private int current; + private int minusOne; + private int minusTwo; - Coefficients coefficients = coefficients48000[bandIndex]; + private ChannelProcessor(float[] bandMultipliers) { + this.history = new float[BAND_COUNT * 6]; + this.bandMultipliers = bandMultipliers; + this.current = 0; + this.minusOne = 2; + this.minusTwo = 1; + } - float bandResult = coefficients.alpha * (sample - history[x + minusTwo]) + - coefficients.gamma * history[y + minusOne] - - coefficients.beta * history[y + minusTwo]; + private void process(float[] samples, int startIndex, int endIndex) { + for (int sampleIndex = startIndex; sampleIndex < endIndex; sampleIndex++) { + float sample = samples[sampleIndex]; + float result = sample * 0.25f; - history[x + current] = sample; - history[y + current] = bandResult; + for (int bandIndex = 0; bandIndex < BAND_COUNT; bandIndex++) { + int x = bandIndex * 6; + int y = x + 3; - result += bandResult * bandMultipliers[bandIndex]; - } + Coefficients coefficients = coefficients48000[bandIndex]; - samples[sampleIndex] = Math.min(Math.max(result * 4.0f, -1.0f), 1.0f); + float bandResult = coefficients.alpha * (sample - history[x + minusTwo]) + + coefficients.gamma * history[y + minusOne] - + coefficients.beta * history[y + minusTwo]; - if (++current == 3) { - current = 0; - } + history[x + current] = sample; + history[y + current] = bandResult; - if (++minusOne == 3) { - minusOne = 0; - } + result += bandResult * bandMultipliers[bandIndex]; + } + + samples[sampleIndex] = Math.min(Math.max(result * 4.0f, -1.0f), 1.0f); + + if (++current == 3) { + current = 0; + } - if (++minusTwo == 3) { - minusTwo = 0; + if (++minusOne == 3) { + minusOne = 0; + } + + if (++minusTwo == 3) { + minusTwo = 0; + } + } } - } - } - private void reset() { - Arrays.fill(history, 0.0f); + private void reset() { + Arrays.fill(history, 0.0f); + } } - } - private static class Coefficients { - private final float beta; - private final float alpha; - private final float gamma; + private static class Coefficients { + private final float beta; + private final float alpha; + private final float gamma; - private Coefficients(float beta, float alpha, float gamma) { - this.beta = beta; - this.alpha = alpha; - this.gamma = gamma; + private Coefficients(float beta, float alpha, float gamma) { + this.beta = beta; + this.alpha = alpha; + this.gamma = gamma; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/equalizer/EqualizerConfiguration.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/equalizer/EqualizerConfiguration.java index 70127f6e3..ab630894b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/equalizer/EqualizerConfiguration.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/equalizer/EqualizerConfiguration.java @@ -15,7 +15,7 @@ public EqualizerConfiguration(float[] bandMultipliers) { } /** - * @param band The index of the band. If this is not a valid band index, the method has no effect. + * @param band The index of the band. If this is not a valid band index, the method has no effect. * @param value The multiplier for this band. Default value is 0. Valid values are from -0.25 to 1. -0.25 means that * the given frequency is completely muted and 0.25 means it is doubled. Note that this may change the * volume of the output. diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/equalizer/EqualizerFactory.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/equalizer/EqualizerFactory.java index c5a7da811..fb546df82 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/equalizer/EqualizerFactory.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/equalizer/EqualizerFactory.java @@ -14,19 +14,19 @@ * the only custom filter used. */ public class EqualizerFactory extends EqualizerConfiguration implements PcmFilterFactory { - /** - * Creates a new instance no gains applied initially. - */ - public EqualizerFactory() { - super(new float[Equalizer.BAND_COUNT]); - } + /** + * Creates a new instance no gains applied initially. + */ + public EqualizerFactory() { + super(new float[Equalizer.BAND_COUNT]); + } - @Override - public List buildChain(AudioTrack track, AudioDataFormat format, UniversalPcmAudioFilter output) { - if (Equalizer.isCompatible(format)) { - return Collections.singletonList(new Equalizer(format.channelCount, output, bandMultipliers)); - } else { - return Collections.emptyList(); + @Override + public List buildChain(AudioTrack track, AudioDataFormat format, UniversalPcmAudioFilter output) { + if (Equalizer.isCompatible(format)) { + return Collections.singletonList(new Equalizer(format.channelCount, output, bandMultipliers)); + } else { + return Collections.emptyList(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/volume/AudioFrameVolumeChanger.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/volume/AudioFrameVolumeChanger.java index c187eeb72..b3cb13526 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/volume/AudioFrameVolumeChanger.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/volume/AudioFrameVolumeChanger.java @@ -17,89 +17,90 @@ * A frame rebuilder to apply a specific volume level to the frames. */ public class AudioFrameVolumeChanger implements AudioFrameRebuilder { - private final AudioConfiguration configuration; - private final AudioDataFormat format; - private final int newVolume; - private final ShortBuffer sampleBuffer; - private final PcmVolumeProcessor volumeProcessor; - - private AudioChunkEncoder encoder; - private AudioChunkDecoder decoder; - private int frameIndex; - - private AudioFrameVolumeChanger(AudioConfiguration configuration, AudioDataFormat format, int newVolume) { - this.configuration = configuration; - this.format = format; - this.newVolume = newVolume; - - this.sampleBuffer = ByteBuffer - .allocateDirect(format.totalSampleCount() * 2) - .order(ByteOrder.nativeOrder()) - .asShortBuffer(); - this.volumeProcessor = new PcmVolumeProcessor(100); - } - - @Override - public AudioFrame rebuild(AudioFrame frame) { - if (frame.getVolume() == newVolume) { - return frame; + private final AudioConfiguration configuration; + private final AudioDataFormat format; + private final int newVolume; + private final ShortBuffer sampleBuffer; + private final PcmVolumeProcessor volumeProcessor; + + private AudioChunkEncoder encoder; + private AudioChunkDecoder decoder; + private int frameIndex; + + private AudioFrameVolumeChanger(AudioConfiguration configuration, AudioDataFormat format, int newVolume) { + this.configuration = configuration; + this.format = format; + this.newVolume = newVolume; + + this.sampleBuffer = ByteBuffer + .allocateDirect(format.totalSampleCount() * 2) + .order(ByteOrder.nativeOrder()) + .asShortBuffer(); + this.volumeProcessor = new PcmVolumeProcessor(100); } - decoder.decode(frame.getData(), sampleBuffer); + @Override + public AudioFrame rebuild(AudioFrame frame) { + if (frame.getVolume() == newVolume) { + return frame; + } - int targetVolume = newVolume; + decoder.decode(frame.getData(), sampleBuffer); - if (++frameIndex < 50) { - targetVolume = (int) ((newVolume - frame.getVolume()) * (frameIndex / 50.0) + frame.getVolume()); - } + int targetVolume = newVolume; - // Volume 0 is stored in the frame with volume 100 buffer - if (targetVolume != 0) { - volumeProcessor.applyVolume(frame.getVolume(), targetVolume, sampleBuffer); - } + if (++frameIndex < 50) { + targetVolume = (int) ((newVolume - frame.getVolume()) * (frameIndex / 50.0) + frame.getVolume()); + } - byte[] bytes = encoder.encode(sampleBuffer); + // Volume 0 is stored in the frame with volume 100 buffer + if (targetVolume != 0) { + volumeProcessor.applyVolume(frame.getVolume(), targetVolume, sampleBuffer); + } - // One frame per 20ms is consumed. To not spike the CPU usage, reencode only once per 5ms. By the time the buffer is - // fully rebuilt, it is probably near to 3/4 its maximum size. - try { - Thread.sleep(5); - } catch (InterruptedException e) { - // Keep it interrupted, it will trip on the next interruptible operation - Thread.currentThread().interrupt(); - } + byte[] bytes = encoder.encode(sampleBuffer); - return new ImmutableAudioFrame(frame.getTimecode(), bytes, targetVolume, format); - } + // One frame per 20ms is consumed. To not spike the CPU usage, reencode only once per 5ms. By the time the buffer is + // fully rebuilt, it is probably near to 3/4 its maximum size. + try { + Thread.sleep(5); + } catch (InterruptedException e) { + // Keep it interrupted, it will trip on the next interruptible operation + Thread.currentThread().interrupt(); + } - private void setupLibraries() { - encoder = format.createEncoder(configuration); - decoder = format.createDecoder(); - } + return new ImmutableAudioFrame(frame.getTimecode(), bytes, targetVolume, format); + } - private void clearLibraries() { - if (encoder != null) { - encoder.close(); + private void setupLibraries() { + encoder = format.createEncoder(configuration); + decoder = format.createDecoder(); } - if (decoder != null) { - decoder.close(); + private void clearLibraries() { + if (encoder != null) { + encoder.close(); + } + + if (decoder != null) { + decoder.close(); + } } - } - - /** - * Applies a volume level to the buffered frames of a frame consumer - * @param context Configuration and output information for processing - */ - public static void apply(AudioProcessingContext context) { - AudioFrameVolumeChanger volumeChanger = new AudioFrameVolumeChanger(context.configuration, context.outputFormat, - context.playerOptions.volumeLevel.get()); - - try { - volumeChanger.setupLibraries(); - context.frameBuffer.rebuild(volumeChanger); - } finally { - volumeChanger.clearLibraries(); + + /** + * Applies a volume level to the buffered frames of a frame consumer + * + * @param context Configuration and output information for processing + */ + public static void apply(AudioProcessingContext context) { + AudioFrameVolumeChanger volumeChanger = new AudioFrameVolumeChanger(context.configuration, context.outputFormat, + context.playerOptions.volumeLevel.get()); + + try { + volumeChanger.setupLibraries(); + context.frameBuffer.rebuild(volumeChanger); + } finally { + volumeChanger.clearLibraries(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/volume/PcmVolumeProcessor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/volume/PcmVolumeProcessor.java index 9ff3fb90f..ec0dab983 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/volume/PcmVolumeProcessor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/volume/PcmVolumeProcessor.java @@ -6,81 +6,81 @@ * Class used to apply a volume level to short PCM buffers */ public class PcmVolumeProcessor { - private int currentVolume = -1; - private int integerMultiplier; + private int currentVolume = -1; + private int integerMultiplier; - /** - * @param initialVolume Initial volume level (only useful for getLastVolume() as specified with each call) - */ - public PcmVolumeProcessor(int initialVolume) { - setupMultipliers(initialVolume); - } - - /** - * @return Last volume level used with this processor - */ - public int getLastVolume() { - return currentVolume; - } - - /** - * @param lastVolume Value to explicitly set for the return value of getLastVolume() - */ - public void setLastVolume(int lastVolume) { - currentVolume = lastVolume; - } + /** + * @param initialVolume Initial volume level (only useful for getLastVolume() as specified with each call) + */ + public PcmVolumeProcessor(int initialVolume) { + setupMultipliers(initialVolume); + } - /** - * @param initialVolume The input volume of the samples - * @param targetVolume The target volume of the samples - * @param buffer The buffer containing the samples - */ - public void applyVolume(int initialVolume, int targetVolume, ShortBuffer buffer) { - if (initialVolume != 100 && initialVolume != 0) { - setupMultipliers(initialVolume); - unapplyCurrentVolume(buffer); + /** + * @return Last volume level used with this processor + */ + public int getLastVolume() { + return currentVolume; } - setupMultipliers(targetVolume); - applyCurrentVolume(buffer); - } + /** + * @param lastVolume Value to explicitly set for the return value of getLastVolume() + */ + public void setLastVolume(int lastVolume) { + currentVolume = lastVolume; + } - private void setupMultipliers(int activeVolume) { - if (currentVolume != activeVolume) { - currentVolume = activeVolume; + /** + * @param initialVolume The input volume of the samples + * @param targetVolume The target volume of the samples + * @param buffer The buffer containing the samples + */ + public void applyVolume(int initialVolume, int targetVolume, ShortBuffer buffer) { + if (initialVolume != 100 && initialVolume != 0) { + setupMultipliers(initialVolume); + unapplyCurrentVolume(buffer); + } - if (activeVolume <= 150) { - float floatMultiplier = (float) Math.tan(activeVolume * 0.0079f); - integerMultiplier = (int) (floatMultiplier * 10000); - } else { - integerMultiplier = 24621 * activeVolume / 150; - } + setupMultipliers(targetVolume); + applyCurrentVolume(buffer); } - } - private void applyCurrentVolume(ShortBuffer buffer) { - if (currentVolume == 100) { - return; + private void setupMultipliers(int activeVolume) { + if (currentVolume != activeVolume) { + currentVolume = activeVolume; + + if (activeVolume <= 150) { + float floatMultiplier = (float) Math.tan(activeVolume * 0.0079f); + integerMultiplier = (int) (floatMultiplier * 10000); + } else { + integerMultiplier = 24621 * activeVolume / 150; + } + } } - int endOffset = buffer.limit(); + private void applyCurrentVolume(ShortBuffer buffer) { + if (currentVolume == 100) { + return; + } - for (int i = buffer.position(); i < endOffset; i++) { - int value = buffer.get(i) * integerMultiplier / 10000; - buffer.put(i, (short) Math.max(-32767, Math.min(32767, value))); - } - } + int endOffset = buffer.limit(); - private void unapplyCurrentVolume(ShortBuffer buffer) { - if (integerMultiplier == 0) { - return; + for (int i = buffer.position(); i < endOffset; i++) { + int value = buffer.get(i) * integerMultiplier / 10000; + buffer.put(i, (short) Math.max(-32767, Math.min(32767, value))); + } } - int endOffset = buffer.limit(); + private void unapplyCurrentVolume(ShortBuffer buffer) { + if (integerMultiplier == 0) { + return; + } + + int endOffset = buffer.limit(); - for (int i = buffer.position(); i < endOffset; i++) { - int value = buffer.get(i) * 10000 / integerMultiplier; - buffer.put(i, (short) Math.max(-32767, Math.min(32767, value))); + for (int i = buffer.position(); i < endOffset; i++) { + int value = buffer.get(i) * 10000 / integerMultiplier; + buffer.put(i, (short) Math.max(-32767, Math.min(32767, value))); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/volume/VolumePostProcessor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/volume/VolumePostProcessor.java index 4f495d42d..6057911f5 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/volume/VolumePostProcessor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/filter/volume/VolumePostProcessor.java @@ -9,35 +9,35 @@ * Audio chunk post processor to apply selected volume. */ public class VolumePostProcessor implements AudioPostProcessor { - private final PcmVolumeProcessor volumeProcessor; - private final AudioProcessingContext context; - - /** - * @param context Configuration and output information for processing - */ - public VolumePostProcessor(AudioProcessingContext context) { - this.context = context; - this.volumeProcessor = new PcmVolumeProcessor(context.playerOptions.volumeLevel.get()); - } - - @Override - public void process(long timecode, ShortBuffer buffer) throws InterruptedException { - int currentVolume = context.playerOptions.volumeLevel.get(); - - if (currentVolume != volumeProcessor.getLastVolume()) { - AudioFrameVolumeChanger.apply(context); + private final PcmVolumeProcessor volumeProcessor; + private final AudioProcessingContext context; + + /** + * @param context Configuration and output information for processing + */ + public VolumePostProcessor(AudioProcessingContext context) { + this.context = context; + this.volumeProcessor = new PcmVolumeProcessor(context.playerOptions.volumeLevel.get()); } - // Volume 0 is stored in the frame with volume 100 buffer - if (currentVolume != 0) { - volumeProcessor.applyVolume(100, currentVolume, buffer); - } else { - volumeProcessor.setLastVolume(0); + @Override + public void process(long timecode, ShortBuffer buffer) throws InterruptedException { + int currentVolume = context.playerOptions.volumeLevel.get(); + + if (currentVolume != volumeProcessor.getLastVolume()) { + AudioFrameVolumeChanger.apply(context); + } + + // Volume 0 is stored in the frame with volume 100 buffer + if (currentVolume != 0) { + volumeProcessor.applyVolume(100, currentVolume, buffer); + } else { + volumeProcessor.setLastVolume(0); + } } - } - @Override - public void close() { - // Nothing to close here - } + @Override + public void close() { + // Nothing to close here + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/AudioDataFormat.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/AudioDataFormat.java index 8914094ea..77c420d27 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/AudioDataFormat.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/AudioDataFormat.java @@ -10,94 +10,94 @@ * Describes the format for audio with fixed chunk size. */ public abstract class AudioDataFormat { - /** - * Number of channels. - */ - public final int channelCount; - /** - * Sample rate (frequency). - */ - public final int sampleRate; - /** - * Number of samples in one chunk. - */ - public final int chunkSampleCount; - - /** - * @param channelCount Number of channels. - * @param sampleRate Sample rate (frequency). - * @param chunkSampleCount Number of samples in one chunk. - */ - public AudioDataFormat(int channelCount, int sampleRate, int chunkSampleCount) { - this.channelCount = channelCount; - this.sampleRate = sampleRate; - this.chunkSampleCount = chunkSampleCount; - } - - /** - * @return Total number of samples in one frame. - */ - public int totalSampleCount() { - return chunkSampleCount * channelCount; - } - - /** - * @return The duration in milliseconds of one frame in this format. - */ - public long frameDuration() { - return chunkSampleCount * 1000L / sampleRate; - } - - /** - * @return Name of the codec. - */ - public abstract String codecName(); - - /** - * @return Byte array representing a frame of silence in this format. - */ - public abstract byte[] silenceBytes(); - - /** - * @return Generally expected average size of a frame in this format. - */ - public abstract int expectedChunkSize(); - - /** - * @return Maximum size of a frame in this format. - */ - public abstract int maximumChunkSize(); - - /** - * @return Decoder to convert data in this format to short PCM. - */ - public abstract AudioChunkDecoder createDecoder(); - - /** - * @param configuration Configuration to use for encoding. - * @return Encoder to convert data in short PCM format to this format. - */ - public abstract AudioChunkEncoder createEncoder(AudioConfiguration configuration); - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - AudioDataFormat that = (AudioDataFormat) o; - - if (channelCount != that.channelCount) return false; - if (sampleRate != that.sampleRate) return false; - if (chunkSampleCount != that.chunkSampleCount) return false; - return Objects.equals(codecName(), that.codecName()); - } - - @Override - public int hashCode() { - int result = channelCount; - result = 31 * result + sampleRate; - result = 31 * result + chunkSampleCount; - result = 31 * result + codecName().hashCode(); - return result; - } + /** + * Number of channels. + */ + public final int channelCount; + /** + * Sample rate (frequency). + */ + public final int sampleRate; + /** + * Number of samples in one chunk. + */ + public final int chunkSampleCount; + + /** + * @param channelCount Number of channels. + * @param sampleRate Sample rate (frequency). + * @param chunkSampleCount Number of samples in one chunk. + */ + public AudioDataFormat(int channelCount, int sampleRate, int chunkSampleCount) { + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.chunkSampleCount = chunkSampleCount; + } + + /** + * @return Total number of samples in one frame. + */ + public int totalSampleCount() { + return chunkSampleCount * channelCount; + } + + /** + * @return The duration in milliseconds of one frame in this format. + */ + public long frameDuration() { + return chunkSampleCount * 1000L / sampleRate; + } + + /** + * @return Name of the codec. + */ + public abstract String codecName(); + + /** + * @return Byte array representing a frame of silence in this format. + */ + public abstract byte[] silenceBytes(); + + /** + * @return Generally expected average size of a frame in this format. + */ + public abstract int expectedChunkSize(); + + /** + * @return Maximum size of a frame in this format. + */ + public abstract int maximumChunkSize(); + + /** + * @return Decoder to convert data in this format to short PCM. + */ + public abstract AudioChunkDecoder createDecoder(); + + /** + * @param configuration Configuration to use for encoding. + * @return Encoder to convert data in short PCM format to this format. + */ + public abstract AudioChunkEncoder createEncoder(AudioConfiguration configuration); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AudioDataFormat that = (AudioDataFormat) o; + + if (channelCount != that.channelCount) return false; + if (sampleRate != that.sampleRate) return false; + if (chunkSampleCount != that.chunkSampleCount) return false; + return Objects.equals(codecName(), that.codecName()); + } + + @Override + public int hashCode() { + int result = channelCount; + result = 31 * result + sampleRate; + result = 31 * result + chunkSampleCount; + result = 31 * result + codecName().hashCode(); + return result; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/AudioDataFormatTools.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/AudioDataFormatTools.java index d1db5e445..bb53baf89 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/AudioDataFormatTools.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/AudioDataFormatTools.java @@ -9,23 +9,23 @@ */ public class AudioDataFormatTools { - /** - * @param format Audio data format to convert to JDK audio format - * @return JDK audio format for the specified format. - */ - public static AudioFormat toAudioFormat(AudioDataFormat format) { - if (format instanceof Pcm16AudioDataFormat) { - return new AudioFormat( - PCM_SIGNED, - format.sampleRate, - 16, - format.channelCount, - format.channelCount * 2, - format.sampleRate, - format.codecName().equals(Pcm16AudioDataFormat.CODEC_NAME_BE) - ); - } else { - throw new IllegalStateException("Only PCM is currently supported."); + /** + * @param format Audio data format to convert to JDK audio format + * @return JDK audio format for the specified format. + */ + public static AudioFormat toAudioFormat(AudioDataFormat format) { + if (format instanceof Pcm16AudioDataFormat) { + return new AudioFormat( + PCM_SIGNED, + format.sampleRate, + 16, + format.channelCount, + format.channelCount * 2, + format.sampleRate, + format.codecName().equals(Pcm16AudioDataFormat.CODEC_NAME_BE) + ); + } else { + throw new IllegalStateException("Only PCM is currently supported."); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/AudioPlayerInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/AudioPlayerInputStream.java index 7a14f33a4..94732b72c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/AudioPlayerInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/AudioPlayerInputStream.java @@ -1,9 +1,7 @@ package com.sedmelluq.discord.lavaplayer.format; import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; -import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayer; import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools; -import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.TrackStateListener; import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame; @@ -20,113 +18,113 @@ * Provides an audio player as an input stream. When nothing is playing, it returns silence instead of blocking. */ public class AudioPlayerInputStream extends InputStream { - private final AudioPlayer player; - private final AudioDataFormat format; - private final long timeout; - private final boolean provideSilence; - private ByteBuffer current; - - /** - * @param format Format of the frames expected from the player - * @param player The player to read frames from - * @param timeout Timeout till track stuck event is sent. Each time a new frame is required from the player, it asks - * for a frame with the specified timeout. In case that timeout is reached, the track stuck event is - * sent and if providing silence is enabled, silence is provided as the next frame. - * @param provideSilence True if the stream should return silence instead of blocking in case nothing is playing or - * read times out. - */ - public AudioPlayerInputStream(AudioDataFormat format, AudioPlayer player, long timeout, boolean provideSilence) { - this.format = format; - this.player = player; - this.timeout = timeout; - this.provideSilence = provideSilence; - } - - @Override - public int read() throws IOException { - ensureAvailable(); - return current.get(); - } - - @Override - public int read(byte[] buffer, int offset, int length) throws IOException { - int currentOffset = offset; - - while (currentOffset < length) { - ensureAvailable(); - - int piece = Math.min(current.remaining(), length - currentOffset); - current.get(buffer, currentOffset, piece); - currentOffset += piece; + private final AudioPlayer player; + private final AudioDataFormat format; + private final long timeout; + private final boolean provideSilence; + private ByteBuffer current; + + /** + * @param format Format of the frames expected from the player + * @param player The player to read frames from + * @param timeout Timeout till track stuck event is sent. Each time a new frame is required from the player, it asks + * for a frame with the specified timeout. In case that timeout is reached, the track stuck event is + * sent and if providing silence is enabled, silence is provided as the next frame. + * @param provideSilence True if the stream should return silence instead of blocking in case nothing is playing or + * read times out. + */ + public AudioPlayerInputStream(AudioDataFormat format, AudioPlayer player, long timeout, boolean provideSilence) { + this.format = format; + this.player = player; + this.timeout = timeout; + this.provideSilence = provideSilence; } - return currentOffset - offset; - } - - @Override - public int available() throws IOException { - return current != null ? current.remaining() : 0; - } - - @Override - public void close() throws IOException { - player.stopTrack(); - } - - /** - * Create an instance of AudioInputStream using an AudioPlayer as a source. - * - * @param player Format of the frames expected from the player - * @param format The player to read frames from - * @param stuckTimeout Timeout till track stuck event is sent and silence is returned on reading - * @param provideSilence Returns true if the stream should provide silence if no track is being played or when getting - * track frames times out. - * @return An audio input stream usable with JDK sound system - */ - public static AudioInputStream createStream(AudioPlayer player, AudioDataFormat format, long stuckTimeout, boolean provideSilence) { - AudioFormat jdkFormat = AudioDataFormatTools.toAudioFormat(format); - return new AudioInputStream(new AudioPlayerInputStream(format, player, stuckTimeout, provideSilence), jdkFormat, -1); - } - - private void ensureAvailable() throws IOException { - while (available() == 0) { - try { - attemptRetrieveFrame(); - } catch (TimeoutException e) { - notifyTrackStuck(); - } catch (InterruptedException e) { - ExceptionTools.keepInterrupted(e); - throw new InterruptedIOException(); - } - - if (available() == 0 && provideSilence) { - addFrame(format.silenceBytes()); - break; - } + @Override + public int read() throws IOException { + ensureAvailable(); + return current.get(); } - } - private void attemptRetrieveFrame() throws TimeoutException, InterruptedException { - AudioFrame frame = player.provide(timeout, TimeUnit.MILLISECONDS); + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + int currentOffset = offset; - if (frame != null) { - if (!format.equals(frame.getFormat())) { - throw new IllegalStateException("Frame read from the player uses a different format than expected."); - } + while (currentOffset < length) { + ensureAvailable(); - addFrame(frame.getData()); - } else if (!provideSilence) { - Thread.sleep(10); + int piece = Math.min(current.remaining(), length - currentOffset); + current.get(buffer, currentOffset, piece); + currentOffset += piece; + } + + return currentOffset - offset; + } + + @Override + public int available() throws IOException { + return current != null ? current.remaining() : 0; + } + + @Override + public void close() throws IOException { + player.stopTrack(); + } + + /** + * Create an instance of AudioInputStream using an AudioPlayer as a source. + * + * @param player Format of the frames expected from the player + * @param format The player to read frames from + * @param stuckTimeout Timeout till track stuck event is sent and silence is returned on reading + * @param provideSilence Returns true if the stream should provide silence if no track is being played or when getting + * track frames times out. + * @return An audio input stream usable with JDK sound system + */ + public static AudioInputStream createStream(AudioPlayer player, AudioDataFormat format, long stuckTimeout, boolean provideSilence) { + AudioFormat jdkFormat = AudioDataFormatTools.toAudioFormat(format); + return new AudioInputStream(new AudioPlayerInputStream(format, player, stuckTimeout, provideSilence), jdkFormat, -1); } - } - private void addFrame(byte[] data) { - current = ByteBuffer.wrap(data); - } + private void ensureAvailable() throws IOException { + while (available() == 0) { + try { + attemptRetrieveFrame(); + } catch (TimeoutException e) { + notifyTrackStuck(); + } catch (InterruptedException e) { + ExceptionTools.keepInterrupted(e); + throw new InterruptedIOException(); + } + + if (available() == 0 && provideSilence) { + addFrame(format.silenceBytes()); + break; + } + } + } + + private void attemptRetrieveFrame() throws TimeoutException, InterruptedException { + AudioFrame frame = player.provide(timeout, TimeUnit.MILLISECONDS); + + if (frame != null) { + if (!format.equals(frame.getFormat())) { + throw new IllegalStateException("Frame read from the player uses a different format than expected."); + } + + addFrame(frame.getData()); + } else if (!provideSilence) { + Thread.sleep(10); + } + } + + private void addFrame(byte[] data) { + current = ByteBuffer.wrap(data); + } - private void notifyTrackStuck() { - if (player instanceof TrackStateListener) { - ((TrackStateListener) player).onTrackStuck(player.getPlayingTrack(), timeout); + private void notifyTrackStuck() { + if (player instanceof TrackStateListener) { + ((TrackStateListener) player).onTrackStuck(player.getPlayingTrack(), timeout); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/OpusAudioDataFormat.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/OpusAudioDataFormat.java index 77f6f8c32..b494cad7f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/OpusAudioDataFormat.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/OpusAudioDataFormat.java @@ -10,62 +10,62 @@ * An {@link AudioDataFormat} for OPUS. */ public class OpusAudioDataFormat extends AudioDataFormat { - public static final String CODEC_NAME = "OPUS"; + public static final String CODEC_NAME = "OPUS"; - private static final byte[] SILENT_OPUS_FRAME = new byte[] {(byte) 0xFC, (byte) 0xFF, (byte) 0xFE}; + private static final byte[] SILENT_OPUS_FRAME = new byte[]{(byte) 0xFC, (byte) 0xFF, (byte) 0xFE}; - private final int maximumChunkSize; - private final int expectedChunkSize; + private final int maximumChunkSize; + private final int expectedChunkSize; - /** - * @param channelCount Number of channels. - * @param sampleRate Sample rate (frequency). - * @param chunkSampleCount Number of samples in one chunk. - */ - public OpusAudioDataFormat(int channelCount, int sampleRate, int chunkSampleCount) { - super(channelCount, sampleRate, chunkSampleCount); + /** + * @param channelCount Number of channels. + * @param sampleRate Sample rate (frequency). + * @param chunkSampleCount Number of samples in one chunk. + */ + public OpusAudioDataFormat(int channelCount, int sampleRate, int chunkSampleCount) { + super(channelCount, sampleRate, chunkSampleCount); - this.maximumChunkSize = 32 + 1536 * chunkSampleCount / 960; - this.expectedChunkSize = 32 + 512 * chunkSampleCount / 960; - } + this.maximumChunkSize = 32 + 1536 * chunkSampleCount / 960; + this.expectedChunkSize = 32 + 512 * chunkSampleCount / 960; + } - @Override - public String codecName() { - return CODEC_NAME; - } + @Override + public String codecName() { + return CODEC_NAME; + } - @Override - public byte[] silenceBytes() { - return SILENT_OPUS_FRAME; - } + @Override + public byte[] silenceBytes() { + return SILENT_OPUS_FRAME; + } - @Override - public int expectedChunkSize() { - return expectedChunkSize; - } + @Override + public int expectedChunkSize() { + return expectedChunkSize; + } - @Override - public int maximumChunkSize() { - return maximumChunkSize; - } + @Override + public int maximumChunkSize() { + return maximumChunkSize; + } - @Override - public AudioChunkDecoder createDecoder() { - return new OpusChunkDecoder(this); - } + @Override + public AudioChunkDecoder createDecoder() { + return new OpusChunkDecoder(this); + } - @Override - public AudioChunkEncoder createEncoder(AudioConfiguration configuration) { - return new OpusChunkEncoder(configuration, this); - } + @Override + public AudioChunkEncoder createEncoder(AudioConfiguration configuration) { + return new OpusChunkEncoder(configuration, this); + } - @Override - public boolean equals(Object o) { - return this == o || o != null && getClass() == o.getClass() && super.equals(o); - } + @Override + public boolean equals(Object o) { + return this == o || o != null && getClass() == o.getClass() && super.equals(o); + } - @Override - public int hashCode() { - return super.hashCode(); - } + @Override + public int hashCode() { + return super.hashCode(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/Pcm16AudioDataFormat.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/Pcm16AudioDataFormat.java index 2a2a7ae2f..79c567659 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/Pcm16AudioDataFormat.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/Pcm16AudioDataFormat.java @@ -10,61 +10,61 @@ * An {@link AudioDataFormat} for 16-bit signed PCM. */ public class Pcm16AudioDataFormat extends AudioDataFormat { - public static final String CODEC_NAME_BE = "PCM_S16_BE"; - public static final String CODEC_NAME_LE = "PCM_S16_LE"; + public static final String CODEC_NAME_BE = "PCM_S16_BE"; + public static final String CODEC_NAME_LE = "PCM_S16_LE"; - private final boolean bigEndian; - private final byte[] silenceBytes; + private final boolean bigEndian; + private final byte[] silenceBytes; - /** - * @param channelCount Number of channels. - * @param sampleRate Sample rate (frequency). - * @param chunkSampleCount Number of samples in one chunk. - * @param bigEndian Whether the samples are in big-endian format (as opposed to little-endian). - */ - public Pcm16AudioDataFormat(int channelCount, int sampleRate, int chunkSampleCount, boolean bigEndian) { - super(channelCount, sampleRate, chunkSampleCount); - this.bigEndian = bigEndian; - this.silenceBytes = new byte[channelCount * chunkSampleCount * 2]; - } + /** + * @param channelCount Number of channels. + * @param sampleRate Sample rate (frequency). + * @param chunkSampleCount Number of samples in one chunk. + * @param bigEndian Whether the samples are in big-endian format (as opposed to little-endian). + */ + public Pcm16AudioDataFormat(int channelCount, int sampleRate, int chunkSampleCount, boolean bigEndian) { + super(channelCount, sampleRate, chunkSampleCount); + this.bigEndian = bigEndian; + this.silenceBytes = new byte[channelCount * chunkSampleCount * 2]; + } - @Override - public String codecName() { - return CODEC_NAME_BE; - } + @Override + public String codecName() { + return CODEC_NAME_BE; + } - @Override - public byte[] silenceBytes() { - return silenceBytes; - } + @Override + public byte[] silenceBytes() { + return silenceBytes; + } - @Override - public int expectedChunkSize() { - return silenceBytes.length; - } + @Override + public int expectedChunkSize() { + return silenceBytes.length; + } - @Override - public int maximumChunkSize() { - return silenceBytes.length; - } + @Override + public int maximumChunkSize() { + return silenceBytes.length; + } - @Override - public AudioChunkDecoder createDecoder() { - return new PcmChunkDecoder(this, bigEndian); - } + @Override + public AudioChunkDecoder createDecoder() { + return new PcmChunkDecoder(this, bigEndian); + } - @Override - public AudioChunkEncoder createEncoder(AudioConfiguration configuration) { - return new PcmChunkEncoder(this, bigEndian); - } + @Override + public AudioChunkEncoder createEncoder(AudioConfiguration configuration) { + return new PcmChunkEncoder(this, bigEndian); + } - @Override - public boolean equals(Object o) { - return this == o || o != null && getClass() == o.getClass() && super.equals(o); - } + @Override + public boolean equals(Object o) { + return this == o || o != null && getClass() == o.getClass() && super.equals(o); + } - @Override - public int hashCode() { - return super.hashCode(); - } + @Override + public int hashCode() { + return super.hashCode(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/StandardAudioDataFormats.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/StandardAudioDataFormats.java index 3b5c125d0..40cdb4740 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/StandardAudioDataFormats.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/StandardAudioDataFormats.java @@ -4,24 +4,24 @@ * Standard output formats compatible with Discord. */ public class StandardAudioDataFormats { - /** - * The Opus configuration used by both Discord and YouTube. Default. - */ - public static final AudioDataFormat DISCORD_OPUS = new OpusAudioDataFormat(2, 48000, 960); - /** - * Signed 16-bit big-endian PCM format matching the parameters used by Discord. - */ - public static final AudioDataFormat DISCORD_PCM_S16_BE = new Pcm16AudioDataFormat(2, 48000, 960, true); - /** - * Signed 16-bit little-endian PCM format matching the parameters used by Discord. - */ - public static final AudioDataFormat DISCORD_PCM_S16_LE = new Pcm16AudioDataFormat(2, 48000, 960, false); - /** - * Signed 16-bit big-endian PCM format matching with the most common sample rate. - */ - public static final AudioDataFormat COMMON_PCM_S16_BE = new Pcm16AudioDataFormat(2, 44100, 960, true); - /** - * Signed 16-bit little-endian PCM format matching with the most common sample rate. - */ - public static final AudioDataFormat COMMON_PCM_S16_LE = new Pcm16AudioDataFormat(2, 44100, 960, false); + /** + * The Opus configuration used by both Discord and YouTube. Default. + */ + public static final AudioDataFormat DISCORD_OPUS = new OpusAudioDataFormat(2, 48000, 960); + /** + * Signed 16-bit big-endian PCM format matching the parameters used by Discord. + */ + public static final AudioDataFormat DISCORD_PCM_S16_BE = new Pcm16AudioDataFormat(2, 48000, 960, true); + /** + * Signed 16-bit little-endian PCM format matching the parameters used by Discord. + */ + public static final AudioDataFormat DISCORD_PCM_S16_LE = new Pcm16AudioDataFormat(2, 48000, 960, false); + /** + * Signed 16-bit big-endian PCM format matching with the most common sample rate. + */ + public static final AudioDataFormat COMMON_PCM_S16_BE = new Pcm16AudioDataFormat(2, 44100, 960, true); + /** + * Signed 16-bit little-endian PCM format matching with the most common sample rate. + */ + public static final AudioDataFormat COMMON_PCM_S16_LE = new Pcm16AudioDataFormat(2, 44100, 960, false); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/AudioChunkDecoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/AudioChunkDecoder.java index 07482e0a5..9d2833fd6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/AudioChunkDecoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/AudioChunkDecoder.java @@ -6,14 +6,14 @@ * Decodes one chunk of audio into internal PCM format. */ public interface AudioChunkDecoder { - /** - * @param encoded Encoded bytes - * @param buffer Output buffer for the PCM data - */ - void decode(byte[] encoded, ShortBuffer buffer); + /** + * @param encoded Encoded bytes + * @param buffer Output buffer for the PCM data + */ + void decode(byte[] encoded, ShortBuffer buffer); - /** - * Frees up all held resources. - */ - void close(); + /** + * Frees up all held resources. + */ + void close(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/AudioChunkEncoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/AudioChunkEncoder.java index ecc5bba38..00edf789d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/AudioChunkEncoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/AudioChunkEncoder.java @@ -7,20 +7,20 @@ * Encodes one chunk of audio from internal PCM format. */ public interface AudioChunkEncoder { - /** - * @param buffer Input buffer containing the PCM samples. - * @return Encoded bytes - */ - byte[] encode(ShortBuffer buffer); + /** + * @param buffer Input buffer containing the PCM samples. + * @return Encoded bytes + */ + byte[] encode(ShortBuffer buffer); - /** - * @param buffer Input buffer containing the PCM samples. - * @param out Output buffer to store the encoded bytes in - */ - void encode(ShortBuffer buffer, ByteBuffer out); + /** + * @param buffer Input buffer containing the PCM samples. + * @param out Output buffer to store the encoded bytes in + */ + void encode(ShortBuffer buffer, ByteBuffer out); - /** - * Frees up all held resources. - */ - void close(); + /** + * Frees up all held resources. + */ + void close(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/OpusChunkDecoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/OpusChunkDecoder.java index 48e4b404b..773210cc5 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/OpusChunkDecoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/OpusChunkDecoder.java @@ -10,29 +10,29 @@ * Audio chunk decoder for Opus codec. */ public class OpusChunkDecoder implements AudioChunkDecoder { - private final OpusDecoder decoder; - private final ByteBuffer encodedBuffer; + private final OpusDecoder decoder; + private final ByteBuffer encodedBuffer; - /** - * @param format Source audio format. - */ - public OpusChunkDecoder(AudioDataFormat format) { - encodedBuffer = ByteBuffer.allocateDirect(4096); - decoder = new OpusDecoder(format.sampleRate, format.channelCount); - } + /** + * @param format Source audio format. + */ + public OpusChunkDecoder(AudioDataFormat format) { + encodedBuffer = ByteBuffer.allocateDirect(4096); + decoder = new OpusDecoder(format.sampleRate, format.channelCount); + } - @Override - public void decode(byte[] encoded, ShortBuffer buffer) { - encodedBuffer.clear(); - encodedBuffer.put(encoded); - encodedBuffer.flip(); + @Override + public void decode(byte[] encoded, ShortBuffer buffer) { + encodedBuffer.clear(); + encodedBuffer.put(encoded); + encodedBuffer.flip(); - buffer.clear(); - decoder.decode(encodedBuffer, buffer); - } + buffer.clear(); + decoder.decode(encodedBuffer, buffer); + } - @Override - public void close() { - decoder.close(); - } + @Override + public void close() { + decoder.close(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/OpusChunkEncoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/OpusChunkEncoder.java index 6916e0b98..47ddd59a5 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/OpusChunkEncoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/OpusChunkEncoder.java @@ -11,46 +11,46 @@ * Audio chunk encoder for Opus codec. */ public class OpusChunkEncoder implements AudioChunkEncoder { - private final AudioDataFormat format; - private final OpusEncoder encoder; - private final ByteBuffer encodedBuffer; - - /** - * @param configuration Audio configuration used for configuring the encoder - * @param format Target audio format. - */ - public OpusChunkEncoder(AudioConfiguration configuration, AudioDataFormat format) { - encodedBuffer = ByteBuffer.allocateDirect(format.maximumChunkSize()); - encoder = new OpusEncoder(format.sampleRate, format.channelCount, configuration.getOpusEncodingQuality()); - this.format = format; - } - - @Override - public byte[] encode(ShortBuffer buffer) { - encoder.encode(buffer, format.chunkSampleCount, encodedBuffer); - - byte[] bytes = new byte[encodedBuffer.remaining()]; - encodedBuffer.get(bytes); - return bytes; - } - - @Override - public void encode(ShortBuffer buffer, ByteBuffer outBuffer) { - if (outBuffer.isDirect()) { - encoder.encode(buffer, format.chunkSampleCount, outBuffer); - } else { - encoder.encode(buffer, format.chunkSampleCount, encodedBuffer); - - int length = encodedBuffer.remaining(); - encodedBuffer.get(outBuffer.array(), 0, length); - - outBuffer.position(0); - outBuffer.limit(length); + private final AudioDataFormat format; + private final OpusEncoder encoder; + private final ByteBuffer encodedBuffer; + + /** + * @param configuration Audio configuration used for configuring the encoder + * @param format Target audio format. + */ + public OpusChunkEncoder(AudioConfiguration configuration, AudioDataFormat format) { + encodedBuffer = ByteBuffer.allocateDirect(format.maximumChunkSize()); + encoder = new OpusEncoder(format.sampleRate, format.channelCount, configuration.getOpusEncodingQuality()); + this.format = format; } - } - @Override - public void close() { - encoder.close(); - } + @Override + public byte[] encode(ShortBuffer buffer) { + encoder.encode(buffer, format.chunkSampleCount, encodedBuffer); + + byte[] bytes = new byte[encodedBuffer.remaining()]; + encodedBuffer.get(bytes); + return bytes; + } + + @Override + public void encode(ShortBuffer buffer, ByteBuffer outBuffer) { + if (outBuffer.isDirect()) { + encoder.encode(buffer, format.chunkSampleCount, outBuffer); + } else { + encoder.encode(buffer, format.chunkSampleCount, encodedBuffer); + + int length = encodedBuffer.remaining(); + encodedBuffer.get(outBuffer.array(), 0, length); + + outBuffer.position(0); + outBuffer.limit(length); + } + } + + @Override + public void close() { + encoder.close(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/PcmChunkDecoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/PcmChunkDecoder.java index 6296abcb5..cc23266c6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/PcmChunkDecoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/PcmChunkDecoder.java @@ -10,39 +10,39 @@ * Audio chunk decoder for PCM data. */ public class PcmChunkDecoder implements AudioChunkDecoder { - private final ByteBuffer encodedAsByte; - private final ShortBuffer encodedAsShort; - - /** - * @param format Source audio format. - * @param bigEndian Whether the samples are in big-endian format (as opposed to little-endian). - */ - public PcmChunkDecoder(AudioDataFormat format, boolean bigEndian) { - this.encodedAsByte = ByteBuffer.allocate(format.maximumChunkSize()); - - if (!bigEndian) { - encodedAsByte.order(ByteOrder.LITTLE_ENDIAN); - } + private final ByteBuffer encodedAsByte; + private final ShortBuffer encodedAsShort; + + /** + * @param format Source audio format. + * @param bigEndian Whether the samples are in big-endian format (as opposed to little-endian). + */ + public PcmChunkDecoder(AudioDataFormat format, boolean bigEndian) { + this.encodedAsByte = ByteBuffer.allocate(format.maximumChunkSize()); + + if (!bigEndian) { + encodedAsByte.order(ByteOrder.LITTLE_ENDIAN); + } - this.encodedAsShort = encodedAsByte.asShortBuffer(); - } + this.encodedAsShort = encodedAsByte.asShortBuffer(); + } - @Override - public void decode(byte[] encoded, ShortBuffer buffer) { - buffer.clear(); + @Override + public void decode(byte[] encoded, ShortBuffer buffer) { + buffer.clear(); - encodedAsByte.clear(); - encodedAsByte.put(encoded); + encodedAsByte.clear(); + encodedAsByte.put(encoded); - encodedAsShort.clear(); - encodedAsShort.limit(encodedAsByte.position() / 2); + encodedAsShort.clear(); + encodedAsShort.limit(encodedAsByte.position() / 2); - buffer.put(encodedAsShort); - buffer.rewind(); - } + buffer.put(encodedAsShort); + buffer.rewind(); + } - @Override - public void close() { - // Nothing to close here - } + @Override + public void close() { + // Nothing to close here + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/PcmChunkEncoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/PcmChunkEncoder.java index 9a35447d2..af2d7c51c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/PcmChunkEncoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/format/transcoder/PcmChunkEncoder.java @@ -10,55 +10,55 @@ * Audio chunk encoder for PCM data. */ public class PcmChunkEncoder implements AudioChunkEncoder { - private final ByteBuffer encoded; - private final ShortBuffer encodedAsShort; - - /** - * @param format Target audio format. - * @param bigEndian Whether the samples are in big-endian format (as opposed to little-endian). - */ - public PcmChunkEncoder(AudioDataFormat format, boolean bigEndian) { - this.encoded = ByteBuffer.allocate(format.maximumChunkSize()); - - if (!bigEndian) { - encoded.order(ByteOrder.LITTLE_ENDIAN); - } + private final ByteBuffer encoded; + private final ShortBuffer encodedAsShort; + + /** + * @param format Target audio format. + * @param bigEndian Whether the samples are in big-endian format (as opposed to little-endian). + */ + public PcmChunkEncoder(AudioDataFormat format, boolean bigEndian) { + this.encoded = ByteBuffer.allocate(format.maximumChunkSize()); - this.encodedAsShort = encoded.asShortBuffer(); - } + if (!bigEndian) { + encoded.order(ByteOrder.LITTLE_ENDIAN); + } - @Override - public byte[] encode(ShortBuffer buffer) { - buffer.mark(); + this.encodedAsShort = encoded.asShortBuffer(); + } - encodedAsShort.clear(); - encodedAsShort.put(buffer); + @Override + public byte[] encode(ShortBuffer buffer) { + buffer.mark(); - encoded.clear(); - encoded.limit(encodedAsShort.position() * 2); + encodedAsShort.clear(); + encodedAsShort.put(buffer); - byte[] encodedBytes = new byte[encoded.remaining()]; - encoded.get(encodedBytes); + encoded.clear(); + encoded.limit(encodedAsShort.position() * 2); - buffer.reset(); - return encodedBytes; - } + byte[] encodedBytes = new byte[encoded.remaining()]; + encoded.get(encodedBytes); - @Override - public void encode(ShortBuffer buffer, ByteBuffer out) { - buffer.mark(); + buffer.reset(); + return encodedBytes; + } - encodedAsShort.clear(); - encodedAsShort.put(buffer); + @Override + public void encode(ShortBuffer buffer, ByteBuffer out) { + buffer.mark(); - out.put(encoded.array(), 0, encodedAsShort.position() * 2); - out.flip(); + encodedAsShort.clear(); + encodedAsShort.put(buffer); - buffer.reset(); - } + out.put(encoded.array(), 0, encodedAsShort.position() * 2); + out.flip(); - @Override - public void close() { - // Nothing to close here - } + buffer.reset(); + } + + @Override + public void close() { + // Nothing to close here + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/ConnectorNativeLibLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/ConnectorNativeLibLoader.java index 4efbd4859..fd5cdaf22 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/ConnectorNativeLibLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/ConnectorNativeLibLoader.java @@ -7,18 +7,18 @@ * Methods for loading the connector library. */ public class ConnectorNativeLibLoader { - private static final NativeLibraryLoader[] loaders = new NativeLibraryLoader[] { - NativeLibraryLoader.createFiltered(ConnectorNativeLibLoader.class, "libmpg123-0", - it -> it.osType == DefaultOperatingSystemTypes.WINDOWS), - NativeLibraryLoader.create(ConnectorNativeLibLoader.class, "connector") - }; + private static final NativeLibraryLoader[] loaders = new NativeLibraryLoader[]{ + NativeLibraryLoader.createFiltered(ConnectorNativeLibLoader.class, "libmpg123-0", + it -> it.osType == DefaultOperatingSystemTypes.WINDOWS), + NativeLibraryLoader.create(ConnectorNativeLibLoader.class, "connector") + }; - /** - * Loads the connector library with its dependencies for the current system - */ - public static void loadConnectorLibrary() { - for (NativeLibraryLoader loader : loaders) { - loader.load(); + /** + * Loads the connector library with its dependencies for the current system + */ + public static void loadConnectorLibrary() { + for (NativeLibraryLoader loader : loaders) { + loader.load(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/aac/AacDecoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/aac/AacDecoder.java index ed796bbdd..1587239e6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/aac/AacDecoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/aac/AacDecoder.java @@ -14,228 +14,240 @@ * layer. The only AAC type verified to work with this is AAC_LC. */ public class AacDecoder extends NativeResourceHolder { - private static final int TRANSPORT_NONE = 0; - - private static final ShortBuffer NO_BUFFER = ByteBuffer.allocateDirect(0).asShortBuffer(); - - private static final int ERROR_NOT_ENOUGH_BITS = 4098; - private static final int ERROR_OUTPUT_BUFFER_TOO_SMALL = 8204; - - public static final int AAC_LC = 2; - - private final AacDecoderLibrary library; - private final long instance; - - /** - * Create a new decoder. - */ - public AacDecoder() { - library = AacDecoderLibrary.getInstance(); - instance = library.create(TRANSPORT_NONE); - } - - /** - * Configure the decoder. Must be called before the first decoding. - * - * @param objectType Audio object type as defined for Audio Specific Config: https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio - * @param frequency Frequency of samples in Hz - * @param channels Number of channels. - * @throws IllegalStateException If the decoder has already been closed. - */ - public int configure(int objectType, int frequency, int channels) { - long buffer = encodeConfiguration(objectType, frequency, channels); - - return configureRaw(buffer); - } - - /** - * Configure the decoder. Must be called before the first decoding. - * - * @param config Raw ASC format configuration - * @throws IllegalStateException If the decoder has already been closed. - */ - public int configure(byte[] config) { - if (config.length > 8) { - throw new IllegalArgumentException("Cannot process a header larger than size 8"); - } + private static final int TRANSPORT_NONE = 0; - long buffer = 0; - for (int i = 0; i < config.length; i++) { - buffer |= ((long) config[i]) << (i << 3); - } + private static final ShortBuffer NO_BUFFER = ByteBuffer.allocateDirect(0).asShortBuffer(); - return configureRaw(buffer); - } + private static final int ERROR_NOT_ENOUGH_BITS = 4098; + private static final int ERROR_OUTPUT_BUFFER_TOO_SMALL = 8204; - private synchronized int configureRaw(long buffer) { - checkNotReleased(); + public static final int AAC_LC = 2; - if (ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN) { - buffer = Long.reverseBytes(buffer); + private final AacDecoderLibrary library; + private final long instance; + + /** + * Create a new decoder. + */ + public AacDecoder() { + library = AacDecoderLibrary.getInstance(); + instance = library.create(TRANSPORT_NONE); } - return library.configure(instance, buffer); - } + /** + * Configure the decoder. Must be called before the first decoding. + * + * @param objectType Audio object type as defined for Audio Specific Config: https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio + * @param frequency Frequency of samples in Hz + * @param channels Number of channels. + * @throws IllegalStateException If the decoder has already been closed. + */ + public int configure(int objectType, int frequency, int channels) { + long buffer = encodeConfiguration(objectType, frequency, channels); - private static long encodeConfiguration(int objectType, int frequency, int channels) { - try { - ByteBuffer buffer = ByteBuffer.allocate(8); - buffer.order(ByteOrder.nativeOrder()); - BitStreamWriter bitWriter = new BitStreamWriter(new ByteBufferOutputStream(buffer)); + return configureRaw(buffer); + } - bitWriter.write(objectType, 5); + /** + * Configure the decoder. Must be called before the first decoding. + * + * @param config Raw ASC format configuration + * @throws IllegalStateException If the decoder has already been closed. + */ + public int configure(byte[] config) { + if (config.length > 8) { + throw new IllegalArgumentException("Cannot process a header larger than size 8"); + } - int frequencyIndex = getFrequencyIndex(frequency); - bitWriter.write(frequencyIndex, 4); + long buffer = 0; + for (int i = 0; i < config.length; i++) { + buffer |= ((long) config[i]) << (i << 3); + } - if (frequencyIndex == 15) { - bitWriter.write(frequency, 24); - } + return configureRaw(buffer); + } - bitWriter.write(channels, 4); - bitWriter.flush(); + private synchronized int configureRaw(long buffer) { + checkNotReleased(); - buffer.clear(); + if (ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN) { + buffer = Long.reverseBytes(buffer); + } - return buffer.getLong(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static int getFrequencyIndex(int frequency) { - switch (frequency) { - case 96000: return 0; - case 88200: return 1; - case 64000: return 2; - case 48000: return 3; - case 44100: return 4; - case 32000: return 5; - case 24000: return 6; - case 22050: return 7; - case 16000: return 8; - case 12000: return 9; - case 11025: return 10; - case 8000: return 11; - case 7350: return 12; - default: return 15; - } - } - - /** - * Fill the internal decoding buffer with the bytes from the buffer. May consume less bytes than the buffer provides. - * - * @param buffer DirectBuffer which contains the bytes to be added. Position and limit are respected and position is - * updated as a result of this operation. - * @return The number of bytes consumed from the provided buffer. - * - * @throws IllegalArgumentException If the buffer is not a DirectBuffer. - * @throws IllegalStateException If the decoder has already been closed. - */ - public synchronized int fill(ByteBuffer buffer) { - checkNotReleased(); - - if (!buffer.isDirect()) { - throw new IllegalArgumentException("Buffer argument must be a direct buffer."); + return library.configure(instance, buffer); } - int readBytes = library.fill(instance, buffer, buffer.position(), buffer.limit()); - if (readBytes < 0) { - throw new IllegalStateException("Filling decoder failed with error " + (-readBytes)); - } + private static long encodeConfiguration(int objectType, int frequency, int channels) { + try { + ByteBuffer buffer = ByteBuffer.allocate(8); + buffer.order(ByteOrder.nativeOrder()); + BitStreamWriter bitWriter = new BitStreamWriter(new ByteBufferOutputStream(buffer)); - buffer.position(buffer.position() + readBytes); - return readBytes; - } - - /** - * Decode a frame of audio into the given buffer. - * - * @param buffer DirectBuffer of signed PCM samples where the decoded frame will be stored. The buffer size must be at - * least of size frameSize * channels * 2. Buffer position and limit are ignored and not - * updated. - * @param flush Whether all the buffered data should be flushed, set to true if no more input is expected. - * @return True if the frame buffer was filled, false if there was not enough input for decoding a full frame. - * - * @throws IllegalArgumentException If the buffer is not a DirectBuffer. - * @throws IllegalStateException If the decoding library returns an error other than running out of input data. - * @throws IllegalStateException If the decoder has already been closed. - */ - public synchronized boolean decode(ShortBuffer buffer, boolean flush) { - checkNotReleased(); - - if (!buffer.isDirect()) { - throw new IllegalArgumentException("Buffer argument must be a direct buffer."); - } + bitWriter.write(objectType, 5); - int result = library.decode(instance, buffer, buffer.capacity(), flush); - if (result != 0 && result != ERROR_NOT_ENOUGH_BITS) { - throw new IllegalStateException("Error from decoder " + result); - } + int frequencyIndex = getFrequencyIndex(frequency); + bitWriter.write(frequencyIndex, 4); - return result == 0; - } + if (frequencyIndex == 15) { + bitWriter.write(frequency, 24); + } - /** - * @return Correct stream info. The values passed to configure method do not account for SBR and PS and detecting - * these is a part of the decoding process. If there was not enough input for decoding a full frame, null is - * returned. - * @throws IllegalStateException If the decoder result produced an unexpected error. - */ - public synchronized StreamInfo resolveStreamInfo() { - checkNotReleased(); + bitWriter.write(channels, 4); + bitWriter.flush(); - int result = library.decode(instance, NO_BUFFER, 0, false); + buffer.clear(); - if (result == ERROR_NOT_ENOUGH_BITS) { - return null; - } else if (result != ERROR_OUTPUT_BUFFER_TOO_SMALL) { - throw new IllegalStateException("Expected decoding to halt, got: " + result); + return buffer.getLong(); + } catch (IOException e) { + throw new RuntimeException(e); + } } - long combinedValue = library.getStreamInfo(instance); - if (combinedValue == 0) { - throw new IllegalStateException("Native library failed to detect stream info."); + private static int getFrequencyIndex(int frequency) { + switch (frequency) { + case 96000: + return 0; + case 88200: + return 1; + case 64000: + return 2; + case 48000: + return 3; + case 44100: + return 4; + case 32000: + return 5; + case 24000: + return 6; + case 22050: + return 7; + case 16000: + return 8; + case 12000: + return 9; + case 11025: + return 10; + case 8000: + return 11; + case 7350: + return 12; + default: + return 15; + } } - return new StreamInfo( - (int) (combinedValue >>> 32L), - (int) (combinedValue & 0xFFFF), - (int) ((combinedValue >>> 16L) & 0xFFFF) - ); - } - - @Override - protected void freeResources() { - library.destroy(instance); - } - - /** - * AAC stream information. - */ - public static class StreamInfo { /** - * Sample rate (adjusted to SBR) of the current stream. + * Fill the internal decoding buffer with the bytes from the buffer. May consume less bytes than the buffer provides. + * + * @param buffer DirectBuffer which contains the bytes to be added. Position and limit are respected and position is + * updated as a result of this operation. + * @return The number of bytes consumed from the provided buffer. + * @throws IllegalArgumentException If the buffer is not a DirectBuffer. + * @throws IllegalStateException If the decoder has already been closed. */ - public final int sampleRate; + public synchronized int fill(ByteBuffer buffer) { + checkNotReleased(); + + if (!buffer.isDirect()) { + throw new IllegalArgumentException("Buffer argument must be a direct buffer."); + } + + int readBytes = library.fill(instance, buffer, buffer.position(), buffer.limit()); + if (readBytes < 0) { + throw new IllegalStateException("Filling decoder failed with error " + (-readBytes)); + } + + buffer.position(buffer.position() + readBytes); + return readBytes; + } + /** - * Channel count (adjusted to PS) of the current stream. + * Decode a frame of audio into the given buffer. + * + * @param buffer DirectBuffer of signed PCM samples where the decoded frame will be stored. The buffer size must be at + * least of size frameSize * channels * 2. Buffer position and limit are ignored and not + * updated. + * @param flush Whether all the buffered data should be flushed, set to true if no more input is expected. + * @return True if the frame buffer was filled, false if there was not enough input for decoding a full frame. + * @throws IllegalArgumentException If the buffer is not a DirectBuffer. + * @throws IllegalStateException If the decoding library returns an error other than running out of input data. + * @throws IllegalStateException If the decoder has already been closed. */ - public final int channels; + public synchronized boolean decode(ShortBuffer buffer, boolean flush) { + checkNotReleased(); + + if (!buffer.isDirect()) { + throw new IllegalArgumentException("Buffer argument must be a direct buffer."); + } + + int result = library.decode(instance, buffer, buffer.capacity(), flush); + if (result != 0 && result != ERROR_NOT_ENOUGH_BITS) { + throw new IllegalStateException("Error from decoder " + result); + } + + return result == 0; + } + /** - * Number of samples per channel per frame. + * @return Correct stream info. The values passed to configure method do not account for SBR and PS and detecting + * these is a part of the decoding process. If there was not enough input for decoding a full frame, null is + * returned. + * @throws IllegalStateException If the decoder result produced an unexpected error. */ - public final int frameSize; + public synchronized StreamInfo resolveStreamInfo() { + checkNotReleased(); + + int result = library.decode(instance, NO_BUFFER, 0, false); + + if (result == ERROR_NOT_ENOUGH_BITS) { + return null; + } else if (result != ERROR_OUTPUT_BUFFER_TOO_SMALL) { + throw new IllegalStateException("Expected decoding to halt, got: " + result); + } + + long combinedValue = library.getStreamInfo(instance); + if (combinedValue == 0) { + throw new IllegalStateException("Native library failed to detect stream info."); + } + + return new StreamInfo( + (int) (combinedValue >>> 32L), + (int) (combinedValue & 0xFFFF), + (int) ((combinedValue >>> 16L) & 0xFFFF) + ); + } + + @Override + protected void freeResources() { + library.destroy(instance); + } /** - * @param sampleRate Sample rate (adjusted to SBR) of the current stream. - * @param channels Channel count (adjusted to PS) of the current stream. - * @param frameSize Number of samples per channel per frame. + * AAC stream information. */ - public StreamInfo(int sampleRate, int channels, int frameSize) { - this.sampleRate = sampleRate; - this.channels = channels; - this.frameSize = frameSize; + public static class StreamInfo { + /** + * Sample rate (adjusted to SBR) of the current stream. + */ + public final int sampleRate; + /** + * Channel count (adjusted to PS) of the current stream. + */ + public final int channels; + /** + * Number of samples per channel per frame. + */ + public final int frameSize; + + /** + * @param sampleRate Sample rate (adjusted to SBR) of the current stream. + * @param channels Channel count (adjusted to PS) of the current stream. + * @param frameSize Number of samples per channel per frame. + */ + public StreamInfo(int sampleRate, int channels, int frameSize) { + this.sampleRate = sampleRate; + this.channels = channels; + this.frameSize = frameSize; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/aac/AacDecoderLibrary.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/aac/AacDecoderLibrary.java index 3ee11eb88..6328aed26 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/aac/AacDecoderLibrary.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/aac/AacDecoderLibrary.java @@ -1,28 +1,29 @@ package com.sedmelluq.discord.lavaplayer.natives.aac; import com.sedmelluq.discord.lavaplayer.natives.ConnectorNativeLibLoader; + import java.nio.ByteBuffer; import java.nio.ShortBuffer; class AacDecoderLibrary { - private AacDecoderLibrary() { + private AacDecoderLibrary() { - } + } - static AacDecoderLibrary getInstance() { - ConnectorNativeLibLoader.loadConnectorLibrary(); - return new AacDecoderLibrary(); - } + static AacDecoderLibrary getInstance() { + ConnectorNativeLibLoader.loadConnectorLibrary(); + return new AacDecoderLibrary(); + } - native long create(int transportType); + native long create(int transportType); - native void destroy(long instance); + native void destroy(long instance); - native int configure(long instance, long bufferData); + native int configure(long instance, long bufferData); - native int fill(long instance, ByteBuffer directBuffer, int offset, int length); + native int fill(long instance, ByteBuffer directBuffer, int offset, int length); - native int decode(long instance, ShortBuffer directBuffer, int length, boolean flush); + native int decode(long instance, ShortBuffer directBuffer, int length, boolean flush); - native long getStreamInfo(long instance); + native long getStreamInfo(long instance); } 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 8565d1ae9..6df56ac14 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 @@ -1,6 +1,7 @@ package com.sedmelluq.discord.lavaplayer.natives.mp3; import com.sedmelluq.lava.common.natives.NativeResourceHolder; + import java.nio.ByteBuffer; import java.nio.ShortBuffer; @@ -8,210 +9,249 @@ * A wrapper around the native methods of OpusDecoderLibrary. */ public class Mp3Decoder extends NativeResourceHolder { - public static final long MPEG1_SAMPLES_PER_FRAME = 1152; - public static final long MPEG2_SAMPLES_PER_FRAME = 576; - public static final int HEADER_SIZE = 4; - - private static final int ERROR_NEED_MORE = -10; - private static final int ERROR_NEW_FORMAT = -11; - - private final Mp3DecoderLibrary library; - private final long instance; - - /** - * Create a new instance of mp3 decoder - */ - public Mp3Decoder() { - library = Mp3DecoderLibrary.getInstance(); - instance = library.create(); - - if (instance == 0) { - throw new IllegalStateException("Failed to create a decoder instance"); - } - } - - /** - * Encode the input buffer to output. - * @param directInput Input byte buffer - * @param directOutput Output sample buffer - * @return Number of samples written to the output - */ - public int decode(ByteBuffer directInput, ShortBuffer directOutput) { - checkNotReleased(); - - if (!directInput.isDirect() || !directOutput.isDirect()) { - throw new IllegalArgumentException("Arguments must be direct buffers."); - } - - int result = library.decode(instance, directInput, directInput.remaining(), directOutput, directOutput.remaining() * 2); - - while (result == ERROR_NEW_FORMAT) { - result = library.decode(instance, directInput, 0, directOutput, directOutput.remaining() * 2); - } - - if (result == ERROR_NEED_MORE) { - result = 0; - } else if (result < 0) { - throw new IllegalStateException("Decoding failed with error " + result); - } - - directOutput.position(result / 2); - directOutput.flip(); - - return result / 2; - } - - @Override - protected void freeResources() { - library.destroy(instance); - } - - 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); - } - - /** - * Get the sample rate for the current frame - * @param buffer Buffer which contains the frame header - * @param offset Offset to the frame header - * @return Sample rate - */ - public static int getFrameSampleRate(byte[] buffer, int offset) { - return isMpegVersionOne(buffer, offset) ? getFrameSampleRateV1(buffer, offset) : getFrameSampleRateV2(buffer, offset); - } - - /** - * Get the number of channels in the current frame - * @param buffer Buffer which contains the frame header - * @param offset Offset to the frame header - * @return Number of channels - */ - 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"); - } - } - - 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"); - } - } - - /** - * Get the frame size of the specified 4 bytes - * @param buffer Buffer which contains the frame header - * @param offset Offset to the frame header - * @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); - } - - /** - * Get the average frame size based on this frame - * @param buffer Buffer which contains the frame header - * @param offset Offset to the frame header - * @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; - } - - /** - * @param buffer Buffer which contains the frame header - * @param offset Offset to the frame header - * @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; - } - - public static int getMaximumFrameSize() { - return calculateFrameSize(true, 320000, 32000, true); - } + public static final long MPEG1_SAMPLES_PER_FRAME = 1152; + public static final long MPEG2_SAMPLES_PER_FRAME = 576; + public static final int HEADER_SIZE = 4; + + private static final int ERROR_NEED_MORE = -10; + private static final int ERROR_NEW_FORMAT = -11; + + private final Mp3DecoderLibrary library; + private final long instance; + + /** + * Create a new instance of mp3 decoder + */ + public Mp3Decoder() { + library = Mp3DecoderLibrary.getInstance(); + instance = library.create(); + + if (instance == 0) { + throw new IllegalStateException("Failed to create a decoder instance"); + } + } + + /** + * Encode the input buffer to output. + * + * @param directInput Input byte buffer + * @param directOutput Output sample buffer + * @return Number of samples written to the output + */ + public int decode(ByteBuffer directInput, ShortBuffer directOutput) { + checkNotReleased(); + + if (!directInput.isDirect() || !directOutput.isDirect()) { + throw new IllegalArgumentException("Arguments must be direct buffers."); + } + + int result = library.decode(instance, directInput, directInput.remaining(), directOutput, directOutput.remaining() * 2); + + while (result == ERROR_NEW_FORMAT) { + result = library.decode(instance, directInput, 0, directOutput, directOutput.remaining() * 2); + } + + if (result == ERROR_NEED_MORE) { + result = 0; + } else if (result < 0) { + throw new IllegalStateException("Decoding failed with error " + result); + } + + directOutput.position(result / 2); + directOutput.flip(); + + return result / 2; + } + + @Override + protected void freeResources() { + library.destroy(instance); + } + + 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); + } + + /** + * Get the sample rate for the current frame + * + * @param buffer Buffer which contains the frame header + * @param offset Offset to the frame header + * @return Sample rate + */ + public static int getFrameSampleRate(byte[] buffer, int offset) { + return isMpegVersionOne(buffer, offset) ? getFrameSampleRateV1(buffer, offset) : getFrameSampleRateV2(buffer, offset); + } + + /** + * Get the number of channels in the current frame + * + * @param buffer Buffer which contains the frame header + * @param offset Offset to the frame header + * @return Number of channels + */ + 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"); + } + } + + 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"); + } + } + + /** + * Get the frame size of the specified 4 bytes + * + * @param buffer Buffer which contains the frame header + * @param offset Offset to the frame header + * @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); + } + + /** + * Get the average frame size based on this frame + * + * @param buffer Buffer which contains the frame header + * @param offset Offset to the frame header + * @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; + } + + /** + * @param buffer Buffer which contains the frame header + * @param offset Offset to the frame header + * @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; + } + + public static int getMaximumFrameSize() { + return calculateFrameSize(true, 320000, 32000, true); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/mp3/Mp3DecoderLibrary.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/mp3/Mp3DecoderLibrary.java index 78f18f5c0..5ca94a94c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/mp3/Mp3DecoderLibrary.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/mp3/Mp3DecoderLibrary.java @@ -6,18 +6,18 @@ import java.nio.ShortBuffer; class Mp3DecoderLibrary { - private Mp3DecoderLibrary() { + private Mp3DecoderLibrary() { - } + } - static Mp3DecoderLibrary getInstance() { - ConnectorNativeLibLoader.loadConnectorLibrary(); - return new Mp3DecoderLibrary(); - } + static Mp3DecoderLibrary getInstance() { + ConnectorNativeLibLoader.loadConnectorLibrary(); + return new Mp3DecoderLibrary(); + } - native long create(); + native long create(); - native void destroy(long instance); + native void destroy(long instance); - native int decode(long instance, ByteBuffer directInput, int inputLength, ShortBuffer directOutput, int outputLengthInBytes); + native int decode(long instance, ByteBuffer directInput, int inputLength, ShortBuffer directOutput, int outputLengthInBytes); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusDecoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusDecoder.java index b14a3fcd7..7bc28d156 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusDecoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusDecoder.java @@ -1,6 +1,7 @@ package com.sedmelluq.discord.lavaplayer.natives.opus; import com.sedmelluq.lava.common.natives.NativeResourceHolder; + import java.nio.ByteBuffer; import java.nio.ShortBuffer; @@ -8,104 +9,106 @@ * A wrapper around the native methods of OpusDecoderLibrary. */ public class OpusDecoder extends NativeResourceHolder { - private final OpusDecoderLibrary library; - private final long instance; - private final int channels; - - /** - * @param sampleRate Input sample rate - * @param channels Channel count - */ - public OpusDecoder(int sampleRate, int channels) { - library = OpusDecoderLibrary.getInstance(); - instance = library.create(sampleRate, channels); - this.channels = channels; - - if (instance == 0) { - throw new IllegalStateException("Failed to create a decoder instance with sample rate " + - sampleRate + " and channel count " + channels); - } - } - - /** - * Encode the input buffer to output. - * @param directInput Input byte buffer - * @param directOutput Output sample buffer - * @return Number of bytes written to the output - */ - public int decode(ByteBuffer directInput, ShortBuffer directOutput) { - checkNotReleased(); - - if (!directInput.isDirect() || !directOutput.isDirect()) { - throw new IllegalArgumentException("Arguments must be direct buffers."); + private final OpusDecoderLibrary library; + private final long instance; + private final int channels; + + /** + * @param sampleRate Input sample rate + * @param channels Channel count + */ + public OpusDecoder(int sampleRate, int channels) { + library = OpusDecoderLibrary.getInstance(); + instance = library.create(sampleRate, channels); + this.channels = channels; + + if (instance == 0) { + throw new IllegalStateException("Failed to create a decoder instance with sample rate " + + sampleRate + " and channel count " + channels); + } } - directOutput.clear(); - int result = library.decode(instance, directInput, directInput.remaining(), directOutput, directOutput.remaining() / channels); + /** + * Encode the input buffer to output. + * + * @param directInput Input byte buffer + * @param directOutput Output sample buffer + * @return Number of bytes written to the output + */ + public int decode(ByteBuffer directInput, ShortBuffer directOutput) { + checkNotReleased(); - if (result < 0) { - throw new IllegalStateException("Decoding failed with error " + result); - } + if (!directInput.isDirect() || !directOutput.isDirect()) { + throw new IllegalArgumentException("Arguments must be direct buffers."); + } + + directOutput.clear(); + int result = library.decode(instance, directInput, directInput.remaining(), directOutput, directOutput.remaining() / channels); - directOutput.position(result * channels); - directOutput.flip(); - - return result; - } - - @Override - protected void freeResources() { - library.destroy(instance); - } - - /** - * Get the frame size from an opus packet - * @param sampleRate The sample rate of the packet - * @param buffer The buffer containing the packet - * @param offset Packet offset in the buffer - * @param length Packet length in the buffer - * @return Frame size - */ - public static int getPacketFrameSize(int sampleRate, byte[] buffer, int offset, int length) { - if (length < 1) { - return 0; + if (result < 0) { + throw new IllegalStateException("Decoding failed with error " + result); + } + + directOutput.position(result * channels); + directOutput.flip(); + + return result; } - int frameCount = getPacketFrameCount(buffer, offset, length); - if (frameCount < 0) { - return 0; + @Override + protected void freeResources() { + library.destroy(instance); } - int samples = frameCount * getPacketSamplesPerFrame(sampleRate, buffer[offset]); - if (samples * 25 > sampleRate * 3) { - return 0; + /** + * Get the frame size from an opus packet + * + * @param sampleRate The sample rate of the packet + * @param buffer The buffer containing the packet + * @param offset Packet offset in the buffer + * @param length Packet length in the buffer + * @return Frame size + */ + public static int getPacketFrameSize(int sampleRate, byte[] buffer, int offset, int length) { + if (length < 1) { + return 0; + } + + int frameCount = getPacketFrameCount(buffer, offset, length); + if (frameCount < 0) { + return 0; + } + + int samples = frameCount * getPacketSamplesPerFrame(sampleRate, buffer[offset]); + if (samples * 25 > sampleRate * 3) { + return 0; + } + + return samples; } - return samples; - } - - private static int getPacketFrameCount(byte[] buffer, int offset, int length) { - switch (buffer[offset] & 0x03) { - case 0: - return 1; - case 3: - return length < 2 ? -1 : buffer[offset + 1] & 0x3F; - default: - return 2; + private static int getPacketFrameCount(byte[] buffer, int offset, int length) { + switch (buffer[offset] & 0x03) { + case 0: + return 1; + case 3: + return length < 2 ? -1 : buffer[offset + 1] & 0x3F; + default: + return 2; + } } - } - - private static int getPacketSamplesPerFrame(int frequency, int firstByte) { - int shiftBits = (firstByte >> 3) & 0x03; - - if ((firstByte & 0x80) != 0) { - return (frequency << shiftBits) / 400; - } else if ((firstByte & 0x60) == 0x60) { - return (firstByte & 0x08) != 0 ? frequency / 50 : frequency / 100; - } else if (shiftBits == 3) { - return frequency * 60 / 1000; - } else { - return (frequency << shiftBits) / 100; + + private static int getPacketSamplesPerFrame(int frequency, int firstByte) { + int shiftBits = (firstByte >> 3) & 0x03; + + if ((firstByte & 0x80) != 0) { + return (frequency << shiftBits) / 400; + } else if ((firstByte & 0x60) == 0x60) { + return (firstByte & 0x08) != 0 ? frequency / 50 : frequency / 100; + } else if (shiftBits == 3) { + return frequency * 60 / 1000; + } else { + return (frequency << shiftBits) / 100; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusDecoderLibrary.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusDecoderLibrary.java index 2c05ab7e4..5613195fa 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusDecoderLibrary.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusDecoderLibrary.java @@ -6,18 +6,18 @@ import java.nio.ShortBuffer; class OpusDecoderLibrary { - private OpusDecoderLibrary() { + private OpusDecoderLibrary() { - } + } - static OpusDecoderLibrary getInstance() { - ConnectorNativeLibLoader.loadConnectorLibrary(); - return new OpusDecoderLibrary(); - } + static OpusDecoderLibrary getInstance() { + ConnectorNativeLibLoader.loadConnectorLibrary(); + return new OpusDecoderLibrary(); + } - native long create(int sampleRate, int channels); + native long create(int sampleRate, int channels); - native void destroy(long instance); + native void destroy(long instance); - native int decode(long instance, ByteBuffer directInput, int inputSize, ShortBuffer directOutput, int frameSize); + native int decode(long instance, ByteBuffer directInput, int inputSize, ShortBuffer directOutput, int frameSize); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusEncoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusEncoder.java index 43c272dc5..91f37b726 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusEncoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusEncoder.java @@ -1,6 +1,7 @@ package com.sedmelluq.discord.lavaplayer.natives.opus; import com.sedmelluq.lava.common.natives.NativeResourceHolder; + import java.nio.ByteBuffer; import java.nio.ShortBuffer; @@ -8,52 +9,53 @@ * A wrapper around the native methods of OpusEncoderLibrary. */ public class OpusEncoder extends NativeResourceHolder { - private final OpusEncoderLibrary library; - private final long instance; - - /** - * @param sampleRate Input sample rate - * @param channels Channel count - * @param quality Encoding quality (0-10) - */ - public OpusEncoder(int sampleRate, int channels, int quality) { - library = OpusEncoderLibrary.getInstance(); - instance = library.create(sampleRate, channels, OpusEncoderLibrary.APPLICATION_AUDIO, quality); - - if (instance == 0) { - throw new IllegalStateException("Failed to create an encoder instance"); - } - } - - /** - * Encode the input buffer to output. - * @param directInput Input sample buffer - * @param frameSize Number of samples per channel - * @param directOutput Output byte buffer - * @return Number of bytes written to the output - */ - public int encode(ShortBuffer directInput, int frameSize, ByteBuffer directOutput) { - checkNotReleased(); - - if (!directInput.isDirect() || !directOutput.isDirect()) { - throw new IllegalArgumentException("Arguments must be direct buffers."); + private final OpusEncoderLibrary library; + private final long instance; + + /** + * @param sampleRate Input sample rate + * @param channels Channel count + * @param quality Encoding quality (0-10) + */ + public OpusEncoder(int sampleRate, int channels, int quality) { + library = OpusEncoderLibrary.getInstance(); + instance = library.create(sampleRate, channels, OpusEncoderLibrary.APPLICATION_AUDIO, quality); + + if (instance == 0) { + throw new IllegalStateException("Failed to create an encoder instance"); + } } - directOutput.clear(); - int result = library.encode(instance, directInput, frameSize, directOutput, directOutput.capacity()); + /** + * Encode the input buffer to output. + * + * @param directInput Input sample buffer + * @param frameSize Number of samples per channel + * @param directOutput Output byte buffer + * @return Number of bytes written to the output + */ + public int encode(ShortBuffer directInput, int frameSize, ByteBuffer directOutput) { + checkNotReleased(); - if (result < 0) { - throw new IllegalStateException("Encoding failed with error " + result); - } + if (!directInput.isDirect() || !directOutput.isDirect()) { + throw new IllegalArgumentException("Arguments must be direct buffers."); + } + + directOutput.clear(); + int result = library.encode(instance, directInput, frameSize, directOutput, directOutput.capacity()); - directOutput.position(result); - directOutput.flip(); + if (result < 0) { + throw new IllegalStateException("Encoding failed with error " + result); + } - return result; - } + directOutput.position(result); + directOutput.flip(); - @Override - protected void freeResources() { - library.destroy(instance); - } + return result; + } + + @Override + protected void freeResources() { + library.destroy(instance); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusEncoderLibrary.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusEncoderLibrary.java index c0156de7c..69560fc92 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusEncoderLibrary.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/opus/OpusEncoderLibrary.java @@ -6,20 +6,20 @@ import java.nio.ShortBuffer; class OpusEncoderLibrary { - static final int APPLICATION_AUDIO = 2049; + static final int APPLICATION_AUDIO = 2049; - private OpusEncoderLibrary() { + private OpusEncoderLibrary() { - } + } - static OpusEncoderLibrary getInstance() { - ConnectorNativeLibLoader.loadConnectorLibrary(); - return new OpusEncoderLibrary(); - } + static OpusEncoderLibrary getInstance() { + ConnectorNativeLibLoader.loadConnectorLibrary(); + return new OpusEncoderLibrary(); + } - native long create(int sampleRate, int channels, int application, int quality); + native long create(int sampleRate, int channels, int application, int quality); - native void destroy(long instance); + native void destroy(long instance); - native int encode(long instance, ShortBuffer directInput, int frameSize, ByteBuffer directOutput, int outputCapacity); + native int encode(long instance, ShortBuffer directInput, int frameSize, ByteBuffer directOutput, int outputCapacity); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/samplerate/SampleRateConverter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/samplerate/SampleRateConverter.java index 54ab0a5c7..e95c647af 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/samplerate/SampleRateConverter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/samplerate/SampleRateConverter.java @@ -6,89 +6,89 @@ * Sample rate converter backed by libsamplerate */ public class SampleRateConverter extends NativeResourceHolder { - private final SampleRateLibrary library; - private final double ratio; - private final long instance; + private final SampleRateLibrary library; + private final double ratio; + private final long instance; - /** - * @param type Resampling type - * @param channels Number of channels - * @param sourceRate Source sample rate - * @param targetRate Target sample rate - */ - public SampleRateConverter(ResamplingType type, int channels, int sourceRate, int targetRate) { - ratio = (double)targetRate / (double)sourceRate; - library = SampleRateLibrary.getInstance(); - instance = library.create(type.ordinal(), channels); + /** + * @param type Resampling type + * @param channels Number of channels + * @param sourceRate Source sample rate + * @param targetRate Target sample rate + */ + public SampleRateConverter(ResamplingType type, int channels, int sourceRate, int targetRate) { + ratio = (double) targetRate / (double) sourceRate; + library = SampleRateLibrary.getInstance(); + instance = library.create(type.ordinal(), channels); - if (instance == 0) { - throw new IllegalStateException("Could not create an instance of sample rate converter."); + if (instance == 0) { + throw new IllegalStateException("Could not create an instance of sample rate converter."); + } } - } - /** - * Reset the converter, makes sure previous data does not affect next incoming data - */ - public void reset() { - checkNotReleased(); + /** + * Reset the converter, makes sure previous data does not affect next incoming data + */ + public void reset() { + checkNotReleased(); - library.reset(instance); - } + library.reset(instance); + } - /** - * @param input Input buffer - * @param inputOffset Offset for input buffer - * @param inputLength Length for input buffer - * @param output Output buffer - * @param outputOffset Offset for output buffer - * @param outputLength Length for output buffer - * @param endOfInput If this is the last piece of input - * @param progress Instance that is filled with the progress - */ - public void process(float[] input, int inputOffset, int inputLength, float[] output, int outputOffset, int outputLength, boolean endOfInput, Progress progress) { - checkNotReleased(); + /** + * @param input Input buffer + * @param inputOffset Offset for input buffer + * @param inputLength Length for input buffer + * @param output Output buffer + * @param outputOffset Offset for output buffer + * @param outputLength Length for output buffer + * @param endOfInput If this is the last piece of input + * @param progress Instance that is filled with the progress + */ + public void process(float[] input, int inputOffset, int inputLength, float[] output, int outputOffset, int outputLength, boolean endOfInput, Progress progress) { + checkNotReleased(); - int error = library.process(instance, input, inputOffset, inputLength, output, outputOffset, outputLength, endOfInput, ratio, progress.fields); + int error = library.process(instance, input, inputOffset, inputLength, output, outputOffset, outputLength, endOfInput, ratio, progress.fields); - if (error != 0) { - throw new RuntimeException("Failed to convert sample rate, error " + error + "."); + if (error != 0) { + throw new RuntimeException("Failed to convert sample rate, error " + error + "."); + } } - } - - @Override - protected void freeResources() { - library.destroy(instance); - } - /** - * Progress of converting one piece of data - */ - public static class Progress { - private final int[] fields = new int[2]; + @Override + protected void freeResources() { + library.destroy(instance); + } /** - * @return Number of samples used from the input buffer + * Progress of converting one piece of data */ - public int getInputUsed() { - return fields[0]; + public static class Progress { + private final int[] fields = new int[2]; + + /** + * @return Number of samples used from the input buffer + */ + public int getInputUsed() { + return fields[0]; + } + + /** + * @return Number of samples written to the output buffer + */ + public int getOutputGenerated() { + return fields[1]; + } } /** - * @return Number of samples written to the output buffer + * Available resampling types */ - public int getOutputGenerated() { - return fields[1]; + public enum ResamplingType { + SINC_BEST_QUALITY, + SINC_MEDIUM_QUALITY, + SINC_FASTEST, + ZERO_ORDER_HOLD, + LINEAR } - } - - /** - * Available resampling types - */ - public enum ResamplingType { - SINC_BEST_QUALITY, - SINC_MEDIUM_QUALITY, - SINC_FASTEST, - ZERO_ORDER_HOLD, - LINEAR - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/samplerate/SampleRateLibrary.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/samplerate/SampleRateLibrary.java index 24c819722..973c6db55 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/samplerate/SampleRateLibrary.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/samplerate/SampleRateLibrary.java @@ -3,20 +3,20 @@ import com.sedmelluq.discord.lavaplayer.natives.ConnectorNativeLibLoader; class SampleRateLibrary { - private SampleRateLibrary() { + private SampleRateLibrary() { - } + } - static SampleRateLibrary getInstance() { - ConnectorNativeLibLoader.loadConnectorLibrary(); - return new SampleRateLibrary(); - } + static SampleRateLibrary getInstance() { + ConnectorNativeLibLoader.loadConnectorLibrary(); + return new SampleRateLibrary(); + } - native long create(int type, int channels); + native long create(int type, int channels); - native void destroy(long instance); + native void destroy(long instance); - native void reset(long instance); + native void reset(long instance); - native int process(long instance, float[] in, int inOffset, int inLength, float[] out, int outOffset, int outLength, boolean endOfInput, double sourceRatio, int[] progress); + native int process(long instance, float[] in, int inOffset, int inLength, float[] out, int outOffset, int outLength, boolean endOfInput, double sourceRatio, int[] progress); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/statistics/CpuStatistics.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/statistics/CpuStatistics.java index 822b7ca0e..adc342d5e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/statistics/CpuStatistics.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/statistics/CpuStatistics.java @@ -1,110 +1,106 @@ package com.sedmelluq.discord.lavaplayer.natives.statistics; -import static com.sedmelluq.discord.lavaplayer.natives.statistics.CpuStatisticsLibrary.Timings.PROCESS_KERNEL; -import static com.sedmelluq.discord.lavaplayer.natives.statistics.CpuStatisticsLibrary.Timings.PROCESS_USER; -import static com.sedmelluq.discord.lavaplayer.natives.statistics.CpuStatisticsLibrary.Timings.SYSTEM_KERNEL; -import static com.sedmelluq.discord.lavaplayer.natives.statistics.CpuStatisticsLibrary.Timings.SYSTEM_TOTAL; -import static com.sedmelluq.discord.lavaplayer.natives.statistics.CpuStatisticsLibrary.Timings.SYSTEM_USER; +import static com.sedmelluq.discord.lavaplayer.natives.statistics.CpuStatisticsLibrary.Timings.*; /** * Provides information about system CPU usage. */ public class CpuStatistics { - private static final int TIMINGS_LENGTH = CpuStatisticsLibrary.Timings.class.getEnumConstants().length; + private static final int TIMINGS_LENGTH = CpuStatisticsLibrary.Timings.class.getEnumConstants().length; - private final CpuStatisticsLibrary library = CpuStatisticsLibrary.getInstance(); + private final CpuStatisticsLibrary library = CpuStatisticsLibrary.getInstance(); - /** - * @return Absolute CPU timings at the current moment - */ - public Times getSystemTimes() { - long[] values = new long[TIMINGS_LENGTH]; - library.getSystemTimes(values); - - return new Times( - values[SYSTEM_TOTAL.id()], - values[SYSTEM_USER.id()], - values[SYSTEM_KERNEL.id()], - values[PROCESS_USER.id()], - values[PROCESS_KERNEL.id()] - ); - } - - /** - * CPU timings - */ - public static class Times { - /** - * Total amount of CPU time since system start - */ - public final long systemTotal; - /** - * Total amount of CPU time spent in user mode - */ - public final long systemUser; - /** - * Total amount of CPU time spent in kernel mode - */ - public final long systemKernel; /** - * Total amount of CPU time this process has spent in user mode + * @return Absolute CPU timings at the current moment */ - public final long processUser; - /** - * Total amount of CPU time this process has spent in kernel mode - */ - public final long processKernel; + public Times getSystemTimes() { + long[] values = new long[TIMINGS_LENGTH]; + library.getSystemTimes(values); - /** - * @param systemTotal Total amount of CPU time since system start - * @param systemUser Total amount of CPU time spent in user mode - * @param systemKernel Total amount of CPU time spent in kernel mode - * @param processUser Total amount of CPU time this process has spent in user mode - * @param processKernel Total amount of CPU time this process has spent in kernel mode - */ - public Times(long systemTotal, long systemUser, long systemKernel, long processUser, long processKernel) { - this.systemTotal = systemTotal; - this.systemUser = systemUser; - this.systemKernel = systemKernel; - this.processUser = processUser; - this.processKernel = processKernel; + return new Times( + values[SYSTEM_TOTAL.id()], + values[SYSTEM_USER.id()], + values[SYSTEM_KERNEL.id()], + values[PROCESS_USER.id()], + values[PROCESS_KERNEL.id()] + ); } /** - * @return The ratio of used CPU time to total CPU time + * CPU timings */ - public float getSystemUsage() { - if (systemTotal == 0) { - return 0.0f; - } else { - return (float) (systemUser + systemKernel) / systemTotal; - } + public static class Times { + /** + * Total amount of CPU time since system start + */ + public final long systemTotal; + /** + * Total amount of CPU time spent in user mode + */ + public final long systemUser; + /** + * Total amount of CPU time spent in kernel mode + */ + public final long systemKernel; + /** + * Total amount of CPU time this process has spent in user mode + */ + public final long processUser; + /** + * Total amount of CPU time this process has spent in kernel mode + */ + public final long processKernel; + + /** + * @param systemTotal Total amount of CPU time since system start + * @param systemUser Total amount of CPU time spent in user mode + * @param systemKernel Total amount of CPU time spent in kernel mode + * @param processUser Total amount of CPU time this process has spent in user mode + * @param processKernel Total amount of CPU time this process has spent in kernel mode + */ + public Times(long systemTotal, long systemUser, long systemKernel, long processUser, long processKernel) { + this.systemTotal = systemTotal; + this.systemUser = systemUser; + this.systemKernel = systemKernel; + this.processUser = processUser; + this.processKernel = processKernel; + } + + /** + * @return The ratio of used CPU time to total CPU time + */ + public float getSystemUsage() { + if (systemTotal == 0) { + return 0.0f; + } else { + return (float) (systemUser + systemKernel) / systemTotal; + } + } + + /** + * @return The ratio of used CPU time by current process to total CPU time + */ + public float getProcessUsage() { + if (systemTotal == 0) { + return 0.0f; + } else { + return (float) (processUser + processKernel) / systemTotal; + } + } } /** - * @return The ratio of used CPU time by current process to total CPU time + * @param old Older timing values + * @param current Newer timing values + * @return Difference between two timings */ - public float getProcessUsage() { - if (systemTotal == 0) { - return 0.0f; - } else { - return (float) (processUser + processKernel) / systemTotal; - } + public static Times diff(Times old, Times current) { + return new Times( + current.systemTotal - old.systemTotal, + current.systemUser - old.systemUser, + current.systemKernel - old.systemKernel, + current.processUser - old.processUser, + current.processKernel - old.processKernel + ); } - } - - /** - * @param old Older timing values - * @param current Newer timing values - * @return Difference between two timings - */ - public static Times diff(Times old, Times current) { - return new Times( - current.systemTotal - old.systemTotal, - current.systemUser - old.systemUser, - current.systemKernel - old.systemKernel, - current.processUser - old.processUser, - current.processKernel - old.processKernel - ); - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/statistics/CpuStatisticsLibrary.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/statistics/CpuStatisticsLibrary.java index 0c58d7150..e7aec9b7b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/statistics/CpuStatisticsLibrary.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/statistics/CpuStatisticsLibrary.java @@ -3,26 +3,26 @@ import com.sedmelluq.discord.lavaplayer.natives.ConnectorNativeLibLoader; class CpuStatisticsLibrary { - private CpuStatisticsLibrary() { + private CpuStatisticsLibrary() { - } + } - static CpuStatisticsLibrary getInstance() { - ConnectorNativeLibLoader.loadConnectorLibrary(); - return new CpuStatisticsLibrary(); - } + static CpuStatisticsLibrary getInstance() { + ConnectorNativeLibLoader.loadConnectorLibrary(); + return new CpuStatisticsLibrary(); + } - native void getSystemTimes(long[] timingArray); + native void getSystemTimes(long[] timingArray); - enum Timings { - SYSTEM_TOTAL, - SYSTEM_USER, - SYSTEM_KERNEL, - PROCESS_USER, - PROCESS_KERNEL; + enum Timings { + SYSTEM_TOTAL, + SYSTEM_USER, + SYSTEM_KERNEL, + PROCESS_USER, + PROCESS_KERNEL; - int id() { - return ordinal(); + int id() { + return ordinal(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/vorbis/VorbisDecoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/vorbis/VorbisDecoder.java index 7f022bd29..114fff4c9 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/vorbis/VorbisDecoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/vorbis/VorbisDecoder.java @@ -1,97 +1,101 @@ package com.sedmelluq.discord.lavaplayer.natives.vorbis; import com.sedmelluq.lava.common.natives.NativeResourceHolder; + import java.nio.ByteBuffer; /** * A wrapper around the native methods of AacDecoder, which uses libvorbis native library. */ public class VorbisDecoder extends NativeResourceHolder { - private final VorbisDecoderLibrary library; - private final long instance; - private int channelCount = 0; - - /** - * Create an instance. - */ - public VorbisDecoder() { - library = VorbisDecoderLibrary.getInstance(); - instance = library.create(); - } - - /** - * Initialize the decoder by passing in identification and setup header data. See - * https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-170001.2.6 for definitions. The comment header is not required as - * it is not actually used for decoding setup. - * - * @param infoBuffer Identification header, including the 'vorbis' string. - * @param setupBuffer Setup header (also known as codebook header), including the 'vorbis' string. - */ - public void initialise(ByteBuffer infoBuffer, ByteBuffer setupBuffer) { - checkNotReleased(); - - if (!infoBuffer.isDirect() || !setupBuffer.isDirect()) { - throw new IllegalArgumentException("Buffer argument must be a direct buffer."); + private final VorbisDecoderLibrary library; + private final long instance; + private int channelCount = 0; + + /** + * Create an instance. + */ + public VorbisDecoder() { + library = VorbisDecoderLibrary.getInstance(); + instance = library.create(); } - if (!library.initialise(instance, infoBuffer, infoBuffer.position(), infoBuffer.remaining(), setupBuffer, - setupBuffer.position(), setupBuffer.remaining())) { + /** + * Initialize the decoder by passing in identification and setup header data. See + * https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-170001.2.6 for definitions. The comment header is not required as + * it is not actually used for decoding setup. + * + * @param infoBuffer Identification header, including the 'vorbis' string. + * @param setupBuffer Setup header (also known as codebook header), including the 'vorbis' string. + */ + public void initialise(ByteBuffer infoBuffer, ByteBuffer setupBuffer) { + checkNotReleased(); - throw new IllegalStateException("Could not initialise library."); - } + if (!infoBuffer.isDirect() || !setupBuffer.isDirect()) { + throw new IllegalArgumentException("Buffer argument must be a direct buffer."); + } - channelCount = library.getChannelCount(instance); - } - - /** - * Get the number of channels, valid only after initialisation. - * @return Number of channels - */ - public int getChannelCount() { - return channelCount; - } - - /** - * Provide input for the decoder - * @param buffer Buffer with the input - */ - public void input(ByteBuffer buffer) { - checkNotReleased(); - - if (!buffer.isDirect()) { - throw new IllegalArgumentException("Buffer argument must be a direct buffer."); - } + if (!library.initialise(instance, infoBuffer, infoBuffer.position(), infoBuffer.remaining(), setupBuffer, + setupBuffer.position(), setupBuffer.remaining())) { - int result = library.input(instance, buffer, buffer.position(), buffer.remaining()); - buffer.position(buffer.position() + buffer.remaining()); + throw new IllegalStateException("Could not initialise library."); + } - if (result != 0) { - throw new IllegalStateException("Passing input failed with error " + result + "."); + channelCount = library.getChannelCount(instance); } - } - - /** - * Fetch output from the decoder - * @param channels Channel buffers to fetch the output to - * @return The number of samples fetched for each channel - */ - public int output(float[][] channels) { - checkNotReleased(); - - if (channels.length != channelCount) { - throw new IllegalStateException("Invalid channel float buffer length"); + + /** + * Get the number of channels, valid only after initialisation. + * + * @return Number of channels + */ + public int getChannelCount() { + return channelCount; } - int result = library.output(instance, channels, channels[0].length); - if (result < 0) { - throw new IllegalStateException("Retrieving output failed"); + /** + * Provide input for the decoder + * + * @param buffer Buffer with the input + */ + public void input(ByteBuffer buffer) { + checkNotReleased(); + + if (!buffer.isDirect()) { + throw new IllegalArgumentException("Buffer argument must be a direct buffer."); + } + + int result = library.input(instance, buffer, buffer.position(), buffer.remaining()); + buffer.position(buffer.position() + buffer.remaining()); + + if (result != 0) { + throw new IllegalStateException("Passing input failed with error " + result + "."); + } } - return result; - } + /** + * Fetch output from the decoder + * + * @param channels Channel buffers to fetch the output to + * @return The number of samples fetched for each channel + */ + public int output(float[][] channels) { + checkNotReleased(); + + if (channels.length != channelCount) { + throw new IllegalStateException("Invalid channel float buffer length"); + } + + int result = library.output(instance, channels, channels[0].length); + if (result < 0) { + throw new IllegalStateException("Retrieving output failed"); + } + + return result; + } - @Override - protected void freeResources() { - library.destroy(instance); - } + @Override + protected void freeResources() { + library.destroy(instance); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/vorbis/VorbisDecoderLibrary.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/vorbis/VorbisDecoderLibrary.java index 1ad505e60..0c47460d6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/vorbis/VorbisDecoderLibrary.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/natives/vorbis/VorbisDecoderLibrary.java @@ -5,25 +5,25 @@ import java.nio.ByteBuffer; class VorbisDecoderLibrary { - private VorbisDecoderLibrary() { + private VorbisDecoderLibrary() { - } + } - static VorbisDecoderLibrary getInstance() { - ConnectorNativeLibLoader.loadConnectorLibrary(); - return new VorbisDecoderLibrary(); - } + static VorbisDecoderLibrary getInstance() { + ConnectorNativeLibLoader.loadConnectorLibrary(); + return new VorbisDecoderLibrary(); + } - native long create(); + native long create(); - native void destroy(long instance); + native void destroy(long instance); - native boolean initialise(long instance, ByteBuffer infoBuffer, int infoOffset, int infoLength, - ByteBuffer setupBuffer, int setupOffset, int setupLength); + native boolean initialise(long instance, ByteBuffer infoBuffer, int infoOffset, int infoLength, + ByteBuffer setupBuffer, int setupOffset, int setupLength); - native int getChannelCount(long instance); + native int getChannelCount(long instance); - native int input(long instance, ByteBuffer directBuffer, int offset, int length); + native int input(long instance, ByteBuffer directBuffer, int offset, int length); - native int output(long instance, float[][] channels, int length); + native int output(long instance, float[][] channels, int length); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioConfiguration.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioConfiguration.java index 26fa2be0f..8ecdee892 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioConfiguration.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioConfiguration.java @@ -9,84 +9,84 @@ * Configuration for audio processing. */ public class AudioConfiguration { - public static final int OPUS_QUALITY_MAX = 10; - - private volatile ResamplingQuality resamplingQuality; - private volatile int opusEncodingQuality; - private volatile AudioDataFormat outputFormat; - private volatile boolean filterHotSwapEnabled; - private volatile AudioFrameBufferFactory frameBufferFactory; - - /** - * Create a new configuration with default values. - */ - public AudioConfiguration() { - resamplingQuality = ResamplingQuality.LOW; - opusEncodingQuality = OPUS_QUALITY_MAX; - outputFormat = StandardAudioDataFormats.DISCORD_OPUS; - filterHotSwapEnabled = false; - frameBufferFactory = AllocatingAudioFrameBuffer::new; - } - - public ResamplingQuality getResamplingQuality() { - return resamplingQuality; - } - - public void setResamplingQuality(ResamplingQuality resamplingQuality) { - this.resamplingQuality = resamplingQuality; - } - - public int getOpusEncodingQuality() { - return opusEncodingQuality; - } - - public void setOpusEncodingQuality(int opusEncodingQuality) { - this.opusEncodingQuality = Math.max(0, Math.min(opusEncodingQuality, OPUS_QUALITY_MAX)); - } - - public AudioDataFormat getOutputFormat() { - return outputFormat; - } - - public void setOutputFormat(AudioDataFormat outputFormat) { - this.outputFormat = outputFormat; - } - - public boolean isFilterHotSwapEnabled() { - return filterHotSwapEnabled; - } - - public void setFilterHotSwapEnabled(boolean filterHotSwapEnabled) { - this.filterHotSwapEnabled = filterHotSwapEnabled; - } - - public AudioFrameBufferFactory getFrameBufferFactory() { - return frameBufferFactory; - } - - public void setFrameBufferFactory(AudioFrameBufferFactory frameBufferFactory) { - this.frameBufferFactory = frameBufferFactory; - } - - /** - * @return A copy of this configuration. - */ - public AudioConfiguration copy() { - AudioConfiguration copy = new AudioConfiguration(); - copy.setResamplingQuality(resamplingQuality); - copy.setOpusEncodingQuality(opusEncodingQuality); - copy.setOutputFormat(outputFormat); - copy.setFilterHotSwapEnabled(filterHotSwapEnabled); - copy.setFrameBufferFactory(frameBufferFactory); - return copy; - } - - /** - * Resampling quality levels - */ - public enum ResamplingQuality { - HIGH, - MEDIUM, - LOW - } + public static final int OPUS_QUALITY_MAX = 10; + + private volatile ResamplingQuality resamplingQuality; + private volatile int opusEncodingQuality; + private volatile AudioDataFormat outputFormat; + private volatile boolean filterHotSwapEnabled; + private volatile AudioFrameBufferFactory frameBufferFactory; + + /** + * Create a new configuration with default values. + */ + public AudioConfiguration() { + resamplingQuality = ResamplingQuality.LOW; + opusEncodingQuality = OPUS_QUALITY_MAX; + outputFormat = StandardAudioDataFormats.DISCORD_OPUS; + filterHotSwapEnabled = false; + frameBufferFactory = AllocatingAudioFrameBuffer::new; + } + + public ResamplingQuality getResamplingQuality() { + return resamplingQuality; + } + + public void setResamplingQuality(ResamplingQuality resamplingQuality) { + this.resamplingQuality = resamplingQuality; + } + + public int getOpusEncodingQuality() { + return opusEncodingQuality; + } + + public void setOpusEncodingQuality(int opusEncodingQuality) { + this.opusEncodingQuality = Math.max(0, Math.min(opusEncodingQuality, OPUS_QUALITY_MAX)); + } + + public AudioDataFormat getOutputFormat() { + return outputFormat; + } + + public void setOutputFormat(AudioDataFormat outputFormat) { + this.outputFormat = outputFormat; + } + + public boolean isFilterHotSwapEnabled() { + return filterHotSwapEnabled; + } + + public void setFilterHotSwapEnabled(boolean filterHotSwapEnabled) { + this.filterHotSwapEnabled = filterHotSwapEnabled; + } + + public AudioFrameBufferFactory getFrameBufferFactory() { + return frameBufferFactory; + } + + public void setFrameBufferFactory(AudioFrameBufferFactory frameBufferFactory) { + this.frameBufferFactory = frameBufferFactory; + } + + /** + * @return A copy of this configuration. + */ + public AudioConfiguration copy() { + AudioConfiguration copy = new AudioConfiguration(); + copy.setResamplingQuality(resamplingQuality); + copy.setOpusEncodingQuality(opusEncodingQuality); + copy.setOutputFormat(outputFormat); + copy.setFilterHotSwapEnabled(filterHotSwapEnabled); + copy.setFrameBufferFactory(frameBufferFactory); + return copy; + } + + /** + * Resampling quality levels + */ + public enum ResamplingQuality { + HIGH, + MEDIUM, + LOW + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioLoadResultHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioLoadResultHandler.java index e67acaad2..6289dea5d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioLoadResultHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioLoadResultHandler.java @@ -8,26 +8,29 @@ * Handles the result of loading an item from an audio player manager. */ public interface AudioLoadResultHandler { - /** - * Called when the requested item is a track and it was successfully loaded. - * @param track The loaded track - */ - void trackLoaded(AudioTrack track); + /** + * Called when the requested item is a track and it was successfully loaded. + * + * @param track The loaded track + */ + void trackLoaded(AudioTrack track); - /** - * Called when the requested item is a playlist and it was successfully loaded. - * @param playlist The loaded playlist - */ - void playlistLoaded(AudioPlaylist playlist); + /** + * Called when the requested item is a playlist and it was successfully loaded. + * + * @param playlist The loaded playlist + */ + void playlistLoaded(AudioPlaylist playlist); - /** - * Called when there were no items found by the specified identifier. - */ - void noMatches(); + /** + * Called when there were no items found by the specified identifier. + */ + void noMatches(); - /** - * Called when loading an item failed with an exception. - * @param exception The exception that was thrown - */ - void loadFailed(FriendlyException exception); + /** + * Called when loading an item failed with an exception. + * + * @param exception The exception that was thrown + */ + void loadFailed(FriendlyException exception); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayer.java index 04c4e274d..777d25c09 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayer.java @@ -1,104 +1,77 @@ package com.sedmelluq.discord.lavaplayer.player; import com.sedmelluq.discord.lavaplayer.filter.PcmFilterFactory; -import com.sedmelluq.discord.lavaplayer.player.event.AudioEvent; import com.sedmelluq.discord.lavaplayer.player.event.AudioEventListener; -import com.sedmelluq.discord.lavaplayer.player.event.PlayerPauseEvent; -import com.sedmelluq.discord.lavaplayer.player.event.PlayerResumeEvent; -import com.sedmelluq.discord.lavaplayer.player.event.TrackEndEvent; -import com.sedmelluq.discord.lavaplayer.player.event.TrackExceptionEvent; -import com.sedmelluq.discord.lavaplayer.player.event.TrackStartEvent; -import com.sedmelluq.discord.lavaplayer.player.event.TrackStuckEvent; -import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools; -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; -import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; -import com.sedmelluq.discord.lavaplayer.track.InternalAudioTrack; -import com.sedmelluq.discord.lavaplayer.track.TrackStateListener; -import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame; import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrameProvider; -import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrameProviderTools; -import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; - -import static com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason.CLEANUP; -import static com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason.FINISHED; -import static com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason.LOAD_FAILED; -import static com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason.REPLACED; -import static com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason.STOPPED; /** * An audio player that is capable of playing audio tracks and provides audio frames from the currently playing track. */ public interface AudioPlayer extends AudioFrameProvider { - /** - * @return Currently playing track - */ - AudioTrack getPlayingTrack(); - - /** - * @param track The track to start playing - */ - void playTrack(AudioTrack track); - - /** - * @param track The track to start playing, passing null will stop the current track and return false - * @param noInterrupt Whether to only start if nothing else is playing - * @return True if the track was started - */ - boolean startTrack(AudioTrack track, boolean noInterrupt); - - /** - * Stop currently playing track. - */ - void stopTrack(); - - int getVolume(); - - void setVolume(int volume); - - void setFilterFactory(PcmFilterFactory factory); - - void setFrameBufferDuration(Integer duration); - - /** - * @return Whether the player is paused - */ - boolean isPaused(); - - /** - * @param value True to pause, false to resume - */ - void setPaused(boolean value); - - /** - * Destroy the player and stop playing track. - */ - void destroy(); - - /** - * Add a listener to events from this player. - * @param listener New listener - */ - void addListener(AudioEventListener listener); - - /** - * Remove an attached listener using identity comparison. - * @param listener The listener to remove - */ - void removeListener(AudioEventListener listener); - - /** - * Check if the player should be "cleaned up" - stopped due to nothing using it, with the given threshold. - * @param threshold Threshold in milliseconds to use - */ - void checkCleanup(long threshold); + /** + * @return Currently playing track + */ + AudioTrack getPlayingTrack(); + + /** + * @param track The track to start playing + */ + void playTrack(AudioTrack track); + + /** + * @param track The track to start playing, passing null will stop the current track and return false + * @param noInterrupt Whether to only start if nothing else is playing + * @return True if the track was started + */ + boolean startTrack(AudioTrack track, boolean noInterrupt); + + /** + * Stop currently playing track. + */ + void stopTrack(); + + int getVolume(); + + void setVolume(int volume); + + void setFilterFactory(PcmFilterFactory factory); + + void setFrameBufferDuration(Integer duration); + + /** + * @return Whether the player is paused + */ + boolean isPaused(); + + /** + * @param value True to pause, false to resume + */ + void setPaused(boolean value); + + /** + * Destroy the player and stop playing track. + */ + void destroy(); + + /** + * Add a listener to events from this player. + * + * @param listener New listener + */ + void addListener(AudioEventListener listener); + + /** + * Remove an attached listener using identity comparison. + * + * @param listener The listener to remove + */ + void removeListener(AudioEventListener listener); + + /** + * Check if the player should be "cleaned up" - stopped due to nothing using it, with the given threshold. + * + * @param threshold Threshold in milliseconds to use + */ + void checkCleanup(long threshold); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayerLifecycleManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayerLifecycleManager.java index 9d4ef4462..d0921adf4 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayerLifecycleManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayerLifecycleManager.java @@ -5,11 +5,7 @@ import com.sedmelluq.discord.lavaplayer.player.event.TrackEndEvent; import com.sedmelluq.discord.lavaplayer.player.event.TrackStartEvent; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -17,57 +13,57 @@ * Triggers cleanup checks on all active audio players at a fixed interval. */ public class AudioPlayerLifecycleManager implements Runnable, AudioEventListener { - private static final long CHECK_INTERVAL = 10000; + private static final long CHECK_INTERVAL = 10000; - private final ConcurrentMap activePlayers; - private final ScheduledExecutorService scheduler; - private final AtomicLong cleanupThreshold; - private final AtomicReference> scheduledTask; + private final ConcurrentMap activePlayers; + private final ScheduledExecutorService scheduler; + private final AtomicLong cleanupThreshold; + private final AtomicReference> scheduledTask; - /** - * @param scheduler Scheduler to use for the cleanup check task - * @param cleanupThreshold Threshold for player cleanup - */ - public AudioPlayerLifecycleManager(ScheduledExecutorService scheduler, AtomicLong cleanupThreshold) { - this.activePlayers = new ConcurrentHashMap<>(); - this.scheduler = scheduler; - this.cleanupThreshold = cleanupThreshold; - this.scheduledTask = new AtomicReference<>(); - } + /** + * @param scheduler Scheduler to use for the cleanup check task + * @param cleanupThreshold Threshold for player cleanup + */ + public AudioPlayerLifecycleManager(ScheduledExecutorService scheduler, AtomicLong cleanupThreshold) { + this.activePlayers = new ConcurrentHashMap<>(); + this.scheduler = scheduler; + this.cleanupThreshold = cleanupThreshold; + this.scheduledTask = new AtomicReference<>(); + } - /** - * Initialise the scheduled task. - */ - public void initialise() { - ScheduledFuture task = scheduler.scheduleAtFixedRate(this, CHECK_INTERVAL, CHECK_INTERVAL, TimeUnit.MILLISECONDS); - if (!scheduledTask.compareAndSet(null, task)) { - task.cancel(false); + /** + * Initialise the scheduled task. + */ + public void initialise() { + ScheduledFuture task = scheduler.scheduleAtFixedRate(this, CHECK_INTERVAL, CHECK_INTERVAL, TimeUnit.MILLISECONDS); + if (!scheduledTask.compareAndSet(null, task)) { + task.cancel(false); + } } - } - /** - * Stop the scheduled task. - */ - public void shutdown() { - ScheduledFuture task = scheduledTask.getAndSet(null); - if (task != null) { - task.cancel(false); + /** + * Stop the scheduled task. + */ + public void shutdown() { + ScheduledFuture task = scheduledTask.getAndSet(null); + if (task != null) { + task.cancel(false); + } } - } - @Override - public void onEvent(AudioEvent event) { - if (event instanceof TrackStartEvent) { - activePlayers.put(event.player, event.player); - } else if (event instanceof TrackEndEvent) { - activePlayers.remove(event.player); + @Override + public void onEvent(AudioEvent event) { + if (event instanceof TrackStartEvent) { + activePlayers.put(event.player, event.player); + } else if (event instanceof TrackEndEvent) { + activePlayers.remove(event.player); + } } - } - @Override - public void run() { - for (AudioPlayer player : activePlayers.keySet()) { - player.checkCleanup(cleanupThreshold.get()); + @Override + public void run() { + for (AudioPlayer player : activePlayers.keySet()) { + player.checkCleanup(cleanupThreshold.get()); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayerManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayerManager.java index 406f88e28..35eab02c4 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayerManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayerManager.java @@ -6,215 +6,221 @@ import com.sedmelluq.discord.lavaplayer.track.AudioReference; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.DecodedTrackHolder; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.HttpClientBuilder; + import java.io.IOException; import java.util.List; import java.util.concurrent.Future; import java.util.function.Consumer; import java.util.function.Function; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.impl.client.HttpClientBuilder; /** * Audio player manager which is used for creating audio players and loading tracks and playlists. */ public interface AudioPlayerManager { - /** - * Shut down the manager. All threads will be stopped, the manager cannot be used any further. All players created - * with this manager will stop and all source managers registered to this manager will also be shut down. - * - * Every thread created by the audio manager is a daemon thread, so calling this is not required for an application - * to be able to gracefully shut down, however it should be called if the application continues without requiring this - * manager any longer. - */ - void shutdown(); - - /** - * Enable reporting GC pause length statistics to log (warn level with lengths bad for latency, debug level otherwise) - */ - void enableGcMonitoring(); - - /** - * @param sourceManager The source manager to register, which will be used for subsequent loadItem calls - */ - void registerSourceManager(AudioSourceManager sourceManager); - - /** - * Shortcut for accessing a source manager of a certain class. - * @param klass The class of the source manager to return. - * @param The class of the source manager. - * @return The source manager of the specified class, or null if not registered. - */ - T source(Class klass); - - /** - * @return A list of all registered audio source managers. - */ - List getSourceManagers(); - - /** - * Schedules loading a track or playlist with the specified identifier. - * @param identifier The identifier that a specific source manager should be able to find the track with. - * @param resultHandler A handler to process the result of this operation. It can either end by finding a track, - * finding a playlist, finding nothing or terminating with an exception. - * @return A future for this operation - * @see #loadItem(AudioReference, AudioLoadResultHandler) - */ - default Future loadItem(final String identifier, final AudioLoadResultHandler resultHandler) { - return loadItem(new AudioReference(identifier, null), resultHandler); - } - - /** - * Schedules loading a track or playlist with the specified identifier. - * @param reference The audio reference that holds the identifier that a specific source manager - * should be able to find the track with. - * @param resultHandler A handler to process the result of this operation. It can either end by finding a track, - * finding a playlist, finding nothing or terminating with an exception. - * @return A future for this operation - * @see #loadItem(String, AudioLoadResultHandler) - */ - Future loadItem(final AudioReference reference, final AudioLoadResultHandler resultHandler); - - /** - * Loads a track or playlist with the specified identifier. - * @param identifier The identifier that a specific source manager should be able to find the track with. - * @param resultHandler A handler to process the result of this operation. It can either end by finding a track, - * finding a playlist, finding nothing or terminating with an exception. - * @see #loadItemSync(AudioReference, AudioLoadResultHandler) - */ - default void loadItemSync(final String identifier, final AudioLoadResultHandler resultHandler) { - loadItemSync(new AudioReference(identifier, null), resultHandler); - } - - /** - * Loads a track or playlist with the specified identifier. - * @param reference The audio reference that holds the identifier that a specific source manager - * should be able to find the track with. - * @param resultHandler A handler to process the result of this operation. It can either end by finding a track, - * finding a playlist, finding nothing or terminating with an exception. - * @see #loadItemSync(String, AudioLoadResultHandler) - */ - void loadItemSync(final AudioReference reference, final AudioLoadResultHandler resultHandler); - - /** - * Schedules loading a track or playlist with the specified identifier with an ordering key so that items with the - * same ordering key are handled sequentially in the order of calls to this method. - * - * @param orderingKey Object to use as the key for the ordering channel - * @param identifier The identifier that a specific source manager should be able to find the track with. - * @param resultHandler A handler to process the result of this operation. It can either end by finding a track, - * finding a playlist, finding nothing or terminating with an exception. - * @return A future for this operation - * @see #loadItemOrdered(Object, AudioReference, AudioLoadResultHandler) - */ - default Future loadItemOrdered(Object orderingKey, final String identifier, final AudioLoadResultHandler resultHandler) { - return loadItemOrdered(orderingKey, new AudioReference(identifier, null), resultHandler); - } - - /** - * Schedules loading a track or playlist with the specified identifier with an ordering key so that items with the - * same ordering key are handled sequentially in the order of calls to this method. - * - * @param orderingKey Object to use as the key for the ordering channel - * @param reference The audio reference that holds the identifier that a specific source manager - * should be able to find the track with. - * @param resultHandler A handler to process the result of this operation. It can either end by finding a track, - * finding a playlist, finding nothing or terminating with an exception. - * @return A future for this operation - * @see #loadItemOrdered(Object, String, AudioLoadResultHandler) - */ - Future loadItemOrdered(Object orderingKey, final AudioReference reference, final AudioLoadResultHandler resultHandler); - - /** - * Encode a track into an output stream. If the decoder is not supposed to know the number of tracks in advance, then - * the encoder should call MessageOutput#finish() after all the tracks it wanted to write have been written. This will - * make decodeTrack() return null at that position - * - * @param stream The message stream to write it to. - * @param track The track to encode. - * @throws IOException On IO error. - */ - void encodeTrack(MessageOutput stream, AudioTrack track) throws IOException; - - /** - * Decode a track from an input stream. Null returns value indicates reaching the position where the decoder had - * called MessageOutput#finish(). - * - * @param stream The message stream to read it from. - * @return Holder containing the track if it was successfully decoded. - * @throws IOException On IO error. - */ - DecodedTrackHolder decodeTrack(MessageInput stream) throws IOException; - - /** - * @return Audio processing configuration used for tracks executed by this manager. - */ - AudioConfiguration getConfiguration(); - - /** - * Seek ghosting is the effect where while a seek is in progress, buffered audio from the previous location will be - * served until seek is ready or the buffer is empty. - * - * @return True if seek ghosting is enabled. - */ - boolean isUsingSeekGhosting(); - - /** - * @param useSeekGhosting The new state of seek ghosting - */ - void setUseSeekGhosting(boolean useSeekGhosting); - - /** - * @return The length of the internal buffer for audio in milliseconds. - */ - int getFrameBufferDuration(); - - /** - * @param frameBufferDuration New length of the internal buffer for audio in milliseconds. - */ - void setFrameBufferDuration(int frameBufferDuration); - - /** - * Sets the threshold for how long a track can be stuck until the TrackStuckEvent is sent out. A track is considered - * to be stuck if the player receives requests for audio samples from the track, but the audio frame provider of that - * track has been returning no data for the specified time. - * - * @param trackStuckThreshold The threshold in milliseconds. - */ - void setTrackStuckThreshold(long trackStuckThreshold); - - /** - * Sets the threshold for clearing an audio player when it has not been queried for the specified amount of time. - * - * @param cleanupThreshold The threshold in milliseconds. - */ - void setPlayerCleanupThreshold(long cleanupThreshold); - - /** - * Sets the number of threads used for loading processing item load requests. - * - * @param poolSize Maximum number of concurrent threads used for loading items. - */ - void setItemLoaderThreadPoolSize(int poolSize); - - /** - * @return New audio player. - */ - AudioPlayer createPlayer(); - - /** - * @param configurator Function used to reconfigure the request config of all sources which perform HTTP requests. - * Applied to all current and future registered sources. Setting this while sources are already in - * use will close all active connections, so this should be called before the sources have been - * used. - */ - void setHttpRequestConfigurator(Function configurator); - - /** - * @param configurator Function used to reconfigure the HTTP builder of all sources which perform HTTP requests. - * Applied to all current and future registered sources. Setting this while sources are already in - * use will close all active connections, so this should be called before the sources have been - * used. - */ - void setHttpBuilderConfigurator(Consumer configurator); -} \ No newline at end of file + /** + * Shut down the manager. All threads will be stopped, the manager cannot be used any further. All players created + * with this manager will stop and all source managers registered to this manager will also be shut down. + *

+ * Every thread created by the audio manager is a daemon thread, so calling this is not required for an application + * to be able to gracefully shut down, however it should be called if the application continues without requiring this + * manager any longer. + */ + void shutdown(); + + /** + * Enable reporting GC pause length statistics to log (warn level with lengths bad for latency, debug level otherwise) + */ + void enableGcMonitoring(); + + /** + * @param sourceManager The source manager to register, which will be used for subsequent loadItem calls + */ + void registerSourceManager(AudioSourceManager sourceManager); + + /** + * Shortcut for accessing a source manager of a certain class. + * + * @param klass The class of the source manager to return. + * @param The class of the source manager. + * @return The source manager of the specified class, or null if not registered. + */ + T source(Class klass); + + /** + * @return A list of all registered audio source managers. + */ + List getSourceManagers(); + + /** + * Schedules loading a track or playlist with the specified identifier. + * + * @param identifier The identifier that a specific source manager should be able to find the track with. + * @param resultHandler A handler to process the result of this operation. It can either end by finding a track, + * finding a playlist, finding nothing or terminating with an exception. + * @return A future for this operation + * @see #loadItem(AudioReference, AudioLoadResultHandler) + */ + default Future loadItem(final String identifier, final AudioLoadResultHandler resultHandler) { + return loadItem(new AudioReference(identifier, null), resultHandler); + } + + /** + * Schedules loading a track or playlist with the specified identifier. + * + * @param reference The audio reference that holds the identifier that a specific source manager + * should be able to find the track with. + * @param resultHandler A handler to process the result of this operation. It can either end by finding a track, + * finding a playlist, finding nothing or terminating with an exception. + * @return A future for this operation + * @see #loadItem(String, AudioLoadResultHandler) + */ + Future loadItem(final AudioReference reference, final AudioLoadResultHandler resultHandler); + + /** + * Loads a track or playlist with the specified identifier. + * + * @param identifier The identifier that a specific source manager should be able to find the track with. + * @param resultHandler A handler to process the result of this operation. It can either end by finding a track, + * finding a playlist, finding nothing or terminating with an exception. + * @see #loadItemSync(AudioReference, AudioLoadResultHandler) + */ + default void loadItemSync(final String identifier, final AudioLoadResultHandler resultHandler) { + loadItemSync(new AudioReference(identifier, null), resultHandler); + } + + /** + * Loads a track or playlist with the specified identifier. + * + * @param reference The audio reference that holds the identifier that a specific source manager + * should be able to find the track with. + * @param resultHandler A handler to process the result of this operation. It can either end by finding a track, + * finding a playlist, finding nothing or terminating with an exception. + * @see #loadItemSync(String, AudioLoadResultHandler) + */ + void loadItemSync(final AudioReference reference, final AudioLoadResultHandler resultHandler); + + /** + * Schedules loading a track or playlist with the specified identifier with an ordering key so that items with the + * same ordering key are handled sequentially in the order of calls to this method. + * + * @param orderingKey Object to use as the key for the ordering channel + * @param identifier The identifier that a specific source manager should be able to find the track with. + * @param resultHandler A handler to process the result of this operation. It can either end by finding a track, + * finding a playlist, finding nothing or terminating with an exception. + * @return A future for this operation + * @see #loadItemOrdered(Object, AudioReference, AudioLoadResultHandler) + */ + default Future loadItemOrdered(Object orderingKey, final String identifier, final AudioLoadResultHandler resultHandler) { + return loadItemOrdered(orderingKey, new AudioReference(identifier, null), resultHandler); + } + + /** + * Schedules loading a track or playlist with the specified identifier with an ordering key so that items with the + * same ordering key are handled sequentially in the order of calls to this method. + * + * @param orderingKey Object to use as the key for the ordering channel + * @param reference The audio reference that holds the identifier that a specific source manager + * should be able to find the track with. + * @param resultHandler A handler to process the result of this operation. It can either end by finding a track, + * finding a playlist, finding nothing or terminating with an exception. + * @return A future for this operation + * @see #loadItemOrdered(Object, String, AudioLoadResultHandler) + */ + Future loadItemOrdered(Object orderingKey, final AudioReference reference, final AudioLoadResultHandler resultHandler); + + /** + * Encode a track into an output stream. If the decoder is not supposed to know the number of tracks in advance, then + * the encoder should call MessageOutput#finish() after all the tracks it wanted to write have been written. This will + * make decodeTrack() return null at that position + * + * @param stream The message stream to write it to. + * @param track The track to encode. + * @throws IOException On IO error. + */ + void encodeTrack(MessageOutput stream, AudioTrack track) throws IOException; + + /** + * Decode a track from an input stream. Null returns value indicates reaching the position where the decoder had + * called MessageOutput#finish(). + * + * @param stream The message stream to read it from. + * @return Holder containing the track if it was successfully decoded. + * @throws IOException On IO error. + */ + DecodedTrackHolder decodeTrack(MessageInput stream) throws IOException; + + /** + * @return Audio processing configuration used for tracks executed by this manager. + */ + AudioConfiguration getConfiguration(); + + /** + * Seek ghosting is the effect where while a seek is in progress, buffered audio from the previous location will be + * served until seek is ready or the buffer is empty. + * + * @return True if seek ghosting is enabled. + */ + boolean isUsingSeekGhosting(); + + /** + * @param useSeekGhosting The new state of seek ghosting + */ + void setUseSeekGhosting(boolean useSeekGhosting); + + /** + * @return The length of the internal buffer for audio in milliseconds. + */ + int getFrameBufferDuration(); + + /** + * @param frameBufferDuration New length of the internal buffer for audio in milliseconds. + */ + void setFrameBufferDuration(int frameBufferDuration); + + /** + * Sets the threshold for how long a track can be stuck until the TrackStuckEvent is sent out. A track is considered + * to be stuck if the player receives requests for audio samples from the track, but the audio frame provider of that + * track has been returning no data for the specified time. + * + * @param trackStuckThreshold The threshold in milliseconds. + */ + void setTrackStuckThreshold(long trackStuckThreshold); + + /** + * Sets the threshold for clearing an audio player when it has not been queried for the specified amount of time. + * + * @param cleanupThreshold The threshold in milliseconds. + */ + void setPlayerCleanupThreshold(long cleanupThreshold); + + /** + * Sets the number of threads used for loading processing item load requests. + * + * @param poolSize Maximum number of concurrent threads used for loading items. + */ + void setItemLoaderThreadPoolSize(int poolSize); + + /** + * @return New audio player. + */ + AudioPlayer createPlayer(); + + /** + * @param configurator Function used to reconfigure the request config of all sources which perform HTTP requests. + * Applied to all current and future registered sources. Setting this while sources are already in + * use will close all active connections, so this should be called before the sources have been + * used. + */ + void setHttpRequestConfigurator(Function configurator); + + /** + * @param configurator Function used to reconfigure the HTTP builder of all sources which perform HTTP requests. + * Applied to all current and future registered sources. Setting this while sources are already in + * use will close all active connections, so this should be called before the sources have been + * used. + */ + void setHttpBuilderConfigurator(Consumer configurator); +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayerOptions.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayerOptions.java index 51ebbf111..286c4e332 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayerOptions.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/AudioPlayerOptions.java @@ -9,26 +9,26 @@ * Mutable options of an audio player which may be applied in real-time. */ public class AudioPlayerOptions { - /** - * Volume level of the audio, see {@link AudioPlayer#setVolume(int)}. Applied in real-time. - */ - public final AtomicInteger volumeLevel; - /** - * Current PCM filter factory. Applied in real-time. - */ - public final AtomicReference filterFactory; - /** - * Current frame buffer size. If not set, the global default is used. Changing this only affects the next track that - * is started. - */ - public final AtomicReference frameBufferDuration; + /** + * Volume level of the audio, see {@link AudioPlayer#setVolume(int)}. Applied in real-time. + */ + public final AtomicInteger volumeLevel; + /** + * Current PCM filter factory. Applied in real-time. + */ + public final AtomicReference filterFactory; + /** + * Current frame buffer size. If not set, the global default is used. Changing this only affects the next track that + * is started. + */ + public final AtomicReference frameBufferDuration; - /** - * New instance of player options. By default, frame buffer duration is not set, hence taken from global settings. - */ - public AudioPlayerOptions() { - this.volumeLevel = new AtomicInteger(100); - this.filterFactory = new AtomicReference<>(); - this.frameBufferDuration = new AtomicReference<>(); - } + /** + * New instance of player options. By default, frame buffer duration is not set, hence taken from global settings. + */ + public AudioPlayerOptions() { + this.volumeLevel = new AtomicInteger(100); + this.filterFactory = new AtomicReference<>(); + this.frameBufferDuration = new AtomicReference<>(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/DefaultAudioPlayer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/DefaultAudioPlayer.java index 8c922e54e..20bac46a2 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/DefaultAudioPlayer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/DefaultAudioPlayer.java @@ -1,26 +1,14 @@ package com.sedmelluq.discord.lavaplayer.player; import com.sedmelluq.discord.lavaplayer.filter.PcmFilterFactory; -import com.sedmelluq.discord.lavaplayer.player.event.AudioEvent; -import com.sedmelluq.discord.lavaplayer.player.event.AudioEventListener; -import com.sedmelluq.discord.lavaplayer.player.event.PlayerPauseEvent; -import com.sedmelluq.discord.lavaplayer.player.event.PlayerResumeEvent; -import com.sedmelluq.discord.lavaplayer.player.event.TrackEndEvent; -import com.sedmelluq.discord.lavaplayer.player.event.TrackExceptionEvent; -import com.sedmelluq.discord.lavaplayer.player.event.TrackStartEvent; -import com.sedmelluq.discord.lavaplayer.player.event.TrackStuckEvent; +import com.sedmelluq.discord.lavaplayer.player.event.*; import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; import com.sedmelluq.discord.lavaplayer.track.InternalAudioTrack; import com.sedmelluq.discord.lavaplayer.track.TrackStateListener; -import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame; -import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrameProvider; -import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrameProviderTools; -import com.sedmelluq.discord.lavaplayer.track.playback.AudioTrackExecutor; -import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor; -import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame; +import com.sedmelluq.discord.lavaplayer.track.playback.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,368 +19,367 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; -import static com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason.CLEANUP; -import static com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason.FINISHED; -import static com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason.LOAD_FAILED; -import static com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason.REPLACED; -import static com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason.STOPPED; +import static com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason.*; /** * An audio player that is capable of playing audio tracks and provides audio frames from the currently playing track. */ public class DefaultAudioPlayer implements AudioPlayer, TrackStateListener { - private static final Logger log = LoggerFactory.getLogger(AudioPlayer.class); - - private volatile InternalAudioTrack activeTrack; - private volatile long lastRequestTime; - private volatile long lastReceiveTime; - private volatile boolean stuckEventSent; - private volatile InternalAudioTrack shadowTrack; - private final AtomicBoolean paused; - private final DefaultAudioPlayerManager manager; - private final List listeners; - private final Object trackSwitchLock; - private final AudioPlayerOptions options; - - /** - * @param manager Audio player manager which this player is attached to - */ - public DefaultAudioPlayer(DefaultAudioPlayerManager manager) { - this.manager = manager; - activeTrack = null; - paused = new AtomicBoolean(); - listeners = new ArrayList<>(); - trackSwitchLock = new Object(); - options = new AudioPlayerOptions(); - } - - /** - * @return Currently playing track - */ - public AudioTrack getPlayingTrack() { - return activeTrack; - } - - /** - * @param track The track to start playing - */ - public void playTrack(AudioTrack track) { - startTrack(track, false); - } - - /** - * @param track The track to start playing, passing null will stop the current track and return false - * @param noInterrupt Whether to only start if nothing else is playing - * @return True if the track was started - */ - public boolean startTrack(AudioTrack track, boolean noInterrupt) { - InternalAudioTrack newTrack = (InternalAudioTrack) track; - InternalAudioTrack previousTrack; - - synchronized (trackSwitchLock) { - previousTrack = activeTrack; - - if (noInterrupt && previousTrack != null) { - return false; - } - - activeTrack = newTrack; - lastRequestTime = System.currentTimeMillis(); - lastReceiveTime = System.nanoTime(); - stuckEventSent = false; - - if (previousTrack != null) { - previousTrack.stop(); - dispatchEvent(new TrackEndEvent(this, previousTrack, newTrack == null ? STOPPED : REPLACED)); - - shadowTrack = previousTrack; - } + private static final Logger log = LoggerFactory.getLogger(AudioPlayer.class); + + private volatile InternalAudioTrack activeTrack; + private volatile long lastRequestTime; + private volatile long lastReceiveTime; + private volatile boolean stuckEventSent; + private volatile InternalAudioTrack shadowTrack; + private final AtomicBoolean paused; + private final DefaultAudioPlayerManager manager; + private final List listeners; + private final Object trackSwitchLock; + private final AudioPlayerOptions options; + + /** + * @param manager Audio player manager which this player is attached to + */ + public DefaultAudioPlayer(DefaultAudioPlayerManager manager) { + this.manager = manager; + activeTrack = null; + paused = new AtomicBoolean(); + listeners = new ArrayList<>(); + trackSwitchLock = new Object(); + options = new AudioPlayerOptions(); } - if (newTrack == null) { - shadowTrack = null; - return false; + /** + * @return Currently playing track + */ + public AudioTrack getPlayingTrack() { + return activeTrack; } - dispatchEvent(new TrackStartEvent(this, newTrack)); + /** + * @param track The track to start playing + */ + public void playTrack(AudioTrack track) { + startTrack(track, false); + } - manager.executeTrack(this, newTrack, manager.getConfiguration(), options); - return true; - } + /** + * @param track The track to start playing, passing null will stop the current track and return false + * @param noInterrupt Whether to only start if nothing else is playing + * @return True if the track was started + */ + public boolean startTrack(AudioTrack track, boolean noInterrupt) { + InternalAudioTrack newTrack = (InternalAudioTrack) track; + InternalAudioTrack previousTrack; + + synchronized (trackSwitchLock) { + previousTrack = activeTrack; + + if (noInterrupt && previousTrack != null) { + return false; + } + + activeTrack = newTrack; + lastRequestTime = System.currentTimeMillis(); + lastReceiveTime = System.nanoTime(); + stuckEventSent = false; + + if (previousTrack != null) { + previousTrack.stop(); + dispatchEvent(new TrackEndEvent(this, previousTrack, newTrack == null ? STOPPED : REPLACED)); + + shadowTrack = previousTrack; + } + } - /** - * Stop currently playing track. - */ - public void stopTrack() { - stopWithReason(STOPPED); - } + if (newTrack == null) { + shadowTrack = null; + return false; + } - private void stopWithReason(AudioTrackEndReason reason) { - shadowTrack = null; + dispatchEvent(new TrackStartEvent(this, newTrack)); - synchronized (trackSwitchLock) { - InternalAudioTrack previousTrack = activeTrack; - activeTrack = null; + manager.executeTrack(this, newTrack, manager.getConfiguration(), options); + return true; + } - if (previousTrack != null) { - previousTrack.stop(); - dispatchEvent(new TrackEndEvent(this, previousTrack, reason)); - } + /** + * Stop currently playing track. + */ + public void stopTrack() { + stopWithReason(STOPPED); } - } - private AudioFrame provideShadowFrame() { - InternalAudioTrack shadow = shadowTrack; - AudioFrame frame = null; + private void stopWithReason(AudioTrackEndReason reason) { + shadowTrack = null; - if (shadow != null) { - frame = shadow.provide(); + synchronized (trackSwitchLock) { + InternalAudioTrack previousTrack = activeTrack; + activeTrack = null; - if (frame != null && frame.isTerminator()) { - shadowTrack = null; - frame = null; - } + if (previousTrack != null) { + previousTrack.stop(); + dispatchEvent(new TrackEndEvent(this, previousTrack, reason)); + } + } } - return frame; - } + private AudioFrame provideShadowFrame() { + InternalAudioTrack shadow = shadowTrack; + AudioFrame frame = null; - private boolean provideShadowFrame(MutableAudioFrame targetFrame) { - InternalAudioTrack shadow = shadowTrack; + if (shadow != null) { + frame = shadow.provide(); - if (shadow != null && shadow.provide(targetFrame)) { - if (targetFrame.isTerminator()) { - shadowTrack = null; - return false; - } + if (frame != null && frame.isTerminator()) { + shadowTrack = null; + frame = null; + } + } - return true; + return frame; } - return false; - } + private boolean provideShadowFrame(MutableAudioFrame targetFrame) { + InternalAudioTrack shadow = shadowTrack; - @Override - public AudioFrame provide() { - return AudioFrameProviderTools.delegateToTimedProvide(this); - } + if (shadow != null && shadow.provide(targetFrame)) { + if (targetFrame.isTerminator()) { + shadowTrack = null; + return false; + } - @Override - public AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { - InternalAudioTrack track; + return true; + } - lastRequestTime = System.currentTimeMillis(); + return false; + } - if (timeout == 0 && paused.get()) { - return null; + @Override + public AudioFrame provide() { + return AudioFrameProviderTools.delegateToTimedProvide(this); } - while ((track = activeTrack) != null) { - AudioFrame frame = timeout > 0 ? track.provide(timeout, unit) : track.provide(); + @Override + public AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { + InternalAudioTrack track; - if (frame != null) { - lastReceiveTime = System.nanoTime(); - shadowTrack = null; + lastRequestTime = System.currentTimeMillis(); - if (frame.isTerminator()) { - handleTerminator(track); - continue; + if (timeout == 0 && paused.get()) { + return null; } - } else if (timeout == 0) { - checkStuck(track); - frame = provideShadowFrame(); - } + while ((track = activeTrack) != null) { + AudioFrame frame = timeout > 0 ? track.provide(timeout, unit) : track.provide(); - return frame; - } + if (frame != null) { + lastReceiveTime = System.nanoTime(); + shadowTrack = null; - return null; - } + if (frame.isTerminator()) { + handleTerminator(track); + continue; + } + } else if (timeout == 0) { + checkStuck(track); - @Override - public boolean provide(MutableAudioFrame targetFrame) { - try { - return provide(targetFrame, 0, TimeUnit.MILLISECONDS); - } catch (TimeoutException | InterruptedException e) { - ExceptionTools.keepInterrupted(e); - throw new RuntimeException(e); + frame = provideShadowFrame(); + } + + return frame; + } + + return null; } - } - @Override - public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) - throws TimeoutException, InterruptedException { + @Override + public boolean provide(MutableAudioFrame targetFrame) { + try { + return provide(targetFrame, 0, TimeUnit.MILLISECONDS); + } catch (TimeoutException | InterruptedException e) { + ExceptionTools.keepInterrupted(e); + throw new RuntimeException(e); + } + } - InternalAudioTrack track; + @Override + public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) + throws TimeoutException, InterruptedException { - lastRequestTime = System.currentTimeMillis(); + InternalAudioTrack track; - if (timeout == 0 && paused.get()) { - return false; - } + lastRequestTime = System.currentTimeMillis(); - while ((track = activeTrack) != null) { - if (timeout > 0 ? track.provide(targetFrame, timeout, unit) : track.provide(targetFrame)) { - lastReceiveTime = System.nanoTime(); - shadowTrack = null; + if (timeout == 0 && paused.get()) { + return false; + } - if (targetFrame.isTerminator()) { - handleTerminator(track); - continue; + while ((track = activeTrack) != null) { + if (timeout > 0 ? track.provide(targetFrame, timeout, unit) : track.provide(targetFrame)) { + lastReceiveTime = System.nanoTime(); + shadowTrack = null; + + if (targetFrame.isTerminator()) { + handleTerminator(track); + continue; + } + + return true; + } else if (timeout == 0) { + checkStuck(track); + return provideShadowFrame(targetFrame); + } else { + return false; + } } - return true; - } else if (timeout == 0) { - checkStuck(track); - return provideShadowFrame(targetFrame); - } else { return false; - } } - return false; - } + private void handleTerminator(InternalAudioTrack track) { + synchronized (trackSwitchLock) { + if (activeTrack == track) { + activeTrack = null; - private void handleTerminator(InternalAudioTrack track) { - synchronized (trackSwitchLock) { - if (activeTrack == track) { - activeTrack = null; - - dispatchEvent(new TrackEndEvent(this, track, track.getActiveExecutor().failedBeforeLoad() ? LOAD_FAILED : FINISHED)); - } + dispatchEvent(new TrackEndEvent(this, track, track.getActiveExecutor().failedBeforeLoad() ? LOAD_FAILED : FINISHED)); + } + } } - } - private void checkStuck(AudioTrack track) { - if (!stuckEventSent && System.nanoTime() - lastReceiveTime > manager.getTrackStuckThresholdNanos()) { - stuckEventSent = true; + private void checkStuck(AudioTrack track) { + if (!stuckEventSent && System.nanoTime() - lastReceiveTime > manager.getTrackStuckThresholdNanos()) { + stuckEventSent = true; - StackTraceElement[] stackTrace = getStackTrace(track); - long threshold = TimeUnit.NANOSECONDS.toMillis(manager.getTrackStuckThresholdNanos()); + StackTraceElement[] stackTrace = getStackTrace(track); + long threshold = TimeUnit.NANOSECONDS.toMillis(manager.getTrackStuckThresholdNanos()); - dispatchEvent(new TrackStuckEvent(this, track, threshold, stackTrace)); + dispatchEvent(new TrackStuckEvent(this, track, threshold, stackTrace)); + } } - } - private StackTraceElement[] getStackTrace(AudioTrack track) { - if (track instanceof InternalAudioTrack) { - AudioTrackExecutor executor = ((InternalAudioTrack) track).getActiveExecutor(); + private StackTraceElement[] getStackTrace(AudioTrack track) { + if (track instanceof InternalAudioTrack) { + AudioTrackExecutor executor = ((InternalAudioTrack) track).getActiveExecutor(); - if (executor instanceof LocalAudioTrackExecutor) { - return ((LocalAudioTrackExecutor) executor).getStackTrace(); - } + if (executor instanceof LocalAudioTrackExecutor) { + return ((LocalAudioTrackExecutor) executor).getStackTrace(); + } + } + + return null; } - return null; - } + public int getVolume() { + return options.volumeLevel.get(); + } - public int getVolume() { - return options.volumeLevel.get(); - } + public void setVolume(int volume) { + options.volumeLevel.set(Math.min(1000, Math.max(0, volume))); + } - public void setVolume(int volume) { - options.volumeLevel.set(Math.min(1000, Math.max(0, volume))); - } + public void setFilterFactory(PcmFilterFactory factory) { + options.filterFactory.set(factory); + } - public void setFilterFactory(PcmFilterFactory factory) { - options.filterFactory.set(factory); - } + public void setFrameBufferDuration(Integer duration) { + if (duration != null) { + duration = Math.max(200, duration); + } - public void setFrameBufferDuration(Integer duration) { - if (duration != null) { - duration = Math.max(200, duration); + options.frameBufferDuration.set(duration); } - options.frameBufferDuration.set(duration); - } - - /** - * @return Whether the player is paused - */ - public boolean isPaused() { - return paused.get(); - } - - /** - * @param value True to pause, false to resume - */ - public void setPaused(boolean value) { - if (paused.compareAndSet(!value, value)) { - if (value) { - dispatchEvent(new PlayerPauseEvent(this)); - } else { - dispatchEvent(new PlayerResumeEvent(this)); - lastReceiveTime = System.nanoTime(); - } + /** + * @return Whether the player is paused + */ + public boolean isPaused() { + return paused.get(); } - } - - /** - * Destroy the player and stop playing track. - */ - public void destroy() { - stopTrack(); - } - - /** - * Add a listener to events from this player. - * @param listener New listener - */ - public void addListener(AudioEventListener listener) { - synchronized (trackSwitchLock) { - listeners.add(listener); + + /** + * @param value True to pause, false to resume + */ + public void setPaused(boolean value) { + if (paused.compareAndSet(!value, value)) { + if (value) { + dispatchEvent(new PlayerPauseEvent(this)); + } else { + dispatchEvent(new PlayerResumeEvent(this)); + lastReceiveTime = System.nanoTime(); + } + } + } + + /** + * Destroy the player and stop playing track. + */ + public void destroy() { + stopTrack(); } - } - - /** - * Remove an attached listener using identity comparison. - * @param listener The listener to remove - */ - public void removeListener(AudioEventListener listener) { - synchronized (trackSwitchLock) { - for (Iterator iterator = listeners.iterator(); iterator.hasNext(); ) { - if (iterator.next() == listener) { - iterator.remove(); + + /** + * Add a listener to events from this player. + * + * @param listener New listener + */ + public void addListener(AudioEventListener listener) { + synchronized (trackSwitchLock) { + listeners.add(listener); } - } } - } - private void dispatchEvent(AudioEvent event) { - log.debug("Firing an event with class {}", event.getClass().getSimpleName()); + /** + * Remove an attached listener using identity comparison. + * + * @param listener The listener to remove + */ + public void removeListener(AudioEventListener listener) { + synchronized (trackSwitchLock) { + for (Iterator iterator = listeners.iterator(); iterator.hasNext(); ) { + if (iterator.next() == listener) { + iterator.remove(); + } + } + } + } - synchronized (trackSwitchLock) { - for (AudioEventListener listener : listeners) { - try { - listener.onEvent(event); - } catch (Exception e) { - log.error("Handler of event {} threw an exception.", event, e); + private void dispatchEvent(AudioEvent event) { + log.debug("Firing an event with class {}", event.getClass().getSimpleName()); + + synchronized (trackSwitchLock) { + for (AudioEventListener listener : listeners) { + try { + listener.onEvent(event); + } catch (Exception e) { + log.error("Handler of event {} threw an exception.", event, e); + } + } } - } } - } - - @Override - public void onTrackException(AudioTrack track, FriendlyException exception) { - dispatchEvent(new TrackExceptionEvent(this, track, exception)); - } - - @Override - public void onTrackStuck(AudioTrack track, long thresholdMs) { - dispatchEvent(new TrackStuckEvent(this, track, thresholdMs, null)); - } - - /** - * Check if the player should be "cleaned up" - stopped due to nothing using it, with the given threshold. - * @param threshold Threshold in milliseconds to use - */ - public void checkCleanup(long threshold) { - AudioTrack track = getPlayingTrack(); - if (track != null && System.currentTimeMillis() - lastRequestTime >= threshold) { - log.debug("Triggering cleanup on an audio player playing track {}", track); - - stopWithReason(CLEANUP); + + @Override + public void onTrackException(AudioTrack track, FriendlyException exception) { + dispatchEvent(new TrackExceptionEvent(this, track, exception)); + } + + @Override + public void onTrackStuck(AudioTrack track, long thresholdMs) { + dispatchEvent(new TrackStuckEvent(this, track, thresholdMs, null)); + } + + /** + * Check if the player should be "cleaned up" - stopped due to nothing using it, with the given threshold. + * + * @param threshold Threshold in milliseconds to use + */ + public void checkCleanup(long threshold) { + AudioTrack track = getPlayingTrack(); + if (track != null && System.currentTimeMillis() - lastRequestTime >= threshold) { + log.debug("Triggering cleanup on an audio player playing track {}", track); + + stopWithReason(CLEANUP); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/DefaultAudioPlayerManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/DefaultAudioPlayerManager.java index 0763c8b74..c173c37c3 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/DefaultAudioPlayerManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/DefaultAudioPlayerManager.java @@ -3,7 +3,9 @@ import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; import com.sedmelluq.discord.lavaplayer.source.ProbingAudioSourceManager; import com.sedmelluq.discord.lavaplayer.tools.*; -import com.sedmelluq.discord.lavaplayer.tools.io.*; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; +import com.sedmelluq.discord.lavaplayer.tools.io.MessageInput; +import com.sedmelluq.discord.lavaplayer.tools.io.MessageOutput; import com.sedmelluq.discord.lavaplayer.track.*; import com.sedmelluq.discord.lavaplayer.track.playback.AudioTrackExecutor; import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor; @@ -15,449 +17,456 @@ import org.slf4j.LoggerFactory; import java.io.*; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Function; -import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.*; +import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.FAULT; +import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS; /** * The default implementation of audio player manager. */ public class DefaultAudioPlayerManager implements AudioPlayerManager { - private static final int TRACK_INFO_VERSIONED = 1; - private static final int TRACK_INFO_VERSION = 3; + private static final int TRACK_INFO_VERSIONED = 1; + private static final int TRACK_INFO_VERSION = 3; - private static final int DEFAULT_FRAME_BUFFER_DURATION = (int) TimeUnit.SECONDS.toMillis(5); - private static final int DEFAULT_CLEANUP_THRESHOLD = (int) TimeUnit.MINUTES.toMillis(1); + private static final int DEFAULT_FRAME_BUFFER_DURATION = (int) TimeUnit.SECONDS.toMillis(5); + private static final int DEFAULT_CLEANUP_THRESHOLD = (int) TimeUnit.MINUTES.toMillis(1); - private static final int MAXIMUM_LOAD_REDIRECTS = 5; - private static final int DEFAULT_LOADER_POOL_SIZE = 10; - private static final int LOADER_QUEUE_CAPACITY = 5000; + private static final int MAXIMUM_LOAD_REDIRECTS = 5; + private static final int DEFAULT_LOADER_POOL_SIZE = 10; + private static final int LOADER_QUEUE_CAPACITY = 5000; - private static final Logger log = LoggerFactory.getLogger(DefaultAudioPlayerManager.class); + private static final Logger log = LoggerFactory.getLogger(DefaultAudioPlayerManager.class); - private final List sourceManagers; - private volatile Function httpConfigurator; - private volatile Consumer httpBuilderConfigurator; + private final List sourceManagers; + private volatile Function httpConfigurator; + private volatile Consumer httpBuilderConfigurator; - // Executors - private final ExecutorService trackPlaybackExecutorService; - private final ThreadPoolExecutor trackInfoExecutorService; - private final ScheduledExecutorService scheduledExecutorService; - private final OrderedExecutor orderedInfoExecutor; + // Executors + private final ExecutorService trackPlaybackExecutorService; + private final ThreadPoolExecutor trackInfoExecutorService; + private final ScheduledExecutorService scheduledExecutorService; + private final OrderedExecutor orderedInfoExecutor; - // Configuration - private volatile long trackStuckThreshold; - private volatile AudioConfiguration configuration; - private final AtomicLong cleanupThreshold; - private volatile int frameBufferDuration; - private volatile boolean useSeekGhosting; + // Configuration + private volatile long trackStuckThreshold; + private volatile AudioConfiguration configuration; + private final AtomicLong cleanupThreshold; + private volatile int frameBufferDuration; + private volatile boolean useSeekGhosting; - // Additional services - private final GarbageCollectionMonitor garbageCollectionMonitor; - private final AudioPlayerLifecycleManager lifecycleManager; + // Additional services + private final GarbageCollectionMonitor garbageCollectionMonitor; + private final AudioPlayerLifecycleManager lifecycleManager; - /** - * Create a new instance - */ - public DefaultAudioPlayerManager() { - sourceManagers = new ArrayList<>(); + /** + * Create a new instance + */ + public DefaultAudioPlayerManager() { + sourceManagers = new ArrayList<>(); - // Executors - trackPlaybackExecutorService = new ThreadPoolExecutor(1, Integer.MAX_VALUE, 10, TimeUnit.SECONDS, + // Executors + trackPlaybackExecutorService = new ThreadPoolExecutor(1, Integer.MAX_VALUE, 10, TimeUnit.SECONDS, new SynchronousQueue<>(), new DaemonThreadFactory("playback")); - trackInfoExecutorService = ExecutorTools.createEagerlyScalingExecutor(1, DEFAULT_LOADER_POOL_SIZE, + trackInfoExecutorService = ExecutorTools.createEagerlyScalingExecutor(1, DEFAULT_LOADER_POOL_SIZE, TimeUnit.SECONDS.toMillis(30), LOADER_QUEUE_CAPACITY, new DaemonThreadFactory("info-loader")); - scheduledExecutorService = Executors.newScheduledThreadPool(1, new DaemonThreadFactory("manager")); - orderedInfoExecutor = new OrderedExecutor(trackInfoExecutorService); + scheduledExecutorService = Executors.newScheduledThreadPool(1, new DaemonThreadFactory("manager")); + orderedInfoExecutor = new OrderedExecutor(trackInfoExecutorService); + + // Configuration + trackStuckThreshold = TimeUnit.MILLISECONDS.toNanos(10000); + configuration = new AudioConfiguration(); + cleanupThreshold = new AtomicLong(DEFAULT_CLEANUP_THRESHOLD); + frameBufferDuration = DEFAULT_FRAME_BUFFER_DURATION; + useSeekGhosting = true; + + // Additional services + garbageCollectionMonitor = new GarbageCollectionMonitor(scheduledExecutorService); + lifecycleManager = new AudioPlayerLifecycleManager(scheduledExecutorService, cleanupThreshold); + lifecycleManager.initialise(); + } - // Configuration - trackStuckThreshold = TimeUnit.MILLISECONDS.toNanos(10000); - configuration = new AudioConfiguration(); - cleanupThreshold = new AtomicLong(DEFAULT_CLEANUP_THRESHOLD); - frameBufferDuration = DEFAULT_FRAME_BUFFER_DURATION; - useSeekGhosting = true; + @Override + public void shutdown() { + garbageCollectionMonitor.disable(); + lifecycleManager.shutdown(); + + for (AudioSourceManager sourceManager : sourceManagers) { + sourceManager.shutdown(); + } + + ExecutorTools.shutdownExecutor(trackPlaybackExecutorService, "track playback"); + ExecutorTools.shutdownExecutor(trackInfoExecutorService, "track info"); + ExecutorTools.shutdownExecutor(scheduledExecutorService, "scheduled operations"); + } + + @Override + public void enableGcMonitoring() { + garbageCollectionMonitor.enable(); + } + + @Override + public void registerSourceManager(AudioSourceManager sourceManager) { + sourceManagers.add(sourceManager); + + if (sourceManager instanceof HttpConfigurable) { + Function configurator = httpConfigurator; + + if (configurator != null) { + ((HttpConfigurable) sourceManager).configureRequests(configurator); + } + + Consumer builderConfigurator = httpBuilderConfigurator; + + if (builderConfigurator != null) { + ((HttpConfigurable) sourceManager).configureBuilder(builderConfigurator); + } + } + } + + @Override + public T source(Class klass) { + for (AudioSourceManager sourceManager : sourceManagers) { + if (klass.isAssignableFrom(sourceManager.getClass())) { + return (T) sourceManager; + } + } - // Additional services - garbageCollectionMonitor = new GarbageCollectionMonitor(scheduledExecutorService); - lifecycleManager = new AudioPlayerLifecycleManager(scheduledExecutorService, cleanupThreshold); - lifecycleManager.initialise(); - } - - @Override - public void shutdown() { - garbageCollectionMonitor.disable(); - lifecycleManager.shutdown(); - - for (AudioSourceManager sourceManager : sourceManagers) { - sourceManager.shutdown(); - } - - ExecutorTools.shutdownExecutor(trackPlaybackExecutorService, "track playback"); - ExecutorTools.shutdownExecutor(trackInfoExecutorService, "track info"); - ExecutorTools.shutdownExecutor(scheduledExecutorService, "scheduled operations"); - } - - @Override - public void enableGcMonitoring() { - garbageCollectionMonitor.enable(); - } - - @Override - public void registerSourceManager(AudioSourceManager sourceManager) { - sourceManagers.add(sourceManager); - - if (sourceManager instanceof HttpConfigurable) { - Function configurator = httpConfigurator; - - if (configurator != null) { - ((HttpConfigurable) sourceManager).configureRequests(configurator); - } - - Consumer builderConfigurator = httpBuilderConfigurator; - - if (builderConfigurator != null) { - ((HttpConfigurable) sourceManager).configureBuilder(builderConfigurator); - } - } - } - - @Override - public T source(Class klass) { - for (AudioSourceManager sourceManager : sourceManagers) { - if (klass.isAssignableFrom(sourceManager.getClass())) { - return (T) sourceManager; - } - } - - return null; - } - - @Override - public List getSourceManagers() { - return Collections.unmodifiableList(sourceManagers); - } - - @Override - public void loadItemSync(final AudioReference reference, final AudioLoadResultHandler resultHandler) { - boolean[] reported = new boolean[1]; - - try { - if (!checkSourcesForItem(reference, resultHandler, reported)) { - log.debug("No matches for track with identifier {}.", reference.identifier); - resultHandler.noMatches(); - } - } catch (Throwable throwable) { - if (reported[0]) { - log.warn("Load result handler for {} threw an exception", reference.identifier, throwable); - } else { - dispatchItemLoadFailure(reference.identifier, resultHandler, throwable); - } - - ExceptionTools.rethrowErrors(throwable); - } - } - - @Override - public Future loadItem(final AudioReference reference, final AudioLoadResultHandler resultHandler) { - try { - return trackInfoExecutorService.submit(() -> { - loadItemSync(reference, resultHandler); return null; - }); - } catch (RejectedExecutionException e) { - return handleLoadRejected(reference.identifier, resultHandler, e); } - } - @Override - public Future loadItemOrdered(Object orderingKey, final AudioReference reference, final AudioLoadResultHandler resultHandler) { - try { - return orderedInfoExecutor.submit(orderingKey, () -> { - loadItemSync(reference, resultHandler); + @Override + public List getSourceManagers() { + return Collections.unmodifiableList(sourceManagers); + } + + @Override + public void loadItemSync(final AudioReference reference, final AudioLoadResultHandler resultHandler) { + boolean[] reported = new boolean[1]; + + try { + if (!checkSourcesForItem(reference, resultHandler, reported)) { + log.debug("No matches for track with identifier {}.", reference.identifier); + resultHandler.noMatches(); + } + } catch (Throwable throwable) { + if (reported[0]) { + log.warn("Load result handler for {} threw an exception", reference.identifier, throwable); + } else { + dispatchItemLoadFailure(reference.identifier, resultHandler, throwable); + } + + ExceptionTools.rethrowErrors(throwable); + } + } + + @Override + public Future loadItem(final AudioReference reference, final AudioLoadResultHandler resultHandler) { + try { + return trackInfoExecutorService.submit(() -> { + loadItemSync(reference, resultHandler); + return null; + }); + } catch (RejectedExecutionException e) { + return handleLoadRejected(reference.identifier, resultHandler, e); + } + } + + @Override + public Future loadItemOrdered(Object orderingKey, final AudioReference reference, final AudioLoadResultHandler resultHandler) { + try { + return orderedInfoExecutor.submit(orderingKey, () -> { + loadItemSync(reference, resultHandler); + return null; + }); + } catch (RejectedExecutionException e) { + return handleLoadRejected(reference.identifier, resultHandler, e); + } + } + + private Future handleLoadRejected(String identifier, AudioLoadResultHandler resultHandler, RejectedExecutionException e) { + FriendlyException exception = new FriendlyException("Cannot queue loading a track, queue is full.", SUSPICIOUS, e); + ExceptionTools.log(log, exception, "queueing item " + identifier); + + resultHandler.loadFailed(exception); + + return ExecutorTools.COMPLETED_VOID; + } + + private void dispatchItemLoadFailure(String identifier, AudioLoadResultHandler resultHandler, Throwable throwable) { + FriendlyException exception = ExceptionTools.wrapUnfriendlyExceptions("Something went wrong when looking up the track", FAULT, throwable); + ExceptionTools.log(log, exception, "loading item " + identifier); + + resultHandler.loadFailed(exception); + } + + @Override + public void encodeTrack(MessageOutput stream, AudioTrack track) throws IOException { + DataOutput output = stream.startMessage(); + output.write(TRACK_INFO_VERSION); + + AudioTrackInfo trackInfo = track.getInfo(); + output.writeUTF(trackInfo.title); + output.writeUTF(trackInfo.author); + output.writeLong(trackInfo.length); + output.writeUTF(trackInfo.identifier); + output.writeBoolean(trackInfo.isStream); + DataFormatTools.writeNullableText(output, trackInfo.uri); + DataFormatTools.writeNullableText(output, trackInfo.artworkUrl); + DataFormatTools.writeNullableText(output, trackInfo.isrc); + + encodeTrackDetails(track, output); + output.writeLong(track.getPosition()); + + stream.commitMessage(TRACK_INFO_VERSIONED); + } + + @Override + public DecodedTrackHolder decodeTrack(MessageInput stream) throws IOException { + DataInput input = stream.nextMessage(); + if (input == null) { + return null; + } + + int version = (stream.getMessageFlags() & TRACK_INFO_VERSIONED) != 0 ? (input.readByte() & 0xFF) : 1; + + AudioTrackInfo trackInfo = new AudioTrackInfo( + input.readUTF(), + input.readUTF(), + input.readLong(), + input.readUTF(), + input.readBoolean(), + version >= 2 ? DataFormatTools.readNullableText(input) : null, + version >= 3 ? DataFormatTools.readNullableText(input) : null, + version >= 3 ? DataFormatTools.readNullableText(input) : null + ); + AudioTrack track = decodeTrackDetails(trackInfo, input); + long position = input.readLong(); + + if (track != null) { + track.setPosition(position); + } + + stream.skipRemainingBytes(); + + return new DecodedTrackHolder(track); + } + + /** + * Encodes an audio track to a byte array. Does not include AudioTrackInfo in the buffer. + * + * @param track The track to encode + * @return The bytes of the encoded data + */ + public byte[] encodeTrackDetails(AudioTrack track) { + try { + ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); + DataOutput output = new DataOutputStream(byteOutput); + + encodeTrackDetails(track, output); + + return byteOutput.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void encodeTrackDetails(AudioTrack track, DataOutput output) throws IOException { + AudioSourceManager sourceManager = track.getSourceManager(); + output.writeUTF(sourceManager.getSourceName()); + sourceManager.encodeTrack(track, output); + } + + /** + * Decodes an audio track from a byte array. + * + * @param trackInfo Track info for the track to decode + * @param buffer Byte array containing the encoded track + * @return Decoded audio track + */ + public AudioTrack decodeTrackDetails(AudioTrackInfo trackInfo, byte[] buffer) { + try { + DataInput input = new DataInputStream(new ByteArrayInputStream(buffer)); + return decodeTrackDetails(trackInfo, input); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private AudioTrack decodeTrackDetails(AudioTrackInfo trackInfo, DataInput input) throws IOException { + String sourceName = input.readUTF(); + + for (AudioSourceManager sourceManager : sourceManagers) { + if (sourceName.equals(sourceManager.getSourceName())) { + return sourceManager.decodeTrack(trackInfo, input); + } + } + return null; - }); - } catch (RejectedExecutionException e) { - return handleLoadRejected(reference.identifier, resultHandler, e); - } - } - - private Future handleLoadRejected(String identifier, AudioLoadResultHandler resultHandler, RejectedExecutionException e) { - FriendlyException exception = new FriendlyException("Cannot queue loading a track, queue is full.", SUSPICIOUS, e); - ExceptionTools.log(log, exception, "queueing item " + identifier); - - resultHandler.loadFailed(exception); - - return ExecutorTools.COMPLETED_VOID; - } - - private void dispatchItemLoadFailure(String identifier, AudioLoadResultHandler resultHandler, Throwable throwable) { - FriendlyException exception = ExceptionTools.wrapUnfriendlyExceptions("Something went wrong when looking up the track", FAULT, throwable); - ExceptionTools.log(log, exception, "loading item " + identifier); - - resultHandler.loadFailed(exception); - } - - @Override - public void encodeTrack(MessageOutput stream, AudioTrack track) throws IOException { - DataOutput output = stream.startMessage(); - output.write(TRACK_INFO_VERSION); - - AudioTrackInfo trackInfo = track.getInfo(); - output.writeUTF(trackInfo.title); - output.writeUTF(trackInfo.author); - output.writeLong(trackInfo.length); - output.writeUTF(trackInfo.identifier); - output.writeBoolean(trackInfo.isStream); - DataFormatTools.writeNullableText(output, trackInfo.uri); - DataFormatTools.writeNullableText(output, trackInfo.artworkUrl); - DataFormatTools.writeNullableText(output, trackInfo.isrc); - - encodeTrackDetails(track, output); - output.writeLong(track.getPosition()); - - stream.commitMessage(TRACK_INFO_VERSIONED); - } - - @Override - public DecodedTrackHolder decodeTrack(MessageInput stream) throws IOException { - DataInput input = stream.nextMessage(); - if (input == null) { - return null; - } - - int version = (stream.getMessageFlags() & TRACK_INFO_VERSIONED) != 0 ? (input.readByte() & 0xFF) : 1; - - AudioTrackInfo trackInfo = new AudioTrackInfo( - input.readUTF(), - input.readUTF(), - input.readLong(), - input.readUTF(), - input.readBoolean(), - version >= 2 ? DataFormatTools.readNullableText(input) : null, - version >= 3 ? DataFormatTools.readNullableText(input) : null, - version >= 3 ? DataFormatTools.readNullableText(input) : null - ); - AudioTrack track = decodeTrackDetails(trackInfo, input); - long position = input.readLong(); - - if (track != null) { - track.setPosition(position); - } - - stream.skipRemainingBytes(); - - return new DecodedTrackHolder(track); - } - - /** - * Encodes an audio track to a byte array. Does not include AudioTrackInfo in the buffer. - * @param track The track to encode - * @return The bytes of the encoded data - */ - public byte[] encodeTrackDetails(AudioTrack track) { - try { - ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); - DataOutput output = new DataOutputStream(byteOutput); - - encodeTrackDetails(track, output); - - return byteOutput.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private void encodeTrackDetails(AudioTrack track, DataOutput output) throws IOException { - AudioSourceManager sourceManager = track.getSourceManager(); - output.writeUTF(sourceManager.getSourceName()); - sourceManager.encodeTrack(track, output); - } - - /** - * Decodes an audio track from a byte array. - * @param trackInfo Track info for the track to decode - * @param buffer Byte array containing the encoded track - * @return Decoded audio track - */ - public AudioTrack decodeTrackDetails(AudioTrackInfo trackInfo, byte[] buffer) { - try { - DataInput input = new DataInputStream(new ByteArrayInputStream(buffer)); - return decodeTrackDetails(trackInfo, input); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private AudioTrack decodeTrackDetails(AudioTrackInfo trackInfo, DataInput input) throws IOException { - String sourceName = input.readUTF(); - - for (AudioSourceManager sourceManager : sourceManagers) { - if (sourceName.equals(sourceManager.getSourceName())) { - return sourceManager.decodeTrack(trackInfo, input); - } - } - - return null; - } - - /** - * Executes an audio track with the given player and volume. - * @param listener A listener for track state events - * @param track The audio track to execute - * @param configuration The audio configuration to use for executing - * @param playerOptions Options of the audio player - */ - public void executeTrack(TrackStateListener listener, InternalAudioTrack track, AudioConfiguration configuration, - AudioPlayerOptions playerOptions) { - - final AudioTrackExecutor executor = createExecutorForTrack(track, configuration, playerOptions); - track.assignExecutor(executor, true); - - trackPlaybackExecutorService.execute(() -> executor.execute(listener)); - } - - private AudioTrackExecutor createExecutorForTrack(InternalAudioTrack track, AudioConfiguration configuration, - AudioPlayerOptions playerOptions) { - - AudioTrackExecutor customExecutor = track.createLocalExecutor(this); - if (customExecutor != null) { - return customExecutor; - } else { - int bufferDuration = Optional.ofNullable(playerOptions.frameBufferDuration.get()).orElse(frameBufferDuration); - return new LocalAudioTrackExecutor(track, configuration, playerOptions, useSeekGhosting, bufferDuration); - - } - } - - @Override - public AudioConfiguration getConfiguration() { - return configuration; - } - - @Override - public boolean isUsingSeekGhosting() { - return useSeekGhosting; - } - - @Override - public void setUseSeekGhosting(boolean useSeekGhosting) { - this.useSeekGhosting = useSeekGhosting; - } - - @Override - public int getFrameBufferDuration() { - return frameBufferDuration; - } - - @Override - public void setFrameBufferDuration(int frameBufferDuration) { - this.frameBufferDuration = Math.max(200, frameBufferDuration); - } - - @Override - public void setTrackStuckThreshold(long trackStuckThreshold) { - this.trackStuckThreshold = TimeUnit.MILLISECONDS.toNanos(trackStuckThreshold); - } - - public long getTrackStuckThresholdNanos() { - return trackStuckThreshold; - } - - @Override - public void setPlayerCleanupThreshold(long cleanupThreshold) { - this.cleanupThreshold.set(cleanupThreshold); - } - - @Override - public void setItemLoaderThreadPoolSize(int poolSize) { - trackInfoExecutorService.setMaximumPoolSize(poolSize); - } - - private boolean checkSourcesForItem(AudioReference reference, AudioLoadResultHandler resultHandler, boolean[] reported) { - AudioReference currentReference = reference; - - for (int redirects = 0; redirects < MAXIMUM_LOAD_REDIRECTS && currentReference.identifier != null; redirects++) { - AudioItem item = checkSourcesForItemOnce(currentReference, resultHandler, reported); - if (item == null) { - return false; - } else if (!(item instanceof AudioReference)) { - return true; - } - currentReference = (AudioReference) item; - } - - return false; - } - - private AudioItem checkSourcesForItemOnce(AudioReference reference, AudioLoadResultHandler resultHandler, boolean[] reported) { - for (AudioSourceManager sourceManager : sourceManagers) { - if (reference.containerDescriptor != null && !(sourceManager instanceof ProbingAudioSourceManager)) { - continue; - } - - AudioItem item = sourceManager.loadItem(this, reference); - - if (item != null) { - if (item instanceof AudioTrack) { - log.debug("Loaded a track with identifier {} using {}.", reference.identifier, sourceManager.getClass().getSimpleName()); - reported[0] = true; - resultHandler.trackLoaded((AudioTrack) item); - } else if (item instanceof AudioPlaylist) { - log.debug("Loaded a playlist with identifier {} using {}.", reference.identifier, sourceManager.getClass().getSimpleName()); - reported[0] = true; - resultHandler.playlistLoaded((AudioPlaylist) item); + } + + /** + * Executes an audio track with the given player and volume. + * + * @param listener A listener for track state events + * @param track The audio track to execute + * @param configuration The audio configuration to use for executing + * @param playerOptions Options of the audio player + */ + public void executeTrack(TrackStateListener listener, InternalAudioTrack track, AudioConfiguration configuration, + AudioPlayerOptions playerOptions) { + + final AudioTrackExecutor executor = createExecutorForTrack(track, configuration, playerOptions); + track.assignExecutor(executor, true); + + trackPlaybackExecutorService.execute(() -> executor.execute(listener)); + } + + private AudioTrackExecutor createExecutorForTrack(InternalAudioTrack track, AudioConfiguration configuration, + AudioPlayerOptions playerOptions) { + + AudioTrackExecutor customExecutor = track.createLocalExecutor(this); + if (customExecutor != null) { + return customExecutor; + } else { + int bufferDuration = Optional.ofNullable(playerOptions.frameBufferDuration.get()).orElse(frameBufferDuration); + return new LocalAudioTrackExecutor(track, configuration, playerOptions, useSeekGhosting, bufferDuration); + } - return item; - } } - return null; - } + @Override + public AudioConfiguration getConfiguration() { + return configuration; + } - public ExecutorService getExecutor() { - return trackPlaybackExecutorService; - } + @Override + public boolean isUsingSeekGhosting() { + return useSeekGhosting; + } - @Override - public AudioPlayer createPlayer() { - AudioPlayer player = constructPlayer(); - player.addListener(lifecycleManager); + @Override + public void setUseSeekGhosting(boolean useSeekGhosting) { + this.useSeekGhosting = useSeekGhosting; + } - return player; - } + @Override + public int getFrameBufferDuration() { + return frameBufferDuration; + } - protected AudioPlayer constructPlayer() { - return new DefaultAudioPlayer(this); - } + @Override + public void setFrameBufferDuration(int frameBufferDuration) { + this.frameBufferDuration = Math.max(200, frameBufferDuration); + } - @Override - public void setHttpRequestConfigurator(Function configurator) { - this.httpConfigurator = configurator; + @Override + public void setTrackStuckThreshold(long trackStuckThreshold) { + this.trackStuckThreshold = TimeUnit.MILLISECONDS.toNanos(trackStuckThreshold); + } - if (configurator != null) { - for (AudioSourceManager sourceManager : sourceManagers) { - if (sourceManager instanceof HttpConfigurable) { - ((HttpConfigurable) sourceManager).configureRequests(configurator); + public long getTrackStuckThresholdNanos() { + return trackStuckThreshold; + } + + @Override + public void setPlayerCleanupThreshold(long cleanupThreshold) { + this.cleanupThreshold.set(cleanupThreshold); + } + + @Override + public void setItemLoaderThreadPoolSize(int poolSize) { + trackInfoExecutorService.setMaximumPoolSize(poolSize); + } + + private boolean checkSourcesForItem(AudioReference reference, AudioLoadResultHandler resultHandler, boolean[] reported) { + AudioReference currentReference = reference; + + for (int redirects = 0; redirects < MAXIMUM_LOAD_REDIRECTS && currentReference.identifier != null; redirects++) { + AudioItem item = checkSourcesForItemOnce(currentReference, resultHandler, reported); + if (item == null) { + return false; + } else if (!(item instanceof AudioReference)) { + return true; + } + currentReference = (AudioReference) item; } - } + + return false; } - } - @Override - public void setHttpBuilderConfigurator(Consumer configurator) { - this.httpBuilderConfigurator = configurator; + private AudioItem checkSourcesForItemOnce(AudioReference reference, AudioLoadResultHandler resultHandler, boolean[] reported) { + for (AudioSourceManager sourceManager : sourceManagers) { + if (reference.containerDescriptor != null && !(sourceManager instanceof ProbingAudioSourceManager)) { + continue; + } + + AudioItem item = sourceManager.loadItem(this, reference); + + if (item != null) { + if (item instanceof AudioTrack) { + log.debug("Loaded a track with identifier {} using {}.", reference.identifier, sourceManager.getClass().getSimpleName()); + reported[0] = true; + resultHandler.trackLoaded((AudioTrack) item); + } else if (item instanceof AudioPlaylist) { + log.debug("Loaded a playlist with identifier {} using {}.", reference.identifier, sourceManager.getClass().getSimpleName()); + reported[0] = true; + resultHandler.playlistLoaded((AudioPlaylist) item); + } + return item; + } + } - if (configurator != null) { - for (AudioSourceManager sourceManager : sourceManagers) { - if (sourceManager instanceof HttpConfigurable) { - ((HttpConfigurable) sourceManager).configureBuilder(configurator); + return null; + } + + public ExecutorService getExecutor() { + return trackPlaybackExecutorService; + } + + @Override + public AudioPlayer createPlayer() { + AudioPlayer player = constructPlayer(); + player.addListener(lifecycleManager); + + return player; + } + + protected AudioPlayer constructPlayer() { + return new DefaultAudioPlayer(this); + } + + @Override + public void setHttpRequestConfigurator(Function configurator) { + this.httpConfigurator = configurator; + + if (configurator != null) { + for (AudioSourceManager sourceManager : sourceManagers) { + if (sourceManager instanceof HttpConfigurable) { + ((HttpConfigurable) sourceManager).configureRequests(configurator); + } + } + } + } + + @Override + public void setHttpBuilderConfigurator(Consumer configurator) { + this.httpBuilderConfigurator = configurator; + + if (configurator != null) { + for (AudioSourceManager sourceManager : sourceManagers) { + if (sourceManager instanceof HttpConfigurable) { + ((HttpConfigurable) sourceManager).configureBuilder(configurator); + } + } } - } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/FunctionalResultHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/FunctionalResultHandler.java index b7376997f..94c6d4274 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/FunctionalResultHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/FunctionalResultHandler.java @@ -10,53 +10,53 @@ * Helper class for creating an audio result handler using only methods that can be passed as lambdas. */ public class FunctionalResultHandler implements AudioLoadResultHandler { - private final Consumer trackConsumer; - private final Consumer playlistConsumer; - private final Runnable emptyResultHandler; - private final Consumer exceptionConsumer; - - /** - * Refer to {@link AudioLoadResultHandler} methods for details on when each method is called. - * - * @param trackConsumer Consumer for single track result - * @param playlistConsumer Consumer for playlist result - * @param emptyResultHandler Empty result handler - * @param exceptionConsumer Consumer for an exception when loading the item fails - */ - public FunctionalResultHandler(Consumer trackConsumer, Consumer playlistConsumer, - Runnable emptyResultHandler, Consumer exceptionConsumer) { - - this.trackConsumer = trackConsumer; - this.playlistConsumer = playlistConsumer; - this.emptyResultHandler = emptyResultHandler; - this.exceptionConsumer = exceptionConsumer; - } - - @Override - public void trackLoaded(AudioTrack track) { - if (trackConsumer != null) { - trackConsumer.accept(track); + private final Consumer trackConsumer; + private final Consumer playlistConsumer; + private final Runnable emptyResultHandler; + private final Consumer exceptionConsumer; + + /** + * Refer to {@link AudioLoadResultHandler} methods for details on when each method is called. + * + * @param trackConsumer Consumer for single track result + * @param playlistConsumer Consumer for playlist result + * @param emptyResultHandler Empty result handler + * @param exceptionConsumer Consumer for an exception when loading the item fails + */ + public FunctionalResultHandler(Consumer trackConsumer, Consumer playlistConsumer, + Runnable emptyResultHandler, Consumer exceptionConsumer) { + + this.trackConsumer = trackConsumer; + this.playlistConsumer = playlistConsumer; + this.emptyResultHandler = emptyResultHandler; + this.exceptionConsumer = exceptionConsumer; } - } - @Override - public void playlistLoaded(AudioPlaylist playlist) { - if (playlistConsumer != null) { - playlistConsumer.accept(playlist); + @Override + public void trackLoaded(AudioTrack track) { + if (trackConsumer != null) { + trackConsumer.accept(track); + } } - } - @Override - public void noMatches() { - if (emptyResultHandler != null) { - emptyResultHandler.run(); + @Override + public void playlistLoaded(AudioPlaylist playlist) { + if (playlistConsumer != null) { + playlistConsumer.accept(playlist); + } } - } - @Override - public void loadFailed(FriendlyException exception) { - if (exceptionConsumer != null) { - exceptionConsumer.accept(exception); + @Override + public void noMatches() { + if (emptyResultHandler != null) { + emptyResultHandler.run(); + } + } + + @Override + public void loadFailed(FriendlyException exception) { + if (exceptionConsumer != null) { + exceptionConsumer.accept(exception); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/AudioEvent.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/AudioEvent.java index a01100dd0..32b67152e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/AudioEvent.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/AudioEvent.java @@ -6,15 +6,15 @@ * An event related to an audio player. */ public abstract class AudioEvent { - /** - * The related audio player. - */ - public final AudioPlayer player; + /** + * The related audio player. + */ + public final AudioPlayer player; - /** - * @param player The related audio player. - */ - public AudioEvent(AudioPlayer player) { - this.player = player; - } + /** + * @param player The related audio player. + */ + public AudioEvent(AudioPlayer player) { + this.player = player; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/AudioEventAdapter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/AudioEventAdapter.java index 3f3a5e9a3..4b27abb70 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/AudioEventAdapter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/AudioEventAdapter.java @@ -9,74 +9,74 @@ * Adapter for different event handlers as method overrides */ public abstract class AudioEventAdapter implements AudioEventListener { - /** - * @param player Audio player - */ - public void onPlayerPause(AudioPlayer player) { - // Adapter dummy method - } + /** + * @param player Audio player + */ + public void onPlayerPause(AudioPlayer player) { + // Adapter dummy method + } - /** - * @param player Audio player - */ - public void onPlayerResume(AudioPlayer player) { - // Adapter dummy method - } + /** + * @param player Audio player + */ + public void onPlayerResume(AudioPlayer player) { + // Adapter dummy method + } - /** - * @param player Audio player - * @param track Audio track that started - */ - public void onTrackStart(AudioPlayer player, AudioTrack track) { - // Adapter dummy method - } + /** + * @param player Audio player + * @param track Audio track that started + */ + public void onTrackStart(AudioPlayer player, AudioTrack track) { + // Adapter dummy method + } - /** - * @param player Audio player - * @param track Audio track that ended - * @param endReason The reason why the track stopped playing - */ - public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { - // Adapter dummy method - } + /** + * @param player Audio player + * @param track Audio track that ended + * @param endReason The reason why the track stopped playing + */ + public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { + // Adapter dummy method + } - /** - * @param player Audio player - * @param track Audio track where the exception occurred - * @param exception The exception that occurred - */ - public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception) { - // Adapter dummy method - } + /** + * @param player Audio player + * @param track Audio track where the exception occurred + * @param exception The exception that occurred + */ + public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception) { + // Adapter dummy method + } - /** - * @param player Audio player - * @param track Audio track where the exception occurred - * @param thresholdMs The wait threshold that was exceeded for this event to trigger - */ - public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) { - // Adapter dummy method - } + /** + * @param player Audio player + * @param track Audio track where the exception occurred + * @param thresholdMs The wait threshold that was exceeded for this event to trigger + */ + public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) { + // Adapter dummy method + } - public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs, StackTraceElement[] stackTrace) { - onTrackStuck(player, track, thresholdMs); - } + public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs, StackTraceElement[] stackTrace) { + onTrackStuck(player, track, thresholdMs); + } - @Override - public void onEvent(AudioEvent event) { - if (event instanceof PlayerPauseEvent) { - onPlayerPause(event.player); - } else if (event instanceof PlayerResumeEvent) { - onPlayerResume(event.player); - } else if (event instanceof TrackStartEvent) { - onTrackStart(event.player, ((TrackStartEvent) event).track); - } else if (event instanceof TrackEndEvent) { - onTrackEnd(event.player, ((TrackEndEvent) event).track, ((TrackEndEvent) event).endReason); - } else if (event instanceof TrackExceptionEvent) { - onTrackException(event.player, ((TrackExceptionEvent) event).track, ((TrackExceptionEvent) event).exception); - } else if (event instanceof TrackStuckEvent) { - TrackStuckEvent stuck = (TrackStuckEvent) event; - onTrackStuck(event.player, stuck.track, stuck.thresholdMs, stuck.stackTrace); + @Override + public void onEvent(AudioEvent event) { + if (event instanceof PlayerPauseEvent) { + onPlayerPause(event.player); + } else if (event instanceof PlayerResumeEvent) { + onPlayerResume(event.player); + } else if (event instanceof TrackStartEvent) { + onTrackStart(event.player, ((TrackStartEvent) event).track); + } else if (event instanceof TrackEndEvent) { + onTrackEnd(event.player, ((TrackEndEvent) event).track, ((TrackEndEvent) event).endReason); + } else if (event instanceof TrackExceptionEvent) { + onTrackException(event.player, ((TrackExceptionEvent) event).track, ((TrackExceptionEvent) event).exception); + } else if (event instanceof TrackStuckEvent) { + TrackStuckEvent stuck = (TrackStuckEvent) event; + onTrackStuck(event.player, stuck.track, stuck.thresholdMs, stuck.stackTrace); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/AudioEventListener.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/AudioEventListener.java index 08f4eaace..88f3a4bd7 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/AudioEventListener.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/AudioEventListener.java @@ -4,8 +4,8 @@ * Listener of audio events. */ public interface AudioEventListener { - /** - * @param event The event - */ - void onEvent(AudioEvent event); + /** + * @param event The event + */ + void onEvent(AudioEvent event); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/PlayerPauseEvent.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/PlayerPauseEvent.java index aec398185..2da74bc1f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/PlayerPauseEvent.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/PlayerPauseEvent.java @@ -6,10 +6,10 @@ * Event that is fired when a player is paused. */ public class PlayerPauseEvent extends AudioEvent { - /** - * @param player Audio player - */ - public PlayerPauseEvent(AudioPlayer player) { - super(player); - } + /** + * @param player Audio player + */ + public PlayerPauseEvent(AudioPlayer player) { + super(player); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/PlayerResumeEvent.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/PlayerResumeEvent.java index 5b7e8badf..0b10531f0 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/PlayerResumeEvent.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/PlayerResumeEvent.java @@ -6,10 +6,10 @@ * Event that is fired when a player is resumed. */ public class PlayerResumeEvent extends AudioEvent { - /** - * @param player Audio player - */ - public PlayerResumeEvent(AudioPlayer player) { - super(player); - } + /** + * @param player Audio player + */ + public PlayerResumeEvent(AudioPlayer player) { + super(player); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackEndEvent.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackEndEvent.java index 29b8e6e24..674cd162b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackEndEvent.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackEndEvent.java @@ -8,23 +8,23 @@ * Event that is fired when an audio track ends in an audio player, either by interruption, exception or reaching the end. */ public class TrackEndEvent extends AudioEvent { - /** - * Audio track that ended - */ - public final AudioTrack track; - /** - * The reason why the track stopped playing - */ - public final AudioTrackEndReason endReason; + /** + * Audio track that ended + */ + public final AudioTrack track; + /** + * The reason why the track stopped playing + */ + public final AudioTrackEndReason endReason; - /** - * @param player Audio player - * @param track Audio track that ended - * @param endReason The reason why the track stopped playing - */ - public TrackEndEvent(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { - super(player); - this.track = track; - this.endReason = endReason; - } + /** + * @param player Audio player + * @param track Audio track that ended + * @param endReason The reason why the track stopped playing + */ + public TrackEndEvent(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { + super(player); + this.track = track; + this.endReason = endReason; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackExceptionEvent.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackExceptionEvent.java index 32469b459..a82e85c1c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackExceptionEvent.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackExceptionEvent.java @@ -8,23 +8,23 @@ * Event that is fired when an exception occurs in an audio track that causes it to halt or not start. */ public class TrackExceptionEvent extends AudioEvent { - /** - * Audio track where the exception occurred - */ - public final AudioTrack track; - /** - * The exception that occurred - */ - public final FriendlyException exception; + /** + * Audio track where the exception occurred + */ + public final AudioTrack track; + /** + * The exception that occurred + */ + public final FriendlyException exception; - /** - * @param player Audio player - * @param track Audio track where the exception occurred - * @param exception The exception that occurred - */ - public TrackExceptionEvent(AudioPlayer player, AudioTrack track, FriendlyException exception) { - super(player); - this.track = track; - this.exception = exception; - } + /** + * @param player Audio player + * @param track Audio track where the exception occurred + * @param exception The exception that occurred + */ + public TrackExceptionEvent(AudioPlayer player, AudioTrack track, FriendlyException exception) { + super(player); + this.track = track; + this.exception = exception; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackStartEvent.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackStartEvent.java index e338c82ad..2c8f683d6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackStartEvent.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackStartEvent.java @@ -7,17 +7,17 @@ * Event that is fired when a track starts playing. */ public class TrackStartEvent extends AudioEvent { - /** - * Audio track that started - */ - public final AudioTrack track; + /** + * Audio track that started + */ + public final AudioTrack track; - /** - * @param player Audio player - * @param track Audio track that started - */ - public TrackStartEvent(AudioPlayer player, AudioTrack track) { - super(player); - this.track = track; - } + /** + * @param player Audio player + * @param track Audio track that started + */ + public TrackStartEvent(AudioPlayer player, AudioTrack track) { + super(player); + this.track = track; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackStuckEvent.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackStuckEvent.java index 4799f08cb..1a0dbc61f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackStuckEvent.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/event/TrackStuckEvent.java @@ -8,26 +8,26 @@ * by the threshold set via AudioPlayerManager.setTrackStuckThreshold(). */ public class TrackStuckEvent extends AudioEvent { - /** - * Audio track where the exception occurred - */ - public final AudioTrack track; - /** - * The wait threshold that was exceeded for this event to trigger - */ - public final long thresholdMs; + /** + * Audio track where the exception occurred + */ + public final AudioTrack track; + /** + * The wait threshold that was exceeded for this event to trigger + */ + public final long thresholdMs; - public final StackTraceElement[] stackTrace; + public final StackTraceElement[] stackTrace; - /** - * @param player Audio player - * @param track Audio track where the exception occurred - * @param thresholdMs The wait threshold that was exceeded for this event to trigger - */ - public TrackStuckEvent(AudioPlayer player, AudioTrack track, long thresholdMs, StackTraceElement[] stackTrace) { - super(player); - this.track = track; - this.thresholdMs = thresholdMs; - this.stackTrace = stackTrace; - } + /** + * @param player Audio player + * @param track Audio track where the exception occurred + * @param thresholdMs The wait threshold that was exceeded for this event to trigger + */ + public TrackStuckEvent(AudioPlayer player, AudioTrack track, long thresholdMs, StackTraceElement[] stackTrace) { + super(player); + this.track = track; + this.thresholdMs = thresholdMs; + this.stackTrace = stackTrace; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/hook/AudioOutputHook.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/hook/AudioOutputHook.java index f31b3fe49..4d5201cd9 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/hook/AudioOutputHook.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/hook/AudioOutputHook.java @@ -7,10 +7,10 @@ * Hook for intercepting outgoing audio frames from AudioPlayer. */ public interface AudioOutputHook { - /** - * @param player Audio player where the frame is coming from - * @param frame Audio frame - * @return The frame to pass onto the actual caller - */ - AudioFrame outgoingFrame(AudioPlayer player, AudioFrame frame); + /** + * @param player Audio player where the frame is coming from + * @param frame Audio frame + * @return The frame to pass onto the actual caller + */ + AudioFrame outgoingFrame(AudioPlayer player, AudioFrame frame); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/AudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/AudioSourceManager.java index 1a98279f9..1c26af72b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/AudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/AudioSourceManager.java @@ -14,59 +14,59 @@ * Manager for a source of audio items. */ public interface AudioSourceManager { - /** - * Every source manager implementation should have its unique name as it is used to determine which source manager - * should be able to decode a serialized audio track. - * - * @return The name of this source manager - */ - String getSourceName(); + /** + * Every source manager implementation should have its unique name as it is used to determine which source manager + * should be able to decode a serialized audio track. + * + * @return The name of this source manager + */ + String getSourceName(); - /** - * Returns an audio track for the input string. It should return null if it can immediately detect that there is no - * track for this identifier for this source. If checking that requires more expensive operations, then it should - * return a track instance and check that in InternalAudioTrack#loadTrackInfo. - * - * @param manager The audio manager to attach to the loaded tracks - * @param reference The reference with the identifier which the source manager should find the track with - * @return The loaded item or null on unrecognized identifier - */ - AudioItem loadItem(AudioPlayerManager manager, AudioReference reference); + /** + * Returns an audio track for the input string. It should return null if it can immediately detect that there is no + * track for this identifier for this source. If checking that requires more expensive operations, then it should + * return a track instance and check that in InternalAudioTrack#loadTrackInfo. + * + * @param manager The audio manager to attach to the loaded tracks + * @param reference The reference with the identifier which the source manager should find the track with + * @return The loaded item or null on unrecognized identifier + */ + AudioItem loadItem(AudioPlayerManager manager, AudioReference reference); - /** - * Returns whether the specified track can be encoded. The argument is always a track created by this manager. Being - * encodable also means that it must be possible to play this track on a different node, so it should not depend on - * any resources that are only available on the current system. - * - * @param track The track to check - * @return True if it is encodable - */ - boolean isTrackEncodable(AudioTrack track); + /** + * Returns whether the specified track can be encoded. The argument is always a track created by this manager. Being + * encodable also means that it must be possible to play this track on a different node, so it should not depend on + * any resources that are only available on the current system. + * + * @param track The track to check + * @return True if it is encodable + */ + boolean isTrackEncodable(AudioTrack track); - /** - * Encodes an audio track into the specified output. The contents of AudioTrackInfo do not have to be included since - * they are written to the output already before this call. This will only be called for tracks which were loaded by - * this source manager and for which isEncodable() returns true. - * - * @param track The track to encode - * @param output Output where to write the decoded format to - * @throws IOException On write error. - */ - void encodeTrack(AudioTrack track, DataOutput output) throws IOException; + /** + * Encodes an audio track into the specified output. The contents of AudioTrackInfo do not have to be included since + * they are written to the output already before this call. This will only be called for tracks which were loaded by + * this source manager and for which isEncodable() returns true. + * + * @param track The track to encode + * @param output Output where to write the decoded format to + * @throws IOException On write error. + */ + void encodeTrack(AudioTrack track, DataOutput output) throws IOException; - /** - * Decodes an audio track from the encoded format encoded with encodeTrack(). - * - * @param trackInfo The track info - * @param input The input where to read the bytes of the encoded format - * @return The decoded track - * @throws IOException On read error. - */ - AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException; + /** + * Decodes an audio track from the encoded format encoded with encodeTrack(). + * + * @param trackInfo The track info + * @param input The input where to read the bytes of the encoded format + * @return The decoded track + * @throws IOException On read error. + */ + AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException; - /** - * Shut down the source manager, freeing all associated resources and threads. A source manager is not responsible for - * terminating the tracks that it has created. - */ - void shutdown(); + /** + * Shut down the source manager, freeing all associated resources and threads. A source manager is not responsible for + * terminating the tracks that it has created. + */ + void shutdown(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/AudioSourceManagers.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/AudioSourceManagers.java index 72e8d2801..ae2eea528 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/AudioSourceManagers.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/AudioSourceManagers.java @@ -17,48 +17,48 @@ * A helper class for registering built-in source managers to a player manager. */ public class AudioSourceManagers { - /** - * See {@link #registerRemoteSources(AudioPlayerManager, MediaContainerRegistry)}, but with default containers. - */ - public static void registerRemoteSources(AudioPlayerManager playerManager) { - registerRemoteSources(playerManager, MediaContainerRegistry.DEFAULT_REGISTRY); - } + /** + * See {@link #registerRemoteSources(AudioPlayerManager, MediaContainerRegistry)}, but with default containers. + */ + public static void registerRemoteSources(AudioPlayerManager playerManager) { + registerRemoteSources(playerManager, MediaContainerRegistry.DEFAULT_REGISTRY); + } - /** - * Registers all built-in remote audio sources to the specified player manager. Local file audio source must be - * registered separately. - * - * @param playerManager Player manager to register the source managers to - * @param containerRegistry Media container registry to be used by any probing sources. - */ - public static void registerRemoteSources(AudioPlayerManager playerManager, MediaContainerRegistry containerRegistry) { - playerManager.registerSourceManager(new YoutubeAudioSourceManager(true, null, null)); - playerManager.registerSourceManager(new YandexMusicAudioSourceManager(true)); - playerManager.registerSourceManager(SoundCloudAudioSourceManager.createDefault()); - playerManager.registerSourceManager(new BandcampAudioSourceManager()); - playerManager.registerSourceManager(new VimeoAudioSourceManager()); - playerManager.registerSourceManager(new TwitchStreamAudioSourceManager()); - playerManager.registerSourceManager(new BeamAudioSourceManager()); - playerManager.registerSourceManager(new GetyarnAudioSourceManager()); - playerManager.registerSourceManager(new HttpAudioSourceManager(containerRegistry)); - } + /** + * Registers all built-in remote audio sources to the specified player manager. Local file audio source must be + * registered separately. + * + * @param playerManager Player manager to register the source managers to + * @param containerRegistry Media container registry to be used by any probing sources. + */ + public static void registerRemoteSources(AudioPlayerManager playerManager, MediaContainerRegistry containerRegistry) { + playerManager.registerSourceManager(new YoutubeAudioSourceManager(true, null, null)); + playerManager.registerSourceManager(new YandexMusicAudioSourceManager(true)); + playerManager.registerSourceManager(SoundCloudAudioSourceManager.createDefault()); + playerManager.registerSourceManager(new BandcampAudioSourceManager()); + playerManager.registerSourceManager(new VimeoAudioSourceManager()); + playerManager.registerSourceManager(new TwitchStreamAudioSourceManager()); + playerManager.registerSourceManager(new BeamAudioSourceManager()); + playerManager.registerSourceManager(new GetyarnAudioSourceManager()); + playerManager.registerSourceManager(new HttpAudioSourceManager(containerRegistry)); + } - /** - * Registers the local file source manager to the specified player manager. - * - * @param playerManager Player manager to register the source manager to - */ - public static void registerLocalSource(AudioPlayerManager playerManager) { - registerLocalSource(playerManager, MediaContainerRegistry.DEFAULT_REGISTRY); - } + /** + * Registers the local file source manager to the specified player manager. + * + * @param playerManager Player manager to register the source manager to + */ + public static void registerLocalSource(AudioPlayerManager playerManager) { + registerLocalSource(playerManager, MediaContainerRegistry.DEFAULT_REGISTRY); + } - /** - * Registers the local file source manager to the specified player manager. - * - * @param playerManager Player manager to register the source manager to - * @param containerRegistry Media container registry to be used by the local source. - */ - public static void registerLocalSource(AudioPlayerManager playerManager, MediaContainerRegistry containerRegistry) { - playerManager.registerSourceManager(new LocalAudioSourceManager(containerRegistry)); - } + /** + * Registers the local file source manager to the specified player manager. + * + * @param playerManager Player manager to register the source manager to + * @param containerRegistry Media container registry to be used by the local source. + */ + public static void registerLocalSource(AudioPlayerManager playerManager, MediaContainerRegistry containerRegistry) { + playerManager.registerSourceManager(new LocalAudioSourceManager(containerRegistry)); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/ProbingAudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/ProbingAudioSourceManager.java index 43ce63891..27d34bd03 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/ProbingAudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/ProbingAudioSourceManager.java @@ -1,9 +1,9 @@ package com.sedmelluq.discord.lavaplayer.source; +import com.sedmelluq.discord.lavaplayer.container.MediaContainerDescriptor; import com.sedmelluq.discord.lavaplayer.container.MediaContainerDetectionResult; import com.sedmelluq.discord.lavaplayer.container.MediaContainerProbe; import com.sedmelluq.discord.lavaplayer.container.MediaContainerRegistry; -import com.sedmelluq.discord.lavaplayer.container.MediaContainerDescriptor; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioItem; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; @@ -19,51 +19,51 @@ * The base class for audio sources which use probing to detect container type. */ public abstract class ProbingAudioSourceManager implements AudioSourceManager { - private static final char PARAMETERS_SEPARATOR = '|'; + private static final char PARAMETERS_SEPARATOR = '|'; + + protected final MediaContainerRegistry containerRegistry; - protected final MediaContainerRegistry containerRegistry; + protected ProbingAudioSourceManager(MediaContainerRegistry containerRegistry) { + this.containerRegistry = containerRegistry; + } - protected ProbingAudioSourceManager(MediaContainerRegistry containerRegistry) { - this.containerRegistry = containerRegistry; - } + protected AudioItem handleLoadResult(MediaContainerDetectionResult result) { + if (result != null) { + if (result.isReference()) { + return result.getReference(); + } else if (!result.isContainerDetected()) { + throw new FriendlyException("Unknown file format.", COMMON, null); + } else if (!result.isSupportedFile()) { + throw new FriendlyException(result.getUnsupportedReason(), COMMON, null); + } else { + return createTrack(result.getTrackInfo(), result.getContainerDescriptor()); + } + } - protected AudioItem handleLoadResult(MediaContainerDetectionResult result) { - if (result != null) { - if (result.isReference()) { - return result.getReference(); - } else if (!result.isContainerDetected()) { - throw new FriendlyException("Unknown file format.", COMMON, null); - } else if (!result.isSupportedFile()) { - throw new FriendlyException(result.getUnsupportedReason(), COMMON, null); - } else { - return createTrack(result.getTrackInfo(), result.getContainerDescriptor()); - } + return null; } - return null; - } + protected abstract AudioTrack createTrack(AudioTrackInfo trackInfo, MediaContainerDescriptor containerTrackFactory); - protected abstract AudioTrack createTrack(AudioTrackInfo trackInfo, MediaContainerDescriptor containerTrackFactory); + protected void encodeTrackFactory(MediaContainerDescriptor factory, DataOutput output) throws IOException { + String probeInfo = factory.probe.getName() + (factory.parameters != null ? PARAMETERS_SEPARATOR + + factory.parameters : ""); - protected void encodeTrackFactory(MediaContainerDescriptor factory, DataOutput output) throws IOException { - String probeInfo = factory.probe.getName() + (factory.parameters != null ? PARAMETERS_SEPARATOR + - factory.parameters : ""); + output.writeUTF(probeInfo); + } - output.writeUTF(probeInfo); - } + protected MediaContainerDescriptor decodeTrackFactory(DataInput input) throws IOException { + String probeInfo = input.readUTF(); + int separatorPosition = probeInfo.indexOf(PARAMETERS_SEPARATOR); - protected MediaContainerDescriptor decodeTrackFactory(DataInput input) throws IOException { - String probeInfo = input.readUTF(); - int separatorPosition = probeInfo.indexOf(PARAMETERS_SEPARATOR); + String probeName = separatorPosition < 0 ? probeInfo : probeInfo.substring(0, separatorPosition); + String parameters = separatorPosition < 0 ? null : probeInfo.substring(separatorPosition + 1); - String probeName = separatorPosition < 0 ? probeInfo : probeInfo.substring(0, separatorPosition); - String parameters = separatorPosition < 0 ? null : probeInfo.substring(separatorPosition + 1); + MediaContainerProbe probe = containerRegistry.find(probeName); + if (probe != null) { + return new MediaContainerDescriptor(probe, parameters); + } - MediaContainerProbe probe = containerRegistry.find(probeName); - if (probe != null) { - return new MediaContainerDescriptor(probe, parameters); + return null; } - - return null; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/bandcamp/BandcampAudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/bandcamp/BandcampAudioSourceManager.java index 542b0ccd5..9e84ed121 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/bandcamp/BandcampAudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/bandcamp/BandcampAudioSourceManager.java @@ -23,7 +23,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; @@ -37,204 +36,204 @@ * Audio source manager that implements finding Bandcamp tracks based on URL. */ public class BandcampAudioSourceManager implements AudioSourceManager, HttpConfigurable { - private static final String URL_REGEX = "^(https?://(?:[^.]+\\.|)bandcamp\\.com)/(track|album)/([a-zA-Z0-9-_]+)/?(?:\\?.*|)$"; - private static final Pattern urlRegex = Pattern.compile(URL_REGEX); + private static final String URL_REGEX = "^(https?://(?:[^.]+\\.|)bandcamp\\.com)/(track|album)/([a-zA-Z0-9-_]+)/?(?:\\?.*|)$"; + private static final Pattern urlRegex = Pattern.compile(URL_REGEX); - private static final String ARTWORK_URL_FORMAT = "https://f4.bcbits.com/img/a%s_1.png"; + private static final String ARTWORK_URL_FORMAT = "https://f4.bcbits.com/img/a%s_1.png"; - private final HttpInterfaceManager httpInterfaceManager; + private final HttpInterfaceManager httpInterfaceManager; - /** - * Create an instance. - */ - public BandcampAudioSourceManager() { - httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - } + /** + * Create an instance. + */ + public BandcampAudioSourceManager() { + httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + } + + @Override + public String getSourceName() { + return "bandcamp"; + } - @Override - public String getSourceName() { - return "bandcamp"; - } + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + UrlInfo urlInfo = parseUrl(reference.identifier); - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - UrlInfo urlInfo = parseUrl(reference.identifier); + if (urlInfo != null) { + if (urlInfo.isAlbum) { + return loadAlbum(urlInfo); + } else { + return loadTrack(urlInfo); + } + } - if (urlInfo != null) { - if (urlInfo.isAlbum) { - return loadAlbum(urlInfo); - } else { - return loadTrack(urlInfo); - } + return null; } - return null; - } + private UrlInfo parseUrl(String url) { + Matcher matcher = urlRegex.matcher(url); - private UrlInfo parseUrl(String url) { - Matcher matcher = urlRegex.matcher(url); + if (matcher.matches()) { + return new UrlInfo(url, matcher.group(1), "album".equals(matcher.group(2))); + } else { + return null; + } + } - if (matcher.matches()) { - return new UrlInfo(url, matcher.group(1), "album".equals(matcher.group(2))); - } else { - return null; + private AudioItem loadTrack(UrlInfo urlInfo) { + return extractFromPage(urlInfo.fullUrl, (httpClient, text) -> { + JsonBrowser trackListInfo = readTrackListInformation(text); + String artist = trackListInfo.get("artist").safeText(); + String artworkUrl = extractArtwork(trackListInfo); + + return extractTrack(trackListInfo.get("trackinfo").index(0), urlInfo.baseUrl, artist, artworkUrl); + }); } - } - private AudioItem loadTrack(UrlInfo urlInfo) { - return extractFromPage(urlInfo.fullUrl, (httpClient, text) -> { - JsonBrowser trackListInfo = readTrackListInformation(text); - String artist = trackListInfo.get("artist").safeText(); - String artworkUrl = extractArtwork(trackListInfo); + private AudioItem loadAlbum(UrlInfo urlInfo) { + return extractFromPage(urlInfo.fullUrl, (httpClient, text) -> { + JsonBrowser trackListInfo = readTrackListInformation(text); + String artist = trackListInfo.get("artist").text(); + String artworkUrl = extractArtwork(trackListInfo); + + List tracks = new ArrayList<>(); + for (JsonBrowser trackInfo : trackListInfo.get("trackinfo").values()) { + tracks.add(extractTrack(trackInfo, urlInfo.baseUrl, artist, artworkUrl)); + } - return extractTrack(trackListInfo.get("trackinfo").index(0), urlInfo.baseUrl, artist, artworkUrl); - }); - } + JsonBrowser albumInfo = readAlbumInformation(text); + return new BasicAudioPlaylist(albumInfo.get("current").get("title").text(), tracks, null, false); + }); + } + + private AudioTrack extractTrack(JsonBrowser trackInfo, String bandUrl, String artist, String artworkUrl) { + String trackPageUrl = bandUrl + trackInfo.get("title_link").text(); + + return new BandcampAudioTrack(new AudioTrackInfo( + trackInfo.get("title").text(), + artist, + (long) (trackInfo.get("duration").as(Double.class) * 1000.0), + bandUrl + trackInfo.get("title_link").text(), + false, + trackPageUrl, + artworkUrl, + null + ), this); + } - private AudioItem loadAlbum(UrlInfo urlInfo) { - return extractFromPage(urlInfo.fullUrl, (httpClient, text) -> { - JsonBrowser trackListInfo = readTrackListInformation(text); - String artist = trackListInfo.get("artist").text(); - String artworkUrl = extractArtwork(trackListInfo); + private JsonBrowser readAlbumInformation(String text) throws IOException { + String albumInfoJson = DataFormatTools.extractBetween(text, "data-tralbum=\"", "\""); - List tracks = new ArrayList<>(); - for (JsonBrowser trackInfo : trackListInfo.get("trackinfo").values()) { - tracks.add(extractTrack(trackInfo, urlInfo.baseUrl, artist, artworkUrl)); - } + if (albumInfoJson == null) { + throw new FriendlyException("Album information not found on the Bandcamp page.", SUSPICIOUS, null); + } - JsonBrowser albumInfo = readAlbumInformation(text); - return new BasicAudioPlaylist(albumInfo.get("current").get("title").text(), tracks, null, false); - }); - } + albumInfoJson = albumInfoJson.replace(""", "\""); + return JsonBrowser.parse(albumInfoJson); + } - private AudioTrack extractTrack(JsonBrowser trackInfo, String bandUrl, String artist, String artworkUrl) { - String trackPageUrl = bandUrl + trackInfo.get("title_link").text(); + JsonBrowser readTrackListInformation(String text) throws IOException { + String trackInfoJson = DataFormatTools.extractBetween(text, "data-tralbum=\"", "\""); - return new BandcampAudioTrack(new AudioTrackInfo( - trackInfo.get("title").text(), - artist, - (long) (trackInfo.get("duration").as(Double.class) * 1000.0), - bandUrl + trackInfo.get("title_link").text(), - false, - trackPageUrl, - artworkUrl, - null - ), this); - } + if (trackInfoJson == null) { + throw new FriendlyException("Track information not found on the Bandcamp page.", SUSPICIOUS, null); + } - private JsonBrowser readAlbumInformation(String text) throws IOException { - String albumInfoJson = DataFormatTools.extractBetween(text, "data-tralbum=\"", "\""); + trackInfoJson = trackInfoJson.replace(""", "\""); + return JsonBrowser.parse(trackInfoJson); + } - if (albumInfoJson == null) { - throw new FriendlyException("Album information not found on the Bandcamp page.", SUSPICIOUS, null); + private AudioItem extractFromPage(String url, AudioItemExtractor extractor) { + try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { + return extractFromPageWithInterface(httpInterface, url, extractor); + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions("Loading information for a Bandcamp track failed.", FAULT, e); + } } - albumInfoJson = albumInfoJson.replace(""", "\""); - return JsonBrowser.parse(albumInfoJson); - } + private AudioItem extractFromPageWithInterface(HttpInterface httpInterface, String url, AudioItemExtractor extractor) throws Exception { + String responseText; - JsonBrowser readTrackListInformation(String text) throws IOException { - String trackInfoJson = DataFormatTools.extractBetween(text, "data-tralbum=\"", "\""); + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(url))) { + int statusCode = response.getStatusLine().getStatusCode(); - if (trackInfoJson == null) { - throw new FriendlyException("Track information not found on the Bandcamp page.", SUSPICIOUS, null); + if (statusCode == HttpStatus.SC_NOT_FOUND) { + return new AudioReference(null, null); + } else if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new IOException("Invalid status code for track page: " + statusCode); + } + + responseText = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + } + + return extractor.extract(httpInterface, responseText); } - trackInfoJson = trackInfoJson.replace(""", "\""); - return JsonBrowser.parse(trackInfoJson); - } + private String extractArtwork(JsonBrowser root) { + String artId = root.get("art_id").text(); + if (artId != null) { + if (artId.length() < 10) { + StringBuilder builder = new StringBuilder(artId); + while (builder.length() < 10) { + builder.insert(0, "0"); + } + artId = builder.toString(); + } + return String.format(ARTWORK_URL_FORMAT, artId); + } + return null; + } - private AudioItem extractFromPage(String url, AudioItemExtractor extractor) { - try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { - return extractFromPageWithInterface(httpInterface, url, extractor); - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions("Loading information for a Bandcamp track failed.", FAULT, e); + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; } - } - private AudioItem extractFromPageWithInterface(HttpInterface httpInterface, String url, AudioItemExtractor extractor) throws Exception { - String responseText; + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + // No special values to encode + } - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(url))) { - int statusCode = response.getStatusLine().getStatusCode(); + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new BandcampAudioTrack(trackInfo, this); + } - if (statusCode == HttpStatus.SC_NOT_FOUND) { - return new AudioReference(null, null); - } else if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new IOException("Invalid status code for track page: " + statusCode); - } + @Override + public void shutdown() { + ExceptionTools.closeWithWarnings(httpInterfaceManager); + } - responseText = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + /** + * @return Get an HTTP interface for a playing track. + */ + public HttpInterface getHttpInterface() { + return httpInterfaceManager.getInterface(); } - return extractor.extract(httpInterface, responseText); - } + @Override + public void configureRequests(Function configurator) { + httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + httpInterfaceManager.configureBuilder(configurator); + } - private String extractArtwork(JsonBrowser root) { - String artId = root.get("art_id").text(); - if (artId != null) { - if (artId.length() < 10) { - StringBuilder builder = new StringBuilder(artId); - while (builder.length() < 10) { - builder.insert(0, "0"); + private interface AudioItemExtractor { + AudioItem extract(HttpInterface httpInterface, String text) throws Exception; + } + + private static class UrlInfo { + public final String fullUrl; + public final String baseUrl; + public final boolean isAlbum; + + private UrlInfo(String fullUrl, String baseUrl, boolean isAlbum) { + this.fullUrl = fullUrl; + this.baseUrl = baseUrl; + this.isAlbum = isAlbum; } - artId = builder.toString(); - } - return String.format(ARTWORK_URL_FORMAT, artId); - } - return null; - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - // No special values to encode - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new BandcampAudioTrack(trackInfo, this); - } - - @Override - public void shutdown() { - ExceptionTools.closeWithWarnings(httpInterfaceManager); - } - - /** - * @return Get an HTTP interface for a playing track. - */ - public HttpInterface getHttpInterface() { - return httpInterfaceManager.getInterface(); - } - - @Override - public void configureRequests(Function configurator) { - httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - httpInterfaceManager.configureBuilder(configurator); - } - - private interface AudioItemExtractor { - AudioItem extract(HttpInterface httpInterface, String text) throws Exception; - } - - private static class UrlInfo { - public final String fullUrl; - public final String baseUrl; - public final boolean isAlbum; - - private UrlInfo(String fullUrl, String baseUrl, boolean isAlbum) { - this.fullUrl = fullUrl; - this.baseUrl = baseUrl; - this.isAlbum = isAlbum; - } - } -} \ No newline at end of file + } +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/bandcamp/BandcampAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/bandcamp/BandcampAudioTrack.java index 49452adeb..24f30c03d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/bandcamp/BandcampAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/bandcamp/BandcampAudioTrack.java @@ -24,55 +24,55 @@ * Audio track that handles processing Bandcamp tracks. */ public class BandcampAudioTrack extends DelegatedAudioTrack { - private static final Logger log = LoggerFactory.getLogger(BandcampAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(BandcampAudioTrack.class); - private final BandcampAudioSourceManager sourceManager; + private final BandcampAudioSourceManager sourceManager; - /** - * @param trackInfo Track info - * @param sourceManager Source manager which was used to find this track - */ - public BandcampAudioTrack(AudioTrackInfo trackInfo, BandcampAudioSourceManager sourceManager) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param sourceManager Source manager which was used to find this track + */ + public BandcampAudioTrack(AudioTrackInfo trackInfo, BandcampAudioSourceManager sourceManager) { + super(trackInfo); - this.sourceManager = sourceManager; - } + this.sourceManager = sourceManager; + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { - log.debug("Loading Bandcamp track page from URL: {}", trackInfo.identifier); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { + log.debug("Loading Bandcamp track page from URL: {}", trackInfo.identifier); - String trackMediaUrl = getTrackMediaUrl(httpInterface); - log.debug("Starting Bandcamp track from URL: {}", trackMediaUrl); + String trackMediaUrl = getTrackMediaUrl(httpInterface); + log.debug("Starting Bandcamp track from URL: {}", trackMediaUrl); - try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(trackMediaUrl), null)) { - processDelegate(new Mp3AudioTrack(trackInfo, stream), localExecutor); - } + try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(trackMediaUrl), null)) { + processDelegate(new Mp3AudioTrack(trackInfo, stream), localExecutor); + } + } } - } - private String getTrackMediaUrl(HttpInterface httpInterface) throws IOException { - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackInfo.identifier))) { - int statusCode = response.getStatusLine().getStatusCode(); - if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new IOException("Invalid status code for track page: " + statusCode); - } + private String getTrackMediaUrl(HttpInterface httpInterface) throws IOException { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackInfo.identifier))) { + int statusCode = response.getStatusLine().getStatusCode(); + if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new IOException("Invalid status code for track page: " + statusCode); + } - String responseText = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - JsonBrowser trackInfo = sourceManager.readTrackListInformation(responseText); + String responseText = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + JsonBrowser trackInfo = sourceManager.readTrackListInformation(responseText); - return trackInfo.get("trackinfo").index(0).get("file").get("mp3-128").text(); + return trackInfo.get("trackinfo").index(0).get("file").get("mp3-128").text(); + } } - } - @Override - protected AudioTrack makeShallowClone() { - return new BandcampAudioTrack(trackInfo, sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new BandcampAudioTrack(trackInfo, sourceManager); + } - @Override - public AudioSourceManager getSourceManager() { - return sourceManager; - } + @Override + public AudioSourceManager getSourceManager() { + return sourceManager; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/beam/BeamAudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/beam/BeamAudioSourceManager.java index 568e979e6..598dbf34f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/beam/BeamAudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/beam/BeamAudioSourceManager.java @@ -20,7 +20,6 @@ import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; -import java.util.Collections; import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Matcher; @@ -32,116 +31,116 @@ * Audio source manager which detects Beam.pro tracks by URL. */ public class BeamAudioSourceManager implements AudioSourceManager, HttpConfigurable { - private static final String STREAM_NAME_REGEX = "^https://(?:www\\.)?(?:beam\\.pro|mixer\\.com)/([^/]+)$"; - private static final Pattern streamNameRegex = Pattern.compile(STREAM_NAME_REGEX); - - private final HttpInterfaceManager httpInterfaceManager; - - /** - * Create an instance. - */ - public BeamAudioSourceManager() { - this.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - } - - @Override - public String getSourceName() { - return "beam.pro"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - String streamName = getChannelNameFromUrl(reference.identifier); - if (streamName == null) { - return null; + private static final String STREAM_NAME_REGEX = "^https://(?:www\\.)?(?:beam\\.pro|mixer\\.com)/([^/]+)$"; + private static final Pattern streamNameRegex = Pattern.compile(STREAM_NAME_REGEX); + + private final HttpInterfaceManager httpInterfaceManager; + + /** + * Create an instance. + */ + public BeamAudioSourceManager() { + this.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + } + + @Override + public String getSourceName() { + return "beam.pro"; + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + String streamName = getChannelNameFromUrl(reference.identifier); + if (streamName == null) { + return null; + } + + JsonBrowser channelInfo = fetchStreamChannelInfo(streamName); + + if (channelInfo == null) { + return AudioReference.NO_TRACK; + } else { + String displayName = channelInfo.get("name").text(); + String id = getPlayedStreamId(channelInfo); + String thumbnailUrl = channelInfo.get("thumbnail").get("url").text(); + + if (displayName == null || id == null) { + throw new IllegalStateException("Expected id and name fields from Beam channel info."); + } + + return new BeamAudioTrack(new AudioTrackInfo( + displayName, + streamName, + Units.DURATION_MS_UNKNOWN, + id + "|" + streamName + "|" + reference.identifier, + true, + "https://beam.pro/" + streamName, + thumbnailUrl, + null + ), this); + } } - JsonBrowser channelInfo = fetchStreamChannelInfo(streamName); - - if (channelInfo == null) { - return AudioReference.NO_TRACK; - } else { - String displayName = channelInfo.get("name").text(); - String id = getPlayedStreamId(channelInfo); - String thumbnailUrl = channelInfo.get("thumbnail").get("url").text(); - - if (displayName == null || id == null) { - throw new IllegalStateException("Expected id and name fields from Beam channel info."); - } - - return new BeamAudioTrack(new AudioTrackInfo( - displayName, - streamName, - Units.DURATION_MS_UNKNOWN, - id + "|" + streamName + "|" + reference.identifier, - true, - "https://beam.pro/" + streamName, - thumbnailUrl, - null - ), this); + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; } - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - // Nothing special to do, URL (identifier) is enough - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new BeamAudioTrack(trackInfo, this); - } - - @Override - public void shutdown() { - // Nothing to shut down. - } - - private static String getPlayedStreamId(JsonBrowser channelInfo) { - // If there is a hostee, this means that the current channel itself is not actually broadcasting anything and all - // further requests should be performed with the ID of the hostee. Hostee is not rechecked later so it will keep - // playing the current hostee even if it changes. - String hosteeId = channelInfo.get("hosteeId").text(); - return hosteeId != null ? hosteeId : channelInfo.get("id").text(); - } - - private static String getChannelNameFromUrl(String url) { - Matcher matcher = streamNameRegex.matcher(url); - if (!matcher.matches()) { - return null; + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + // Nothing special to do, URL (identifier) is enough + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new BeamAudioTrack(trackInfo, this); + } + + @Override + public void shutdown() { + // Nothing to shut down. } - return matcher.group(1); - } + private static String getPlayedStreamId(JsonBrowser channelInfo) { + // If there is a hostee, this means that the current channel itself is not actually broadcasting anything and all + // further requests should be performed with the ID of the hostee. Hostee is not rechecked later so it will keep + // playing the current hostee even if it changes. + String hosteeId = channelInfo.get("hosteeId").text(); + return hosteeId != null ? hosteeId : channelInfo.get("id").text(); + } + + private static String getChannelNameFromUrl(String url) { + Matcher matcher = streamNameRegex.matcher(url); + if (!matcher.matches()) { + return null; + } + + return matcher.group(1); + } + + private JsonBrowser fetchStreamChannelInfo(String name) { + try (HttpInterface httpInterface = getHttpInterface()) { + return HttpClientTools.fetchResponseAsJson(httpInterface, + new HttpGet("https://mixer.com/api/v1/channels/" + name + "?noCount=1")); + } catch (IOException e) { + throw new FriendlyException("Loading Beam channel information failed.", SUSPICIOUS, e); + } + } + + /** + * @return Get an HTTP interface for a playing track. + */ + public HttpInterface getHttpInterface() { + return httpInterfaceManager.getInterface(); + } + + @Override + public void configureRequests(Function configurator) { + httpInterfaceManager.configureRequests(configurator); + } - private JsonBrowser fetchStreamChannelInfo(String name) { - try (HttpInterface httpInterface = getHttpInterface()) { - return HttpClientTools.fetchResponseAsJson(httpInterface, - new HttpGet("https://mixer.com/api/v1/channels/" + name + "?noCount=1")); - } catch (IOException e) { - throw new FriendlyException("Loading Beam channel information failed.", SUSPICIOUS, e); + @Override + public void configureBuilder(Consumer configurator) { + httpInterfaceManager.configureBuilder(configurator); } - } - - /** - * @return Get an HTTP interface for a playing track. - */ - public HttpInterface getHttpInterface() { - return httpInterfaceManager.getInterface(); - } - - @Override - public void configureRequests(Function configurator) { - httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - httpInterfaceManager.configureBuilder(configurator); - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/beam/BeamAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/beam/BeamAudioTrack.java index 84e8049f1..d354d0a64 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/beam/BeamAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/beam/BeamAudioTrack.java @@ -1,7 +1,6 @@ package com.sedmelluq.discord.lavaplayer.source.beam; import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; -import com.sedmelluq.discord.lavaplayer.source.stream.M3uStreamAudioTrack; import com.sedmelluq.discord.lavaplayer.source.stream.M3uStreamSegmentUrlProvider; import com.sedmelluq.discord.lavaplayer.source.stream.MpegTsM3uStreamAudioTrack; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; @@ -15,55 +14,55 @@ * Audio track that handles processing Beam.pro tracks. */ public class BeamAudioTrack extends MpegTsM3uStreamAudioTrack { - private static final Logger log = LoggerFactory.getLogger(BeamAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(BeamAudioTrack.class); - private final BeamAudioSourceManager sourceManager; - private final M3uStreamSegmentUrlProvider segmentUrlProvider; + private final BeamAudioSourceManager sourceManager; + private final M3uStreamSegmentUrlProvider segmentUrlProvider; - /** - * @param trackInfo Track info - * @param sourceManager Source manager which was used to find this track - */ - public BeamAudioTrack(AudioTrackInfo trackInfo, BeamAudioSourceManager sourceManager) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param sourceManager Source manager which was used to find this track + */ + public BeamAudioTrack(AudioTrackInfo trackInfo, BeamAudioSourceManager sourceManager) { + super(trackInfo); - this.sourceManager = sourceManager; - this.segmentUrlProvider = new BeamSegmentUrlProvider(getChannelId()); - } + this.sourceManager = sourceManager; + this.segmentUrlProvider = new BeamSegmentUrlProvider(getChannelId()); + } - @Override - protected M3uStreamSegmentUrlProvider getSegmentUrlProvider() { - return segmentUrlProvider; - } + @Override + protected M3uStreamSegmentUrlProvider getSegmentUrlProvider() { + return segmentUrlProvider; + } - @Override - protected HttpInterface getHttpInterface() { - return sourceManager.getHttpInterface(); - } + @Override + protected HttpInterface getHttpInterface() { + return sourceManager.getHttpInterface(); + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - log.debug("Starting to play Beam channel {}.", getChannelUrl()); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + log.debug("Starting to play Beam channel {}.", getChannelUrl()); - super.process(localExecutor); - } + super.process(localExecutor); + } - @Override - protected AudioTrack makeShallowClone() { - return new BeamAudioTrack(trackInfo, sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new BeamAudioTrack(trackInfo, sourceManager); + } - @Override - public AudioSourceManager getSourceManager() { - return sourceManager; - } + @Override + public AudioSourceManager getSourceManager() { + return sourceManager; + } - private String getChannelId() { - return trackInfo.identifier.substring(0, trackInfo.identifier.indexOf('|')); - } + private String getChannelId() { + return trackInfo.identifier.substring(0, trackInfo.identifier.indexOf('|')); + } - private String getChannelUrl() { - return trackInfo.identifier.substring(trackInfo.identifier.lastIndexOf('|') + 1); - } + private String getChannelUrl() { + return trackInfo.identifier.substring(trackInfo.identifier.lastIndexOf('|') + 1); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/beam/BeamSegmentUrlProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/beam/BeamSegmentUrlProvider.java index c29b87110..e3a6a6206 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/beam/BeamSegmentUrlProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/beam/BeamSegmentUrlProvider.java @@ -19,53 +19,53 @@ * Provider for Beam segment URLs from a channel. */ public class BeamSegmentUrlProvider extends M3uStreamSegmentUrlProvider { - private static final Logger log = LoggerFactory.getLogger(BeamSegmentUrlProvider.class); + private static final Logger log = LoggerFactory.getLogger(BeamSegmentUrlProvider.class); - private final String channelId; - private String streamSegmentPlaylistUrl; + private final String channelId; + private String streamSegmentPlaylistUrl; - /** - * @param channelId Channel ID number. - */ - public BeamSegmentUrlProvider(String channelId) { - this.channelId = channelId; - } - - @Override - protected String getQualityFromM3uDirective(ExtendedM3uParser.Line directiveLine) { - return directiveLine.directiveArguments.get("NAME"); - } + /** + * @param channelId Channel ID number. + */ + public BeamSegmentUrlProvider(String channelId) { + this.channelId = channelId; + } - @Override - protected String fetchSegmentPlaylistUrl(HttpInterface httpInterface) throws IOException { - if (streamSegmentPlaylistUrl != null) { - return streamSegmentPlaylistUrl; + @Override + protected String getQualityFromM3uDirective(ExtendedM3uParser.Line directiveLine) { + return directiveLine.directiveArguments.get("NAME"); } - HttpUriRequest jsonRequest = new HttpGet("https://mixer.com/api/v1/channels/" + channelId + "/manifest.light2"); - JsonBrowser lightManifest = HttpClientTools.fetchResponseAsJson(httpInterface, jsonRequest); + @Override + protected String fetchSegmentPlaylistUrl(HttpInterface httpInterface) throws IOException { + if (streamSegmentPlaylistUrl != null) { + return streamSegmentPlaylistUrl; + } - if (lightManifest == null) { - throw new IllegalStateException("Did not find light manifest at " + jsonRequest.getURI()); - } + HttpUriRequest jsonRequest = new HttpGet("https://mixer.com/api/v1/channels/" + channelId + "/manifest.light2"); + JsonBrowser lightManifest = HttpClientTools.fetchResponseAsJson(httpInterface, jsonRequest); - HttpUriRequest manifestRequest = new HttpGet("https://mixer.com" + lightManifest.get("hlsSrc").text()); - List streams = loadChannelStreamsList(fetchResponseLines(httpInterface, manifestRequest, - "mixer channel streams list")); + if (lightManifest == null) { + throw new IllegalStateException("Did not find light manifest at " + jsonRequest.getURI()); + } - if (streams.isEmpty()) { - throw new IllegalStateException("No streams available on channel."); - } + HttpUriRequest manifestRequest = new HttpGet("https://mixer.com" + lightManifest.get("hlsSrc").text()); + List streams = loadChannelStreamsList(fetchResponseLines(httpInterface, manifestRequest, + "mixer channel streams list")); - ChannelStreamInfo stream = streams.get(0); + if (streams.isEmpty()) { + throw new IllegalStateException("No streams available on channel."); + } - log.debug("Chose stream with quality {} from url {}", stream.quality, stream.url); - streamSegmentPlaylistUrl = stream.url; - return streamSegmentPlaylistUrl; - } + ChannelStreamInfo stream = streams.get(0); - @Override - protected HttpUriRequest createSegmentGetRequest(String url) { - return new HttpGet(url); - } + log.debug("Chose stream with quality {} from url {}", stream.quality, stream.url); + streamSegmentPlaylistUrl = stream.url; + return streamSegmentPlaylistUrl; + } + + @Override + protected HttpUriRequest createSegmentGetRequest(String url) { + return new HttpGet(url); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/getyarn/GetyarnAudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/getyarn/GetyarnAudioSourceManager.java index b513e7161..9dd7cd3b6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/getyarn/GetyarnAudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/getyarn/GetyarnAudioSourceManager.java @@ -3,16 +3,20 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager; -import com.sedmelluq.discord.lavaplayer.tools.io.ThreadLocalHttpInterfaceManager; +import com.sedmelluq.discord.lavaplayer.tools.io.*; import com.sedmelluq.discord.lavaplayer.track.AudioItem; import com.sedmelluq.discord.lavaplayer.track.AudioReference; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; import com.sedmelluq.discord.lavaplayer.track.info.AudioTrackInfoBuilder; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.HttpClientBuilder; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; @@ -21,13 +25,6 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.io.IOUtils; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.HttpClientBuilder; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS; @@ -35,89 +32,89 @@ * Audio source manager which detects getyarn.io tracks by URL. */ public class GetyarnAudioSourceManager implements HttpConfigurable, AudioSourceManager { - private static final Pattern GETYARN_REGEX = Pattern.compile("(?:http://|https://(?:www\\.)?)?getyarn\\.io/yarn-clip/(.*)"); - - private final HttpInterfaceManager httpInterfaceManager; - - public GetyarnAudioSourceManager() { - httpInterfaceManager = new ThreadLocalHttpInterfaceManager( - HttpClientTools - .createSharedCookiesHttpBuilder() - .setRedirectStrategy(new HttpClientTools.NoRedirectsStrategy()), - HttpClientTools.DEFAULT_REQUEST_CONFIG - ); - } - - @Override - public String getSourceName() { - return "getyarn.io"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - final Matcher m = GETYARN_REGEX.matcher(reference.identifier); - - if (!m.matches()) { - return null; + private static final Pattern GETYARN_REGEX = Pattern.compile("(?:http://|https://(?:www\\.)?)?getyarn\\.io/yarn-clip/(.*)"); + + private final HttpInterfaceManager httpInterfaceManager; + + public GetyarnAudioSourceManager() { + httpInterfaceManager = new ThreadLocalHttpInterfaceManager( + HttpClientTools + .createSharedCookiesHttpBuilder() + .setRedirectStrategy(new HttpClientTools.NoRedirectsStrategy()), + HttpClientTools.DEFAULT_REQUEST_CONFIG + ); + } + + @Override + public String getSourceName() { + return "getyarn.io"; + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + final Matcher m = GETYARN_REGEX.matcher(reference.identifier); + + if (!m.matches()) { + return null; + } + + return extractVideoUrlFromPage(reference); + } + + private AudioTrack createTrack(AudioTrackInfo trackInfo) { + return new GetyarnAudioTrack(trackInfo, this); + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) { + // No custom values that need saving + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { + return new GetyarnAudioTrack(trackInfo, this); + } + + @Override + public void shutdown() { + // Nothing to shut down + } + + public HttpInterface getHttpInterface() { + return httpInterfaceManager.getInterface(); + } + + @Override + public void configureRequests(Function configurator) { + httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + httpInterfaceManager.configureBuilder(configurator); } - return extractVideoUrlFromPage(reference); - } - - private AudioTrack createTrack(AudioTrackInfo trackInfo) { - return new GetyarnAudioTrack(trackInfo, this); - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) { - // No custom values that need saving - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { - return new GetyarnAudioTrack(trackInfo, this); - } - - @Override - public void shutdown() { - // Nothing to shut down - } - - public HttpInterface getHttpInterface() { - return httpInterfaceManager.getInterface(); - } - - @Override - public void configureRequests(Function configurator) { - httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - httpInterfaceManager.configureBuilder(configurator); - } - - private AudioTrack extractVideoUrlFromPage(AudioReference reference) { - try (final CloseableHttpResponse response = getHttpInterface().execute(new HttpGet(reference.identifier))) { - final String html = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - final Document document = Jsoup.parse(html); - - final AudioTrackInfo trackInfo = AudioTrackInfoBuilder.empty() - .setUri(reference.identifier) - .setAuthor("Unknown") - .setIsStream(false) - .setIdentifier(document.selectFirst("meta[property=og:video:secure_url]").attr("content")) - .setTitle(document.selectFirst("meta[property=og:title]").attr("content")) - .build(); - - return createTrack(trackInfo); - } catch (IOException e) { - throw new FriendlyException("Failed to load info for yarn clip", SUSPICIOUS, null); + private AudioTrack extractVideoUrlFromPage(AudioReference reference) { + try (final CloseableHttpResponse response = getHttpInterface().execute(new HttpGet(reference.identifier))) { + final String html = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + final Document document = Jsoup.parse(html); + + final AudioTrackInfo trackInfo = AudioTrackInfoBuilder.empty() + .setUri(reference.identifier) + .setAuthor("Unknown") + .setIsStream(false) + .setIdentifier(document.selectFirst("meta[property=og:video:secure_url]").attr("content")) + .setTitle(document.selectFirst("meta[property=og:title]").attr("content")) + .build(); + + return createTrack(trackInfo); + } catch (IOException e) { + throw new FriendlyException("Failed to load info for yarn clip", SUSPICIOUS, null); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/getyarn/GetyarnAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/getyarn/GetyarnAudioTrack.java index 24aee0a59..8837acd68 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/getyarn/GetyarnAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/getyarn/GetyarnAudioTrack.java @@ -8,37 +8,38 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; import com.sedmelluq.discord.lavaplayer.track.DelegatedAudioTrack; import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor; -import java.net.URI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.URI; + public class GetyarnAudioTrack extends DelegatedAudioTrack { - private static final Logger log = LoggerFactory.getLogger(DelegatedAudioTrack.class); - private final GetyarnAudioSourceManager sourceManager; - - public GetyarnAudioTrack(AudioTrackInfo trackInfo, GetyarnAudioSourceManager sourceManager) { - super(trackInfo); - - this.sourceManager = sourceManager; - } - - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { - log.debug("Starting getyarn.io track from URL: {}", trackInfo.identifier); - - try (PersistentHttpStream inputStream = new PersistentHttpStream( - httpInterface, - new URI(trackInfo.identifier), - Units.CONTENT_LENGTH_UNKNOWN - )) { - processDelegate(new MpegAudioTrack(trackInfo, inputStream), localExecutor); - } + private static final Logger log = LoggerFactory.getLogger(DelegatedAudioTrack.class); + private final GetyarnAudioSourceManager sourceManager; + + public GetyarnAudioTrack(AudioTrackInfo trackInfo, GetyarnAudioSourceManager sourceManager) { + super(trackInfo); + + this.sourceManager = sourceManager; } - } - @Override - protected AudioTrack makeShallowClone() { - return new GetyarnAudioTrack(trackInfo, sourceManager); - } + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { + log.debug("Starting getyarn.io track from URL: {}", trackInfo.identifier); + + try (PersistentHttpStream inputStream = new PersistentHttpStream( + httpInterface, + new URI(trackInfo.identifier), + Units.CONTENT_LENGTH_UNKNOWN + )) { + processDelegate(new MpegAudioTrack(trackInfo, inputStream), localExecutor); + } + } + } + + @Override + protected AudioTrack makeShallowClone() { + return new GetyarnAudioTrack(trackInfo, sourceManager); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/http/HttpAudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/http/HttpAudioSourceManager.java index 744dbacee..58e8baafb 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/http/HttpAudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/http/HttpAudioSourceManager.java @@ -1,20 +1,11 @@ package com.sedmelluq.discord.lavaplayer.source.http; -import com.sedmelluq.discord.lavaplayer.container.MediaContainerDetection; -import com.sedmelluq.discord.lavaplayer.container.MediaContainerDetectionResult; -import com.sedmelluq.discord.lavaplayer.container.MediaContainerHints; -import com.sedmelluq.discord.lavaplayer.container.MediaContainerDescriptor; -import com.sedmelluq.discord.lavaplayer.container.MediaContainerRegistry; +import com.sedmelluq.discord.lavaplayer.container.*; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.source.ProbingAudioSourceManager; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.tools.Units; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager; -import com.sedmelluq.discord.lavaplayer.tools.io.PersistentHttpStream; -import com.sedmelluq.discord.lavaplayer.tools.io.ThreadLocalHttpInterfaceManager; +import com.sedmelluq.discord.lavaplayer.tools.io.*; import com.sedmelluq.discord.lavaplayer.track.AudioItem; import com.sedmelluq.discord.lavaplayer.track.AudioReference; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; @@ -41,134 +32,134 @@ * Audio source manager which implements finding audio files from HTTP addresses. */ public class HttpAudioSourceManager extends ProbingAudioSourceManager implements HttpConfigurable { - private final HttpInterfaceManager httpInterfaceManager; - - /** - * Create a new instance with default media container registry. - */ - public HttpAudioSourceManager() { - this(MediaContainerRegistry.DEFAULT_REGISTRY); - } - - /** - * Create a new instance. - */ - public HttpAudioSourceManager(MediaContainerRegistry containerRegistry) { - super(containerRegistry); - - httpInterfaceManager = new ThreadLocalHttpInterfaceManager( - HttpClientTools - .createSharedCookiesHttpBuilder() - .setRedirectStrategy(new HttpClientTools.NoRedirectsStrategy()), - HttpClientTools.DEFAULT_REQUEST_CONFIG - ); - } - - @Override - public String getSourceName() { - return "http"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - AudioReference httpReference = getAsHttpReference(reference); - if (httpReference == null) { - return null; + private final HttpInterfaceManager httpInterfaceManager; + + /** + * Create a new instance with default media container registry. + */ + public HttpAudioSourceManager() { + this(MediaContainerRegistry.DEFAULT_REGISTRY); } - if (httpReference.containerDescriptor != null) { - return createTrack(AudioTrackInfoBuilder.create(reference, null).build(), httpReference.containerDescriptor); - } else { - return handleLoadResult(detectContainer(httpReference)); + /** + * Create a new instance. + */ + public HttpAudioSourceManager(MediaContainerRegistry containerRegistry) { + super(containerRegistry); + + httpInterfaceManager = new ThreadLocalHttpInterfaceManager( + HttpClientTools + .createSharedCookiesHttpBuilder() + .setRedirectStrategy(new HttpClientTools.NoRedirectsStrategy()), + HttpClientTools.DEFAULT_REQUEST_CONFIG + ); } - } - - @Override - protected AudioTrack createTrack(AudioTrackInfo trackInfo, MediaContainerDescriptor containerDescriptor) { - return new HttpAudioTrack(trackInfo, containerDescriptor, this); - } - - /** - * @return Get an HTTP interface for a playing track. - */ - public HttpInterface getHttpInterface() { - return httpInterfaceManager.getInterface(); - } - - @Override - public void configureRequests(Function configurator) { - httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - httpInterfaceManager.configureBuilder(configurator); - } - - public static AudioReference getAsHttpReference(AudioReference reference) { - if (reference.identifier.startsWith("https://") || reference.identifier.startsWith("http://")) { - return reference; - } else if (reference.identifier.startsWith("icy://")) { - return new AudioReference("http://" + reference.identifier.substring(6), reference.title); + + @Override + public String getSourceName() { + return "http"; } - return null; - } - private MediaContainerDetectionResult detectContainer(AudioReference reference) { - MediaContainerDetectionResult result; + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + AudioReference httpReference = getAsHttpReference(reference); + if (httpReference == null) { + return null; + } + + if (httpReference.containerDescriptor != null) { + return createTrack(AudioTrackInfoBuilder.create(reference, null).build(), httpReference.containerDescriptor); + } else { + return handleLoadResult(detectContainer(httpReference)); + } + } - try (HttpInterface httpInterface = getHttpInterface()) { - result = detectContainerWithClient(httpInterface, reference); - } catch (IOException e) { - throw new FriendlyException("Connecting to the URL failed.", SUSPICIOUS, e); + @Override + protected AudioTrack createTrack(AudioTrackInfo trackInfo, MediaContainerDescriptor containerDescriptor) { + return new HttpAudioTrack(trackInfo, containerDescriptor, this); } - return result; - } + /** + * @return Get an HTTP interface for a playing track. + */ + public HttpInterface getHttpInterface() { + return httpInterfaceManager.getInterface(); + } - private MediaContainerDetectionResult detectContainerWithClient(HttpInterface httpInterface, AudioReference reference) throws IOException { - try (PersistentHttpStream inputStream = new PersistentHttpStream(httpInterface, new URI(reference.identifier), Units.CONTENT_LENGTH_UNKNOWN)) { - int statusCode = inputStream.checkStatusCode(); - String redirectUrl = HttpClientTools.getRedirectLocation(reference.identifier, inputStream.getCurrentResponse()); + @Override + public void configureRequests(Function configurator) { + httpInterfaceManager.configureRequests(configurator); + } - if (redirectUrl != null) { - return refer(null, new AudioReference(redirectUrl, null)); - } else if (statusCode == HttpStatus.SC_NOT_FOUND) { + @Override + public void configureBuilder(Consumer configurator) { + httpInterfaceManager.configureBuilder(configurator); + } + + public static AudioReference getAsHttpReference(AudioReference reference) { + if (reference.identifier.startsWith("https://") || reference.identifier.startsWith("http://")) { + return reference; + } else if (reference.identifier.startsWith("icy://")) { + return new AudioReference("http://" + reference.identifier.substring(6), reference.title); + } return null; - } else if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new FriendlyException("That URL is not playable.", COMMON, new IllegalStateException("Status code " + statusCode)); - } - - MediaContainerHints hints = MediaContainerHints.from(getHeaderValue(inputStream.getCurrentResponse(), "Content-Type"), null); - return new MediaContainerDetection(containerRegistry, reference, inputStream, hints).detectContainer(); - } catch (URISyntaxException e) { - throw new FriendlyException("Not a valid URL.", COMMON, e); } - } - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } + private MediaContainerDetectionResult detectContainer(AudioReference reference) { + MediaContainerDetectionResult result; - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - encodeTrackFactory(((HttpAudioTrack) track).getContainerTrackFactory(), output); - } + try (HttpInterface httpInterface = getHttpInterface()) { + result = detectContainerWithClient(httpInterface, reference); + } catch (IOException e) { + throw new FriendlyException("Connecting to the URL failed.", SUSPICIOUS, e); + } - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - MediaContainerDescriptor containerTrackFactory = decodeTrackFactory(input); + return result; + } - if (containerTrackFactory != null) { - return new HttpAudioTrack(trackInfo, containerTrackFactory, this); + private MediaContainerDetectionResult detectContainerWithClient(HttpInterface httpInterface, AudioReference reference) throws IOException { + try (PersistentHttpStream inputStream = new PersistentHttpStream(httpInterface, new URI(reference.identifier), Units.CONTENT_LENGTH_UNKNOWN)) { + int statusCode = inputStream.checkStatusCode(); + String redirectUrl = HttpClientTools.getRedirectLocation(reference.identifier, inputStream.getCurrentResponse()); + + if (redirectUrl != null) { + return refer(null, new AudioReference(redirectUrl, null)); + } else if (statusCode == HttpStatus.SC_NOT_FOUND) { + return null; + } else if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new FriendlyException("That URL is not playable.", COMMON, new IllegalStateException("Status code " + statusCode)); + } + + MediaContainerHints hints = MediaContainerHints.from(getHeaderValue(inputStream.getCurrentResponse(), "Content-Type"), null); + return new MediaContainerDetection(containerRegistry, reference, inputStream, hints).detectContainer(); + } catch (URISyntaxException e) { + throw new FriendlyException("Not a valid URL.", COMMON, e); + } } - return null; - } + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } - @Override - public void shutdown() { - // Nothing to shut down - } + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + encodeTrackFactory(((HttpAudioTrack) track).getContainerTrackFactory(), output); + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + MediaContainerDescriptor containerTrackFactory = decodeTrackFactory(input); + + if (containerTrackFactory != null) { + return new HttpAudioTrack(trackInfo, containerTrackFactory, this); + } + + return null; + } + + @Override + public void shutdown() { + // Nothing to shut down + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/http/HttpAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/http/HttpAudioTrack.java index b256c24f9..fbe9b794b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/http/HttpAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/http/HttpAudioTrack.java @@ -19,50 +19,50 @@ * Audio track that handles processing HTTP addresses as audio tracks. */ public class HttpAudioTrack extends DelegatedAudioTrack { - private static final Logger log = LoggerFactory.getLogger(HttpAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(HttpAudioTrack.class); - private final MediaContainerDescriptor containerTrackFactory; - private final HttpAudioSourceManager sourceManager; + private final MediaContainerDescriptor containerTrackFactory; + private final HttpAudioSourceManager sourceManager; - /** - * @param trackInfo Track info - * @param containerTrackFactory Container track factory - contains the probe with its parameters. - * @param sourceManager Source manager used to load this track - */ - public HttpAudioTrack(AudioTrackInfo trackInfo, MediaContainerDescriptor containerTrackFactory, - HttpAudioSourceManager sourceManager) { + /** + * @param trackInfo Track info + * @param containerTrackFactory Container track factory - contains the probe with its parameters. + * @param sourceManager Source manager used to load this track + */ + public HttpAudioTrack(AudioTrackInfo trackInfo, MediaContainerDescriptor containerTrackFactory, + HttpAudioSourceManager sourceManager) { - super(trackInfo); + super(trackInfo); - this.containerTrackFactory = containerTrackFactory; - this.sourceManager = sourceManager; - } + this.containerTrackFactory = containerTrackFactory; + this.sourceManager = sourceManager; + } - /** - * @return The media probe which handles creating a container-specific delegated track for this track. - */ - public MediaContainerDescriptor getContainerTrackFactory() { - return containerTrackFactory; - } + /** + * @return The media probe which handles creating a container-specific delegated track for this track. + */ + public MediaContainerDescriptor getContainerTrackFactory() { + return containerTrackFactory; + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { - log.debug("Starting http track from URL: {}", trackInfo.identifier); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { + log.debug("Starting http track from URL: {}", trackInfo.identifier); - try (PersistentHttpStream inputStream = new PersistentHttpStream(httpInterface, new URI(trackInfo.identifier), Units.CONTENT_LENGTH_UNKNOWN)) { - processDelegate((InternalAudioTrack) containerTrackFactory.createTrack(trackInfo, inputStream), localExecutor); - } + try (PersistentHttpStream inputStream = new PersistentHttpStream(httpInterface, new URI(trackInfo.identifier), Units.CONTENT_LENGTH_UNKNOWN)) { + processDelegate((InternalAudioTrack) containerTrackFactory.createTrack(trackInfo, inputStream), localExecutor); + } + } } - } - @Override - protected AudioTrack makeShallowClone() { - return new HttpAudioTrack(trackInfo, containerTrackFactory, sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new HttpAudioTrack(trackInfo, containerTrackFactory, sourceManager); + } - @Override - public AudioSourceManager getSourceManager() { - return sourceManager; - } + @Override + public AudioSourceManager getSourceManager() { + return sourceManager; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/local/LocalAudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/local/LocalAudioSourceManager.java index 6aa2651ae..aee1796bd 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/local/LocalAudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/local/LocalAudioSourceManager.java @@ -1,10 +1,6 @@ package com.sedmelluq.discord.lavaplayer.source.local; -import com.sedmelluq.discord.lavaplayer.container.MediaContainerDetection; -import com.sedmelluq.discord.lavaplayer.container.MediaContainerDetectionResult; -import com.sedmelluq.discord.lavaplayer.container.MediaContainerHints; -import com.sedmelluq.discord.lavaplayer.container.MediaContainerRegistry; -import com.sedmelluq.discord.lavaplayer.container.MediaContainerDescriptor; +import com.sedmelluq.discord.lavaplayer.container.*; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.source.ProbingAudioSourceManager; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; @@ -24,70 +20,70 @@ * Audio source manager that implements finding audio files from the local file system. */ public class LocalAudioSourceManager extends ProbingAudioSourceManager { - public LocalAudioSourceManager() { - this(MediaContainerRegistry.DEFAULT_REGISTRY); - } - - public LocalAudioSourceManager(MediaContainerRegistry containerRegistry) { - super(containerRegistry); - } - - @Override - public String getSourceName() { - return "local"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - File file = new File(reference.identifier); - - if (file.exists() && file.isFile() && file.canRead()) { - return handleLoadResult(detectContainerForFile(reference, file)); - } else { - return null; + public LocalAudioSourceManager() { + this(MediaContainerRegistry.DEFAULT_REGISTRY); } - } - - @Override - protected AudioTrack createTrack(AudioTrackInfo trackInfo, MediaContainerDescriptor containerTrackFactory) { - return new LocalAudioTrack(trackInfo, containerTrackFactory, this); - } - - private MediaContainerDetectionResult detectContainerForFile(AudioReference reference, File file) { - try (LocalSeekableInputStream inputStream = new LocalSeekableInputStream(file)) { - int lastDotIndex = file.getName().lastIndexOf('.'); - String fileExtension = lastDotIndex >= 0 ? file.getName().substring(lastDotIndex + 1) : null; - - return new MediaContainerDetection(containerRegistry, reference, inputStream, - MediaContainerHints.from(null, fileExtension)).detectContainer(); - } catch (IOException e) { - throw new FriendlyException("Failed to open file for reading.", SUSPICIOUS, e); + + public LocalAudioSourceManager(MediaContainerRegistry containerRegistry) { + super(containerRegistry); + } + + @Override + public String getSourceName() { + return "local"; + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + File file = new File(reference.identifier); + + if (file.exists() && file.isFile() && file.canRead()) { + return handleLoadResult(detectContainerForFile(reference, file)); + } else { + return null; + } + } + + @Override + protected AudioTrack createTrack(AudioTrackInfo trackInfo, MediaContainerDescriptor containerTrackFactory) { + return new LocalAudioTrack(trackInfo, containerTrackFactory, this); } - } - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } + private MediaContainerDetectionResult detectContainerForFile(AudioReference reference, File file) { + try (LocalSeekableInputStream inputStream = new LocalSeekableInputStream(file)) { + int lastDotIndex = file.getName().lastIndexOf('.'); + String fileExtension = lastDotIndex >= 0 ? file.getName().substring(lastDotIndex + 1) : null; - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - encodeTrackFactory(((LocalAudioTrack) track).getContainerTrackFactory(), output); - } + return new MediaContainerDetection(containerRegistry, reference, inputStream, + MediaContainerHints.from(null, fileExtension)).detectContainer(); + } catch (IOException e) { + throw new FriendlyException("Failed to open file for reading.", SUSPICIOUS, e); + } + } - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - MediaContainerDescriptor containerTrackFactory = decodeTrackFactory(input); + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } - if (containerTrackFactory != null) { - return new LocalAudioTrack(trackInfo, containerTrackFactory, this); + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + encodeTrackFactory(((LocalAudioTrack) track).getContainerTrackFactory(), output); } - return null; - } + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + MediaContainerDescriptor containerTrackFactory = decodeTrackFactory(input); + + if (containerTrackFactory != null) { + return new LocalAudioTrack(trackInfo, containerTrackFactory, this); + } - @Override - public void shutdown() { - // Nothing to shut down - } -} \ No newline at end of file + return null; + } + + @Override + public void shutdown() { + // Nothing to shut down + } +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/local/LocalAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/local/LocalAudioTrack.java index e9244ca21..4072d408c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/local/LocalAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/local/LocalAudioTrack.java @@ -14,46 +14,46 @@ * Audio track that handles processing local files as audio tracks. */ public class LocalAudioTrack extends DelegatedAudioTrack { - private final File file; - private final MediaContainerDescriptor containerTrackFactory; - private final LocalAudioSourceManager sourceManager; - - /** - * @param trackInfo Track info - * @param containerTrackFactory Probe track factory - contains the probe with its parameters. - * @param sourceManager Source manager used to load this track - */ - public LocalAudioTrack(AudioTrackInfo trackInfo, MediaContainerDescriptor containerTrackFactory, - LocalAudioSourceManager sourceManager) { - - super(trackInfo); - - this.file = new File(trackInfo.identifier); - this.containerTrackFactory = containerTrackFactory; - this.sourceManager = sourceManager; - } - - /** - * @return The media probe which handles creating a container-specific delegated track for this track. - */ - public MediaContainerDescriptor getContainerTrackFactory() { - return containerTrackFactory; - } - - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - try (LocalSeekableInputStream inputStream = new LocalSeekableInputStream(file)) { - processDelegate((InternalAudioTrack) containerTrackFactory.createTrack(trackInfo, inputStream), localExecutor); + private final File file; + private final MediaContainerDescriptor containerTrackFactory; + private final LocalAudioSourceManager sourceManager; + + /** + * @param trackInfo Track info + * @param containerTrackFactory Probe track factory - contains the probe with its parameters. + * @param sourceManager Source manager used to load this track + */ + public LocalAudioTrack(AudioTrackInfo trackInfo, MediaContainerDescriptor containerTrackFactory, + LocalAudioSourceManager sourceManager) { + + super(trackInfo); + + this.file = new File(trackInfo.identifier); + this.containerTrackFactory = containerTrackFactory; + this.sourceManager = sourceManager; } - } - - @Override - protected AudioTrack makeShallowClone() { - return new LocalAudioTrack(trackInfo, containerTrackFactory, sourceManager); - } - - @Override - public AudioSourceManager getSourceManager() { - return sourceManager; - } -} \ No newline at end of file + + /** + * @return The media probe which handles creating a container-specific delegated track for this track. + */ + public MediaContainerDescriptor getContainerTrackFactory() { + return containerTrackFactory; + } + + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + try (LocalSeekableInputStream inputStream = new LocalSeekableInputStream(file)) { + processDelegate((InternalAudioTrack) containerTrackFactory.createTrack(trackInfo, inputStream), localExecutor); + } + } + + @Override + protected AudioTrack makeShallowClone() { + return new LocalAudioTrack(trackInfo, containerTrackFactory, sourceManager); + } + + @Override + public AudioSourceManager getSourceManager() { + return sourceManager; + } +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/local/LocalSeekableInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/local/LocalSeekableInputStream.java index 239a56246..0b8b325ec 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/local/LocalSeekableInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/local/LocalSeekableInputStream.java @@ -18,95 +18,95 @@ * Seekable input stream implementation for local files. */ public class LocalSeekableInputStream extends SeekableInputStream { - private static final Logger log = LoggerFactory.getLogger(LocalSeekableInputStream.class); - - private final FileInputStream inputStream; - private final FileChannel channel; - private final ExtendedBufferedInputStream bufferedStream; - private long position; - - /** - * @param file File to create a stream for. - */ - public LocalSeekableInputStream(File file) { - super(file.length(), 0); - - try { - inputStream = new FileInputStream(file); - bufferedStream = new ExtendedBufferedInputStream(inputStream); - channel = inputStream.getChannel(); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); + private static final Logger log = LoggerFactory.getLogger(LocalSeekableInputStream.class); + + private final FileInputStream inputStream; + private final FileChannel channel; + private final ExtendedBufferedInputStream bufferedStream; + private long position; + + /** + * @param file File to create a stream for. + */ + public LocalSeekableInputStream(File file) { + super(file.length(), 0); + + try { + inputStream = new FileInputStream(file); + bufferedStream = new ExtendedBufferedInputStream(inputStream); + channel = inputStream.getChannel(); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } } - } - @Override - public int read() throws IOException { - int result = bufferedStream.read(); - if (result >= 0) { - position++; + @Override + public int read() throws IOException { + int result = bufferedStream.read(); + if (result >= 0) { + position++; + } + + return result; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int read = bufferedStream.read(b, off, len); + position += read; + return read; + } + + @Override + public long skip(long n) throws IOException { + long skipped = bufferedStream.skip(n); + position += skipped; + return skipped; + } + + @Override + public int available() throws IOException { + return bufferedStream.available(); + } + + @Override + public synchronized void reset() throws IOException { + throw new IOException("mark/reset not supported"); + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public void close() throws IOException { + try { + channel.close(); + } catch (IOException e) { + log.debug("Failed to close channel", e); + } + } + + @Override + public long getPosition() { + return position; + } + + @Override + public boolean canSeekHard() { + return true; + } + + @Override + public List getTrackInfoProviders() { + return Collections.emptyList(); } - return result; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - int read = bufferedStream.read(b, off, len); - position += read; - return read; - } - - @Override - public long skip(long n) throws IOException { - long skipped = bufferedStream.skip(n); - position += skipped; - return skipped; - } - - @Override - public int available() throws IOException { - return bufferedStream.available(); - } - - @Override - public synchronized void reset() throws IOException { - throw new IOException("mark/reset not supported"); - } - - @Override - public boolean markSupported() { - return false; - } - - @Override - public void close() throws IOException { - try { - channel.close(); - } catch (IOException e) { - log.debug("Failed to close channel", e); + @Override + protected void seekHard(long position) throws IOException { + channel.position(position); + this.position = position; + bufferedStream.discardBuffer(); } - } - - @Override - public long getPosition() { - return position; - } - - @Override - public boolean canSeekHard() { - return true; - } - - @Override - public List getTrackInfoProviders() { - return Collections.emptyList(); - } - - @Override - protected void seekHard(long position) throws IOException { - channel.position(position); - this.position = position; - bufferedStream.discardBuffer(); - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/nico/NicoAudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/nico/NicoAudioSourceManager.java index c8ea96b51..8f9dc9abc 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/nico/NicoAudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/nico/NicoAudioSourceManager.java @@ -4,10 +4,10 @@ import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.track.AudioItem; import com.sedmelluq.discord.lavaplayer.track.AudioReference; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; @@ -30,7 +30,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; -import java.util.Collections; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Function; @@ -44,154 +43,154 @@ * Audio source manager that implements finding NicoNico tracks based on URL. */ public class NicoAudioSourceManager implements AudioSourceManager, HttpConfigurable { - private static final String TRACK_URL_REGEX = "^(?:http://|https://|)(?:www\\.|)nicovideo\\.jp/watch/(sm[0-9]+)(?:\\?.*|)$"; - - private static final Pattern trackUrlPattern = Pattern.compile(TRACK_URL_REGEX); - - private final String email; - private final String password; - private final HttpInterfaceManager httpInterfaceManager; - private final AtomicBoolean loggedIn; - - /** - * @param email Site account email - * @param password Site account password - */ - public NicoAudioSourceManager(String email, String password) { - this.email = email; - this.password = password; - httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - loggedIn = new AtomicBoolean(); - } - - @Override - public String getSourceName() { - return "niconico"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - Matcher trackMatcher = trackUrlPattern.matcher(reference.identifier); - - if (trackMatcher.matches()) { - return loadTrack(trackMatcher.group(1)); + private static final String TRACK_URL_REGEX = "^(?:http://|https://|)(?:www\\.|)nicovideo\\.jp/watch/(sm[0-9]+)(?:\\?.*|)$"; + + private static final Pattern trackUrlPattern = Pattern.compile(TRACK_URL_REGEX); + + private final String email; + private final String password; + private final HttpInterfaceManager httpInterfaceManager; + private final AtomicBoolean loggedIn; + + /** + * @param email Site account email + * @param password Site account password + */ + public NicoAudioSourceManager(String email, String password) { + this.email = email; + this.password = password; + httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + loggedIn = new AtomicBoolean(); } - return null; - } + @Override + public String getSourceName() { + return "niconico"; + } - private AudioTrack loadTrack(String videoId) { - checkLoggedIn(); + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + Matcher trackMatcher = trackUrlPattern.matcher(reference.identifier); - try (HttpInterface httpInterface = getHttpInterface()) { - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet("http://ext.nicovideo.jp/api/getthumbinfo/" + videoId))) { - int statusCode = response.getStatusLine().getStatusCode(); - if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new IOException("Unexpected response code from video info: " + statusCode); + if (trackMatcher.matches()) { + return loadTrack(trackMatcher.group(1)); } - Document document = Jsoup.parse(response.getEntity().getContent(), StandardCharsets.UTF_8.name(), "", Parser.xmlParser()); - return extractTrackFromXml(videoId, document); - } - } catch (IOException e) { - throw new FriendlyException("Error occurred when extracting video info.", SUSPICIOUS, e); + return null; } - } - - private AudioTrack extractTrackFromXml(String videoId, Document document) { - for (Element element : document.select(":root > thumb")) { - String uploader = element.select("user_nickname").first().text(); - String title = element.select("title").first().text(); - String thumbnailUrl = element.select("thumbnail_url").first().text(); - long duration = DataFormatTools.durationTextToMillis(element.select("length").first().text()); - - return new NicoAudioTrack(new AudioTrackInfo(title, - uploader, - duration, - videoId, - false, - getWatchUrl(videoId), - thumbnailUrl, - null - ), this); + + private AudioTrack loadTrack(String videoId) { + checkLoggedIn(); + + try (HttpInterface httpInterface = getHttpInterface()) { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet("http://ext.nicovideo.jp/api/getthumbinfo/" + videoId))) { + int statusCode = response.getStatusLine().getStatusCode(); + if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new IOException("Unexpected response code from video info: " + statusCode); + } + + Document document = Jsoup.parse(response.getEntity().getContent(), StandardCharsets.UTF_8.name(), "", Parser.xmlParser()); + return extractTrackFromXml(videoId, document); + } + } catch (IOException e) { + throw new FriendlyException("Error occurred when extracting video info.", SUSPICIOUS, e); + } } - return null; - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - // No extra information to save - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new NicoAudioTrack(trackInfo, this); - } - - @Override - public void shutdown() { - // Nothing to shut down - } - - /** - * @return Get an HTTP interface for a playing track. - */ - public HttpInterface getHttpInterface() { - return httpInterfaceManager.getInterface(); - } - - @Override - public void configureRequests(Function configurator) { - httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - httpInterfaceManager.configureBuilder(configurator); - } - - void checkLoggedIn() { - synchronized (loggedIn) { - if (loggedIn.get()) { - return; - } - - HttpPost loginRequest = new HttpPost("https://secure.nicovideo.jp/secure/login"); - - loginRequest.setEntity(new UrlEncodedFormEntity(Arrays.asList( - new BasicNameValuePair("mail", email), - new BasicNameValuePair("password", password) - ), StandardCharsets.UTF_8)); - - try (HttpInterface httpInterface = getHttpInterface()) { - try (CloseableHttpResponse response = httpInterface.execute(loginRequest)) { - int statusCode = response.getStatusLine().getStatusCode(); - - if (statusCode != 302) { - throw new IOException("Unexpected response code " + statusCode); - } - - Header location = response.getFirstHeader("Location"); - - if (location == null || location.getValue().contains("message=")) { - throw new FriendlyException("Login details for NicoNico are invalid.", COMMON, null); - } - - loggedIn.set(true); + private AudioTrack extractTrackFromXml(String videoId, Document document) { + for (Element element : document.select(":root > thumb")) { + String uploader = element.select("user_nickname").first().text(); + String title = element.select("title").first().text(); + String thumbnailUrl = element.select("thumbnail_url").first().text(); + long duration = DataFormatTools.durationTextToMillis(element.select("length").first().text()); + + return new NicoAudioTrack(new AudioTrackInfo(title, + uploader, + duration, + videoId, + false, + getWatchUrl(videoId), + thumbnailUrl, + null + ), this); } - } catch (IOException e) { - throw new FriendlyException("Exception when trying to log into NicoNico", SUSPICIOUS, e); - } + + return null; + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + // No extra information to save + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new NicoAudioTrack(trackInfo, this); + } + + @Override + public void shutdown() { + // Nothing to shut down + } + + /** + * @return Get an HTTP interface for a playing track. + */ + public HttpInterface getHttpInterface() { + return httpInterfaceManager.getInterface(); } - } - private static String getWatchUrl(String videoId) { - return "http://www.nicovideo.jp/watch/" + videoId; - } + @Override + public void configureRequests(Function configurator) { + httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + httpInterfaceManager.configureBuilder(configurator); + } + + void checkLoggedIn() { + synchronized (loggedIn) { + if (loggedIn.get()) { + return; + } + + HttpPost loginRequest = new HttpPost("https://secure.nicovideo.jp/secure/login"); + + loginRequest.setEntity(new UrlEncodedFormEntity(Arrays.asList( + new BasicNameValuePair("mail", email), + new BasicNameValuePair("password", password) + ), StandardCharsets.UTF_8)); + + try (HttpInterface httpInterface = getHttpInterface()) { + try (CloseableHttpResponse response = httpInterface.execute(loginRequest)) { + int statusCode = response.getStatusLine().getStatusCode(); + + if (statusCode != 302) { + throw new IOException("Unexpected response code " + statusCode); + } + + Header location = response.getFirstHeader("Location"); + + if (location == null || location.getValue().contains("message=")) { + throw new FriendlyException("Login details for NicoNico are invalid.", COMMON, null); + } + + loggedIn.set(true); + } + } catch (IOException e) { + throw new FriendlyException("Exception when trying to log into NicoNico", SUSPICIOUS, e); + } + } + } + + private static String getWatchUrl(String videoId) { + return "http://www.nicovideo.jp/watch/" + videoId; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/nico/NicoAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/nico/NicoAudioTrack.java index 8a3261e10..80b523b0d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/nico/NicoAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/nico/NicoAudioTrack.java @@ -27,72 +27,72 @@ * Audio track that handles processing NicoNico tracks. */ public class NicoAudioTrack extends DelegatedAudioTrack { - private static final Logger log = LoggerFactory.getLogger(NicoAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(NicoAudioTrack.class); - private final NicoAudioSourceManager sourceManager; + private final NicoAudioSourceManager sourceManager; - /** - * @param trackInfo Track info - * @param sourceManager Source manager which was used to find this track - */ - public NicoAudioTrack(AudioTrackInfo trackInfo, NicoAudioSourceManager sourceManager) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param sourceManager Source manager which was used to find this track + */ + public NicoAudioTrack(AudioTrackInfo trackInfo, NicoAudioSourceManager sourceManager) { + super(trackInfo); - this.sourceManager = sourceManager; - } + this.sourceManager = sourceManager; + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - sourceManager.checkLoggedIn(); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + sourceManager.checkLoggedIn(); - try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { - loadVideoMainPage(httpInterface); - String playbackUrl = loadPlaybackUrl(httpInterface); + try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { + loadVideoMainPage(httpInterface); + String playbackUrl = loadPlaybackUrl(httpInterface); - log.debug("Starting NicoNico track from URL: {}", playbackUrl); + log.debug("Starting NicoNico track from URL: {}", playbackUrl); - try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(playbackUrl), null)) { - processDelegate(new MpegAudioTrack(trackInfo, stream), localExecutor); - } + try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(playbackUrl), null)) { + processDelegate(new MpegAudioTrack(trackInfo, stream), localExecutor); + } + } } - } - private void loadVideoMainPage(HttpInterface httpInterface) throws IOException { - HttpGet request = new HttpGet("http://www.nicovideo.jp/watch/" + trackInfo.identifier); + private void loadVideoMainPage(HttpInterface httpInterface) throws IOException { + HttpGet request = new HttpGet("http://www.nicovideo.jp/watch/" + trackInfo.identifier); - try (CloseableHttpResponse response = httpInterface.execute(request)) { - int statusCode = response.getStatusLine().getStatusCode(); - if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new IOException("Unexpected status code from video main page: " + statusCode); - } + try (CloseableHttpResponse response = httpInterface.execute(request)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new IOException("Unexpected status code from video main page: " + statusCode); + } - EntityUtils.consume(response.getEntity()); + EntityUtils.consume(response.getEntity()); + } } - } - private String loadPlaybackUrl(HttpInterface httpInterface) throws IOException { - HttpGet request = new HttpGet("http://flapi.nicovideo.jp/api/getflv/" + trackInfo.identifier); + private String loadPlaybackUrl(HttpInterface httpInterface) throws IOException { + HttpGet request = new HttpGet("http://flapi.nicovideo.jp/api/getflv/" + trackInfo.identifier); - try (CloseableHttpResponse response = httpInterface.execute(request)) { - int statusCode = response.getStatusLine().getStatusCode(); - if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new IOException("Unexpected status code from playback parameters page: " + statusCode); - } + try (CloseableHttpResponse response = httpInterface.execute(request)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new IOException("Unexpected status code from playback parameters page: " + statusCode); + } - String text = EntityUtils.toString(response.getEntity()); - Map format = convertToMapLayout(URLEncodedUtils.parse(text, StandardCharsets.UTF_8)); + String text = EntityUtils.toString(response.getEntity()); + Map format = convertToMapLayout(URLEncodedUtils.parse(text, StandardCharsets.UTF_8)); - return format.get("url"); + return format.get("url"); + } } - } - @Override - protected AudioTrack makeShallowClone() { - return new NicoAudioTrack(trackInfo, sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new NicoAudioTrack(trackInfo, sourceManager); + } - @Override - public AudioSourceManager getSourceManager() { - return sourceManager; - } + @Override + public AudioSourceManager getSourceManager() { + return sourceManager; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudDataLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudDataLoader.java index f63598c9d..ca7bec7d9 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudDataLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudDataLoader.java @@ -3,39 +3,40 @@ import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.utils.URIBuilder; import org.apache.http.util.EntityUtils; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; + public class DefaultSoundCloudDataLoader implements SoundCloudDataLoader { - @Override - public JsonBrowser load(HttpInterface httpInterface, String url) throws IOException { - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(buildUri(url)))) { - if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND) { - return JsonBrowser.NULL_BROWSER; - } + @Override + public JsonBrowser load(HttpInterface httpInterface, String url) throws IOException { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(buildUri(url)))) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND) { + return JsonBrowser.NULL_BROWSER; + } - HttpClientTools.assertSuccessWithContent(response, "video page response"); + HttpClientTools.assertSuccessWithContent(response, "video page response"); - String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); - return JsonBrowser.parse(json); + return JsonBrowser.parse(json); + } } - } - private URI buildUri(String url) { - try { - return new URIBuilder("https://api-v2.soundcloud.com/resolve") - .addParameter("url", url) - .build(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); + private URI buildUri(String url) { + try { + return new URIBuilder("https://api-v2.soundcloud.com/resolve") + .addParameter("url", url) + .build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } - } } 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 f8bbe31b7..c7098517c 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 @@ -3,99 +3,100 @@ import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.ThumbnailTools; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; -import java.util.ArrayList; -import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; + public class DefaultSoundCloudDataReader implements SoundCloudDataReader { - private static final Logger log = LoggerFactory.getLogger(DefaultSoundCloudDataReader.class); - - @Override - public JsonBrowser findTrackData(JsonBrowser rootData) { - return findEntryOfKind(rootData, "track"); - } - - @Override - public String readTrackId(JsonBrowser trackData) { - return trackData.get("id").safeText(); - } - - @Override - public boolean isTrackBlocked(JsonBrowser trackData) { - return "BLOCK".equals(trackData.get("policy").safeText()); - } - - @Override - public AudioTrackInfo readTrackInfo(JsonBrowser trackData, String identifier) { - return new AudioTrackInfo( - trackData.get("title").safeText(), - trackData.get("user").get("username").safeText(), - trackData.get("full_duration").as(Integer.class), - identifier, - false, - trackData.get("permalink_url").text(), - ThumbnailTools.getSoundCloudThumbnail(trackData), - null - ); - } - - @Override - public List readTrackFormats(JsonBrowser trackData) { - List formats = new ArrayList<>(); - String trackId = readTrackId(trackData); - - if (trackId.isEmpty()) { - log.warn("Track data {} missing track ID: {}.", trackId, trackData.format()); + private static final Logger log = LoggerFactory.getLogger(DefaultSoundCloudDataReader.class); + + @Override + public JsonBrowser findTrackData(JsonBrowser rootData) { + return findEntryOfKind(rootData, "track"); } - for (JsonBrowser transcoding : trackData.get("media").get("transcodings").values()) { - JsonBrowser format = transcoding.get("format"); + @Override + public String readTrackId(JsonBrowser trackData) { + return trackData.get("id").safeText(); + } - String protocol = format.get("protocol").safeText(); - String mimeType = format.get("mime_type").safeText(); + @Override + public boolean isTrackBlocked(JsonBrowser trackData) { + return "BLOCK".equals(trackData.get("policy").safeText()); + } - if (!protocol.isEmpty() && !mimeType.isEmpty()) { - String lookupUrl = transcoding.get("url").safeText(); + @Override + public AudioTrackInfo readTrackInfo(JsonBrowser trackData, String identifier) { + return new AudioTrackInfo( + trackData.get("title").safeText(), + trackData.get("user").get("username").safeText(), + trackData.get("full_duration").as(Integer.class), + identifier, + false, + trackData.get("permalink_url").text(), + ThumbnailTools.getSoundCloudThumbnail(trackData), + null + ); + } - if (!lookupUrl.isEmpty()) { - formats.add(new DefaultSoundCloudTrackFormat(trackId, protocol, mimeType, lookupUrl)); - } else { - log.warn("Transcoding of {} missing url: {}.", trackId, transcoding.format()); + @Override + public List readTrackFormats(JsonBrowser trackData) { + List formats = new ArrayList<>(); + String trackId = readTrackId(trackData); + + if (trackId.isEmpty()) { + log.warn("Track data {} missing track ID: {}.", trackId, trackData.format()); } - } else { - log.warn("Transcoding of {} missing protocol/mimetype: {}.", trackId, transcoding.format()); - } - } - return formats; - } + for (JsonBrowser transcoding : trackData.get("media").get("transcodings").values()) { + JsonBrowser format = transcoding.get("format"); - @Override - public JsonBrowser findPlaylistData(JsonBrowser rootData, String kind) { - return findEntryOfKind(rootData, kind); - } + String protocol = format.get("protocol").safeText(); + String mimeType = format.get("mime_type").safeText(); - @Override - public String readPlaylistName(JsonBrowser playlistData) { - return playlistData.get("title").safeText(); - } + if (!protocol.isEmpty() && !mimeType.isEmpty()) { + String lookupUrl = transcoding.get("url").safeText(); - @Override - public String readPlaylistIdentifier(JsonBrowser playlistData) { - return playlistData.get("permalink").safeText(); - } + if (!lookupUrl.isEmpty()) { + formats.add(new DefaultSoundCloudTrackFormat(trackId, protocol, mimeType, lookupUrl)); + } else { + log.warn("Transcoding of {} missing url: {}.", trackId, transcoding.format()); + } + } else { + log.warn("Transcoding of {} missing protocol/mimetype: {}.", trackId, transcoding.format()); + } + } + + return formats; + } - @Override - public List readPlaylistTracks(JsonBrowser playlistData) { - return playlistData.get("tracks").values(); - } + @Override + public JsonBrowser findPlaylistData(JsonBrowser rootData, String kind) { + return findEntryOfKind(rootData, kind); + } + + @Override + public String readPlaylistName(JsonBrowser playlistData) { + return playlistData.get("title").safeText(); + } - protected JsonBrowser findEntryOfKind(JsonBrowser data, String kind) { - if (data.isMap() && kind.equals(data.get("kind").text())) { - return data; + @Override + public String readPlaylistIdentifier(JsonBrowser playlistData) { + return playlistData.get("permalink").safeText(); } - return null; - } + @Override + public List readPlaylistTracks(JsonBrowser playlistData) { + return playlistData.get("tracks").values(); + } + + protected JsonBrowser findEntryOfKind(JsonBrowser data, String kind) { + if (data.isMap() && kind.equals(data.get("kind").text())) { + return data; + } + + return null; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudFormatHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudFormatHandler.java index 2d3d1868f..0e6c966f5 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudFormatHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudFormatHandler.java @@ -3,79 +3,79 @@ import java.util.List; public class DefaultSoundCloudFormatHandler implements SoundCloudFormatHandler { - private static final FormatType[] TYPES = FormatType.values(); - - @Override - public SoundCloudTrackFormat chooseBestFormat(List formats) { - for (FormatType type : TYPES) { - for (SoundCloudTrackFormat format : formats) { - if (type.matches(format)) { - return format; + private static final FormatType[] TYPES = FormatType.values(); + + @Override + public SoundCloudTrackFormat chooseBestFormat(List formats) { + for (FormatType type : TYPES) { + for (SoundCloudTrackFormat format : formats) { + if (type.matches(format)) { + return format; + } + } } - } + + throw new RuntimeException("Did not detect any supported formats"); } - throw new RuntimeException("Did not detect any supported formats"); - } + @Override + public String buildFormatIdentifier(SoundCloudTrackFormat format) { + for (FormatType type : TYPES) { + if (type.matches(format)) { + return type.prefix + format.getLookupUrl(); + } + } - @Override - public String buildFormatIdentifier(SoundCloudTrackFormat format) { - for (FormatType type : TYPES) { - if (type.matches(format)) { - return type.prefix + format.getLookupUrl(); - } + return "X:" + format.getLookupUrl(); } - return "X:" + format.getLookupUrl(); - } + @Override + public SoundCloudM3uInfo getM3uInfo(String identifier) { + if (identifier.startsWith(FormatType.TYPE_M3U_OPUS.prefix)) { + return new SoundCloudM3uInfo(identifier.substring(2), SoundCloudOpusSegmentDecoder::new); + } else if (identifier.startsWith(FormatType.TYPE_M3U_MP3.prefix)) { + return new SoundCloudM3uInfo(identifier.substring(2), SoundCloudMp3SegmentDecoder::new); + } - @Override - public SoundCloudM3uInfo getM3uInfo(String identifier) { - if (identifier.startsWith(FormatType.TYPE_M3U_OPUS.prefix)) { - return new SoundCloudM3uInfo(identifier.substring(2), SoundCloudOpusSegmentDecoder::new); - } else if (identifier.startsWith(FormatType.TYPE_M3U_MP3.prefix)) { - return new SoundCloudM3uInfo(identifier.substring(2), SoundCloudMp3SegmentDecoder::new); + return null; } - return null; - } + @Override + public String getMp3LookupUrl(String identifier) { + if (identifier.startsWith("M:")) { + return identifier.substring(2); + } - @Override - public String getMp3LookupUrl(String identifier) { - if (identifier.startsWith("M:")) { - return identifier.substring(2); + return null; } - return null; - } + private static SoundCloudTrackFormat findFormat(List formats, FormatType type) { + for (SoundCloudTrackFormat format : formats) { + if (type.matches(format)) { + return format; + } + } - private static SoundCloudTrackFormat findFormat(List formats, FormatType type) { - for (SoundCloudTrackFormat format : formats) { - if (type.matches(format)) { - return format; - } + return null; } - return null; - } + private enum FormatType { + TYPE_M3U_OPUS("hls", "audio/ogg", "O:"), + TYPE_M3U_MP3("hls", "audio/mpeg", "U:"), + TYPE_SIMPLE_MP3("progressive", "audio/mpeg", "M:"); - private enum FormatType { - TYPE_M3U_OPUS("hls", "audio/ogg", "O:"), - TYPE_M3U_MP3("hls", "audio/mpeg", "U:"), - TYPE_SIMPLE_MP3("progressive", "audio/mpeg", "M:"); + public final String protocol; + public final String mimeType; + public final String prefix; - public final String protocol; - public final String mimeType; - public final String prefix; - - FormatType(String protocol, String mimeType, String prefix) { - this.protocol = protocol; - this.mimeType = mimeType; - this.prefix = prefix; - } + FormatType(String protocol, String mimeType, String prefix) { + this.protocol = protocol; + this.mimeType = mimeType; + this.prefix = prefix; + } - public boolean matches(SoundCloudTrackFormat format) { - return protocol.equals(format.getProtocol()) && format.getMimeType().contains(mimeType); + public boolean matches(SoundCloudTrackFormat format) { + return protocol.equals(format.getProtocol()) && format.getMimeType().contains(mimeType); + } } - } -} \ No newline at end of file +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudPlaylistLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudPlaylistLoader.java index dc5a69808..05bfad225 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudPlaylistLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudPlaylistLoader.java @@ -9,162 +9,158 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.StringJoiner; +import java.util.*; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URIBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS; public class DefaultSoundCloudPlaylistLoader implements SoundCloudPlaylistLoader { - private static final Logger log = LoggerFactory.getLogger(DefaultSoundCloudPlaylistLoader.class); - - protected static final String PLAYLIST_URL_REGEX = "^(?:http://|https://|)(?:www\\.|)(?:m\\.|)soundcloud\\.com/([a-zA-Z0-9-_:]+)/sets/([a-zA-Z0-9-_:]+)/?([a-zA-Z0-9-_:]+)?(?:\\?.*|)$"; - protected static final Pattern playlistUrlPattern = Pattern.compile(PLAYLIST_URL_REGEX); - - protected final SoundCloudDataLoader dataLoader; - protected final SoundCloudDataReader dataReader; - protected final SoundCloudFormatHandler formatHandler; - - public DefaultSoundCloudPlaylistLoader( - SoundCloudDataLoader dataLoader, - SoundCloudDataReader dataReader, - SoundCloudFormatHandler formatHandler - ) { - this.dataLoader = dataLoader; - this.dataReader = dataReader; - this.formatHandler = formatHandler; - } - - @Override - public AudioPlaylist load( - String identifier, - HttpInterfaceManager httpInterfaceManager, - Function trackFactory - ) { - String url = SoundCloudHelper.nonMobileUrl(identifier); - - if (playlistUrlPattern.matcher(url).matches()) { - return loadFromSet(httpInterfaceManager, url, trackFactory); - } else { - return null; + private static final Logger log = LoggerFactory.getLogger(DefaultSoundCloudPlaylistLoader.class); + + protected static final String PLAYLIST_URL_REGEX = "^(?:http://|https://|)(?:www\\.|)(?:m\\.|)soundcloud\\.com/([a-zA-Z0-9-_:]+)/sets/([a-zA-Z0-9-_:]+)/?([a-zA-Z0-9-_:]+)?(?:\\?.*|)$"; + protected static final Pattern playlistUrlPattern = Pattern.compile(PLAYLIST_URL_REGEX); + + protected final SoundCloudDataLoader dataLoader; + protected final SoundCloudDataReader dataReader; + protected final SoundCloudFormatHandler formatHandler; + + public DefaultSoundCloudPlaylistLoader( + SoundCloudDataLoader dataLoader, + SoundCloudDataReader dataReader, + SoundCloudFormatHandler formatHandler + ) { + this.dataLoader = dataLoader; + this.dataReader = dataReader; + this.formatHandler = formatHandler; } - } - - protected AudioPlaylist loadFromSet( - HttpInterfaceManager httpInterfaceManager, - String playlistWebUrl, - Function trackFactory - ) { - try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { - JsonBrowser rootData = dataLoader.load(httpInterface, playlistWebUrl); - String kind = rootData.get("kind").text(); - JsonBrowser playlistData = dataReader.findPlaylistData(rootData, kind); - - return new BasicAudioPlaylist( - dataReader.readPlaylistName(playlistData), - loadPlaylistTracks(httpInterface, playlistData, trackFactory), - null, - false - ); - } catch (IOException e) { - throw new FriendlyException("Loading playlist from SoundCloud failed.", SUSPICIOUS, e); + + @Override + public AudioPlaylist load( + String identifier, + HttpInterfaceManager httpInterfaceManager, + Function trackFactory + ) { + String url = SoundCloudHelper.nonMobileUrl(identifier); + + if (playlistUrlPattern.matcher(url).matches()) { + return loadFromSet(httpInterfaceManager, url, trackFactory); + } else { + return null; + } } - } - protected List loadPlaylistTracks( - HttpInterface httpInterface, - JsonBrowser playlistData, - Function trackFactory - ) throws IOException { - String playlistId = dataReader.readPlaylistIdentifier(playlistData); + protected AudioPlaylist loadFromSet( + HttpInterfaceManager httpInterfaceManager, + String playlistWebUrl, + Function trackFactory + ) { + try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { + JsonBrowser rootData = dataLoader.load(httpInterface, playlistWebUrl); + String kind = rootData.get("kind").text(); + JsonBrowser playlistData = dataReader.findPlaylistData(rootData, kind); + + return new BasicAudioPlaylist( + dataReader.readPlaylistName(playlistData), + loadPlaylistTracks(httpInterface, playlistData, trackFactory), + null, + false + ); + } catch (IOException e) { + throw new FriendlyException("Loading playlist from SoundCloud failed.", SUSPICIOUS, e); + } + } - List trackIds = dataReader.readPlaylistTracks(playlistData).stream() - .map(dataReader::readTrackId) - .collect(Collectors.toList()); + protected List loadPlaylistTracks( + HttpInterface httpInterface, + JsonBrowser playlistData, + Function trackFactory + ) throws IOException { + String playlistId = dataReader.readPlaylistIdentifier(playlistData); - int numTrackIds = trackIds.size(); - List trackDataList = new ArrayList<>(); + List trackIds = dataReader.readPlaylistTracks(playlistData).stream() + .map(dataReader::readTrackId) + .collect(Collectors.toList()); - for (int i = 0; i < numTrackIds; i += 50) { - int last = Math.min(i + 50, numTrackIds); - List trackIdSegment = trackIds.subList(i, last); + int numTrackIds = trackIds.size(); + List trackDataList = new ArrayList<>(); - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(buildTrackListUrl(trackIdSegment)))) { - HttpClientTools.assertSuccessWithContent(response, "track list response"); + for (int i = 0; i < numTrackIds; i += 50) { + int last = Math.min(i + 50, numTrackIds); + List trackIdSegment = trackIds.subList(i, last); - JsonBrowser trackList = JsonBrowser.parse(response.getEntity().getContent()); - trackDataList.addAll(trackList.values()); - } - } + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(buildTrackListUrl(trackIdSegment)))) { + HttpClientTools.assertSuccessWithContent(response, "track list response"); - sortPlaylistTracks(trackDataList, trackIds); + JsonBrowser trackList = JsonBrowser.parse(response.getEntity().getContent()); + trackDataList.addAll(trackList.values()); + } + } - int blockedCount = 0; - List tracks = new ArrayList<>(); + sortPlaylistTracks(trackDataList, trackIds); + + int blockedCount = 0; + List tracks = new ArrayList<>(); + + for (JsonBrowser trackData : trackDataList) { + if (dataReader.isTrackBlocked(trackData)) { + blockedCount++; + } else { + try { + tracks.add(trackFactory.apply(dataReader.readTrackInfo( + trackData, + formatHandler.buildFormatIdentifier( + formatHandler.chooseBestFormat(dataReader.readTrackFormats(trackData)) + ) + ))); + } catch (Exception e) { + log.error("In soundcloud playlist {}, failed to load track", playlistId, e); + } + } + } - for (JsonBrowser trackData : trackDataList) { - if (dataReader.isTrackBlocked(trackData)) { - blockedCount++; - } else { - try { - tracks.add(trackFactory.apply(dataReader.readTrackInfo( - trackData, - formatHandler.buildFormatIdentifier( - formatHandler.chooseBestFormat(dataReader.readTrackFormats(trackData)) - ) - ))); - } catch (Exception e) { - log.error("In soundcloud playlist {}, failed to load track", playlistId, e); + if (blockedCount > 0) { + log.debug("In soundcloud playlist {}, {} tracks were omitted because they are blocked.", + playlistId, blockedCount); } - } - } - if (blockedCount > 0) { - log.debug("In soundcloud playlist {}, {} tracks were omitted because they are blocked.", - playlistId, blockedCount); + return tracks; } - return tracks; - } - - protected URI buildTrackListUrl(List trackIds) { - try { - StringJoiner joiner = new StringJoiner(","); - for (String trackId : trackIds) { - joiner.add(trackId); - } - - return new URIBuilder("https://api-v2.soundcloud.com/tracks") - .addParameter("ids", joiner.toString()) - .build(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); + protected URI buildTrackListUrl(List trackIds) { + try { + StringJoiner joiner = new StringJoiner(","); + for (String trackId : trackIds) { + joiner.add(trackId); + } + + return new URIBuilder("https://api-v2.soundcloud.com/tracks") + .addParameter("ids", joiner.toString()) + .build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } - } - protected void sortPlaylistTracks(List trackDataList, List trackIds) { - Map positions = new HashMap<>(); + protected void sortPlaylistTracks(List trackDataList, List trackIds) { + Map positions = new HashMap<>(); - for (int i = 0; i < trackIds.size(); i++) { - positions.put(trackIds.get(i), i); - } + for (int i = 0; i < trackIds.size(); i++) { + positions.put(trackIds.get(i), i); + } - trackDataList.sort(Comparator.comparingInt(trackData -> - positions.getOrDefault(dataReader.readTrackId(trackData), Integer.MAX_VALUE) - )); - } + trackDataList.sort(Comparator.comparingInt(trackData -> + positions.getOrDefault(dataReader.readTrackId(trackData), Integer.MAX_VALUE) + )); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudTrackFormat.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudTrackFormat.java index 4909179d4..a1a0bdf3e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudTrackFormat.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/DefaultSoundCloudTrackFormat.java @@ -1,35 +1,35 @@ package com.sedmelluq.discord.lavaplayer.source.soundcloud; public class DefaultSoundCloudTrackFormat implements SoundCloudTrackFormat { - private final String trackId; - private final String protocol; - private final String mimeType; - private final String lookupUrl; + private final String trackId; + private final String protocol; + private final String mimeType; + private final String lookupUrl; - public DefaultSoundCloudTrackFormat(String trackId, String protocol, String mimeType, String lookupUrl) { - this.trackId = trackId; - this.protocol = protocol; - this.mimeType = mimeType; - this.lookupUrl = lookupUrl; - } + public DefaultSoundCloudTrackFormat(String trackId, String protocol, String mimeType, String lookupUrl) { + this.trackId = trackId; + this.protocol = protocol; + this.mimeType = mimeType; + this.lookupUrl = lookupUrl; + } - @Override - public String getTrackId() { - return trackId; - } + @Override + public String getTrackId() { + return trackId; + } - @Override - public String getProtocol() { - return protocol; - } + @Override + public String getProtocol() { + return protocol; + } - @Override - public String getMimeType() { - return mimeType; - } + @Override + public String getMimeType() { + return mimeType; + } - @Override - public String getLookupUrl() { - return lookupUrl; - } + @Override + public String getLookupUrl() { + return lookupUrl; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudAudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudAudioSourceManager.java index 93e88e87c..d6198f112 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudAudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudAudioSourceManager.java @@ -8,11 +8,17 @@ import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager; -import com.sedmelluq.discord.lavaplayer.track.AudioItem; -import com.sedmelluq.discord.lavaplayer.track.AudioReference; -import com.sedmelluq.discord.lavaplayer.track.AudioTrack; -import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; -import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.*; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; + import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; @@ -25,15 +31,6 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.io.IOUtils; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.util.EntityUtils; import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.COMMON; import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS; @@ -42,409 +39,410 @@ * Audio source manager that implements finding SoundCloud tracks based on URL. */ public class SoundCloudAudioSourceManager implements AudioSourceManager, HttpConfigurable { - private static final int DEFAULT_SEARCH_RESULTS = 10; - private static final int MAXIMUM_SEARCH_RESULTS = 200; - - private static final String MOBILE_URL_REGEX = "^(?:http://|https://|)soundcloud\\.app\\.goo\\.gl/([a-zA-Z0-9-_]+)/?(?:\\?.*|)$"; - private static final String TRACK_URL_REGEX = "^(?:http://|https://|)(?:www\\.|)(?:m\\.|)soundcloud\\.com/([a-zA-Z0-9-_]+)/([a-zA-Z0-9-_]+)/?(?:\\?.*|)$"; - private static final String SHORT_TRACK_URL_REGEX = "^https://on.soundcloud\\.com/[a-zA-Z0-9-_]+/?(?:\\?.*|)$"; - private static final String UNLISTED_URL_REGEX = "^(?:http://|https://|)(?:www\\.|)(?:m\\.|)soundcloud\\.com/([a-zA-Z0-9-_]+)/([a-zA-Z0-9-_]+)/s-([a-zA-Z0-9-_]+)(?:\\?.*|)$"; - private static final String LIKED_URL_REGEX = "^(?:http://|https://|)(?:www\\.|)(?:m\\.|)soundcloud\\.com/([a-zA-Z0-9-_]+)/likes/?(?:\\?.*|)$"; - private static final String LIKED_USER_URN_REGEX = "\"urn\":\"soundcloud:users:([0-9]+)\",\"username\":\"([^\"]+)\""; - private static final String SEARCH_PREFIX = "scsearch"; - private static final String SEARCH_PREFIX_DEFAULT = "scsearch:"; - private static final String SEARCH_REGEX = SEARCH_PREFIX + "\\[([0-9]{1,9}),([0-9]{1,9})\\]:\\s*(.*)\\s*"; - - private static final Pattern mobileUrlPattern = Pattern.compile(MOBILE_URL_REGEX); - private static final Pattern trackUrlPattern = Pattern.compile(TRACK_URL_REGEX); - private static final Pattern shortTrackUrlPattern = Pattern.compile(SHORT_TRACK_URL_REGEX); - private static final Pattern unlistedUrlPattern = Pattern.compile(UNLISTED_URL_REGEX); - private static final Pattern likedUrlPattern = Pattern.compile(LIKED_URL_REGEX); - private static final Pattern likedUserUrnPattern = Pattern.compile(LIKED_USER_URN_REGEX); - private static final Pattern searchPattern = Pattern.compile(SEARCH_REGEX); - - private final SoundCloudDataReader dataReader; - private final SoundCloudDataLoader dataLoader; - private final SoundCloudFormatHandler formatHandler; - private final SoundCloudPlaylistLoader playlistLoader; - private final HttpInterfaceManager httpInterfaceManager; - private final SoundCloudClientIdTracker clientIdTracker; - private final boolean allowSearch; - - public static SoundCloudAudioSourceManager createDefault() { - SoundCloudDataReader dataReader = new DefaultSoundCloudDataReader(); - SoundCloudDataLoader dataLoader = new DefaultSoundCloudDataLoader(); - SoundCloudFormatHandler formatHandler = new DefaultSoundCloudFormatHandler(); - - return new SoundCloudAudioSourceManager(true, dataReader, dataLoader, formatHandler, - new DefaultSoundCloudPlaylistLoader(dataLoader, dataReader, formatHandler)); - } - - public static Builder builder() { - return new Builder(); - } - - /** - * Create an instance. - * @param allowSearch Whether to allow search queries as identifiers - */ - public SoundCloudAudioSourceManager( - boolean allowSearch, - SoundCloudDataReader dataReader, - SoundCloudDataLoader dataLoader, - SoundCloudFormatHandler formatHandler, - SoundCloudPlaylistLoader playlistLoader - ) { - this.allowSearch = allowSearch; - this.dataReader = dataReader; - this.dataLoader = dataLoader; - this.formatHandler = formatHandler; - this.playlistLoader = playlistLoader; - - httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - clientIdTracker = new SoundCloudClientIdTracker(httpInterfaceManager); - httpInterfaceManager.setHttpContextFilter(new SoundCloudHttpContextFilter(clientIdTracker)); - } - - public SoundCloudFormatHandler getFormatHandler() { - return formatHandler; - } - - @Override - public String getSourceName() { - return "soundcloud"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - Matcher mobileUrlMatcher = mobileUrlPattern.matcher(reference.identifier); - if (mobileUrlMatcher.matches()) { - reference = SoundCloudHelper.redirectMobileLink(httpInterfaceManager.getInterface(), reference); + private static final int DEFAULT_SEARCH_RESULTS = 10; + private static final int MAXIMUM_SEARCH_RESULTS = 200; + + private static final String MOBILE_URL_REGEX = "^(?:http://|https://|)soundcloud\\.app\\.goo\\.gl/([a-zA-Z0-9-_]+)/?(?:\\?.*|)$"; + private static final String TRACK_URL_REGEX = "^(?:http://|https://|)(?:www\\.|)(?:m\\.|)soundcloud\\.com/([a-zA-Z0-9-_]+)/([a-zA-Z0-9-_]+)/?(?:\\?.*|)$"; + private static final String SHORT_TRACK_URL_REGEX = "^https://on.soundcloud\\.com/[a-zA-Z0-9-_]+/?(?:\\?.*|)$"; + private static final String UNLISTED_URL_REGEX = "^(?:http://|https://|)(?:www\\.|)(?:m\\.|)soundcloud\\.com/([a-zA-Z0-9-_]+)/([a-zA-Z0-9-_]+)/s-([a-zA-Z0-9-_]+)(?:\\?.*|)$"; + private static final String LIKED_URL_REGEX = "^(?:http://|https://|)(?:www\\.|)(?:m\\.|)soundcloud\\.com/([a-zA-Z0-9-_]+)/likes/?(?:\\?.*|)$"; + private static final String LIKED_USER_URN_REGEX = "\"urn\":\"soundcloud:users:([0-9]+)\",\"username\":\"([^\"]+)\""; + private static final String SEARCH_PREFIX = "scsearch"; + private static final String SEARCH_PREFIX_DEFAULT = "scsearch:"; + private static final String SEARCH_REGEX = SEARCH_PREFIX + "\\[([0-9]{1,9}),([0-9]{1,9})\\]:\\s*(.*)\\s*"; + + private static final Pattern mobileUrlPattern = Pattern.compile(MOBILE_URL_REGEX); + private static final Pattern trackUrlPattern = Pattern.compile(TRACK_URL_REGEX); + private static final Pattern shortTrackUrlPattern = Pattern.compile(SHORT_TRACK_URL_REGEX); + private static final Pattern unlistedUrlPattern = Pattern.compile(UNLISTED_URL_REGEX); + private static final Pattern likedUrlPattern = Pattern.compile(LIKED_URL_REGEX); + private static final Pattern likedUserUrnPattern = Pattern.compile(LIKED_USER_URN_REGEX); + private static final Pattern searchPattern = Pattern.compile(SEARCH_REGEX); + + private final SoundCloudDataReader dataReader; + private final SoundCloudDataLoader dataLoader; + private final SoundCloudFormatHandler formatHandler; + private final SoundCloudPlaylistLoader playlistLoader; + private final HttpInterfaceManager httpInterfaceManager; + private final SoundCloudClientIdTracker clientIdTracker; + private final boolean allowSearch; + + public static SoundCloudAudioSourceManager createDefault() { + SoundCloudDataReader dataReader = new DefaultSoundCloudDataReader(); + SoundCloudDataLoader dataLoader = new DefaultSoundCloudDataLoader(); + SoundCloudFormatHandler formatHandler = new DefaultSoundCloudFormatHandler(); + + return new SoundCloudAudioSourceManager(true, dataReader, dataLoader, formatHandler, + new DefaultSoundCloudPlaylistLoader(dataLoader, dataReader, formatHandler)); } - Matcher shortTrackMatcher = shortTrackUrlPattern.matcher(reference.identifier); - if (shortTrackMatcher.matches()) { - reference = SoundCloudHelper.resolveShortTrackUrl(httpInterfaceManager.getInterface(), reference); + public static Builder builder() { + return new Builder(); } - AudioItem track = processAsSingleTrack(reference); - - if (track == null) { - track = playlistLoader.load(reference.identifier, httpInterfaceManager, this::buildTrackFromInfo); + /** + * Create an instance. + * + * @param allowSearch Whether to allow search queries as identifiers + */ + public SoundCloudAudioSourceManager( + boolean allowSearch, + SoundCloudDataReader dataReader, + SoundCloudDataLoader dataLoader, + SoundCloudFormatHandler formatHandler, + SoundCloudPlaylistLoader playlistLoader + ) { + this.allowSearch = allowSearch; + this.dataReader = dataReader; + this.dataLoader = dataLoader; + this.formatHandler = formatHandler; + this.playlistLoader = playlistLoader; + + httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + clientIdTracker = new SoundCloudClientIdTracker(httpInterfaceManager); + httpInterfaceManager.setHttpContextFilter(new SoundCloudHttpContextFilter(clientIdTracker)); } - if (track == null) { - track = processAsLikedTracks(reference); + public SoundCloudFormatHandler getFormatHandler() { + return formatHandler; } - if (track == null && allowSearch) { - track = processAsSearchQuery(reference); + @Override + public String getSourceName() { + return "soundcloud"; } - return track; - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) { - // No extra information to save - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { - return new SoundCloudAudioTrack(trackInfo, this); - } - - @Override - public void shutdown() { - // Nothing to shut down - } - - public String getClientId() { - return clientIdTracker.getClientId(); - } - - /** - * @return Get an HTTP interface for a playing track. - */ - public HttpInterface getHttpInterface() { - return httpInterfaceManager.getInterface(); - } - - @Override - public void configureRequests(Function configurator) { - httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - httpInterfaceManager.configureBuilder(configurator); - } - - private AudioTrack processAsSingleTrack(AudioReference reference) { - String url = SoundCloudHelper.nonMobileUrl(reference.identifier); - - Matcher trackUrlMatcher = trackUrlPattern.matcher(url); - if (trackUrlMatcher.matches() && !"likes".equals(trackUrlMatcher.group(2))) { - return loadTrack(url); + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + Matcher mobileUrlMatcher = mobileUrlPattern.matcher(reference.identifier); + if (mobileUrlMatcher.matches()) { + reference = SoundCloudHelper.redirectMobileLink(httpInterfaceManager.getInterface(), reference); + } + + Matcher shortTrackMatcher = shortTrackUrlPattern.matcher(reference.identifier); + if (shortTrackMatcher.matches()) { + reference = SoundCloudHelper.resolveShortTrackUrl(httpInterfaceManager.getInterface(), reference); + } + + AudioItem track = processAsSingleTrack(reference); + + if (track == null) { + track = playlistLoader.load(reference.identifier, httpInterfaceManager, this::buildTrackFromInfo); + } + + if (track == null) { + track = processAsLikedTracks(reference); + } + + if (track == null && allowSearch) { + track = processAsSearchQuery(reference); + } + + return track; } - Matcher unlistedUrlMatcher = unlistedUrlPattern.matcher(url); - if (unlistedUrlMatcher.matches()) { - return loadTrack(url); + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; } - return null; - } + @Override + public void encodeTrack(AudioTrack track, DataOutput output) { + // No extra information to save + } - private AudioItem processAsLikedTracks(AudioReference reference) { - String url = SoundCloudHelper.nonMobileUrl(reference.identifier); + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { + return new SoundCloudAudioTrack(trackInfo, this); + } - if (likedUrlPattern.matcher(url).matches()) { - return loadFromLikedTracks(url); - } else { - return null; + @Override + public void shutdown() { + // Nothing to shut down } - } - public AudioTrack loadTrack(String trackWebUrl) { - try (HttpInterface httpInterface = getHttpInterface()) { - JsonBrowser rootData = dataLoader.load(httpInterface, trackWebUrl); - JsonBrowser trackData = dataReader.findTrackData(rootData); + public String getClientId() { + return clientIdTracker.getClientId(); + } - if (trackData == null) { - throw new FriendlyException("This track is not available", COMMON, null); - } + /** + * @return Get an HTTP interface for a playing track. + */ + public HttpInterface getHttpInterface() { + return httpInterfaceManager.getInterface(); + } - return loadFromTrackData(trackData); - } catch (IOException e) { - throw new FriendlyException("Loading track from SoundCloud failed.", SUSPICIOUS, e); + @Override + public void configureRequests(Function configurator) { + httpInterfaceManager.configureRequests(configurator); } - } - - protected AudioTrack loadFromTrackData(JsonBrowser trackData) { - SoundCloudTrackFormat format = formatHandler.chooseBestFormat(dataReader.readTrackFormats(trackData)); - return buildTrackFromInfo(dataReader.readTrackInfo(trackData, formatHandler.buildFormatIdentifier(format))); - } - - private AudioTrack buildTrackFromInfo(AudioTrackInfo trackInfo) { - return new SoundCloudAudioTrack(trackInfo, this); - } - - private AudioItem loadFromLikedTracks(String likedListUrl) { - try (HttpInterface httpInterface = getHttpInterface()) { - UserInfo userInfo = findUserIdFromLikedList(httpInterface, likedListUrl); - if (userInfo == null) { - return AudioReference.NO_TRACK; - } - - return extractTracksFromLikedList(loadLikedListForUserId(httpInterface, userInfo), userInfo); - } catch (IOException e) { - throw new FriendlyException("Loading liked tracks from SoundCloud failed.", SUSPICIOUS, e); + + @Override + public void configureBuilder(Consumer configurator) { + httpInterfaceManager.configureBuilder(configurator); } - } - private UserInfo findUserIdFromLikedList(HttpInterface httpInterface, String likedListUrl) throws IOException { - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(likedListUrl))) { - int statusCode = response.getStatusLine().getStatusCode(); + private AudioTrack processAsSingleTrack(AudioReference reference) { + String url = SoundCloudHelper.nonMobileUrl(reference.identifier); - if (statusCode == HttpStatus.SC_NOT_FOUND) { - return null; - } else if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new IOException("Invalid status code for track list response: " + statusCode); - } + Matcher trackUrlMatcher = trackUrlPattern.matcher(url); + if (trackUrlMatcher.matches() && !"likes".equals(trackUrlMatcher.group(2))) { + return loadTrack(url); + } + + Matcher unlistedUrlMatcher = unlistedUrlPattern.matcher(url); + if (unlistedUrlMatcher.matches()) { + return loadTrack(url); + } - Matcher matcher = likedUserUrnPattern.matcher(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); - return matcher.find() ? new UserInfo(matcher.group(1), matcher.group(2)) : null; + return null; } - } - private JsonBrowser loadLikedListForUserId(HttpInterface httpInterface, UserInfo userInfo) throws IOException { - URI uri = URI.create("https://api-v2.soundcloud.com/users/" + userInfo.id + "/likes?limit=200&offset=0"); + private AudioItem processAsLikedTracks(AudioReference reference) { + String url = SoundCloudHelper.nonMobileUrl(reference.identifier); - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(uri))) { - HttpClientTools.assertSuccessWithContent(response, "liked tracks response"); - return JsonBrowser.parse(response.getEntity().getContent()); + if (likedUrlPattern.matcher(url).matches()) { + return loadFromLikedTracks(url); + } else { + return null; + } } - } - private AudioItem extractTracksFromLikedList(JsonBrowser likedTracks, UserInfo userInfo) { - List tracks = new ArrayList<>(); + public AudioTrack loadTrack(String trackWebUrl) { + try (HttpInterface httpInterface = getHttpInterface()) { + JsonBrowser rootData = dataLoader.load(httpInterface, trackWebUrl); + JsonBrowser trackData = dataReader.findTrackData(rootData); - for (JsonBrowser item : likedTracks.get("collection").values()) { - JsonBrowser trackItem = item.get("track"); + if (trackData == null) { + throw new FriendlyException("This track is not available", COMMON, null); + } - if (!trackItem.isNull() && !dataReader.isTrackBlocked(trackItem)) { - tracks.add(loadFromTrackData(trackItem)); - } + return loadFromTrackData(trackData); + } catch (IOException e) { + throw new FriendlyException("Loading track from SoundCloud failed.", SUSPICIOUS, e); + } } - return new BasicAudioPlaylist("Liked by " + userInfo.name, tracks, null, false); - } - - private static class UserInfo { - private final String id; - private final String name; - - private UserInfo(String id, String name) { - this.id = id; - this.name = name; + protected AudioTrack loadFromTrackData(JsonBrowser trackData) { + SoundCloudTrackFormat format = formatHandler.chooseBestFormat(dataReader.readTrackFormats(trackData)); + return buildTrackFromInfo(dataReader.readTrackInfo(trackData, formatHandler.buildFormatIdentifier(format))); } - } - private AudioItem processAsSearchQuery(AudioReference reference) { - if (reference.identifier.startsWith(SEARCH_PREFIX)) { - if (reference.identifier.startsWith(SEARCH_PREFIX_DEFAULT)) { - return loadSearchResult(reference.identifier.substring(SEARCH_PREFIX_DEFAULT.length()).trim(), 0, DEFAULT_SEARCH_RESULTS); - } + private AudioTrack buildTrackFromInfo(AudioTrackInfo trackInfo) { + return new SoundCloudAudioTrack(trackInfo, this); + } - Matcher searchMatcher = searchPattern.matcher(reference.identifier); + private AudioItem loadFromLikedTracks(String likedListUrl) { + try (HttpInterface httpInterface = getHttpInterface()) { + UserInfo userInfo = findUserIdFromLikedList(httpInterface, likedListUrl); + if (userInfo == null) { + return AudioReference.NO_TRACK; + } - if (searchMatcher.matches()) { - return loadSearchResult(searchMatcher.group(3), Integer.parseInt(searchMatcher.group(1)), Integer.parseInt(searchMatcher.group(2))); - } + return extractTracksFromLikedList(loadLikedListForUserId(httpInterface, userInfo), userInfo); + } catch (IOException e) { + throw new FriendlyException("Loading liked tracks from SoundCloud failed.", SUSPICIOUS, e); + } } - return null; - } + private UserInfo findUserIdFromLikedList(HttpInterface httpInterface, String likedListUrl) throws IOException { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(likedListUrl))) { + int statusCode = response.getStatusLine().getStatusCode(); - private AudioItem loadSearchResult(String query, int offset, int rawLimit) { - int limit = Math.min(rawLimit, MAXIMUM_SEARCH_RESULTS); + if (statusCode == HttpStatus.SC_NOT_FOUND) { + return null; + } else if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new IOException("Invalid status code for track list response: " + statusCode); + } - try ( - HttpInterface httpInterface = getHttpInterface(); - CloseableHttpResponse response = httpInterface.execute(new HttpGet(buildSearchUri(query, offset, limit))) - ) { - return loadSearchResultsFromResponse(response, query); - } catch (IOException e) { - throw new FriendlyException("Loading search results from SoundCloud failed.", SUSPICIOUS, e); + Matcher matcher = likedUserUrnPattern.matcher(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); + return matcher.find() ? new UserInfo(matcher.group(1), matcher.group(2)) : null; + } } - } - - private AudioItem loadSearchResultsFromResponse(HttpResponse response, String query) throws IOException { - try { - JsonBrowser searchResults = JsonBrowser.parse(response.getEntity().getContent()); - return extractTracksFromSearchResults(query, searchResults); - } finally { - EntityUtils.consumeQuietly(response.getEntity()); + + private JsonBrowser loadLikedListForUserId(HttpInterface httpInterface, UserInfo userInfo) throws IOException { + URI uri = URI.create("https://api-v2.soundcloud.com/users/" + userInfo.id + "/likes?limit=200&offset=0"); + + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(uri))) { + HttpClientTools.assertSuccessWithContent(response, "liked tracks response"); + return JsonBrowser.parse(response.getEntity().getContent()); + } } - } - - private URI buildSearchUri(String query, int offset, int limit) { - try { - return new URIBuilder("https://api-v2.soundcloud.com/search/tracks") - .addParameter("q", query) - .addParameter("offset", String.valueOf(offset)) - .addParameter("limit", String.valueOf(limit)) - .build(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); + + private AudioItem extractTracksFromLikedList(JsonBrowser likedTracks, UserInfo userInfo) { + List tracks = new ArrayList<>(); + + for (JsonBrowser item : likedTracks.get("collection").values()) { + JsonBrowser trackItem = item.get("track"); + + if (!trackItem.isNull() && !dataReader.isTrackBlocked(trackItem)) { + tracks.add(loadFromTrackData(trackItem)); + } + } + + return new BasicAudioPlaylist("Liked by " + userInfo.name, tracks, null, false); } - } - private AudioItem extractTracksFromSearchResults(String query, JsonBrowser searchResults) { - List tracks = new ArrayList<>(); + private static class UserInfo { + private final String id; + private final String name; - for (JsonBrowser item : searchResults.get("collection").values()) { - if (!item.isNull()) { - tracks.add(loadFromTrackData(item)); - } + private UserInfo(String id, String name) { + this.id = id; + this.name = name; + } } - return new BasicAudioPlaylist("Search results for: " + query, tracks, null, true); - } + private AudioItem processAsSearchQuery(AudioReference reference) { + if (reference.identifier.startsWith(SEARCH_PREFIX)) { + if (reference.identifier.startsWith(SEARCH_PREFIX_DEFAULT)) { + return loadSearchResult(reference.identifier.substring(SEARCH_PREFIX_DEFAULT.length()).trim(), 0, DEFAULT_SEARCH_RESULTS); + } - public static class Builder { - private boolean allowSearch = true; - private SoundCloudDataReader dataReader; - private SoundCloudDataLoader dataLoader; - private SoundCloudFormatHandler formatHandler; - private SoundCloudPlaylistLoader playlistLoader; - private PlaylistLoaderFactory playlistLoaderFactory; + Matcher searchMatcher = searchPattern.matcher(reference.identifier); - public Builder withAllowSearch(boolean allowSearch) { - this.allowSearch = allowSearch; - return this; - } + if (searchMatcher.matches()) { + return loadSearchResult(searchMatcher.group(3), Integer.parseInt(searchMatcher.group(1)), Integer.parseInt(searchMatcher.group(2))); + } + } - public Builder withDataReader(SoundCloudDataReader dataReader) { - this.dataReader = dataReader; - return this; + return null; } - public Builder withDataLoader(SoundCloudDataLoader dataLoader) { - this.dataLoader = dataLoader; - return this; + private AudioItem loadSearchResult(String query, int offset, int rawLimit) { + int limit = Math.min(rawLimit, MAXIMUM_SEARCH_RESULTS); + + try ( + HttpInterface httpInterface = getHttpInterface(); + CloseableHttpResponse response = httpInterface.execute(new HttpGet(buildSearchUri(query, offset, limit))) + ) { + return loadSearchResultsFromResponse(response, query); + } catch (IOException e) { + throw new FriendlyException("Loading search results from SoundCloud failed.", SUSPICIOUS, e); + } } - public Builder withFormatHandler(SoundCloudFormatHandler formatHandler) { - this.formatHandler = formatHandler; - return this; + private AudioItem loadSearchResultsFromResponse(HttpResponse response, String query) throws IOException { + try { + JsonBrowser searchResults = JsonBrowser.parse(response.getEntity().getContent()); + return extractTracksFromSearchResults(query, searchResults); + } finally { + EntityUtils.consumeQuietly(response.getEntity()); + } } - public Builder withPlaylistLoader(SoundCloudPlaylistLoader playlistLoader) { - this.playlistLoader = playlistLoader; - return this; + private URI buildSearchUri(String query, int offset, int limit) { + try { + return new URIBuilder("https://api-v2.soundcloud.com/search/tracks") + .addParameter("q", query) + .addParameter("offset", String.valueOf(offset)) + .addParameter("limit", String.valueOf(limit)) + .build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } - public Builder withPlaylistLoaderFactory(PlaylistLoaderFactory playlistLoaderFactory) { - this.playlistLoaderFactory = playlistLoaderFactory; - return this; + private AudioItem extractTracksFromSearchResults(String query, JsonBrowser searchResults) { + List tracks = new ArrayList<>(); + + for (JsonBrowser item : searchResults.get("collection").values()) { + if (!item.isNull()) { + tracks.add(loadFromTrackData(item)); + } + } + + return new BasicAudioPlaylist("Search results for: " + query, tracks, null, true); } - public SoundCloudAudioSourceManager build() { - SoundCloudDataReader usedDataReader = dataReader; + public static class Builder { + private boolean allowSearch = true; + private SoundCloudDataReader dataReader; + private SoundCloudDataLoader dataLoader; + private SoundCloudFormatHandler formatHandler; + private SoundCloudPlaylistLoader playlistLoader; + private PlaylistLoaderFactory playlistLoaderFactory; + + public Builder withAllowSearch(boolean allowSearch) { + this.allowSearch = allowSearch; + return this; + } - if (usedDataReader == null) { - usedDataReader = new DefaultSoundCloudDataReader(); - } + public Builder withDataReader(SoundCloudDataReader dataReader) { + this.dataReader = dataReader; + return this; + } - SoundCloudDataLoader usedDataLoader = dataLoader; + public Builder withDataLoader(SoundCloudDataLoader dataLoader) { + this.dataLoader = dataLoader; + return this; + } - if (usedDataLoader == null) { - usedDataLoader = new DefaultSoundCloudDataLoader(); - } + public Builder withFormatHandler(SoundCloudFormatHandler formatHandler) { + this.formatHandler = formatHandler; + return this; + } - SoundCloudFormatHandler usedFormatHandler = formatHandler; + public Builder withPlaylistLoader(SoundCloudPlaylistLoader playlistLoader) { + this.playlistLoader = playlistLoader; + return this; + } - if (usedFormatHandler == null) { - usedFormatHandler = new DefaultSoundCloudFormatHandler(); - } + public Builder withPlaylistLoaderFactory(PlaylistLoaderFactory playlistLoaderFactory) { + this.playlistLoaderFactory = playlistLoaderFactory; + return this; + } - SoundCloudPlaylistLoader usedPlaylistLoader = playlistLoader; + public SoundCloudAudioSourceManager build() { + SoundCloudDataReader usedDataReader = dataReader; - if (usedPlaylistLoader == null) { - PlaylistLoaderFactory factory = playlistLoaderFactory; + if (usedDataReader == null) { + usedDataReader = new DefaultSoundCloudDataReader(); + } - if (factory != null) { - usedPlaylistLoader = factory.create(usedDataReader, usedDataLoader, usedFormatHandler); + SoundCloudDataLoader usedDataLoader = dataLoader; + + if (usedDataLoader == null) { + usedDataLoader = new DefaultSoundCloudDataLoader(); + } + + SoundCloudFormatHandler usedFormatHandler = formatHandler; + + if (usedFormatHandler == null) { + usedFormatHandler = new DefaultSoundCloudFormatHandler(); + } + + SoundCloudPlaylistLoader usedPlaylistLoader = playlistLoader; + + if (usedPlaylistLoader == null) { + PlaylistLoaderFactory factory = playlistLoaderFactory; + + if (factory != null) { + usedPlaylistLoader = factory.create(usedDataReader, usedDataLoader, usedFormatHandler); + } + } + + if (usedPlaylistLoader == null) { + usedPlaylistLoader = new DefaultSoundCloudPlaylistLoader(usedDataLoader, usedDataReader, usedFormatHandler); + } + + return new SoundCloudAudioSourceManager( + allowSearch, + usedDataReader, + usedDataLoader, + usedFormatHandler, + usedPlaylistLoader + ); } - } - - if (usedPlaylistLoader == null) { - usedPlaylistLoader = new DefaultSoundCloudPlaylistLoader(usedDataLoader, usedDataReader, usedFormatHandler); - } - - return new SoundCloudAudioSourceManager( - allowSearch, - usedDataReader, - usedDataLoader, - usedFormatHandler, - usedPlaylistLoader - ); - } - @FunctionalInterface - interface PlaylistLoaderFactory { - SoundCloudPlaylistLoader create( - SoundCloudDataReader dataReader, - SoundCloudDataLoader dataLoader, - SoundCloudFormatHandler formatHandler - ); + @FunctionalInterface + interface PlaylistLoaderFactory { + SoundCloudPlaylistLoader create( + SoundCloudDataReader dataReader, + SoundCloudDataLoader dataLoader, + SoundCloudFormatHandler formatHandler + ); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudAudioTrack.java index 794a78ad8..d7e9abaf5 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudAudioTrack.java @@ -9,87 +9,88 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; import com.sedmelluq.discord.lavaplayer.track.DelegatedAudioTrack; import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor; -import java.io.IOException; -import java.net.URI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.net.URI; + /** * Audio track that handles processing SoundCloud tracks. */ public class SoundCloudAudioTrack extends DelegatedAudioTrack { - private static final Logger log = LoggerFactory.getLogger(SoundCloudAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(SoundCloudAudioTrack.class); - private final SoundCloudAudioSourceManager sourceManager; + private final SoundCloudAudioSourceManager sourceManager; - /** - * @param trackInfo Track info - * @param sourceManager Source manager which was used to find this track - */ - public SoundCloudAudioTrack(AudioTrackInfo trackInfo, SoundCloudAudioSourceManager sourceManager) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param sourceManager Source manager which was used to find this track + */ + public SoundCloudAudioTrack(AudioTrackInfo trackInfo, SoundCloudAudioSourceManager sourceManager) { + super(trackInfo); - this.sourceManager = sourceManager; - } - - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { - playFromIdentifier(httpInterface, trackInfo.identifier, false, localExecutor); - } - } - - private void playFromIdentifier( - HttpInterface httpInterface, - String identifier, - boolean recursion, - LocalAudioTrackExecutor localExecutor - ) throws Exception { - SoundCloudM3uInfo m3uInfo = sourceManager.getFormatHandler().getM3uInfo(identifier); - - if (m3uInfo != null) { - processDelegate(new SoundCloudM3uAudioTrack(trackInfo, httpInterface, m3uInfo), localExecutor); - return; + this.sourceManager = sourceManager; } - String mp3LookupUrl = sourceManager.getFormatHandler().getMp3LookupUrl(identifier); - - if (mp3LookupUrl != null) { - String playbackUrl = SoundCloudHelper.loadPlaybackUrl(httpInterface, identifier.substring(2)); - loadFromMp3Url(localExecutor, httpInterface, playbackUrl); - return; + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { + playFromIdentifier(httpInterface, trackInfo.identifier, false, localExecutor); + } } - if (!recursion) { - // Old "track ID" entry? Let's "load" it to get url. - AudioTrack track = sourceManager.loadTrack(trackInfo.uri); - playFromIdentifier(httpInterface, track.getIdentifier(), true, localExecutor); + private void playFromIdentifier( + HttpInterface httpInterface, + String identifier, + boolean recursion, + LocalAudioTrackExecutor localExecutor + ) throws Exception { + SoundCloudM3uInfo m3uInfo = sourceManager.getFormatHandler().getM3uInfo(identifier); + + if (m3uInfo != null) { + processDelegate(new SoundCloudM3uAudioTrack(trackInfo, httpInterface, m3uInfo), localExecutor); + return; + } + + String mp3LookupUrl = sourceManager.getFormatHandler().getMp3LookupUrl(identifier); + + if (mp3LookupUrl != null) { + String playbackUrl = SoundCloudHelper.loadPlaybackUrl(httpInterface, identifier.substring(2)); + loadFromMp3Url(localExecutor, httpInterface, playbackUrl); + return; + } + + if (!recursion) { + // Old "track ID" entry? Let's "load" it to get url. + AudioTrack track = sourceManager.loadTrack(trackInfo.uri); + playFromIdentifier(httpInterface, track.getIdentifier(), true, localExecutor); + } } - } - private void loadFromMp3Url( - LocalAudioTrackExecutor localExecutor, - HttpInterface httpInterface, - String trackUrl - ) throws Exception { - log.debug("Starting SoundCloud track from URL: {}", trackUrl); + private void loadFromMp3Url( + LocalAudioTrackExecutor localExecutor, + HttpInterface httpInterface, + String trackUrl + ) throws Exception { + log.debug("Starting SoundCloud track from URL: {}", trackUrl); - try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(trackUrl), null)) { - if (!HttpClientTools.isSuccessWithContent(stream.checkStatusCode())) { - throw new IOException("Invalid status code for soundcloud stream: " + stream.checkStatusCode()); - } + try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(trackUrl), null)) { + if (!HttpClientTools.isSuccessWithContent(stream.checkStatusCode())) { + throw new IOException("Invalid status code for soundcloud stream: " + stream.checkStatusCode()); + } - processDelegate(new Mp3AudioTrack(trackInfo, stream), localExecutor); + processDelegate(new Mp3AudioTrack(trackInfo, stream), localExecutor); + } } - } - @Override - protected AudioTrack makeShallowClone() { - return new SoundCloudAudioTrack(trackInfo, sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new SoundCloudAudioTrack(trackInfo, sourceManager); + } - @Override - public AudioSourceManager getSourceManager() { - return sourceManager; - } + @Override + public AudioSourceManager getSourceManager() { + return sourceManager; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudClientIdTracker.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudClientIdTracker.java index df67a5db4..a3311bcac 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudClientIdTracker.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudClientIdTracker.java @@ -3,10 +3,6 @@ import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager; -import java.io.IOException; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.protocol.HttpClientContext; @@ -14,113 +10,118 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public class SoundCloudClientIdTracker { - private static final Logger log = LoggerFactory.getLogger(SoundCloudClientIdTracker.class); - - private static final String ID_FETCH_CONTEXT_ATTRIBUTE = "sc-raw"; - private static final long CLIENT_ID_REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(1); - private static final String PAGE_APP_SCRIPT_REGEX = "https://[A-Za-z0-9-.]+/assets/[a-f0-9-]+\\.js"; - private static final String APP_SCRIPT_CLIENT_ID_REGEX = "[^_]client_id:\"([a-zA-Z0-9-_]+)\""; - - private static final Pattern pageAppScriptPattern = Pattern.compile(PAGE_APP_SCRIPT_REGEX); - private static final Pattern appScriptClientIdPattern = Pattern.compile(APP_SCRIPT_CLIENT_ID_REGEX); - - private final Object clientIdLock = new Object(); - private final HttpInterfaceManager httpInterfaceManager; - private String clientId; - private long lastClientIdUpdate; - - public SoundCloudClientIdTracker(HttpInterfaceManager httpInterfaceManager) { - this.httpInterfaceManager = httpInterfaceManager; - } - - /** - * Updates the clientID if more than {@link #CLIENT_ID_REFRESH_INTERVAL} time has passed since last updated. - */ - public void updateClientId() { - synchronized (clientIdLock) { - long now = System.currentTimeMillis(); - if (now - lastClientIdUpdate < CLIENT_ID_REFRESH_INTERVAL) { - log.debug("Client ID was recently updated, not updating again right away."); - return; - } - - lastClientIdUpdate = now; - log.info("Updating SoundCloud client ID (current is {}).", clientId); - - try { - clientId = findClientIdFromSite(); - log.info("Updating SoundCloud client ID succeeded, new ID is {}.", clientId); - } catch (Exception e) { - log.error("SoundCloud client ID update failed.", e); - } + private static final Logger log = LoggerFactory.getLogger(SoundCloudClientIdTracker.class); + + private static final String ID_FETCH_CONTEXT_ATTRIBUTE = "sc-raw"; + private static final long CLIENT_ID_REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(1); + private static final String PAGE_APP_SCRIPT_REGEX = "https://[A-Za-z0-9-.]+/assets/[a-f0-9-]+\\.js"; + private static final String APP_SCRIPT_CLIENT_ID_REGEX = "[^_]client_id:\"([a-zA-Z0-9-_]+)\""; + + private static final Pattern pageAppScriptPattern = Pattern.compile(PAGE_APP_SCRIPT_REGEX); + private static final Pattern appScriptClientIdPattern = Pattern.compile(APP_SCRIPT_CLIENT_ID_REGEX); + + private final Object clientIdLock = new Object(); + private final HttpInterfaceManager httpInterfaceManager; + private String clientId; + private long lastClientIdUpdate; + + public SoundCloudClientIdTracker(HttpInterfaceManager httpInterfaceManager) { + this.httpInterfaceManager = httpInterfaceManager; + } + + /** + * Updates the clientID if more than {@link #CLIENT_ID_REFRESH_INTERVAL} time has passed since last updated. + */ + public void updateClientId() { + synchronized (clientIdLock) { + long now = System.currentTimeMillis(); + if (now - lastClientIdUpdate < CLIENT_ID_REFRESH_INTERVAL) { + log.debug("Client ID was recently updated, not updating again right away."); + return; + } + + lastClientIdUpdate = now; + log.info("Updating SoundCloud client ID (current is {}).", clientId); + + try { + clientId = findClientIdFromSite(); + log.info("Updating SoundCloud client ID succeeded, new ID is {}.", clientId); + } catch (Exception e) { + log.error("SoundCloud client ID update failed.", e); + } + } } - } - public String getClientId() { - synchronized (clientIdLock) { - if (clientId == null) { - updateClientId(); - } + public String getClientId() { + synchronized (clientIdLock) { + if (clientId == null) { + updateClientId(); + } - return clientId; + return clientId; + } } - } - public boolean isIdFetchContext(HttpClientContext context) { - return context.getAttribute(ID_FETCH_CONTEXT_ATTRIBUTE) == Boolean.TRUE; - } + public boolean isIdFetchContext(HttpClientContext context) { + return context.getAttribute(ID_FETCH_CONTEXT_ATTRIBUTE) == Boolean.TRUE; + } - private String findClientIdFromSite() throws IOException { - try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { - httpInterface.getContext().setAttribute(ID_FETCH_CONTEXT_ATTRIBUTE, true); + private String findClientIdFromSite() throws IOException { + try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { + httpInterface.getContext().setAttribute(ID_FETCH_CONTEXT_ATTRIBUTE, true); - String scriptUrl = findApplicationScriptUrl(httpInterface); - return findClientIdFromApplicationScript(httpInterface, scriptUrl); + String scriptUrl = findApplicationScriptUrl(httpInterface); + return findClientIdFromApplicationScript(httpInterface, scriptUrl); + } } - } - - private String findApplicationScriptUrl(HttpInterface httpInterface) throws IOException { - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet("https://soundcloud.com"))) { - int statusCode = response.getStatusLine().getStatusCode(); - if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new IOException("Invalid status code for main page response: " + statusCode); - } - - String page = EntityUtils.toString(response.getEntity()); - Matcher scriptMatcher = pageAppScriptPattern.matcher(page); - String result = getLastMatchWithinLimit(scriptMatcher, 9); - - if (result != null) { - return result; - } else { - throw new IllegalStateException("Could not find application script from main page."); - } + + private String findApplicationScriptUrl(HttpInterface httpInterface) throws IOException { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet("https://soundcloud.com"))) { + int statusCode = response.getStatusLine().getStatusCode(); + if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new IOException("Invalid status code for main page response: " + statusCode); + } + + String page = EntityUtils.toString(response.getEntity()); + Matcher scriptMatcher = pageAppScriptPattern.matcher(page); + String result = getLastMatchWithinLimit(scriptMatcher, 9); + + if (result != null) { + return result; + } else { + throw new IllegalStateException("Could not find application script from main page."); + } + } } - } - - private String getLastMatchWithinLimit(Matcher m, int limit) { - String lastMatch = null; - for(int i = 0; m.find() && i < limit; ++i) - lastMatch = m.group(); - return lastMatch; - } - - private String findClientIdFromApplicationScript(HttpInterface httpInterface, String scriptUrl) throws IOException { - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(scriptUrl))) { - int statusCode = response.getStatusLine().getStatusCode(); - if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new IOException("Invalid status code for application script response: " + statusCode); - } - - String page = EntityUtils.toString(response.getEntity()); - Matcher clientIdMatcher = appScriptClientIdPattern.matcher(page); - - if (clientIdMatcher.find()) { - return clientIdMatcher.group(1); - } else { - throw new IllegalStateException("Could not find client ID from application script."); - } + + private String getLastMatchWithinLimit(Matcher m, int limit) { + String lastMatch = null; + for (int i = 0; m.find() && i < limit; ++i) + lastMatch = m.group(); + return lastMatch; + } + + private String findClientIdFromApplicationScript(HttpInterface httpInterface, String scriptUrl) throws IOException { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(scriptUrl))) { + int statusCode = response.getStatusLine().getStatusCode(); + if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new IOException("Invalid status code for application script response: " + statusCode); + } + + String page = EntityUtils.toString(response.getEntity()); + Matcher clientIdMatcher = appScriptClientIdPattern.matcher(page); + + if (clientIdMatcher.find()) { + return clientIdMatcher.group(1); + } else { + throw new IllegalStateException("Could not find client ID from application script."); + } + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudDataLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudDataLoader.java index 5e43f8cfb..7b0e1452b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudDataLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudDataLoader.java @@ -2,8 +2,9 @@ import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; + import java.io.IOException; public interface SoundCloudDataLoader { - JsonBrowser load(HttpInterface httpInterface, String url) throws IOException; + JsonBrowser load(HttpInterface httpInterface, String url) throws IOException; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudDataReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudDataReader.java index cd1cb4165..004751a1a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudDataReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudDataReader.java @@ -2,24 +2,25 @@ import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; + import java.util.List; public interface SoundCloudDataReader { - JsonBrowser findTrackData(JsonBrowser rootData); + JsonBrowser findTrackData(JsonBrowser rootData); - String readTrackId(JsonBrowser trackData); + String readTrackId(JsonBrowser trackData); - boolean isTrackBlocked(JsonBrowser trackData); + boolean isTrackBlocked(JsonBrowser trackData); - AudioTrackInfo readTrackInfo(JsonBrowser trackData, String identifier); + AudioTrackInfo readTrackInfo(JsonBrowser trackData, String identifier); - List readTrackFormats(JsonBrowser trackData); + List readTrackFormats(JsonBrowser trackData); - JsonBrowser findPlaylistData(JsonBrowser rootData, String kind); + JsonBrowser findPlaylistData(JsonBrowser rootData, String kind); - String readPlaylistName(JsonBrowser playlistData); + String readPlaylistName(JsonBrowser playlistData); - String readPlaylistIdentifier(JsonBrowser playlistData); + String readPlaylistIdentifier(JsonBrowser playlistData); - List readPlaylistTracks(JsonBrowser playlistData); + List readPlaylistTracks(JsonBrowser playlistData); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudFormatHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudFormatHandler.java index 71dd16ce9..3bc501369 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudFormatHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudFormatHandler.java @@ -3,11 +3,11 @@ import java.util.List; public interface SoundCloudFormatHandler { - SoundCloudTrackFormat chooseBestFormat(List formats); + SoundCloudTrackFormat chooseBestFormat(List formats); - String buildFormatIdentifier(SoundCloudTrackFormat format); + String buildFormatIdentifier(SoundCloudTrackFormat format); - SoundCloudM3uInfo getM3uInfo(String identifier); + SoundCloudM3uInfo getM3uInfo(String identifier); - String getMp3LookupUrl(String identifier); + String getMp3LookupUrl(String identifier); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudHelper.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudHelper.java index 8ea5fc64d..4277c1318 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudHelper.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudHelper.java @@ -21,53 +21,53 @@ import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS; public class SoundCloudHelper { - public static String nonMobileUrl(String url) { - if (url.startsWith("https://m.")) { - return "https://" + url.substring("https://m.".length()); - } else { - return url; + public static String nonMobileUrl(String url) { + if (url.startsWith("https://m.")) { + return "https://" + url.substring("https://m.".length()); + } else { + return url; + } } - } - public static String loadPlaybackUrl(HttpInterface httpInterface, String jsonUrl) throws IOException { - try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, URI.create(jsonUrl), null)) { - if (!HttpClientTools.isSuccessWithContent(stream.checkStatusCode())) { - throw new IOException("Invalid status code for soundcloud stream: " + stream.checkStatusCode()); - } + public static String loadPlaybackUrl(HttpInterface httpInterface, String jsonUrl) throws IOException { + try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, URI.create(jsonUrl), null)) { + if (!HttpClientTools.isSuccessWithContent(stream.checkStatusCode())) { + throw new IOException("Invalid status code for soundcloud stream: " + stream.checkStatusCode()); + } - JsonBrowser json = JsonBrowser.parse(stream); - return json.get("url").text(); + JsonBrowser json = JsonBrowser.parse(stream); + return json.get("url").text(); + } } - } - public static AudioReference redirectMobileLink(HttpInterface httpInterface, AudioReference reference) { - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(reference.identifier))) { - HttpClientTools.assertSuccessWithContent(response, "mobile redirect response"); - HttpClientContext context = httpInterface.getContext(); - List redirects = context.getRedirectLocations(); - if (redirects != null && !redirects.isEmpty()) { - return new AudioReference(redirects.get(0).toString(), null); - } else { - throw new FriendlyException("Unable to process soundcloud mobile link", SUSPICIOUS, - new IllegalStateException("Expected soundcloud to redirect soundcloud.app.goo.gl link to a valid track/playlist link, but it did not redirect at all")); - } - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions(e); + public static AudioReference redirectMobileLink(HttpInterface httpInterface, AudioReference reference) { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(reference.identifier))) { + HttpClientTools.assertSuccessWithContent(response, "mobile redirect response"); + HttpClientContext context = httpInterface.getContext(); + List redirects = context.getRedirectLocations(); + if (redirects != null && !redirects.isEmpty()) { + return new AudioReference(redirects.get(0).toString(), null); + } else { + throw new FriendlyException("Unable to process soundcloud mobile link", SUSPICIOUS, + new IllegalStateException("Expected soundcloud to redirect soundcloud.app.goo.gl link to a valid track/playlist link, but it did not redirect at all")); + } + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions(e); + } } - } - public static AudioReference resolveShortTrackUrl(HttpInterface httpInterface, AudioReference reference) { - HttpHead request = new HttpHead(reference.identifier); - request.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build()); - try (CloseableHttpResponse response = httpInterface.execute(request)) { - Header header = response.getLastHeader("Location"); - if (header == null) { - throw new FriendlyException("Unable to resolve Soundcloud short URL", SUSPICIOUS, - new IllegalStateException("Unable to locate canonical URL")); - } - return new AudioReference(header.getValue(), null); - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions(e); + public static AudioReference resolveShortTrackUrl(HttpInterface httpInterface, AudioReference reference) { + HttpHead request = new HttpHead(reference.identifier); + request.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build()); + try (CloseableHttpResponse response = httpInterface.execute(request)) { + Header header = response.getLastHeader("Location"); + if (header == null) { + throw new FriendlyException("Unable to resolve Soundcloud short URL", SUSPICIOUS, + new IllegalStateException("Unable to locate canonical URL")); + } + return new AudioReference(header.getValue(), null); + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions(e); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudHttpContextFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudHttpContextFilter.java index 2e1da2663..1da088141 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudHttpContextFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudHttpContextFilter.java @@ -2,8 +2,6 @@ import com.sedmelluq.discord.lavaplayer.tools.http.HttpContextFilter; import com.sedmelluq.discord.lavaplayer.tools.http.HttpContextRetryCounter; -import java.net.URI; -import java.net.URISyntaxException; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpRequestBase; @@ -11,69 +9,72 @@ import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.client.utils.URIBuilder; +import java.net.URI; +import java.net.URISyntaxException; + public class SoundCloudHttpContextFilter implements HttpContextFilter { - private static final HttpContextRetryCounter retryCounter = new HttpContextRetryCounter("sc-id-retry"); + private static final HttpContextRetryCounter retryCounter = new HttpContextRetryCounter("sc-id-retry"); - private final SoundCloudClientIdTracker clientIdTracker; + private final SoundCloudClientIdTracker clientIdTracker; - public SoundCloudHttpContextFilter(SoundCloudClientIdTracker clientIdTracker) { - this.clientIdTracker = clientIdTracker; - } + public SoundCloudHttpContextFilter(SoundCloudClientIdTracker clientIdTracker) { + this.clientIdTracker = clientIdTracker; + } - @Override - public void onContextOpen(HttpClientContext context) { + @Override + public void onContextOpen(HttpClientContext context) { - } + } - @Override - public void onContextClose(HttpClientContext context) { + @Override + public void onContextClose(HttpClientContext context) { - } + } - @Override - public void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition) { - request.setHeader("user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + + @Override + public void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition) { + request.setHeader("user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/76.0.3809.100 Safari/537.36"); - retryCounter.handleUpdate(context, isRepetition); + retryCounter.handleUpdate(context, isRepetition); - if (clientIdTracker.isIdFetchContext(context)) { - // Used for fetching client ID, let's not recurse. - return; - } else if (request.getURI().getHost().contains("sndcdn.com")) { - // CDN urls do not require client ID (it actually breaks them) - return; - } + if (clientIdTracker.isIdFetchContext(context)) { + // Used for fetching client ID, let's not recurse. + return; + } else if (request.getURI().getHost().contains("sndcdn.com")) { + // CDN urls do not require client ID (it actually breaks them) + return; + } - try { - URI uri = new URIBuilder(request.getURI()) - .setParameter("client_id", clientIdTracker.getClientId()) - .build(); - - if (request instanceof HttpRequestBase) { - ((HttpRequestBase) request).setURI(uri); - } else { - throw new IllegalStateException("Cannot update request URI."); - } - } catch (URISyntaxException e) { - throw new RuntimeException(e); + try { + URI uri = new URIBuilder(request.getURI()) + .setParameter("client_id", clientIdTracker.getClientId()) + .build(); + + if (request instanceof HttpRequestBase) { + ((HttpRequestBase) request).setURI(uri); + } else { + throw new IllegalStateException("Cannot update request URI."); + } + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } - } - - @Override - public boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response) { - if (clientIdTracker.isIdFetchContext(context) || retryCounter.getRetryCount(context) >= 1) { - return false; - } else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { - clientIdTracker.updateClientId(); - return true; - } else { - return false; + + @Override + public boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response) { + if (clientIdTracker.isIdFetchContext(context) || retryCounter.getRetryCount(context) >= 1) { + return false; + } else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { + clientIdTracker.updateClientId(); + return true; + } else { + return false; + } } - } - @Override - public boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error) { - return false; - } + @Override + public boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error) { + return false; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudM3uAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudM3uAudioTrack.java index 27a539a9f..21bdb3cf6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudM3uAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudM3uAudioTrack.java @@ -10,174 +10,175 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; import com.sedmelluq.discord.lavaplayer.track.DelegatedAudioTrack; import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor; +import org.apache.http.client.methods.HttpGet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; -import org.apache.http.client.methods.HttpGet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class SoundCloudM3uAudioTrack extends DelegatedAudioTrack { - private static final Logger log = LoggerFactory.getLogger(SoundCloudM3uAudioTrack.class); - - private static final long SEGMENT_UPDATE_INTERVAL = TimeUnit.MINUTES.toMillis(10); + private static final Logger log = LoggerFactory.getLogger(SoundCloudM3uAudioTrack.class); - private final HttpInterface httpInterface; - private final SoundCloudM3uInfo m3uInfo; + private static final long SEGMENT_UPDATE_INTERVAL = TimeUnit.MINUTES.toMillis(10); - public SoundCloudM3uAudioTrack(AudioTrackInfo trackInfo, HttpInterface httpInterface, SoundCloudM3uInfo m3uInfo) { - super(trackInfo); - this.httpInterface = httpInterface; - this.m3uInfo = m3uInfo; - } + private final HttpInterface httpInterface; + private final SoundCloudM3uInfo m3uInfo; - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - try (SegmentTracker segmentTracker = createSegmentTracker()) { - segmentTracker.decoder.prepareStream(true); - - localExecutor.executeProcessingLoop(() -> segmentTracker.decoder.playStream( - localExecutor.getProcessingContext(), - segmentTracker.streamStartPosition, - segmentTracker.desiredPosition - ), segmentTracker::seekToTimecode, true); - } - } - - private List loadSegments() throws IOException { - String playbackUrl = SoundCloudHelper.loadPlaybackUrl(httpInterface, m3uInfo.lookupUrl); - return HlsStreamSegmentParser.parseFromUrl(httpInterface, playbackUrl); - } - - private SegmentTracker createSegmentTracker() throws IOException { - List initialSegments = loadSegments(); - SegmentTracker tracker = new SegmentTracker(initialSegments); - tracker.setupDecoder(m3uInfo.decoderFactory); - return tracker; - } - - private class SegmentTracker implements AutoCloseable { - private final List segments; - private long desiredPosition = 0; - private long streamStartPosition = 0; - private long lastUpdate; - private SoundCloudSegmentDecoder decoder; - private int segmentIndex = 0; - - private SegmentTracker(List segments) { - this.segments = segments; - this.lastUpdate = System.currentTimeMillis(); + public SoundCloudM3uAudioTrack(AudioTrackInfo trackInfo, HttpInterface httpInterface, SoundCloudM3uInfo m3uInfo) { + super(trackInfo); + this.httpInterface = httpInterface; + this.m3uInfo = m3uInfo; } - private void setupDecoder(SoundCloudSegmentDecoder.Factory factory) { - decoder = factory.create(this::createChainedStream); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + try (SegmentTracker segmentTracker = createSegmentTracker()) { + segmentTracker.decoder.prepareStream(true); + + localExecutor.executeProcessingLoop(() -> segmentTracker.decoder.playStream( + localExecutor.getProcessingContext(), + segmentTracker.streamStartPosition, + segmentTracker.desiredPosition + ), segmentTracker::seekToTimecode, true); + } } - private SeekableInputStream createChainedStream() { - return new NonSeekableInputStream(new ChainedInputStream(this::getNextStream)); + private List loadSegments() throws IOException { + String playbackUrl = SoundCloudHelper.loadPlaybackUrl(httpInterface, m3uInfo.lookupUrl); + return HlsStreamSegmentParser.parseFromUrl(httpInterface, playbackUrl); } - private void seekToTimecode(long timecode) throws IOException { - long segmentTimecode = 0; - - for (int i = 0; i < segments.size(); i++) { - Long duration = segments.get(i).duration; + private SegmentTracker createSegmentTracker() throws IOException { + List initialSegments = loadSegments(); + SegmentTracker tracker = new SegmentTracker(initialSegments); + tracker.setupDecoder(m3uInfo.decoderFactory); + return tracker; + } - if (duration == null) { - break; + private class SegmentTracker implements AutoCloseable { + private final List segments; + private long desiredPosition = 0; + private long streamStartPosition = 0; + private long lastUpdate; + private SoundCloudSegmentDecoder decoder; + private int segmentIndex = 0; + + private SegmentTracker(List segments) { + this.segments = segments; + this.lastUpdate = System.currentTimeMillis(); } - long nextTimecode = segmentTimecode + duration; + private void setupDecoder(SoundCloudSegmentDecoder.Factory factory) { + decoder = factory.create(this::createChainedStream); + } - if (timecode >= segmentTimecode && timecode < nextTimecode) { - seekToSegment(i, timecode, segmentTimecode); - return; + private SeekableInputStream createChainedStream() { + return new NonSeekableInputStream(new ChainedInputStream(this::getNextStream)); } - segmentTimecode = nextTimecode; - } + private void seekToTimecode(long timecode) throws IOException { + long segmentTimecode = 0; - seekToEnd(); - } + for (int i = 0; i < segments.size(); i++) { + Long duration = segments.get(i).duration; - private void seekToSegment(int index, long requestedTimecode, long segmentTimecode) throws IOException { - decoder.resetStream(); + if (duration == null) { + break; + } - segmentIndex = index; - desiredPosition = requestedTimecode; - streamStartPosition = segmentTimecode; + long nextTimecode = segmentTimecode + duration; - decoder.prepareStream(streamStartPosition == 0); - } + if (timecode >= segmentTimecode && timecode < nextTimecode) { + seekToSegment(i, timecode, segmentTimecode); + return; + } - private void seekToEnd() throws IOException { - decoder.resetStream(); + segmentTimecode = nextTimecode; + } - segmentIndex = segments.size(); - } + seekToEnd(); + } - private InputStream getNextStream() { - HlsStreamSegment segment = getNextSegment(); + private void seekToSegment(int index, long requestedTimecode, long segmentTimecode) throws IOException { + decoder.resetStream(); - if (segment == null) { - return null; - } + segmentIndex = index; + desiredPosition = requestedTimecode; + streamStartPosition = segmentTimecode; - return HttpStreamTools.streamContent(httpInterface, new HttpGet(segment.url)); - } + decoder.prepareStream(streamStartPosition == 0); + } - private void updateSegmentList() { - try { - List newSegments = loadSegments(); + private void seekToEnd() throws IOException { + decoder.resetStream(); - if (newSegments.size() != segments.size()) { - log.error("For {}, received different number of segments on update, skipping.", trackInfo.identifier); - return; + segmentIndex = segments.size(); } - for (int i = 0; i < segments.size(); i++) { - if (!Objects.equals(newSegments.get(i).duration, segments.get(i).duration)) { - log.error("For {}, segment {} has different length than previously on update.", trackInfo.identifier, i); - return; - } + private InputStream getNextStream() { + HlsStreamSegment segment = getNextSegment(); + + if (segment == null) { + return null; + } + + return HttpStreamTools.streamContent(httpInterface, new HttpGet(segment.url)); } - for (int i = 0; i < segments.size(); i++) { - segments.set(i, newSegments.get(i)); + private void updateSegmentList() { + try { + List newSegments = loadSegments(); + + if (newSegments.size() != segments.size()) { + log.error("For {}, received different number of segments on update, skipping.", trackInfo.identifier); + return; + } + + for (int i = 0; i < segments.size(); i++) { + if (!Objects.equals(newSegments.get(i).duration, segments.get(i).duration)) { + log.error("For {}, segment {} has different length than previously on update.", trackInfo.identifier, i); + return; + } + } + + for (int i = 0; i < segments.size(); i++) { + segments.set(i, newSegments.get(i)); + } + } catch (Exception e) { + log.error("For {}, failed to update segment list, skipping.", trackInfo.identifier, e); + } } - } catch (Exception e) { - log.error("For {}, failed to update segment list, skipping.", trackInfo.identifier, e); - } - } - private void checkSegmentListUpdate() { - long now = System.currentTimeMillis(); - long delta = now - lastUpdate; + private void checkSegmentListUpdate() { + long now = System.currentTimeMillis(); + long delta = now - lastUpdate; - if (delta > SEGMENT_UPDATE_INTERVAL) { - log.debug("For {}, {}ms has passed since last segment update, updating", trackInfo.identifier, delta); + if (delta > SEGMENT_UPDATE_INTERVAL) { + log.debug("For {}, {}ms has passed since last segment update, updating", trackInfo.identifier, delta); - updateSegmentList(); - lastUpdate = now; - } - } + updateSegmentList(); + lastUpdate = now; + } + } - private HlsStreamSegment getNextSegment() { - int current = segmentIndex++; + private HlsStreamSegment getNextSegment() { + int current = segmentIndex++; - if (current < segments.size()) { - checkSegmentListUpdate(); - return segments.get(current); - } else { - return null; - } - } + if (current < segments.size()) { + checkSegmentListUpdate(); + return segments.get(current); + } else { + return null; + } + } - @Override - public void close() throws Exception { - decoder.resetStream(); + @Override + public void close() throws Exception { + decoder.resetStream(); + } } - } -} \ No newline at end of file +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudM3uInfo.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudM3uInfo.java index 5f75ed3b3..3f4b4ac33 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudM3uInfo.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudM3uInfo.java @@ -1,11 +1,11 @@ package com.sedmelluq.discord.lavaplayer.source.soundcloud; public class SoundCloudM3uInfo { - public final String lookupUrl; - public final SoundCloudSegmentDecoder.Factory decoderFactory; + public final String lookupUrl; + public final SoundCloudSegmentDecoder.Factory decoderFactory; - public SoundCloudM3uInfo(String lookupUrl, SoundCloudSegmentDecoder.Factory decoderFactory) { - this.lookupUrl = lookupUrl; - this.decoderFactory = decoderFactory; - } -} \ No newline at end of file + public SoundCloudM3uInfo(String lookupUrl, SoundCloudSegmentDecoder.Factory decoderFactory) { + this.lookupUrl = lookupUrl; + this.decoderFactory = decoderFactory; + } +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudMp3SegmentDecoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudMp3SegmentDecoder.java index 9deb389bc..62f66a981 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudMp3SegmentDecoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudMp3SegmentDecoder.java @@ -3,46 +3,47 @@ import com.sedmelluq.discord.lavaplayer.container.mp3.Mp3TrackProvider; import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; import com.sedmelluq.discord.lavaplayer.track.playback.AudioProcessingContext; + import java.io.IOException; import java.util.function.Supplier; public class SoundCloudMp3SegmentDecoder implements SoundCloudSegmentDecoder { - private final Supplier nextStreamProvider; - - public SoundCloudMp3SegmentDecoder(Supplier nextStreamProvider) { - this.nextStreamProvider = nextStreamProvider; - } - - @Override - public void prepareStream(boolean beginning) { - // Nothing to do. - } - - @Override - public void resetStream() { - // Nothing to do. - } - - @Override - public void playStream( - AudioProcessingContext context, - long startPosition, - long desiredPosition - ) throws InterruptedException, IOException { - try (SeekableInputStream stream = nextStreamProvider.get()) { - Mp3TrackProvider trackProvider = new Mp3TrackProvider(context, stream); - - try { - trackProvider.parseHeaders(); - trackProvider.provideFrames(); - } finally { - trackProvider.close(); - } + private final Supplier nextStreamProvider; + + public SoundCloudMp3SegmentDecoder(Supplier nextStreamProvider) { + this.nextStreamProvider = nextStreamProvider; } - } - @Override - public void close() { - // Nothing to do. - } -} \ No newline at end of file + @Override + public void prepareStream(boolean beginning) { + // Nothing to do. + } + + @Override + public void resetStream() { + // Nothing to do. + } + + @Override + public void playStream( + AudioProcessingContext context, + long startPosition, + long desiredPosition + ) throws InterruptedException, IOException { + try (SeekableInputStream stream = nextStreamProvider.get()) { + Mp3TrackProvider trackProvider = new Mp3TrackProvider(context, stream); + + try { + trackProvider.parseHeaders(); + trackProvider.provideFrames(); + } finally { + trackProvider.close(); + } + } + } + + @Override + public void close() { + // Nothing to do. + } +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudOpusSegmentDecoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudOpusSegmentDecoder.java index d604b5e0c..e3205988d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudOpusSegmentDecoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudOpusSegmentDecoder.java @@ -6,72 +6,73 @@ import com.sedmelluq.discord.lavaplayer.container.ogg.OggTrackLoader; import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; import com.sedmelluq.discord.lavaplayer.track.playback.AudioProcessingContext; + import java.io.IOException; import java.util.function.Supplier; public class SoundCloudOpusSegmentDecoder implements SoundCloudSegmentDecoder { - private final Supplier nextStreamProvider; - private OggPacketInputStream lastJoinedStream; - private OggTrackBlueprint blueprint; + private final Supplier nextStreamProvider; + private OggPacketInputStream lastJoinedStream; + private OggTrackBlueprint blueprint; - public SoundCloudOpusSegmentDecoder(Supplier nextStreamProvider) { - this.nextStreamProvider = nextStreamProvider; - } + public SoundCloudOpusSegmentDecoder(Supplier nextStreamProvider) { + this.nextStreamProvider = nextStreamProvider; + } - @Override - public void prepareStream(boolean beginning) throws IOException { - OggPacketInputStream stream = obtainStream(); + @Override + public void prepareStream(boolean beginning) throws IOException { + OggPacketInputStream stream = obtainStream(); - if (beginning) { - OggTrackBlueprint newBlueprint = OggTrackLoader.loadTrackBlueprint(stream); + if (beginning) { + OggTrackBlueprint newBlueprint = OggTrackLoader.loadTrackBlueprint(stream); - if (blueprint == null) { - if (newBlueprint == null) { - throw new IOException("No OGG track detected in the stream."); - } + if (blueprint == null) { + if (newBlueprint == null) { + throw new IOException("No OGG track detected in the stream."); + } - blueprint = newBlueprint; - } - } else { - stream.startNewTrack(); + blueprint = newBlueprint; + } + } else { + stream.startNewTrack(); + } } - } - @Override - public void resetStream() throws IOException { - if (lastJoinedStream != null) { - lastJoinedStream.close(); - lastJoinedStream = null; + @Override + public void resetStream() throws IOException { + if (lastJoinedStream != null) { + lastJoinedStream.close(); + lastJoinedStream = null; + } } - } - @Override - public void playStream( - AudioProcessingContext context, - long startPosition, - long desiredPosition - ) throws InterruptedException, IOException { - try (OggTrackHandler handler = blueprint.loadTrackHandler(obtainStream())) { - handler.initialise( - context, - startPosition, - desiredPosition - ); + @Override + public void playStream( + AudioProcessingContext context, + long startPosition, + long desiredPosition + ) throws InterruptedException, IOException { + try (OggTrackHandler handler = blueprint.loadTrackHandler(obtainStream())) { + handler.initialise( + context, + startPosition, + desiredPosition + ); - handler.provideFrames(); + handler.provideFrames(); + } } - } - - @Override - public void close() throws Exception { - resetStream(); - } - private OggPacketInputStream obtainStream() { - if (lastJoinedStream == null) { - lastJoinedStream = new OggPacketInputStream(nextStreamProvider.get(), true); + @Override + public void close() throws Exception { + resetStream(); } - return lastJoinedStream; - } -} \ No newline at end of file + private OggPacketInputStream obtainStream() { + if (lastJoinedStream == null) { + lastJoinedStream = new OggPacketInputStream(nextStreamProvider.get(), true); + } + + return lastJoinedStream; + } +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudPlaylistLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudPlaylistLoader.java index 2d9e49e56..a425b49db 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudPlaylistLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudPlaylistLoader.java @@ -4,12 +4,13 @@ import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; + import java.util.function.Function; public interface SoundCloudPlaylistLoader { - AudioPlaylist load( - String identifier, - HttpInterfaceManager httpInterfaceManager, - Function trackFactory - ); + AudioPlaylist load( + String identifier, + HttpInterfaceManager httpInterfaceManager, + Function trackFactory + ); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudSegmentDecoder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudSegmentDecoder.java index 35aa442fe..92076b4b2 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudSegmentDecoder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudSegmentDecoder.java @@ -2,21 +2,22 @@ import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream; import com.sedmelluq.discord.lavaplayer.track.playback.AudioProcessingContext; + import java.io.IOException; import java.util.function.Supplier; public interface SoundCloudSegmentDecoder extends AutoCloseable { - void prepareStream(boolean beginning) throws IOException; + void prepareStream(boolean beginning) throws IOException; - void resetStream() throws IOException; + void resetStream() throws IOException; - void playStream( - AudioProcessingContext context, - long startPosition, - long desiredPosition - ) throws InterruptedException, IOException; + void playStream( + AudioProcessingContext context, + long startPosition, + long desiredPosition + ) throws InterruptedException, IOException; - interface Factory { - SoundCloudSegmentDecoder create(Supplier nextStreamProvider); - } -} \ No newline at end of file + interface Factory { + SoundCloudSegmentDecoder create(Supplier nextStreamProvider); + } +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudTrackFormat.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudTrackFormat.java index 74caf3b6c..afdb1a868 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudTrackFormat.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/soundcloud/SoundCloudTrackFormat.java @@ -1,11 +1,11 @@ package com.sedmelluq.discord.lavaplayer.source.soundcloud; public interface SoundCloudTrackFormat { - String getTrackId(); + String getTrackId(); - String getProtocol(); + String getProtocol(); - String getMimeType(); + String getMimeType(); - String getLookupUrl(); + String getLookupUrl(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/stream/M3uStreamAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/stream/M3uStreamAudioTrack.java index b47ad248f..1fb75ce97 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/stream/M3uStreamAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/stream/M3uStreamAudioTrack.java @@ -12,28 +12,28 @@ * Audio track that handles processing M3U segment streams which using MPEG-TS wrapped ADTS codec. */ public abstract class M3uStreamAudioTrack extends DelegatedAudioTrack { - /** - * @param trackInfo Track info - */ - public M3uStreamAudioTrack(AudioTrackInfo trackInfo) { - super(trackInfo); - } + /** + * @param trackInfo Track info + */ + public M3uStreamAudioTrack(AudioTrackInfo trackInfo) { + super(trackInfo); + } - protected abstract M3uStreamSegmentUrlProvider getSegmentUrlProvider(); + protected abstract M3uStreamSegmentUrlProvider getSegmentUrlProvider(); - protected abstract HttpInterface getHttpInterface(); + protected abstract HttpInterface getHttpInterface(); - protected abstract void processJoinedStream( - LocalAudioTrackExecutor localExecutor, - InputStream stream - ) throws Exception; + protected abstract void processJoinedStream( + LocalAudioTrackExecutor localExecutor, + InputStream stream + ) throws Exception; - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - try (final HttpInterface httpInterface = getHttpInterface()) { - try (ChainedInputStream chainedInputStream = new ChainedInputStream(() -> getSegmentUrlProvider().getNextSegmentStream(httpInterface))) { - processJoinedStream(localExecutor, chainedInputStream); - } + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + try (final HttpInterface httpInterface = getHttpInterface()) { + try (ChainedInputStream chainedInputStream = new ChainedInputStream(() -> getSegmentUrlProvider().getNextSegmentStream(httpInterface))) { + processJoinedStream(localExecutor, chainedInputStream); + } + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/stream/M3uStreamSegmentUrlProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/stream/M3uStreamSegmentUrlProvider.java index ca93a9f51..e80843ea0 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/stream/M3uStreamSegmentUrlProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/stream/M3uStreamSegmentUrlProvider.java @@ -26,219 +26,219 @@ * {@link M3uStreamSegmentUrlProvider#getNextSegmentStream}. */ public abstract class M3uStreamSegmentUrlProvider { - private static final long SEGMENT_WAIT_STEP_MS = 200; - private static final RequestConfig streamingRequestConfig = RequestConfig.custom().setSocketTimeout(5000).setConnectionRequestTimeout(5000).setConnectTimeout(5000).build(); - - protected SegmentInfo lastSegment; - - protected static String createSegmentUrl(String playlistUrl, String segmentName) { - return URI.create(playlistUrl).resolve(segmentName).toString(); - } - - /** - * If applicable, extracts the quality information from the M3U directive which describes one stream in the root M3U. - * - * @param directiveLine Directive line with arguments. - * @return The quality name extracted from the directive line. - */ - protected abstract String getQualityFromM3uDirective(ExtendedM3uParser.Line directiveLine); - - protected abstract String fetchSegmentPlaylistUrl(HttpInterface httpInterface) throws IOException; - - /** - * Logic for getting the URL for the next segment. - * - * @param httpInterface HTTP interface to use for any requests required to perform to find the segment URL. - * @return The direct stream URL of the next segment. - */ - protected String getNextSegmentUrl(HttpInterface httpInterface) { - try { - String streamSegmentPlaylistUrl = fetchSegmentPlaylistUrl(httpInterface); - if (streamSegmentPlaylistUrl == null) { - return null; - } - - long startTime = System.currentTimeMillis(); - SegmentInfo nextSegment; - - while (true) { - List segments = loadStreamSegmentsList(httpInterface, streamSegmentPlaylistUrl); - nextSegment = chooseNextSegment(segments, lastSegment); - - if (nextSegment != null || !shouldWaitForSegment(startTime, segments)) { - break; - } - - Thread.sleep(SEGMENT_WAIT_STEP_MS); - } - - if (nextSegment == null) { - return null; - } + private static final long SEGMENT_WAIT_STEP_MS = 200; + private static final RequestConfig streamingRequestConfig = RequestConfig.custom().setSocketTimeout(5000).setConnectionRequestTimeout(5000).setConnectTimeout(5000).build(); - lastSegment = nextSegment; - return createSegmentUrl(streamSegmentPlaylistUrl, lastSegment.url); - } catch (IOException e) { - throw new FriendlyException("Failed to get next part of the stream.", SUSPICIOUS, e); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - /** - * Fetches the input stream for the next segment in the M3U stream. - * - * @param httpInterface HTTP interface to use for any requests required to perform to find the segment URL. - * @return Input stream of the next segment. - */ - public InputStream getNextSegmentStream(HttpInterface httpInterface) { - httpInterface.getContext().setRequestConfig(streamingRequestConfig); - String url = getNextSegmentUrl(httpInterface); - if (url == null) { - return null; - } + protected SegmentInfo lastSegment; - CloseableHttpResponse response = null; - boolean success = false; - - try { - response = httpInterface.execute(createSegmentGetRequest(url)); - int statusCode = response.getStatusLine().getStatusCode(); - - if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new IOException("Invalid status code from segment data URL: " + statusCode); - } - - success = true; - return response.getEntity().getContent(); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - if (response != null && !success) { - ExceptionTools.closeWithWarnings(response); - } + protected static String createSegmentUrl(String playlistUrl, String segmentName) { + return URI.create(playlistUrl).resolve(segmentName).toString(); } - } - protected abstract HttpUriRequest createSegmentGetRequest(String url); - - protected List loadChannelStreamsList(String[] lines) { - ExtendedM3uParser.Line streamInfoLine = null; + /** + * If applicable, extracts the quality information from the M3U directive which describes one stream in the root M3U. + * + * @param directiveLine Directive line with arguments. + * @return The quality name extracted from the directive line. + */ + protected abstract String getQualityFromM3uDirective(ExtendedM3uParser.Line directiveLine); - List streams = new ArrayList<>(); + protected abstract String fetchSegmentPlaylistUrl(HttpInterface httpInterface) throws IOException; - for (String lineText : lines) { - ExtendedM3uParser.Line line = ExtendedM3uParser.parseLine(lineText); + /** + * Logic for getting the URL for the next segment. + * + * @param httpInterface HTTP interface to use for any requests required to perform to find the segment URL. + * @return The direct stream URL of the next segment. + */ + protected String getNextSegmentUrl(HttpInterface httpInterface) { + try { + String streamSegmentPlaylistUrl = fetchSegmentPlaylistUrl(httpInterface); + if (streamSegmentPlaylistUrl == null) { + return null; + } + + long startTime = System.currentTimeMillis(); + SegmentInfo nextSegment; + + while (true) { + List segments = loadStreamSegmentsList(httpInterface, streamSegmentPlaylistUrl); + nextSegment = chooseNextSegment(segments, lastSegment); + + if (nextSegment != null || !shouldWaitForSegment(startTime, segments)) { + break; + } + + Thread.sleep(SEGMENT_WAIT_STEP_MS); + } + + if (nextSegment == null) { + return null; + } + + lastSegment = nextSegment; + return createSegmentUrl(streamSegmentPlaylistUrl, lastSegment.url); + } catch (IOException e) { + throw new FriendlyException("Failed to get next part of the stream.", SUSPICIOUS, e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } - if (line.isData() && streamInfoLine != null) { - String quality = getQualityFromM3uDirective(streamInfoLine); - if (quality != null) { - streams.add(new ChannelStreamInfo(quality, line.lineData)); + /** + * Fetches the input stream for the next segment in the M3U stream. + * + * @param httpInterface HTTP interface to use for any requests required to perform to find the segment URL. + * @return Input stream of the next segment. + */ + public InputStream getNextSegmentStream(HttpInterface httpInterface) { + httpInterface.getContext().setRequestConfig(streamingRequestConfig); + String url = getNextSegmentUrl(httpInterface); + if (url == null) { + return null; } - streamInfoLine = null; - } else if (line.isDirective() && "EXT-X-STREAM-INF".equals(line.directiveName)) { - streamInfoLine = line; - } + CloseableHttpResponse response = null; + boolean success = false; + + try { + response = httpInterface.execute(createSegmentGetRequest(url)); + int statusCode = response.getStatusLine().getStatusCode(); + + if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new IOException("Invalid status code from segment data URL: " + statusCode); + } + + success = true; + return response.getEntity().getContent(); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + if (response != null && !success) { + ExceptionTools.closeWithWarnings(response); + } + } } - return streams; - } + protected abstract HttpUriRequest createSegmentGetRequest(String url); - protected List loadStreamSegmentsList(HttpInterface httpInterface, String streamSegmentPlaylistUrl) throws IOException { - List segments = new ArrayList<>(); - ExtendedM3uParser.Line segmentInfo = null; + protected List loadChannelStreamsList(String[] lines) { + ExtendedM3uParser.Line streamInfoLine = null; - for (String lineText : fetchResponseLines(httpInterface, new HttpGet(streamSegmentPlaylistUrl), "stream segments list")) { - ExtendedM3uParser.Line line = ExtendedM3uParser.parseLine(lineText); + List streams = new ArrayList<>(); - if (line.isDirective() && "EXTINF".equals(line.directiveName)) { - segmentInfo = line; - } + for (String lineText : lines) { + ExtendedM3uParser.Line line = ExtendedM3uParser.parseLine(lineText); - if (line.isData()) { - if (segmentInfo != null && segmentInfo.extraData.contains(",")) { - String[] fields = segmentInfo.extraData.split(",", 2); - segments.add(new SegmentInfo(line.lineData, parseSecondDuration(fields[0]), fields[1])); - } else { - segments.add(new SegmentInfo(line.lineData, null, null)); - } - } - } + if (line.isData() && streamInfoLine != null) { + String quality = getQualityFromM3uDirective(streamInfoLine); + if (quality != null) { + streams.add(new ChannelStreamInfo(quality, line.lineData)); + } - return segments; - } + streamInfoLine = null; + } else if (line.isDirective() && "EXT-X-STREAM-INF".equals(line.directiveName)) { + streamInfoLine = line; + } + } - private static Long parseSecondDuration(String value) { - try { - double asDouble = Double.parseDouble(value); - return (long) (asDouble * 1000.0); - } catch (NumberFormatException ignored) { - return null; + return streams; } - } - protected SegmentInfo chooseNextSegment(List segments, SegmentInfo lastSegment) { - SegmentInfo selected = null; + protected List loadStreamSegmentsList(HttpInterface httpInterface, String streamSegmentPlaylistUrl) throws IOException { + List segments = new ArrayList<>(); + ExtendedM3uParser.Line segmentInfo = null; + + for (String lineText : fetchResponseLines(httpInterface, new HttpGet(streamSegmentPlaylistUrl), "stream segments list")) { + ExtendedM3uParser.Line line = ExtendedM3uParser.parseLine(lineText); + + if (line.isDirective() && "EXTINF".equals(line.directiveName)) { + segmentInfo = line; + } + + if (line.isData()) { + if (segmentInfo != null && segmentInfo.extraData.contains(",")) { + String[] fields = segmentInfo.extraData.split(",", 2); + segments.add(new SegmentInfo(line.lineData, parseSecondDuration(fields[0]), fields[1])); + } else { + segments.add(new SegmentInfo(line.lineData, null, null)); + } + } + } - for (int i = segments.size() - 1; i >= 0; i--) { - SegmentInfo current = segments.get(i); - if (lastSegment != null && current.url.equals(lastSegment.url)) { - break; - } + return segments; + } - selected = current; + private static Long parseSecondDuration(String value) { + try { + double asDouble = Double.parseDouble(value); + return (long) (asDouble * 1000.0); + } catch (NumberFormatException ignored) { + return null; + } } - return selected; - } + protected SegmentInfo chooseNextSegment(List segments, SegmentInfo lastSegment) { + SegmentInfo selected = null; - private boolean shouldWaitForSegment(long startTime, List segments) { - if (!segments.isEmpty()) { - SegmentInfo sampleSegment = segments.get(0); + for (int i = segments.size() - 1; i >= 0; i--) { + SegmentInfo current = segments.get(i); + if (lastSegment != null && current.url.equals(lastSegment.url)) { + break; + } - if (sampleSegment.duration != null) { - return System.currentTimeMillis() - startTime < sampleSegment.duration; - } + selected = current; + } + + return selected; } - return false; - } + private boolean shouldWaitForSegment(long startTime, List segments) { + if (!segments.isEmpty()) { + SegmentInfo sampleSegment = segments.get(0); - protected static class ChannelStreamInfo { - /** - * Stream quality extracted from stream M3U directive. - */ - public final String quality; - /** - * URL for stream segment list. - */ - public final String url; + if (sampleSegment.duration != null) { + return System.currentTimeMillis() - startTime < sampleSegment.duration; + } + } - private ChannelStreamInfo(String quality, String url) { - this.quality = quality; - this.url = url; + return false; } - } - protected static class SegmentInfo { - /** - * URL of the segment. - */ - public final String url; - /** - * Duration of the segment in milliseconds. null if unknown. - */ - public final Long duration; - /** - * Name of the segment. null if unknown. - */ - public final String name; + protected static class ChannelStreamInfo { + /** + * Stream quality extracted from stream M3U directive. + */ + public final String quality; + /** + * URL for stream segment list. + */ + public final String url; + + private ChannelStreamInfo(String quality, String url) { + this.quality = quality; + this.url = url; + } + } - public SegmentInfo(String url, Long duration, String name) { - this.url = url; - this.duration = duration; - this.name = name; + protected static class SegmentInfo { + /** + * URL of the segment. + */ + public final String url; + /** + * Duration of the segment in milliseconds. null if unknown. + */ + public final Long duration; + /** + * Name of the segment. null if unknown. + */ + public final String name; + + public SegmentInfo(String url, Long duration, String name) { + this.url = url; + this.duration = duration; + this.name = name; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/stream/MpegTsM3uStreamAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/stream/MpegTsM3uStreamAudioTrack.java index e32346007..737933d77 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/stream/MpegTsM3uStreamAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/stream/MpegTsM3uStreamAudioTrack.java @@ -11,18 +11,18 @@ import static com.sedmelluq.discord.lavaplayer.container.mpegts.MpegTsElementaryInputStream.ADTS_ELEMENTARY_STREAM; public abstract class MpegTsM3uStreamAudioTrack extends M3uStreamAudioTrack { - /** - * @param trackInfo Track info - */ - public MpegTsM3uStreamAudioTrack(AudioTrackInfo trackInfo) { - super(trackInfo); - } + /** + * @param trackInfo Track info + */ + public MpegTsM3uStreamAudioTrack(AudioTrackInfo trackInfo) { + super(trackInfo); + } - @Override - protected void processJoinedStream(LocalAudioTrackExecutor localExecutor, InputStream stream) throws Exception { - MpegTsElementaryInputStream elementaryInputStream = new MpegTsElementaryInputStream(stream, ADTS_ELEMENTARY_STREAM); - PesPacketInputStream pesPacketInputStream = new PesPacketInputStream(elementaryInputStream); + @Override + protected void processJoinedStream(LocalAudioTrackExecutor localExecutor, InputStream stream) throws Exception { + MpegTsElementaryInputStream elementaryInputStream = new MpegTsElementaryInputStream(stream, ADTS_ELEMENTARY_STREAM); + PesPacketInputStream pesPacketInputStream = new PesPacketInputStream(elementaryInputStream); - processDelegate(new AdtsAudioTrack(trackInfo, pesPacketInputStream), localExecutor); - } + processDelegate(new AdtsAudioTrack(trackInfo, pesPacketInputStream), localExecutor); + } } 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 be510f64b..b88800643 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 @@ -2,11 +2,7 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; -import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; -import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools; -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; -import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; -import com.sedmelluq.discord.lavaplayer.tools.Units; +import com.sedmelluq.discord.lavaplayer.tools.*; import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; @@ -43,175 +39,176 @@ * Audio source manager which detects Twitch tracks by URL. */ public class TwitchStreamAudioSourceManager implements AudioSourceManager, HttpConfigurable { - private static final String STREAM_NAME_REGEX = "^https://(?:www\\.|go\\.|m\\.)?twitch.tv/([^/]+)$"; - private static final Pattern streamNameRegex = Pattern.compile(STREAM_NAME_REGEX); - - private final HttpInterfaceManager httpInterfaceManager; - private String twitchClientId; - private String twitchDeviceId; - - /** - * Create an instance. - */ - public TwitchStreamAudioSourceManager() { - httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - initRequestHeaders(); - } - - public String getClientId() { - return twitchClientId; - } - - public String getDeviceId() { - return twitchDeviceId; - } - - @Override - public String getSourceName() { - return "twitch"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - String streamName = getChannelIdentifierFromUrl(reference.identifier); - if (streamName == null) { - return null; - } - - JsonBrowser channelInfo = fetchStreamChannelInfo(streamName).get("data").get("user"); - - if (channelInfo == null || channelInfo.get("stream").get("type").isNull()) { - return AudioReference.NO_TRACK; - } else { - String title = channelInfo.get("lastBroadcast").get("title").text(); - - final String thumbnail = channelInfo.get("profileImageURL").text().replaceFirst("-70x70", "-300x300"); - - return new TwitchStreamAudioTrack(new AudioTrackInfo( - title, - streamName, - Units.DURATION_MS_UNKNOWN, - reference.identifier, - true, - reference.identifier, - thumbnail, - null - ), this); - } - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - // Nothing special to do, URL (identifier) is enough - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new TwitchStreamAudioTrack(trackInfo, this); - } - - /** - * Extract channel identifier from a channel URL. - * @param url Channel URL - * @return Channel identifier (for API requests) - */ - public static String getChannelIdentifierFromUrl(String url) { - Matcher matcher = streamNameRegex.matcher(url); - if (!matcher.matches()) { - return null; - } - - return matcher.group(1); - } - - /** - * @param url Request URL - * @return Request with necessary headers attached. - */ - public HttpUriRequest createGetRequest(String url) { - return addClientHeaders(new HttpGet(url), twitchClientId, twitchDeviceId); - } - - /** - * @param url Request URL - * @return Request with necessary headers attached. - */ - public HttpUriRequest createGetRequest(URI url) { - return addClientHeaders(new HttpGet(url), twitchClientId, twitchDeviceId); - } - - /** - * @return Get an HTTP interface for a playing track. - */ - public HttpInterface getHttpInterface() { - return httpInterfaceManager.getInterface(); - } - - @Override - public void configureRequests(Function configurator) { - httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - httpInterfaceManager.configureBuilder(configurator); - } - - private static HttpUriRequest addClientHeaders(HttpUriRequest request, String clientId, String deviceId) { - request.setHeader("Client-ID", clientId); - request.setHeader("X-Device-ID", deviceId); - return request; - } - - protected JsonBrowser fetchAccessToken(String name) { - try (HttpInterface httpInterface = getHttpInterface()) { - HttpPost post = new HttpPost(TWITCH_GRAPHQL_BASE_URL); - addClientHeaders(post, twitchClientId, twitchDeviceId); - post.setEntity(new StringEntity(String.format(ACCESS_TOKEN_PAYLOAD, name))); - return HttpClientTools.fetchResponseAsJson(httpInterface, post); - } catch (IOException e) { - throw new FriendlyException("Loading Twitch channel access token failed.", SUSPICIOUS, e); - } - } - - private JsonBrowser fetchStreamChannelInfo(String channelId) { - try (HttpInterface httpInterface = getHttpInterface()) { - HttpPost post = new HttpPost(TWITCH_GRAPHQL_BASE_URL); - addClientHeaders(post, twitchClientId, twitchDeviceId); - post.setEntity(new StringEntity(String.format(METADATA_PAYLOAD, channelId))); - return HttpClientTools.fetchResponseAsJson(httpInterface, post); - } catch (IOException e) { - throw new FriendlyException("Loading Twitch channel information failed.", SUSPICIOUS, e); - } - } - - private void initRequestHeaders() { - try (HttpInterface httpInterface = getHttpInterface()) { - HttpGet get = new HttpGet("https://www.twitch.tv"); - get.setHeader("Accept", "text/html"); - CloseableHttpResponse response = httpInterface.execute(get); - HttpClientTools.assertSuccessWithContent(response, "twitch main page"); - - String responseText = EntityUtils.toString(response.getEntity()); - twitchClientId = DataFormatTools.extractBetween(responseText, "clientId=\"", "\""); - - for (Header header : response.getAllHeaders()) { - if (header.getName().contains("Set-Cookie") && header.getValue().contains("unique_id=")) { - twitchDeviceId = DataFormatTools.extractBetween(header.toString(), "unique_id=", ";"); + private static final String STREAM_NAME_REGEX = "^https://(?:www\\.|go\\.|m\\.)?twitch.tv/([^/]+)$"; + private static final Pattern streamNameRegex = Pattern.compile(STREAM_NAME_REGEX); + + private final HttpInterfaceManager httpInterfaceManager; + private String twitchClientId; + private String twitchDeviceId; + + /** + * Create an instance. + */ + public TwitchStreamAudioSourceManager() { + httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + initRequestHeaders(); + } + + public String getClientId() { + return twitchClientId; + } + + public String getDeviceId() { + return twitchDeviceId; + } + + @Override + public String getSourceName() { + return "twitch"; + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + String streamName = getChannelIdentifierFromUrl(reference.identifier); + if (streamName == null) { + return null; + } + + JsonBrowser channelInfo = fetchStreamChannelInfo(streamName).get("data").get("user"); + + if (channelInfo == null || channelInfo.get("stream").get("type").isNull()) { + return AudioReference.NO_TRACK; + } else { + String title = channelInfo.get("lastBroadcast").get("title").text(); + + final String thumbnail = channelInfo.get("profileImageURL").text().replaceFirst("-70x70", "-300x300"); + + return new TwitchStreamAudioTrack(new AudioTrackInfo( + title, + streamName, + Units.DURATION_MS_UNKNOWN, + reference.identifier, + true, + reference.identifier, + thumbnail, + null + ), this); + } + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + // Nothing special to do, URL (identifier) is enough + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new TwitchStreamAudioTrack(trackInfo, this); + } + + /** + * Extract channel identifier from a channel URL. + * + * @param url Channel URL + * @return Channel identifier (for API requests) + */ + public static String getChannelIdentifierFromUrl(String url) { + Matcher matcher = streamNameRegex.matcher(url); + if (!matcher.matches()) { + return null; + } + + return matcher.group(1); + } + + /** + * @param url Request URL + * @return Request with necessary headers attached. + */ + public HttpUriRequest createGetRequest(String url) { + return addClientHeaders(new HttpGet(url), twitchClientId, twitchDeviceId); + } + + /** + * @param url Request URL + * @return Request with necessary headers attached. + */ + public HttpUriRequest createGetRequest(URI url) { + return addClientHeaders(new HttpGet(url), twitchClientId, twitchDeviceId); + } + + /** + * @return Get an HTTP interface for a playing track. + */ + public HttpInterface getHttpInterface() { + return httpInterfaceManager.getInterface(); + } + + @Override + public void configureRequests(Function configurator) { + httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + httpInterfaceManager.configureBuilder(configurator); + } + + private static HttpUriRequest addClientHeaders(HttpUriRequest request, String clientId, String deviceId) { + request.setHeader("Client-ID", clientId); + request.setHeader("X-Device-ID", deviceId); + return request; + } + + protected JsonBrowser fetchAccessToken(String name) { + try (HttpInterface httpInterface = getHttpInterface()) { + HttpPost post = new HttpPost(TWITCH_GRAPHQL_BASE_URL); + addClientHeaders(post, twitchClientId, twitchDeviceId); + post.setEntity(new StringEntity(String.format(ACCESS_TOKEN_PAYLOAD, name))); + return HttpClientTools.fetchResponseAsJson(httpInterface, post); + } catch (IOException e) { + throw new FriendlyException("Loading Twitch channel access token failed.", SUSPICIOUS, e); + } + } + + private JsonBrowser fetchStreamChannelInfo(String channelId) { + try (HttpInterface httpInterface = getHttpInterface()) { + HttpPost post = new HttpPost(TWITCH_GRAPHQL_BASE_URL); + addClientHeaders(post, twitchClientId, twitchDeviceId); + post.setEntity(new StringEntity(String.format(METADATA_PAYLOAD, channelId))); + return HttpClientTools.fetchResponseAsJson(httpInterface, post); + } catch (IOException e) { + throw new FriendlyException("Loading Twitch channel information failed.", SUSPICIOUS, e); } - } - } catch (IOException e) { - throw new FriendlyException("Loading Twitch main page failed.", SUSPICIOUS, e); } - } - @Override - public void shutdown() { - ExceptionTools.closeWithWarnings(httpInterfaceManager); - } + private void initRequestHeaders() { + try (HttpInterface httpInterface = getHttpInterface()) { + HttpGet get = new HttpGet("https://www.twitch.tv"); + get.setHeader("Accept", "text/html"); + CloseableHttpResponse response = httpInterface.execute(get); + HttpClientTools.assertSuccessWithContent(response, "twitch main page"); + + String responseText = EntityUtils.toString(response.getEntity()); + twitchClientId = DataFormatTools.extractBetween(responseText, "clientId=\"", "\""); + + for (Header header : response.getAllHeaders()) { + if (header.getName().contains("Set-Cookie") && header.getValue().contains("unique_id=")) { + twitchDeviceId = DataFormatTools.extractBetween(header.toString(), "unique_id=", ";"); + } + } + } catch (IOException e) { + throw new FriendlyException("Loading Twitch main page failed.", SUSPICIOUS, e); + } + } + + @Override + public void shutdown() { + ExceptionTools.closeWithWarnings(httpInterfaceManager); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/twitch/TwitchStreamAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/twitch/TwitchStreamAudioTrack.java index bd2db6180..ebfea1ffd 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/twitch/TwitchStreamAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/twitch/TwitchStreamAudioTrack.java @@ -1,7 +1,6 @@ package com.sedmelluq.discord.lavaplayer.source.twitch; import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; -import com.sedmelluq.discord.lavaplayer.source.stream.M3uStreamAudioTrack; import com.sedmelluq.discord.lavaplayer.source.stream.M3uStreamSegmentUrlProvider; import com.sedmelluq.discord.lavaplayer.source.stream.MpegTsM3uStreamAudioTrack; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; @@ -17,53 +16,53 @@ * Audio track that handles processing Twitch tracks. */ public class TwitchStreamAudioTrack extends MpegTsM3uStreamAudioTrack { - private static final Logger log = LoggerFactory.getLogger(TwitchStreamAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(TwitchStreamAudioTrack.class); - private final TwitchStreamAudioSourceManager sourceManager; - private final M3uStreamSegmentUrlProvider segmentUrlProvider; + private final TwitchStreamAudioSourceManager sourceManager; + private final M3uStreamSegmentUrlProvider segmentUrlProvider; - /** - * @param trackInfo Track info - * @param sourceManager Source manager which was used to find this track - */ - public TwitchStreamAudioTrack(AudioTrackInfo trackInfo, TwitchStreamAudioSourceManager sourceManager) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param sourceManager Source manager which was used to find this track + */ + public TwitchStreamAudioTrack(AudioTrackInfo trackInfo, TwitchStreamAudioSourceManager sourceManager) { + super(trackInfo); - this.sourceManager = sourceManager; - this.segmentUrlProvider = new TwitchStreamSegmentUrlProvider(getChannelName(), sourceManager); - } + this.sourceManager = sourceManager; + this.segmentUrlProvider = new TwitchStreamSegmentUrlProvider(getChannelName(), sourceManager); + } - /** - * @return Name of the channel of the stream. - */ - public String getChannelName() { - return getChannelIdentifierFromUrl(trackInfo.identifier); - } + /** + * @return Name of the channel of the stream. + */ + public String getChannelName() { + return getChannelIdentifierFromUrl(trackInfo.identifier); + } - @Override - protected M3uStreamSegmentUrlProvider getSegmentUrlProvider() { - return segmentUrlProvider; - } + @Override + protected M3uStreamSegmentUrlProvider getSegmentUrlProvider() { + return segmentUrlProvider; + } - @Override - protected HttpInterface getHttpInterface() { - return sourceManager.getHttpInterface(); - } + @Override + protected HttpInterface getHttpInterface() { + return sourceManager.getHttpInterface(); + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - log.debug("Starting to play Twitch channel {}.", getChannelName()); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + log.debug("Starting to play Twitch channel {}.", getChannelName()); - super.process(localExecutor); - } + super.process(localExecutor); + } - @Override - protected AudioTrack makeShallowClone() { - return new TwitchStreamAudioTrack(trackInfo, sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new TwitchStreamAudioTrack(trackInfo, sourceManager); + } - @Override - public AudioSourceManager getSourceManager() { - return sourceManager; - } + @Override + public AudioSourceManager getSourceManager() { + return sourceManager; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/twitch/TwitchStreamSegmentUrlProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/twitch/TwitchStreamSegmentUrlProvider.java index 7dae9a3d0..263c0701e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/twitch/TwitchStreamSegmentUrlProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/twitch/TwitchStreamSegmentUrlProvider.java @@ -20,127 +20,127 @@ * Provider for Twitch segment URLs from a channel. */ public class TwitchStreamSegmentUrlProvider extends M3uStreamSegmentUrlProvider { - private static final String TOKEN_PARAMETER = "token"; - - private static final Logger log = LoggerFactory.getLogger(TwitchStreamSegmentUrlProvider.class); - - private final String channelName; - private final TwitchStreamAudioSourceManager manager; - - private String streamSegmentPlaylistUrl; - private long tokenExpirationTime; - - /** - * @param channelName Channel identifier. - * @param manager Twitch source manager. - */ - public TwitchStreamSegmentUrlProvider(String channelName, TwitchStreamAudioSourceManager manager) { - this.channelName = channelName; - this.manager = manager; - this.tokenExpirationTime = -1; - } - - @Override - protected String getQualityFromM3uDirective(ExtendedM3uParser.Line directiveLine) { - return directiveLine.directiveArguments.get("VIDEO"); - } - - @Override - protected String fetchSegmentPlaylistUrl(HttpInterface httpInterface) throws IOException { - if (System.currentTimeMillis() < tokenExpirationTime) { - return streamSegmentPlaylistUrl; + private static final String TOKEN_PARAMETER = "token"; + + private static final Logger log = LoggerFactory.getLogger(TwitchStreamSegmentUrlProvider.class); + + private final String channelName; + private final TwitchStreamAudioSourceManager manager; + + private String streamSegmentPlaylistUrl; + private long tokenExpirationTime; + + /** + * @param channelName Channel identifier. + * @param manager Twitch source manager. + */ + public TwitchStreamSegmentUrlProvider(String channelName, TwitchStreamAudioSourceManager manager) { + this.channelName = channelName; + this.manager = manager; + this.tokenExpirationTime = -1; } - JsonBrowser tokenJson = manager.fetchAccessToken(channelName); - AccessToken token = new AccessToken( - JsonBrowser.parse(tokenJson.get("data").get("streamPlaybackAccessToken").get("value").text()), - tokenJson.get("data").get("streamPlaybackAccessToken").get("signature").text() - ); - String url = getChannelStreamsUrl(token).toString(); - HttpUriRequest request = new HttpGet(url); - ChannelStreams streams = loadChannelStreamsInfo(HttpClientTools.fetchResponseLines(httpInterface, request, "channel streams list")); - - if (streams.entries.isEmpty()) { - throw new IllegalStateException("No streams available on channel."); + @Override + protected String getQualityFromM3uDirective(ExtendedM3uParser.Line directiveLine) { + return directiveLine.directiveArguments.get("VIDEO"); } - ChannelStreamInfo stream = streams.entries.get(streams.entries.size() - 1); + @Override + protected String fetchSegmentPlaylistUrl(HttpInterface httpInterface) throws IOException { + if (System.currentTimeMillis() < tokenExpirationTime) { + return streamSegmentPlaylistUrl; + } + + JsonBrowser tokenJson = manager.fetchAccessToken(channelName); + AccessToken token = new AccessToken( + JsonBrowser.parse(tokenJson.get("data").get("streamPlaybackAccessToken").get("value").text()), + tokenJson.get("data").get("streamPlaybackAccessToken").get("signature").text() + ); + String url = getChannelStreamsUrl(token).toString(); + HttpUriRequest request = new HttpGet(url); + ChannelStreams streams = loadChannelStreamsInfo(HttpClientTools.fetchResponseLines(httpInterface, request, "channel streams list")); - log.debug("Chose stream with quality {} from url {}", stream.quality, stream.url); - streamSegmentPlaylistUrl = stream.url; + if (streams.entries.isEmpty()) { + throw new IllegalStateException("No streams available on channel."); + } - long tokenServerExpirationTime = token.value.get("expires").as(Long.class) * 1000L; - tokenExpirationTime = System.currentTimeMillis() + (tokenServerExpirationTime - streams.serverTime) - 5000; + ChannelStreamInfo stream = streams.entries.get(streams.entries.size() - 1); - return streamSegmentPlaylistUrl; - } + log.debug("Chose stream with quality {} from url {}", stream.quality, stream.url); + streamSegmentPlaylistUrl = stream.url; + + long tokenServerExpirationTime = token.value.get("expires").as(Long.class) * 1000L; + tokenExpirationTime = System.currentTimeMillis() + (tokenServerExpirationTime - streams.serverTime) - 5000; + + return streamSegmentPlaylistUrl; + } + + @Override + protected HttpUriRequest createSegmentGetRequest(String url) { + return manager.createGetRequest(url); + } - @Override - protected HttpUriRequest createSegmentGetRequest(String url) { - return manager.createGetRequest(url); - } + private ChannelStreams loadChannelStreamsInfo(String[] lines) { + List streams = loadChannelStreamsList(lines); + ExtendedM3uParser.Line twitchInfoLine = null; - private ChannelStreams loadChannelStreamsInfo(String[] lines) { - List streams = loadChannelStreamsList(lines); - ExtendedM3uParser.Line twitchInfoLine = null; + for (String lineText : lines) { + ExtendedM3uParser.Line line = ExtendedM3uParser.parseLine(lineText); - for (String lineText : lines) { - ExtendedM3uParser.Line line = ExtendedM3uParser.parseLine(lineText); + if (line.isDirective() && "EXT-X-TWITCH-INFO".equals(line.directiveName)) { + twitchInfoLine = line; + } + } - if (line.isDirective() && "EXT-X-TWITCH-INFO".equals(line.directiveName)) { - twitchInfoLine = line; - } + return buildChannelStreamsInfo(twitchInfoLine, streams); } - return buildChannelStreamsInfo(twitchInfoLine, streams); - } + private ChannelStreams buildChannelStreamsInfo(ExtendedM3uParser.Line twitchInfoLine, List streams) { + String serverTimeValue = twitchInfoLine != null ? twitchInfoLine.directiveArguments.get("SERVER-TIME") : null; - private ChannelStreams buildChannelStreamsInfo(ExtendedM3uParser.Line twitchInfoLine, List streams) { - String serverTimeValue = twitchInfoLine != null ? twitchInfoLine.directiveArguments.get("SERVER-TIME") : null; + if (serverTimeValue == null) { + throw new IllegalStateException("Required server time information not available."); + } - if (serverTimeValue == null) { - throw new IllegalStateException("Required server time information not available."); + return new ChannelStreams( + (long) (Double.parseDouble(serverTimeValue) * 1000.0), + streams + ); } - return new ChannelStreams( - (long) (Double.parseDouble(serverTimeValue) * 1000.0), - streams - ); - } - - private URI getChannelStreamsUrl(AccessToken token) { - try { - return new URIBuilder("https://usher.ttvnw.net/api/channel/hls/" + channelName + ".m3u8") - .addParameter(TOKEN_PARAMETER, token.value.format()) - .addParameter("sig", token.signature) - .addParameter("allow_source", "true") - .addParameter("allow_spectre", "true") - .addParameter("allow_audio_only", "true") - .addParameter("player_backend", "html5") - .addParameter("expgroup", "regular") - .build(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); + private URI getChannelStreamsUrl(AccessToken token) { + try { + return new URIBuilder("https://usher.ttvnw.net/api/channel/hls/" + channelName + ".m3u8") + .addParameter(TOKEN_PARAMETER, token.value.format()) + .addParameter("sig", token.signature) + .addParameter("allow_source", "true") + .addParameter("allow_spectre", "true") + .addParameter("allow_audio_only", "true") + .addParameter("player_backend", "html5") + .addParameter("expgroup", "regular") + .build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } - } - private static class ChannelStreams { - private final long serverTime; - private final List entries; + private static class ChannelStreams { + private final long serverTime; + private final List entries; - private ChannelStreams(long serverTime, List entries) { - this.serverTime = serverTime; - this.entries = entries; + private ChannelStreams(long serverTime, List entries) { + this.serverTime = serverTime; + this.entries = entries; + } } - } - private static class AccessToken { - private final JsonBrowser value; - private final String signature; + private static class AccessToken { + private final JsonBrowser value; + private final String signature; - private AccessToken(JsonBrowser value, String signature) { - this.value = value; - this.signature = signature; + private AccessToken(JsonBrowser value, String signature) { + this.value = value; + this.signature = signature; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/vimeo/VimeoAudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/vimeo/VimeoAudioSourceManager.java index 9519f8275..7d87a2c24 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/vimeo/VimeoAudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/vimeo/VimeoAudioSourceManager.java @@ -6,10 +6,10 @@ import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.track.AudioItem; import com.sedmelluq.discord.lavaplayer.track.AudioReference; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; @@ -25,7 +25,6 @@ import java.io.DataOutput; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Collections; import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Pattern; @@ -36,114 +35,114 @@ * Audio source manager which detects Vimeo tracks by URL. */ public class VimeoAudioSourceManager implements AudioSourceManager, HttpConfigurable { - private static final String TRACK_URL_REGEX = "^https://vimeo.com/[0-9]+(?:\\?.*|)$"; - private static final Pattern trackUrlPattern = Pattern.compile(TRACK_URL_REGEX); - - private final HttpInterfaceManager httpInterfaceManager; - - /** - * Create an instance. - */ - public VimeoAudioSourceManager() { - httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - } - - @Override - public String getSourceName() { - return "vimeo"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - if (!trackUrlPattern.matcher(reference.identifier).matches()) { - return null; + private static final String TRACK_URL_REGEX = "^https://vimeo.com/[0-9]+(?:\\?.*|)$"; + private static final Pattern trackUrlPattern = Pattern.compile(TRACK_URL_REGEX); + + private final HttpInterfaceManager httpInterfaceManager; + + /** + * Create an instance. + */ + public VimeoAudioSourceManager() { + httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + } + + @Override + public String getSourceName() { + return "vimeo"; + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + if (!trackUrlPattern.matcher(reference.identifier).matches()) { + return null; + } + + try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { + return loadFromTrackPage(httpInterface, reference.identifier); + } catch (IOException e) { + throw new FriendlyException("Loading Vimeo track information failed.", SUSPICIOUS, e); + } + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; } - try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { - return loadFromTrackPage(httpInterface, reference.identifier); - } catch (IOException e) { - throw new FriendlyException("Loading Vimeo track information failed.", SUSPICIOUS, e); + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + // Nothing special to encode } - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - // Nothing special to encode - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new VimeoAudioTrack(trackInfo, this); - } - - @Override - public void shutdown() { - ExceptionTools.closeWithWarnings(httpInterfaceManager); - } - - /** - * @return Get an HTTP interface for a playing track. - */ - public HttpInterface getHttpInterface() { - return httpInterfaceManager.getInterface(); - } - - @Override - public void configureRequests(Function configurator) { - httpInterfaceManager.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - httpInterfaceManager.configureBuilder(configurator); - } - - JsonBrowser loadConfigJsonFromPageContent(String content) throws IOException { - String configText = DataFormatTools.extractBetween(content, "window.vimeo.clip_page_config = ", "\n"); - - if (configText != null) { - return JsonBrowser.parse(configText); + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new VimeoAudioTrack(trackInfo, this); + } + + @Override + public void shutdown() { + ExceptionTools.closeWithWarnings(httpInterfaceManager); + } + + /** + * @return Get an HTTP interface for a playing track. + */ + public HttpInterface getHttpInterface() { + return httpInterfaceManager.getInterface(); } - return null; - } + @Override + public void configureRequests(Function configurator) { + httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + httpInterfaceManager.configureBuilder(configurator); + } - private AudioItem loadFromTrackPage(HttpInterface httpInterface, String trackUrl) throws IOException { - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackUrl))) { - int statusCode = response.getStatusLine().getStatusCode(); + JsonBrowser loadConfigJsonFromPageContent(String content) throws IOException { + String configText = DataFormatTools.extractBetween(content, "window.vimeo.clip_page_config = ", "\n"); - if (statusCode == HttpStatus.SC_NOT_FOUND) { - return AudioReference.NO_TRACK; - } else if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new FriendlyException("Server responded with an error.", SUSPICIOUS, - new IllegalStateException("Response code is " + statusCode)); - } + if (configText != null) { + return JsonBrowser.parse(configText); + } - return loadTrackFromPageContent(trackUrl, IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); + return null; } - } - private AudioTrack loadTrackFromPageContent(String trackUrl, String content) throws IOException { - JsonBrowser config = loadConfigJsonFromPageContent(content); + private AudioItem loadFromTrackPage(HttpInterface httpInterface, String trackUrl) throws IOException { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackUrl))) { + int statusCode = response.getStatusLine().getStatusCode(); - if (config == null) { - throw new FriendlyException("Track information not found on the page.", SUSPICIOUS, null); + if (statusCode == HttpStatus.SC_NOT_FOUND) { + return AudioReference.NO_TRACK; + } else if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new FriendlyException("Server responded with an error.", SUSPICIOUS, + new IllegalStateException("Response code is " + statusCode)); + } + + return loadTrackFromPageContent(trackUrl, IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); + } } - return new VimeoAudioTrack(new AudioTrackInfo( - config.get("clip").get("title").text(), - config.get("owner").get("display_name").text(), - (long) (config.get("clip").get("duration").get("raw").as(Double.class) * 1000.0), - trackUrl, - false, - trackUrl, - config.get("thumbnail").get("src").text(), - null - ), this); - } + private AudioTrack loadTrackFromPageContent(String trackUrl, String content) throws IOException { + JsonBrowser config = loadConfigJsonFromPageContent(content); + + if (config == null) { + throw new FriendlyException("Track information not found on the page.", SUSPICIOUS, null); + } + + return new VimeoAudioTrack(new AudioTrackInfo( + config.get("clip").get("title").text(), + config.get("owner").get("display_name").text(), + (long) (config.get("clip").get("duration").get("raw").as(Double.class) * 1000.0), + trackUrl, + false, + trackUrl, + config.get("thumbnail").get("src").text(), + null + ), this); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/vimeo/VimeoAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/vimeo/VimeoAudioTrack.java index 783263b44..53beec889 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/vimeo/VimeoAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/vimeo/VimeoAudioTrack.java @@ -27,78 +27,78 @@ * Audio track that handles processing Vimeo tracks. */ public class VimeoAudioTrack extends DelegatedAudioTrack { - private static final Logger log = LoggerFactory.getLogger(VimeoAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(VimeoAudioTrack.class); - private final VimeoAudioSourceManager sourceManager; + private final VimeoAudioSourceManager sourceManager; - /** - * @param trackInfo Track info - * @param sourceManager Source manager which was used to find this track - */ - public VimeoAudioTrack(AudioTrackInfo trackInfo, VimeoAudioSourceManager sourceManager) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param sourceManager Source manager which was used to find this track + */ + public VimeoAudioTrack(AudioTrackInfo trackInfo, VimeoAudioSourceManager sourceManager) { + super(trackInfo); - this.sourceManager = sourceManager; - } + this.sourceManager = sourceManager; + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { - String playbackUrl = loadPlaybackUrl(httpInterface); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { + String playbackUrl = loadPlaybackUrl(httpInterface); - log.debug("Starting Vimeo track from URL: {}", playbackUrl); + log.debug("Starting Vimeo track from URL: {}", playbackUrl); - try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(playbackUrl), null)) { - processDelegate(new MpegAudioTrack(trackInfo, stream), localExecutor); - } + try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(playbackUrl), null)) { + processDelegate(new MpegAudioTrack(trackInfo, stream), localExecutor); + } + } } - } - private String loadPlaybackUrl(HttpInterface httpInterface) throws IOException { - JsonBrowser config = loadPlayerConfig(httpInterface); - if (config == null) { - throw new FriendlyException("Track information not present on the page.", SUSPICIOUS, null); - } + private String loadPlaybackUrl(HttpInterface httpInterface) throws IOException { + JsonBrowser config = loadPlayerConfig(httpInterface); + if (config == null) { + throw new FriendlyException("Track information not present on the page.", SUSPICIOUS, null); + } - String trackConfigUrl = config.get("player").get("config_url").text(); - JsonBrowser trackConfig = loadTrackConfig(httpInterface, trackConfigUrl); + String trackConfigUrl = config.get("player").get("config_url").text(); + JsonBrowser trackConfig = loadTrackConfig(httpInterface, trackConfigUrl); - return trackConfig.get("request").get("files").get("progressive").index(0).get("url").text(); - } + return trackConfig.get("request").get("files").get("progressive").index(0).get("url").text(); + } - private JsonBrowser loadPlayerConfig(HttpInterface httpInterface) throws IOException { - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackInfo.identifier))) { - int statusCode = response.getStatusLine().getStatusCode(); + private JsonBrowser loadPlayerConfig(HttpInterface httpInterface) throws IOException { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackInfo.identifier))) { + int statusCode = response.getStatusLine().getStatusCode(); - if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new FriendlyException("Server responded with an error.", SUSPICIOUS, - new IllegalStateException("Response code for player config is " + statusCode)); - } + if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new FriendlyException("Server responded with an error.", SUSPICIOUS, + new IllegalStateException("Response code for player config is " + statusCode)); + } - return sourceManager.loadConfigJsonFromPageContent(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); + return sourceManager.loadConfigJsonFromPageContent(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); + } } - } - private JsonBrowser loadTrackConfig(HttpInterface httpInterface, String trackAccessInfoUrl) throws IOException { - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackAccessInfoUrl))) { - int statusCode = response.getStatusLine().getStatusCode(); + private JsonBrowser loadTrackConfig(HttpInterface httpInterface, String trackAccessInfoUrl) throws IOException { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackAccessInfoUrl))) { + int statusCode = response.getStatusLine().getStatusCode(); - if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new FriendlyException("Server responded with an error.", SUSPICIOUS, - new IllegalStateException("Response code for track access info is " + statusCode)); - } + if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new FriendlyException("Server responded with an error.", SUSPICIOUS, + new IllegalStateException("Response code for track access info is " + statusCode)); + } - return JsonBrowser.parse(response.getEntity().getContent()); + return JsonBrowser.parse(response.getEntity().getContent()); + } } - } - @Override - protected AudioTrack makeShallowClone() { - return new VimeoAudioTrack(trackInfo, sourceManager); - } + @Override + protected AudioTrack makeShallowClone() { + return new VimeoAudioTrack(trackInfo, sourceManager); + } - @Override - public AudioSourceManager getSourceManager() { - return sourceManager; - } + @Override + public AudioSourceManager getSourceManager() { + return sourceManager; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/AbstractYandexMusicApiLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/AbstractYandexMusicApiLoader.java index f49e5e8e4..b18e3e54f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/AbstractYandexMusicApiLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/AbstractYandexMusicApiLoader.java @@ -19,49 +19,49 @@ public abstract class AbstractYandexMusicApiLoader implements YandexMusicApiLoader { - protected HttpInterfaceManager httpInterfaceManager; + protected HttpInterfaceManager httpInterfaceManager; - AbstractYandexMusicApiLoader() { - httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - httpInterfaceManager.setHttpContextFilter(new YandexHttpContextFilter()); - } + AbstractYandexMusicApiLoader() { + httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + httpInterfaceManager.setHttpContextFilter(new YandexHttpContextFilter()); + } - protected T extractFromApi(String url, ApiExtractor extractor) { - try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { - String responseText; + protected T extractFromApi(String url, ApiExtractor extractor) { + try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { + String responseText; - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(url))) { - int statusCode = response.getStatusLine().getStatusCode(); - if (statusCode != 200) { - throw new IOException("Invalid status code: " + statusCode); + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(url))) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != 200) { + throw new IOException("Invalid status code: " + statusCode); + } + responseText = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + } + JsonBrowser response = JsonBrowser.parse(responseText); + if (response.isNull()) { + throw new FriendlyException("Couldn't get API response.", SUSPICIOUS, null); + } + response = response.get("result"); + if (response.isNull() && !response.isList()) { + throw new FriendlyException("Couldn't get API response result.", SUSPICIOUS, null); + } + return extractor.extract(httpInterface, response); + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions("Loading information for a Yandex Music track failed.", FAULT, e); } - responseText = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - } - JsonBrowser response = JsonBrowser.parse(responseText); - if (response.isNull()) { - throw new FriendlyException("Couldn't get API response.", SUSPICIOUS, null); - } - response = response.get("result"); - if (response.isNull() && !response.isList()) { - throw new FriendlyException("Couldn't get API response result.", SUSPICIOUS, null); - } - return extractor.extract(httpInterface, response); - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions("Loading information for a Yandex Music track failed.", FAULT, e); } - } - @Override - public ExtendedHttpConfigurable getHttpConfiguration() { - return httpInterfaceManager; - } + @Override + public ExtendedHttpConfigurable getHttpConfiguration() { + return httpInterfaceManager; + } - @Override - public void shutdown() { - ExceptionTools.closeWithWarnings(httpInterfaceManager); - } + @Override + public void shutdown() { + ExceptionTools.closeWithWarnings(httpInterfaceManager); + } - protected interface ApiExtractor { - T extract(HttpInterface httpInterface, JsonBrowser result) throws Exception; - } + protected interface ApiExtractor { + T extract(HttpInterface httpInterface, JsonBrowser result) throws Exception; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexMusicDirectUrlLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexMusicDirectUrlLoader.java index a1b8bcac2..cac6f3e73 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexMusicDirectUrlLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexMusicDirectUrlLoader.java @@ -5,7 +5,6 @@ import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; -import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; @@ -19,56 +18,56 @@ public class DefaultYandexMusicDirectUrlLoader extends AbstractYandexMusicApiLoader implements YandexMusicDirectUrlLoader { - private static final String TRACK_DOWNLOAD_INFO = "https://api.music.yandex.net/tracks/%s/download-info"; - private static final String DIRECT_URL_FORMAT = "https://%s/get-%s/%s/%s%s"; - private static final String MP3_SALT = "XGRlBW9FXlekgbPrRHuSiA"; + private static final String TRACK_DOWNLOAD_INFO = "https://api.music.yandex.net/tracks/%s/download-info"; + private static final String DIRECT_URL_FORMAT = "https://%s/get-%s/%s/%s%s"; + private static final String MP3_SALT = "XGRlBW9FXlekgbPrRHuSiA"; - @Override - public String getDirectUrl(String trackId, String codec) { - return extractFromApi(String.format(TRACK_DOWNLOAD_INFO, trackId), (httpClient, codecsList) -> { - JsonBrowser codecResult = codecsList.values().stream() - .filter(e -> codec.equals(e.get("codec").text())) - .findFirst() - .orElse(null); - if (codecResult == null) { - throw new FriendlyException("Couldn't find supported track format.", SUSPICIOUS, null); - } - String storageUrl = codecResult.get("downloadInfoUrl").text(); - DownloadInfo info = extractDownloadInfo(storageUrl); + @Override + public String getDirectUrl(String trackId, String codec) { + return extractFromApi(String.format(TRACK_DOWNLOAD_INFO, trackId), (httpClient, codecsList) -> { + JsonBrowser codecResult = codecsList.values().stream() + .filter(e -> codec.equals(e.get("codec").text())) + .findFirst() + .orElse(null); + if (codecResult == null) { + throw new FriendlyException("Couldn't find supported track format.", SUSPICIOUS, null); + } + String storageUrl = codecResult.get("downloadInfoUrl").text(); + DownloadInfo info = extractDownloadInfo(storageUrl); - String sign = DigestUtils.md5Hex(MP3_SALT + info.path.substring(1) + info.s); + String sign = DigestUtils.md5Hex(MP3_SALT + info.path.substring(1) + info.s); - return String.format(DIRECT_URL_FORMAT, info.host, codec, sign, info.ts, info.path); - }); - } + return String.format(DIRECT_URL_FORMAT, info.host, codec, sign, info.ts, info.path); + }); + } - private DownloadInfo extractDownloadInfo(String storageUrl) throws IOException { - try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { - String responseText; - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(storageUrl))) { - int statusCode = response.getStatusLine().getStatusCode(); - if (statusCode != 200) { - throw new IOException("Invalid status code for track storage info: " + statusCode); + private DownloadInfo extractDownloadInfo(String storageUrl) throws IOException { + try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { + String responseText; + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(storageUrl))) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != 200) { + throw new IOException("Invalid status code for track storage info: " + statusCode); + } + responseText = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + } + DownloadInfo info = new DownloadInfo(); + info.host = DataFormatTools.extractBetween(responseText, "", ""); + info.path = DataFormatTools.extractBetween(responseText, "", ""); + info.ts = DataFormatTools.extractBetween(responseText, "", ""); + info.region = DataFormatTools.extractBetween(responseText, "", ""); + info.s = DataFormatTools.extractBetween(responseText, "", ""); + return info; + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions("Loading information for a Yandex Music track failed.", FAULT, e); } - responseText = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - } - DownloadInfo info = new DownloadInfo(); - info.host = DataFormatTools.extractBetween(responseText, "", ""); - info.path = DataFormatTools.extractBetween(responseText, "", ""); - info.ts = DataFormatTools.extractBetween(responseText, "", ""); - info.region = DataFormatTools.extractBetween(responseText, "", ""); - info.s = DataFormatTools.extractBetween(responseText, "", ""); - return info; - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions("Loading information for a Yandex Music track failed.", FAULT, e); } - } - private class DownloadInfo { - String host; - String path; - String ts; - String region; - String s; - } + private class DownloadInfo { + String host; + String path; + String ts; + String region; + String s; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexMusicPlaylistLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexMusicPlaylistLoader.java index 1c2a649b6..a80d9c696 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexMusicPlaylistLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexMusicPlaylistLoader.java @@ -13,99 +13,99 @@ import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS; public class DefaultYandexMusicPlaylistLoader extends DefaultYandexMusicTrackLoader implements YandexMusicPlaylistLoader { - private static final String PLAYLIST_INFO_FORMAT = "https://api.music.yandex.net/users/%s/playlists/%s"; - private static final String ALBUM_INFO_FORMAT = "https://api.music.yandex.net/albums/%s/with-tracks"; - private static final String ARTIST_INFO_FORMAT = "https://api.music.yandex.net/artists/%s/brief-info"; - - private final ExecutorService tracksLoader; - - public DefaultYandexMusicPlaylistLoader() { - tracksLoader = Executors.newCachedThreadPool(); - } - - @Override - public AudioItem loadPlaylist(String login, String id, String trackProperty, Function trackFactory) { - return loadPlaylistUrl(String.format(PLAYLIST_INFO_FORMAT, login, id), trackProperty, trackFactory); - } - - @Override - public AudioItem loadPlaylist(String id, String trackProperty, Function trackFactory) { - if (trackProperty.equals("volumes")) { - return loadPlaylistUrl(String.format(ALBUM_INFO_FORMAT, id), trackProperty, trackFactory); - } else { - return loadPlaylistUrl(String.format(ARTIST_INFO_FORMAT, id), trackProperty, trackFactory); + private static final String PLAYLIST_INFO_FORMAT = "https://api.music.yandex.net/users/%s/playlists/%s"; + private static final String ALBUM_INFO_FORMAT = "https://api.music.yandex.net/albums/%s/with-tracks"; + private static final String ARTIST_INFO_FORMAT = "https://api.music.yandex.net/artists/%s/brief-info"; + + private final ExecutorService tracksLoader; + + public DefaultYandexMusicPlaylistLoader() { + tracksLoader = Executors.newCachedThreadPool(); + } + + @Override + public AudioItem loadPlaylist(String login, String id, String trackProperty, Function trackFactory) { + return loadPlaylistUrl(String.format(PLAYLIST_INFO_FORMAT, login, id), trackProperty, trackFactory); } - } - - private AudioItem loadPlaylistUrl(String url, String trackProperty, Function trackFactory) { - return extractFromApi(url, (httpClient, result) -> { - if (hasError(result)) return AudioReference.NO_TRACK; - JsonBrowser volumes = result.get(trackProperty); - if (volumes.isNull()) { - throw new FriendlyException("Volumes is empty", SUSPICIOUS, null); - } - - List> futures = new ArrayList<>(); - CompletionService completionService = new ExecutorCompletionService<>(tracksLoader); - for (JsonBrowser trackInfo : volumes.values()) { - if (trackInfo.isList()) { - for (JsonBrowser innerInfo : trackInfo.values()) { - futures.add(completionService.submit(() -> loadTrack(innerInfo, trackFactory))); - } + + @Override + public AudioItem loadPlaylist(String id, String trackProperty, Function trackFactory) { + if (trackProperty.equals("volumes")) { + return loadPlaylistUrl(String.format(ALBUM_INFO_FORMAT, id), trackProperty, trackFactory); } else { - futures.add(completionService.submit(() -> loadTrack(trackInfo, trackFactory))); + return loadPlaylistUrl(String.format(ARTIST_INFO_FORMAT, id), trackProperty, trackFactory); } - } - if (futures.isEmpty()) { - return AudioReference.NO_TRACK; - } - - List tracks = FutureTools.awaitList(completionService, futures); - if (futures.isEmpty()) { - return AudioReference.NO_TRACK; - } - - String name; - if (trackProperty.equals("volumes") || trackProperty.equals("tracks")) { - name = result.get("title").text(); - } else { - name = result.get("artist").get("name").text(); - } - - return new BasicAudioPlaylist(name, tracks, null, false); - }); - } - - static boolean hasError(JsonBrowser result) { - JsonBrowser error = result.get("error"); - if (!error.isNull()) { - String code = error.text(); - if ("not-found".equals(code)) { - return true; - } - throw new FriendlyException(String.format("Yandex Music returned an error code: %s", code), SUSPICIOUS, null); } - return false; - } - private AudioTrack loadTrack(JsonBrowser trackInfo, Function trackFactory) { - if (!trackInfo.get("title").isNull()) { - return YandexMusicUtils.extractTrack(trackInfo, trackFactory); + private AudioItem loadPlaylistUrl(String url, String trackProperty, Function trackFactory) { + return extractFromApi(url, (httpClient, result) -> { + if (hasError(result)) return AudioReference.NO_TRACK; + JsonBrowser volumes = result.get(trackProperty); + if (volumes.isNull()) { + throw new FriendlyException("Volumes is empty", SUSPICIOUS, null); + } + + List> futures = new ArrayList<>(); + CompletionService completionService = new ExecutorCompletionService<>(tracksLoader); + for (JsonBrowser trackInfo : volumes.values()) { + if (trackInfo.isList()) { + for (JsonBrowser innerInfo : trackInfo.values()) { + futures.add(completionService.submit(() -> loadTrack(innerInfo, trackFactory))); + } + } else { + futures.add(completionService.submit(() -> loadTrack(trackInfo, trackFactory))); + } + } + if (futures.isEmpty()) { + return AudioReference.NO_TRACK; + } + + List tracks = FutureTools.awaitList(completionService, futures); + if (futures.isEmpty()) { + return AudioReference.NO_TRACK; + } + + String name; + if (trackProperty.equals("volumes") || trackProperty.equals("tracks")) { + name = result.get("title").text(); + } else { + name = result.get("artist").get("name").text(); + } + + return new BasicAudioPlaylist(name, tracks, null, false); + }); } - if (!trackInfo.get("track").isNull()) { - return YandexMusicUtils.extractTrack(trackInfo, trackFactory); + + static boolean hasError(JsonBrowser result) { + JsonBrowser error = result.get("error"); + if (!error.isNull()) { + String code = error.text(); + if ("not-found".equals(code)) { + return true; + } + throw new FriendlyException(String.format("Yandex Music returned an error code: %s", code), SUSPICIOUS, null); + } + return false; } - String trackId = trackInfo.get("id").text(); - String albumId = trackInfo.get("albumId").text(); - if (trackId == null || albumId == null) { - throw new FriendlyException("Could not load playlist track", FriendlyException.Severity.COMMON, null); + + private AudioTrack loadTrack(JsonBrowser trackInfo, Function trackFactory) { + if (!trackInfo.get("title").isNull()) { + return YandexMusicUtils.extractTrack(trackInfo, trackFactory); + } + if (!trackInfo.get("track").isNull()) { + return YandexMusicUtils.extractTrack(trackInfo, trackFactory); + } + String trackId = trackInfo.get("id").text(); + String albumId = trackInfo.get("albumId").text(); + if (trackId == null || albumId == null) { + throw new FriendlyException("Could not load playlist track", FriendlyException.Severity.COMMON, null); + } + return (AudioTrack) loadTrack(albumId, trackId, trackFactory); + } + + @Override + public void shutdown() { + super.shutdown(); + tracksLoader.shutdown(); } - return (AudioTrack) loadTrack(albumId, trackId, trackFactory); - } - - @Override - public void shutdown() { - super.shutdown(); - tracksLoader.shutdown(); - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexMusicTrackLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexMusicTrackLoader.java index 7c021acb9..c2ea4a2b6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexMusicTrackLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexMusicTrackLoader.java @@ -10,17 +10,17 @@ public class DefaultYandexMusicTrackLoader extends AbstractYandexMusicApiLoader implements YandexMusicTrackLoader { - private static final String TRACKS_INFO_FORMAT = "https://api.music.yandex.net/tracks?trackIds="; + private static final String TRACKS_INFO_FORMAT = "https://api.music.yandex.net/tracks?trackIds="; - @Override - public AudioItem loadTrack(String albumId, String trackId, Function trackFactory) { - StringBuilder id = new StringBuilder(trackId); - if (!albumId.isEmpty()) id.append(":").append(albumId); + @Override + public AudioItem loadTrack(String albumId, String trackId, Function trackFactory) { + StringBuilder id = new StringBuilder(trackId); + if (!albumId.isEmpty()) id.append(":").append(albumId); - return extractFromApi(TRACKS_INFO_FORMAT + id, (httpClient, result) -> { - JsonBrowser entry = result.index(0); - if (DefaultYandexMusicPlaylistLoader.hasError(entry)) return AudioReference.NO_TRACK; - return YandexMusicUtils.extractTrack(entry, trackFactory); - }); - } + return extractFromApi(TRACKS_INFO_FORMAT + id, (httpClient, result) -> { + JsonBrowser entry = result.index(0); + if (DefaultYandexMusicPlaylistLoader.hasError(entry)) return AudioReference.NO_TRACK; + return YandexMusicUtils.extractTrack(entry, trackFactory); + }); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexSearchProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexSearchProvider.java index ae66e9271..98f84a7e7 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexSearchProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/DefaultYandexSearchProvider.java @@ -14,115 +14,115 @@ public class DefaultYandexSearchProvider extends AbstractYandexMusicApiLoader implements YandexMusicSearchResultLoader { - private static final int DEFAULT_LIMIT = 10; + private static final int DEFAULT_LIMIT = 10; - private static final String TRACKS_INFO_FORMAT = "https://api.music.yandex.net/search?type=%s&page=0&text=%s"; + private static final String TRACKS_INFO_FORMAT = "https://api.music.yandex.net/search?type=%s&page=0&text=%s"; - private static final String SEARCH_PREFIX = "ymsearch"; + private static final String SEARCH_PREFIX = "ymsearch"; - private static final Pattern SEARCH_PATTERN = Pattern.compile("ymsearch(:([a-zA-Z]+))?(:([0-9]+))?:([^:]+)"); + private static final Pattern SEARCH_PATTERN = Pattern.compile("ymsearch(:([a-zA-Z]+))?(:([0-9]+))?:([^:]+)"); - @Override - public AudioItem loadSearchResult(String query, YandexMusicPlaylistLoader playlistLoader, Function trackFactory) { - if (query == null || !query.startsWith(SEARCH_PREFIX)) { - return null; - } - Matcher matcher = SEARCH_PATTERN.matcher(query); - if (!matcher.find()) { - return null; - } - String type = getValidType(matcher.group(2)); - int limit = getValidLimit(matcher.group(4)); - String text = matcher.group(5); - try { - return extractFromApi(String.format(TRACKS_INFO_FORMAT, type, URLEncoder.encode(text, "UTF-8")), (httpClient, result) -> { - if ("track".equalsIgnoreCase(type)) { - return loadTracks(getResults(result, "tracks"), limit, trackFactory); + @Override + public AudioItem loadSearchResult(String query, YandexMusicPlaylistLoader playlistLoader, Function trackFactory) { + if (query == null || !query.startsWith(SEARCH_PREFIX)) { + return null; } - if ("album".equalsIgnoreCase(type)) { - return loadAlbum(getResults(result, "albums"), playlistLoader, trackFactory); + Matcher matcher = SEARCH_PATTERN.matcher(query); + if (!matcher.find()) { + return null; } - if ("playlist".equalsIgnoreCase(type)) { - return loadPlaylist(getResults(result, "playlists"), playlistLoader, trackFactory); + String type = getValidType(matcher.group(2)); + int limit = getValidLimit(matcher.group(4)); + String text = matcher.group(5); + try { + return extractFromApi(String.format(TRACKS_INFO_FORMAT, type, URLEncoder.encode(text, "UTF-8")), (httpClient, result) -> { + if ("track".equalsIgnoreCase(type)) { + return loadTracks(getResults(result, "tracks"), limit, trackFactory); + } + if ("album".equalsIgnoreCase(type)) { + return loadAlbum(getResults(result, "albums"), playlistLoader, trackFactory); + } + if ("playlist".equalsIgnoreCase(type)) { + return loadPlaylist(getResults(result, "playlists"), playlistLoader, trackFactory); + } + return AudioReference.NO_TRACK; + }); + } catch (Exception e) { + throw new FriendlyException("Could not load search results", FriendlyException.Severity.SUSPICIOUS, e); } - return AudioReference.NO_TRACK; - }); - } catch (Exception e) { - throw new FriendlyException("Could not load search results", FriendlyException.Severity.SUSPICIOUS, e); } - } - private AudioItem loadTracks(List results, int limit, Function trackFactory) { - List tracks = new ArrayList<>(limit); - for (JsonBrowser entry : results) { - tracks.add(YandexMusicUtils.extractTrack(entry, trackFactory)); - if (tracks.size() >= limit) { - break; - } - } - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; + private AudioItem loadTracks(List results, int limit, Function trackFactory) { + List tracks = new ArrayList<>(limit); + for (JsonBrowser entry : results) { + tracks.add(YandexMusicUtils.extractTrack(entry, trackFactory)); + if (tracks.size() >= limit) { + break; + } + } + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } + return new BasicAudioPlaylist("Yandex search result", tracks, null, true); } - return new BasicAudioPlaylist("Yandex search result", tracks, null, true); - } - private AudioItem loadPlaylist(List results, - YandexMusicPlaylistLoader playlistLoader, - Function trackFactory) { - if (results.isEmpty()) { - return AudioReference.NO_TRACK; + private AudioItem loadPlaylist(List results, + YandexMusicPlaylistLoader playlistLoader, + Function trackFactory) { + if (results.isEmpty()) { + return AudioReference.NO_TRACK; + } + JsonBrowser first = results.get(0); + return playlistLoader.loadPlaylist(first.get("owner").get("login").safeText(), + first.get("kind").safeText(), "tracks", trackFactory); } - JsonBrowser first = results.get(0); - return playlistLoader.loadPlaylist(first.get("owner").get("login").safeText(), - first.get("kind").safeText(), "tracks", trackFactory); - } - private AudioItem loadAlbum(List results, - YandexMusicPlaylistLoader playlistLoader, - Function trackFactory) { - if (results.isEmpty()) { - return AudioReference.NO_TRACK; + private AudioItem loadAlbum(List results, + YandexMusicPlaylistLoader playlistLoader, + Function trackFactory) { + if (results.isEmpty()) { + return AudioReference.NO_TRACK; + } + JsonBrowser first = results.get(0); + return playlistLoader.loadPlaylist(first.get("id").safeText(), "volumes", trackFactory); } - JsonBrowser first = results.get(0); - return playlistLoader.loadPlaylist(first.get("id").safeText(), "volumes", trackFactory); - } - private List getResults(JsonBrowser root, String property) { - root = root.get(property); - if (root.isNull()) { - return Collections.emptyList(); - } - root = root.get("results"); - if (root.isNull() || !root.isList()) { - throw new FriendlyException("Invalid search response [2]", FriendlyException.Severity.COMMON, null); + private List getResults(JsonBrowser root, String property) { + root = root.get(property); + if (root.isNull()) { + return Collections.emptyList(); + } + root = root.get("results"); + if (root.isNull() || !root.isList()) { + throw new FriendlyException("Invalid search response [2]", FriendlyException.Severity.COMMON, null); + } + return root.values(); } - return root.values(); - } - private String getValidType(String type) { - if (type == null) { - return "track"; - } - type = type.trim(); - if (type.equalsIgnoreCase("track") - || type.equalsIgnoreCase("playlist") - || type.equalsIgnoreCase("album")) { - return type; + private String getValidType(String type) { + if (type == null) { + return "track"; + } + type = type.trim(); + if (type.equalsIgnoreCase("track") + || type.equalsIgnoreCase("playlist") + || type.equalsIgnoreCase("album")) { + return type; + } + return "track"; } - return "track"; - } - private Integer getValidLimit(String limit) { - try { - if (limit != null) { - int result = Integer.parseInt(limit); - if (result > 0 && result < 100) { - return result; + private Integer getValidLimit(String limit) { + try { + if (limit != null) { + int result = Integer.parseInt(limit); + if (result > 0 && result < 100) { + return result; + } + } + } catch (NumberFormatException e) { + // fall down to default } - } - } catch (NumberFormatException e) { - // fall down to default + return DEFAULT_LIMIT; } - return DEFAULT_LIMIT; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexHttpContextFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexHttpContextFilter.java index 51161204b..267f91020 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexHttpContextFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexHttpContextFilter.java @@ -9,46 +9,46 @@ public class YandexHttpContextFilter implements HttpContextFilter { - private static String oAuthToken = null; + private static String oAuthToken = null; - public static void setOAuthToken(String value) { - oAuthToken = value; - } + public static void setOAuthToken(String value) { + oAuthToken = value; + } + + @Override + public void onContextOpen(HttpClientContext context) { + CookieStore cookieStore = context.getCookieStore(); - @Override - public void onContextOpen(HttpClientContext context) { - CookieStore cookieStore = context.getCookieStore(); + if (cookieStore == null) { + cookieStore = new BasicCookieStore(); + context.setCookieStore(cookieStore); + } - if (cookieStore == null) { - cookieStore = new BasicCookieStore(); - context.setCookieStore(cookieStore); + // Reset cookies for each sequence of requests. + cookieStore.clear(); } - // Reset cookies for each sequence of requests. - cookieStore.clear(); - } - - @Override - public void onContextClose(HttpClientContext context) { - - } - - @Override - public void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition) { - request.setHeader("User-Agent", "Yandex-Music-API"); - request.setHeader("X-Yandex-Music-Client", "WindowsPhone/3.20"); - if (oAuthToken != null) { - request.setHeader("Authorization", "OAuth " + oAuthToken); + @Override + public void onContextClose(HttpClientContext context) { + } - } - @Override - public boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response) { - return false; - } + @Override + public void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition) { + request.setHeader("User-Agent", "Yandex-Music-API"); + request.setHeader("X-Yandex-Music-Client", "WindowsPhone/3.20"); + if (oAuthToken != null) { + request.setHeader("Authorization", "OAuth " + oAuthToken); + } + } - @Override - public boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error) { - return false; - } + @Override + public boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response) { + return false; + } + + @Override + public boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error) { + return false; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicApiLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicApiLoader.java index be3adfb6d..61b1240c7 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicApiLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicApiLoader.java @@ -3,6 +3,7 @@ import com.sedmelluq.discord.lavaplayer.tools.http.ExtendedHttpConfigurable; public interface YandexMusicApiLoader { - ExtendedHttpConfigurable getHttpConfiguration(); - void shutdown(); + ExtendedHttpConfigurable getHttpConfiguration(); + + void shutdown(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicAudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicAudioSourceManager.java index 8782fa1b1..24a4479d8 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicAudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicAudioSourceManager.java @@ -29,185 +29,185 @@ * Audio source manager that implements finding Yandex Music tracks based on URL. */ public class YandexMusicAudioSourceManager implements AudioSourceManager, HttpConfigurable { - private static final String PROTOCOL_REGEX = "https?://"; - private static final String DOMAIN_REGEX = "music\\.yandex\\.[a-zA-Z]+"; - private static final String TRACK_ID_REGEX = "track/([0-9]+)(?:\\?.*|)"; - private static final String ALBUM_ID_REGEX = "album/([0-9]+)(?:\\?.*|)"; - private static final String ARTIST_ID_REGEX = "artist/([0-9]+)(?:/tracks)?(?:\\?.*|)"; - private static final String PLAYLIST_ID_REGEX = "playlists/([0-9]+)(?:\\?.*|)"; - private static final String USER_REGEX = "users/(.+)"; - - private static final Pattern trackUrlPattern = Pattern.compile("^" + - PROTOCOL_REGEX + DOMAIN_REGEX + "/" + - ALBUM_ID_REGEX + "/" + TRACK_ID_REGEX + "$" - ); - private static final Pattern shortTrackUrlPattern = Pattern.compile("^" + - PROTOCOL_REGEX + DOMAIN_REGEX + "/" + - TRACK_ID_REGEX + "$" - ); - private static final Pattern albumUrlPattern = Pattern.compile("^" + - PROTOCOL_REGEX + DOMAIN_REGEX + "/" + - ALBUM_ID_REGEX + "$" - ); - private static final Pattern artistUrlPattern = Pattern.compile("^" + - PROTOCOL_REGEX + DOMAIN_REGEX + "/" + - ARTIST_ID_REGEX + "$" - ); - private static final Pattern playlistUrlPattern = Pattern.compile("^" + - PROTOCOL_REGEX + DOMAIN_REGEX + "/" + - USER_REGEX + "/" + PLAYLIST_ID_REGEX + "$" - ); - - private final boolean allowSearch; - - private final HttpInterfaceManager httpInterfaceManager; - private final ExtendedHttpConfigurable combinedHttpConfiguration; - - private final YandexMusicDirectUrlLoader directUrlLoader; - private final YandexMusicTrackLoader trackLoader; - private final YandexMusicPlaylistLoader playlistLoader; - private final YandexMusicSearchResultLoader searchResultLoader; - - public YandexMusicAudioSourceManager() { - this(true); - } - - public YandexMusicAudioSourceManager(boolean allowSearch) { - this( - allowSearch, - new DefaultYandexMusicTrackLoader(), - new DefaultYandexMusicPlaylistLoader(), - new DefaultYandexMusicDirectUrlLoader(), - new DefaultYandexSearchProvider() + private static final String PROTOCOL_REGEX = "https?://"; + private static final String DOMAIN_REGEX = "music\\.yandex\\.[a-zA-Z]+"; + private static final String TRACK_ID_REGEX = "track/([0-9]+)(?:\\?.*|)"; + private static final String ALBUM_ID_REGEX = "album/([0-9]+)(?:\\?.*|)"; + private static final String ARTIST_ID_REGEX = "artist/([0-9]+)(?:/tracks)?(?:\\?.*|)"; + private static final String PLAYLIST_ID_REGEX = "playlists/([0-9]+)(?:\\?.*|)"; + private static final String USER_REGEX = "users/(.+)"; + + private static final Pattern trackUrlPattern = Pattern.compile("^" + + PROTOCOL_REGEX + DOMAIN_REGEX + "/" + + ALBUM_ID_REGEX + "/" + TRACK_ID_REGEX + "$" ); - } - - /** - * Create an instance. - */ - public YandexMusicAudioSourceManager( - boolean allowSearch, - YandexMusicTrackLoader trackLoader, - YandexMusicPlaylistLoader playlistLoader, - YandexMusicDirectUrlLoader directUrlLoader, - YandexMusicSearchResultLoader searchResultLoader) { - this.allowSearch = allowSearch; - this.trackLoader = trackLoader; - this.playlistLoader = playlistLoader; - this.directUrlLoader = directUrlLoader; - this.searchResultLoader = searchResultLoader; - - httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - httpInterfaceManager.setHttpContextFilter(new YandexHttpContextFilter()); - - combinedHttpConfiguration = new MultiHttpConfigurable(Arrays.asList( - httpInterfaceManager, - trackLoader.getHttpConfiguration(), - playlistLoader.getHttpConfiguration(), - directUrlLoader.getHttpConfiguration(), - searchResultLoader.getHttpConfiguration() - )); - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - Matcher matcher; - if ((matcher = trackUrlPattern.matcher(reference.identifier)).matches()) { - return trackLoader.loadTrack(matcher.group(1), matcher.group(2), this::getTrack); - } - if ((matcher = shortTrackUrlPattern.matcher(reference.identifier)).matches()) { - return trackLoader.loadTrack("", matcher.group(1), this::getTrack); - } - if ((matcher = playlistUrlPattern.matcher(reference.identifier)).matches()) { - return playlistLoader.loadPlaylist(matcher.group(1), matcher.group(2), "tracks", this::getTrack); - } - if ((matcher = albumUrlPattern.matcher(reference.identifier)).matches()) { - return playlistLoader.loadPlaylist(matcher.group(1), "volumes", this::getTrack); - } - if ((matcher = artistUrlPattern.matcher(reference.identifier)).matches()) { - return playlistLoader.loadPlaylist(matcher.group(1), "popularTracks", this::getTrack); - } - if (allowSearch) { - return searchResultLoader.loadSearchResult(reference.identifier, playlistLoader, this::getTrack); - } - return null; - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { - // No special values to encode - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { - return new YandexMusicAudioTrack(trackInfo, this); - } - - public AudioTrack getTrack(AudioTrackInfo info) { - return new YandexMusicAudioTrack(info, this); - } - - @Override - public void shutdown() { - ExceptionTools.closeWithWarnings(httpInterfaceManager); - trackLoader.shutdown(); - playlistLoader.shutdown(); - searchResultLoader.shutdown(); - directUrlLoader.shutdown(); - } - - public YandexMusicDirectUrlLoader getDirectUrlLoader() { - return directUrlLoader; - } - - /** - * @return Get an HTTP interface for a playing track. - */ - public HttpInterface getHttpInterface() { - return httpInterfaceManager.getInterface(); - } - - @Override - public void configureRequests(Function configurator) { - combinedHttpConfiguration.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - combinedHttpConfiguration.configureBuilder(configurator); - } - - public ExtendedHttpConfigurable getHttpConfiguration() { - return combinedHttpConfiguration; - } - - public ExtendedHttpConfigurable getMainHttpConfiguration() { - return httpInterfaceManager; - } - - public ExtendedHttpConfigurable getTrackLHttpConfiguration() { - return trackLoader.getHttpConfiguration(); - } - - public ExtendedHttpConfigurable getPlaylistLHttpConfiguration() { - return playlistLoader.getHttpConfiguration(); - } - - public ExtendedHttpConfigurable getDirectUrlLHttpConfiguration() { - return directUrlLoader.getHttpConfiguration(); - } - - public ExtendedHttpConfigurable getSearchHttpConfiguration() { - return searchResultLoader.getHttpConfiguration(); - } - - @Override - public String getSourceName() { - return "yandex-music"; - } + private static final Pattern shortTrackUrlPattern = Pattern.compile("^" + + PROTOCOL_REGEX + DOMAIN_REGEX + "/" + + TRACK_ID_REGEX + "$" + ); + private static final Pattern albumUrlPattern = Pattern.compile("^" + + PROTOCOL_REGEX + DOMAIN_REGEX + "/" + + ALBUM_ID_REGEX + "$" + ); + private static final Pattern artistUrlPattern = Pattern.compile("^" + + PROTOCOL_REGEX + DOMAIN_REGEX + "/" + + ARTIST_ID_REGEX + "$" + ); + private static final Pattern playlistUrlPattern = Pattern.compile("^" + + PROTOCOL_REGEX + DOMAIN_REGEX + "/" + + USER_REGEX + "/" + PLAYLIST_ID_REGEX + "$" + ); + + private final boolean allowSearch; + + private final HttpInterfaceManager httpInterfaceManager; + private final ExtendedHttpConfigurable combinedHttpConfiguration; + + private final YandexMusicDirectUrlLoader directUrlLoader; + private final YandexMusicTrackLoader trackLoader; + private final YandexMusicPlaylistLoader playlistLoader; + private final YandexMusicSearchResultLoader searchResultLoader; + + public YandexMusicAudioSourceManager() { + this(true); + } + + public YandexMusicAudioSourceManager(boolean allowSearch) { + this( + allowSearch, + new DefaultYandexMusicTrackLoader(), + new DefaultYandexMusicPlaylistLoader(), + new DefaultYandexMusicDirectUrlLoader(), + new DefaultYandexSearchProvider() + ); + } + + /** + * Create an instance. + */ + public YandexMusicAudioSourceManager( + boolean allowSearch, + YandexMusicTrackLoader trackLoader, + YandexMusicPlaylistLoader playlistLoader, + YandexMusicDirectUrlLoader directUrlLoader, + YandexMusicSearchResultLoader searchResultLoader) { + this.allowSearch = allowSearch; + this.trackLoader = trackLoader; + this.playlistLoader = playlistLoader; + this.directUrlLoader = directUrlLoader; + this.searchResultLoader = searchResultLoader; + + httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + httpInterfaceManager.setHttpContextFilter(new YandexHttpContextFilter()); + + combinedHttpConfiguration = new MultiHttpConfigurable(Arrays.asList( + httpInterfaceManager, + trackLoader.getHttpConfiguration(), + playlistLoader.getHttpConfiguration(), + directUrlLoader.getHttpConfiguration(), + searchResultLoader.getHttpConfiguration() + )); + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + Matcher matcher; + if ((matcher = trackUrlPattern.matcher(reference.identifier)).matches()) { + return trackLoader.loadTrack(matcher.group(1), matcher.group(2), this::getTrack); + } + if ((matcher = shortTrackUrlPattern.matcher(reference.identifier)).matches()) { + return trackLoader.loadTrack("", matcher.group(1), this::getTrack); + } + if ((matcher = playlistUrlPattern.matcher(reference.identifier)).matches()) { + return playlistLoader.loadPlaylist(matcher.group(1), matcher.group(2), "tracks", this::getTrack); + } + if ((matcher = albumUrlPattern.matcher(reference.identifier)).matches()) { + return playlistLoader.loadPlaylist(matcher.group(1), "volumes", this::getTrack); + } + if ((matcher = artistUrlPattern.matcher(reference.identifier)).matches()) { + return playlistLoader.loadPlaylist(matcher.group(1), "popularTracks", this::getTrack); + } + if (allowSearch) { + return searchResultLoader.loadSearchResult(reference.identifier, playlistLoader, this::getTrack); + } + return null; + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + // No special values to encode + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new YandexMusicAudioTrack(trackInfo, this); + } + + public AudioTrack getTrack(AudioTrackInfo info) { + return new YandexMusicAudioTrack(info, this); + } + + @Override + public void shutdown() { + ExceptionTools.closeWithWarnings(httpInterfaceManager); + trackLoader.shutdown(); + playlistLoader.shutdown(); + searchResultLoader.shutdown(); + directUrlLoader.shutdown(); + } + + public YandexMusicDirectUrlLoader getDirectUrlLoader() { + return directUrlLoader; + } + + /** + * @return Get an HTTP interface for a playing track. + */ + public HttpInterface getHttpInterface() { + return httpInterfaceManager.getInterface(); + } + + @Override + public void configureRequests(Function configurator) { + combinedHttpConfiguration.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + combinedHttpConfiguration.configureBuilder(configurator); + } + + public ExtendedHttpConfigurable getHttpConfiguration() { + return combinedHttpConfiguration; + } + + public ExtendedHttpConfigurable getMainHttpConfiguration() { + return httpInterfaceManager; + } + + public ExtendedHttpConfigurable getTrackLHttpConfiguration() { + return trackLoader.getHttpConfiguration(); + } + + public ExtendedHttpConfigurable getPlaylistLHttpConfiguration() { + return playlistLoader.getHttpConfiguration(); + } + + public ExtendedHttpConfigurable getDirectUrlLHttpConfiguration() { + return directUrlLoader.getHttpConfiguration(); + } + + public ExtendedHttpConfigurable getSearchHttpConfiguration() { + return searchResultLoader.getHttpConfiguration(); + } + + @Override + public String getSourceName() { + return "yandex-music"; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicAudioTrack.java index 3bdd4af17..8fa99be3f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicAudioTrack.java @@ -17,37 +17,37 @@ * Audio track that handles processing Yandex Music tracks. */ public class YandexMusicAudioTrack extends DelegatedAudioTrack { - private static final Logger log = LoggerFactory.getLogger(YandexMusicAudioTrack.class); - - private final YandexMusicAudioSourceManager sourceManager; - - /** - * @param trackInfo Track info - * @param sourceManager Source manager which was used to find this track - */ - public YandexMusicAudioTrack(AudioTrackInfo trackInfo, YandexMusicAudioSourceManager sourceManager) { - super(trackInfo); - this.sourceManager = sourceManager; - } - - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { - String trackMediaUrl = sourceManager.getDirectUrlLoader().getDirectUrl(trackInfo.identifier, "mp3"); - log.debug("Starting Yandex Music track from URL: {}", trackMediaUrl); - try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(trackMediaUrl), null)) { - processDelegate(new Mp3AudioTrack(trackInfo, stream), localExecutor); - } + private static final Logger log = LoggerFactory.getLogger(YandexMusicAudioTrack.class); + + private final YandexMusicAudioSourceManager sourceManager; + + /** + * @param trackInfo Track info + * @param sourceManager Source manager which was used to find this track + */ + public YandexMusicAudioTrack(AudioTrackInfo trackInfo, YandexMusicAudioSourceManager sourceManager) { + super(trackInfo); + this.sourceManager = sourceManager; + } + + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { + String trackMediaUrl = sourceManager.getDirectUrlLoader().getDirectUrl(trackInfo.identifier, "mp3"); + log.debug("Starting Yandex Music track from URL: {}", trackMediaUrl); + try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(trackMediaUrl), null)) { + processDelegate(new Mp3AudioTrack(trackInfo, stream), localExecutor); + } + } } - } - @Override - public AudioTrack makeClone() { - return new YandexMusicAudioTrack(trackInfo, sourceManager); - } + @Override + public AudioTrack makeClone() { + return new YandexMusicAudioTrack(trackInfo, sourceManager); + } - @Override - public AudioSourceManager getSourceManager() { - return sourceManager; - } + @Override + public AudioSourceManager getSourceManager() { + return sourceManager; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicPlaylistLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicPlaylistLoader.java index 67b9eea7f..676d3bd0d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicPlaylistLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicPlaylistLoader.java @@ -7,6 +7,7 @@ import java.util.function.Function; public interface YandexMusicPlaylistLoader extends YandexMusicApiLoader { - AudioItem loadPlaylist(String login, String id, String trackProperty, Function trackFactory); - AudioItem loadPlaylist(String album, String trackProperty, Function trackFactory); + AudioItem loadPlaylist(String login, String id, String trackProperty, Function trackFactory); + + AudioItem loadPlaylist(String album, String trackProperty, Function trackFactory); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicSearchResultLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicSearchResultLoader.java index 4088c6738..6ad9eeded 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicSearchResultLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicSearchResultLoader.java @@ -7,7 +7,7 @@ import java.util.function.Function; public interface YandexMusicSearchResultLoader extends YandexMusicApiLoader { - AudioItem loadSearchResult(String query, - YandexMusicPlaylistLoader playlistLoader, - Function trackFactory); + AudioItem loadSearchResult(String query, + YandexMusicPlaylistLoader playlistLoader, + Function trackFactory); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicTrackLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicTrackLoader.java index 403b2de48..585d5e259 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicTrackLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicTrackLoader.java @@ -7,5 +7,5 @@ import java.util.function.Function; public interface YandexMusicTrackLoader extends YandexMusicApiLoader { - AudioItem loadTrack(String albumId, String trackId, Function trackFactory); + AudioItem loadTrack(String albumId, String trackId, Function trackFactory); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicUtils.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicUtils.java index 06ed12176..3336f3ff1 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicUtils.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/yamusic/YandexMusicUtils.java @@ -4,55 +4,54 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; -import java.util.Collections; import java.util.function.Function; import java.util.stream.Collectors; public class YandexMusicUtils { - private static final String TRACK_URL_FORMAT = "https://music.yandex.ru/album/%s/track/%s"; - - public static AudioTrack extractTrack(JsonBrowser trackInfo, Function trackFactory) { - if (!trackInfo.get("track").isNull()) { - trackInfo = trackInfo.get("track"); - } - String artists = trackInfo.get("artists").values().stream() - .map(e -> e.get("name").text()) - .collect(Collectors.joining(", ")); - - String trackId = trackInfo.get("id").text(); - - JsonBrowser album = trackInfo.get("albums").index(0); - - String albumId = album.get("id").text(); - - String artworkUrl = null; - JsonBrowser cover = trackInfo.get("coverUri"); - if (!cover.isNull()) { - artworkUrl = "https://" + cover.text().replace("%%", "1000x1000"); - } - if (artworkUrl == null) { - JsonBrowser ogImage = trackInfo.get("ogImage"); - if (!ogImage.isNull()) { - artworkUrl = "https://" + ogImage.text().replace("%%", "1000x1000"); - } + private static final String TRACK_URL_FORMAT = "https://music.yandex.ru/album/%s/track/%s"; + + public static AudioTrack extractTrack(JsonBrowser trackInfo, Function trackFactory) { + if (!trackInfo.get("track").isNull()) { + trackInfo = trackInfo.get("track"); + } + String artists = trackInfo.get("artists").values().stream() + .map(e -> e.get("name").text()) + .collect(Collectors.joining(", ")); + + String trackId = trackInfo.get("id").text(); + + JsonBrowser album = trackInfo.get("albums").index(0); + + String albumId = album.get("id").text(); + + String artworkUrl = null; + JsonBrowser cover = trackInfo.get("coverUri"); + if (!cover.isNull()) { + artworkUrl = "https://" + cover.text().replace("%%", "1000x1000"); + } + if (artworkUrl == null) { + JsonBrowser ogImage = trackInfo.get("ogImage"); + if (!ogImage.isNull()) { + artworkUrl = "https://" + ogImage.text().replace("%%", "1000x1000"); + } + } + + if (artworkUrl == null) { + cover = album.get("coverUri"); + if (!cover.isNull()) { + artworkUrl = "https://" + cover.text().replace("%%", "1000x1000"); + } + } + + return trackFactory.apply(new AudioTrackInfo( + trackInfo.get("title").text(), + artists, + trackInfo.get("durationMs").as(Long.class), + trackInfo.get("id").text(), + false, + String.format(TRACK_URL_FORMAT, albumId, trackId), + artworkUrl, + null + )); } - - if (artworkUrl == null) { - cover = album.get("coverUri"); - if (!cover.isNull()) { - artworkUrl = "https://" + cover.text().replace("%%", "1000x1000"); - } - } - - return trackFactory.apply(new AudioTrackInfo( - trackInfo.get("title").text(), - artists, - trackInfo.get("durationMs").as(Long.class), - trackInfo.get("id").text(), - false, - String.format(TRACK_URL_FORMAT, albumId, trackId), - artworkUrl, - null - )); - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeLinkRouter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeLinkRouter.java index 9aab76439..b3a47a651 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeLinkRouter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeLinkRouter.java @@ -1,162 +1,163 @@ package com.sedmelluq.discord.lavaplayer.source.youtube; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URIBuilder; + import java.net.URISyntaxException; import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Collectors; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URIBuilder; import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.COMMON; public class DefaultYoutubeLinkRouter implements YoutubeLinkRouter { - private static final String SEARCH_PREFIX = "ytsearch:"; - private static final String SEARCH_MUSIC_PREFIX = "ytmsearch:"; - - private static final String PROTOCOL_REGEX = "(?:http://|https://|)"; - private static final String DOMAIN_REGEX = "(?:www\\.|m\\.|music\\.|)youtube\\.com"; - private static final String SHORT_DOMAIN_REGEX = "(?:www\\.|)youtu\\.be"; - private static final String VIDEO_ID_REGEX = "(?[a-zA-Z0-9_-]{11})"; - private static final String PLAYLIST_ID_REGEX = "(?(PL|LL|FL|UU)[a-zA-Z0-9_-]+)"; - - private static final Pattern directVideoIdPattern = Pattern.compile("^" + VIDEO_ID_REGEX + "$"); - - private final Extractor[] extractors = new Extractor[] { - new Extractor(directVideoIdPattern, Routes::track), - new Extractor(Pattern.compile("^" + PLAYLIST_ID_REGEX + "$"), this::routeDirectPlaylist), - new Extractor(Pattern.compile("^" + PROTOCOL_REGEX + DOMAIN_REGEX + "/.*"), this::routeFromMainDomain), - new Extractor(Pattern.compile("^" + PROTOCOL_REGEX + SHORT_DOMAIN_REGEX + "/.*"), this::routeFromShortDomain), - new Extractor(Pattern.compile("^" + PROTOCOL_REGEX + DOMAIN_REGEX + "/embed/.*"), this::routeFromEmbed), - new Extractor(Pattern.compile("^" + PROTOCOL_REGEX + DOMAIN_REGEX + "/shorts/.*"), this::routeFromShorts) - }; - - @Override - public T route(String link, Routes routes) { - if (link.startsWith(SEARCH_PREFIX)) { - return routes.search(link.substring(SEARCH_PREFIX.length()).trim()); - } else if (link.startsWith(SEARCH_MUSIC_PREFIX)) { - return routes.searchMusic(link.substring(SEARCH_MUSIC_PREFIX.length()).trim()); + private static final String SEARCH_PREFIX = "ytsearch:"; + private static final String SEARCH_MUSIC_PREFIX = "ytmsearch:"; + + private static final String PROTOCOL_REGEX = "(?:http://|https://|)"; + private static final String DOMAIN_REGEX = "(?:www\\.|m\\.|music\\.|)youtube\\.com"; + private static final String SHORT_DOMAIN_REGEX = "(?:www\\.|)youtu\\.be"; + private static final String VIDEO_ID_REGEX = "(?[a-zA-Z0-9_-]{11})"; + private static final String PLAYLIST_ID_REGEX = "(?(PL|LL|FL|UU)[a-zA-Z0-9_-]+)"; + + private static final Pattern directVideoIdPattern = Pattern.compile("^" + VIDEO_ID_REGEX + "$"); + + private final Extractor[] extractors = new Extractor[]{ + new Extractor(directVideoIdPattern, Routes::track), + new Extractor(Pattern.compile("^" + PLAYLIST_ID_REGEX + "$"), this::routeDirectPlaylist), + new Extractor(Pattern.compile("^" + PROTOCOL_REGEX + DOMAIN_REGEX + "/.*"), this::routeFromMainDomain), + new Extractor(Pattern.compile("^" + PROTOCOL_REGEX + SHORT_DOMAIN_REGEX + "/.*"), this::routeFromShortDomain), + new Extractor(Pattern.compile("^" + PROTOCOL_REGEX + DOMAIN_REGEX + "/embed/.*"), this::routeFromEmbed), + new Extractor(Pattern.compile("^" + PROTOCOL_REGEX + DOMAIN_REGEX + "/shorts/.*"), this::routeFromShorts) + }; + + @Override + public T route(String link, Routes routes) { + if (link.startsWith(SEARCH_PREFIX)) { + return routes.search(link.substring(SEARCH_PREFIX.length()).trim()); + } else if (link.startsWith(SEARCH_MUSIC_PREFIX)) { + return routes.searchMusic(link.substring(SEARCH_MUSIC_PREFIX.length()).trim()); + } + + for (Extractor extractor : extractors) { + if (extractor.pattern.matcher(link).matches()) { + T item = extractor.router.extract(routes, link); + + if (item != null) { + return item; + } + } + } + + return null; } - for (Extractor extractor : extractors) { - if (extractor.pattern.matcher(link).matches()) { - T item = extractor.router.extract(routes, link); + protected T routeDirectPlaylist(Routes routes, String id) { + return routes.playlist(id, null); + } - if (item != null) { - return item; + protected T routeFromMainDomain(Routes routes, String url) { + UrlInfo urlInfo = getUrlInfo(url, true); + + if ("/watch".equals(urlInfo.path)) { + String videoId = urlInfo.parameters.get("v"); + + if (videoId != null) { + return routeFromUrlWithVideoId(routes, videoId, urlInfo); + } + } else if ("/playlist".equals(urlInfo.path)) { + String playlistId = urlInfo.parameters.get("list"); + + if (playlistId != null) { + return routes.playlist(playlistId, null); + } + } else if ("/watch_videos".equals(urlInfo.path)) { + String videoIds = urlInfo.parameters.get("video_ids"); + if (videoIds != null) { + return routes.anonymous(videoIds); + } } - } + + return null; } - return null; - } - - protected T routeDirectPlaylist(Routes routes, String id) { - return routes.playlist(id, null); - } - - protected T routeFromMainDomain(Routes routes, String url) { - UrlInfo urlInfo = getUrlInfo(url, true); - - if ("/watch".equals(urlInfo.path)) { - String videoId = urlInfo.parameters.get("v"); - - if (videoId != null) { - return routeFromUrlWithVideoId(routes, videoId, urlInfo); - } - } else if ("/playlist".equals(urlInfo.path)) { - String playlistId = urlInfo.parameters.get("list"); - - if (playlistId != null) { - return routes.playlist(playlistId, null); - } - } else if ("/watch_videos".equals(urlInfo.path)) { - String videoIds = urlInfo.parameters.get("video_ids"); - if (videoIds != null) { - return routes.anonymous(videoIds); - } + protected T routeFromUrlWithVideoId(Routes routes, String videoId, UrlInfo urlInfo) { + if (videoId.length() > 11) { + // YouTube allows extra junk in the end, it redirects to the correct video. + videoId = videoId.substring(0, 11); + } + + if (!directVideoIdPattern.matcher(videoId).matches()) { + return routes.none(); + } else if (urlInfo.parameters.containsKey("list")) { + String playlistId = urlInfo.parameters.get("list"); + + if (playlistId.startsWith("RD")) { + return routes.mix(playlistId, videoId); + } else { + return routes.playlist(urlInfo.parameters.get("list"), videoId); + } + } else { + return routes.track(videoId); + } } - return null; - } + protected T routeFromShortDomain(Routes routes, String url) { + UrlInfo urlInfo = getUrlInfo(url, true); + return routeFromUrlWithVideoId(routes, urlInfo.path.substring(1), urlInfo); + } - protected T routeFromUrlWithVideoId(Routes routes, String videoId, UrlInfo urlInfo) { - if (videoId.length() > 11) { - // YouTube allows extra junk in the end, it redirects to the correct video. - videoId = videoId.substring(0, 11); + protected T routeFromEmbed(Routes routes, String url) { + UrlInfo urlInfo = getUrlInfo(url, true); + return routeFromUrlWithVideoId(routes, urlInfo.path.substring(7), urlInfo); } - if (!directVideoIdPattern.matcher(videoId).matches()) { - return routes.none(); - } else if (urlInfo.parameters.containsKey("list")) { - String playlistId = urlInfo.parameters.get("list"); - - if (playlistId.startsWith("RD")) { - return routes.mix(playlistId, videoId); - } else { - return routes.playlist(urlInfo.parameters.get("list"), videoId); - } - } else { - return routes.track(videoId); + protected T routeFromShorts(Routes routes, String url) { + UrlInfo urlInfo = getUrlInfo(url, true); + return routeFromUrlWithVideoId(routes, urlInfo.path.substring(8), urlInfo); } - } - - protected T routeFromShortDomain(Routes routes, String url) { - UrlInfo urlInfo = getUrlInfo(url, true); - return routeFromUrlWithVideoId(routes, urlInfo.path.substring(1), urlInfo); - } - - protected T routeFromEmbed(Routes routes, String url) { - UrlInfo urlInfo = getUrlInfo(url, true); - return routeFromUrlWithVideoId(routes, urlInfo.path.substring(7), urlInfo); - } - - protected T routeFromShorts(Routes routes, String url) { - UrlInfo urlInfo = getUrlInfo(url, true); - return routeFromUrlWithVideoId(routes, urlInfo.path.substring(8), urlInfo); - } - - private static UrlInfo getUrlInfo(String url, boolean retryValidPart) { - try { - if (!url.startsWith("http://") && !url.startsWith("https://")) { - url = "https://" + url; - } - - URIBuilder builder = new URIBuilder(url); - return new UrlInfo(builder.getPath(), builder.getQueryParams().stream() - .filter(it -> it.getValue() != null) - .collect(Collectors.toMap(NameValuePair::getName, NameValuePair::getValue, (a, b) -> a))); - } catch (URISyntaxException e) { - if (retryValidPart) { - return getUrlInfo(url.substring(0, e.getIndex() - 1), false); - } else { - throw new FriendlyException("Not a valid URL: " + url, COMMON, e); - } + + private static UrlInfo getUrlInfo(String url, boolean retryValidPart) { + try { + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://" + url; + } + + URIBuilder builder = new URIBuilder(url); + return new UrlInfo(builder.getPath(), builder.getQueryParams().stream() + .filter(it -> it.getValue() != null) + .collect(Collectors.toMap(NameValuePair::getName, NameValuePair::getValue, (a, b) -> a))); + } catch (URISyntaxException e) { + if (retryValidPart) { + return getUrlInfo(url.substring(0, e.getIndex() - 1), false); + } else { + throw new FriendlyException("Not a valid URL: " + url, COMMON, e); + } + } } - } - private static class UrlInfo { - private final String path; - private final Map parameters; + private static class UrlInfo { + private final String path; + private final Map parameters; - private UrlInfo(String path, Map parameters) { - this.path = path; - this.parameters = parameters; + private UrlInfo(String path, Map parameters) { + this.path = path; + this.parameters = parameters; + } } - } - private static class Extractor { - private final Pattern pattern; - private final ExtractorRouter router; + private static class Extractor { + private final Pattern pattern; + private final ExtractorRouter router; - private Extractor(Pattern pattern, ExtractorRouter router) { - this.pattern = pattern; - this.router = router; + private Extractor(Pattern pattern, ExtractorRouter router) { + this.pattern = pattern; + this.router = router; + } } - } - private interface ExtractorRouter { - T extract(Routes routes, String url); - } + private interface ExtractorRouter { + T extract(Routes routes, String url); + } } 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 f2c862cfa..ceb473c43 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 @@ -26,43 +26,43 @@ import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.COMMON; public class DefaultYoutubePlaylistLoader implements YoutubePlaylistLoader { - private volatile int playlistPageCount = 6; - - @Override - public void setPlaylistPageCount(int playlistPageCount) { - this.playlistPageCount = playlistPageCount; - } - - @Override - public AudioPlaylist load(HttpInterface httpInterface, String playlistId, String selectedVideoId, - Function trackFactory) { - HttpPost post = new HttpPost(BROWSE_URL); - YoutubeClientConfig clientConfig = YoutubeClientConfig.ANDROID.copy() - .withRootField("browseId", "VL" + playlistId) - .setAttribute(httpInterface); - StringEntity payload = new StringEntity(clientConfig.toJsonString(), "UTF-8"); - post.setEntity(payload); - try (CloseableHttpResponse response = httpInterface.execute(post)) { - HttpClientTools.assertSuccessWithContent(response, "playlist response"); - HttpClientTools.assertJsonContentType(response); - - JsonBrowser json = JsonBrowser.parse(response.getEntity().getContent()); - return buildPlaylist(httpInterface, json, selectedVideoId, trackFactory); - } catch (IOException e) { - throw new RuntimeException(e); + private volatile int playlistPageCount = 6; + + @Override + public void setPlaylistPageCount(int playlistPageCount) { + this.playlistPageCount = playlistPageCount; } - } - private AudioPlaylist buildPlaylist(HttpInterface httpInterface, JsonBrowser json, String selectedVideoId, - Function trackFactory) throws IOException { + @Override + public AudioPlaylist load(HttpInterface httpInterface, String playlistId, String selectedVideoId, + Function trackFactory) { + HttpPost post = new HttpPost(BROWSE_URL); + YoutubeClientConfig clientConfig = YoutubeClientConfig.ANDROID.copy() + .withRootField("browseId", "VL" + playlistId) + .setAttribute(httpInterface); + StringEntity payload = new StringEntity(clientConfig.toJsonString(), "UTF-8"); + post.setEntity(payload); + try (CloseableHttpResponse response = httpInterface.execute(post)) { + HttpClientTools.assertSuccessWithContent(response, "playlist response"); + HttpClientTools.assertJsonContentType(response); + + JsonBrowser json = JsonBrowser.parse(response.getEntity().getContent()); + return buildPlaylist(httpInterface, json, selectedVideoId, trackFactory); + } catch (IOException e) { + throw new RuntimeException(e); + } + } - String errorAlertMessage = findErrorAlert(json); + private AudioPlaylist buildPlaylist(HttpInterface httpInterface, JsonBrowser json, String selectedVideoId, + Function trackFactory) throws IOException { - if (errorAlertMessage != null) { - throw new FriendlyException(errorAlertMessage, COMMON, null); - } + String errorAlertMessage = findErrorAlert(json); - String playlistName = json + if (errorAlertMessage != null) { + throw new FriendlyException(errorAlertMessage, COMMON, null); + } + + String playlistName = json .get("header") .get("playlistHeaderRenderer") .get("title") @@ -71,7 +71,7 @@ private AudioPlaylist buildPlaylist(HttpInterface httpInterface, JsonBrowser jso .get("text") .text(); - JsonBrowser playlistVideoList = json + JsonBrowser playlistVideoList = json .get("contents") .get("singleColumnBrowseResultsRenderer") .get("tabs") @@ -83,112 +83,112 @@ private AudioPlaylist buildPlaylist(HttpInterface httpInterface, JsonBrowser jso .index(0) .get("playlistVideoListRenderer"); - List tracks = new ArrayList<>(); - String continuationsToken = extractPlaylistTracks(playlistVideoList, tracks, trackFactory); - int loadCount = 0; - int pageCount = playlistPageCount; - - // Also load the next pages, each result gives us a JSON with separate values for list html and next page loader html - while (continuationsToken != null && ++loadCount < pageCount) { - HttpPost post = new HttpPost(BROWSE_URL); - YoutubeClientConfig clientConfig = YoutubeClientConfig.ANDROID.copy() - .withRootField("continuation", continuationsToken) - .setAttribute(httpInterface); - StringEntity payload = new StringEntity(clientConfig.toJsonString(), "UTF-8"); - post.setEntity(payload); - try (CloseableHttpResponse response = httpInterface.execute(post)) { - HttpClientTools.assertSuccessWithContent(response, "playlist response"); - - JsonBrowser continuationJson = JsonBrowser.parse(response.getEntity().getContent()); - - JsonBrowser playlistVideoListPage = continuationJson.get("continuationContents") - .get("playlistVideoListContinuation"); - - continuationsToken = extractPlaylistTracks(playlistVideoListPage, tracks, trackFactory); - } - } - - return new BasicAudioPlaylist(playlistName, tracks, findSelectedTrack(tracks, selectedVideoId), false); - } - - private String findErrorAlert(JsonBrowser jsonResponse) { - JsonBrowser alerts = jsonResponse.get("alerts"); - - if (!alerts.isNull()) { - for (JsonBrowser alert : alerts.values()) { - JsonBrowser alertInner = alert.get("alertRenderer"); - String type = alertInner.get("type").text(); - - if ("ERROR".equals(type)) { - JsonBrowser textObject = alertInner.get("text"); - - String text; - if (!textObject.get("simpleText").isNull()) { - text = textObject.get("simpleText").text(); - } else { - text = textObject.get("runs").values().stream() - .map(run -> run.get("text").text()) - .collect(Collectors.joining()); - } - - return text; + List tracks = new ArrayList<>(); + String continuationsToken = extractPlaylistTracks(playlistVideoList, tracks, trackFactory); + int loadCount = 0; + int pageCount = playlistPageCount; + + // Also load the next pages, each result gives us a JSON with separate values for list html and next page loader html + while (continuationsToken != null && ++loadCount < pageCount) { + HttpPost post = new HttpPost(BROWSE_URL); + YoutubeClientConfig clientConfig = YoutubeClientConfig.ANDROID.copy() + .withRootField("continuation", continuationsToken) + .setAttribute(httpInterface); + StringEntity payload = new StringEntity(clientConfig.toJsonString(), "UTF-8"); + post.setEntity(payload); + try (CloseableHttpResponse response = httpInterface.execute(post)) { + HttpClientTools.assertSuccessWithContent(response, "playlist response"); + + JsonBrowser continuationJson = JsonBrowser.parse(response.getEntity().getContent()); + + JsonBrowser playlistVideoListPage = continuationJson.get("continuationContents") + .get("playlistVideoListContinuation"); + + continuationsToken = extractPlaylistTracks(playlistVideoListPage, tracks, trackFactory); + } } - } - } - return null; - } - - private AudioTrack findSelectedTrack(List tracks, String selectedVideoId) { - if (selectedVideoId != null) { - for (AudioTrack track : tracks) { - if (selectedVideoId.equals(track.getIdentifier())) { - return track; - } - } + return new BasicAudioPlaylist(playlistName, tracks, findSelectedTrack(tracks, selectedVideoId), false); } - return null; - } - - private String extractPlaylistTracks(JsonBrowser playlistVideoList, List tracks, - Function trackFactory) { - JsonBrowser contents = playlistVideoList.get("contents"); - if (contents.isNull()) return null; - - final List playlistTrackEntries = contents.values(); - for (JsonBrowser track : playlistTrackEntries) { - JsonBrowser item = track.get("playlistVideoRenderer"); - - JsonBrowser shortBylineText = item.get("shortBylineText"); + private String findErrorAlert(JsonBrowser jsonResponse) { + JsonBrowser alerts = jsonResponse.get("alerts"); + + if (!alerts.isNull()) { + for (JsonBrowser alert : alerts.values()) { + JsonBrowser alertInner = alert.get("alertRenderer"); + String type = alertInner.get("type").text(); + + if ("ERROR".equals(type)) { + JsonBrowser textObject = alertInner.get("text"); + + String text; + if (!textObject.get("simpleText").isNull()) { + text = textObject.get("simpleText").text(); + } else { + text = textObject.get("runs").values().stream() + .map(run -> run.get("text").text()) + .collect(Collectors.joining()); + } + + return text; + } + } + } - // If the isPlayable property does not exist, it means the video is removed or private - // If the shortBylineText property does not exist, it means the Track is Region blocked - if (!item.get("isPlayable").isNull() && !shortBylineText.isNull()) { - String videoId = item.get("videoId").text(); - JsonBrowser titleField = item.get("title"); - String title = Optional.ofNullable(titleField.get("simpleText").text()) - .orElse(titleField.get("runs").index(0).get("text").text()); - String author = shortBylineText.get("runs").index(0).get("text").text(); - JsonBrowser lengthSeconds = item.get("lengthSeconds"); - long duration = Units.secondsToMillis(lengthSeconds.asLong(Units.DURATION_SEC_UNKNOWN)); + return null; + } - AudioTrackInfo info = new AudioTrackInfo(title, author, duration, videoId, false, - WATCH_URL_PREFIX + videoId, ThumbnailTools.getYouTubeThumbnail(item, videoId), null); + private AudioTrack findSelectedTrack(List tracks, String selectedVideoId) { + if (selectedVideoId != null) { + for (AudioTrack track : tracks) { + if (selectedVideoId.equals(track.getIdentifier())) { + return track; + } + } + } - tracks.add(trackFactory.apply(info)); - } + return null; } - JsonBrowser continuations = playlistVideoList.get("continuations") + private String extractPlaylistTracks(JsonBrowser playlistVideoList, List tracks, + Function trackFactory) { + JsonBrowser contents = playlistVideoList.get("contents"); + if (contents.isNull()) return null; + + final List playlistTrackEntries = contents.values(); + for (JsonBrowser track : playlistTrackEntries) { + JsonBrowser item = track.get("playlistVideoRenderer"); + + JsonBrowser shortBylineText = item.get("shortBylineText"); + + // If the isPlayable property does not exist, it means the video is removed or private + // If the shortBylineText property does not exist, it means the Track is Region blocked + if (!item.get("isPlayable").isNull() && !shortBylineText.isNull()) { + String videoId = item.get("videoId").text(); + JsonBrowser titleField = item.get("title"); + String title = Optional.ofNullable(titleField.get("simpleText").text()) + .orElse(titleField.get("runs").index(0).get("text").text()); + String author = shortBylineText.get("runs").index(0).get("text").text(); + JsonBrowser lengthSeconds = item.get("lengthSeconds"); + long duration = Units.secondsToMillis(lengthSeconds.asLong(Units.DURATION_SEC_UNKNOWN)); + + AudioTrackInfo info = new AudioTrackInfo(title, author, duration, videoId, false, + WATCH_URL_PREFIX + videoId, ThumbnailTools.getYouTubeThumbnail(item, videoId), null); + + tracks.add(trackFactory.apply(info)); + } + } + + JsonBrowser continuations = playlistVideoList.get("continuations") .index(0) .get("nextContinuationData"); - String continuationsToken; - if (!continuations.isNull()) { - continuationsToken = continuations.get("continuation").text(); - return continuationsToken; - } + String continuationsToken; + if (!continuations.isNull()) { + continuationsToken = continuations.get("continuation").text(); + return continuationsToken; + } - return null; - } + return null; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeTrackDetails.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeTrackDetails.java index 735acc89f..74fca03fe 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeTrackDetails.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeTrackDetails.java @@ -1,15 +1,7 @@ package com.sedmelluq.discord.lavaplayer.source.youtube; -import com.sedmelluq.discord.lavaplayer.source.youtube.format.LegacyAdaptiveFormatsExtractor; -import com.sedmelluq.discord.lavaplayer.source.youtube.format.LegacyDashMpdFormatsExtractor; -import com.sedmelluq.discord.lavaplayer.source.youtube.format.LegacyStreamMapFormatsExtractor; -import com.sedmelluq.discord.lavaplayer.source.youtube.format.StreamingDataFormatsExtractor; -import com.sedmelluq.discord.lavaplayer.source.youtube.format.YoutubeTrackFormatExtractor; -import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools; -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; -import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; -import com.sedmelluq.discord.lavaplayer.tools.ThumbnailTools; -import com.sedmelluq.discord.lavaplayer.tools.Units; +import com.sedmelluq.discord.lavaplayer.source.youtube.format.*; +import com.sedmelluq.discord.lavaplayer.tools.*; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; import org.slf4j.Logger; @@ -23,135 +15,135 @@ import static com.sedmelluq.discord.lavaplayer.tools.Units.DURATION_MS_UNKNOWN; public class DefaultYoutubeTrackDetails implements YoutubeTrackDetails { - private static final Logger log = LoggerFactory.getLogger(DefaultYoutubeTrackDetails.class); - - private static final YoutubeTrackFormatExtractor[] FORMAT_EXTRACTORS = new YoutubeTrackFormatExtractor[] { - new LegacyAdaptiveFormatsExtractor(), - new StreamingDataFormatsExtractor(), - new LegacyDashMpdFormatsExtractor(), - new LegacyStreamMapFormatsExtractor() - }; - - private final String videoId; - private final YoutubeTrackJsonData data; - - public DefaultYoutubeTrackDetails(String videoId, YoutubeTrackJsonData data) { - this.videoId = videoId; - this.data = data; - } - - @Override - public AudioTrackInfo getTrackInfo() { - return loadTrackInfo(); - } - - @Override - public List getFormats(HttpInterface httpInterface, YoutubeSignatureResolver signatureResolver) { - try { - return loadTrackFormats(httpInterface, signatureResolver); - } catch (Exception e) { - throw ExceptionTools.toRuntimeException(e); - } - } - - @Override - public String getPlayerScript() { - return data.playerScriptUrl; - } - - private List loadTrackFormats( - HttpInterface httpInterface, - YoutubeSignatureResolver signatureResolver - ) { - for (YoutubeTrackFormatExtractor extractor : FORMAT_EXTRACTORS) { - List formats = extractor.extract(data, httpInterface, signatureResolver); - - if (!formats.isEmpty()) { - return formats; - } - } + private static final Logger log = LoggerFactory.getLogger(DefaultYoutubeTrackDetails.class); - log.warn( - "Video {} with no detected format field, response {} polymer {}", - videoId, - data.playerResponse.format(), - data.polymerArguments.format() - ); + private static final YoutubeTrackFormatExtractor[] FORMAT_EXTRACTORS = new YoutubeTrackFormatExtractor[]{ + new LegacyAdaptiveFormatsExtractor(), + new StreamingDataFormatsExtractor(), + new LegacyDashMpdFormatsExtractor(), + new LegacyStreamMapFormatsExtractor() + }; - throw new FriendlyException("Unable to play this YouTube track.", SUSPICIOUS, - new IllegalStateException("No track formats found.")); - } + private final String videoId; + private final YoutubeTrackJsonData data; - private AudioTrackInfo loadTrackInfo() { - JsonBrowser playabilityStatus = data.playerResponse.get("playabilityStatus"); + public DefaultYoutubeTrackDetails(String videoId, YoutubeTrackJsonData data) { + this.videoId = videoId; + this.data = data; + } - if ("ERROR".equals(playabilityStatus.get("status").text())) { - throw new FriendlyException(playabilityStatus.get("reason").text(), COMMON, null); + @Override + public AudioTrackInfo getTrackInfo() { + return loadTrackInfo(); } - JsonBrowser videoDetails = data.playerResponse.get("videoDetails"); + @Override + public List getFormats(HttpInterface httpInterface, YoutubeSignatureResolver signatureResolver) { + try { + return loadTrackFormats(httpInterface, signatureResolver); + } catch (Exception e) { + throw ExceptionTools.toRuntimeException(e); + } + } - if (videoDetails.isNull()) { - return loadLegacyTrackInfo(); + @Override + public String getPlayerScript() { + return data.playerScriptUrl; } - TemporalInfo temporalInfo = TemporalInfo.fromRawData( - !playabilityStatus.get("liveStreamability").isNull(), - videoDetails.get("lengthSeconds"), - false - ); + private List loadTrackFormats( + HttpInterface httpInterface, + YoutubeSignatureResolver signatureResolver + ) { + for (YoutubeTrackFormatExtractor extractor : FORMAT_EXTRACTORS) { + List formats = extractor.extract(data, httpInterface, signatureResolver); + + if (!formats.isEmpty()) { + return formats; + } + } + + log.warn( + "Video {} with no detected format field, response {} polymer {}", + videoId, + data.playerResponse.format(), + data.polymerArguments.format() + ); + + throw new FriendlyException("Unable to play this YouTube track.", SUSPICIOUS, + new IllegalStateException("No track formats found.")); + } - return buildTrackInfo(videoId, videoDetails.get("title").text(), videoDetails.get("author").text(), temporalInfo, ThumbnailTools.getYouTubeThumbnail(videoDetails, videoId)); - } + private AudioTrackInfo loadTrackInfo() { + JsonBrowser playabilityStatus = data.playerResponse.get("playabilityStatus"); - private AudioTrackInfo loadLegacyTrackInfo() { - JsonBrowser args = data.polymerArguments; + if ("ERROR".equals(playabilityStatus.get("status").text())) { + throw new FriendlyException(playabilityStatus.get("reason").text(), COMMON, null); + } - if ("fail".equals(args.get("status").text())) { - throw new FriendlyException(args.get("reason").text(), COMMON, null); + JsonBrowser videoDetails = data.playerResponse.get("videoDetails"); + + if (videoDetails.isNull()) { + return loadLegacyTrackInfo(); + } + + TemporalInfo temporalInfo = TemporalInfo.fromRawData( + !playabilityStatus.get("liveStreamability").isNull(), + videoDetails.get("lengthSeconds"), + false + ); + + return buildTrackInfo(videoId, videoDetails.get("title").text(), videoDetails.get("author").text(), temporalInfo, ThumbnailTools.getYouTubeThumbnail(videoDetails, videoId)); } - TemporalInfo temporalInfo = TemporalInfo.fromRawData( - "1".equals(args.get("live_playback").text()), - args.get("length_seconds"), - true - ); + private AudioTrackInfo loadLegacyTrackInfo() { + JsonBrowser args = data.polymerArguments; - return buildTrackInfo(videoId, args.get("title").text(), args.get("author").text(), temporalInfo, ThumbnailTools.getYouTubeThumbnail(args, videoId)); - } + if ("fail".equals(args.get("status").text())) { + throw new FriendlyException(args.get("reason").text(), COMMON, null); + } - private AudioTrackInfo buildTrackInfo(String videoId, String title, String uploader, TemporalInfo temporalInfo, String thumbnail) { - return new AudioTrackInfo(title, uploader, temporalInfo.durationMillis, videoId, temporalInfo.isActiveStream, - WATCH_URL_PREFIX + videoId, thumbnail, null); - } + TemporalInfo temporalInfo = TemporalInfo.fromRawData( + "1".equals(args.get("live_playback").text()), + args.get("length_seconds"), + true + ); - private static class TemporalInfo { - public final boolean isActiveStream; - public final long durationMillis; + return buildTrackInfo(videoId, args.get("title").text(), args.get("author").text(), temporalInfo, ThumbnailTools.getYouTubeThumbnail(args, videoId)); + } - private TemporalInfo(boolean isActiveStream, long durationMillis) { - this.isActiveStream = isActiveStream; - this.durationMillis = durationMillis; + private AudioTrackInfo buildTrackInfo(String videoId, String title, String uploader, TemporalInfo temporalInfo, String thumbnail) { + return new AudioTrackInfo(title, uploader, temporalInfo.durationMillis, videoId, temporalInfo.isActiveStream, + WATCH_URL_PREFIX + videoId, thumbnail, null); } - public static TemporalInfo fromRawData(boolean wasLiveStream, JsonBrowser durationSecondsField, boolean legacy) { - long durationValue = durationSecondsField.asLong(0L); - boolean isActiveStream; - if (wasLiveStream && !legacy) { - // Premiers have total duration info field but acting as usual stream so when we try play it we don't know - // current position of it since YT don't provide such info so assume duration is unknown. - isActiveStream = true; - durationValue = 0; - } else { - // VODs are not really live streams, even though that field in JSON claims they are. If it is actually live, then - // duration is also missing or 0. - isActiveStream = wasLiveStream && durationValue == 0; - } - - return new TemporalInfo( - isActiveStream, - durationValue == 0 ? DURATION_MS_UNKNOWN : Units.secondsToMillis(durationValue) - ); + private static class TemporalInfo { + public final boolean isActiveStream; + public final long durationMillis; + + private TemporalInfo(boolean isActiveStream, long durationMillis) { + this.isActiveStream = isActiveStream; + this.durationMillis = durationMillis; + } + + public static TemporalInfo fromRawData(boolean wasLiveStream, JsonBrowser durationSecondsField, boolean legacy) { + long durationValue = durationSecondsField.asLong(0L); + boolean isActiveStream; + if (wasLiveStream && !legacy) { + // Premiers have total duration info field but acting as usual stream so when we try play it we don't know + // current position of it since YT don't provide such info so assume duration is unknown. + isActiveStream = true; + durationValue = 0; + } else { + // VODs are not really live streams, even though that field in JSON claims they are. If it is actually live, then + // duration is also missing or 0. + isActiveStream = wasLiveStream && durationValue == 0; + } + + return new TemporalInfo( + isActiveStream, + durationValue == 0 ? DURATION_MS_UNKNOWN : Units.secondsToMillis(durationValue) + ); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeTrackDetailsLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeTrackDetailsLoader.java index dff4164f5..4f1195f7c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeTrackDetailsLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/DefaultYoutubeTrackDetailsLoader.java @@ -26,283 +26,283 @@ import static java.nio.charset.StandardCharsets.UTF_8; public class DefaultYoutubeTrackDetailsLoader implements YoutubeTrackDetailsLoader { - private static final Logger log = LoggerFactory.getLogger(DefaultYoutubeTrackDetailsLoader.class); + private static final Logger log = LoggerFactory.getLogger(DefaultYoutubeTrackDetailsLoader.class); - private volatile CachedPlayerScript cachedPlayerScript = null; + private volatile CachedPlayerScript cachedPlayerScript = null; - @Override - public YoutubeTrackDetails loadDetails(HttpInterface httpInterface, String videoId, boolean requireFormats, YoutubeAudioSourceManager sourceManager) { - try { - return load(httpInterface, videoId, requireFormats, sourceManager); - } catch (IOException e) { - throw ExceptionTools.toRuntimeException(e); - } - } - - private YoutubeTrackDetails load( - HttpInterface httpInterface, - String videoId, - boolean requireFormats, - YoutubeAudioSourceManager sourceManager - ) throws IOException { - JsonBrowser mainInfo = loadTrackInfoFromInnertube(httpInterface, videoId, sourceManager, null); - - try { - YoutubeTrackJsonData initialData = loadBaseResponse(mainInfo, httpInterface, videoId, sourceManager); - - if (initialData == null) { - return null; - } - - if (!videoId.equals(initialData.playerResponse.get("videoDetails").get("videoId").text())) { - throw new FriendlyException("Video returned by YouTube isn't what was requested", COMMON, - new IllegalStateException(initialData.playerResponse.format())); - } - - YoutubeTrackJsonData finalData = augmentWithPlayerScript(initialData, httpInterface, videoId, requireFormats); - return new DefaultYoutubeTrackDetails(videoId, finalData); - } catch (FriendlyException e) { - throw e; - } catch (Exception e) { - throw throwWithDebugInfo(log, e, "Error when extracting data", "mainJson", mainInfo.format()); - } - } - - protected YoutubeTrackJsonData loadBaseResponse( - JsonBrowser mainInfo, - HttpInterface httpInterface, - String videoId, - YoutubeAudioSourceManager sourceManager - ) throws IOException { - YoutubeTrackJsonData data = YoutubeTrackJsonData.fromMainResult(mainInfo); - InfoStatus status = checkPlayabilityStatus(data.playerResponse, false); - - if (status == InfoStatus.DOES_NOT_EXIST) { - return null; + @Override + public YoutubeTrackDetails loadDetails(HttpInterface httpInterface, String videoId, boolean requireFormats, YoutubeAudioSourceManager sourceManager) { + try { + return load(httpInterface, videoId, requireFormats, sourceManager); + } catch (IOException e) { + throw ExceptionTools.toRuntimeException(e); + } } - if (status == InfoStatus.PREMIERE_TRAILER) { - JsonBrowser trackInfo = loadTrackInfoFromInnertube(httpInterface, videoId, sourceManager, status); - data = YoutubeTrackJsonData.fromMainResult(trackInfo - .get("playabilityStatus") - .get("errorScreen") - .get("ypcTrailerRenderer") - .get("unserializedPlayerResponse") - ); - status = checkPlayabilityStatus(data.playerResponse, true); + private YoutubeTrackDetails load( + HttpInterface httpInterface, + String videoId, + boolean requireFormats, + YoutubeAudioSourceManager sourceManager + ) throws IOException { + JsonBrowser mainInfo = loadTrackInfoFromInnertube(httpInterface, videoId, sourceManager, null); + + try { + YoutubeTrackJsonData initialData = loadBaseResponse(mainInfo, httpInterface, videoId, sourceManager); + + if (initialData == null) { + return null; + } + + if (!videoId.equals(initialData.playerResponse.get("videoDetails").get("videoId").text())) { + throw new FriendlyException("Video returned by YouTube isn't what was requested", COMMON, + new IllegalStateException(initialData.playerResponse.format())); + } + + YoutubeTrackJsonData finalData = augmentWithPlayerScript(initialData, httpInterface, videoId, requireFormats); + return new DefaultYoutubeTrackDetails(videoId, finalData); + } catch (FriendlyException e) { + throw e; + } catch (Exception e) { + throw throwWithDebugInfo(log, e, "Error when extracting data", "mainJson", mainInfo.format()); + } } - if (status == InfoStatus.REQUIRES_LOGIN) { - JsonBrowser trackInfo = loadTrackInfoFromInnertube(httpInterface, videoId, sourceManager, status); - data = YoutubeTrackJsonData.fromMainResult(trackInfo); - status = checkPlayabilityStatus(data.playerResponse, true); + protected YoutubeTrackJsonData loadBaseResponse( + JsonBrowser mainInfo, + HttpInterface httpInterface, + String videoId, + YoutubeAudioSourceManager sourceManager + ) throws IOException { + YoutubeTrackJsonData data = YoutubeTrackJsonData.fromMainResult(mainInfo); + InfoStatus status = checkPlayabilityStatus(data.playerResponse, false); + + if (status == InfoStatus.DOES_NOT_EXIST) { + return null; + } + + if (status == InfoStatus.PREMIERE_TRAILER) { + JsonBrowser trackInfo = loadTrackInfoFromInnertube(httpInterface, videoId, sourceManager, status); + data = YoutubeTrackJsonData.fromMainResult(trackInfo + .get("playabilityStatus") + .get("errorScreen") + .get("ypcTrailerRenderer") + .get("unserializedPlayerResponse") + ); + status = checkPlayabilityStatus(data.playerResponse, true); + } + + if (status == InfoStatus.REQUIRES_LOGIN) { + JsonBrowser trackInfo = loadTrackInfoFromInnertube(httpInterface, videoId, sourceManager, status); + data = YoutubeTrackJsonData.fromMainResult(trackInfo); + status = checkPlayabilityStatus(data.playerResponse, true); + } + + if (status == InfoStatus.NON_EMBEDDABLE) { + JsonBrowser trackInfo = loadTrackInfoFromInnertube(httpInterface, videoId, sourceManager, status); + data = YoutubeTrackJsonData.fromMainResult(trackInfo); + checkPlayabilityStatus(data.playerResponse, true); + } + + return data; } - if (status == InfoStatus.NON_EMBEDDABLE) { - JsonBrowser trackInfo = loadTrackInfoFromInnertube(httpInterface, videoId, sourceManager, status); - data = YoutubeTrackJsonData.fromMainResult(trackInfo); - checkPlayabilityStatus(data.playerResponse, true); + protected InfoStatus checkPlayabilityStatus(JsonBrowser playerResponse, boolean secondCheck) { + JsonBrowser statusBlock = playerResponse.get("playabilityStatus"); + + if (statusBlock.isNull()) { + throw new RuntimeException("No playability status block."); + } + + String status = statusBlock.get("status").text(); + + if (status == null) { + throw new RuntimeException("No playability status field."); + } else if ("OK".equals(status)) { + return InfoStatus.INFO_PRESENT; + } else if ("ERROR".equals(status)) { + String errorReason = statusBlock.get("reason").text(); + + if (errorReason.contains("This video is unavailable")) { + return InfoStatus.DOES_NOT_EXIST; + } else { + throw new FriendlyException(errorReason, COMMON, null); + } + } else if ("UNPLAYABLE".equals(status)) { + String unplayableReason = getUnplayableReason(statusBlock); + + if (unplayableReason.contains("Playback on other websites has been disabled by the video owner")) { + return InfoStatus.NON_EMBEDDABLE; + } + + throw new FriendlyException(unplayableReason, COMMON, null); + } else if ("LOGIN_REQUIRED".equals(status)) { + String loginReason = statusBlock.get("reason").text(); + + if (loginReason.contains("This video is private")) { + throw new FriendlyException("This is a private video.", COMMON, null); + } + + if (loginReason.contains("This video may be inappropriate for some users") && secondCheck) { + throw new FriendlyException("This video requires age verification.", SUSPICIOUS, + new IllegalStateException("You did not set email and password in YoutubeAudioSourceManager.")); + } + + return InfoStatus.REQUIRES_LOGIN; + } else if ("CONTENT_CHECK_REQUIRED".equals(status)) { + throw new FriendlyException(getUnplayableReason(statusBlock), COMMON, null); + } else if ("LIVE_STREAM_OFFLINE".equals(status)) { + if (!statusBlock.get("errorScreen").get("ypcTrailerRenderer").isNull()) { + return InfoStatus.PREMIERE_TRAILER; + } + + throw new FriendlyException(getUnplayableReason(statusBlock), COMMON, null); + } else { + throw new FriendlyException("This video cannot be viewed anonymously.", COMMON, null); + } } - return data; - } - - protected InfoStatus checkPlayabilityStatus(JsonBrowser playerResponse, boolean secondCheck) { - JsonBrowser statusBlock = playerResponse.get("playabilityStatus"); - - if (statusBlock.isNull()) { - throw new RuntimeException("No playability status block."); + protected enum InfoStatus { + INFO_PRESENT, + REQUIRES_LOGIN, + DOES_NOT_EXIST, + CONTENT_CHECK_REQUIRED, + LIVE_STREAM_OFFLINE, + PREMIERE_TRAILER, + NON_EMBEDDABLE } - String status = statusBlock.get("status").text(); - - if (status == null) { - throw new RuntimeException("No playability status field."); - } else if ("OK".equals(status)) { - return InfoStatus.INFO_PRESENT; - } else if ("ERROR".equals(status)) { - String errorReason = statusBlock.get("reason").text(); - - if (errorReason.contains("This video is unavailable")) { - return InfoStatus.DOES_NOT_EXIST; - } else { - throw new FriendlyException(errorReason, COMMON, null); - } - } else if ("UNPLAYABLE".equals(status)) { - String unplayableReason = getUnplayableReason(statusBlock); - - if (unplayableReason.contains("Playback on other websites has been disabled by the video owner")) { - return InfoStatus.NON_EMBEDDABLE; - } - - throw new FriendlyException(unplayableReason, COMMON, null); - } else if ("LOGIN_REQUIRED".equals(status)) { - String loginReason = statusBlock.get("reason").text(); - - if (loginReason.contains("This video is private")) { - throw new FriendlyException("This is a private video.", COMMON, null); - } - - if (loginReason.contains("This video may be inappropriate for some users") && secondCheck) { - throw new FriendlyException("This video requires age verification.", SUSPICIOUS, - new IllegalStateException("You did not set email and password in YoutubeAudioSourceManager.")); - } - - return InfoStatus.REQUIRES_LOGIN; - } else if ("CONTENT_CHECK_REQUIRED".equals(status)) { - throw new FriendlyException(getUnplayableReason(statusBlock), COMMON, null); - } else if ("LIVE_STREAM_OFFLINE".equals(status)) { - if (!statusBlock.get("errorScreen").get("ypcTrailerRenderer").isNull()) { - return InfoStatus.PREMIERE_TRAILER; - } - - throw new FriendlyException(getUnplayableReason(statusBlock), COMMON, null); - } else { - throw new FriendlyException("This video cannot be viewed anonymously.", COMMON, null); + protected String getUnplayableReason(JsonBrowser statusBlock) { + JsonBrowser playerErrorMessage = statusBlock.get("errorScreen").get("playerErrorMessageRenderer"); + String unplayableReason = statusBlock.get("reason").text(); + + if (!playerErrorMessage.get("subreason").isNull()) { + JsonBrowser subreason = playerErrorMessage.get("subreason"); + + if (!subreason.get("simpleText").isNull()) { + unplayableReason = subreason.get("simpleText").text(); + } else if (!subreason.get("runs").isNull() && subreason.get("runs").isList()) { + StringBuilder reasonBuilder = new StringBuilder(); + subreason.get("runs").values().forEach( + item -> reasonBuilder.append(item.get("text").text()).append('\n') + ); + unplayableReason = reasonBuilder.toString(); + } + } + + return unplayableReason; } - } - - protected enum InfoStatus { - INFO_PRESENT, - REQUIRES_LOGIN, - DOES_NOT_EXIST, - CONTENT_CHECK_REQUIRED, - LIVE_STREAM_OFFLINE, - PREMIERE_TRAILER, - NON_EMBEDDABLE - } - - protected String getUnplayableReason(JsonBrowser statusBlock) { - JsonBrowser playerErrorMessage = statusBlock.get("errorScreen").get("playerErrorMessageRenderer"); - String unplayableReason = statusBlock.get("reason").text(); - - if (!playerErrorMessage.get("subreason").isNull()) { - JsonBrowser subreason = playerErrorMessage.get("subreason"); - - if (!subreason.get("simpleText").isNull()) { - unplayableReason = subreason.get("simpleText").text(); - } else if (!subreason.get("runs").isNull() && subreason.get("runs").isList()) { - StringBuilder reasonBuilder = new StringBuilder(); - subreason.get("runs").values().forEach( - item -> reasonBuilder.append(item.get("text").text()).append('\n') + + protected JsonBrowser loadTrackInfoFromInnertube( + HttpInterface httpInterface, + String videoId, + YoutubeAudioSourceManager sourceManager, + InfoStatus infoStatus + ) throws IOException { + if (cachedPlayerScript == null) fetchScript(videoId, httpInterface); + + YoutubeSignatureCipher playerScriptTimestamp = sourceManager.getSignatureResolver().getExtractedScript( + httpInterface, + cachedPlayerScript.playerScriptUrl ); - unplayableReason = reasonBuilder.toString(); - } + HttpPost post = new HttpPost(PLAYER_URL); + YoutubeClientConfig clientConfig; + + if (infoStatus == InfoStatus.PREMIERE_TRAILER) { + // Android client gives encoded Base64 response to trailer which is also protobuf so we can't decode it + clientConfig = YoutubeClientConfig.WEB.copy(); + } else if (infoStatus == InfoStatus.NON_EMBEDDABLE) { + // Used when age restriction bypass failed, if we have valid auth then most likely this request will be successful + clientConfig = YoutubeClientConfig.ANDROID.copy() + .withRootField("params", PLAYER_PARAMS); + } else if (infoStatus == InfoStatus.REQUIRES_LOGIN) { + // Age restriction bypass + clientConfig = YoutubeClientConfig.TV_EMBEDDED.copy(); + } else { + // Default payload from what we start trying to get required data + clientConfig = YoutubeClientConfig.ANDROID.copy() + .withClientField("clientScreen", CLIENT_SCREEN_EMBED) + .withThirdPartyEmbedUrl(CLIENT_THIRD_PARTY_EMBED) + .withRootField("params", PLAYER_PARAMS); + } + + clientConfig + .withRootField("racyCheckOk", true) + .withRootField("contentCheckOk", true) + .withRootField("videoId", videoId) + .withPlaybackSignatureTimestamp(playerScriptTimestamp.scriptTimestamp) + .setAttribute(httpInterface); + + log.debug("Loading track info with payload: {}", clientConfig.toJsonString()); + + post.setEntity(new StringEntity(clientConfig.toJsonString(), "UTF-8")); + try (CloseableHttpResponse response = httpInterface.execute(post)) { + HttpClientTools.assertSuccessWithContent(response, "video page response"); + + String responseText = EntityUtils.toString(response.getEntity(), UTF_8); + + try { + return JsonBrowser.parse(responseText); + } catch (FriendlyException e) { + throw e; + } catch (Exception e) { + throw new FriendlyException("Received unexpected response from YouTube.", SUSPICIOUS, + new RuntimeException("Failed to parse: " + responseText, e)); + } + } } - return unplayableReason; - } - - protected JsonBrowser loadTrackInfoFromInnertube( - HttpInterface httpInterface, - String videoId, - YoutubeAudioSourceManager sourceManager, - InfoStatus infoStatus - ) throws IOException { - if (cachedPlayerScript == null) fetchScript(videoId, httpInterface); - - YoutubeSignatureCipher playerScriptTimestamp = sourceManager.getSignatureResolver().getExtractedScript( - httpInterface, - cachedPlayerScript.playerScriptUrl - ); - HttpPost post = new HttpPost(PLAYER_URL); - YoutubeClientConfig clientConfig; - - if (infoStatus == InfoStatus.PREMIERE_TRAILER) { - // Android client gives encoded Base64 response to trailer which is also protobuf so we can't decode it - clientConfig = YoutubeClientConfig.WEB.copy(); - } else if (infoStatus == InfoStatus.NON_EMBEDDABLE) { - // Used when age restriction bypass failed, if we have valid auth then most likely this request will be successful - clientConfig = YoutubeClientConfig.ANDROID.copy() - .withRootField("params", PLAYER_PARAMS); - } else if (infoStatus == InfoStatus.REQUIRES_LOGIN) { - // Age restriction bypass - clientConfig = YoutubeClientConfig.TV_EMBEDDED.copy(); - } else { - // Default payload from what we start trying to get required data - clientConfig = YoutubeClientConfig.ANDROID.copy() - .withClientField("clientScreen", CLIENT_SCREEN_EMBED) - .withThirdPartyEmbedUrl(CLIENT_THIRD_PARTY_EMBED) - .withRootField("params", PLAYER_PARAMS); - } + protected YoutubeTrackJsonData augmentWithPlayerScript( + YoutubeTrackJsonData data, + HttpInterface httpInterface, + String videoId, + boolean requireFormats + ) throws IOException { + long now = System.currentTimeMillis(); - clientConfig - .withRootField("racyCheckOk", true) - .withRootField("contentCheckOk", true) - .withRootField("videoId", videoId) - .withPlaybackSignatureTimestamp(playerScriptTimestamp.scriptTimestamp) - .setAttribute(httpInterface); - - log.debug("Loading track info with payload: {}", clientConfig.toJsonString()); - - post.setEntity(new StringEntity(clientConfig.toJsonString(), "UTF-8")); - try (CloseableHttpResponse response = httpInterface.execute(post)) { - HttpClientTools.assertSuccessWithContent(response, "video page response"); - - String responseText = EntityUtils.toString(response.getEntity(), UTF_8); - - try { - return JsonBrowser.parse(responseText); - } catch (FriendlyException e) { - throw e; - } catch (Exception e) { - throw new FriendlyException("Received unexpected response from YouTube.", SUSPICIOUS, - new RuntimeException("Failed to parse: " + responseText, e)); - } - } - } - - protected YoutubeTrackJsonData augmentWithPlayerScript( - YoutubeTrackJsonData data, - HttpInterface httpInterface, - String videoId, - boolean requireFormats - ) throws IOException { - long now = System.currentTimeMillis(); - - if (data.playerScriptUrl != null) { - cachedPlayerScript = new CachedPlayerScript(data.playerScriptUrl, now); - return data; - } else if (!requireFormats) { - return data; - } + if (data.playerScriptUrl != null) { + cachedPlayerScript = new CachedPlayerScript(data.playerScriptUrl, now); + return data; + } else if (!requireFormats) { + return data; + } - CachedPlayerScript cached = cachedPlayerScript; + CachedPlayerScript cached = cachedPlayerScript; - if (cached != null && cached.timestamp + 600000L >= now) { - return data.withPlayerScriptUrl(cached.playerScriptUrl); - } + if (cached != null && cached.timestamp + 600000L >= now) { + return data.withPlayerScriptUrl(cached.playerScriptUrl); + } - return data.withPlayerScriptUrl(fetchScript(videoId, httpInterface)); - } + return data.withPlayerScriptUrl(fetchScript(videoId, httpInterface)); + } - private String fetchScript(String videoId, HttpInterface httpInterface) throws IOException { - long now = System.currentTimeMillis(); + private String fetchScript(String videoId, HttpInterface httpInterface) throws IOException { + long now = System.currentTimeMillis(); - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet("https://www.youtube.com/embed/" + videoId))) { - HttpClientTools.assertSuccessWithContent(response, "youtube embed video id"); + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet("https://www.youtube.com/embed/" + videoId))) { + HttpClientTools.assertSuccessWithContent(response, "youtube embed video id"); - String responseText = EntityUtils.toString(response.getEntity()); - String encodedUrl = DataFormatTools.extractBetween(responseText, "\"jsUrl\":\"", "\""); + String responseText = EntityUtils.toString(response.getEntity()); + String encodedUrl = DataFormatTools.extractBetween(responseText, "\"jsUrl\":\"", "\""); - if (encodedUrl == null) { - throw throwWithDebugInfo(log, null, "no jsUrl found", "html", responseText); - } + if (encodedUrl == null) { + throw throwWithDebugInfo(log, null, "no jsUrl found", "html", responseText); + } - String fetchedPlayerScript = JsonBrowser.parse("{\"url\":\"" + encodedUrl + "\"}").get("url").text(); - cachedPlayerScript = new CachedPlayerScript(fetchedPlayerScript, now); + String fetchedPlayerScript = JsonBrowser.parse("{\"url\":\"" + encodedUrl + "\"}").get("url").text(); + cachedPlayerScript = new CachedPlayerScript(fetchedPlayerScript, now); - return fetchedPlayerScript; + return fetchedPlayerScript; + } } - } - protected static class CachedPlayerScript { - public final String playerScriptUrl; - public final long timestamp; + protected static class CachedPlayerScript { + public final String playerScriptUrl; + public final long timestamp; - public CachedPlayerScript(String playerScriptUrl, long timestamp) { - this.playerScriptUrl = playerScriptUrl; - this.timestamp = timestamp; + public CachedPlayerScript(String playerScriptUrl, long timestamp) { + this.playerScriptUrl = playerScriptUrl; + this.timestamp = timestamp; + } } - } -} \ No newline at end of file +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeAccessTokenTracker.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeAccessTokenTracker.java index 618dfb857..434a6fd86 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeAccessTokenTracker.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeAccessTokenTracker.java @@ -32,484 +32,472 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.ANDROID_AUTH_URL; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.CHECKIN_ACCOUNT_URL; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.LOGIN_ACCOUNT_URL; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.SAVE_ACCOUNT_URL; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.TOKEN_PAYLOAD; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.TOKEN_REFRESH_PAYLOAD; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.TV_AUTH_CODE_PAYLOAD; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.TV_AUTH_CODE_URL; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.TV_AUTH_TOKEN_PAYLOAD; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.TV_AUTH_TOKEN_REFRESH_PAYLOAD; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.TV_AUTH_TOKEN_URL; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.VISITOR_ID_URL; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.YOUTUBE_ORIGIN; +import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.*; import static com.sedmelluq.discord.lavaplayer.tools.DataFormatTools.convertToMapLayout; import static com.sedmelluq.discord.lavaplayer.tools.ExceptionTools.throwWithDebugInfo; import static java.nio.charset.StandardCharsets.UTF_8; public class YoutubeAccessTokenTracker { - private static final Logger log = LoggerFactory.getLogger(YoutubeAccessTokenTracker.class); - - private static final String AUTH_SCRIPT_REGEX = ""; - private static final String IDENTITY_REGEX = "\\{clientId:\"(.+?)\",.+?:\"(.+?)\""; - - private static final Pattern authScriptPattern = Pattern.compile(AUTH_SCRIPT_REGEX); - private static final Pattern identityPattern = Pattern.compile(IDENTITY_REGEX); - - private static final String TOKEN_FETCH_CONTEXT_ATTRIBUTE = "yt-raw"; - private static final long MASTER_TOKEN_REFRESH_INTERVAL = TimeUnit.DAYS.toMillis(7); - private static final long DEFAULT_ACCESS_TOKEN_REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(1); - private static final long VISITOR_ID_REFRESH_INTERVAL = TimeUnit.MINUTES.toMillis(10); - - private final Object tokenLock = new Object(); - private final HttpInterfaceManager httpInterfaceManager; - private final String email; - private final String password; - private String masterToken; - private String accessToken; - private String visitorId; - private long lastMasterTokenUpdate; - private long lastAccessTokenUpdate; - private long lastVisitorIdUpdate; - private long accessTokenRefreshInterval = DEFAULT_ACCESS_TOKEN_REFRESH_INTERVAL; - private boolean loggedAgeRestrictionsWarning = false; - private boolean masterTokenFromTV = false; - private volatile CachedAuthScript cachedAuthScript = null; - - public YoutubeAccessTokenTracker(HttpInterfaceManager httpInterfaceManager, String email, String password) { - this.httpInterfaceManager = httpInterfaceManager; - this.email = email; - this.password = password; - } - - /** - * Updates the master token if more than {@link #MASTER_TOKEN_REFRESH_INTERVAL} time has passed since last updated. - */ - public void updateMasterToken() { - synchronized (tokenLock) { - if (DataFormatTools.isNullOrEmpty(email) && DataFormatTools.isNullOrEmpty(password)) { - if (!loggedAgeRestrictionsWarning) { - log.warn("YouTube auth tokens can't be retrieved because email and password is not set in YoutubeAudioSourceManager, age restricted videos will throw exceptions."); - loggedAgeRestrictionsWarning = true; - } - return; - } - - if (loggedAgeRestrictionsWarning) return; - - long now = System.currentTimeMillis(); - if (now - lastMasterTokenUpdate < MASTER_TOKEN_REFRESH_INTERVAL) { - log.debug("YouTube master token was recently updated, not updating again right away."); - return; - } - - lastMasterTokenUpdate = now; - log.info("Updating YouTube master token (current is {}).", masterToken); + private static final Logger log = LoggerFactory.getLogger(YoutubeAccessTokenTracker.class); + + private static final String AUTH_SCRIPT_REGEX = ""; + private static final String IDENTITY_REGEX = "\\{clientId:\"(.+?)\",.+?:\"(.+?)\""; + + private static final Pattern authScriptPattern = Pattern.compile(AUTH_SCRIPT_REGEX); + private static final Pattern identityPattern = Pattern.compile(IDENTITY_REGEX); + + private static final String TOKEN_FETCH_CONTEXT_ATTRIBUTE = "yt-raw"; + private static final long MASTER_TOKEN_REFRESH_INTERVAL = TimeUnit.DAYS.toMillis(7); + private static final long DEFAULT_ACCESS_TOKEN_REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(1); + private static final long VISITOR_ID_REFRESH_INTERVAL = TimeUnit.MINUTES.toMillis(10); + + private final Object tokenLock = new Object(); + private final HttpInterfaceManager httpInterfaceManager; + private final String email; + private final String password; + private String masterToken; + private String accessToken; + private String visitorId; + private long lastMasterTokenUpdate; + private long lastAccessTokenUpdate; + private long lastVisitorIdUpdate; + private long accessTokenRefreshInterval = DEFAULT_ACCESS_TOKEN_REFRESH_INTERVAL; + private boolean loggedAgeRestrictionsWarning = false; + private boolean masterTokenFromTV = false; + private volatile CachedAuthScript cachedAuthScript = null; + + public YoutubeAccessTokenTracker(HttpInterfaceManager httpInterfaceManager, String email, String password) { + this.httpInterfaceManager = httpInterfaceManager; + this.email = email; + this.password = password; + } - // Don't block main thread since if first auth method failed then we go to second and it's require waiting when user is complete auth. - CompletableFuture.runAsync(() -> { - try { - Thread.sleep(100L); - masterToken = fetchMasterToken(); - log.info("Updating YouTube master token succeeded, new token is {}.", masterToken); - } catch (Exception e) { - log.error("YouTube master token update failed.", e); + /** + * Updates the master token if more than {@link #MASTER_TOKEN_REFRESH_INTERVAL} time has passed since last updated. + */ + public void updateMasterToken() { + synchronized (tokenLock) { + if (DataFormatTools.isNullOrEmpty(email) && DataFormatTools.isNullOrEmpty(password)) { + if (!loggedAgeRestrictionsWarning) { + log.warn("YouTube auth tokens can't be retrieved because email and password is not set in YoutubeAudioSourceManager, age restricted videos will throw exceptions."); + loggedAgeRestrictionsWarning = true; + } + return; + } + + if (loggedAgeRestrictionsWarning) return; + + long now = System.currentTimeMillis(); + if (now - lastMasterTokenUpdate < MASTER_TOKEN_REFRESH_INTERVAL) { + log.debug("YouTube master token was recently updated, not updating again right away."); + return; + } + + lastMasterTokenUpdate = now; + log.info("Updating YouTube master token (current is {}).", masterToken); + + // Don't block main thread since if first auth method failed then we go to second and it's require waiting when user is complete auth. + CompletableFuture.runAsync(() -> { + try { + Thread.sleep(100L); + masterToken = fetchMasterToken(); + log.info("Updating YouTube master token succeeded, new token is {}.", masterToken); + } catch (Exception e) { + log.error("YouTube master token update failed.", e); + } + }); } - }); } - } - - /** - * Updates the access token if more than {@link #accessTokenRefreshInterval} time has passed since last updated. - */ - public void updateAccessToken() { - synchronized (tokenLock) { - if (DataFormatTools.isNullOrEmpty(email) && DataFormatTools.isNullOrEmpty(password)) { - if (!loggedAgeRestrictionsWarning) { - log.warn("YouTube auth tokens can't be retrieved because email and password is not set in YoutubeAudioSourceManager, age restricted videos will throw exceptions."); - loggedAgeRestrictionsWarning = true; + + /** + * Updates the access token if more than {@link #accessTokenRefreshInterval} time has passed since last updated. + */ + public void updateAccessToken() { + synchronized (tokenLock) { + if (DataFormatTools.isNullOrEmpty(email) && DataFormatTools.isNullOrEmpty(password)) { + if (!loggedAgeRestrictionsWarning) { + log.warn("YouTube auth tokens can't be retrieved because email and password is not set in YoutubeAudioSourceManager, age restricted videos will throw exceptions."); + loggedAgeRestrictionsWarning = true; + } + return; + } + + if (DataFormatTools.isNullOrEmpty(masterToken) && loggedAgeRestrictionsWarning) { + return; + } + + long now = System.currentTimeMillis(); + if (now - lastAccessTokenUpdate < accessTokenRefreshInterval) { + log.debug("YouTube access token was recently updated, not updating again right away."); + return; + } + + lastAccessTokenUpdate = now; + log.info("Updating YouTube access token (current is {}).", accessToken); + + try { + accessToken = fetchAccessToken(); + log.info("Updating YouTube access token succeeded, new token is {}, next update will be after {} seconds.", + accessToken, + TimeUnit.MILLISECONDS.toSeconds(accessTokenRefreshInterval) + ); + } catch (Exception e) { + log.error("YouTube access token update failed.", e); + } } - return; - } - - if (DataFormatTools.isNullOrEmpty(masterToken) && loggedAgeRestrictionsWarning) { - return; - } - - long now = System.currentTimeMillis(); - if (now - lastAccessTokenUpdate < accessTokenRefreshInterval) { - log.debug("YouTube access token was recently updated, not updating again right away."); - return; - } - - lastAccessTokenUpdate = now; - log.info("Updating YouTube access token (current is {}).", accessToken); - - try { - accessToken = fetchAccessToken(); - log.info("Updating YouTube access token succeeded, new token is {}, next update will be after {} seconds.", - accessToken, - TimeUnit.MILLISECONDS.toSeconds(accessTokenRefreshInterval) - ); - } catch (Exception e) { - log.error("YouTube access token update failed.", e); - } } - } - - /** - * Updates the visitor id if more than {@link #VISITOR_ID_REFRESH_INTERVAL} time has passed since last updated. - */ - public String updateVisitorId() { - synchronized (tokenLock) { - long now = System.currentTimeMillis(); - if (now - lastVisitorIdUpdate < VISITOR_ID_REFRESH_INTERVAL) { - log.debug("YouTube visitor id was recently updated, not updating again right away."); - return visitorId; - } - - lastVisitorIdUpdate = now; - log.info("Updating YouTube visitor id (current is {}).", visitorId); - - try { - visitorId = fetchVisitorId(); - log.info("Updating YouTube visitor id succeeded, new one is {}, next update will be after {} seconds.", - visitorId, - TimeUnit.MILLISECONDS.toSeconds(VISITOR_ID_REFRESH_INTERVAL) - ); - } catch (Exception e) { - log.error("YouTube visitor id update failed.", e); - } - return visitorId; + /** + * Updates the visitor id if more than {@link #VISITOR_ID_REFRESH_INTERVAL} time has passed since last updated. + */ + public String updateVisitorId() { + synchronized (tokenLock) { + long now = System.currentTimeMillis(); + if (now - lastVisitorIdUpdate < VISITOR_ID_REFRESH_INTERVAL) { + log.debug("YouTube visitor id was recently updated, not updating again right away."); + return visitorId; + } + + lastVisitorIdUpdate = now; + log.info("Updating YouTube visitor id (current is {}).", visitorId); + + try { + visitorId = fetchVisitorId(); + log.info("Updating YouTube visitor id succeeded, new one is {}, next update will be after {} seconds.", + visitorId, + TimeUnit.MILLISECONDS.toSeconds(VISITOR_ID_REFRESH_INTERVAL) + ); + } catch (Exception e) { + log.error("YouTube visitor id update failed.", e); + } + + return visitorId; + } } - } - public String getMasterToken() { - synchronized (tokenLock) { - if (masterToken == null) { - updateMasterToken(); - } + public String getMasterToken() { + synchronized (tokenLock) { + if (masterToken == null) { + updateMasterToken(); + } - return masterToken; + return masterToken; + } } - } - public String getAccessToken() { - synchronized (tokenLock) { - if (accessToken == null) { - updateAccessToken(); - } + public String getAccessToken() { + synchronized (tokenLock) { + if (accessToken == null) { + updateAccessToken(); + } - return accessToken; + return accessToken; + } } - } - public String getVisitorId() { - synchronized (tokenLock) { - if (visitorId == null) { - updateVisitorId(); - } + public String getVisitorId() { + synchronized (tokenLock) { + if (visitorId == null) { + updateVisitorId(); + } - return visitorId; + return visitorId; + } } - } - public boolean isTokenFetchContext(HttpClientContext context) { - return context.getAttribute(TOKEN_FETCH_CONTEXT_ATTRIBUTE) == Boolean.TRUE; - } + public boolean isTokenFetchContext(HttpClientContext context) { + return context.getAttribute(TOKEN_FETCH_CONTEXT_ATTRIBUTE) == Boolean.TRUE; + } - private String fetchMasterToken() throws IOException { - try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { - httpInterface.getContext().setAttribute(TOKEN_FETCH_CONTEXT_ATTRIBUTE, true); + private String fetchMasterToken() throws IOException { + try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { + httpInterface.getContext().setAttribute(TOKEN_FETCH_CONTEXT_ATTRIBUTE, true); - return requestMasterToken(httpInterface); + return requestMasterToken(httpInterface); + } } - } - private String fetchAccessToken() throws IOException { - try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { - httpInterface.getContext().setAttribute(TOKEN_FETCH_CONTEXT_ATTRIBUTE, true); + private String fetchAccessToken() throws IOException { + try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { + httpInterface.getContext().setAttribute(TOKEN_FETCH_CONTEXT_ATTRIBUTE, true); - return requestAccessToken(httpInterface); + return requestAccessToken(httpInterface); + } } - } - private String fetchVisitorId() throws IOException { - try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { - httpInterface.getContext().setAttribute(TOKEN_FETCH_CONTEXT_ATTRIBUTE, true); + private String fetchVisitorId() throws IOException { + try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { + httpInterface.getContext().setAttribute(TOKEN_FETCH_CONTEXT_ATTRIBUTE, true); - YoutubeClientConfig clientConfig = YoutubeClientConfig.ANDROID.copy().setAttribute(httpInterface); - HttpPost visitorIdPost = new HttpPost(VISITOR_ID_URL); - StringEntity visitorIdPayload = new StringEntity(clientConfig.toJsonString(), "UTF-8"); - visitorIdPost.setEntity(visitorIdPayload); + YoutubeClientConfig clientConfig = YoutubeClientConfig.ANDROID.copy().setAttribute(httpInterface); + HttpPost visitorIdPost = new HttpPost(VISITOR_ID_URL); + StringEntity visitorIdPayload = new StringEntity(clientConfig.toJsonString(), "UTF-8"); + visitorIdPost.setEntity(visitorIdPayload); - try (CloseableHttpResponse response = httpInterface.execute(visitorIdPost)) { - HttpClientTools.assertSuccessWithContent(response, "youtube visitor id"); + try (CloseableHttpResponse response = httpInterface.execute(visitorIdPost)) { + HttpClientTools.assertSuccessWithContent(response, "youtube visitor id"); - String responseText = EntityUtils.toString(response.getEntity()); - JsonBrowser json = JsonBrowser.parse(responseText); + String responseText = EntityUtils.toString(response.getEntity()); + JsonBrowser json = JsonBrowser.parse(responseText); - return json.get("responseContext").get("visitorData").text(); - } - } - } - - private String requestMasterToken(HttpInterface httpInterface) throws IOException { - HttpPost masterTokenPost = new HttpPost(LOGIN_ACCOUNT_URL); - StringEntity masterTokenPayload = new StringEntity(String.format(TOKEN_PAYLOAD, email, password)); - masterTokenPost.setEntity(masterTokenPayload); - - try (CloseableHttpResponse masterTokenResponse = httpInterface.execute(masterTokenPost)) { - String responseText = EntityUtils.toString(masterTokenResponse.getEntity(), UTF_8); - JsonBrowser jsonBrowser = JsonBrowser.parse(responseText); - - if (masterTokenResponse.getStatusLine().getStatusCode() == 400) { - loggedAgeRestrictionsWarning = true; - } - - HttpClientTools.assertSuccessWithContent(masterTokenResponse, "login account response [" + jsonBrowser.get("exception").safeText() + "]"); - - if (jsonBrowser.get("tv").asBoolean(false)) { - masterTokenFromTV = true; - return jsonBrowser.get("refresh_token").text(); - } else { - String services = jsonBrowser.get("services").text(); - if (!jsonBrowser.get("continueUrl").isNull()) { - return continueUrl(httpInterface, jsonBrowser); - } else if (!services.contains("android") || !services.contains("youtube")) { - createAndroidAccount(httpInterface, jsonBrowser); + return json.get("responseContext").get("visitorData").text(); + } } - } + } - return jsonBrowser.get("aas_et").text(); + private String requestMasterToken(HttpInterface httpInterface) throws IOException { + HttpPost masterTokenPost = new HttpPost(LOGIN_ACCOUNT_URL); + StringEntity masterTokenPayload = new StringEntity(String.format(TOKEN_PAYLOAD, email, password)); + masterTokenPost.setEntity(masterTokenPayload); + + try (CloseableHttpResponse masterTokenResponse = httpInterface.execute(masterTokenPost)) { + String responseText = EntityUtils.toString(masterTokenResponse.getEntity(), UTF_8); + JsonBrowser jsonBrowser = JsonBrowser.parse(responseText); + + if (masterTokenResponse.getStatusLine().getStatusCode() == 400) { + loggedAgeRestrictionsWarning = true; + } + + HttpClientTools.assertSuccessWithContent(masterTokenResponse, "login account response [" + jsonBrowser.get("exception").safeText() + "]"); + + if (jsonBrowser.get("tv").asBoolean(false)) { + masterTokenFromTV = true; + return jsonBrowser.get("refresh_token").text(); + } else { + String services = jsonBrowser.get("services").text(); + if (!jsonBrowser.get("continueUrl").isNull()) { + return continueUrl(httpInterface, jsonBrowser); + } else if (!services.contains("android") || !services.contains("youtube")) { + createAndroidAccount(httpInterface, jsonBrowser); + } + } + + return jsonBrowser.get("aas_et").text(); + } } - } - - private String requestAccessToken(HttpInterface httpInterface) throws IOException { - if (masterTokenFromTV) { - if (cachedAuthScript == null) fetchTVScript(httpInterface); - - HttpPost post = new HttpPost(TV_AUTH_TOKEN_URL); - post.setEntity(new StringEntity(String.format(TV_AUTH_TOKEN_REFRESH_PAYLOAD, - cachedAuthScript.clientId, - cachedAuthScript.clientSecret, - masterToken - ), "UTF-8")); - - try (CloseableHttpResponse response = httpInterface.execute(post)) { - HttpClientTools.assertSuccessWithContent(response, "access token tv response"); - - String responseText = EntityUtils.toString(response.getEntity(), UTF_8); - JsonBrowser responseJson = JsonBrowser.parse(responseText); - - accessTokenRefreshInterval = TimeUnit.SECONDS.toMillis(responseJson.get("expires_in").asLong(DEFAULT_ACCESS_TOKEN_REFRESH_INTERVAL)); - return responseJson.get("access_token").text(); - } - } else { - List params = new ArrayList<>(); - params.add(new BasicNameValuePair("app", "com.google.android.youtube")); - params.add(new BasicNameValuePair("client_sig", "24bb24c05e47e0aefa68a58a766179d9b613a600")); - params.add(new BasicNameValuePair("google_play_services_version", "214516005")); - params.add(new BasicNameValuePair("service", "oauth2:https://www.googleapis.com/auth/youtube")); - params.add(new BasicNameValuePair("Token", masterToken)); - HttpPost post = new HttpPost(buildUri(ANDROID_AUTH_URL, params)); - - try (CloseableHttpResponse response = httpInterface.execute(post)) { - HttpClientTools.assertSuccessWithContent(response, "access token android response"); - - Map map = convertToMapLayout(EntityUtils.toString(response.getEntity())); - accessTokenRefreshInterval = TimeUnit.SECONDS.toMillis(Long.parseLong(map.get("ExpiresInDurationSec"))); - return map.get("Auth"); - } + + private String requestAccessToken(HttpInterface httpInterface) throws IOException { + if (masterTokenFromTV) { + if (cachedAuthScript == null) fetchTVScript(httpInterface); + + HttpPost post = new HttpPost(TV_AUTH_TOKEN_URL); + post.setEntity(new StringEntity(String.format(TV_AUTH_TOKEN_REFRESH_PAYLOAD, + cachedAuthScript.clientId, + cachedAuthScript.clientSecret, + masterToken + ), "UTF-8")); + + try (CloseableHttpResponse response = httpInterface.execute(post)) { + HttpClientTools.assertSuccessWithContent(response, "access token tv response"); + + String responseText = EntityUtils.toString(response.getEntity(), UTF_8); + JsonBrowser responseJson = JsonBrowser.parse(responseText); + + accessTokenRefreshInterval = TimeUnit.SECONDS.toMillis(responseJson.get("expires_in").asLong(DEFAULT_ACCESS_TOKEN_REFRESH_INTERVAL)); + return responseJson.get("access_token").text(); + } + } else { + List params = new ArrayList<>(); + params.add(new BasicNameValuePair("app", "com.google.android.youtube")); + params.add(new BasicNameValuePair("client_sig", "24bb24c05e47e0aefa68a58a766179d9b613a600")); + params.add(new BasicNameValuePair("google_play_services_version", "214516005")); + params.add(new BasicNameValuePair("service", "oauth2:https://www.googleapis.com/auth/youtube")); + params.add(new BasicNameValuePair("Token", masterToken)); + HttpPost post = new HttpPost(buildUri(ANDROID_AUTH_URL, params)); + + try (CloseableHttpResponse response = httpInterface.execute(post)) { + HttpClientTools.assertSuccessWithContent(response, "access token android response"); + + Map map = convertToMapLayout(EntityUtils.toString(response.getEntity())); + accessTokenRefreshInterval = TimeUnit.SECONDS.toMillis(Long.parseLong(map.get("ExpiresInDurationSec"))); + return map.get("Auth"); + } + } } - } - private void createAndroidAccount(HttpInterface httpInterface, JsonBrowser jsonBrowser) throws IOException { - log.info("Account " + jsonBrowser.get("email").text() + " don't have Android or YouTube profile, creating new one..."); + private void createAndroidAccount(HttpInterface httpInterface, JsonBrowser jsonBrowser) throws IOException { + log.info("Account " + jsonBrowser.get("email").text() + " don't have Android or YouTube profile, creating new one..."); - HttpPost post = new HttpPost(CHECKIN_ACCOUNT_URL); - StringEntity payload = new StringEntity(String.format(TOKEN_PAYLOAD, email, password)); - post.setEntity(payload); + HttpPost post = new HttpPost(CHECKIN_ACCOUNT_URL); + StringEntity payload = new StringEntity(String.format(TOKEN_PAYLOAD, email, password)); + post.setEntity(payload); - try (CloseableHttpResponse response = httpInterface.execute(post)) { - HttpClientTools.assertSuccessWithContent(response, "creating android profile response"); + try (CloseableHttpResponse response = httpInterface.execute(post)) { + HttpClientTools.assertSuccessWithContent(response, "creating android profile response"); + } } - } - private String continueUrl(HttpInterface httpInterface, JsonBrowser jsonBrowser) throws IOException { - log.warn("Not successful attempt to login into account " + jsonBrowser.get("email").text() + ", trying obtain oauth2 token through continue url..."); + private String continueUrl(HttpInterface httpInterface, JsonBrowser jsonBrowser) throws IOException { + log.warn("Not successful attempt to login into account " + jsonBrowser.get("email").text() + ", trying obtain oauth2 token through continue url..."); - HttpPost post = new HttpPost(jsonBrowser.get("continueUrl").text()); - RequestConfig config = RequestConfig.custom().setCookieSpec(CookieSpecs.NETSCAPE).setRedirectsEnabled(true).build(); - post.setConfig(config); + HttpPost post = new HttpPost(jsonBrowser.get("continueUrl").text()); + RequestConfig config = RequestConfig.custom().setCookieSpec(CookieSpecs.NETSCAPE).setRedirectsEnabled(true).build(); + post.setConfig(config); - try (CloseableHttpResponse response = httpInterface.execute(post)) { - HttpClientTools.assertSuccessWithRedirectContent(response, "oauth2 redirect response"); + try (CloseableHttpResponse response = httpInterface.execute(post)) { + HttpClientTools.assertSuccessWithRedirectContent(response, "oauth2 redirect response"); - URI redirect = httpInterface.getFinalLocation(); - try (CloseableHttpResponse redirectResponse = httpInterface.execute(new HttpGet(redirect))) { - return exchangeOAuth2Token(httpInterface, redirectResponse); - } + URI redirect = httpInterface.getFinalLocation(); + try (CloseableHttpResponse redirectResponse = httpInterface.execute(new HttpGet(redirect))) { + return exchangeOAuth2Token(httpInterface, redirectResponse); + } + } } - } - private String exchangeOAuth2Token(HttpInterface httpInterface, CloseableHttpResponse response) throws IOException { - for (Header header : response.getAllHeaders()) { - if (header.getName().contains("Set-Cookie") && header.getValue().contains("oauth_token")) { - String oauthToken = DataFormatTools.extractBetween(header.toString(), "oauth_token=", ";"); + private String exchangeOAuth2Token(HttpInterface httpInterface, CloseableHttpResponse response) throws IOException { + for (Header header : response.getAllHeaders()) { + if (header.getName().contains("Set-Cookie") && header.getValue().contains("oauth_token")) { + String oauthToken = DataFormatTools.extractBetween(header.toString(), "oauth_token=", ";"); - List params = new ArrayList<>(); - params.add(new BasicNameValuePair("Token", oauthToken)); - params.add(new BasicNameValuePair("ACCESS_TOKEN", "1")); - params.add(new BasicNameValuePair("service", "ac2dm")); - HttpPost post = new HttpPost(buildUri(ANDROID_AUTH_URL, params)); + List params = new ArrayList<>(); + params.add(new BasicNameValuePair("Token", oauthToken)); + params.add(new BasicNameValuePair("ACCESS_TOKEN", "1")); + params.add(new BasicNameValuePair("service", "ac2dm")); + HttpPost post = new HttpPost(buildUri(ANDROID_AUTH_URL, params)); - try (CloseableHttpResponse exchangeResponse = httpInterface.execute(post)) { - HttpClientTools.assertSuccessWithContent(exchangeResponse, "exchange oauth2 token response"); + try (CloseableHttpResponse exchangeResponse = httpInterface.execute(post)) { + HttpClientTools.assertSuccessWithContent(exchangeResponse, "exchange oauth2 token response"); - Map map = convertToMapLayout(EntityUtils.toString(exchangeResponse.getEntity())); - return map.get("Token"); + Map map = convertToMapLayout(EntityUtils.toString(exchangeResponse.getEntity())); + return map.get("Token"); + } + } } - } - } - // In case if first auth method failed, start trying second one - log.warn("First auth method failed, trying second one..."); - return requestAuthCode(httpInterface, fetchTVScript(httpInterface)); - } + // In case if first auth method failed, start trying second one + log.warn("First auth method failed, trying second one..."); + return requestAuthCode(httpInterface, fetchTVScript(httpInterface)); + } - private CachedAuthScript fetchTVScript(HttpInterface httpInterface) throws IOException { - HttpGet get = new HttpGet(YOUTUBE_ORIGIN + "/tv"); - get.setHeader("User-Agent", "Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version"); + private CachedAuthScript fetchTVScript(HttpInterface httpInterface) throws IOException { + HttpGet get = new HttpGet(YOUTUBE_ORIGIN + "/tv"); + get.setHeader("User-Agent", "Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version"); - try (CloseableHttpResponse response = httpInterface.execute(get)) { - HttpClientTools.assertSuccessWithContent(response, "youtube tv page response"); + try (CloseableHttpResponse response = httpInterface.execute(get)) { + HttpClientTools.assertSuccessWithContent(response, "youtube tv page response"); - String responseText = EntityUtils.toString(response.getEntity()); - Matcher authScript = authScriptPattern.matcher(responseText); + String responseText = EntityUtils.toString(response.getEntity()); + Matcher authScript = authScriptPattern.matcher(responseText); - if (!authScript.find()) { - throw throwWithDebugInfo(log, null, "no base-js found", "html", responseText); - } + if (!authScript.find()) { + throw throwWithDebugInfo(log, null, "no base-js found", "html", responseText); + } - return extractIdentity(httpInterface, authScript.group(1)); + return extractIdentity(httpInterface, authScript.group(1)); + } } - } - private CachedAuthScript extractIdentity(HttpInterface httpInterface, String scriptUrl) throws IOException { - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(YOUTUBE_ORIGIN + scriptUrl))) { - HttpClientTools.assertSuccessWithContent(response, "tv script response"); + private CachedAuthScript extractIdentity(HttpInterface httpInterface, String scriptUrl) throws IOException { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(YOUTUBE_ORIGIN + scriptUrl))) { + HttpClientTools.assertSuccessWithContent(response, "tv script response"); - String responseText = EntityUtils.toString(response.getEntity()); - Matcher identity = identityPattern.matcher(responseText); + String responseText = EntityUtils.toString(response.getEntity()); + Matcher identity = identityPattern.matcher(responseText); - if (!identity.find()) { - throw throwWithDebugInfo(log, null, "no identity in base-js found", "js", responseText); - } + if (!identity.find()) { + throw throwWithDebugInfo(log, null, "no identity in base-js found", "js", responseText); + } - return cachedAuthScript = new CachedAuthScript(identity.group(1), identity.group(2)); + return cachedAuthScript = new CachedAuthScript(identity.group(1), identity.group(2)); + } } - } - private String requestAuthCode(HttpInterface httpInterface, CachedAuthScript script) throws IOException { - HttpPost post = new HttpPost(TV_AUTH_CODE_URL); - post.setEntity(new StringEntity(String.format(TV_AUTH_CODE_PAYLOAD, script.clientId, UUID.randomUUID()), "UTF-8")); + private String requestAuthCode(HttpInterface httpInterface, CachedAuthScript script) throws IOException { + HttpPost post = new HttpPost(TV_AUTH_CODE_URL); + post.setEntity(new StringEntity(String.format(TV_AUTH_CODE_PAYLOAD, script.clientId, UUID.randomUUID()), "UTF-8")); - try (CloseableHttpResponse response = httpInterface.execute(post)) { - HttpClientTools.assertSuccessWithContent(response, "auth code response"); + try (CloseableHttpResponse response = httpInterface.execute(post)) { + HttpClientTools.assertSuccessWithContent(response, "auth code response"); - String responseText = EntityUtils.toString(response.getEntity(), UTF_8); - JsonBrowser responseJson = JsonBrowser.parse(responseText); + String responseText = EntityUtils.toString(response.getEntity(), UTF_8); + JsonBrowser responseJson = JsonBrowser.parse(responseText); - return waitForAuth(httpInterface, responseJson, script); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - private String waitForAuth(HttpInterface httpInterface, JsonBrowser json, CachedAuthScript script) throws IOException, InterruptedException { - log.info("Open your browser, go to {} and enter code {}, this is required to complete auth in provided account," + - " usually this needed to be done once," + - " LavaPlayer will wait and check for auth completion every 5 seconds.", - json.get("verification_url").text(), - json.get("user_code").text() - ); - Thread.sleep(5000L); - - HttpPost authPost = new HttpPost(TV_AUTH_TOKEN_URL); - authPost.setEntity(new StringEntity(String.format(TV_AUTH_TOKEN_PAYLOAD, - script.clientId, - script.clientSecret, - json.get("device_code").text() - ), "UTF-8")); - - try (CloseableHttpResponse authResponse = httpInterface.execute(authPost)) { - HttpClientTools.assertSuccessWithContent(authResponse, "auth wait response"); - - String responseText = EntityUtils.toString(authResponse.getEntity(), UTF_8); - JsonBrowser responseJson = JsonBrowser.parse(responseText); - JsonBrowser errorJson = responseJson.get("error"); - - if (!errorJson.isNull()) { - String text = errorJson.text(); - if ("authorization_pending".equals(text)) { - return waitForAuth(httpInterface, json, script); - } else if ("expired_token".equals(text)) { - log.warn("Token was expired, new one will be generated..."); - return requestAuthCode(httpInterface, script); - } else if ("access_denied".equals(text)) { - throw new RuntimeException("Auth access was denied, second auth method failed."); - } else if ("slow_down".equals(text)) { - throw new RuntimeException("You are being rate limited, second auth method failed."); - } else { - throw new RuntimeException(String.format("Unknown response from auth (%s)", errorJson.text())); + return waitForAuth(httpInterface, responseJson, script); + } catch (InterruptedException e) { + throw new RuntimeException(e); } - } else { - String refreshToken = responseJson.get("refresh_token").text(); - HttpPost savePost = new HttpPost(SAVE_ACCOUNT_URL); - savePost.setEntity(new StringEntity(String.format(TOKEN_REFRESH_PAYLOAD, - email, - password, - refreshToken + } + + private String waitForAuth(HttpInterface httpInterface, JsonBrowser json, CachedAuthScript script) throws IOException, InterruptedException { + log.info("Open your browser, go to {} and enter code {}, this is required to complete auth in provided account," + + " usually this needed to be done once," + + " LavaPlayer will wait and check for auth completion every 5 seconds.", + json.get("verification_url").text(), + json.get("user_code").text() + ); + Thread.sleep(5000L); + + HttpPost authPost = new HttpPost(TV_AUTH_TOKEN_URL); + authPost.setEntity(new StringEntity(String.format(TV_AUTH_TOKEN_PAYLOAD, + script.clientId, + script.clientSecret, + json.get("device_code").text() ), "UTF-8")); - try (CloseableHttpResponse saveResponse = httpInterface.execute(savePost)) { - HttpClientTools.assertSuccessWithContent(saveResponse, "auth save response"); - - accessToken = responseJson.get("access_token").text(); - accessTokenRefreshInterval = TimeUnit.SECONDS.toMillis(responseJson.get("expires_in").asLong(DEFAULT_ACCESS_TOKEN_REFRESH_INTERVAL)); - lastAccessTokenUpdate = System.currentTimeMillis(); - masterTokenFromTV = true; - log.info("Auth was successful and updating YouTube access token succeeded, new token is {}, next update will be after {} seconds.", - accessToken, - TimeUnit.MILLISECONDS.toSeconds(accessTokenRefreshInterval) - ); - return refreshToken; + try (CloseableHttpResponse authResponse = httpInterface.execute(authPost)) { + HttpClientTools.assertSuccessWithContent(authResponse, "auth wait response"); + + String responseText = EntityUtils.toString(authResponse.getEntity(), UTF_8); + JsonBrowser responseJson = JsonBrowser.parse(responseText); + JsonBrowser errorJson = responseJson.get("error"); + + if (!errorJson.isNull()) { + String text = errorJson.text(); + if ("authorization_pending".equals(text)) { + return waitForAuth(httpInterface, json, script); + } else if ("expired_token".equals(text)) { + log.warn("Token was expired, new one will be generated..."); + return requestAuthCode(httpInterface, script); + } else if ("access_denied".equals(text)) { + throw new RuntimeException("Auth access was denied, second auth method failed."); + } else if ("slow_down".equals(text)) { + throw new RuntimeException("You are being rate limited, second auth method failed."); + } else { + throw new RuntimeException(String.format("Unknown response from auth (%s)", errorJson.text())); + } + } else { + String refreshToken = responseJson.get("refresh_token").text(); + HttpPost savePost = new HttpPost(SAVE_ACCOUNT_URL); + savePost.setEntity(new StringEntity(String.format(TOKEN_REFRESH_PAYLOAD, + email, + password, + refreshToken + ), "UTF-8")); + + try (CloseableHttpResponse saveResponse = httpInterface.execute(savePost)) { + HttpClientTools.assertSuccessWithContent(saveResponse, "auth save response"); + + accessToken = responseJson.get("access_token").text(); + accessTokenRefreshInterval = TimeUnit.SECONDS.toMillis(responseJson.get("expires_in").asLong(DEFAULT_ACCESS_TOKEN_REFRESH_INTERVAL)); + lastAccessTokenUpdate = System.currentTimeMillis(); + masterTokenFromTV = true; + log.info("Auth was successful and updating YouTube access token succeeded, new token is {}, next update will be after {} seconds.", + accessToken, + TimeUnit.MILLISECONDS.toSeconds(accessTokenRefreshInterval) + ); + return refreshToken; + } + } } - } } - } - - private URI buildUri(String url, List params) { - try { - return new URIBuilder(url) - .addParameters(params) - .build(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); + + private URI buildUri(String url, List params) { + try { + return new URIBuilder(url) + .addParameters(params) + .build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } - } - protected static class CachedAuthScript { - public final String clientId; - public final String clientSecret; + protected static class CachedAuthScript { + public final String clientId; + public final String clientSecret; - public CachedAuthScript(String clientId, String clientSecret) { - this.clientId = clientId; - this.clientSecret = clientSecret; + public CachedAuthScript(String clientId, String clientSecret) { + this.clientId = clientId; + this.clientSecret = clientSecret; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeAudioSourceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeAudioSourceManager.java index 3cd91eaf1..bd55a680d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeAudioSourceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeAudioSourceManager.java @@ -15,13 +15,6 @@ import com.sedmelluq.discord.lavaplayer.track.AudioReference; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; -import java.io.DataInput; -import java.io.DataOutput; -import java.net.URI; -import java.util.Arrays; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -30,6 +23,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.DataInput; +import java.io.DataOutput; +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.COMMON; import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.FAULT; import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS; @@ -38,290 +39,291 @@ * Audio source manager that implements finding Youtube videos or playlists based on an URL or ID. */ public class YoutubeAudioSourceManager implements AudioSourceManager, HttpConfigurable { - private static final Logger log = LoggerFactory.getLogger(YoutubeAudioSourceManager.class); - - private final YoutubeSignatureResolver signatureResolver; - private final HttpInterfaceManager httpInterfaceManager; - private final ExtendedHttpConfigurable combinedHttpConfiguration; - private final YoutubeMixLoader mixLoader; - private final YoutubeAccessTokenTracker accessTokenTracker; - private final boolean allowSearch; - private final YoutubeTrackDetailsLoader trackDetailsLoader; - private final YoutubeSearchResultLoader searchResultLoader; - private final YoutubeSearchMusicResultLoader searchMusicResultLoader; - private final YoutubePlaylistLoader playlistLoader; - private final YoutubeLinkRouter linkRouter; - private final LoadingRoutes loadingRoutes; - - /** - * Create an instance with default settings. - */ - public YoutubeAudioSourceManager() { - this(true, null, null); - } - - /** - * Create an instance. - * @param allowSearch Whether to allow search queries as identifiers - * @param email Email of Google account to auth in, required for playing age restricted tracks - * @param password Password of Google account to auth in, required for playing age restricted tracks - */ - public YoutubeAudioSourceManager(boolean allowSearch, String email, String password) { - this( - allowSearch, - email, - password, - new DefaultYoutubeTrackDetailsLoader(), - new YoutubeSearchProvider(), - new YoutubeSearchMusicProvider(), - new YoutubeSignatureCipherManager(), - new DefaultYoutubePlaylistLoader(), - new DefaultYoutubeLinkRouter(), - new YoutubeMixProvider() - ); - } - - public YoutubeAudioSourceManager( - boolean allowSearch, - String email, - String password, - YoutubeTrackDetailsLoader trackDetailsLoader, - YoutubeSearchResultLoader searchResultLoader, - YoutubeSearchMusicResultLoader searchMusicResultLoader, - YoutubeSignatureResolver signatureResolver, - YoutubePlaylistLoader playlistLoader, - YoutubeLinkRouter linkRouter, - YoutubeMixLoader mixLoader - ) { - httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); - accessTokenTracker = new YoutubeAccessTokenTracker(httpInterfaceManager, email, password); - YoutubeHttpContextFilter youtubeHttpContextFilter = new YoutubeHttpContextFilter(); - youtubeHttpContextFilter.setTokenTracker(accessTokenTracker); - httpInterfaceManager.setHttpContextFilter(youtubeHttpContextFilter); - - if (!DataFormatTools.isNullOrEmpty(email) && !DataFormatTools.isNullOrEmpty(password)) { - // Prepare master token on startup - accessTokenTracker.updateMasterToken(); + private static final Logger log = LoggerFactory.getLogger(YoutubeAudioSourceManager.class); + + private final YoutubeSignatureResolver signatureResolver; + private final HttpInterfaceManager httpInterfaceManager; + private final ExtendedHttpConfigurable combinedHttpConfiguration; + private final YoutubeMixLoader mixLoader; + private final YoutubeAccessTokenTracker accessTokenTracker; + private final boolean allowSearch; + private final YoutubeTrackDetailsLoader trackDetailsLoader; + private final YoutubeSearchResultLoader searchResultLoader; + private final YoutubeSearchMusicResultLoader searchMusicResultLoader; + private final YoutubePlaylistLoader playlistLoader; + private final YoutubeLinkRouter linkRouter; + private final LoadingRoutes loadingRoutes; + + /** + * Create an instance with default settings. + */ + public YoutubeAudioSourceManager() { + this(true, null, null); } - this.allowSearch = allowSearch; - this.trackDetailsLoader = trackDetailsLoader; - this.signatureResolver = signatureResolver; - this.searchResultLoader = searchResultLoader; - this.searchMusicResultLoader = searchMusicResultLoader; - this.playlistLoader = playlistLoader; - this.linkRouter = linkRouter; - this.mixLoader = mixLoader; - this.loadingRoutes = new LoadingRoutes(); - - combinedHttpConfiguration = new MultiHttpConfigurable(Arrays.asList( - httpInterfaceManager, - searchResultLoader.getHttpConfiguration(), - searchMusicResultLoader.getHttpConfiguration() - )); - } - - public YoutubeTrackDetailsLoader getTrackDetailsLoader() { - return trackDetailsLoader; - } - - public YoutubeSignatureResolver getSignatureResolver() { - return signatureResolver; - } - - /** - * @param playlistPageCount Maximum number of pages loaded from one playlist. There are 100 tracks per page. - */ - public void setPlaylistPageCount(int playlistPageCount) { - playlistLoader.setPlaylistPageCount(playlistPageCount); - } - - @Override - public String getSourceName() { - return "youtube"; - } - - @Override - public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { - try { - return loadItemOnce(reference); - } catch (FriendlyException exception) { - // In case of a connection reset exception, try once more. - if (HttpClientTools.isRetriableNetworkException(exception.getCause())) { - return loadItemOnce(reference); - } else { - throw exception; - } + /** + * Create an instance. + * + * @param allowSearch Whether to allow search queries as identifiers + * @param email Email of Google account to auth in, required for playing age restricted tracks + * @param password Password of Google account to auth in, required for playing age restricted tracks + */ + public YoutubeAudioSourceManager(boolean allowSearch, String email, String password) { + this( + allowSearch, + email, + password, + new DefaultYoutubeTrackDetailsLoader(), + new YoutubeSearchProvider(), + new YoutubeSearchMusicProvider(), + new YoutubeSignatureCipherManager(), + new DefaultYoutubePlaylistLoader(), + new DefaultYoutubeLinkRouter(), + new YoutubeMixProvider() + ); } - } - - @Override - public boolean isTrackEncodable(AudioTrack track) { - return true; - } - - @Override - public void encodeTrack(AudioTrack track, DataOutput output) { - // No custom values that need saving - } - - @Override - public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { - return new YoutubeAudioTrack(trackInfo, this); - } - - @Override - public void shutdown() { - ExceptionTools.closeWithWarnings(httpInterfaceManager); - } - - public YoutubeAccessTokenTracker getAccessTokenTracker() { - return accessTokenTracker; - } - - /** - * @return Get an HTTP interface for a playing track. - */ - public HttpInterface getHttpInterface() { - return httpInterfaceManager.getInterface(); - } - - @Override - public void configureRequests(Function configurator) { - combinedHttpConfiguration.configureRequests(configurator); - } - - @Override - public void configureBuilder(Consumer configurator) { - combinedHttpConfiguration.configureBuilder(configurator); - } - - public ExtendedHttpConfigurable getHttpConfiguration() { - return combinedHttpConfiguration; - } - - public ExtendedHttpConfigurable getMainHttpConfiguration() { - return httpInterfaceManager; - } - - public ExtendedHttpConfigurable getSearchHttpConfiguration() { - return searchResultLoader.getHttpConfiguration(); - } - - public ExtendedHttpConfigurable getSearchMusicHttpConfiguration() { - return searchMusicResultLoader.getHttpConfiguration(); - } - - private AudioItem loadItemOnce(AudioReference reference) { - return linkRouter.route(reference.identifier, loadingRoutes); - } - - /** - * Loads a single track from video ID. - * - * @param videoId ID of the YouTube video. - * @param mustExist True if it should throw an exception on missing track, otherwise returns AudioReference.NO_TRACK. - * @return Loaded YouTube track. - */ - public AudioItem loadTrackWithVideoId(String videoId, boolean mustExist) { - try (HttpInterface httpInterface = getHttpInterface()) { - YoutubeTrackDetails details = trackDetailsLoader.loadDetails(httpInterface, videoId, false, this); - - if (details == null) { - if (mustExist) { - throw new FriendlyException("Video unavailable", COMMON, null); - } else { - return AudioReference.NO_TRACK; + + public YoutubeAudioSourceManager( + boolean allowSearch, + String email, + String password, + YoutubeTrackDetailsLoader trackDetailsLoader, + YoutubeSearchResultLoader searchResultLoader, + YoutubeSearchMusicResultLoader searchMusicResultLoader, + YoutubeSignatureResolver signatureResolver, + YoutubePlaylistLoader playlistLoader, + YoutubeLinkRouter linkRouter, + YoutubeMixLoader mixLoader + ) { + httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + accessTokenTracker = new YoutubeAccessTokenTracker(httpInterfaceManager, email, password); + YoutubeHttpContextFilter youtubeHttpContextFilter = new YoutubeHttpContextFilter(); + youtubeHttpContextFilter.setTokenTracker(accessTokenTracker); + httpInterfaceManager.setHttpContextFilter(youtubeHttpContextFilter); + + if (!DataFormatTools.isNullOrEmpty(email) && !DataFormatTools.isNullOrEmpty(password)) { + // Prepare master token on startup + accessTokenTracker.updateMasterToken(); } - } - return new YoutubeAudioTrack(details.getTrackInfo(), this); - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions("Loading information for a YouTube track failed.", FAULT, e); + this.allowSearch = allowSearch; + this.trackDetailsLoader = trackDetailsLoader; + this.signatureResolver = signatureResolver; + this.searchResultLoader = searchResultLoader; + this.searchMusicResultLoader = searchMusicResultLoader; + this.playlistLoader = playlistLoader; + this.linkRouter = linkRouter; + this.mixLoader = mixLoader; + this.loadingRoutes = new LoadingRoutes(); + + combinedHttpConfiguration = new MultiHttpConfigurable(Arrays.asList( + httpInterfaceManager, + searchResultLoader.getHttpConfiguration(), + searchMusicResultLoader.getHttpConfiguration() + )); } - } - private YoutubeAudioTrack buildTrackFromInfo(AudioTrackInfo info) { - return new YoutubeAudioTrack(info, this); - } + public YoutubeTrackDetailsLoader getTrackDetailsLoader() { + return trackDetailsLoader; + } + + public YoutubeSignatureResolver getSignatureResolver() { + return signatureResolver; + } - private class LoadingRoutes implements YoutubeLinkRouter.Routes { + /** + * @param playlistPageCount Maximum number of pages loaded from one playlist. There are 100 tracks per page. + */ + public void setPlaylistPageCount(int playlistPageCount) { + playlistLoader.setPlaylistPageCount(playlistPageCount); + } @Override - public AudioItem track(String videoId) { - return loadTrackWithVideoId(videoId, false); + public String getSourceName() { + return "youtube"; } @Override - public AudioItem playlist(String playlistId, String selectedVideoId) { - log.debug("Starting to load playlist with ID {}", playlistId); - - try (HttpInterface httpInterface = getHttpInterface()) { - return playlistLoader.load(httpInterface, playlistId, selectedVideoId, - YoutubeAudioSourceManager.this::buildTrackFromInfo); - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions(e); - } + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + try { + return loadItemOnce(reference); + } catch (FriendlyException exception) { + // In case of a connection reset exception, try once more. + if (HttpClientTools.isRetriableNetworkException(exception.getCause())) { + return loadItemOnce(reference); + } else { + throw exception; + } + } } @Override - public AudioItem mix(String mixId, String selectedVideoId) { - log.debug("Starting to load mix with ID {} selected track {}", mixId, selectedVideoId); - - try (HttpInterface httpInterface = getHttpInterface()) { - return mixLoader.load(httpInterface, mixId, selectedVideoId, - YoutubeAudioSourceManager.this::buildTrackFromInfo); - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions(e); - } + public boolean isTrackEncodable(AudioTrack track) { + return true; } @Override - public AudioItem search(String query) { - if (allowSearch) { - return searchResultLoader.loadSearchResult( - query, - YoutubeAudioSourceManager.this::buildTrackFromInfo - ); - } - return null; + public void encodeTrack(AudioTrack track, DataOutput output) { + // No custom values that need saving } @Override - public AudioItem searchMusic(String query) { - if (allowSearch) { - return searchMusicResultLoader.loadSearchMusicResult( - query, - YoutubeAudioSourceManager.this::buildTrackFromInfo - ); - } - return null; + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) { + return new YoutubeAudioTrack(trackInfo, this); } @Override - public AudioItem anonymous(String videoIds) { - try (HttpInterface httpInterface = getHttpInterface()) { - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet("https://www.youtube.com/watch_videos?video_ids=" + videoIds))) { - HttpClientTools.assertSuccessWithContent(response, "playlist response"); - HttpClientContext context = httpInterface.getContext(); - // youtube currently transforms watch_video links into a link with a video id and a list id. - // because that's what happens, we can simply re-process with the redirected link - List redirects = context.getRedirectLocations(); - if (redirects != null && !redirects.isEmpty()) { - return new AudioReference(redirects.get(0).toString(), null); - } else { - throw new FriendlyException("Unable to process youtube watch_videos link", SUSPICIOUS, - new IllegalStateException("Expected youtube to redirect watch_videos link to a watch?v={id}&list={list_id} link, but it did not redirect at all")); - } - } - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions(e); - } + public void shutdown() { + ExceptionTools.closeWithWarnings(httpInterfaceManager); + } + + public YoutubeAccessTokenTracker getAccessTokenTracker() { + return accessTokenTracker; + } + + /** + * @return Get an HTTP interface for a playing track. + */ + public HttpInterface getHttpInterface() { + return httpInterfaceManager.getInterface(); } @Override - public AudioItem none() { - return AudioReference.NO_TRACK; + public void configureRequests(Function configurator) { + combinedHttpConfiguration.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + combinedHttpConfiguration.configureBuilder(configurator); + } + + public ExtendedHttpConfigurable getHttpConfiguration() { + return combinedHttpConfiguration; + } + + public ExtendedHttpConfigurable getMainHttpConfiguration() { + return httpInterfaceManager; + } + + public ExtendedHttpConfigurable getSearchHttpConfiguration() { + return searchResultLoader.getHttpConfiguration(); + } + + public ExtendedHttpConfigurable getSearchMusicHttpConfiguration() { + return searchMusicResultLoader.getHttpConfiguration(); + } + + private AudioItem loadItemOnce(AudioReference reference) { + return linkRouter.route(reference.identifier, loadingRoutes); + } + + /** + * Loads a single track from video ID. + * + * @param videoId ID of the YouTube video. + * @param mustExist True if it should throw an exception on missing track, otherwise returns AudioReference.NO_TRACK. + * @return Loaded YouTube track. + */ + public AudioItem loadTrackWithVideoId(String videoId, boolean mustExist) { + try (HttpInterface httpInterface = getHttpInterface()) { + YoutubeTrackDetails details = trackDetailsLoader.loadDetails(httpInterface, videoId, false, this); + + if (details == null) { + if (mustExist) { + throw new FriendlyException("Video unavailable", COMMON, null); + } else { + return AudioReference.NO_TRACK; + } + } + + return new YoutubeAudioTrack(details.getTrackInfo(), this); + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions("Loading information for a YouTube track failed.", FAULT, e); + } + } + + private YoutubeAudioTrack buildTrackFromInfo(AudioTrackInfo info) { + return new YoutubeAudioTrack(info, this); + } + + private class LoadingRoutes implements YoutubeLinkRouter.Routes { + + @Override + public AudioItem track(String videoId) { + return loadTrackWithVideoId(videoId, false); + } + + @Override + public AudioItem playlist(String playlistId, String selectedVideoId) { + log.debug("Starting to load playlist with ID {}", playlistId); + + try (HttpInterface httpInterface = getHttpInterface()) { + return playlistLoader.load(httpInterface, playlistId, selectedVideoId, + YoutubeAudioSourceManager.this::buildTrackFromInfo); + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions(e); + } + } + + @Override + public AudioItem mix(String mixId, String selectedVideoId) { + log.debug("Starting to load mix with ID {} selected track {}", mixId, selectedVideoId); + + try (HttpInterface httpInterface = getHttpInterface()) { + return mixLoader.load(httpInterface, mixId, selectedVideoId, + YoutubeAudioSourceManager.this::buildTrackFromInfo); + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions(e); + } + } + + @Override + public AudioItem search(String query) { + if (allowSearch) { + return searchResultLoader.loadSearchResult( + query, + YoutubeAudioSourceManager.this::buildTrackFromInfo + ); + } + return null; + } + + @Override + public AudioItem searchMusic(String query) { + if (allowSearch) { + return searchMusicResultLoader.loadSearchMusicResult( + query, + YoutubeAudioSourceManager.this::buildTrackFromInfo + ); + } + return null; + } + + @Override + public AudioItem anonymous(String videoIds) { + try (HttpInterface httpInterface = getHttpInterface()) { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet("https://www.youtube.com/watch_videos?video_ids=" + videoIds))) { + HttpClientTools.assertSuccessWithContent(response, "playlist response"); + HttpClientContext context = httpInterface.getContext(); + // youtube currently transforms watch_video links into a link with a video id and a list id. + // because that's what happens, we can simply re-process with the redirected link + List redirects = context.getRedirectLocations(); + if (redirects != null && !redirects.isEmpty()) { + return new AudioReference(redirects.get(0).toString(), null); + } else { + throw new FriendlyException("Unable to process youtube watch_videos link", SUSPICIOUS, + new IllegalStateException("Expected youtube to redirect watch_videos link to a watch?v={id}&list={list_id} link, but it did not redirect at all")); + } + } + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions(e); + } + } + + @Override + public AudioItem none() { + return AudioReference.NO_TRACK; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeAudioTrack.java index 441e7f1da..992bb09c1 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeAudioTrack.java @@ -9,11 +9,12 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; import com.sedmelluq.discord.lavaplayer.track.DelegatedAudioTrack; import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.net.URI; import java.util.List; import java.util.StringJoiner; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import static com.sedmelluq.discord.lavaplayer.container.Formats.MIME_AUDIO_WEBM; import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.COMMON; @@ -23,135 +24,135 @@ * Audio track that handles processing Youtube videos as audio tracks. */ public class YoutubeAudioTrack extends DelegatedAudioTrack { - private static final Logger log = LoggerFactory.getLogger(YoutubeAudioTrack.class); + private static final Logger log = LoggerFactory.getLogger(YoutubeAudioTrack.class); - private final YoutubeAudioSourceManager sourceManager; + private final YoutubeAudioSourceManager sourceManager; - /** - * @param trackInfo Track info - * @param sourceManager Source manager which was used to find this track - */ - public YoutubeAudioTrack(AudioTrackInfo trackInfo, YoutubeAudioSourceManager sourceManager) { - super(trackInfo); + /** + * @param trackInfo Track info + * @param sourceManager Source manager which was used to find this track + */ + public YoutubeAudioTrack(AudioTrackInfo trackInfo, YoutubeAudioSourceManager sourceManager) { + super(trackInfo); - this.sourceManager = sourceManager; - } + this.sourceManager = sourceManager; + } - @Override - public void process(LocalAudioTrackExecutor localExecutor) throws Exception { - try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { - FormatWithUrl format = loadBestFormatWithUrl(httpInterface); + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { + FormatWithUrl format = loadBestFormatWithUrl(httpInterface); - log.debug("Starting track from URL: {}", format.signedUrl); + log.debug("Starting track from URL: {}", format.signedUrl); - if (trackInfo.isStream || format.details.getContentLength() == CONTENT_LENGTH_UNKNOWN) { - processStream(localExecutor, format); - } else { - processStatic(localExecutor, httpInterface, format); - } + if (trackInfo.isStream || format.details.getContentLength() == CONTENT_LENGTH_UNKNOWN) { + processStream(localExecutor, format); + } else { + processStatic(localExecutor, httpInterface, format); + } + } } - } - - @Override - public boolean isSeekable() { - return true; - } - - private void processStatic(LocalAudioTrackExecutor localExecutor, HttpInterface httpInterface, FormatWithUrl format) throws Exception { - try (YoutubePersistentHttpStream stream = new YoutubePersistentHttpStream(httpInterface, format.signedUrl, format.details.getContentLength())) { - if (format.details.getType().getMimeType().endsWith("/webm")) { - processDelegate(new MatroskaAudioTrack(trackInfo, stream), localExecutor); - } else { - processDelegate(new MpegAudioTrack(trackInfo, stream), localExecutor); - } - } - } - - private void processStream(LocalAudioTrackExecutor localExecutor, FormatWithUrl format) throws Exception { - if (MIME_AUDIO_WEBM.equals(format.details.getType().getMimeType())) { - throw new FriendlyException("YouTube WebM streams are currently not supported.", COMMON, null); - } else { - try (HttpInterface streamingInterface = sourceManager.getHttpInterface()) { - processDelegate(new YoutubeMpegStreamAudioTrack(trackInfo, streamingInterface, format.signedUrl), localExecutor); - } + + @Override + public boolean isSeekable() { + return true; } - } - private FormatWithUrl loadBestFormatWithUrl(HttpInterface httpInterface) throws Exception { - YoutubeTrackDetails details = sourceManager.getTrackDetailsLoader() - .loadDetails(httpInterface, getIdentifier(), true, sourceManager); + private void processStatic(LocalAudioTrackExecutor localExecutor, HttpInterface httpInterface, FormatWithUrl format) throws Exception { + try (YoutubePersistentHttpStream stream = new YoutubePersistentHttpStream(httpInterface, format.signedUrl, format.details.getContentLength())) { + if (format.details.getType().getMimeType().endsWith("/webm")) { + processDelegate(new MatroskaAudioTrack(trackInfo, stream), localExecutor); + } else { + processDelegate(new MpegAudioTrack(trackInfo, stream), localExecutor); + } + } + } - // If the error reason is "Video unavailable" details will return null - if (details == null) { - throw new FriendlyException("This video is not available", FriendlyException.Severity.COMMON, null); + private void processStream(LocalAudioTrackExecutor localExecutor, FormatWithUrl format) throws Exception { + if (MIME_AUDIO_WEBM.equals(format.details.getType().getMimeType())) { + throw new FriendlyException("YouTube WebM streams are currently not supported.", COMMON, null); + } else { + try (HttpInterface streamingInterface = sourceManager.getHttpInterface()) { + processDelegate(new YoutubeMpegStreamAudioTrack(trackInfo, streamingInterface, format.signedUrl), localExecutor); + } + } } - List formats = details.getFormats(httpInterface, sourceManager.getSignatureResolver()); + private FormatWithUrl loadBestFormatWithUrl(HttpInterface httpInterface) throws Exception { + YoutubeTrackDetails details = sourceManager.getTrackDetailsLoader() + .loadDetails(httpInterface, getIdentifier(), true, sourceManager); + + // If the error reason is "Video unavailable" details will return null + if (details == null) { + throw new FriendlyException("This video is not available", FriendlyException.Severity.COMMON, null); + } - YoutubeTrackFormat format = findBestSupportedFormat(formats); + List formats = details.getFormats(httpInterface, sourceManager.getSignatureResolver()); - URI signedUrl = sourceManager.getSignatureResolver() - .resolveFormatUrl(httpInterface, details.getPlayerScript(), format); + YoutubeTrackFormat format = findBestSupportedFormat(formats); - return new FormatWithUrl(format, signedUrl); - } + URI signedUrl = sourceManager.getSignatureResolver() + .resolveFormatUrl(httpInterface, details.getPlayerScript(), format); - @Override - protected AudioTrack makeShallowClone() { - return new YoutubeAudioTrack(trackInfo, sourceManager); - } + return new FormatWithUrl(format, signedUrl); + } - @Override - public AudioSourceManager getSourceManager() { - return sourceManager; - } + @Override + protected AudioTrack makeShallowClone() { + return new YoutubeAudioTrack(trackInfo, sourceManager); + } - private static boolean isBetterFormat(YoutubeTrackFormat format, YoutubeTrackFormat other) { - YoutubeFormatInfo info = format.getInfo(); + @Override + public AudioSourceManager getSourceManager() { + return sourceManager; + } - if (info == null) { - return false; - } else if (other == null) { - return true; - } else if (MIME_AUDIO_WEBM.equals(info.mimeType) && format.getAudioChannels() > 2) { - // Opus with more than 2 audio channels is unsupported by LavaPlayer currently. - return false; - } else if (info.ordinal() != other.getInfo().ordinal()) { - return info.ordinal() < other.getInfo().ordinal(); - } else { - return format.getBitrate() > other.getBitrate(); + private static boolean isBetterFormat(YoutubeTrackFormat format, YoutubeTrackFormat other) { + YoutubeFormatInfo info = format.getInfo(); + + if (info == null) { + return false; + } else if (other == null) { + return true; + } else if (MIME_AUDIO_WEBM.equals(info.mimeType) && format.getAudioChannels() > 2) { + // Opus with more than 2 audio channels is unsupported by LavaPlayer currently. + return false; + } else if (info.ordinal() != other.getInfo().ordinal()) { + return info.ordinal() < other.getInfo().ordinal(); + } else { + return format.getBitrate() > other.getBitrate(); + } } - } - private static YoutubeTrackFormat findBestSupportedFormat(List formats) { - YoutubeTrackFormat bestFormat = null; + private static YoutubeTrackFormat findBestSupportedFormat(List formats) { + YoutubeTrackFormat bestFormat = null; - for (YoutubeTrackFormat format : formats) { - if (!format.isDefaultAudioTrack()) { - continue; - } + for (YoutubeTrackFormat format : formats) { + if (!format.isDefaultAudioTrack()) { + continue; + } - if (isBetterFormat(format, bestFormat)) { - bestFormat = format; - } - } + if (isBetterFormat(format, bestFormat)) { + bestFormat = format; + } + } - if (bestFormat == null) { - StringJoiner joiner = new StringJoiner(", "); - formats.forEach(format -> joiner.add(format.getType().toString())); - throw new IllegalStateException("No supported audio streams available, available types: " + joiner); - } + if (bestFormat == null) { + StringJoiner joiner = new StringJoiner(", "); + formats.forEach(format -> joiner.add(format.getType().toString())); + throw new IllegalStateException("No supported audio streams available, available types: " + joiner); + } - return bestFormat; - } + return bestFormat; + } - private static class FormatWithUrl { - private final YoutubeTrackFormat details; - private final URI signedUrl; + private static class FormatWithUrl { + private final YoutubeTrackFormat details; + private final URI signedUrl; - private FormatWithUrl(YoutubeTrackFormat details, URI signedUrl) { - this.details = details; - this.signedUrl = signedUrl; + private FormatWithUrl(YoutubeTrackFormat details, URI signedUrl) { + this.details = details; + this.signedUrl = signedUrl; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeCipherOperation.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeCipherOperation.java index f16548095..14d6d1dff 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeCipherOperation.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeCipherOperation.java @@ -14,7 +14,7 @@ public class YoutubeCipherOperation { public final int parameter; /** - * @param type The type of the operation. + * @param type The type of the operation. * @param parameter The parameter for the operation. */ public YoutubeCipherOperation(YoutubeCipherOperationType type, int parameter) { diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeClientConfig.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeClientConfig.java index 2d6ef4be6..5823ca84f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeClientConfig.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeClientConfig.java @@ -3,170 +3,158 @@ import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; import org.json.JSONObject; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.CLIENT_ANDROID_NAME; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.CLIENT_ANDROID_VERSION; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.CLIENT_MUSIC_NAME; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.CLIENT_MUSIC_VERSION; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.CLIENT_SCREEN_EMBED; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.CLIENT_THIRD_PARTY_EMBED; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.CLIENT_TVHTML5_NAME; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.CLIENT_TVHTML5_VERSION; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.CLIENT_WEB_NAME; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.CLIENT_WEB_VERSION; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.INNERTUBE_ANDROID_API_KEY; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.INNERTUBE_MUSIC_API_KEY; -import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.INNERTUBE_WEB_API_KEY; +import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeConstants.*; import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeHttpContextFilter.ATTRIBUTE_USER_AGENT_SPECIFIED; import static com.sedmelluq.discord.lavaplayer.source.youtube.YoutubePayloadHelper.putOnceAndJoin; public class YoutubeClientConfig extends JSONObject { - public static final AndroidVersion DEFAULT_ANDROID_VERSION = AndroidVersion.ANDROID_11; - - public static YoutubeClientConfig ANDROID = new YoutubeClientConfig() - .withApiKey(INNERTUBE_ANDROID_API_KEY) - .withUserAgent(String.format("com.google.android.youtube/%s (Linux; U; Android %s) gzip", CLIENT_ANDROID_VERSION, DEFAULT_ANDROID_VERSION.getOsVersion())) - .withClientName(CLIENT_ANDROID_NAME) - .withClientField("clientVersion", CLIENT_ANDROID_VERSION) - .withClientField("androidSdkVersion", DEFAULT_ANDROID_VERSION.getSdkVersion()) - //.withClientField("osName", "Android") - //.withClientField("osVersion", DEFAULT_ANDROID_VERSION.getOsVersion()) - .withClientDefaultScreenParameters(); - - public static YoutubeClientConfig TV_EMBEDDED = new YoutubeClientConfig() - .withApiKey(INNERTUBE_WEB_API_KEY) //.withApiKey(INNERTUBE_TV_API_KEY) // Requires header (Referer tv.youtube.com) - .withClientName(CLIENT_TVHTML5_NAME) - .withClientField("clientVersion", CLIENT_TVHTML5_VERSION) - .withClientField("clientScreen", CLIENT_SCREEN_EMBED) - .withClientDefaultScreenParameters() - .withThirdPartyEmbedUrl(CLIENT_THIRD_PARTY_EMBED); - - public static YoutubeClientConfig WEB = new YoutubeClientConfig() - .withApiKey(INNERTUBE_WEB_API_KEY) - .withClientName(CLIENT_WEB_NAME) - .withClientField("clientVersion", CLIENT_WEB_VERSION); - - public static YoutubeClientConfig MUSIC = new YoutubeClientConfig() - .withApiKey(INNERTUBE_MUSIC_API_KEY) // Requires header (Referer music.youtube.com) - .withClientName(CLIENT_MUSIC_NAME) - .withClientField("clientVersion", CLIENT_MUSIC_VERSION); - - private String name; - private String userAgent; - private String apiKey; - private final JSONObject root; - - public YoutubeClientConfig() { - this.root = new JSONObject(); - this.userAgent = null; - this.name = null; - } - - private YoutubeClientConfig(JSONObject context, String userAgent, String name) { - this.root = context; - this.userAgent = userAgent; - this.name = name; - } - - public YoutubeClientConfig copy() { - return new YoutubeClientConfig(new JSONObject(root.toMap()), userAgent, name); - } - - public YoutubeClientConfig withClientName(String name) { - this.name = name; - withClientField("clientName", name); - return this; - } - - public String getName() { - return name; - } - - public YoutubeClientConfig withUserAgent(String userAgent) { - this.userAgent = userAgent; - return this; - } - - public String getUserAgent() { - return userAgent; - } - - public YoutubeClientConfig withApiKey(String apiKey) { - this.apiKey = apiKey; - return this; - } - - public String getApiKey() { - return apiKey; - } - - public YoutubeClientConfig withClientDefaultScreenParameters() { - withClientField("screenDensityFloat", 1); - withClientField("screenHeightPoints", 1080); - withClientField("screenPixelDensity", 1); - return withClientField("screenWidthPoints", 1920); - } - - public YoutubeClientConfig withThirdPartyEmbedUrl(String embedUrl) { - JSONObject context = putOnceAndJoin(root, "context"); - JSONObject thirdParty = putOnceAndJoin(context, "thirdParty"); - thirdParty.put("embedUrl", embedUrl); - return this; - } - - public YoutubeClientConfig withPlaybackSignatureTimestamp(String signatureTimestamp) { - JSONObject playbackContext = putOnceAndJoin(root, "playbackContext"); - JSONObject contentPlaybackContext = putOnceAndJoin(playbackContext, "contentPlaybackContext"); - contentPlaybackContext.put("signatureTimestamp", signatureTimestamp); - return this; - } - - public YoutubeClientConfig withRootField(String key, Object value) { - root.put(key, value); - return this; - } - - public YoutubeClientConfig withClientField(String key, Object value) { - JSONObject context = putOnceAndJoin(root, "context"); - JSONObject client = putOnceAndJoin(context, "client"); - client.put(key, value); - return this; - } - - public YoutubeClientConfig withUserField(String key, Object value) { - JSONObject context = putOnceAndJoin(root,"context"); - JSONObject user = putOnceAndJoin(context, "user"); - user.put(key, value); - return this; - } - - public YoutubeClientConfig setAttribute(HttpInterface httpInterface) { - if (userAgent != null) - httpInterface.getContext().setAttribute(ATTRIBUTE_USER_AGENT_SPECIFIED, userAgent); - return this; - } - - public String toJsonString() { - return root.toString(); - } - - public enum AndroidVersion { - // https://apilevels.com/ - ANDROID_11("11", 30); - - private final String osVersion; - private final int sdkVersion; - - AndroidVersion(String osVersion, int sdkVersion) { - this.osVersion = osVersion; - this.sdkVersion = sdkVersion; - } - - public String getOsVersion() { - return osVersion; - } - - public int getSdkVersion() { - return sdkVersion; - } - } + public static final AndroidVersion DEFAULT_ANDROID_VERSION = AndroidVersion.ANDROID_11; + + public static YoutubeClientConfig ANDROID = new YoutubeClientConfig() + .withApiKey(INNERTUBE_ANDROID_API_KEY) + .withUserAgent(String.format("com.google.android.youtube/%s (Linux; U; Android %s) gzip", CLIENT_ANDROID_VERSION, DEFAULT_ANDROID_VERSION.getOsVersion())) + .withClientName(CLIENT_ANDROID_NAME) + .withClientField("clientVersion", CLIENT_ANDROID_VERSION) + .withClientField("androidSdkVersion", DEFAULT_ANDROID_VERSION.getSdkVersion()) + //.withClientField("osName", "Android") + //.withClientField("osVersion", DEFAULT_ANDROID_VERSION.getOsVersion()) + .withClientDefaultScreenParameters(); + + public static YoutubeClientConfig TV_EMBEDDED = new YoutubeClientConfig() + .withApiKey(INNERTUBE_WEB_API_KEY) //.withApiKey(INNERTUBE_TV_API_KEY) // Requires header (Referer tv.youtube.com) + .withClientName(CLIENT_TVHTML5_NAME) + .withClientField("clientVersion", CLIENT_TVHTML5_VERSION) + .withClientField("clientScreen", CLIENT_SCREEN_EMBED) + .withClientDefaultScreenParameters() + .withThirdPartyEmbedUrl(CLIENT_THIRD_PARTY_EMBED); + + public static YoutubeClientConfig WEB = new YoutubeClientConfig() + .withApiKey(INNERTUBE_WEB_API_KEY) + .withClientName(CLIENT_WEB_NAME) + .withClientField("clientVersion", CLIENT_WEB_VERSION); + + public static YoutubeClientConfig MUSIC = new YoutubeClientConfig() + .withApiKey(INNERTUBE_MUSIC_API_KEY) // Requires header (Referer music.youtube.com) + .withClientName(CLIENT_MUSIC_NAME) + .withClientField("clientVersion", CLIENT_MUSIC_VERSION); + + private String name; + private String userAgent; + private String apiKey; + private final JSONObject root; + + public YoutubeClientConfig() { + this.root = new JSONObject(); + this.userAgent = null; + this.name = null; + } + + private YoutubeClientConfig(JSONObject context, String userAgent, String name) { + this.root = context; + this.userAgent = userAgent; + this.name = name; + } + + public YoutubeClientConfig copy() { + return new YoutubeClientConfig(new JSONObject(root.toMap()), userAgent, name); + } + + public YoutubeClientConfig withClientName(String name) { + this.name = name; + withClientField("clientName", name); + return this; + } + + public String getName() { + return name; + } + + public YoutubeClientConfig withUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + public String getUserAgent() { + return userAgent; + } + + public YoutubeClientConfig withApiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public String getApiKey() { + return apiKey; + } + + public YoutubeClientConfig withClientDefaultScreenParameters() { + withClientField("screenDensityFloat", 1); + withClientField("screenHeightPoints", 1080); + withClientField("screenPixelDensity", 1); + return withClientField("screenWidthPoints", 1920); + } + + public YoutubeClientConfig withThirdPartyEmbedUrl(String embedUrl) { + JSONObject context = putOnceAndJoin(root, "context"); + JSONObject thirdParty = putOnceAndJoin(context, "thirdParty"); + thirdParty.put("embedUrl", embedUrl); + return this; + } + + public YoutubeClientConfig withPlaybackSignatureTimestamp(String signatureTimestamp) { + JSONObject playbackContext = putOnceAndJoin(root, "playbackContext"); + JSONObject contentPlaybackContext = putOnceAndJoin(playbackContext, "contentPlaybackContext"); + contentPlaybackContext.put("signatureTimestamp", signatureTimestamp); + return this; + } + + public YoutubeClientConfig withRootField(String key, Object value) { + root.put(key, value); + return this; + } + + public YoutubeClientConfig withClientField(String key, Object value) { + JSONObject context = putOnceAndJoin(root, "context"); + JSONObject client = putOnceAndJoin(context, "client"); + client.put(key, value); + return this; + } + + public YoutubeClientConfig withUserField(String key, Object value) { + JSONObject context = putOnceAndJoin(root, "context"); + JSONObject user = putOnceAndJoin(context, "user"); + user.put(key, value); + return this; + } + + public YoutubeClientConfig setAttribute(HttpInterface httpInterface) { + if (userAgent != null) + httpInterface.getContext().setAttribute(ATTRIBUTE_USER_AGENT_SPECIFIED, userAgent); + return this; + } + + public String toJsonString() { + return root.toString(); + } + + public enum AndroidVersion { + // https://apilevels.com/ + ANDROID_11("11", 30); + + private final String osVersion; + private final int sdkVersion; + + AndroidVersion(String osVersion, int sdkVersion) { + this.osVersion = osVersion; + this.sdkVersion = sdkVersion; + } + + public String getOsVersion() { + return osVersion; + } + + public int getSdkVersion() { + return sdkVersion; + } + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeFormatInfo.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeFormatInfo.java index 303ff2466..988b8a070 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeFormatInfo.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeFormatInfo.java @@ -8,49 +8,50 @@ * The mime type and codec info of a Youtube track format. */ public enum YoutubeFormatInfo { - WEBM_OPUS(MIME_AUDIO_WEBM, CODEC_OPUS), - WEBM_VORBIS(MIME_AUDIO_WEBM, CODEC_VORBIS), - MP4_AAC_LC(MIME_AUDIO_MP4, CODEC_AAC_LC), - WEBM_VIDEO_VORBIS(MIME_VIDEO_WEBM, CODEC_VORBIS), - MP4_VIDEO_AAC_LC(MIME_VIDEO_MP4, CODEC_AAC_LC); - - /** - * Mime type of the format - */ - public final String mimeType; - /** - * Codec name of the format - */ - public final String codec; - - YoutubeFormatInfo(String mimeType, String codec) { - this.mimeType = mimeType; - this.codec = codec; - } - - /** - * Find a matching format info instance from a content type. - * @param contentType The content type to use for matching against known formats - * @return The format info entry that matches the content type - */ - public static YoutubeFormatInfo get(ContentType contentType) { - String mimeType = contentType.getMimeType(); - String codec = contentType.getParameter("codecs"); - - // Check accurate matches - for (YoutubeFormatInfo formatInfo : YoutubeFormatInfo.class.getEnumConstants()) { - if (formatInfo.mimeType.equals(mimeType) && formatInfo.codec.equals(codec)) { - return formatInfo; - } + WEBM_OPUS(MIME_AUDIO_WEBM, CODEC_OPUS), + WEBM_VORBIS(MIME_AUDIO_WEBM, CODEC_VORBIS), + MP4_AAC_LC(MIME_AUDIO_MP4, CODEC_AAC_LC), + WEBM_VIDEO_VORBIS(MIME_VIDEO_WEBM, CODEC_VORBIS), + MP4_VIDEO_AAC_LC(MIME_VIDEO_MP4, CODEC_AAC_LC); + + /** + * Mime type of the format + */ + public final String mimeType; + /** + * Codec name of the format + */ + public final String codec; + + YoutubeFormatInfo(String mimeType, String codec) { + this.mimeType = mimeType; + this.codec = codec; } - // Check for substring matches - for (YoutubeFormatInfo formatInfo : YoutubeFormatInfo.class.getEnumConstants()) { - if (formatInfo.mimeType.equals(mimeType) && codec.contains(formatInfo.codec)) { - return formatInfo; - } + /** + * Find a matching format info instance from a content type. + * + * @param contentType The content type to use for matching against known formats + * @return The format info entry that matches the content type + */ + public static YoutubeFormatInfo get(ContentType contentType) { + String mimeType = contentType.getMimeType(); + String codec = contentType.getParameter("codecs"); + + // Check accurate matches + for (YoutubeFormatInfo formatInfo : YoutubeFormatInfo.class.getEnumConstants()) { + if (formatInfo.mimeType.equals(mimeType) && formatInfo.codec.equals(codec)) { + return formatInfo; + } + } + + // Check for substring matches + for (YoutubeFormatInfo formatInfo : YoutubeFormatInfo.class.getEnumConstants()) { + if (formatInfo.mimeType.equals(mimeType) && codec.contains(formatInfo.codec)) { + return formatInfo; + } + } + + return null; } - - return null; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeHttpContextFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeHttpContextFilter.java index cbdfb1c16..b600df84b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeHttpContextFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeHttpContextFilter.java @@ -22,102 +22,102 @@ import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.COMMON; public class YoutubeHttpContextFilter implements HttpContextFilter { - private static final Logger log = LoggerFactory.getLogger(YoutubeHttpContextFilter.class); - private static final String ATTRIBUTE_RESET_RETRY = "isResetRetry"; - public static final String ATTRIBUTE_USER_AGENT_SPECIFIED = "isUserAgentSpecified"; - private static final HttpContextRetryCounter retryCounter = new HttpContextRetryCounter("yt-token-retry"); + private static final Logger log = LoggerFactory.getLogger(YoutubeHttpContextFilter.class); + private static final String ATTRIBUTE_RESET_RETRY = "isResetRetry"; + public static final String ATTRIBUTE_USER_AGENT_SPECIFIED = "isUserAgentSpecified"; + private static final HttpContextRetryCounter retryCounter = new HttpContextRetryCounter("yt-token-retry"); - private YoutubeAccessTokenTracker tokenTracker; + private YoutubeAccessTokenTracker tokenTracker; - public void setTokenTracker(YoutubeAccessTokenTracker tokenTracker) { - this.tokenTracker = tokenTracker; - } - - @Override - public void onContextOpen(HttpClientContext context) { - CookieStore cookieStore = context.getCookieStore(); - - if (cookieStore == null) { - cookieStore = new BasicCookieStore(); - context.setCookieStore(cookieStore); + public void setTokenTracker(YoutubeAccessTokenTracker tokenTracker) { + this.tokenTracker = tokenTracker; } - // Reset cookies for each sequence of requests. - cookieStore.clear(); - } + @Override + public void onContextOpen(HttpClientContext context) { + CookieStore cookieStore = context.getCookieStore(); - @Override - public void onContextClose(HttpClientContext context) { - - } + if (cookieStore == null) { + cookieStore = new BasicCookieStore(); + context.setCookieStore(cookieStore); + } - @Override - public void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition) { - if (!isRepetition) { - context.removeAttribute(ATTRIBUTE_RESET_RETRY); + // Reset cookies for each sequence of requests. + cookieStore.clear(); } - retryCounter.handleUpdate(context, isRepetition); + @Override + public void onContextClose(HttpClientContext context) { - if (tokenTracker.isTokenFetchContext(context)) { - // Used for fetching access token or visitor id, let's not recurse. - return; } - String userAgent = context.getAttribute(ATTRIBUTE_USER_AGENT_SPECIFIED, String.class); - if (context.getAttribute(ATTRIBUTE_USER_AGENT_SPECIFIED) != null) { - String visitorId = tokenTracker.updateVisitorId(); - request.setHeader("User-Agent", userAgent); - request.setHeader("X-Goog-Visitor-Id", visitorId); - context.removeAttribute(ATTRIBUTE_USER_AGENT_SPECIFIED); - } + @Override + public void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition) { + if (!isRepetition) { + context.removeAttribute(ATTRIBUTE_RESET_RETRY); + } + + retryCounter.handleUpdate(context, isRepetition); + + if (tokenTracker.isTokenFetchContext(context)) { + // Used for fetching access token or visitor id, let's not recurse. + return; + } + + String userAgent = context.getAttribute(ATTRIBUTE_USER_AGENT_SPECIFIED, String.class); + if (context.getAttribute(ATTRIBUTE_USER_AGENT_SPECIFIED) != null) { + String visitorId = tokenTracker.updateVisitorId(); + request.setHeader("User-Agent", userAgent); + request.setHeader("X-Goog-Visitor-Id", visitorId); + context.removeAttribute(ATTRIBUTE_USER_AGENT_SPECIFIED); + } - String accessToken = tokenTracker.getAccessToken(); - if (!DataFormatTools.isNullOrEmpty(accessToken)) { - request.setHeader("Authorization", "Bearer " + accessToken); - } else { - try { - URI uri = new URIBuilder(request.getURI()) - .setParameter("key", YoutubeConstants.INNERTUBE_ANDROID_API_KEY) - .build(); - - if (request instanceof HttpRequestBase) { - ((HttpRequestBase) request).setURI(uri); + String accessToken = tokenTracker.getAccessToken(); + if (!DataFormatTools.isNullOrEmpty(accessToken)) { + request.setHeader("Authorization", "Bearer " + accessToken); } else { - throw new IllegalStateException("Cannot update request URI."); + try { + URI uri = new URIBuilder(request.getURI()) + .setParameter("key", YoutubeConstants.INNERTUBE_ANDROID_API_KEY) + .build(); + + if (request instanceof HttpRequestBase) { + ((HttpRequestBase) request).setURI(uri); + } else { + throw new IllegalStateException("Cannot update request URI."); + } + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } } - } - @Override - public boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response) { - if (response.getStatusLine().getStatusCode() == 429) { - throw new FriendlyException("This IP address has been blocked by YouTube (429).", COMMON, null); - } + @Override + public boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response) { + if (response.getStatusLine().getStatusCode() == 429) { + throw new FriendlyException("This IP address has been blocked by YouTube (429).", COMMON, null); + } - if (tokenTracker.isTokenFetchContext(context) || retryCounter.getRetryCount(context) >= 1) { - return false; - } else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { - tokenTracker.updateAccessToken(); - return true; - } else { - return false; - } - } - - @Override - public boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error) { - // Always retry once in case of connection reset exception. - if (HttpClientTools.isConnectionResetException(error)) { - if (context.getAttribute(ATTRIBUTE_RESET_RETRY) == null) { - context.setAttribute(ATTRIBUTE_RESET_RETRY, true); - return true; - } + if (tokenTracker.isTokenFetchContext(context) || retryCounter.getRetryCount(context) >= 1) { + return false; + } else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { + tokenTracker.updateAccessToken(); + return true; + } else { + return false; + } } - return false; - } + @Override + public boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error) { + // Always retry once in case of connection reset exception. + if (HttpClientTools.isConnectionResetException(error)) { + if (context.getAttribute(ATTRIBUTE_RESET_RETRY) == null) { + context.setAttribute(ATTRIBUTE_RESET_RETRY, true); + return true; + } + } + + return false; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeMixLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeMixLoader.java index 7afa8b393..d2753bd78 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeMixLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeMixLoader.java @@ -4,13 +4,14 @@ import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; + import java.util.function.Function; public interface YoutubeMixLoader { - AudioPlaylist load( - HttpInterface httpInterface, - String mixId, - String selectedVideoId, - Function trackFactory - ); + AudioPlaylist load( + HttpInterface httpInterface, + String mixId, + String selectedVideoId, + Function trackFactory + ); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeMixProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeMixProvider.java index a769b5fb3..8b5a4585c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeMixProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeMixProvider.java @@ -27,91 +27,91 @@ * Handles loading of YouTube mixes. */ public class YoutubeMixProvider implements YoutubeMixLoader { - /** - * Loads tracks from mix in parallel into a playlist entry. - * - * @param mixId ID of the mix - * @param selectedVideoId Selected track, {@link AudioPlaylist#getSelectedTrack()} will return this. - * @return Playlist of the tracks in the mix. - */ - public AudioPlaylist load( - HttpInterface httpInterface, - String mixId, - String selectedVideoId, - Function trackFactory - ) { - String playlistTitle = "YouTube mix"; - List tracks = new ArrayList<>(); - - HttpPost post = new HttpPost(NEXT_URL); - YoutubeClientConfig clientConfig = YoutubeClientConfig.ANDROID.copy() - .withRootField("videoId", selectedVideoId) - .withRootField("playlistId", mixId) - .setAttribute(httpInterface); - StringEntity payload = new StringEntity(clientConfig.toJsonString(), "UTF-8"); - post.setEntity(payload); - try (CloseableHttpResponse response = httpInterface.execute(post)) { - HttpClientTools.assertSuccessWithContent(response, "mix response"); - - JsonBrowser body = JsonBrowser.parse(response.getEntity().getContent()); - JsonBrowser playlist = body.get("contents") - .get("singleColumnWatchNextResults") - .get("playlist") - .get("playlist"); - - JsonBrowser title = playlist.get("title"); - - if (!title.isNull()) { - playlistTitle = title.text(); - } - - extractPlaylistTracks(playlist.get("contents"), tracks, trackFactory); - } catch (IOException e) { - throw new FriendlyException("Could not read mix page.", SUSPICIOUS, e); - } + /** + * Loads tracks from mix in parallel into a playlist entry. + * + * @param mixId ID of the mix + * @param selectedVideoId Selected track, {@link AudioPlaylist#getSelectedTrack()} will return this. + * @return Playlist of the tracks in the mix. + */ + public AudioPlaylist load( + HttpInterface httpInterface, + String mixId, + String selectedVideoId, + Function trackFactory + ) { + String playlistTitle = "YouTube mix"; + List tracks = new ArrayList<>(); + + HttpPost post = new HttpPost(NEXT_URL); + YoutubeClientConfig clientConfig = YoutubeClientConfig.ANDROID.copy() + .withRootField("videoId", selectedVideoId) + .withRootField("playlistId", mixId) + .setAttribute(httpInterface); + StringEntity payload = new StringEntity(clientConfig.toJsonString(), "UTF-8"); + post.setEntity(payload); + try (CloseableHttpResponse response = httpInterface.execute(post)) { + HttpClientTools.assertSuccessWithContent(response, "mix response"); + + JsonBrowser body = JsonBrowser.parse(response.getEntity().getContent()); + JsonBrowser playlist = body.get("contents") + .get("singleColumnWatchNextResults") + .get("playlist") + .get("playlist"); + + JsonBrowser title = playlist.get("title"); + + if (!title.isNull()) { + playlistTitle = title.text(); + } + + extractPlaylistTracks(playlist.get("contents"), tracks, trackFactory); + } catch (IOException e) { + throw new FriendlyException("Could not read mix page.", SUSPICIOUS, e); + } - if (tracks.isEmpty()) { - throw new FriendlyException("Could not find tracks from mix.", SUSPICIOUS, null); - } + if (tracks.isEmpty()) { + throw new FriendlyException("Could not find tracks from mix.", SUSPICIOUS, null); + } - AudioTrack selectedTrack = findSelectedTrack(tracks, selectedVideoId); - return new BasicAudioPlaylist(playlistTitle, tracks, selectedTrack, false); - } - - private void extractPlaylistTracks( - JsonBrowser browser, - List tracks, - Function trackFactory - ) { - for (JsonBrowser video : browser.values()) { - JsonBrowser renderer = video.get("playlistPanelVideoRenderer"); - - if (!renderer.get("unplayableText").isNull()) { - return; - } - - String title = renderer.get("title").get("runs").index(0).get("text").text(); - String author = renderer.get("longBylineText").get("runs").index(0).get("text").text(); - String durationStr = renderer.get("lengthText").get("runs").index(0).get("text").text(); - long duration = DataFormatTools.durationTextToMillis(durationStr); - String identifier = renderer.get("videoId").text(); - String uri = WATCH_URL_PREFIX + identifier; - - AudioTrackInfo trackInfo = new AudioTrackInfo(title, author, duration, identifier, false, uri, - ThumbnailTools.getYouTubeThumbnail(renderer, identifier), null); - tracks.add(trackFactory.apply(trackInfo)); + AudioTrack selectedTrack = findSelectedTrack(tracks, selectedVideoId); + return new BasicAudioPlaylist(playlistTitle, tracks, selectedTrack, false); } - } - private AudioTrack findSelectedTrack(List tracks, String selectedVideoId) { - if (selectedVideoId != null) { - for (AudioTrack track : tracks) { - if (selectedVideoId.equals(track.getIdentifier())) { - return track; + private void extractPlaylistTracks( + JsonBrowser browser, + List tracks, + Function trackFactory + ) { + for (JsonBrowser video : browser.values()) { + JsonBrowser renderer = video.get("playlistPanelVideoRenderer"); + + if (!renderer.get("unplayableText").isNull()) { + return; + } + + String title = renderer.get("title").get("runs").index(0).get("text").text(); + String author = renderer.get("longBylineText").get("runs").index(0).get("text").text(); + String durationStr = renderer.get("lengthText").get("runs").index(0).get("text").text(); + long duration = DataFormatTools.durationTextToMillis(durationStr); + String identifier = renderer.get("videoId").text(); + String uri = WATCH_URL_PREFIX + identifier; + + AudioTrackInfo trackInfo = new AudioTrackInfo(title, author, duration, identifier, false, uri, + ThumbnailTools.getYouTubeThumbnail(renderer, identifier), null); + tracks.add(trackFactory.apply(trackInfo)); } - } } - return null; - } + private AudioTrack findSelectedTrack(List tracks, String selectedVideoId) { + if (selectedVideoId != null) { + for (AudioTrack track : tracks) { + if (selectedVideoId.equals(track.getIdentifier())) { + return track; + } + } + } + + return null; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeMpegStreamAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeMpegStreamAudioTrack.java index 0f7f07173..e0e7dff48 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeMpegStreamAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeMpegStreamAudioTrack.java @@ -33,234 +33,234 @@ * responds to a segment request with 204. */ public class YoutubeMpegStreamAudioTrack extends MpegAudioTrack { - private static final Logger log = LoggerFactory.getLogger(YoutubeMpegStreamAudioTrack.class); - private static final RequestConfig streamingRequestConfig = RequestConfig.custom().setSocketTimeout(3000).setConnectionRequestTimeout(3000).setConnectTimeout(3000).build(); - private static final long EMPTY_RETRY_THRESHOLD_MS = 400; - private static final long EMPTY_RETRY_INTERVAL_MS = 50; - private static final long MAX_REWIND_TIME = 43200; // Seconds - - private final HttpInterface httpInterface; - private final TrackState state; - - /** - * @param trackInfo Track info - * @param httpInterface HTTP interface to use for loading segments - * @param signedUrl URI of the base stream with signature resolved - */ - public YoutubeMpegStreamAudioTrack(AudioTrackInfo trackInfo, HttpInterface httpInterface, URI signedUrl) { - super(trackInfo, null); - - this.httpInterface = httpInterface; - this.state = new TrackState(signedUrl); - - // YouTube does not return a segment until it is ready, this might trigger a connect timeout otherwise. - httpInterface.getContext().setRequestConfig(streamingRequestConfig); - updateGlobalSequence().join(); - } - - @Override - public void process(LocalAudioTrackExecutor localExecutor) { - localExecutor.executeProcessingLoop(() -> execute(localExecutor), this::seek); - } - - @Override - public void setPosition(long position) { - state.seeking = true; - updateGlobalSequence().join(); - getActiveExecutor().setPosition(position); - } - - @Override - public long getDuration() { - return TimeUnit.SECONDS.toMillis(state.globalSequence * TimeUnit.MILLISECONDS.toSeconds(state.globalSequenceDuration)); - } - - @Override - public long getPosition() { - return TimeUnit.SECONDS.toMillis(state.absoluteSequence * TimeUnit.MILLISECONDS.toSeconds(state.globalSequenceDuration)); - } - - private CompletableFuture updateGlobalSequence() { - CompletableFuture updated = new CompletableFuture<>(); - - try (YoutubePersistentHttpStream stream = new YoutubePersistentHttpStream(httpInterface, state.initialUrl, CONTENT_LENGTH_UNKNOWN)) { - MpegFileLoader file = new MpegFileLoader(stream); - file.parseHeaders(); - - SequenceInfo sequenceInfo = extractAbsoluteSequenceFromEvent(file.getLastEventMessage()); - state.globalSequence = sequenceInfo.sequence; - state.globalSequenceDuration = sequenceInfo.duration; - - updated.complete(null); - } catch (IOException e) { - updated.complete(null); + private static final Logger log = LoggerFactory.getLogger(YoutubeMpegStreamAudioTrack.class); + private static final RequestConfig streamingRequestConfig = RequestConfig.custom().setSocketTimeout(3000).setConnectionRequestTimeout(3000).setConnectTimeout(3000).build(); + private static final long EMPTY_RETRY_THRESHOLD_MS = 400; + private static final long EMPTY_RETRY_INTERVAL_MS = 50; + private static final long MAX_REWIND_TIME = 43200; // Seconds + + private final HttpInterface httpInterface; + private final TrackState state; + + /** + * @param trackInfo Track info + * @param httpInterface HTTP interface to use for loading segments + * @param signedUrl URI of the base stream with signature resolved + */ + public YoutubeMpegStreamAudioTrack(AudioTrackInfo trackInfo, HttpInterface httpInterface, URI signedUrl) { + super(trackInfo, null); + + this.httpInterface = httpInterface; + this.state = new TrackState(signedUrl); + + // YouTube does not return a segment until it is ready, this might trigger a connect timeout otherwise. + httpInterface.getContext().setRequestConfig(streamingRequestConfig); + updateGlobalSequence().join(); } - return updated; - } + @Override + public void process(LocalAudioTrackExecutor localExecutor) { + localExecutor.executeProcessingLoop(() -> execute(localExecutor), this::seek); + } + + @Override + public void setPosition(long position) { + state.seeking = true; + updateGlobalSequence().join(); + getActiveExecutor().setPosition(position); + } - private void execute(LocalAudioTrackExecutor localExecutor) throws InterruptedException { - if (!trackInfo.isStream && state.absoluteSequence == null) { - state.absoluteSequence = 0L; + @Override + public long getDuration() { + return TimeUnit.SECONDS.toMillis(state.globalSequence * TimeUnit.MILLISECONDS.toSeconds(state.globalSequenceDuration)); } - try { - while (!state.finished) { - processNextSegmentWithRetry(localExecutor); - state.relativeSequence++; - state.globalSequence++; - } - } finally { - if (state.trackConsumer != null && !state.seeking) { - state.trackConsumer.close(); - } else { - state.seeking = false; - } + @Override + public long getPosition() { + return TimeUnit.SECONDS.toMillis(state.absoluteSequence * TimeUnit.MILLISECONDS.toSeconds(state.globalSequenceDuration)); } - } - private void seek(long timecode) { - long seconds = TimeUnit.MILLISECONDS.toSeconds(timecode); + private CompletableFuture updateGlobalSequence() { + CompletableFuture updated = new CompletableFuture<>(); + + try (YoutubePersistentHttpStream stream = new YoutubePersistentHttpStream(httpInterface, state.initialUrl, CONTENT_LENGTH_UNKNOWN)) { + MpegFileLoader file = new MpegFileLoader(stream); + file.parseHeaders(); + + SequenceInfo sequenceInfo = extractAbsoluteSequenceFromEvent(file.getLastEventMessage()); + state.globalSequence = sequenceInfo.sequence; + state.globalSequenceDuration = sequenceInfo.duration; + + updated.complete(null); + } catch (IOException e) { + updated.complete(null); + } + + return updated; + } - if (seconds > state.globalSequence) { - seconds = state.globalSequence; - } else if (state.globalSequence - seconds > MAX_REWIND_TIME) { - seconds = state.globalSequence - MAX_REWIND_TIME; + private void execute(LocalAudioTrackExecutor localExecutor) throws InterruptedException { + if (!trackInfo.isStream && state.absoluteSequence == null) { + state.absoluteSequence = 0L; + } + + try { + while (!state.finished) { + processNextSegmentWithRetry(localExecutor); + state.relativeSequence++; + state.globalSequence++; + } + } finally { + if (state.trackConsumer != null && !state.seeking) { + state.trackConsumer.close(); + } else { + state.seeking = false; + } + } } - state.absoluteSequence = seconds - 1; - } + private void seek(long timecode) { + long seconds = TimeUnit.MILLISECONDS.toSeconds(timecode); + + if (seconds > state.globalSequence) { + seconds = state.globalSequence; + } else if (state.globalSequence - seconds > MAX_REWIND_TIME) { + seconds = state.globalSequence - MAX_REWIND_TIME; + } - private void processNextSegmentWithRetry( - LocalAudioTrackExecutor localExecutor - ) throws InterruptedException { - if (processNextSegment(localExecutor)) { - return; + state.absoluteSequence = seconds - 1; } - // First attempt gave empty result, possibly because the stream is not yet finished, but the next segment is just - // not ready yet. Keep retrying at EMPTY_RETRY_INTERVAL_MS intervals until EMPTY_RETRY_THRESHOLD_MS is reached. - long waitStart = System.currentTimeMillis(); - long iterationStart = waitStart; - - while (!processNextSegment(localExecutor)) { - // EMPTY_RETRY_THRESHOLD_MS is the maximum time between the end of the first attempt and the beginning of the last - // attempt, to avoid retry being skipped due to response coming slowly. - if (iterationStart - waitStart >= EMPTY_RETRY_THRESHOLD_MS) { - state.finished = true; - break; - } else { - Thread.sleep(EMPTY_RETRY_INTERVAL_MS); - iterationStart = System.currentTimeMillis(); - } + private void processNextSegmentWithRetry( + LocalAudioTrackExecutor localExecutor + ) throws InterruptedException { + if (processNextSegment(localExecutor)) { + return; + } + + // First attempt gave empty result, possibly because the stream is not yet finished, but the next segment is just + // not ready yet. Keep retrying at EMPTY_RETRY_INTERVAL_MS intervals until EMPTY_RETRY_THRESHOLD_MS is reached. + long waitStart = System.currentTimeMillis(); + long iterationStart = waitStart; + + while (!processNextSegment(localExecutor)) { + // EMPTY_RETRY_THRESHOLD_MS is the maximum time between the end of the first attempt and the beginning of the last + // attempt, to avoid retry being skipped due to response coming slowly. + if (iterationStart - waitStart >= EMPTY_RETRY_THRESHOLD_MS) { + state.finished = true; + break; + } else { + Thread.sleep(EMPTY_RETRY_INTERVAL_MS); + iterationStart = System.currentTimeMillis(); + } + } } - } - private boolean processNextSegment( - LocalAudioTrackExecutor localExecutor - ) throws InterruptedException { - URI segmentUrl = getNextSegmentUrl(state); + private boolean processNextSegment( + LocalAudioTrackExecutor localExecutor + ) throws InterruptedException { + URI segmentUrl = getNextSegmentUrl(state); - log.debug("Segment URL: {}", segmentUrl.toString()); + log.debug("Segment URL: {}", segmentUrl.toString()); - try (YoutubePersistentHttpStream stream = new YoutubePersistentHttpStream(httpInterface, segmentUrl, CONTENT_LENGTH_UNKNOWN)) { - if (stream.checkStatusCode() == HttpStatus.SC_NO_CONTENT || stream.getContentLength() == 0) { - return false; - } + try (YoutubePersistentHttpStream stream = new YoutubePersistentHttpStream(httpInterface, segmentUrl, CONTENT_LENGTH_UNKNOWN)) { + if (stream.checkStatusCode() == HttpStatus.SC_NO_CONTENT || stream.getContentLength() == 0) { + return false; + } - // If we were redirected, use that URL as a base for the next segment URL. Otherwise we will likely get redirected - // again on every other request, which is inefficient (redirects across domains, the original URL is always - // closing the connection, whereas the final URL is keep-alive). - state.redirectUrl = httpInterface.getFinalLocation(); + // If we were redirected, use that URL as a base for the next segment URL. Otherwise we will likely get redirected + // again on every other request, which is inefficient (redirects across domains, the original URL is always + // closing the connection, whereas the final URL is keep-alive). + state.redirectUrl = httpInterface.getFinalLocation(); - processSegmentStream(stream, localExecutor.getProcessingContext(), state); + processSegmentStream(stream, localExecutor.getProcessingContext(), state); - stream.releaseConnection(); - } catch (IOException e) { - // IOException here usually means that stream is about to end. - return false; + stream.releaseConnection(); + } catch (IOException e) { + // IOException here usually means that stream is about to end. + return false; + } + + return true; } - return true; - } + private void processSegmentStream(SeekableInputStream stream, AudioProcessingContext context, TrackState state) throws InterruptedException, IOException { + MpegFileLoader file = new MpegFileLoader(stream); + file.parseHeaders(); - private void processSegmentStream(SeekableInputStream stream, AudioProcessingContext context, TrackState state) throws InterruptedException, IOException { - MpegFileLoader file = new MpegFileLoader(stream); - file.parseHeaders(); + if (!trackInfo.isStream) { + state.absoluteSequence++; + } else { + state.absoluteSequence = extractAbsoluteSequenceFromEvent(file.getLastEventMessage()).sequence; + } - if (!trackInfo.isStream) { - state.absoluteSequence++; - } else { - state.absoluteSequence = extractAbsoluteSequenceFromEvent(file.getLastEventMessage()).sequence; - } + if (state.trackConsumer == null) { + state.trackConsumer = loadAudioTrack(file, context); + } - if (state.trackConsumer == null) { - state.trackConsumer = loadAudioTrack(file, context); - } + MpegFileTrackProvider fileReader = file.loadReader(state.trackConsumer); + if (fileReader == null) { + throw new FriendlyException("Unknown MP4 format.", SUSPICIOUS, null); + } - MpegFileTrackProvider fileReader = file.loadReader(state.trackConsumer); - if (fileReader == null) { - throw new FriendlyException("Unknown MP4 format.", SUSPICIOUS, null); + fileReader.provideFrames(); } - fileReader.provideFrames(); - } + private URI getNextSegmentUrl(TrackState state) { + URIBuilder builder = new URIBuilder(state.redirectUrl == null ? state.initialUrl : state.redirectUrl) + .setParameter("rn", String.valueOf(state.relativeSequence)) + .setParameter("rbuf", "0"); - private URI getNextSegmentUrl(TrackState state) { - URIBuilder builder = new URIBuilder(state.redirectUrl == null ? state.initialUrl : state.redirectUrl) - .setParameter("rn", String.valueOf(state.relativeSequence)) - .setParameter("rbuf", "0"); + if (state.absoluteSequence != null) { + builder.setParameter("sq", String.valueOf(state.absoluteSequence + 1)); + } - if (state.absoluteSequence != null) { - builder.setParameter("sq", String.valueOf(state.absoluteSequence + 1)); + try { + return builder.build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } - try { - return builder.build(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } + private SequenceInfo extractAbsoluteSequenceFromEvent(byte[] data) { + if (data == null) { + return null; + } - private SequenceInfo extractAbsoluteSequenceFromEvent(byte[] data) { - if (data == null) { - return null; - } + String message = new String(data, StandardCharsets.UTF_8); + String sequence = DataFormatTools.extractBetween(message, "Sequence-Number: ", "\r\n"); + String duration = DataFormatTools.extractBetween(message, "Target-Duration-Us: ", "\r\n"); - String message = new String(data, StandardCharsets.UTF_8); - String sequence = DataFormatTools.extractBetween(message, "Sequence-Number: ", "\r\n"); - String duration = DataFormatTools.extractBetween(message, "Target-Duration-Us: ", "\r\n"); + if (sequence != null && duration != null) { + return new SequenceInfo(Long.parseLong(sequence), TimeUnit.MICROSECONDS.toMillis(Long.parseLong(duration))); + } - if (sequence != null && duration != null) { - return new SequenceInfo(Long.parseLong(sequence), TimeUnit.MICROSECONDS.toMillis(Long.parseLong(duration))); + return null; } - return null; - } - - private static class TrackState { - private long globalSequenceDuration; - private long globalSequence; - private long relativeSequence; - private Long absoluteSequence; - private MpegTrackConsumer trackConsumer; - private boolean finished; - private boolean seeking; - private URI redirectUrl; - private final URI initialUrl; - - public TrackState(URI initialUrl) { - this.initialUrl = initialUrl; + private static class TrackState { + private long globalSequenceDuration; + private long globalSequence; + private long relativeSequence; + private Long absoluteSequence; + private MpegTrackConsumer trackConsumer; + private boolean finished; + private boolean seeking; + private URI redirectUrl; + private final URI initialUrl; + + public TrackState(URI initialUrl) { + this.initialUrl = initialUrl; + } } - } - private static class SequenceInfo { - private final long sequence; - private final long duration; + private static class SequenceInfo { + private final long sequence; + private final long duration; - public SequenceInfo(long sequence, long duration) { - this.sequence = sequence; - this.duration = duration; + public SequenceInfo(long sequence, long duration) { + this.sequence = sequence; + this.duration = duration; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubePayloadHelper.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubePayloadHelper.java index 7a663e7e0..3f72a8f84 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubePayloadHelper.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubePayloadHelper.java @@ -4,13 +4,13 @@ public class YoutubePayloadHelper { - public static JSONObject putOnceAndJoin(JSONObject json, String key) { - if (key != null) { - if (json.opt(key) != null) { - return json.getJSONObject(key); - } - return json.put(key, new JSONObject()).getJSONObject(key); + public static JSONObject putOnceAndJoin(JSONObject json, String key) { + if (key != null) { + if (json.opt(key) != null) { + return json.getJSONObject(key); + } + return json.put(key, new JSONObject()).getJSONObject(key); + } + return json.getJSONObject(null); } - return json.getJSONObject(null); - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubePersistentHttpStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubePersistentHttpStream.java index 1596756b6..8713899be 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubePersistentHttpStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubePersistentHttpStream.java @@ -16,127 +16,127 @@ * the start position at which to start reading on a new connection. */ public class YoutubePersistentHttpStream extends PersistentHttpStream { - private static final Logger log = LoggerFactory.getLogger(YoutubePersistentHttpStream.class); + private static final Logger log = LoggerFactory.getLogger(YoutubePersistentHttpStream.class); - // Valid range for requesting without throttling is 0-11862014 - private static final long BUFFER_SIZE = 11862014; + // Valid range for requesting without throttling is 0-11862014 + private static final long BUFFER_SIZE = 11862014; - private long rangeEnd; + private long rangeEnd; - /** - * @param httpInterface The HTTP interface to use for requests - * @param contentUrl The URL of the resource - * @param contentLength The length of the resource in bytes - */ - public YoutubePersistentHttpStream(HttpInterface httpInterface, URI contentUrl, long contentLength) { - super(httpInterface, contentUrl, contentLength); - } + /** + * @param httpInterface The HTTP interface to use for requests + * @param contentUrl The URL of the resource + * @param contentLength The length of the resource in bytes + */ + public YoutubePersistentHttpStream(HttpInterface httpInterface, URI contentUrl, long contentLength) { + super(httpInterface, contentUrl, contentLength); + } - @Override - protected URI getConnectUrl() { - if (!contentUrl.toString().contains("rn=")) { - URI rangeUrl = getNextRangeUrl(); + @Override + protected URI getConnectUrl() { + if (!contentUrl.toString().contains("rn=")) { + URI rangeUrl = getNextRangeUrl(); - log.debug("Range URL: {}", rangeUrl.toString()); + log.debug("Range URL: {}", rangeUrl.toString()); - return rangeUrl; - } else { - return contentUrl; - } - } - - @Override - protected int internalRead(byte[] b, int off, int len, boolean attemptReconnect) throws IOException { - connect(false); - long nextExpectedPosition = position + len + (len / 2); - - try { - int result; - if (nextExpectedPosition >= rangeEnd && rangeEnd != 0) { - if (rangeEnd == contentLength) { - result = currentContent.read(b, off, len); - position += result; + return rangeUrl; } else { - result = 0; - handleRangeEnd(null, attemptReconnect); + return contentUrl; } - } else { - result = currentContent.read(b, off, len); - if (result >= 0) { - position += result; - if (position >= rangeEnd && !contentUrl.toString().contains("rn=")) { - handleRangeEnd(null, attemptReconnect); - } - } - } + } - return result; - } catch (IOException e) { - handleRangeEnd(e, attemptReconnect); - return internalRead(b, off, len, false); + @Override + protected int internalRead(byte[] b, int off, int len, boolean attemptReconnect) throws IOException { + connect(false); + long nextExpectedPosition = position + len + (len / 2); + + try { + int result; + if (nextExpectedPosition >= rangeEnd && rangeEnd != 0) { + if (rangeEnd == contentLength) { + result = currentContent.read(b, off, len); + position += result; + } else { + result = 0; + handleRangeEnd(null, attemptReconnect); + } + } else { + result = currentContent.read(b, off, len); + if (result >= 0) { + position += result; + if (position >= rangeEnd && !contentUrl.toString().contains("rn=")) { + handleRangeEnd(null, attemptReconnect); + } + } + } + + return result; + } catch (IOException e) { + handleRangeEnd(e, attemptReconnect); + return internalRead(b, off, len, false); + } } - } - - @Override - protected long internalSkip(long n, boolean attemptReconnect) throws IOException { - connect(false); - long nextExpectedPosition = position + n; - - try { - long result; - if (nextExpectedPosition >= rangeEnd && rangeEnd != 0) { - if (rangeEnd == contentLength) { - result = currentContent.skip(n); - position += result; - } else { - result = n; - position += n; - handleRangeEnd(null, attemptReconnect); + + @Override + protected long internalSkip(long n, boolean attemptReconnect) throws IOException { + connect(false); + long nextExpectedPosition = position + n; + + try { + long result; + if (nextExpectedPosition >= rangeEnd && rangeEnd != 0) { + if (rangeEnd == contentLength) { + result = currentContent.skip(n); + position += result; + } else { + result = n; + position += n; + handleRangeEnd(null, attemptReconnect); + } + } else { + result = currentContent.skip(n); + position += result; + if (position >= rangeEnd && !contentUrl.toString().contains("rn=")) { + handleRangeEnd(null, attemptReconnect); + } + } + + return result; + } catch (IOException e) { + handleRangeEnd(e, attemptReconnect); + return internalSkip(n, false); } - } else { - result = currentContent.skip(n); - position += result; - if (position >= rangeEnd && !contentUrl.toString().contains("rn=")) { - handleRangeEnd(null, attemptReconnect); + } + + private URI getNextRangeUrl() { + rangeEnd = position + BUFFER_SIZE; + + if (rangeEnd > contentLength) { + rangeEnd = contentLength; } - } - return result; - } catch (IOException e) { - handleRangeEnd(e, attemptReconnect); - return internalSkip(n, false); + try { + return new URIBuilder(contentUrl).addParameter("range", position + "-" + rangeEnd).build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } - } - private URI getNextRangeUrl() { - rangeEnd = position + BUFFER_SIZE; + private void handleRangeEnd(IOException exception, boolean attemptReconnect) throws IOException { + if (!attemptReconnect || (!HttpClientTools.isRetriableNetworkException(exception) && exception != null)) { + throw exception; + } - if (rangeEnd > contentLength) { - rangeEnd = contentLength; + close(); } - try { - return new URIBuilder(contentUrl).addParameter("range", position + "-" + rangeEnd).build(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); + @Override + protected boolean useHeadersForRange() { + return false; } - } - private void handleRangeEnd(IOException exception, boolean attemptReconnect) throws IOException { - if (!attemptReconnect || (!HttpClientTools.isRetriableNetworkException(exception) && exception != null)) { - throw exception; + @Override + public boolean canSeekHard() { + return true; } - - close(); - } - - @Override - protected boolean useHeadersForRange() { - return false; - } - - @Override - public boolean canSeekHard() { - return true; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubePlaylistLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubePlaylistLoader.java index 7f7ce85fd..905daaa37 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubePlaylistLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubePlaylistLoader.java @@ -4,11 +4,12 @@ import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; + import java.util.function.Function; public interface YoutubePlaylistLoader { - void setPlaylistPageCount(int playlistPageCount); + void setPlaylistPageCount(int playlistPageCount); - AudioPlaylist load(HttpInterface httpInterface, String playlistId, String selectedVideoId, - Function trackFactory); + AudioPlaylist load(HttpInterface httpInterface, String playlistId, String selectedVideoId, + Function trackFactory); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchMusicProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchMusicProvider.java index 776ac02ea..3fe16533f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchMusicProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchMusicProvider.java @@ -30,137 +30,137 @@ * Handles processing YouTube Music searches. */ public class YoutubeSearchMusicProvider implements YoutubeSearchMusicResultLoader { - private static final Logger log = LoggerFactory.getLogger(YoutubeSearchMusicProvider.class); - - private final HttpInterfaceManager httpInterfaceManager; - - public YoutubeSearchMusicProvider() { - this.httpInterfaceManager = HttpClientTools.createCookielessThreadLocalManager(); - } - - public ExtendedHttpConfigurable getHttpConfiguration() { - return httpInterfaceManager; - } - - /** - * @param query Search query. - * @return Playlist of the first page of music results. - */ - @Override - public AudioItem loadSearchMusicResult(String query, Function trackFactory) { - log.debug("Performing a search music with query {}", query); - - try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { - HttpPost post = new HttpPost(MUSIC_SEARCH_URL); - YoutubeClientConfig clientConfig = YoutubeClientConfig.MUSIC.copy() - .withRootField("query", query) - .withRootField("params", SEARCH_MUSIC_PARAMS) - .setAttribute(httpInterface); - StringEntity payload = new StringEntity(clientConfig.toJsonString(), "UTF-8"); - post.setHeader("Referer", "music.youtube.com"); - post.setEntity(payload); - - try (CloseableHttpResponse response = httpInterface.execute(post)) { - HttpClientTools.assertSuccessWithContent(response, "search music response"); - - String responseText = EntityUtils.toString(response.getEntity(), UTF_8); - - JsonBrowser jsonBrowser = JsonBrowser.parse(responseText); - return extractSearchResults(jsonBrowser, query, trackFactory); - } - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions(e); - } - } - - private AudioItem extractSearchResults(JsonBrowser jsonBrowser, String query, - Function trackFactory) { - List tracks; - log.debug("Attempting to parse results from music search page"); - try { - tracks = extractMusicSearchPage(jsonBrowser, trackFactory); - } catch (IOException e) { - throw new RuntimeException(e); - } + private static final Logger log = LoggerFactory.getLogger(YoutubeSearchMusicProvider.class); - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } else { - return new BasicAudioPlaylist("Search results for: " + query, tracks, null, true); - } - } - - private List extractMusicSearchPage(JsonBrowser jsonBrowser, Function trackFactory) throws IOException { - ArrayList list = new ArrayList<>(); - JsonBrowser tracks = jsonBrowser.get("contents") - .get("tabbedSearchResultsRenderer") - .get("tabs") - .index(0) - .get("tabRenderer") - .get("content") - .get("sectionListRenderer") - .get("contents") - .index(0) - .get("musicShelfRenderer") - .get("contents"); - if (tracks == JsonBrowser.NULL_BROWSER) { - tracks = jsonBrowser.get("contents") - .get("tabbedSearchResultsRenderer") - .get("tabs") - .index(0) - .get("tabRenderer") - .get("content") - .get("sectionListRenderer") - .get("contents") - .index(1) - .get("musicShelfRenderer") - .get("contents"); + private final HttpInterfaceManager httpInterfaceManager; + + public YoutubeSearchMusicProvider() { + this.httpInterfaceManager = HttpClientTools.createCookielessThreadLocalManager(); } - tracks.values().forEach(jsonTrack -> { - AudioTrack track = extractMusicTrack(jsonTrack, trackFactory); - if (track != null) list.add(track); - }); - return list; - } - - private AudioTrack extractMusicTrack(JsonBrowser jsonBrowser, Function trackFactory) { - JsonBrowser thumbnail = jsonBrowser.get("musicResponsiveListItemRenderer").get("thumbnail").get("musicThumbnailRenderer"); - JsonBrowser columns = jsonBrowser.get("musicResponsiveListItemRenderer").get("flexColumns"); - if (columns.isNull()) { - // Somehow don't get track info, ignore - return null; + + public ExtendedHttpConfigurable getHttpConfiguration() { + return httpInterfaceManager; } - JsonBrowser firstColumn = columns.index(0) - .get("musicResponsiveListItemFlexColumnRenderer") - .get("text") - .get("runs") - .index(0); - String title = firstColumn.get("text").text(); - String videoId = firstColumn.get("navigationEndpoint") - .get("watchEndpoint") - .get("videoId").text(); - if (videoId == null) { - // If track is not available on YouTube Music videoId will be empty - return null; + + /** + * @param query Search query. + * @return Playlist of the first page of music results. + */ + @Override + public AudioItem loadSearchMusicResult(String query, Function trackFactory) { + log.debug("Performing a search music with query {}", query); + + try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { + HttpPost post = new HttpPost(MUSIC_SEARCH_URL); + YoutubeClientConfig clientConfig = YoutubeClientConfig.MUSIC.copy() + .withRootField("query", query) + .withRootField("params", SEARCH_MUSIC_PARAMS) + .setAttribute(httpInterface); + StringEntity payload = new StringEntity(clientConfig.toJsonString(), "UTF-8"); + post.setHeader("Referer", "music.youtube.com"); + post.setEntity(payload); + + try (CloseableHttpResponse response = httpInterface.execute(post)) { + HttpClientTools.assertSuccessWithContent(response, "search music response"); + + String responseText = EntityUtils.toString(response.getEntity(), UTF_8); + + JsonBrowser jsonBrowser = JsonBrowser.parse(responseText); + return extractSearchResults(jsonBrowser, query, trackFactory); + } + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions(e); + } } - List secondColumn = columns.index(1) - .get("musicResponsiveListItemFlexColumnRenderer") - .get("text") - .get("runs").values(); - String author = secondColumn.get(0) - .get("text").text(); - JsonBrowser lastElement = secondColumn.get(secondColumn.size() - 1); - - if (!lastElement.get("navigationEndpoint").isNull()) { - // The duration element should not have this key, if it does, then duration is probably missing, so return - return null; + + private AudioItem extractSearchResults(JsonBrowser jsonBrowser, String query, + Function trackFactory) { + List tracks; + log.debug("Attempting to parse results from music search page"); + try { + tracks = extractMusicSearchPage(jsonBrowser, trackFactory); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } else { + return new BasicAudioPlaylist("Search results for: " + query, tracks, null, true); + } } - long duration = DataFormatTools.durationTextToMillis(lastElement.get("text").text()); + private List extractMusicSearchPage(JsonBrowser jsonBrowser, Function trackFactory) throws IOException { + ArrayList list = new ArrayList<>(); + JsonBrowser tracks = jsonBrowser.get("contents") + .get("tabbedSearchResultsRenderer") + .get("tabs") + .index(0) + .get("tabRenderer") + .get("content") + .get("sectionListRenderer") + .get("contents") + .index(0) + .get("musicShelfRenderer") + .get("contents"); + if (tracks == JsonBrowser.NULL_BROWSER) { + tracks = jsonBrowser.get("contents") + .get("tabbedSearchResultsRenderer") + .get("tabs") + .index(0) + .get("tabRenderer") + .get("content") + .get("sectionListRenderer") + .get("contents") + .index(1) + .get("musicShelfRenderer") + .get("contents"); + } + tracks.values().forEach(jsonTrack -> { + AudioTrack track = extractMusicTrack(jsonTrack, trackFactory); + if (track != null) list.add(track); + }); + return list; + } - AudioTrackInfo info = new AudioTrackInfo(title, author, duration, videoId, false, + private AudioTrack extractMusicTrack(JsonBrowser jsonBrowser, Function trackFactory) { + JsonBrowser thumbnail = jsonBrowser.get("musicResponsiveListItemRenderer").get("thumbnail").get("musicThumbnailRenderer"); + JsonBrowser columns = jsonBrowser.get("musicResponsiveListItemRenderer").get("flexColumns"); + if (columns.isNull()) { + // Somehow don't get track info, ignore + return null; + } + JsonBrowser firstColumn = columns.index(0) + .get("musicResponsiveListItemFlexColumnRenderer") + .get("text") + .get("runs") + .index(0); + String title = firstColumn.get("text").text(); + String videoId = firstColumn.get("navigationEndpoint") + .get("watchEndpoint") + .get("videoId").text(); + if (videoId == null) { + // If track is not available on YouTube Music videoId will be empty + return null; + } + List secondColumn = columns.index(1) + .get("musicResponsiveListItemFlexColumnRenderer") + .get("text") + .get("runs").values(); + String author = secondColumn.get(0) + .get("text").text(); + JsonBrowser lastElement = secondColumn.get(secondColumn.size() - 1); + + if (!lastElement.get("navigationEndpoint").isNull()) { + // The duration element should not have this key, if it does, then duration is probably missing, so return + return null; + } + + long duration = DataFormatTools.durationTextToMillis(lastElement.get("text").text()); + + AudioTrackInfo info = new AudioTrackInfo(title, author, duration, videoId, false, WATCH_URL_PREFIX + videoId, ThumbnailTools.getYouTubeMusicThumbnail(thumbnail, videoId), null); - return trackFactory.apply(info); - } -} \ No newline at end of file + return trackFactory.apply(info); + } +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchMusicResultLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchMusicResultLoader.java index 1c9c27056..160356e3d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchMusicResultLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchMusicResultLoader.java @@ -4,10 +4,11 @@ import com.sedmelluq.discord.lavaplayer.track.AudioItem; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; + import java.util.function.Function; public interface YoutubeSearchMusicResultLoader { AudioItem loadSearchMusicResult(String query, Function trackFactory); ExtendedHttpConfigurable getHttpConfiguration(); -} \ No newline at end of file +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchProvider.java index 66565db16..fcbe33e12 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchProvider.java @@ -30,97 +30,97 @@ * Handles processing YouTube searches. */ public class YoutubeSearchProvider implements YoutubeSearchResultLoader { - private static final Logger log = LoggerFactory.getLogger(YoutubeSearchProvider.class); - - private final HttpInterfaceManager httpInterfaceManager; - - public YoutubeSearchProvider() { - this.httpInterfaceManager = HttpClientTools.createCookielessThreadLocalManager(); - } - - public ExtendedHttpConfigurable getHttpConfiguration() { - return httpInterfaceManager; - } - - /** - * @param query Search query. - * @return Playlist of the first page of results. - */ - @Override - public AudioItem loadSearchResult(String query, Function trackFactory) { - log.debug("Performing a search with query {}", query); - - try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { - HttpPost post = new HttpPost(SEARCH_URL); - YoutubeClientConfig clientConfig = YoutubeClientConfig.ANDROID.copy() - .withRootField("query", query) - .withRootField("params", SEARCH_PARAMS) - .setAttribute(httpInterface); - StringEntity payload = new StringEntity(clientConfig.toJsonString(), "UTF-8"); - post.setEntity(payload); - - try (CloseableHttpResponse response = httpInterface.execute(post)) { - HttpClientTools.assertSuccessWithContent(response, "search response"); - - String responseText = EntityUtils.toString(response.getEntity(), UTF_8); - - JsonBrowser jsonBrowser = JsonBrowser.parse(responseText); - return extractSearchResults(jsonBrowser, query, trackFactory); - } - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions(e); + private static final Logger log = LoggerFactory.getLogger(YoutubeSearchProvider.class); + + private final HttpInterfaceManager httpInterfaceManager; + + public YoutubeSearchProvider() { + this.httpInterfaceManager = HttpClientTools.createCookielessThreadLocalManager(); + } + + public ExtendedHttpConfigurable getHttpConfiguration() { + return httpInterfaceManager; } - } - - private AudioItem extractSearchResults(JsonBrowser jsonBrowser, String query, - Function trackFactory) { - List tracks; - log.debug("Attempting to parse results from search page"); - try { - tracks = extractSearchPage(jsonBrowser, trackFactory); - } catch (IOException e) { - throw new RuntimeException(e); + + /** + * @param query Search query. + * @return Playlist of the first page of results. + */ + @Override + public AudioItem loadSearchResult(String query, Function trackFactory) { + log.debug("Performing a search with query {}", query); + + try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { + HttpPost post = new HttpPost(SEARCH_URL); + YoutubeClientConfig clientConfig = YoutubeClientConfig.ANDROID.copy() + .withRootField("query", query) + .withRootField("params", SEARCH_PARAMS) + .setAttribute(httpInterface); + StringEntity payload = new StringEntity(clientConfig.toJsonString(), "UTF-8"); + post.setEntity(payload); + + try (CloseableHttpResponse response = httpInterface.execute(post)) { + HttpClientTools.assertSuccessWithContent(response, "search response"); + + String responseText = EntityUtils.toString(response.getEntity(), UTF_8); + + JsonBrowser jsonBrowser = JsonBrowser.parse(responseText); + return extractSearchResults(jsonBrowser, query, trackFactory); + } + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions(e); + } } - if (tracks.isEmpty()) { - return AudioReference.NO_TRACK; - } else { - return new BasicAudioPlaylist("Search results for: " + query, tracks, null, true); + private AudioItem extractSearchResults(JsonBrowser jsonBrowser, String query, + Function trackFactory) { + List tracks; + log.debug("Attempting to parse results from search page"); + try { + tracks = extractSearchPage(jsonBrowser, trackFactory); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (tracks.isEmpty()) { + return AudioReference.NO_TRACK; + } else { + return new BasicAudioPlaylist("Search results for: " + query, tracks, null, true); + } } - } - private List extractSearchPage(JsonBrowser jsonBrowser, Function trackFactory) throws IOException { - ArrayList list = new ArrayList<>(); - jsonBrowser.get("contents") + private List extractSearchPage(JsonBrowser jsonBrowser, Function trackFactory) throws IOException { + ArrayList list = new ArrayList<>(); + jsonBrowser.get("contents") .get("sectionListRenderer") .get("contents") .values() .forEach(content -> content.get("itemSectionRenderer") - .get("contents") - .values() - .forEach(jsonTrack -> { - AudioTrack track = extractPolymerData(jsonTrack, trackFactory); - if (track != null) list.add(track); - }) + .get("contents") + .values() + .forEach(jsonTrack -> { + AudioTrack track = extractPolymerData(jsonTrack, trackFactory); + if (track != null) list.add(track); + }) ); - return list; - } + return list; + } - private AudioTrack extractPolymerData(JsonBrowser json, Function trackFactory) { - json = json.get("compactVideoRenderer"); - if (json.isNull()) return null; // Ignore everything which is not a track + private AudioTrack extractPolymerData(JsonBrowser json, Function trackFactory) { + json = json.get("compactVideoRenderer"); + if (json.isNull()) return null; // Ignore everything which is not a track - String title = json.get("title").get("runs").index(0).get("text").text(); - String author = json.get("longBylineText").get("runs").index(0).get("text").text(); - if (json.get("lengthText").isNull()) { - return null; // Ignore if the video is a live stream - } - long duration = DataFormatTools.durationTextToMillis(json.get("lengthText").get("runs").index(0).get("text").text()); - String videoId = json.get("videoId").text(); + String title = json.get("title").get("runs").index(0).get("text").text(); + String author = json.get("longBylineText").get("runs").index(0).get("text").text(); + if (json.get("lengthText").isNull()) { + return null; // Ignore if the video is a live stream + } + long duration = DataFormatTools.durationTextToMillis(json.get("lengthText").get("runs").index(0).get("text").text()); + String videoId = json.get("videoId").text(); - AudioTrackInfo info = new AudioTrackInfo(title, author, duration, videoId, false, - WATCH_URL_PREFIX + videoId, ThumbnailTools.getYouTubeThumbnail(json, videoId), null); + AudioTrackInfo info = new AudioTrackInfo(title, author, duration, videoId, false, + WATCH_URL_PREFIX + videoId, ThumbnailTools.getYouTubeThumbnail(json, videoId), null); - return trackFactory.apply(info); - } + return trackFactory.apply(info); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchResultLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchResultLoader.java index 96fd3549a..df3f03731 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchResultLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSearchResultLoader.java @@ -4,10 +4,11 @@ import com.sedmelluq.discord.lavaplayer.track.AudioItem; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; + import java.util.function.Function; public interface YoutubeSearchResultLoader { - AudioItem loadSearchResult(String query, Function trackFactory); + AudioItem loadSearchResult(String query, Function trackFactory); - ExtendedHttpConfigurable getHttpConfiguration(); + ExtendedHttpConfigurable getHttpConfiguration(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureCipher.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureCipher.java index 8ae995c1e..f8c322611 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureCipher.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureCipher.java @@ -10,87 +10,87 @@ * Describes one signature cipher */ public class YoutubeSignatureCipher { - private final List operations = new ArrayList<>(); - String nFunction = ""; - String scriptTimestamp = ""; - String rawScript = ""; + private final List operations = new ArrayList<>(); + String nFunction = ""; + String scriptTimestamp = ""; + String rawScript = ""; - /** - * @param text Text to apply the cipher on - * @return The result of the cipher on the input text - */ - public String apply(String text) { - StringBuilder builder = new StringBuilder(text); + /** + * @param text Text to apply the cipher on + * @return The result of the cipher on the input text + */ + public String apply(String text) { + StringBuilder builder = new StringBuilder(text); - for (YoutubeCipherOperation operation : operations) { - switch (operation.type) { - case SWAP: - int position = operation.parameter % text.length(); - char temp = builder.charAt(0); - builder.setCharAt(0, builder.charAt(position)); - builder.setCharAt(position, temp); - break; - case REVERSE: - builder.reverse(); - break; - case SLICE: - case SPLICE: - builder.delete(0, operation.parameter); - break; - default: - throw new IllegalStateException("All branches should be covered"); - } - } + for (YoutubeCipherOperation operation : operations) { + switch (operation.type) { + case SWAP: + int position = operation.parameter % text.length(); + char temp = builder.charAt(0); + builder.setCharAt(0, builder.charAt(position)); + builder.setCharAt(position, temp); + break; + case REVERSE: + builder.reverse(); + break; + case SLICE: + case SPLICE: + builder.delete(0, operation.parameter); + break; + default: + throw new IllegalStateException("All branches should be covered"); + } + } - return builder.toString(); - } + return builder.toString(); + } - /** - * @param text Text to transform - * @param scriptEngine JavaScript engine to execute function - * @return The result of the n parameter transformation - */ - public String transform(String text, ScriptEngine scriptEngine) throws ScriptException, NoSuchMethodException { - String transformed; + /** + * @param text Text to transform + * @param scriptEngine JavaScript engine to execute function + * @return The result of the n parameter transformation + */ + public String transform(String text, ScriptEngine scriptEngine) throws ScriptException, NoSuchMethodException { + String transformed; - scriptEngine.eval("n=" + nFunction); - transformed = (String) ((Invocable) scriptEngine).invokeFunction("n", text); + scriptEngine.eval("n=" + nFunction); + transformed = (String) ((Invocable) scriptEngine).invokeFunction("n", text); - return transformed; - } + return transformed; + } - /** - * @param operation The operation to add to this cipher - */ - public void addOperation(YoutubeCipherOperation operation) { - operations.add(operation); - } + /** + * @param operation The operation to add to this cipher + */ + public void addOperation(YoutubeCipherOperation operation) { + operations.add(operation); + } - /** - * @return True if the cipher contains no operations. - */ - public boolean isEmpty() { - return operations.isEmpty(); - } + /** + * @return True if the cipher contains no operations. + */ + public boolean isEmpty() { + return operations.isEmpty(); + } - /** - * @param nFunction Extracted "n" function - */ - public void setNFunction(String nFunction) { - this.nFunction = nFunction; - } + /** + * @param nFunction Extracted "n" function + */ + public void setNFunction(String nFunction) { + this.nFunction = nFunction; + } - /** - * @param timestamp The timestamp in cipher - */ - public void setTimestamp(String timestamp) { - scriptTimestamp = timestamp; - } + /** + * @param timestamp The timestamp in cipher + */ + public void setTimestamp(String timestamp) { + scriptTimestamp = timestamp; + } - /** - * @param script Raw script - */ - public void setRawScript(String script) { - rawScript = script; - } + /** + * @param script Raw script + */ + public void setRawScript(String script) { + rawScript = script; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureCipherManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureCipherManager.java index c88622a1d..b0847a129 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureCipherManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureCipherManager.java @@ -34,264 +34,266 @@ * Handles parsing and caching of signature ciphers */ public class YoutubeSignatureCipherManager implements YoutubeSignatureResolver { - private static final Logger log = LoggerFactory.getLogger(YoutubeSignatureCipherManager.class); - - private static final String VARIABLE_PART = "[a-zA-Z_\\$][a-zA-Z_0-9]*"; - private static final String VARIABLE_PART_DEFINE = "\\\"?" + VARIABLE_PART + "\\\"?"; - private static final String BEFORE_ACCESS = "(?:\\[\\\"|\\.)"; - private static final String AFTER_ACCESS = "(?:\\\"\\]|)"; - private static final String VARIABLE_PART_ACCESS = BEFORE_ACCESS + VARIABLE_PART + AFTER_ACCESS; - private static final String REVERSE_PART = ":function\\(a\\)\\{(?:return )?a\\.reverse\\(\\)\\}"; - private static final String SLICE_PART = ":function\\(a,b\\)\\{return a\\.slice\\(b\\)\\}"; - private static final String SPLICE_PART = ":function\\(a,b\\)\\{a\\.splice\\(0,b\\)\\}"; - private static final String SWAP_PART = ":function\\(a,b\\)\\{" + - "var c=a\\[0\\];a\\[0\\]=a\\[b%a\\.length\\];a\\[b(?:%a.length|)\\]=c(?:;return a)?\\}"; - - private static final Pattern functionPattern = Pattern.compile("" + - "function(?: " + VARIABLE_PART + ")?\\(a\\)\\{" + - "a=a\\.split\\(\"\"\\);\\s*" + - "((?:(?:a=)?" + VARIABLE_PART + VARIABLE_PART_ACCESS + "\\(a,\\d+\\);)+)" + - "return a\\.join\\(\"\"\\)" + - "\\}" - ); - - private static final Pattern actionsPattern = Pattern.compile("" + - "var (" + VARIABLE_PART + ")=\\{((?:(?:" + - VARIABLE_PART_DEFINE + REVERSE_PART + "|" + - VARIABLE_PART_DEFINE + SLICE_PART + "|" + - VARIABLE_PART_DEFINE + SPLICE_PART + "|" + - VARIABLE_PART_DEFINE + SWAP_PART + - "),?\\n?)+)\\};" - ); - - private static final String PATTERN_PREFIX = "(?:^|,)\\\"?(" + VARIABLE_PART + ")\\\"?"; - - private static final Pattern reversePattern = Pattern.compile(PATTERN_PREFIX + REVERSE_PART, Pattern.MULTILINE); - private static final Pattern slicePattern = Pattern.compile(PATTERN_PREFIX + SLICE_PART, Pattern.MULTILINE); - private static final Pattern splicePattern = Pattern.compile(PATTERN_PREFIX + SPLICE_PART, Pattern.MULTILINE); - private static final Pattern swapPattern = Pattern.compile(PATTERN_PREFIX + SWAP_PART, Pattern.MULTILINE); - private static final Pattern timestampPattern = Pattern.compile("(signatureTimestamp|sts)[\\:](\\d+)"); - private static final Pattern nFunctionPattern = Pattern.compile( - "function\\(\\s*(\\w+)\\s*\\)\\s*\\{var" + - "\\s*(\\w+)=\\1\\.split\\(\"\"\\),\\s*(\\w+)=(\\[.*?\\]);\\s*\\3\\[\\d+\\]" + - "(.*?try)(\\{.*?\\})catch\\(\\s*(\\w+)\\s*\\)\\s*\\" + - "{\\s*return\"enhanced_except_([A-z0-9-]+)\"\\s*\\+\\s*\\1\\s*}\\s*return\\s*\\2\\.join\\(\"\"\\)\\};", Pattern.DOTALL - ); - - private static final Pattern signatureExtraction = Pattern.compile("/s/([^/]+)/"); - - private final ConcurrentMap cipherCache; - private final Set dumpedScriptUrls; - private final ScriptEngine scriptEngine; - private final Object cipherLoadLock; - - /** - * Create a new signature cipher manager - */ - public YoutubeSignatureCipherManager() { - this.cipherCache = new ConcurrentHashMap<>(); - this.dumpedScriptUrls = new HashSet<>(); - this.scriptEngine = new RhinoScriptEngineFactory().getScriptEngine(); - this.cipherLoadLock = new Object(); - } - - /** - * Produces a valid playback URL for the specified track - * @param httpInterface HTTP interface to use - * @param playerScript Address of the script which is used to decipher signatures - * @param format The track for which to get the URL - * @return Valid playback URL - * @throws IOException On network IO error - */ - @Override - public URI resolveFormatUrl(HttpInterface httpInterface, String playerScript, YoutubeTrackFormat format) throws IOException { - String signature = format.getSignature(); - String nParameter = format.getNParameter(); - URI initialUrl = format.getUrl(); - - URIBuilder uri = new URIBuilder(initialUrl); - YoutubeSignatureCipher cipher = getExtractedScript(httpInterface, playerScript); - - if (!DataFormatTools.isNullOrEmpty(signature)) { - uri.setParameter(format.getSignatureKey(), cipher.apply(signature)); - } + private static final Logger log = LoggerFactory.getLogger(YoutubeSignatureCipherManager.class); + + private static final String VARIABLE_PART = "[a-zA-Z_\\$][a-zA-Z_0-9]*"; + private static final String VARIABLE_PART_DEFINE = "\\\"?" + VARIABLE_PART + "\\\"?"; + private static final String BEFORE_ACCESS = "(?:\\[\\\"|\\.)"; + private static final String AFTER_ACCESS = "(?:\\\"\\]|)"; + private static final String VARIABLE_PART_ACCESS = BEFORE_ACCESS + VARIABLE_PART + AFTER_ACCESS; + private static final String REVERSE_PART = ":function\\(a\\)\\{(?:return )?a\\.reverse\\(\\)\\}"; + private static final String SLICE_PART = ":function\\(a,b\\)\\{return a\\.slice\\(b\\)\\}"; + private static final String SPLICE_PART = ":function\\(a,b\\)\\{a\\.splice\\(0,b\\)\\}"; + private static final String SWAP_PART = ":function\\(a,b\\)\\{" + + "var c=a\\[0\\];a\\[0\\]=a\\[b%a\\.length\\];a\\[b(?:%a.length|)\\]=c(?:;return a)?\\}"; + + private static final Pattern functionPattern = Pattern.compile("" + + "function(?: " + VARIABLE_PART + ")?\\(a\\)\\{" + + "a=a\\.split\\(\"\"\\);\\s*" + + "((?:(?:a=)?" + VARIABLE_PART + VARIABLE_PART_ACCESS + "\\(a,\\d+\\);)+)" + + "return a\\.join\\(\"\"\\)" + + "\\}" + ); - if (!DataFormatTools.isNullOrEmpty(nParameter)) { - try { - uri.setParameter("n", cipher.transform(nParameter, scriptEngine)); - } catch (ScriptException | NoSuchMethodException e) { - dumpProblematicScript(cipherCache.get(playerScript).rawScript, playerScript, String.format("Can't transform n parameter %s with %s n function", nParameter, cipher.nFunction)); - } - } + private static final Pattern actionsPattern = Pattern.compile("" + + "var (" + VARIABLE_PART + ")=\\{((?:(?:" + + VARIABLE_PART_DEFINE + REVERSE_PART + "|" + + VARIABLE_PART_DEFINE + SLICE_PART + "|" + + VARIABLE_PART_DEFINE + SPLICE_PART + "|" + + VARIABLE_PART_DEFINE + SWAP_PART + + "),?\\n?)+)\\};" + ); - try { - return uri.setParameter("ratebypass", "yes").build(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); + private static final String PATTERN_PREFIX = "(?:^|,)\\\"?(" + VARIABLE_PART + ")\\\"?"; + + private static final Pattern reversePattern = Pattern.compile(PATTERN_PREFIX + REVERSE_PART, Pattern.MULTILINE); + private static final Pattern slicePattern = Pattern.compile(PATTERN_PREFIX + SLICE_PART, Pattern.MULTILINE); + private static final Pattern splicePattern = Pattern.compile(PATTERN_PREFIX + SPLICE_PART, Pattern.MULTILINE); + private static final Pattern swapPattern = Pattern.compile(PATTERN_PREFIX + SWAP_PART, Pattern.MULTILINE); + private static final Pattern timestampPattern = Pattern.compile("(signatureTimestamp|sts)[\\:](\\d+)"); + private static final Pattern nFunctionPattern = Pattern.compile( + "function\\(\\s*(\\w+)\\s*\\)\\s*\\{var" + + "\\s*(\\w+)=\\1\\.split\\(\"\"\\),\\s*(\\w+)=(\\[.*?\\]);\\s*\\3\\[\\d+\\]" + + "(.*?try)(\\{.*?\\})catch\\(\\s*(\\w+)\\s*\\)\\s*\\" + + "{\\s*return\"enhanced_except_([A-z0-9-]+)\"\\s*\\+\\s*\\1\\s*}\\s*return\\s*\\2\\.join\\(\"\"\\)\\};", Pattern.DOTALL + ); + + private static final Pattern signatureExtraction = Pattern.compile("/s/([^/]+)/"); + + private final ConcurrentMap cipherCache; + private final Set dumpedScriptUrls; + private final ScriptEngine scriptEngine; + private final Object cipherLoadLock; + + /** + * Create a new signature cipher manager + */ + public YoutubeSignatureCipherManager() { + this.cipherCache = new ConcurrentHashMap<>(); + this.dumpedScriptUrls = new HashSet<>(); + this.scriptEngine = new RhinoScriptEngineFactory().getScriptEngine(); + this.cipherLoadLock = new Object(); } - } - - /** - * Produces a valid dash XML URL from the possibly ciphered URL. - * @param httpInterface HTTP interface instance to use - * @param playerScript Address of the script which is used to decipher signatures - * @param dashUrl URL of the dash XML, possibly with a ciphered signature - * @return Valid dash XML URL - * @throws IOException On network IO error - */ - @Override - public String resolveDashUrl(HttpInterface httpInterface, String playerScript, String dashUrl) throws IOException { - Matcher matcher = signatureExtraction.matcher(dashUrl); - - if (!matcher.find()) { - return dashUrl; + + /** + * Produces a valid playback URL for the specified track + * + * @param httpInterface HTTP interface to use + * @param playerScript Address of the script which is used to decipher signatures + * @param format The track for which to get the URL + * @return Valid playback URL + * @throws IOException On network IO error + */ + @Override + public URI resolveFormatUrl(HttpInterface httpInterface, String playerScript, YoutubeTrackFormat format) throws IOException { + String signature = format.getSignature(); + String nParameter = format.getNParameter(); + URI initialUrl = format.getUrl(); + + URIBuilder uri = new URIBuilder(initialUrl); + YoutubeSignatureCipher cipher = getExtractedScript(httpInterface, playerScript); + + if (!DataFormatTools.isNullOrEmpty(signature)) { + uri.setParameter(format.getSignatureKey(), cipher.apply(signature)); + } + + if (!DataFormatTools.isNullOrEmpty(nParameter)) { + try { + uri.setParameter("n", cipher.transform(nParameter, scriptEngine)); + } catch (ScriptException | NoSuchMethodException e) { + dumpProblematicScript(cipherCache.get(playerScript).rawScript, playerScript, String.format("Can't transform n parameter %s with %s n function", nParameter, cipher.nFunction)); + } + } + + try { + return uri.setParameter("ratebypass", "yes").build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } - YoutubeSignatureCipher cipher = getExtractedScript(httpInterface, playerScript); - return matcher.replaceFirst("/signature/" + cipher.apply(matcher.group(1)) + "/"); - } + /** + * Produces a valid dash XML URL from the possibly ciphered URL. + * + * @param httpInterface HTTP interface instance to use + * @param playerScript Address of the script which is used to decipher signatures + * @param dashUrl URL of the dash XML, possibly with a ciphered signature + * @return Valid dash XML URL + * @throws IOException On network IO error + */ + @Override + public String resolveDashUrl(HttpInterface httpInterface, String playerScript, String dashUrl) throws IOException { + Matcher matcher = signatureExtraction.matcher(dashUrl); + + if (!matcher.find()) { + return dashUrl; + } - @Override - public YoutubeSignatureCipher getExtractedScript(HttpInterface httpInterface, String cipherScriptUrl) throws IOException { - YoutubeSignatureCipher cipherKey = cipherCache.get(cipherScriptUrl); + YoutubeSignatureCipher cipher = getExtractedScript(httpInterface, playerScript); + return matcher.replaceFirst("/signature/" + cipher.apply(matcher.group(1)) + "/"); + } - if (cipherKey == null) { - synchronized (cipherLoadLock) { - log.debug("Parsing player script {}", cipherScriptUrl); + @Override + public YoutubeSignatureCipher getExtractedScript(HttpInterface httpInterface, String cipherScriptUrl) throws IOException { + YoutubeSignatureCipher cipherKey = cipherCache.get(cipherScriptUrl); - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(parseTokenScriptUrl(cipherScriptUrl)))) { - validateResponseCode(cipherScriptUrl, response); + if (cipherKey == null) { + synchronized (cipherLoadLock) { + log.debug("Parsing player script {}", cipherScriptUrl); - cipherKey = extractFromScript(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8), cipherScriptUrl); - cipherCache.put(cipherScriptUrl, cipherKey); + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(parseTokenScriptUrl(cipherScriptUrl)))) { + validateResponseCode(cipherScriptUrl, response); + + cipherKey = extractFromScript(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8), cipherScriptUrl); + cipherCache.put(cipherScriptUrl, cipherKey); + } + } } - } - } - return cipherKey; - } + return cipherKey; + } - private void validateResponseCode(String cipherScriptUrl, CloseableHttpResponse response) throws IOException { - int statusCode = response.getStatusLine().getStatusCode(); + private void validateResponseCode(String cipherScriptUrl, CloseableHttpResponse response) throws IOException { + int statusCode = response.getStatusLine().getStatusCode(); - if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new IOException("Received non-success response code " + statusCode + " from script url " + - cipherScriptUrl + " ( " + parseTokenScriptUrl(cipherScriptUrl) + " )"); + if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new IOException("Received non-success response code " + statusCode + " from script url " + + cipherScriptUrl + " ( " + parseTokenScriptUrl(cipherScriptUrl) + " )"); + } } - } - - private List getQuotedFunctions(String... functionNames) { - return Stream.of(functionNames) - .filter(Objects::nonNull) - .map(Pattern::quote) - .collect(Collectors.toList()); - } - - private void dumpProblematicScript(String script, String sourceUrl, String issue) { - if (!dumpedScriptUrls.add(sourceUrl)) { - return; + + private List getQuotedFunctions(String... functionNames) { + return Stream.of(functionNames) + .filter(Objects::nonNull) + .map(Pattern::quote) + .collect(Collectors.toList()); } - try { - Path path = Files.createTempFile("lavaplayer-yt-player-script", ".js"); - Files.write(path, script.getBytes(StandardCharsets.UTF_8)); + private void dumpProblematicScript(String script, String sourceUrl, String issue) { + if (!dumpedScriptUrls.add(sourceUrl)) { + return; + } + + try { + Path path = Files.createTempFile("lavaplayer-yt-player-script", ".js"); + Files.write(path, script.getBytes(StandardCharsets.UTF_8)); - log.error("Problematic YouTube player script {} detected (issue detected with script: {}). Dumped to {}", - sourceUrl, issue, path.toAbsolutePath()); - } catch (Exception e) { - log.error("Failed to dump problematic YouTube player script {} (issue detected with script: {})", sourceUrl, issue); + log.error("Problematic YouTube player script {} detected (issue detected with script: {}). Dumped to {}", + sourceUrl, issue, path.toAbsolutePath()); + } catch (Exception e) { + log.error("Failed to dump problematic YouTube player script {} (issue detected with script: {})", sourceUrl, issue); + } } - } - private YoutubeSignatureCipher extractFromScript(String script, String sourceUrl) { + private YoutubeSignatureCipher extractFromScript(String script, String sourceUrl) { - Matcher actions = actionsPattern.matcher(script); - Matcher nFunction = nFunctionPattern.matcher(script); - Matcher scriptTimestamp = timestampPattern.matcher(script); + Matcher actions = actionsPattern.matcher(script); + Matcher nFunction = nFunctionPattern.matcher(script); + Matcher scriptTimestamp = timestampPattern.matcher(script); - if (!actions.find()) { - dumpProblematicScript(script, sourceUrl, "no actions match"); - throw new IllegalStateException("Must find action functions from script: " + sourceUrl); - } + if (!actions.find()) { + dumpProblematicScript(script, sourceUrl, "no actions match"); + throw new IllegalStateException("Must find action functions from script: " + sourceUrl); + } - String actionBody = actions.group(2); + String actionBody = actions.group(2); - String reverseKey = extractDollarEscapedFirstGroup(reversePattern, actionBody); - String slicePart = extractDollarEscapedFirstGroup(slicePattern, actionBody); - String splicePart = extractDollarEscapedFirstGroup(splicePattern, actionBody); - String swapKey = extractDollarEscapedFirstGroup(swapPattern, actionBody); + String reverseKey = extractDollarEscapedFirstGroup(reversePattern, actionBody); + String slicePart = extractDollarEscapedFirstGroup(slicePattern, actionBody); + String splicePart = extractDollarEscapedFirstGroup(splicePattern, actionBody); + String swapKey = extractDollarEscapedFirstGroup(swapPattern, actionBody); - Pattern extractor = Pattern.compile( - "(?:a=)?" + Pattern.quote(actions.group(1)) + BEFORE_ACCESS + "(" + - String.join("|", getQuotedFunctions(reverseKey, slicePart, splicePart, swapKey)) + - ")" + AFTER_ACCESS + "\\(a,(\\d+)\\)" - ); + Pattern extractor = Pattern.compile( + "(?:a=)?" + Pattern.quote(actions.group(1)) + BEFORE_ACCESS + "(" + + String.join("|", getQuotedFunctions(reverseKey, slicePart, splicePart, swapKey)) + + ")" + AFTER_ACCESS + "\\(a,(\\d+)\\)" + ); - Matcher functions = functionPattern.matcher(script); - if (!functions.find()) { - dumpProblematicScript(script, sourceUrl, "no decipher function match"); - throw new IllegalStateException("Must find decipher function from script."); - } + Matcher functions = functionPattern.matcher(script); + if (!functions.find()) { + dumpProblematicScript(script, sourceUrl, "no decipher function match"); + throw new IllegalStateException("Must find decipher function from script."); + } - Matcher matcher = extractor.matcher(functions.group(1)); + Matcher matcher = extractor.matcher(functions.group(1)); - if (!scriptTimestamp.find()) { - dumpProblematicScript(script, sourceUrl, "no timestamp match"); - throw new IllegalStateException("Must find timestamp from script: " + sourceUrl); - } + if (!scriptTimestamp.find()) { + dumpProblematicScript(script, sourceUrl, "no timestamp match"); + throw new IllegalStateException("Must find timestamp from script: " + sourceUrl); + } - YoutubeSignatureCipher cipherKey = new YoutubeSignatureCipher(); + YoutubeSignatureCipher cipherKey = new YoutubeSignatureCipher(); - if (nFunction.find()) { - cipherKey.setNFunction(nFunction.group(0)); - } else { - // Don't throw any exceptions here since if n function is not extracted audio still can be played - dumpProblematicScript(script, sourceUrl, "no n function match"); - } + if (nFunction.find()) { + cipherKey.setNFunction(nFunction.group(0)); + } else { + // Don't throw any exceptions here since if n function is not extracted audio still can be played + dumpProblematicScript(script, sourceUrl, "no n function match"); + } - cipherKey.setTimestamp(scriptTimestamp.group(2)); - cipherKey.setRawScript(script); - - while (matcher.find()) { - String type = matcher.group(1); - - if (type.equals(swapKey)) { - cipherKey.addOperation(new YoutubeCipherOperation(YoutubeCipherOperationType.SWAP, Integer.parseInt(matcher.group(2)))); - } else if (type.equals(reverseKey)) { - cipherKey.addOperation(new YoutubeCipherOperation(YoutubeCipherOperationType.REVERSE, 0)); - } else if (type.equals(slicePart)) { - cipherKey.addOperation(new YoutubeCipherOperation(YoutubeCipherOperationType.SLICE, Integer.parseInt(matcher.group(2)))); - } else if (type.equals(splicePart)) { - cipherKey.addOperation(new YoutubeCipherOperation(YoutubeCipherOperationType.SPLICE, Integer.parseInt(matcher.group(2)))); - } else { - dumpProblematicScript(script, sourceUrl, "unknown cipher operation found"); - } + cipherKey.setTimestamp(scriptTimestamp.group(2)); + cipherKey.setRawScript(script); + + while (matcher.find()) { + String type = matcher.group(1); + + if (type.equals(swapKey)) { + cipherKey.addOperation(new YoutubeCipherOperation(YoutubeCipherOperationType.SWAP, Integer.parseInt(matcher.group(2)))); + } else if (type.equals(reverseKey)) { + cipherKey.addOperation(new YoutubeCipherOperation(YoutubeCipherOperationType.REVERSE, 0)); + } else if (type.equals(slicePart)) { + cipherKey.addOperation(new YoutubeCipherOperation(YoutubeCipherOperationType.SLICE, Integer.parseInt(matcher.group(2)))); + } else if (type.equals(splicePart)) { + cipherKey.addOperation(new YoutubeCipherOperation(YoutubeCipherOperationType.SPLICE, Integer.parseInt(matcher.group(2)))); + } else { + dumpProblematicScript(script, sourceUrl, "unknown cipher operation found"); + } + } + + if (cipherKey.isEmpty()) { + log.error("No operations detected from cipher extracted from {}.", sourceUrl); + dumpProblematicScript(script, sourceUrl, "no cipher operations"); + } + + return cipherKey; } - if (cipherKey.isEmpty()) { - log.error("No operations detected from cipher extracted from {}.", sourceUrl); - dumpProblematicScript(script, sourceUrl, "no cipher operations"); + private static String extractDollarEscapedFirstGroup(Pattern pattern, String text) { + Matcher matcher = pattern.matcher(text); + return matcher.find() ? matcher.group(1).replace("$", "\\$") : null; } - return cipherKey; - } - - private static String extractDollarEscapedFirstGroup(Pattern pattern, String text) { - Matcher matcher = pattern.matcher(text); - return matcher.find() ? matcher.group(1).replace("$", "\\$") : null; - } - - private static URI parseTokenScriptUrl(String urlString) { - try { - if (urlString.startsWith("//")) { - return new URI("https:" + urlString); - } else if (urlString.startsWith("/")) { - return new URI("https://www.youtube.com" + urlString); - } else { - return new URI(urlString); - } - } catch (URISyntaxException e) { - throw new RuntimeException(e); + private static URI parseTokenScriptUrl(String urlString) { + try { + if (urlString.startsWith("//")) { + return new URI("https:" + urlString); + } else if (urlString.startsWith("/")) { + return new URI("https://www.youtube.com" + urlString); + } else { + return new URI(urlString); + } + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureResolver.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureResolver.java index 5a7f6a9b8..23d4ac75b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureResolver.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeSignatureResolver.java @@ -6,9 +6,9 @@ import java.net.URI; public interface YoutubeSignatureResolver { - YoutubeSignatureCipher getExtractedScript(HttpInterface httpInterface, String playerScript) throws IOException; + YoutubeSignatureCipher getExtractedScript(HttpInterface httpInterface, String playerScript) throws IOException; - URI resolveFormatUrl(HttpInterface httpInterface, String playerScript, YoutubeTrackFormat format) throws Exception; + URI resolveFormatUrl(HttpInterface httpInterface, String playerScript, YoutubeTrackFormat format) throws Exception; - String resolveDashUrl(HttpInterface httpInterface, String playerScript, String dashUrl) throws Exception; + String resolveDashUrl(HttpInterface httpInterface, String playerScript, String dashUrl) throws Exception; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackDetails.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackDetails.java index 2351e3b0e..4f8121f9b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackDetails.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackDetails.java @@ -2,12 +2,13 @@ import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; + import java.util.List; public interface YoutubeTrackDetails { - AudioTrackInfo getTrackInfo(); + AudioTrackInfo getTrackInfo(); - List getFormats(HttpInterface httpInterface, YoutubeSignatureResolver signatureResolver); + List getFormats(HttpInterface httpInterface, YoutubeSignatureResolver signatureResolver); - String getPlayerScript(); + String getPlayerScript(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackDetailsLoader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackDetailsLoader.java index f6c49e891..5b8b8d45e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackDetailsLoader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackDetailsLoader.java @@ -3,5 +3,5 @@ import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; public interface YoutubeTrackDetailsLoader { - YoutubeTrackDetails loadDetails(HttpInterface httpInterface, String videoId, boolean requireFormats, YoutubeAudioSourceManager sourceManager); + YoutubeTrackDetails loadDetails(HttpInterface httpInterface, String videoId, boolean requireFormats, YoutubeAudioSourceManager sourceManager); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackFormat.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackFormat.java index 974ea4603..4d984e1ca 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackFormat.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackFormat.java @@ -9,121 +9,121 @@ * Describes an available media format for a track */ public class YoutubeTrackFormat { - private final YoutubeFormatInfo info; - private final ContentType type; - private final long bitrate; - private final long contentLength; - private final long audioChannels; - private final String url; - private final String nParameter; - private final String signature; - private final String signatureKey; - private final boolean defaultAudioTrack; + private final YoutubeFormatInfo info; + private final ContentType type; + private final long bitrate; + private final long contentLength; + private final long audioChannels; + private final String url; + private final String nParameter; + private final String signature; + private final String signatureKey; + private final boolean defaultAudioTrack; - /** - * @param type Mime type of the format - * @param bitrate Bitrate of the format - * @param contentLength Length in bytes of the media - * @param audioChannels Number of audio channels - * @param url Base URL for the playback of this format - * @param nParameter n parameter for this format - * @param signature Cipher signature for this format - * @param signatureKey The key to use for deciphered signature in the final playback URL - */ - public YoutubeTrackFormat( - ContentType type, - long bitrate, - long contentLength, - long audioChannels, - String url, - String nParameter, - String signature, - String signatureKey, - boolean isDefaultAudioTrack - ) { - this.info = YoutubeFormatInfo.get(type); - this.type = type; - this.bitrate = bitrate; - this.contentLength = contentLength; - this.audioChannels = audioChannels; - this.url = url; - this.nParameter = nParameter; - this.signature = signature; - this.signatureKey = signatureKey; - this.defaultAudioTrack = isDefaultAudioTrack; - } + /** + * @param type Mime type of the format + * @param bitrate Bitrate of the format + * @param contentLength Length in bytes of the media + * @param audioChannels Number of audio channels + * @param url Base URL for the playback of this format + * @param nParameter n parameter for this format + * @param signature Cipher signature for this format + * @param signatureKey The key to use for deciphered signature in the final playback URL + */ + public YoutubeTrackFormat( + ContentType type, + long bitrate, + long contentLength, + long audioChannels, + String url, + String nParameter, + String signature, + String signatureKey, + boolean isDefaultAudioTrack + ) { + this.info = YoutubeFormatInfo.get(type); + this.type = type; + this.bitrate = bitrate; + this.contentLength = contentLength; + this.audioChannels = audioChannels; + this.url = url; + this.nParameter = nParameter; + this.signature = signature; + this.signatureKey = signatureKey; + this.defaultAudioTrack = isDefaultAudioTrack; + } - /** - * @return Format container and codec info - */ - public YoutubeFormatInfo getInfo() { - return info; - } + /** + * @return Format container and codec info + */ + public YoutubeFormatInfo getInfo() { + return info; + } - /** - * @return Mime type of the format - */ - public ContentType getType() { - return type; - } + /** + * @return Mime type of the format + */ + public ContentType getType() { + return type; + } - /** - * @return Bitrate of the format - */ - public long getBitrate() { - return bitrate; - } + /** + * @return Bitrate of the format + */ + public long getBitrate() { + return bitrate; + } - /** - * @return Count of audio channels in format - */ - public long getAudioChannels() { - return audioChannels; - } + /** + * @return Count of audio channels in format + */ + public long getAudioChannels() { + return audioChannels; + } - /** - * @return Base URL for the playback of this format - */ - public URI getUrl() { - try { - return new URI(url); - } catch (URISyntaxException e) { - throw new RuntimeException(e); + /** + * @return Base URL for the playback of this format + */ + public URI getUrl() { + try { + return new URI(url); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } - } - /** - * @return Length in bytes of the media - */ - public long getContentLength() { - return contentLength; - } + /** + * @return Length in bytes of the media + */ + public long getContentLength() { + return contentLength; + } - /** - * @return n parameter for this format - */ - public String getNParameter() { - return nParameter; - } + /** + * @return n parameter for this format + */ + public String getNParameter() { + return nParameter; + } - /** - * @return Cipher signature for this format - */ - public String getSignature() { - return signature; - } + /** + * @return Cipher signature for this format + */ + public String getSignature() { + return signature; + } - /** - * @return The key to use for deciphered signature in the final playback URL - */ - public String getSignatureKey() { - return signatureKey; - } + /** + * @return The key to use for deciphered signature in the final playback URL + */ + public String getSignatureKey() { + return signatureKey; + } - /** - * @return Whether this format contains an audio track that is used by default. - */ - public boolean isDefaultAudioTrack() { - return defaultAudioTrack; - } + /** + * @return Whether this format contains an audio track that is used by default. + */ + public boolean isDefaultAudioTrack() { + return defaultAudioTrack; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackJsonData.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackJsonData.java index cf332fa7e..d245ce5d5 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackJsonData.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/YoutubeTrackJsonData.java @@ -10,79 +10,79 @@ import static com.sedmelluq.discord.lavaplayer.tools.JsonBrowser.NULL_BROWSER; public class YoutubeTrackJsonData { - private static final Logger log = LoggerFactory.getLogger(DefaultYoutubeTrackDetailsLoader.class); - - public final JsonBrowser playerResponse; - public final JsonBrowser polymerArguments; - public final String playerScriptUrl; - - public YoutubeTrackJsonData(JsonBrowser playerResponse, JsonBrowser polymerArguments, String playerScriptUrl) { - this.playerResponse = playerResponse; - this.polymerArguments = polymerArguments; - this.playerScriptUrl = playerScriptUrl; - } - - public YoutubeTrackJsonData withPlayerScriptUrl(String playerScriptUrl) { - return new YoutubeTrackJsonData(playerResponse, polymerArguments, playerScriptUrl); - } - - public static YoutubeTrackJsonData fromMainResult(JsonBrowser result) { - try { - JsonBrowser playerInfo = NULL_BROWSER; - JsonBrowser playerResponse = NULL_BROWSER; - - List json = result.values(); - JsonBrowser lastElement = json.get(result.values().size() - 1); - if (!lastElement.get("page").isNull()) { - for (JsonBrowser child : result.values()) { - if (child.isMap()) { - if (playerInfo.isNull()) { - playerInfo = child.get("player"); + private static final Logger log = LoggerFactory.getLogger(DefaultYoutubeTrackDetailsLoader.class); + + public final JsonBrowser playerResponse; + public final JsonBrowser polymerArguments; + public final String playerScriptUrl; + + public YoutubeTrackJsonData(JsonBrowser playerResponse, JsonBrowser polymerArguments, String playerScriptUrl) { + this.playerResponse = playerResponse; + this.polymerArguments = polymerArguments; + this.playerScriptUrl = playerScriptUrl; + } + + public YoutubeTrackJsonData withPlayerScriptUrl(String playerScriptUrl) { + return new YoutubeTrackJsonData(playerResponse, polymerArguments, playerScriptUrl); + } + + public static YoutubeTrackJsonData fromMainResult(JsonBrowser result) { + try { + JsonBrowser playerInfo = NULL_BROWSER; + JsonBrowser playerResponse = NULL_BROWSER; + + List json = result.values(); + JsonBrowser lastElement = json.get(result.values().size() - 1); + if (!lastElement.get("page").isNull()) { + for (JsonBrowser child : result.values()) { + if (child.isMap()) { + if (playerInfo.isNull()) { + playerInfo = child.get("player"); + } + + if (playerResponse.isNull()) { + playerResponse = child.get("playerResponse"); + } + } + } + } else { + if (playerResponse.isNull()) { + playerResponse = result; + } } - if (playerResponse.isNull()) { - playerResponse = child.get("playerResponse"); + if (!playerInfo.isNull()) { + return fromPolymerPlayerInfo(playerInfo, playerResponse); + } else if (!playerResponse.isNull()) { + return new YoutubeTrackJsonData(playerResponse, NULL_BROWSER, null); } - } + } catch (Exception e) { + throw throwWithDebugInfo(log, e, "Error parsing result", "json", result.format()); } - } else { - if (playerResponse.isNull()) { - playerResponse = result; - } - } - - if (!playerInfo.isNull()) { - return fromPolymerPlayerInfo(playerInfo, playerResponse); - } else if (!playerResponse.isNull()) { - return new YoutubeTrackJsonData(playerResponse, NULL_BROWSER, null); - } - } catch (Exception e) { - throw throwWithDebugInfo(log, e, "Error parsing result", "json", result.format()); + + throw throwWithDebugInfo(log, null, "Neither player nor playerResponse in result", "json", result.format()); } - throw throwWithDebugInfo(log, null, "Neither player nor playerResponse in result", "json", result.format()); - } + private static YoutubeTrackJsonData fromPolymerPlayerInfo(JsonBrowser playerInfo, JsonBrowser playerResponse) { + JsonBrowser args = playerInfo.get("args"); + String playerScriptUrl = playerInfo.get("assets").get("js").text(); - private static YoutubeTrackJsonData fromPolymerPlayerInfo(JsonBrowser playerInfo, JsonBrowser playerResponse) { - JsonBrowser args = playerInfo.get("args"); - String playerScriptUrl = playerInfo.get("assets").get("js").text(); + String playerResponseText = args.get("player_response").text(); - String playerResponseText = args.get("player_response").text(); + if (playerResponseText == null) { + // In case of Polymer, the playerResponse with formats is the one embedded in args, NOT the one in outer JSON. + // However, if no player_response is available, use the outer playerResponse. + return new YoutubeTrackJsonData(playerResponse, args, playerScriptUrl); + } - if (playerResponseText == null) { - // In case of Polymer, the playerResponse with formats is the one embedded in args, NOT the one in outer JSON. - // However, if no player_response is available, use the outer playerResponse. - return new YoutubeTrackJsonData(playerResponse, args, playerScriptUrl); + return new YoutubeTrackJsonData(parsePlayerResponse(playerResponseText), args, playerScriptUrl); } - return new YoutubeTrackJsonData(parsePlayerResponse(playerResponseText), args, playerScriptUrl); - } - - private static JsonBrowser parsePlayerResponse(String playerResponseText) { - try { - return JsonBrowser.parse(playerResponseText); - } catch (Exception e) { - throw throwWithDebugInfo(log, e, "Failed to parse player_response", "value", playerResponseText); + private static JsonBrowser parsePlayerResponse(String playerResponseText) { + try { + return JsonBrowser.parse(playerResponseText); + } catch (Exception e) { + throw throwWithDebugInfo(log, e, "Failed to parse player_response", "value", playerResponseText); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/LegacyAdaptiveFormatsExtractor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/LegacyAdaptiveFormatsExtractor.java index 0137d6c71..417b3d4a8 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/LegacyAdaptiveFormatsExtractor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/LegacyAdaptiveFormatsExtractor.java @@ -2,45 +2,46 @@ import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeTrackFormat; import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeTrackJsonData; +import org.apache.http.entity.ContentType; + import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; -import org.apache.http.entity.ContentType; import static com.sedmelluq.discord.lavaplayer.tools.DataFormatTools.decodeUrlEncodedItems; public class LegacyAdaptiveFormatsExtractor implements OfflineYoutubeTrackFormatExtractor { - @Override - public List extract(YoutubeTrackJsonData data) { - String adaptiveFormats = data.polymerArguments.get("adaptive_fmts").text(); + @Override + public List extract(YoutubeTrackJsonData data) { + String adaptiveFormats = data.polymerArguments.get("adaptive_fmts").text(); - if (adaptiveFormats == null) { - return Collections.emptyList(); - } + if (adaptiveFormats == null) { + return Collections.emptyList(); + } - return loadTrackFormatsFromAdaptive(adaptiveFormats); - } - - private List loadTrackFormatsFromAdaptive(String adaptiveFormats) { - List tracks = new ArrayList<>(); - - for (String formatString : adaptiveFormats.split(",")) { - Map format = decodeUrlEncodedItems(formatString, false); - - tracks.add(new YoutubeTrackFormat( - ContentType.parse(format.get("type")), - Long.parseLong(format.get("bitrate")), - Long.parseLong(format.get("clen")), - 2, - format.get("url"), - "", - format.get("s"), - format.getOrDefault("sp", DEFAULT_SIGNATURE_KEY), - true - )); + return loadTrackFormatsFromAdaptive(adaptiveFormats); } - return tracks; - } + private List loadTrackFormatsFromAdaptive(String adaptiveFormats) { + List tracks = new ArrayList<>(); + + for (String formatString : adaptiveFormats.split(",")) { + Map format = decodeUrlEncodedItems(formatString, false); + + tracks.add(new YoutubeTrackFormat( + ContentType.parse(format.get("type")), + Long.parseLong(format.get("bitrate")), + Long.parseLong(format.get("clen")), + 2, + format.get("url"), + "", + format.get("s"), + format.getOrDefault("sp", DEFAULT_SIGNATURE_KEY), + true + )); + } + + return tracks; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/LegacyDashMpdFormatsExtractor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/LegacyDashMpdFormatsExtractor.java index bf0ab33ee..640f4c284 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/LegacyDashMpdFormatsExtractor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/LegacyDashMpdFormatsExtractor.java @@ -6,10 +6,6 @@ import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.entity.ContentType; @@ -20,75 +16,80 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + public class LegacyDashMpdFormatsExtractor implements YoutubeTrackFormatExtractor { - private static final Logger log = LoggerFactory.getLogger(LegacyDashMpdFormatsExtractor.class); + private static final Logger log = LoggerFactory.getLogger(LegacyDashMpdFormatsExtractor.class); - @Override - public List extract( - YoutubeTrackJsonData data, - HttpInterface httpInterface, - YoutubeSignatureResolver signatureResolver - ) { - String dashUrl = data.polymerArguments.get("dashmpd").text(); + @Override + public List extract( + YoutubeTrackJsonData data, + HttpInterface httpInterface, + YoutubeSignatureResolver signatureResolver + ) { + String dashUrl = data.polymerArguments.get("dashmpd").text(); - if (dashUrl == null) { - return Collections.emptyList(); - } + if (dashUrl == null) { + return Collections.emptyList(); + } - try { - return loadTrackFormatsFromDash(dashUrl, httpInterface, signatureResolver, data.playerScriptUrl); - } catch (Exception e) { - throw new RuntimeException("Failed to extract formats from dash url " + dashUrl, e); + try { + return loadTrackFormatsFromDash(dashUrl, httpInterface, signatureResolver, data.playerScriptUrl); + } catch (Exception e) { + throw new RuntimeException("Failed to extract formats from dash url " + dashUrl, e); + } } - } - private List loadTrackFormatsFromDash( - String dashUrl, - HttpInterface httpInterface, - YoutubeSignatureResolver signatureResolver, - String playerScriptUrl - ) throws Exception { - String resolvedDashUrl = signatureResolver.resolveDashUrl(httpInterface, playerScriptUrl, dashUrl); + private List loadTrackFormatsFromDash( + String dashUrl, + HttpInterface httpInterface, + YoutubeSignatureResolver signatureResolver, + String playerScriptUrl + ) throws Exception { + String resolvedDashUrl = signatureResolver.resolveDashUrl(httpInterface, playerScriptUrl, dashUrl); - try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(resolvedDashUrl))) { - HttpClientTools.assertSuccessWithContent(response, "track info page response"); + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(resolvedDashUrl))) { + HttpClientTools.assertSuccessWithContent(response, "track info page response"); - Document document = Jsoup.parse(response.getEntity().getContent(), StandardCharsets.UTF_8.name(), "", - Parser.xmlParser()); - return loadTrackFormatsFromDashDocument(document); + Document document = Jsoup.parse(response.getEntity().getContent(), StandardCharsets.UTF_8.name(), "", + Parser.xmlParser()); + return loadTrackFormatsFromDashDocument(document); + } } - } - private List loadTrackFormatsFromDashDocument(Document document) { - List tracks = new ArrayList<>(); + private List loadTrackFormatsFromDashDocument(Document document) { + List tracks = new ArrayList<>(); + + for (Element adaptation : document.select("AdaptationSet")) { + String mimeType = adaptation.attr("mimeType"); - for (Element adaptation : document.select("AdaptationSet")) { - String mimeType = adaptation.attr("mimeType"); + for (Element representation : adaptation.select("Representation")) { + String url = representation.select("BaseURL").first().text(); + String contentLength = DataFormatTools.extractBetween(url, "/clen/", "/"); + String contentType = mimeType + "; codecs=" + representation.attr("codecs"); - for (Element representation : adaptation.select("Representation")) { - String url = representation.select("BaseURL").first().text(); - String contentLength = DataFormatTools.extractBetween(url, "/clen/", "/"); - String contentType = mimeType + "; codecs=" + representation.attr("codecs"); + if (contentLength == null) { + log.debug("Skipping format {} because the content length is missing", contentType); + continue; + } - if (contentLength == null) { - log.debug("Skipping format {} because the content length is missing", contentType); - continue; + tracks.add(new YoutubeTrackFormat( + ContentType.parse(contentType), + Long.parseLong(representation.attr("bandwidth")), + Long.parseLong(contentLength), + 2, + url, + "", + null, + DEFAULT_SIGNATURE_KEY, + true + )); + } } - tracks.add(new YoutubeTrackFormat( - ContentType.parse(contentType), - Long.parseLong(representation.attr("bandwidth")), - Long.parseLong(contentLength), - 2, - url, - "", - null, - DEFAULT_SIGNATURE_KEY, - true - )); - } + return tracks; } - - return tracks; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/LegacyStreamMapFormatsExtractor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/LegacyStreamMapFormatsExtractor.java index 4cdf6f785..4a4599520 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/LegacyStreamMapFormatsExtractor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/LegacyStreamMapFormatsExtractor.java @@ -3,85 +3,86 @@ import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeTrackFormat; import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeTrackJsonData; import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; +import org.apache.http.entity.ContentType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; -import org.apache.http.entity.ContentType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import static com.sedmelluq.discord.lavaplayer.tools.DataFormatTools.decodeUrlEncodedItems; public class LegacyStreamMapFormatsExtractor implements OfflineYoutubeTrackFormatExtractor { - private static final Logger log = LoggerFactory.getLogger(LegacyStreamMapFormatsExtractor.class); + private static final Logger log = LoggerFactory.getLogger(LegacyStreamMapFormatsExtractor.class); - @Override - public List extract(YoutubeTrackJsonData data) { - String formatStreamMap = data.polymerArguments.get("url_encoded_fmt_stream_map").text(); + @Override + public List extract(YoutubeTrackJsonData data) { + String formatStreamMap = data.polymerArguments.get("url_encoded_fmt_stream_map").text(); - if (formatStreamMap == null) { - return Collections.emptyList(); + if (formatStreamMap == null) { + return Collections.emptyList(); + } + + return loadTrackFormatsFromFormatStreamMap(formatStreamMap); } - return loadTrackFormatsFromFormatStreamMap(formatStreamMap); - } + private List loadTrackFormatsFromFormatStreamMap(String adaptiveFormats) { + List tracks = new ArrayList<>(); + boolean anyFailures = false; - private List loadTrackFormatsFromFormatStreamMap(String adaptiveFormats) { - List tracks = new ArrayList<>(); - boolean anyFailures = false; + for (String formatString : adaptiveFormats.split(",")) { + try { + Map format = decodeUrlEncodedItems(formatString, false); + String url = format.get("url"); - for (String formatString : adaptiveFormats.split(",")) { - try { - Map format = decodeUrlEncodedItems(formatString, false); - String url = format.get("url"); + if (url == null) { + continue; + } - if (url == null) { - continue; - } + String contentLength = DataFormatTools.extractBetween(url, "clen=", "&"); - String contentLength = DataFormatTools.extractBetween(url, "clen=", "&"); + if (contentLength == null) { + log.debug("Could not find content length from URL {}, skipping format", url); + continue; + } - if (contentLength == null) { - log.debug("Could not find content length from URL {}, skipping format", url); - continue; + tracks.add(new YoutubeTrackFormat( + ContentType.parse(format.get("type")), + qualityToBitrateValue(format.get("quality")), + Long.parseLong(contentLength), + 2, + url, + "", + format.get("s"), + format.getOrDefault("sp", DEFAULT_SIGNATURE_KEY), + true + )); + } catch (RuntimeException e) { + anyFailures = true; + log.debug("Failed to parse format {}, skipping.", formatString, e); + } } - tracks.add(new YoutubeTrackFormat( - ContentType.parse(format.get("type")), - qualityToBitrateValue(format.get("quality")), - Long.parseLong(contentLength), - 2, - url, - "", - format.get("s"), - format.getOrDefault("sp", DEFAULT_SIGNATURE_KEY), - true - )); - } catch (RuntimeException e) { - anyFailures = true; - log.debug("Failed to parse format {}, skipping.", formatString, e); - } - } + if (tracks.isEmpty() && anyFailures) { + log.warn("In adaptive format map {}, all formats either failed to load or were skipped due to missing fields", + adaptiveFormats); + } - if (tracks.isEmpty() && anyFailures) { - log.warn("In adaptive format map {}, all formats either failed to load or were skipped due to missing fields", - adaptiveFormats); + return tracks; } - return tracks; - } - - private long qualityToBitrateValue(String quality) { - // Return negative bitrate values to indicate missing bitrate info, but still retain the relative order. - if ("small".equals(quality)) { - return -10; - } else if ("medium".equals(quality)) { - return -5; - } else if ("hd720".equals(quality)) { - return -4; - } else { - return -1; + private long qualityToBitrateValue(String quality) { + // Return negative bitrate values to indicate missing bitrate info, but still retain the relative order. + if ("small".equals(quality)) { + return -10; + } else if ("medium".equals(quality)) { + return -5; + } else if ("hd720".equals(quality)) { + return -4; + } else { + return -1; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/OfflineYoutubeTrackFormatExtractor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/OfflineYoutubeTrackFormatExtractor.java index 89b5cdc31..9dd11d292 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/OfflineYoutubeTrackFormatExtractor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/OfflineYoutubeTrackFormatExtractor.java @@ -4,17 +4,18 @@ import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeTrackFormat; import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeTrackJsonData; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; + import java.util.List; public interface OfflineYoutubeTrackFormatExtractor extends YoutubeTrackFormatExtractor { - List extract(YoutubeTrackJsonData data); + List extract(YoutubeTrackJsonData data); - @Override - default List extract( - YoutubeTrackJsonData data, - HttpInterface httpInterface, - YoutubeSignatureResolver signatureResolver - ) { - return extract(data); - } + @Override + default List extract( + YoutubeTrackJsonData data, + HttpInterface httpInterface, + YoutubeSignatureResolver signatureResolver + ) { + return extract(data); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/StreamingDataFormatsExtractor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/StreamingDataFormatsExtractor.java index 86a6c3606..f7a7231c0 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/StreamingDataFormatsExtractor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/StreamingDataFormatsExtractor.java @@ -5,93 +5,94 @@ import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.Units; +import org.apache.http.entity.ContentType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; -import org.apache.http.entity.ContentType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import static com.sedmelluq.discord.lavaplayer.tools.DataFormatTools.decodeUrlEncodedItems; import static com.sedmelluq.discord.lavaplayer.tools.Units.CONTENT_LENGTH_UNKNOWN; public class StreamingDataFormatsExtractor implements OfflineYoutubeTrackFormatExtractor { - private static final Logger log = LoggerFactory.getLogger(StreamingDataFormatsExtractor.class); - - @Override - public List extract(YoutubeTrackJsonData data) { - JsonBrowser streamingData = data.playerResponse.get("streamingData"); + private static final Logger log = LoggerFactory.getLogger(StreamingDataFormatsExtractor.class); - if (streamingData.isNull()) { - return Collections.emptyList(); - } - - JsonBrowser playabilityStatus = data.playerResponse.get("playabilityStatus"); - boolean isLive = data.playerResponse.get("videoDetails").get("isLive").asBoolean(false); - if ("OK".equals(playabilityStatus.get("status").text()) && playabilityStatus.get("reason").safeText().contains("This live event has ended")) { - // Long videos after ending of stream don't contain contentLength field because is not yet processed by YouTube - // This reason disappear after video being processed - isLive = true; - } + @Override + public List extract(YoutubeTrackJsonData data) { + JsonBrowser streamingData = data.playerResponse.get("streamingData"); - List formats = loadTrackFormatsFromStreamingData(streamingData.get("formats"), isLive); - formats.addAll(loadTrackFormatsFromStreamingData(streamingData.get("adaptiveFormats"), isLive)); - return formats; - } + if (streamingData.isNull()) { + return Collections.emptyList(); + } - private List loadTrackFormatsFromStreamingData(JsonBrowser formats, boolean isLive) { - List tracks = new ArrayList<>(); - boolean anyFailures = false; + JsonBrowser playabilityStatus = data.playerResponse.get("playabilityStatus"); + boolean isLive = data.playerResponse.get("videoDetails").get("isLive").asBoolean(false); + if ("OK".equals(playabilityStatus.get("status").text()) && playabilityStatus.get("reason").safeText().contains("This live event has ended")) { + // Long videos after ending of stream don't contain contentLength field because is not yet processed by YouTube + // This reason disappear after video being processed + isLive = true; + } - if (!formats.isNull() && formats.isList()) { - for (JsonBrowser formatJson : formats.values()) { - String url = formatJson.get("url").text(); - String cipher = formatJson.get("cipher").text(); + List formats = loadTrackFormatsFromStreamingData(streamingData.get("formats"), isLive); + formats.addAll(loadTrackFormatsFromStreamingData(streamingData.get("adaptiveFormats"), isLive)); + return formats; + } - if (cipher == null) { - cipher = formatJson.get("signatureCipher").text(); + private List loadTrackFormatsFromStreamingData(JsonBrowser formats, boolean isLive) { + List tracks = new ArrayList<>(); + boolean anyFailures = false; + + if (!formats.isNull() && formats.isList()) { + for (JsonBrowser formatJson : formats.values()) { + String url = formatJson.get("url").text(); + String cipher = formatJson.get("cipher").text(); + + if (cipher == null) { + cipher = formatJson.get("signatureCipher").text(); + } + + Map cipherInfo = cipher != null + ? decodeUrlEncodedItems(cipher, true) + : Collections.emptyMap(); + + Map urlMap = DataFormatTools.isNullOrEmpty(url) + ? decodeUrlEncodedItems(cipherInfo.get("url"), false) + : decodeUrlEncodedItems(url, false); + + try { + long contentLength = formatJson.get("contentLength").asLong(CONTENT_LENGTH_UNKNOWN); + + if (contentLength == CONTENT_LENGTH_UNKNOWN && !isLive) { + log.debug("Track not a live stream, but no contentLength in format {}, skipping", formatJson.format()); + continue; + } + + tracks.add(new YoutubeTrackFormat( + ContentType.parse(formatJson.get("mimeType").text()), + formatJson.get("bitrate").asLong(Units.BITRATE_UNKNOWN), + contentLength, + formatJson.get("audioChannels").asLong(2), + cipherInfo.getOrDefault("url", url), + urlMap.get("n"), + cipherInfo.get("s"), + cipherInfo.getOrDefault("sp", DEFAULT_SIGNATURE_KEY), + formatJson.get("audioTrack").get("audioIsDefault").asBoolean(true) + )); + } catch (RuntimeException e) { + anyFailures = true; + log.debug("Failed to parse format {}, skipping", formatJson, e); + } + } } - Map cipherInfo = cipher != null - ? decodeUrlEncodedItems(cipher, true) - : Collections.emptyMap(); - - Map urlMap = DataFormatTools.isNullOrEmpty(url) - ? decodeUrlEncodedItems(cipherInfo.get("url"), false) - : decodeUrlEncodedItems(url, false); - - try { - long contentLength = formatJson.get("contentLength").asLong(CONTENT_LENGTH_UNKNOWN); - - if (contentLength == CONTENT_LENGTH_UNKNOWN && !isLive) { - log.debug("Track not a live stream, but no contentLength in format {}, skipping", formatJson.format()); - continue; - } - - tracks.add(new YoutubeTrackFormat( - ContentType.parse(formatJson.get("mimeType").text()), - formatJson.get("bitrate").asLong(Units.BITRATE_UNKNOWN), - contentLength, - formatJson.get("audioChannels").asLong(2), - cipherInfo.getOrDefault("url", url), - urlMap.get("n"), - cipherInfo.get("s"), - cipherInfo.getOrDefault("sp", DEFAULT_SIGNATURE_KEY), - formatJson.get("audioTrack").get("audioIsDefault").asBoolean(true) - )); - } catch (RuntimeException e) { - anyFailures = true; - log.debug("Failed to parse format {}, skipping", formatJson, e); + if (tracks.isEmpty() && anyFailures) { + log.warn("In streamingData adaptive formats {}, all formats either failed to load or were skipped due to missing " + + "fields", formats.format()); } - } - } - if (tracks.isEmpty() && anyFailures) { - log.warn("In streamingData adaptive formats {}, all formats either failed to load or were skipped due to missing " + - "fields", formats.format()); + return tracks; } - - return tracks; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/YoutubeTrackFormatExtractor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/YoutubeTrackFormatExtractor.java index 37198cf45..93b58b1bc 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/YoutubeTrackFormatExtractor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/source/youtube/format/YoutubeTrackFormatExtractor.java @@ -4,14 +4,15 @@ import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeTrackFormat; import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeTrackJsonData; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; + import java.util.List; public interface YoutubeTrackFormatExtractor { - String DEFAULT_SIGNATURE_KEY = "signature"; + String DEFAULT_SIGNATURE_KEY = "signature"; - List extract( - YoutubeTrackJsonData response, - HttpInterface httpInterface, - YoutubeSignatureResolver signatureResolver - ); + List extract( + YoutubeTrackJsonData response, + HttpInterface httpInterface, + YoutubeSignatureResolver signatureResolver + ); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/CopyOnUpdateIdentityList.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/CopyOnUpdateIdentityList.java index 6fd9205ec..882232394 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/CopyOnUpdateIdentityList.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/CopyOnUpdateIdentityList.java @@ -10,31 +10,31 @@ * intended for. Not thread-safe. */ public class CopyOnUpdateIdentityList { - public List items = Collections.emptyList(); + public List items = Collections.emptyList(); - public void add(T item) { - for (T existingItem : items) { - if (existingItem == item) { - // No duplicates, do not add again. - return; - } + public void add(T item) { + for (T existingItem : items) { + if (existingItem == item) { + // No duplicates, do not add again. + return; + } + } + + List updated = new ArrayList<>(items.size() + 1); + updated.addAll(items); + updated.add(item); + items = updated; } - List updated = new ArrayList<>(items.size() + 1); - updated.addAll(items); - updated.add(item); - items = updated; - } + public void remove(T item) { + List updated = new ArrayList<>(items.size()); - public void remove(T item) { - List updated = new ArrayList<>(items.size()); + for (T existingItem : items) { + if (existingItem != item) { + updated.add(existingItem); + } + } - for (T existingItem : items) { - if (existingItem != item) { - updated.add(existingItem); - } + items = updated; } - - items = updated; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/DataFormatTools.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/DataFormatTools.java index 08538f0a8..cdabfd506 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/DataFormatTools.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/DataFormatTools.java @@ -1,211 +1,213 @@ package com.sedmelluq.discord.lavaplayer.tools; -import java.nio.charset.StandardCharsets; import org.apache.commons.io.IOUtils; import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; import java.util.regex.Pattern; -import org.apache.http.client.utils.URLEncodedUtils; /** * Helper methods related to strings and maps. */ public class DataFormatTools { - private static final Pattern lineSplitPattern = Pattern.compile("[\\r\\n\\s]*\\n[\\r\\n\\s]*"); - - /** - * Extract text between the first subsequent occurrences of start and end in haystack - * @param haystack The text to search from - * @param start The text after which to start extracting - * @param end The text before which to stop extracting - * @return The extracted string - */ - public static String extractBetween(String haystack, String start, String end) { - int startMatch = haystack.indexOf(start); - - if (startMatch >= 0) { - int startPosition = startMatch + start.length(); - int endPosition = haystack.indexOf(end, startPosition); - - if (endPosition >= 0) { - return haystack.substring(startPosition, endPosition); - } + private static final Pattern lineSplitPattern = Pattern.compile("[\\r\\n\\s]*\\n[\\r\\n\\s]*"); + + /** + * Extract text between the first subsequent occurrences of start and end in haystack + * + * @param haystack The text to search from + * @param start The text after which to start extracting + * @param end The text before which to stop extracting + * @return The extracted string + */ + public static String extractBetween(String haystack, String start, String end) { + int startMatch = haystack.indexOf(start); + + if (startMatch >= 0) { + int startPosition = startMatch + start.length(); + int endPosition = haystack.indexOf(end, startPosition); + + if (endPosition >= 0) { + return haystack.substring(startPosition, endPosition); + } + } + + return null; } - return null; - } + public static String extractBetween(String haystack, TextRange[] candidates) { + for (TextRange candidate : candidates) { + String result = extractBetween(haystack, candidate.start, candidate.end); - public static String extractBetween(String haystack, TextRange[] candidates) { - for (TextRange candidate : candidates) { - String result = extractBetween(haystack, candidate.start, candidate.end); + if (result != null) { + return result; + } + } - if (result != null) { - return result; - } + return null; } - return null; - } + public static String extractAfter(String haystack, String start) { + int startMatch = haystack.indexOf(start); - public static String extractAfter(String haystack, String start) { - int startMatch = haystack.indexOf(start); + if (startMatch >= 0) { + return haystack.substring(startMatch + start.length()); + } - if (startMatch >= 0) { - return haystack.substring(startMatch + start.length()); + return null; } - return null; - } + public static String extractAfter(String haystack, String[] candidates) { + for (String candidate : candidates) { + String result = extractAfter(haystack, candidate); - public static String extractAfter(String haystack, String[] candidates) { - for (String candidate : candidates) { - String result = extractAfter(haystack, candidate); + if (result != null) { + return result; + } + } + + return null; + } + + public static boolean isNullOrEmpty(String text) { + return text == null || text.isEmpty(); + } - if (result != null) { - return result; - } + /** + * Converts name value pairs to a map, with the last entry for each name being present. + * + * @param pairs Name value pairs to convert + * @return The resulting map + */ + public static Map convertToMapLayout(Collection pairs) { + Map map = new HashMap<>(); + for (NameValuePair pair : pairs) { + map.put(pair.getName(), pair.getValue()); + } + return map; } - return null; - } - - public static boolean isNullOrEmpty(String text) { - return text == null || text.isEmpty(); - } - - /** - * Converts name value pairs to a map, with the last entry for each name being present. - * @param pairs Name value pairs to convert - * @return The resulting map - */ - public static Map convertToMapLayout(Collection pairs) { - Map map = new HashMap<>(); - for (NameValuePair pair : pairs) { - map.put(pair.getName(), pair.getValue()); + public static Map convertToMapLayout(String response) { + Map map = new HashMap<>(); + StringTokenizer st = new StringTokenizer(response, "\n\r"); + while (st.hasMoreTokens()) { + String[] keyValue = st.nextToken().split("=", 2); + if (keyValue.length >= 2) { + map.put(keyValue[0], keyValue[1]); + } + } + return map; } - return map; - } - - public static Map convertToMapLayout(String response) { - Map map = new HashMap<>(); - StringTokenizer st = new StringTokenizer(response, "\n\r"); - while (st.hasMoreTokens()) { - String[] keyValue = st.nextToken().split("=", 2); - if (keyValue.length >= 2) { - map.put(keyValue[0], keyValue[1]); - } + + public static Map decodeUrlEncodedItems(String input, boolean escapedSeparator) { + if (escapedSeparator) { + input = input.replace("\\\\u0026", "&"); + } + + return convertToMapLayout(URLEncodedUtils.parse(input, StandardCharsets.UTF_8)); } - return map; - } - public static Map decodeUrlEncodedItems(String input, boolean escapedSeparator) { - if (escapedSeparator) { - input = input.replace("\\\\u0026", "&"); + /** + * Returns the specified default value if the value itself is null. + * + * @param value Value to check + * @param defaultValue Default value to return if value is null + * @param The type of the value + * @return Value or default value + */ + public static T defaultOnNull(T value, T defaultValue) { + return value == null ? defaultValue : value; } - return convertToMapLayout(URLEncodedUtils.parse(input, StandardCharsets.UTF_8)); - } - - /** - * Returns the specified default value if the value itself is null. - * - * @param value Value to check - * @param defaultValue Default value to return if value is null - * @param The type of the value - * @return Value or default value - */ - public static T defaultOnNull(T value, T defaultValue) { - return value == null ? defaultValue : value; - } - - /** - * Consumes a stream and returns it as lines. - * - * @param inputStream Input stream to consume. - * @param charset Character set of the stream - * @return Lines from the stream - * @throws IOException On read error - */ - public static String[] streamToLines(InputStream inputStream, Charset charset) throws IOException { - String text = IOUtils.toString(inputStream, charset); - return lineSplitPattern.split(text); - } - - /** - * Converts duration in the format HH:mm:ss (or mm:ss or ss) to milliseconds. Does not support day count. - * - * @param durationText Duration in text format. - * @return Duration in milliseconds. - */ - public static long durationTextToMillis(String durationText) { - int length = 0; - - for (String part : durationText.split("[:.]")) { - length = length * 60 + Integer.parseInt(part); + /** + * Consumes a stream and returns it as lines. + * + * @param inputStream Input stream to consume. + * @param charset Character set of the stream + * @return Lines from the stream + * @throws IOException On read error + */ + public static String[] streamToLines(InputStream inputStream, Charset charset) throws IOException { + String text = IOUtils.toString(inputStream, charset); + return lineSplitPattern.split(text); } - return length * 1000L; - } - - /** - * Writes a string to output with the additional information whether it is null or not. Compatible with - * {@link #readNullableText(DataInput)}. - * - * @param output Output to write to. - * @param text Text to write. - * @throws IOException On write error. - */ - public static void writeNullableText(DataOutput output, String text) throws IOException { - output.writeBoolean(text != null); - - if (text != null) { - output.writeUTF(text); + /** + * Converts duration in the format HH:mm:ss (or mm:ss or ss) to milliseconds. Does not support day count. + * + * @param durationText Duration in text format. + * @return Duration in milliseconds. + */ + public static long durationTextToMillis(String durationText) { + int length = 0; + + for (String part : durationText.split("[:.]")) { + length = length * 60 + Integer.parseInt(part); + } + + return length * 1000L; } - } - - /** - * Reads a string from input which may be null. Compatible with - * {@link #writeNullableText(DataOutput, String)}. - * - * @param input Input to read from. - * @return The string that was read, or null. - * @throws IOException On read error. - */ - public static String readNullableText(DataInput input) throws IOException { - boolean exists = input.readBoolean(); - return exists ? input.readUTF() : null; - } - - public static boolean arrayRangeEquals(byte[] array, int offset, byte[] segment) { - if (array.length < offset + segment.length) { - return false; + + /** + * Writes a string to output with the additional information whether it is null or not. Compatible with + * {@link #readNullableText(DataInput)}. + * + * @param output Output to write to. + * @param text Text to write. + * @throws IOException On write error. + */ + public static void writeNullableText(DataOutput output, String text) throws IOException { + output.writeBoolean(text != null); + + if (text != null) { + output.writeUTF(text); + } } - for (int i = 0; i < segment.length; i++) { - if (segment[i] != array[i + offset]) { - return false; - } + /** + * Reads a string from input which may be null. Compatible with + * {@link #writeNullableText(DataOutput, String)}. + * + * @param input Input to read from. + * @return The string that was read, or null. + * @throws IOException On read error. + */ + public static String readNullableText(DataInput input) throws IOException { + boolean exists = input.readBoolean(); + return exists ? input.readUTF() : null; } - return true; - } + public static boolean arrayRangeEquals(byte[] array, int offset, byte[] segment) { + if (array.length < offset + segment.length) { + return false; + } + + for (int i = 0; i < segment.length; i++) { + if (segment[i] != array[i + offset]) { + return false; + } + } + + return true; + } - public static class TextRange { - public final String start; - public final String end; + public static class TextRange { + public final String start; + public final String end; - public TextRange(String start, String end) { - this.start = start; - this.end = end; + public TextRange(String start, String end) { + this.start = start; + this.end = end; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/DecodedException.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/DecodedException.java index 6cda41fab..5e914c440 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/DecodedException.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/DecodedException.java @@ -15,9 +15,9 @@ public class DecodedException extends Exception { public final String originalMessage; /** - * @param className Original exception class name + * @param className Original exception class name * @param originalMessage Original exception message - * @param cause Cause of this exception + * @param cause Cause of this exception */ public DecodedException(String className, String originalMessage, DecodedException cause) { super(className + ": " + originalMessage, cause, true, true); diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/ExceptionTools.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/ExceptionTools.java index c2cee6362..12e3aba96 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/ExceptionTools.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/ExceptionTools.java @@ -1,7 +1,6 @@ package com.sedmelluq.discord.lavaplayer.tools; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity; -import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,269 +9,274 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.UUID; /** * Contains common helper methods for dealing with exceptions. */ public class ExceptionTools { - private static final Logger log = LoggerFactory.getLogger(ExceptionTools.class); - private static volatile ErrorDebugInfoHandler debugInfoHandler = new DefaultErrorDebugInfoHandler(); - - /** - * Sometimes it is necessary to catch Throwable instances for logging or reporting purposes. However, unless for - * specific known cases, Error instances should not be blocked from propagating, so rethrow them. - * - * @param throwable The Throwable to check, it is rethrown if it is an Error - */ - public static void rethrowErrors(Throwable throwable) { - if (throwable instanceof Error) { - throw (Error) throwable; - } - } - - /** - * If the exception is not a FriendlyException, wrap with a FriendlyException with the given message - * - * @param message Message of the new FriendlyException if needed - * @param severity Severity of the new FriendlyException - * @param throwable The exception to potentially wrap - * @return Original or wrapped exception - */ - public static FriendlyException wrapUnfriendlyExceptions(String message, Severity severity, Throwable throwable) { - if (throwable instanceof FriendlyException) { - return (FriendlyException) throwable; - } else { - return new FriendlyException(message, severity, throwable); - } - } - - /** - * If the exception is not a FriendlyException, wrap with a RuntimeException - * - * @param throwable The exception to potentially wrap - * @return Original or wrapped exception - */ - public static RuntimeException wrapUnfriendlyExceptions(Throwable throwable) { - if (throwable instanceof FriendlyException) { - return (FriendlyException) throwable; - } else { - return new RuntimeException(throwable); + private static final Logger log = LoggerFactory.getLogger(ExceptionTools.class); + private static volatile ErrorDebugInfoHandler debugInfoHandler = new DefaultErrorDebugInfoHandler(); + + /** + * Sometimes it is necessary to catch Throwable instances for logging or reporting purposes. However, unless for + * specific known cases, Error instances should not be blocked from propagating, so rethrow them. + * + * @param throwable The Throwable to check, it is rethrown if it is an Error + */ + public static void rethrowErrors(Throwable throwable) { + if (throwable instanceof Error) { + throw (Error) throwable; + } } - } - public static RuntimeException toRuntimeException(Exception e) { - if (e instanceof RuntimeException) { - return (RuntimeException) e; - } else { - return new RuntimeException(e); + /** + * If the exception is not a FriendlyException, wrap with a FriendlyException with the given message + * + * @param message Message of the new FriendlyException if needed + * @param severity Severity of the new FriendlyException + * @param throwable The exception to potentially wrap + * @return Original or wrapped exception + */ + public static FriendlyException wrapUnfriendlyExceptions(String message, Severity severity, Throwable throwable) { + if (throwable instanceof FriendlyException) { + return (FriendlyException) throwable; + } else { + return new FriendlyException(message, severity, throwable); + } } - } - - /** - * Finds the first exception which is an instance of the specified class from the throwable cause chain. - * - * @param throwable Throwable to scan. - * @param klass The throwable class to scan for. - * @param The throwable class to scan for. - * @return The first exception in the cause chain (including itself) which is an instance of the specified class. - */ - public static T findDeepException(Throwable throwable, Class klass) { - while (throwable != null) { - if (klass.isAssignableFrom(throwable.getClass())) { - return (T) throwable; - } - - throwable = throwable.getCause(); - } - - return null; - } - /** - * Makes sure thread is set to interrupted state when the throwable is an InterruptedException - * @param throwable Throwable to check - */ - public static void keepInterrupted(Throwable throwable) { - if (throwable instanceof InterruptedException) { - Thread.currentThread().interrupt(); + /** + * If the exception is not a FriendlyException, wrap with a RuntimeException + * + * @param throwable The exception to potentially wrap + * @return Original or wrapped exception + */ + public static RuntimeException wrapUnfriendlyExceptions(Throwable throwable) { + if (throwable instanceof FriendlyException) { + return (FriendlyException) throwable; + } else { + return new RuntimeException(throwable); + } } - } - - /** - * Log a FriendlyException appropriately according to its severity. - * @param log Logger instance to log it to - * @param exception The exception itself - * @param context An object that is included in the log - */ - public static void log(Logger log, FriendlyException exception, Object context) { - switch (exception.severity) { - case COMMON: - log.debug("Common failure for {}: {}", context, exception.getMessage()); - break; - case SUSPICIOUS: - log.warn("Suspicious exception for {}", context, exception); - break; - case FAULT: - default: - log.error("Error in {}", context, exception); - break; + + public static RuntimeException toRuntimeException(Exception e) { + if (e instanceof RuntimeException) { + return (RuntimeException) e; + } else { + return new RuntimeException(e); + } } - } - - public static void setDebugInfoHandler(ErrorDebugInfoHandler debugInfoHandler) { - ExceptionTools.debugInfoHandler = debugInfoHandler; - } - - public static RuntimeException throwWithDebugInfo( - Logger log, - Throwable cause, - String message, - String name, - String value - ) { - ErrorDebugInfo debugInfo = new ErrorDebugInfo(log, UUID.randomUUID().toString(), cause, message, name, value); - debugInfoHandler.handle(debugInfo); - return new RuntimeException(message + " EID: " + debugInfo.errorId + ", " + name + "", cause); - } - - /** - * Encode an exception to an output stream - * @param output Data output - * @param exception Exception to encode - * @throws IOException On IO error - */ - public static void encodeException(DataOutput output, FriendlyException exception) throws IOException { - List causes = new ArrayList<>(); - Throwable next = exception.getCause(); - - while (next != null) { - causes.add(next); - next = next.getCause(); + + /** + * Finds the first exception which is an instance of the specified class from the throwable cause chain. + * + * @param throwable Throwable to scan. + * @param klass The throwable class to scan for. + * @param The throwable class to scan for. + * @return The first exception in the cause chain (including itself) which is an instance of the specified class. + */ + public static T findDeepException(Throwable throwable, Class klass) { + while (throwable != null) { + if (klass.isAssignableFrom(throwable.getClass())) { + return (T) throwable; + } + + throwable = throwable.getCause(); + } + + return null; } - for (int i = causes.size() - 1; i >= 0; i--) { - Throwable cause = causes.get(i); - output.writeBoolean(true); + /** + * Makes sure thread is set to interrupted state when the throwable is an InterruptedException + * + * @param throwable Throwable to check + */ + public static void keepInterrupted(Throwable throwable) { + if (throwable instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + } - String message; + /** + * Log a FriendlyException appropriately according to its severity. + * + * @param log Logger instance to log it to + * @param exception The exception itself + * @param context An object that is included in the log + */ + public static void log(Logger log, FriendlyException exception, Object context) { + switch (exception.severity) { + case COMMON: + log.debug("Common failure for {}: {}", context, exception.getMessage()); + break; + case SUSPICIOUS: + log.warn("Suspicious exception for {}", context, exception); + break; + case FAULT: + default: + log.error("Error in {}", context, exception); + break; + } + } - if (cause instanceof DecodedException) { - output.writeUTF(((DecodedException) cause).className); - message = ((DecodedException) cause).originalMessage; - } else { - output.writeUTF(cause.getClass().getName()); - message = cause.getMessage(); - } + public static void setDebugInfoHandler(ErrorDebugInfoHandler debugInfoHandler) { + ExceptionTools.debugInfoHandler = debugInfoHandler; + } - output.writeBoolean(message != null); - if (message != null) { - output.writeUTF(message); - } + public static RuntimeException throwWithDebugInfo( + Logger log, + Throwable cause, + String message, + String name, + String value + ) { + ErrorDebugInfo debugInfo = new ErrorDebugInfo(log, UUID.randomUUID().toString(), cause, message, name, value); + debugInfoHandler.handle(debugInfo); + return new RuntimeException(message + " EID: " + debugInfo.errorId + ", " + name + "", cause); + } - encodeStackTrace(output, cause); + /** + * Encode an exception to an output stream + * + * @param output Data output + * @param exception Exception to encode + * @throws IOException On IO error + */ + public static void encodeException(DataOutput output, FriendlyException exception) throws IOException { + List causes = new ArrayList<>(); + Throwable next = exception.getCause(); + + while (next != null) { + causes.add(next); + next = next.getCause(); + } + + for (int i = causes.size() - 1; i >= 0; i--) { + Throwable cause = causes.get(i); + output.writeBoolean(true); + + String message; + + if (cause instanceof DecodedException) { + output.writeUTF(((DecodedException) cause).className); + message = ((DecodedException) cause).originalMessage; + } else { + output.writeUTF(cause.getClass().getName()); + message = cause.getMessage(); + } + + output.writeBoolean(message != null); + if (message != null) { + output.writeUTF(message); + } + + encodeStackTrace(output, cause); + } + + output.writeBoolean(false); + output.writeUTF(exception.getMessage()); + output.writeInt(exception.severity.ordinal()); + + encodeStackTrace(output, exception); } - output.writeBoolean(false); - output.writeUTF(exception.getMessage()); - output.writeInt(exception.severity.ordinal()); - - encodeStackTrace(output, exception); - } - - /** - * Closes the specified closeable object. In case that throws an error, logs the error with WARN level, but does not - * rethrow. - * - * @param closeable Object to close. - */ - public static void closeWithWarnings(AutoCloseable closeable) { - try { - closeable.close(); - } catch (Exception e) { - log.warn("Failed to close.", e); + /** + * Closes the specified closeable object. In case that throws an error, logs the error with WARN level, but does not + * rethrow. + * + * @param closeable Object to close. + */ + public static void closeWithWarnings(AutoCloseable closeable) { + try { + closeable.close(); + } catch (Exception e) { + log.warn("Failed to close.", e); + } } - } - - private static void encodeStackTrace(DataOutput output, Throwable throwable) throws IOException { - StackTraceElement[] trace = throwable.getStackTrace(); - output.writeInt(trace.length); - - for (StackTraceElement element : trace) { - output.writeUTF(element.getClassName()); - output.writeUTF(element.getMethodName()); - - String fileName = element.getFileName(); - output.writeBoolean(fileName != null); - if (fileName != null) { - output.writeUTF(fileName); - } - output.writeInt(element.getLineNumber()); + + private static void encodeStackTrace(DataOutput output, Throwable throwable) throws IOException { + StackTraceElement[] trace = throwable.getStackTrace(); + output.writeInt(trace.length); + + for (StackTraceElement element : trace) { + output.writeUTF(element.getClassName()); + output.writeUTF(element.getMethodName()); + + String fileName = element.getFileName(); + output.writeBoolean(fileName != null); + if (fileName != null) { + output.writeUTF(fileName); + } + output.writeInt(element.getLineNumber()); + } } - } - - /** - * Decode an exception from an input stream - * @param input Data input - * @return Decoded exception - * @throws IOException On IO error - */ - public static FriendlyException decodeException(DataInput input) throws IOException { - DecodedException cause = null; - - while (input.readBoolean()) { - cause = new DecodedException(input.readUTF(), input.readBoolean() ? input.readUTF() : null, cause); - cause.setStackTrace(decodeStackTrace(input)); + + /** + * Decode an exception from an input stream + * + * @param input Data input + * @return Decoded exception + * @throws IOException On IO error + */ + public static FriendlyException decodeException(DataInput input) throws IOException { + DecodedException cause = null; + + while (input.readBoolean()) { + cause = new DecodedException(input.readUTF(), input.readBoolean() ? input.readUTF() : null, cause); + cause.setStackTrace(decodeStackTrace(input)); + } + + FriendlyException exception = new FriendlyException(input.readUTF(), Severity.class.getEnumConstants()[input.readInt()], cause); + exception.setStackTrace(decodeStackTrace(input)); + return exception; } - FriendlyException exception = new FriendlyException(input.readUTF(), Severity.class.getEnumConstants()[input.readInt()], cause); - exception.setStackTrace(decodeStackTrace(input)); - return exception; - } + private static StackTraceElement[] decodeStackTrace(DataInput input) throws IOException { + StackTraceElement[] trace = new StackTraceElement[input.readInt()]; - private static StackTraceElement[] decodeStackTrace(DataInput input) throws IOException { - StackTraceElement[] trace = new StackTraceElement[input.readInt()]; + for (int i = 0; i < trace.length; i++) { + trace[i] = new StackTraceElement(input.readUTF(), input.readUTF(), input.readBoolean() ? input.readUTF() : null, input.readInt()); + } - for (int i = 0; i < trace.length; i++) { - trace[i] = new StackTraceElement(input.readUTF(), input.readUTF(), input.readBoolean() ? input.readUTF() : null, input.readInt()); + return trace; } - return trace; - } - - public static class ErrorDebugInfo { - public final Logger log; - public final String errorId; - public final Throwable cause; - public final String message; - public final String name; - public final String value; - - public ErrorDebugInfo( - Logger log, - String errorId, - Throwable cause, - String message, - String name, - String value - ) { - this.log = log; - this.errorId = errorId; - this.cause = cause; - this.message = message; - this.name = name; - this.value = value; + public static class ErrorDebugInfo { + public final Logger log; + public final String errorId; + public final Throwable cause; + public final String message; + public final String name; + public final String value; + + public ErrorDebugInfo( + Logger log, + String errorId, + Throwable cause, + String message, + String name, + String value + ) { + this.log = log; + this.errorId = errorId; + this.cause = cause; + this.message = message; + this.name = name; + this.value = value; + } } - } - public interface ErrorDebugInfoHandler { - void handle(ErrorDebugInfo payload); - } + public interface ErrorDebugInfoHandler { + void handle(ErrorDebugInfo payload); + } - public static class DefaultErrorDebugInfoHandler implements ErrorDebugInfoHandler { + public static class DefaultErrorDebugInfoHandler implements ErrorDebugInfoHandler { - @Override - public void handle(ErrorDebugInfo debugInfo) { - log.warn("{} EID: {}, {}: {}", debugInfo.message, debugInfo.errorId, debugInfo.name, debugInfo.value); + @Override + public void handle(ErrorDebugInfo debugInfo) { + log.warn("{} EID: {}, {}: {}", debugInfo.message, debugInfo.errorId, debugInfo.name, debugInfo.value); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/FriendlyException.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/FriendlyException.java index 89331a837..bf489bab8 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/FriendlyException.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/FriendlyException.java @@ -4,40 +4,40 @@ * An exception with a friendly message. */ public class FriendlyException extends RuntimeException { - /** - * Severity of the exception - */ - public final Severity severity; - - /** - * @param friendlyMessage A message which is understandable to end-users - * @param severity Severity of the exception - * @param cause The cause of the exception with technical details - */ - public FriendlyException(String friendlyMessage, Severity severity, Throwable cause) { - super(friendlyMessage, cause); - - this.severity = severity; - } - - /** - * Severity levels for FriendlyException - */ - public enum Severity { /** - * The cause is known and expected, indicates that there is nothing wrong with the library itself. + * Severity of the exception */ - COMMON, + public final Severity severity; + /** - * The cause might not be exactly known, but is possibly caused by outside factors. For example when an outside - * service responds in a format that we do not expect. + * @param friendlyMessage A message which is understandable to end-users + * @param severity Severity of the exception + * @param cause The cause of the exception with technical details */ - SUSPICIOUS, + public FriendlyException(String friendlyMessage, Severity severity, Throwable cause) { + super(friendlyMessage, cause); + + this.severity = severity; + } + /** - * If the probable cause is an issue with the library or when there is no way to tell what the cause might be. - * This is the default level and other levels are used in cases where the thrower has more in-depth knowledge - * about the error. + * Severity levels for FriendlyException */ - FAULT - } + public enum Severity { + /** + * The cause is known and expected, indicates that there is nothing wrong with the library itself. + */ + COMMON, + /** + * The cause might not be exactly known, but is possibly caused by outside factors. For example when an outside + * service responds in a format that we do not expect. + */ + SUSPICIOUS, + /** + * If the probable cause is an issue with the library or when there is no way to tell what the cause might be. + * This is the default level and other levels are used in cases where the thrower has more in-depth knowledge + * about the error. + */ + FAULT + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/FutureTools.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/FutureTools.java index 051d66009..88f470f7f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/FutureTools.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/FutureTools.java @@ -12,41 +12,41 @@ import java.util.stream.Collectors; public class FutureTools { - private static final Logger log = LoggerFactory.getLogger(FutureTools.class); + private static final Logger log = LoggerFactory.getLogger(FutureTools.class); - @SuppressWarnings("unchecked") - public static List awaitList(CompletionService completionService, List> futures) { + @SuppressWarnings("unchecked") + public static List awaitList(CompletionService completionService, List> futures) { - int received = 0; - boolean failed = false; - while (received < futures.size() && !failed) { - try { - completionService.take(); - received++; - } catch (InterruptedException e) { - log.debug("Received an interruption while taking item ", e); - failed = true; - } catch (Exception e) { - log.debug("Some error occurred while getting futures", e); - failed = true; - } - } - - try { - return (List) futures.stream() - .filter(Future::isDone) - .map(e -> { + int received = 0; + boolean failed = false; + while (received < futures.size() && !failed) { try { - return e.get(); - } catch (Exception ex) { - return null; + completionService.take(); + received++; + } catch (InterruptedException e) { + log.debug("Received an interruption while taking item ", e); + failed = true; + } catch (Exception e) { + log.debug("Some error occurred while getting futures", e); + failed = true; } - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } catch (Exception e) { - log.debug("Some error occurred while getting futures", e); - return Collections.emptyList(); + } + + try { + return (List) futures.stream() + .filter(Future::isDone) + .map(e -> { + try { + return e.get(); + } catch (Exception ex) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } catch (Exception e) { + log.debug("Some error occurred while getting futures", e); + return Collections.emptyList(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/GarbageCollectionMonitor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/GarbageCollectionMonitor.java index 3848b1e61..42d1ca40e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/GarbageCollectionMonitor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/GarbageCollectionMonitor.java @@ -26,117 +26,118 @@ * considered bad for latency, the statistics are logged at a warning level. */ public class GarbageCollectionMonitor implements NotificationListener, Runnable { - private static final Logger log = LoggerFactory.getLogger(GarbageCollectionMonitor.class); - - private static final long REPORTING_FREQUENCY = TimeUnit.MINUTES.toMillis(2); - private static final long[] BUCKETS = new long[] { 2000, 500, 200, 50, 20, 0 }; - - private final ScheduledExecutorService reportingExecutor; - private final int[] bucketCounters; - private final AtomicBoolean enabled; - private final AtomicReference> executorFuture; - - /** - * Create an instance of GC monitor. Does nothing until enabled. - * @param reportingExecutor Executor to use for scheduling reporting task - */ - public GarbageCollectionMonitor(ScheduledExecutorService reportingExecutor) { - this.reportingExecutor = reportingExecutor; - bucketCounters = new int[BUCKETS.length]; - enabled = new AtomicBoolean(); - executorFuture = new AtomicReference<>(); - } - - /** - * Enable GC monitoring and reporting. - */ - public void enable() { - if (enabled.compareAndSet(false, true)) { - registerBeanListener(); - - executorFuture.set(reportingExecutor.scheduleAtFixedRate(this, REPORTING_FREQUENCY, REPORTING_FREQUENCY, TimeUnit.MILLISECONDS)); - - log.info("GC monitoring enabled, reporting results every 2 minutes."); + private static final Logger log = LoggerFactory.getLogger(GarbageCollectionMonitor.class); + + private static final long REPORTING_FREQUENCY = TimeUnit.MINUTES.toMillis(2); + private static final long[] BUCKETS = new long[]{2000, 500, 200, 50, 20, 0}; + + private final ScheduledExecutorService reportingExecutor; + private final int[] bucketCounters; + private final AtomicBoolean enabled; + private final AtomicReference> executorFuture; + + /** + * Create an instance of GC monitor. Does nothing until enabled. + * + * @param reportingExecutor Executor to use for scheduling reporting task + */ + public GarbageCollectionMonitor(ScheduledExecutorService reportingExecutor) { + this.reportingExecutor = reportingExecutor; + bucketCounters = new int[BUCKETS.length]; + enabled = new AtomicBoolean(); + executorFuture = new AtomicReference<>(); } - } - /** - * Disable GC monitoring and reporting. - */ - public void disable() { - if (enabled.compareAndSet(true, false)) { - unregisterBeanListener(); + /** + * Enable GC monitoring and reporting. + */ + public void enable() { + if (enabled.compareAndSet(false, true)) { + registerBeanListener(); - ScheduledFuture scheduledTask = executorFuture.getAndSet(null); - if (scheduledTask != null) { - scheduledTask.cancel(false); - } + executorFuture.set(reportingExecutor.scheduleAtFixedRate(this, REPORTING_FREQUENCY, REPORTING_FREQUENCY, TimeUnit.MILLISECONDS)); - log.info("GC monitoring disabled."); + log.info("GC monitoring enabled, reporting results every 2 minutes."); + } } - } - private void registerBeanListener() { - for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) { - if (gcBean instanceof NotificationEmitter) { - ((NotificationEmitter) gcBean).addNotificationListener(this, null, gcBean); - } + /** + * Disable GC monitoring and reporting. + */ + public void disable() { + if (enabled.compareAndSet(true, false)) { + unregisterBeanListener(); + + ScheduledFuture scheduledTask = executorFuture.getAndSet(null); + if (scheduledTask != null) { + scheduledTask.cancel(false); + } + + log.info("GC monitoring disabled."); + } } - } - - private void unregisterBeanListener() { - for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) { - if (gcBean instanceof NotificationEmitter) { - try { - ((NotificationEmitter) gcBean).removeNotificationListener(this); - } catch (ListenerNotFoundException e) { - log.debug("No listener found on bean {}, should have been there.", gcBean, e); + + private void registerBeanListener() { + for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) { + if (gcBean instanceof NotificationEmitter) { + ((NotificationEmitter) gcBean).addNotificationListener(this, null, gcBean); + } } - } } - } - - private void registerPause(long duration) { - synchronized (bucketCounters) { - for (int i = 0; i < bucketCounters.length; i++) { - if (duration >= BUCKETS[i]) { - bucketCounters[i]++; - break; + + private void unregisterBeanListener() { + for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) { + if (gcBean instanceof NotificationEmitter) { + try { + ((NotificationEmitter) gcBean).removeNotificationListener(this); + } catch (ListenerNotFoundException e) { + log.debug("No listener found on bean {}, should have been there.", gcBean, e); + } + } } - } } - } - @Override - public void handleNotification(Notification notification, Object handback) { - if (GARBAGE_COLLECTION_NOTIFICATION.equals(notification.getType())) { - GarbageCollectionNotificationInfo notificationInfo = from((CompositeData) notification.getUserData()); - GcInfo info = notificationInfo.getGcInfo(); + private void registerPause(long duration) { + synchronized (bucketCounters) { + for (int i = 0; i < bucketCounters.length; i++) { + if (duration >= BUCKETS[i]) { + bucketCounters[i]++; + break; + } + } + } + } + + @Override + public void handleNotification(Notification notification, Object handback) { + if (GARBAGE_COLLECTION_NOTIFICATION.equals(notification.getType())) { + GarbageCollectionNotificationInfo notificationInfo = from((CompositeData) notification.getUserData()); + GcInfo info = notificationInfo.getGcInfo(); - if (info != null && !"No GC".equals(notificationInfo.getGcCause())) { - registerPause(info.getDuration()); - } + if (info != null && !"No GC".equals(notificationInfo.getGcCause())) { + registerPause(info.getDuration()); + } + } } - } - @Override - public void run() { - StringBuilder statistics = new StringBuilder(); - boolean hasBadLatency; + @Override + public void run() { + StringBuilder statistics = new StringBuilder(); + boolean hasBadLatency; - synchronized (bucketCounters) { - hasBadLatency = (bucketCounters[0] + bucketCounters[1] + bucketCounters[2]) > 0; + synchronized (bucketCounters) { + hasBadLatency = (bucketCounters[0] + bucketCounters[1] + bucketCounters[2]) > 0; - for (int i = bucketCounters.length - 1; i >= 0; i--) { - statistics.append(String.format("[Bucket %d = %d] ", BUCKETS[i], bucketCounters[i])); - bucketCounters[i] = 0; - } - } + for (int i = bucketCounters.length - 1; i >= 0; i--) { + statistics.append(String.format("[Bucket %d = %d] ", BUCKETS[i], bucketCounters[i])); + bucketCounters[i] = 0; + } + } - if (hasBadLatency) { - log.warn("Suspicious GC results for the last 2 minutes: {}", statistics); - } else { - log.debug("GC results for the last 2 minutes: {}", statistics); + if (hasBadLatency) { + log.warn("Suspicious GC results for the last 2 minutes: {}", statistics); + } else { + log.debug("GC results for the last 2 minutes: {}", statistics); + } } - } } 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 4fb937234..072416032 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 @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; + import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -16,277 +17,288 @@ * Allows to easily navigate in decoded JSON data */ public class JsonBrowser { - public static final JsonBrowser NULL_BROWSER = new JsonBrowser(null); - - private static final ObjectMapper mapper = setupMapper(); - - private final JsonNode node; - - private JsonBrowser(JsonNode node) { - this.node = node; - } - - /** - * @return True if the value represents a list. - */ - public boolean isList() { - return node instanceof ArrayNode; - } - - /** - * @return True if the value represents a map. - */ - public boolean isMap() { - return node instanceof ObjectNode; - } - - /** - * Get an element at an index for a list value - * @param index List index - * @return JsonBrowser instance which wraps the value at the specified index - */ - public JsonBrowser index(int index) { - if (isList() && index >= 0 && index < node.size()) { - return create(node.get(index)); - } else { - return NULL_BROWSER; + public static final JsonBrowser NULL_BROWSER = new JsonBrowser(null); + + private static final ObjectMapper mapper = setupMapper(); + + private final JsonNode node; + + private JsonBrowser(JsonNode node) { + this.node = node; } - } - - /** - * Get an element by key from a map value - * @param key Map key - * @return JsonBrowser instance which wraps the value with the specified key - */ - public JsonBrowser get(String key) { - if (isMap()) { - return create(node.get(key)); - } else { - return NULL_BROWSER; + + /** + * @return True if the value represents a list. + */ + public boolean isList() { + return node instanceof ArrayNode; } - } - - /** - * Put a value into the map if this instance contains a map. - * @param key The map entry key - * @param item The map entry value - */ - public void put(String key, Object item) { - if (node instanceof ObjectNode) { - if (item instanceof JsonBrowser) { - ((ObjectNode) node).set(key, ((JsonBrowser) item).node); - } else { - ((ObjectNode) node).set(key, mapper.valueToTree(item)); - } - } else { - throw new IllegalStateException("Put only works on a map"); + + /** + * @return True if the value represents a map. + */ + public boolean isMap() { + return node instanceof ObjectNode; } - } - - /** - * Remove a value from the map if this instance contains a map. - * @param key The map entry key - */ - public void remove(String key) { - if (node instanceof ObjectNode) { - ((ObjectNode) node).remove(key); - } else { - throw new IllegalStateException("Remove only works on a map"); + + /** + * Get an element at an index for a list value + * + * @param index List index + * @return JsonBrowser instance which wraps the value at the specified index + */ + public JsonBrowser index(int index) { + if (isList() && index >= 0 && index < node.size()) { + return create(node.get(index)); + } else { + return NULL_BROWSER; + } } - } - - /** - * Add a value to the list if this instance contains a list. - * @param item The list entry value - */ - public void add(Object item) { - if (node instanceof ArrayNode) { - if (item instanceof JsonBrowser) { - ((ArrayNode) node).add(((JsonBrowser) item).node); - } else { - ((ArrayNode) node).add(mapper.valueToTree(item)); - } - } else { - throw new IllegalStateException("Add only works on a list"); + + /** + * Get an element by key from a map value + * + * @param key Map key + * @return JsonBrowser instance which wraps the value with the specified key + */ + public JsonBrowser get(String key) { + if (isMap()) { + return create(node.get(key)); + } else { + return NULL_BROWSER; + } } - } - - /** - * Set a value in the list if this instance contains a list. - * @param index The list index - * @param item The list entry value - */ - public void set(int index, Object item) { - if (node instanceof ArrayNode) { - if (item instanceof JsonBrowser) { - ((ArrayNode) node).insert(index, ((JsonBrowser) item).node); - } else { - ((ArrayNode) node).insert(index, mapper.valueToTree(item)); - } - } else { - throw new IllegalStateException("Add only works on a list"); - } - } - - /** - * Remove a value from the list if this instance contains a list. - * @param index The list index - */ - public void remove(int index) { - if (node instanceof ArrayNode) { - ((ArrayNode) node).remove(index); - } else { - throw new IllegalStateException("Remove only works on a list"); + + /** + * Put a value into the map if this instance contains a map. + * + * @param key The map entry key + * @param item The map entry value + */ + public void put(String key, Object item) { + if (node instanceof ObjectNode) { + if (item instanceof JsonBrowser) { + ((ObjectNode) node).set(key, ((JsonBrowser) item).node); + } else { + ((ObjectNode) node).set(key, mapper.valueToTree(item)); + } + } else { + throw new IllegalStateException("Put only works on a map"); + } } - } - /** - * Returns a list of all the values in this element - * @return The list of values as JsonBrowser elements - */ - public List values() { - List values = new ArrayList<>(); + /** + * Remove a value from the map if this instance contains a map. + * + * @param key The map entry key + */ + public void remove(String key) { + if (node instanceof ObjectNode) { + ((ObjectNode) node).remove(key); + } else { + throw new IllegalStateException("Remove only works on a map"); + } + } + + /** + * Add a value to the list if this instance contains a list. + * + * @param item The list entry value + */ + public void add(Object item) { + if (node instanceof ArrayNode) { + if (item instanceof JsonBrowser) { + ((ArrayNode) node).add(((JsonBrowser) item).node); + } else { + ((ArrayNode) node).add(mapper.valueToTree(item)); + } + } else { + throw new IllegalStateException("Add only works on a list"); + } + } - if (node != null) { - node.elements().forEachRemaining(child -> values.add(new JsonBrowser(child))); + /** + * Set a value in the list if this instance contains a list. + * + * @param index The list index + * @param item The list entry value + */ + public void set(int index, Object item) { + if (node instanceof ArrayNode) { + if (item instanceof JsonBrowser) { + ((ArrayNode) node).insert(index, ((JsonBrowser) item).node); + } else { + ((ArrayNode) node).insert(index, mapper.valueToTree(item)); + } + } else { + throw new IllegalStateException("Add only works on a list"); + } } - return values; - } - - /** - * Attempt to retrieve the value in the specified format - * @param klass The class to retrieve the value as - * @return The value as an instance of the specified class - * @throws IllegalArgumentException If conversion is impossible - */ - public T as(Class klass) { - try { - return mapper.treeToValue(node, klass); - } catch (Exception e) { - throw new RuntimeException(e); + /** + * Remove a value from the list if this instance contains a list. + * + * @param index The list index + */ + public void remove(int index) { + if (node instanceof ArrayNode) { + ((ArrayNode) node).remove(index); + } else { + throw new IllegalStateException("Remove only works on a list"); + } } - } - public T as(TypeReference type) { - try { - return mapper.readValue(mapper.treeAsTokens(node), type); - } catch (Exception e) { - throw new RuntimeException(e); + /** + * Returns a list of all the values in this element + * + * @return The list of values as JsonBrowser elements + */ + public List values() { + List values = new ArrayList<>(); + + if (node != null) { + node.elements().forEachRemaining(child -> values.add(new JsonBrowser(child))); + } + + return values; } - } - - /** - * @return The value of the element as text - */ - public String text() { - if (node != null) { - if (node.isNull()) { + + /** + * Attempt to retrieve the value in the specified format + * + * @param klass The class to retrieve the value as + * @return The value as an instance of the specified class + * @throws IllegalArgumentException If conversion is impossible + */ + public T as(Class klass) { + try { + return mapper.treeToValue(node, klass); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public T as(TypeReference type) { + try { + return mapper.readValue(mapper.treeAsTokens(node), type); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * @return The value of the element as text + */ + public String text() { + if (node != null) { + if (node.isNull()) { + return null; + } else if (node.isTextual()) { + return node.textValue(); + } else if (node.isIntegralNumber()) { + return String.valueOf(node.longValue()); + } else if (node.isNumber()) { + return node.numberValue().toString(); + } else if (node.isBoolean()) { + return String.valueOf(node.booleanValue()); + } else { + return node.toString(); + } + } + return null; - } else if (node.isTextual()) { - return node.textValue(); - } else if (node.isIntegralNumber()) { - return String.valueOf(node.longValue()); - } else if (node.isNumber()) { - return node.numberValue().toString(); - } else if (node.isBoolean()) { - return String.valueOf(node.booleanValue()); - } else { - return node.toString(); - } } - return null; - } - - public boolean asBoolean(boolean defaultValue) { - if (node != null) { - if (node.isBoolean()) { - return node.booleanValue(); - } else if (node.isTextual()) { - if ("true".equals(node.textValue())) { - return true; - } else if ("false".equals(node.textValue())) { - return false; + public boolean asBoolean(boolean defaultValue) { + if (node != null) { + if (node.isBoolean()) { + return node.booleanValue(); + } else if (node.isTextual()) { + if ("true".equals(node.textValue())) { + return true; + } else if ("false".equals(node.textValue())) { + return false; + } + } + } + + return defaultValue; + } + + public long asLong(long defaultValue) { + if (node != null) { + if (node.isNumber()) { + return node.numberValue().longValue(); + } else if (node.isTextual()) { + try { + return Long.parseLong(node.textValue()); + } catch (NumberFormatException ignored) { + // Fall through to default value. + } + } } - } + + return defaultValue; } - return defaultValue; - } + public String safeText() { + String text = text(); + return text != null ? text : ""; + } - public long asLong(long defaultValue) { - if (node != null) { - if (node.isNumber()) { - return node.numberValue().longValue(); - } else if (node.isTextual()) { + public String format() { try { - return Long.parseLong(node.textValue()); - } catch (NumberFormatException ignored) { - // Fall through to default value. + return node != null ? mapper.writeValueAsString(node) : null; + } catch (Exception e) { + return null; } - } } - return defaultValue; - } + /** + * @return The value of the element as text + */ + public boolean isNull() { + return node == null || node.isNull(); + } + + /** + * Parse from string. + * + * @param json The JSON object as a string + * @return JsonBrowser instance for navigating in the result + * @throws IOException When parsing the JSON failed + */ + public static JsonBrowser parse(String json) throws IOException { + return create(mapper.readTree(json)); + } + + /** + * Parse from string. + * + * @param stream The JSON object as a stream + * @return JsonBrowser instance for navigating in the result + * @throws IOException When parsing the JSON failed + */ + public static JsonBrowser parse(InputStream stream) throws IOException { + return create(mapper.readTree(stream)); + } + + public static JsonBrowser newMap() throws IOException { + return create(mapper.createObjectNode()); + } - public String safeText() { - String text = text(); - return text != null ? text : ""; - } + public static JsonBrowser newList() throws IOException { + return create(mapper.createArrayNode()); + } + + private static ObjectMapper setupMapper() { + JsonFactory jsonFactory = new JsonFactory(); + jsonFactory.enable(JsonParser.Feature.ALLOW_COMMENTS); + jsonFactory.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); + return new ObjectMapper(jsonFactory); + } - public String format() { - try { - return node != null ? mapper.writeValueAsString(node) : null; - } catch (Exception e) { - return null; + private static JsonBrowser create(JsonNode node) { + return node != null ? new JsonBrowser(node) : NULL_BROWSER; } - } - - /** - * @return The value of the element as text - */ - public boolean isNull() { - return node == null || node.isNull(); - } - - /** - * Parse from string. - * @param json The JSON object as a string - * @return JsonBrowser instance for navigating in the result - * @throws IOException When parsing the JSON failed - */ - public static JsonBrowser parse(String json) throws IOException { - return create(mapper.readTree(json)); - } - - /** - * Parse from string. - * @param stream The JSON object as a stream - * @return JsonBrowser instance for navigating in the result - * @throws IOException When parsing the JSON failed - */ - public static JsonBrowser parse(InputStream stream) throws IOException { - return create(mapper.readTree(stream)); - } - - public static JsonBrowser newMap() throws IOException { - return create(mapper.createObjectNode()); - } - - public static JsonBrowser newList() throws IOException { - return create(mapper.createArrayNode()); - } - - private static ObjectMapper setupMapper() { - JsonFactory jsonFactory = new JsonFactory(); - jsonFactory.enable(JsonParser.Feature.ALLOW_COMMENTS); - jsonFactory.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); - return new ObjectMapper(jsonFactory); - } - - private static JsonBrowser create(JsonNode node) { - return node != null ? new JsonBrowser(node) : NULL_BROWSER; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/OrderedExecutor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/OrderedExecutor.java index 98b030ffe..5a3619dac 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/OrderedExecutor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/OrderedExecutor.java @@ -1,110 +1,102 @@ package com.sedmelluq.discord.lavaplayer.tools; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RunnableFuture; +import java.util.concurrent.*; /** * Wrapper for executor services which ensures that tasks with the same key are processed in order. */ public class OrderedExecutor { - private final ExecutorService delegateService; - private final ConcurrentMap> states; - - /** - * @param delegateService Executor service where to delegate the actual execution to - */ - public OrderedExecutor(ExecutorService delegateService) { - this.delegateService = delegateService; - this.states = new ConcurrentHashMap<>(); - } - - /** - * @param orderingKey Key for the ordering channel - * @param runnable Runnable to submit to the executor service - * @return Future for the task - */ - public Future submit(Object orderingKey, Runnable runnable) { - RunnableFuture runnableFuture = newTaskFor(runnable, null); - queueOrSubmit(new ChannelRunnable(orderingKey), runnableFuture); - return runnableFuture; - } - - /** - * @param orderingKey Key for the ordering channel - * @param callable Callable to submit to the executor service - * @return Future for the task - */ - public Future submit(Object orderingKey, Callable callable) { - RunnableFuture runnableFuture = newTaskFor(callable); - queueOrSubmit(new ChannelRunnable(orderingKey), runnableFuture); - return runnableFuture; - } - - private void queueOrSubmit(ChannelRunnable runnable, Runnable delegate) { - BlockingQueue newQueue = new LinkedBlockingQueue<>(); - newQueue.add(delegate); - - BlockingQueue existing = states.putIfAbsent(runnable.key, newQueue); - - if (existing != null) { - existing.add(delegate); - - if (states.putIfAbsent(runnable.key, existing) == null) { - delegateService.execute(new ChannelRunnable(runnable.key)); - } - } else { - delegateService.execute(runnable); + private final ExecutorService delegateService; + private final ConcurrentMap> states; + + /** + * @param delegateService Executor service where to delegate the actual execution to + */ + public OrderedExecutor(ExecutorService delegateService) { + this.delegateService = delegateService; + this.states = new ConcurrentHashMap<>(); } - } - private RunnableFuture newTaskFor(Runnable runnable, T value) { - return new FutureTask<>(runnable, value); - } + /** + * @param orderingKey Key for the ordering channel + * @param runnable Runnable to submit to the executor service + * @return Future for the task + */ + public Future submit(Object orderingKey, Runnable runnable) { + RunnableFuture runnableFuture = newTaskFor(runnable, null); + queueOrSubmit(new ChannelRunnable(orderingKey), runnableFuture); + return runnableFuture; + } + + /** + * @param orderingKey Key for the ordering channel + * @param callable Callable to submit to the executor service + * @return Future for the task + */ + public Future submit(Object orderingKey, Callable callable) { + RunnableFuture runnableFuture = newTaskFor(callable); + queueOrSubmit(new ChannelRunnable(orderingKey), runnableFuture); + return runnableFuture; + } - private RunnableFuture newTaskFor(Callable callable) { - return new FutureTask<>(callable); - } + private void queueOrSubmit(ChannelRunnable runnable, Runnable delegate) { + BlockingQueue newQueue = new LinkedBlockingQueue<>(); + newQueue.add(delegate); - private class ChannelRunnable implements Runnable { - private final Object key; + BlockingQueue existing = states.putIfAbsent(runnable.key, newQueue); - private ChannelRunnable(Object key) { - this.key = key; + if (existing != null) { + existing.add(delegate); + + if (states.putIfAbsent(runnable.key, existing) == null) { + delegateService.execute(new ChannelRunnable(runnable.key)); + } + } else { + delegateService.execute(runnable); + } } - @Override - public void run() { - BlockingQueue queue = states.get(key); + private RunnableFuture newTaskFor(Runnable runnable, T value) { + return new FutureTask<>(runnable, value); + } - if (queue != null) { - executeQueue(queue); - } + private RunnableFuture newTaskFor(Callable callable) { + return new FutureTask<>(callable); } - private void executeQueue(BlockingQueue queue) { - Runnable next; + private class ChannelRunnable implements Runnable { + private final Object key; - while ((next = queue.poll()) != null) { - boolean finished = false; + private ChannelRunnable(Object key) { + this.key = key; + } + + @Override + public void run() { + BlockingQueue queue = states.get(key); - try { - next.run(); - finished = true; - } finally { - if (!finished) { - delegateService.execute(new ChannelRunnable(key)); - } + if (queue != null) { + executeQueue(queue); + } } - } - states.remove(key, queue); + private void executeQueue(BlockingQueue queue) { + Runnable next; + + while ((next = queue.poll()) != null) { + boolean finished = false; + + try { + next.run(); + finished = true; + } finally { + if (!finished) { + delegateService.execute(new ChannelRunnable(key)); + } + } + } + + states.remove(key, queue); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/PlayerLibrary.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/PlayerLibrary.java index 33d61942b..1b50365b6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/PlayerLibrary.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/PlayerLibrary.java @@ -9,22 +9,22 @@ * Contains constants with metadata about the library. */ public class PlayerLibrary { - /** - * The currently loaded version of the library. - */ - public static final String VERSION = readVersion(); + /** + * The currently loaded version of the library. + */ + public static final String VERSION = readVersion(); - private static String readVersion() { - InputStream stream = PlayerLibrary.class.getResourceAsStream("version.txt"); + private static String readVersion() { + InputStream stream = PlayerLibrary.class.getResourceAsStream("version.txt"); - try { - if (stream != null) { - return IOUtils.toString(stream, StandardCharsets.UTF_8); - } - } catch (Exception e) { - // Something went wrong. - } + try { + if (stream != null) { + return IOUtils.toString(stream, StandardCharsets.UTF_8); + } + } catch (Exception e) { + // Something went wrong. + } - return "UNKNOWN"; - } + return "UNKNOWN"; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/RingBufferMath.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/RingBufferMath.java index 360856114..cc1b8506d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/RingBufferMath.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/RingBufferMath.java @@ -6,46 +6,46 @@ * Utility class for calculating averages on the last N values, with input and output transformers. */ public class RingBufferMath { - private final double[] values; - private final Function inputProcessor; - private final Function outputProcessor; - private double sum; - private int position; - private int size; + private final double[] values; + private final Function inputProcessor; + private final Function outputProcessor; + private double sum; + private int position; + private int size; - /** - * @param size Maximum number of values to remember. - * @param inputProcessor Input transformer. - * @param outputProcessor Output transformer. - */ - public RingBufferMath(int size, Function inputProcessor, Function outputProcessor) { - this.values = new double[size]; - this.inputProcessor = inputProcessor; - this.outputProcessor = outputProcessor; - } + /** + * @param size Maximum number of values to remember. + * @param inputProcessor Input transformer. + * @param outputProcessor Output transformer. + */ + public RingBufferMath(int size, Function inputProcessor, Function outputProcessor) { + this.values = new double[size]; + this.inputProcessor = inputProcessor; + this.outputProcessor = outputProcessor; + } - /** - * @param value Original value to add (before transformation) - */ - public void add(double value) { - value = inputProcessor.apply(value); + /** + * @param value Original value to add (before transformation) + */ + public void add(double value) { + value = inputProcessor.apply(value); - sum -= values[position]; - values[position] = value; - sum += values[position]; + sum -= values[position]; + values[position] = value; + sum += values[position]; - position = (position + 1) == values.length ? 0 : position + 1; - size = Math.min(values.length, size + 1); - } + position = (position + 1) == values.length ? 0 : position + 1; + size = Math.min(values.length, size + 1); + } - /** - * @return Transformed mean of the internal values. - */ - public double mean() { - if (size == 0) { - return outputProcessor.apply(0.0); - } else { - return outputProcessor.apply(sum / size); + /** + * @return Transformed mean of the internal values. + */ + public double mean() { + if (size == 0) { + return outputProcessor.apply(0.0); + } else { + return outputProcessor.apply(sum / size); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/Units.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/Units.java index a34faa871..ef09e50c9 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/Units.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/Units.java @@ -1,24 +1,24 @@ package com.sedmelluq.discord.lavaplayer.tools; public class Units { - /** - * Not a negative number, so that we would not need to test for it separately when comparing. - */ - public static final long CONTENT_LENGTH_UNKNOWN = Long.MAX_VALUE; - public static final long DURATION_MS_UNKNOWN = Long.MAX_VALUE; - public static final long DURATION_SEC_UNKNOWN = Long.MAX_VALUE; + /** + * Not a negative number, so that we would not need to test for it separately when comparing. + */ + public static final long CONTENT_LENGTH_UNKNOWN = Long.MAX_VALUE; + public static final long DURATION_MS_UNKNOWN = Long.MAX_VALUE; + public static final long DURATION_SEC_UNKNOWN = Long.MAX_VALUE; - public static final long BITRATE_UNKNOWN = -1; + public static final long BITRATE_UNKNOWN = -1; - private static final long SECONDS_MAXIMUM = DURATION_SEC_UNKNOWN / 1000; + private static final long SECONDS_MAXIMUM = DURATION_SEC_UNKNOWN / 1000; - public static long secondsToMillis(long seconds) { - if (seconds == DURATION_SEC_UNKNOWN) { - return DURATION_MS_UNKNOWN; - } else if (seconds > SECONDS_MAXIMUM) { - throw new RuntimeException("Cannot convert " + seconds + " to millis - would overflow."); - } else { - return seconds * 1000; + public static long secondsToMillis(long seconds) { + if (seconds == DURATION_SEC_UNKNOWN) { + return DURATION_MS_UNKNOWN; + } else if (seconds > SECONDS_MAXIMUM) { + throw new RuntimeException("Cannot convert " + seconds + " to millis - would overflow."); + } else { + return seconds * 1000; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/AbstractHttpContextFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/AbstractHttpContextFilter.java index 7eb12b939..9acc2ac84 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/AbstractHttpContextFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/AbstractHttpContextFilter.java @@ -5,48 +5,48 @@ import org.apache.http.client.protocol.HttpClientContext; public abstract class AbstractHttpContextFilter implements HttpContextFilter { - private final HttpContextFilter delegate; + private final HttpContextFilter delegate; - protected AbstractHttpContextFilter(HttpContextFilter delegate) { - this.delegate = delegate; - } - - @Override - public void onContextOpen(HttpClientContext context) { - if (delegate != null) { - delegate.onContextOpen(context); + protected AbstractHttpContextFilter(HttpContextFilter delegate) { + this.delegate = delegate; } - } - @Override - public void onContextClose(HttpClientContext context) { - if (delegate != null) { - delegate.onContextClose(context); + @Override + public void onContextOpen(HttpClientContext context) { + if (delegate != null) { + delegate.onContextOpen(context); + } } - } - @Override - public void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition) { - if (delegate != null) { - delegate.onRequest(context, request, isRepetition); + @Override + public void onContextClose(HttpClientContext context) { + if (delegate != null) { + delegate.onContextClose(context); + } } - } - @Override - public boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response) { - if (delegate != null) { - return delegate.onRequestResponse(context, request, response); + @Override + public void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition) { + if (delegate != null) { + delegate.onRequest(context, request, isRepetition); + } } - return false; - } + @Override + public boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response) { + if (delegate != null) { + return delegate.onRequestResponse(context, request, response); + } - @Override - public boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error) { - if (delegate != null) { - return delegate.onRequestException(context, request, error); + return false; } - return false; - } + @Override + public boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error) { + if (delegate != null) { + return delegate.onRequestException(context, request, error); + } + + return false; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/ExtendedConnectionOperator.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/ExtendedConnectionOperator.java index c1ae6bed2..b2384f3e1 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/ExtendedConnectionOperator.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/ExtendedConnectionOperator.java @@ -1,320 +1,308 @@ package com.sedmelluq.discord.lavaplayer.tools.http; -import java.io.IOException; -import java.net.ConnectException; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.NoRouteToHostException; -import java.net.Socket; -import java.net.SocketTimeoutException; -import java.util.function.IntPredicate; import org.apache.http.HttpHost; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.config.Lookup; import org.apache.http.config.SocketConfig; -import org.apache.http.conn.ConnectTimeoutException; -import org.apache.http.conn.DnsResolver; -import org.apache.http.conn.HttpClientConnectionOperator; -import org.apache.http.conn.HttpHostConnectException; -import org.apache.http.conn.ManagedHttpClientConnection; -import org.apache.http.conn.SchemePortResolver; -import org.apache.http.conn.UnsupportedSchemeException; +import org.apache.http.conn.*; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.LayeredConnectionSocketFactory; import org.apache.http.impl.conn.DefaultSchemePortResolver; import org.apache.http.impl.conn.SystemDefaultDnsResolver; import org.apache.http.protocol.HttpContext; +import java.io.IOException; +import java.net.*; +import java.util.function.IntPredicate; + public class ExtendedConnectionOperator implements HttpClientConnectionOperator { - private static final String SOCKET_FACTORY_REGISTRY = "http.socket-factory-registry"; - private static final String RESOLVED_ADDRESSES = "lp.resolved-addresses"; - - private final Lookup socketFactoryRegistry; - private final SchemePortResolver schemePortResolver; - private final DnsResolver dnsResolver; - - public ExtendedConnectionOperator( - Lookup socketFactoryRegistry, - SchemePortResolver schemePortResolver, - DnsResolver dnsResolver - ) { - this.socketFactoryRegistry = socketFactoryRegistry; - this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE; - this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE; - } - - public static void setResolvedAddresses(HttpContext context, HttpHost host, InetAddress[] addresses) { - if (host == null || addresses == null) { - context.removeAttribute(RESOLVED_ADDRESSES); - } else { - context.setAttribute(RESOLVED_ADDRESSES, new ResolvedAddresses(host, addresses)); + private static final String SOCKET_FACTORY_REGISTRY = "http.socket-factory-registry"; + private static final String RESOLVED_ADDRESSES = "lp.resolved-addresses"; + + private final Lookup socketFactoryRegistry; + private final SchemePortResolver schemePortResolver; + private final DnsResolver dnsResolver; + + public ExtendedConnectionOperator( + Lookup socketFactoryRegistry, + SchemePortResolver schemePortResolver, + DnsResolver dnsResolver + ) { + this.socketFactoryRegistry = socketFactoryRegistry; + this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE; + this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE; } - } - - @Override - public void connect( - ManagedHttpClientConnection connection, - HttpHost host, - InetSocketAddress localAddress, - int connectTimeout, - SocketConfig socketConfig, - HttpContext context - ) throws IOException { - ConnectionSocketFactory socketFactory = getSocketFactory(host, context); - - int port = schemePortResolver.resolve(host); - - InetAddress[] addresses = resolveAddresses(host, context); - int lastMatchIndex = lastMatchIndex(localAddress, addresses); - - for (int i = 0; i < addresses.length; i++) { - if (!addressTypesMatch(localAddress, addresses[i])) { - continue; - } - - InetSocketAddress remoteAddress = new InetSocketAddress(addresses[i], port); - boolean isLast = i == lastMatchIndex; - - try { - boolean connected = connectWithDestination( - socketFactory, context, socketConfig, host, localAddress, connectTimeout, connection, - remoteAddress, addresses, isLast - ); - if (connected) { - return; + public static void setResolvedAddresses(HttpContext context, HttpHost host, InetAddress[] addresses) { + if (host == null || addresses == null) { + context.removeAttribute(RESOLVED_ADDRESSES); + } else { + context.setAttribute(RESOLVED_ADDRESSES, new ResolvedAddresses(host, addresses)); } - } catch (IOException | RuntimeException | Error e) { - complementException(e, host, localAddress, remoteAddress, connectTimeout, addresses, i); - throw e; - } catch (Throwable e) { - RuntimeException delegated = new RuntimeException(e); - complementException(delegated, host, localAddress, remoteAddress, connectTimeout, addresses, i); - throw delegated; - } } - NoRouteToHostException exception = - new NoRouteToHostException("Local address protocol does not match any remote addresses."); - complementException(exception, host, localAddress, null, connectTimeout, addresses, 0); - throw exception; - } - - @Override - public void upgrade(ManagedHttpClientConnection connection, HttpHost host, HttpContext context) throws IOException { - ConnectionSocketFactory socketFactory = getSocketFactory(host, HttpClientContext.adapt(context)); + @Override + public void connect( + ManagedHttpClientConnection connection, + HttpHost host, + InetSocketAddress localAddress, + int connectTimeout, + SocketConfig socketConfig, + HttpContext context + ) throws IOException { + ConnectionSocketFactory socketFactory = getSocketFactory(host, context); + + int port = schemePortResolver.resolve(host); + + InetAddress[] addresses = resolveAddresses(host, context); + int lastMatchIndex = lastMatchIndex(localAddress, addresses); + + for (int i = 0; i < addresses.length; i++) { + if (!addressTypesMatch(localAddress, addresses[i])) { + continue; + } + + InetSocketAddress remoteAddress = new InetSocketAddress(addresses[i], port); + boolean isLast = i == lastMatchIndex; + + try { + boolean connected = connectWithDestination( + socketFactory, context, socketConfig, host, localAddress, connectTimeout, connection, + remoteAddress, addresses, isLast + ); + + if (connected) { + return; + } + } catch (IOException | RuntimeException | Error e) { + complementException(e, host, localAddress, remoteAddress, connectTimeout, addresses, i); + throw e; + } catch (Throwable e) { + RuntimeException delegated = new RuntimeException(e); + complementException(delegated, host, localAddress, remoteAddress, connectTimeout, addresses, i); + throw delegated; + } + } - if (!(socketFactory instanceof LayeredConnectionSocketFactory)) { - throw new UnsupportedSchemeException(host.getSchemeName() + - " protocol does not support connection upgrade"); + NoRouteToHostException exception = + new NoRouteToHostException("Local address protocol does not match any remote addresses."); + complementException(exception, host, localAddress, null, connectTimeout, addresses, 0); + throw exception; } - LayeredConnectionSocketFactory layeredFactory = (LayeredConnectionSocketFactory) socketFactory; + @Override + public void upgrade(ManagedHttpClientConnection connection, HttpHost host, HttpContext context) throws IOException { + ConnectionSocketFactory socketFactory = getSocketFactory(host, HttpClientContext.adapt(context)); + + if (!(socketFactory instanceof LayeredConnectionSocketFactory)) { + throw new UnsupportedSchemeException(host.getSchemeName() + + " protocol does not support connection upgrade"); + } - Socket socket = connection.getSocket(); - int port = this.schemePortResolver.resolve(host); - socket = layeredFactory.createLayeredSocket(socket, host.getHostName(), port, context); + LayeredConnectionSocketFactory layeredFactory = (LayeredConnectionSocketFactory) socketFactory; - connection.bind(socket); - } + Socket socket = connection.getSocket(); + int port = this.schemePortResolver.resolve(host); + socket = layeredFactory.createLayeredSocket(socket, host.getHostName(), port, context); - private InetAddress[] resolveAddresses(HttpHost host, HttpContext context) throws IOException { - if (host.getAddress() != null) { - return new InetAddress[] { host.getAddress() }; + connection.bind(socket); } - Object resolvedObject = context.getAttribute(RESOLVED_ADDRESSES); + private InetAddress[] resolveAddresses(HttpHost host, HttpContext context) throws IOException { + if (host.getAddress() != null) { + return new InetAddress[]{host.getAddress()}; + } + + Object resolvedObject = context.getAttribute(RESOLVED_ADDRESSES); - if (resolvedObject instanceof ResolvedAddresses) { - ResolvedAddresses resolved = (ResolvedAddresses) resolvedObject; + if (resolvedObject instanceof ResolvedAddresses) { + ResolvedAddresses resolved = (ResolvedAddresses) resolvedObject; - if (resolved.host.equals(host)) { - return resolved.addresses; - } + if (resolved.host.equals(host)) { + return resolved.addresses; + } + } + + return dnsResolver.resolve(host.getHostName()); } - return dnsResolver.resolve(host.getHostName()); - } - - private boolean connectWithDestination( - ConnectionSocketFactory socketFactory, - HttpContext context, - SocketConfig socketConfig, - HttpHost host, - InetSocketAddress localAddress, - int connectTimeout, - ManagedHttpClientConnection connection, - InetSocketAddress remoteAddress, - InetAddress[] addresses, - boolean last - ) throws IOException { - Socket socket = socketFactory.createSocket(context); - configureSocket(socket, socketConfig); - - try { - socket = socketFactory.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context); - connection.bind(socket); - return true; - } catch (final SocketTimeoutException ex) { - if (last) { - throw new ConnectTimeoutException(ex, host, addresses); - } - } catch (final ConnectException ex) { - if (last) { - final String msg = ex.getMessage(); - throw "Connection timed out".equals(msg) - ? new ConnectTimeoutException(ex, host, addresses) - : new HttpHostConnectException(ex, host, addresses); - } - } catch (final NoRouteToHostException ex) { - if (last) { - throw ex; - } + private boolean connectWithDestination( + ConnectionSocketFactory socketFactory, + HttpContext context, + SocketConfig socketConfig, + HttpHost host, + InetSocketAddress localAddress, + int connectTimeout, + ManagedHttpClientConnection connection, + InetSocketAddress remoteAddress, + InetAddress[] addresses, + boolean last + ) throws IOException { + Socket socket = socketFactory.createSocket(context); + configureSocket(socket, socketConfig); + + try { + socket = socketFactory.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context); + connection.bind(socket); + return true; + } catch (final SocketTimeoutException ex) { + if (last) { + throw new ConnectTimeoutException(ex, host, addresses); + } + } catch (final ConnectException ex) { + if (last) { + final String msg = ex.getMessage(); + throw "Connection timed out".equals(msg) + ? new ConnectTimeoutException(ex, host, addresses) + : new HttpHostConnectException(ex, host, addresses); + } + } catch (final NoRouteToHostException ex) { + if (last) { + throw ex; + } + } + + return false; } - return false; - } + private int lastMatchIndex(InetSocketAddress localSocketAddress, InetAddress[] remoteAddresses) { + for (int i = remoteAddresses.length - 1; i >= 0; i--) { + if (addressTypesMatch(localSocketAddress, remoteAddresses[i])) { + return i; + } + } - private int lastMatchIndex(InetSocketAddress localSocketAddress, InetAddress[] remoteAddresses) { - for (int i = remoteAddresses.length - 1; i >= 0; i--) { - if (addressTypesMatch(localSocketAddress, remoteAddresses[i])) { - return i; - } + return -1; } - return -1; - } + private boolean addressTypesMatch(InetSocketAddress localSocketAddress, InetAddress remoteAddress) { + InetAddress localAddress = localSocketAddress != null ? localSocketAddress.getAddress() : null; - private boolean addressTypesMatch(InetSocketAddress localSocketAddress, InetAddress remoteAddress) { - InetAddress localAddress = localSocketAddress != null ? localSocketAddress.getAddress() : null; + if (localAddress == null || remoteAddress == null) { + return true; + } - if (localAddress == null || remoteAddress == null) { - return true; + return (localAddress instanceof Inet4Address && remoteAddress instanceof Inet4Address) || + (localAddress instanceof Inet6Address && remoteAddress instanceof Inet6Address); } - return (localAddress instanceof Inet4Address && remoteAddress instanceof Inet4Address) || - (localAddress instanceof Inet6Address && remoteAddress instanceof Inet6Address); - } + private void configureSocket(Socket socket, SocketConfig socketConfig) throws IOException { + socket.setSoTimeout(socketConfig.getSoTimeout()); + socket.setReuseAddress(socketConfig.isSoReuseAddress()); + socket.setTcpNoDelay(socketConfig.isTcpNoDelay()); + socket.setKeepAlive(socketConfig.isSoKeepAlive()); - private void configureSocket(Socket socket, SocketConfig socketConfig) throws IOException { - socket.setSoTimeout(socketConfig.getSoTimeout()); - socket.setReuseAddress(socketConfig.isSoReuseAddress()); - socket.setTcpNoDelay(socketConfig.isTcpNoDelay()); - socket.setKeepAlive(socketConfig.isSoKeepAlive()); + if (socketConfig.getRcvBufSize() > 0) { + socket.setReceiveBufferSize(socketConfig.getRcvBufSize()); + } - if (socketConfig.getRcvBufSize() > 0) { - socket.setReceiveBufferSize(socketConfig.getRcvBufSize()); - } + if (socketConfig.getSndBufSize() > 0) { + socket.setSendBufferSize(socketConfig.getSndBufSize()); + } - if (socketConfig.getSndBufSize() > 0) { - socket.setSendBufferSize(socketConfig.getSndBufSize()); + if (socketConfig.getSoLinger() >= 0) { + socket.setSoLinger(true, socketConfig.getSoLinger()); + } } - if (socketConfig.getSoLinger() >= 0) { - socket.setSoLinger(true, socketConfig.getSoLinger()); - } - } + private ConnectionSocketFactory getSocketFactory(HttpHost host, HttpContext context) throws IOException { + Lookup registry = getSocketFactoryRegistry(context); + ConnectionSocketFactory socketFactory = registry.lookup(host.getSchemeName()); - private ConnectionSocketFactory getSocketFactory(HttpHost host, HttpContext context) throws IOException { - Lookup registry = getSocketFactoryRegistry(context); - ConnectionSocketFactory socketFactory = registry.lookup(host.getSchemeName()); + if (socketFactory == null) { + throw new UnsupportedSchemeException(host.getSchemeName() + " protocol is not supported"); + } - if (socketFactory == null) { - throw new UnsupportedSchemeException(host.getSchemeName() + " protocol is not supported"); + return socketFactory; } - return socketFactory; - } + @SuppressWarnings("unchecked") + private Lookup getSocketFactoryRegistry(HttpContext context) { + Lookup registry = (Lookup) + context.getAttribute(SOCKET_FACTORY_REGISTRY); - @SuppressWarnings("unchecked") - private Lookup getSocketFactoryRegistry(HttpContext context) { - Lookup registry = (Lookup) - context.getAttribute(SOCKET_FACTORY_REGISTRY); + if (registry == null) { + registry = this.socketFactoryRegistry; + } - if (registry == null) { - registry = this.socketFactoryRegistry; + return registry; } - return registry; - } - - private void complementException( - Throwable exception, - HttpHost host, - InetSocketAddress localAddress, - InetSocketAddress remoteAddress, - int connectTimeout, - InetAddress[] addresses, - int currentIndex - ) { - StringBuilder builder = new StringBuilder(); - builder.append("Encountered when opening a connection with the following details:"); - - appendField(builder, "host", host); - appendField(builder, "localAddress", localAddress); - appendField(builder, "remoteAddress", remoteAddress); - - builder.append("\n connectTimeout: ").append(connectTimeout); - - appendAddresses(builder, "triedAddresses", addresses, index -> - index <= currentIndex && addressTypesMatch(localAddress, addresses[index]) - ); - - appendAddresses(builder, "untriedAddresses", addresses, index -> - index > currentIndex && addressTypesMatch(localAddress, addresses[index]) - ); - - appendAddresses(builder, "unsuitableAddresses", addresses, index -> - !addressTypesMatch(localAddress, addresses[index]) - ); - - exception.addSuppressed(new AdditionalDetails(builder.toString())); - } - - private void appendField(StringBuilder builder, String name, Object field) { - builder.append("\n ").append(name).append(": "); - - if (field == null) { - builder.append(""); - } else { - builder.append(field.toString()); + private void complementException( + Throwable exception, + HttpHost host, + InetSocketAddress localAddress, + InetSocketAddress remoteAddress, + int connectTimeout, + InetAddress[] addresses, + int currentIndex + ) { + StringBuilder builder = new StringBuilder(); + builder.append("Encountered when opening a connection with the following details:"); + + appendField(builder, "host", host); + appendField(builder, "localAddress", localAddress); + appendField(builder, "remoteAddress", remoteAddress); + + builder.append("\n connectTimeout: ").append(connectTimeout); + + appendAddresses(builder, "triedAddresses", addresses, index -> + index <= currentIndex && addressTypesMatch(localAddress, addresses[index]) + ); + + appendAddresses(builder, "untriedAddresses", addresses, index -> + index > currentIndex && addressTypesMatch(localAddress, addresses[index]) + ); + + appendAddresses(builder, "unsuitableAddresses", addresses, index -> + !addressTypesMatch(localAddress, addresses[index]) + ); + + exception.addSuppressed(new AdditionalDetails(builder.toString())); } - } - private void appendAddresses(StringBuilder builder, String label, InetAddress[] array, IntPredicate check) { - boolean started = false; + private void appendField(StringBuilder builder, String name, Object field) { + builder.append("\n ").append(name).append(": "); - for (int i = 0; i < array.length; i++) { - if (check.test(i)) { - if (!started) { - builder.append("\n ").append(label).append(": "); - started = true; + if (field == null) { + builder.append(""); + } else { + builder.append(field.toString()); } - - builder.append(array[i]).append(", "); - } } - if (started) { - builder.setLength(builder.length() - 2); + private void appendAddresses(StringBuilder builder, String label, InetAddress[] array, IntPredicate check) { + boolean started = false; + + for (int i = 0; i < array.length; i++) { + if (check.test(i)) { + if (!started) { + builder.append("\n ").append(label).append(": "); + started = true; + } + + builder.append(array[i]).append(", "); + } + } + + if (started) { + builder.setLength(builder.length() - 2); + } } - } - private static class AdditionalDetails extends Exception { - protected AdditionalDetails(String message) { - super(message, null, true, false); + private static class AdditionalDetails extends Exception { + protected AdditionalDetails(String message) { + super(message, null, true, false); + } } - } - private static class ResolvedAddresses { - private final HttpHost host; - private final InetAddress[] addresses; + private static class ResolvedAddresses { + private final HttpHost host; + private final InetAddress[] addresses; - private ResolvedAddresses(HttpHost host, InetAddress[] addresses) { - this.host = host; - this.addresses = addresses; + private ResolvedAddresses(HttpHost host, InetAddress[] addresses) { + this.host = host; + this.addresses = addresses; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/ExtendedHttpClientBuilder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/ExtendedHttpClientBuilder.java index 7d9336483..bd9fcb1f5 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/ExtendedHttpClientBuilder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/ExtendedHttpClientBuilder.java @@ -2,11 +2,6 @@ import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.tools.io.TrustManagerBuilder; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLContext; -import javax.net.ssl.X509TrustManager; import org.apache.http.HttpResponseFactory; import org.apache.http.ProtocolVersion; import org.apache.http.config.MessageConstraints; @@ -38,175 +33,180 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509TrustManager; +import java.util.concurrent.TimeUnit; + import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.COMMON; import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS; public class ExtendedHttpClientBuilder extends HttpClientBuilder { - private static final Logger log = LoggerFactory.getLogger(ExtendedHttpClientBuilder.class); - - private static final SSLContext defaultSslContext = setupSslContext(); - - private SSLContext sslContextOverride; - private String[] sslSupportedProtocols; - private PlainConnectionSocketFactory plainSocketFactory; - private SSLConnectionSocketFactory sslSocketFactory; - private ConnectionManagerFactory connectionManagerFactory = ExtendedHttpClientBuilder::createDefaultConnectionManager; - - @Override - public synchronized CloseableHttpClient build() { - setConnectionManager(createConnectionManager()); - CloseableHttpClient httpClient = super.build(); - setConnectionManager(null); - return httpClient; - } - - /** - * @param sslContextOverride SSL context to make the built clients use. Note that calling - * {@link #setSSLContext(SSLContext)} has no effect because this class cannot access the - * instance set with that nor override the method. - */ - public void setSslContextOverride(SSLContext sslContextOverride) { - this.sslContextOverride = sslContextOverride; - } - - public void setSslSupportedProtocols(String[] protocols) { - this.sslSupportedProtocols = protocols; - } - - public void setPlainConnectionSocketFactory(PlainConnectionSocketFactory plainSocketFactory) { - this.plainSocketFactory = plainSocketFactory; - } - - public void setSslConnectionSocketFactory(SSLConnectionSocketFactory sslSocketFactory) { - this.sslSocketFactory = sslSocketFactory; - } - - public void setConnectionManagerFactory(ConnectionManagerFactory factory) { - this.connectionManagerFactory = factory; - } - - @Override - protected ClientExecChain decorateMainExec(ClientExecChain mainExec) { - return mainExec; - } - - private HttpClientConnectionManager createConnectionManager() { - return connectionManagerFactory.create( - new ExtendedConnectionOperator(createConnectionSocketFactory(), null, null), - createConnectionFactory() - ); - } - - private Registry createConnectionSocketFactory() { - HostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(PublicSuffixMatcherLoader.getDefault()); - ConnectionSocketFactory defaultSslSocketFactory = new SSLConnectionSocketFactory(sslContextOverride != null ? - sslContextOverride : defaultSslContext, sslSupportedProtocols, null, hostnameVerifier); - - return RegistryBuilder.create() - .register("http", plainSocketFactory != null ? plainSocketFactory : PlainConnectionSocketFactory.getSocketFactory()) - .register("https", sslSocketFactory != null ? sslSocketFactory : defaultSslSocketFactory) - .build(); - } - - private static ManagedHttpClientConnectionFactory createConnectionFactory() { - return new ManagedHttpClientConnectionFactory(null, (buffer, constraints) -> - new GarbageAllergicHttpResponseParser( - buffer, - IcyHttpLineParser.ICY_INSTANCE, - DefaultHttpResponseFactory.INSTANCE, - constraints - )); - } - - private static HttpClientConnectionManager createDefaultConnectionManager( - HttpClientConnectionOperator operator, - HttpConnectionFactory connectionFactory - ) { - PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager( - operator, - connectionFactory, - -1, - TimeUnit.MILLISECONDS - ); - - manager.setMaxTotal(3000); - manager.setDefaultMaxPerRoute(1500); - - return manager; - } - - private static SSLContext setupSslContext() { - try { - X509TrustManager trustManager = new TrustManagerBuilder() - .addBuiltinCertificates() - .addFromResourceDirectory("/certificates") - .build(); - - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, new X509TrustManager[] { trustManager }, null); - return context; - } catch (Exception e) { - log.error("Failed to build custom SSL context, using default one.", e); - return SSLContexts.createDefault(); - } - } - - private static class GarbageAllergicHttpResponseParser extends DefaultHttpResponseParser { - public GarbageAllergicHttpResponseParser( - SessionInputBuffer buffer, - LineParser lineParser, - HttpResponseFactory responseFactory, - MessageConstraints constraints - ) { - super(buffer, lineParser, responseFactory, constraints); - } + private static final Logger log = LoggerFactory.getLogger(ExtendedHttpClientBuilder.class); + + private static final SSLContext defaultSslContext = setupSslContext(); + + private SSLContext sslContextOverride; + private String[] sslSupportedProtocols; + private PlainConnectionSocketFactory plainSocketFactory; + private SSLConnectionSocketFactory sslSocketFactory; + private ConnectionManagerFactory connectionManagerFactory = ExtendedHttpClientBuilder::createDefaultConnectionManager; @Override - protected boolean reject(CharArrayBuffer line, int count) { - if (line.length() > 4 && "ICY ".equals(line.substring(0, 4))) { - throw new FriendlyException("ICY protocol is not supported.", COMMON, null); - } else if (count > 10) { - throw new FriendlyException("The server is giving us garbage.", SUSPICIOUS, null); - } - - return false; + public synchronized CloseableHttpClient build() { + setConnectionManager(createConnectionManager()); + CloseableHttpClient httpClient = super.build(); + setConnectionManager(null); + return httpClient; } - } - private static class IcyHttpLineParser extends BasicLineParser { - private static final IcyHttpLineParser ICY_INSTANCE = new IcyHttpLineParser(); - private static final ProtocolVersion ICY_PROTOCOL = new ProtocolVersion("HTTP", 1, 0); + /** + * @param sslContextOverride SSL context to make the built clients use. Note that calling + * {@link #setSSLContext(SSLContext)} has no effect because this class cannot access the + * instance set with that nor override the method. + */ + public void setSslContextOverride(SSLContext sslContextOverride) { + this.sslContextOverride = sslContextOverride; + } - @Override - public ProtocolVersion parseProtocolVersion(CharArrayBuffer buffer, ParserCursor cursor) { - int index = cursor.getPos(); - int bound = cursor.getUpperBound(); + public void setSslSupportedProtocols(String[] protocols) { + this.sslSupportedProtocols = protocols; + } - if (bound >= index + 4 && "ICY ".equals(buffer.substring(index, index + 4))) { - cursor.updatePos(index + 4); - return ICY_PROTOCOL; - } + public void setPlainConnectionSocketFactory(PlainConnectionSocketFactory plainSocketFactory) { + this.plainSocketFactory = plainSocketFactory; + } + + public void setSslConnectionSocketFactory(SSLConnectionSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + } - return super.parseProtocolVersion(buffer, cursor); + public void setConnectionManagerFactory(ConnectionManagerFactory factory) { + this.connectionManagerFactory = factory; } @Override - public boolean hasProtocolVersion(CharArrayBuffer buffer, ParserCursor cursor) { - int index = cursor.getPos(); - int bound = cursor.getUpperBound(); + protected ClientExecChain decorateMainExec(ClientExecChain mainExec) { + return mainExec; + } - if (bound >= index + 4 && "ICY ".equals(buffer.substring(index, index + 4))) { - return true; - } + private HttpClientConnectionManager createConnectionManager() { + return connectionManagerFactory.create( + new ExtendedConnectionOperator(createConnectionSocketFactory(), null, null), + createConnectionFactory() + ); + } - return super.hasProtocolVersion(buffer, cursor); + private Registry createConnectionSocketFactory() { + HostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(PublicSuffixMatcherLoader.getDefault()); + ConnectionSocketFactory defaultSslSocketFactory = new SSLConnectionSocketFactory(sslContextOverride != null ? + sslContextOverride : defaultSslContext, sslSupportedProtocols, null, hostnameVerifier); + + return RegistryBuilder.create() + .register("http", plainSocketFactory != null ? plainSocketFactory : PlainConnectionSocketFactory.getSocketFactory()) + .register("https", sslSocketFactory != null ? sslSocketFactory : defaultSslSocketFactory) + .build(); + } + + private static ManagedHttpClientConnectionFactory createConnectionFactory() { + return new ManagedHttpClientConnectionFactory(null, (buffer, constraints) -> + new GarbageAllergicHttpResponseParser( + buffer, + IcyHttpLineParser.ICY_INSTANCE, + DefaultHttpResponseFactory.INSTANCE, + constraints + )); } - } - public interface ConnectionManagerFactory { - HttpClientConnectionManager create( + private static HttpClientConnectionManager createDefaultConnectionManager( HttpClientConnectionOperator operator, HttpConnectionFactory connectionFactory - ); - } + ) { + PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager( + operator, + connectionFactory, + -1, + TimeUnit.MILLISECONDS + ); + + manager.setMaxTotal(3000); + manager.setDefaultMaxPerRoute(1500); + + return manager; + } + + private static SSLContext setupSslContext() { + try { + X509TrustManager trustManager = new TrustManagerBuilder() + .addBuiltinCertificates() + .addFromResourceDirectory("/certificates") + .build(); + + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, new X509TrustManager[]{trustManager}, null); + return context; + } catch (Exception e) { + log.error("Failed to build custom SSL context, using default one.", e); + return SSLContexts.createDefault(); + } + } + + private static class GarbageAllergicHttpResponseParser extends DefaultHttpResponseParser { + public GarbageAllergicHttpResponseParser( + SessionInputBuffer buffer, + LineParser lineParser, + HttpResponseFactory responseFactory, + MessageConstraints constraints + ) { + super(buffer, lineParser, responseFactory, constraints); + } + + @Override + protected boolean reject(CharArrayBuffer line, int count) { + if (line.length() > 4 && "ICY ".equals(line.substring(0, 4))) { + throw new FriendlyException("ICY protocol is not supported.", COMMON, null); + } else if (count > 10) { + throw new FriendlyException("The server is giving us garbage.", SUSPICIOUS, null); + } + + return false; + } + } + + private static class IcyHttpLineParser extends BasicLineParser { + private static final IcyHttpLineParser ICY_INSTANCE = new IcyHttpLineParser(); + private static final ProtocolVersion ICY_PROTOCOL = new ProtocolVersion("HTTP", 1, 0); + + @Override + public ProtocolVersion parseProtocolVersion(CharArrayBuffer buffer, ParserCursor cursor) { + int index = cursor.getPos(); + int bound = cursor.getUpperBound(); + + if (bound >= index + 4 && "ICY ".equals(buffer.substring(index, index + 4))) { + cursor.updatePos(index + 4); + return ICY_PROTOCOL; + } + + return super.parseProtocolVersion(buffer, cursor); + } + + @Override + public boolean hasProtocolVersion(CharArrayBuffer buffer, ParserCursor cursor) { + int index = cursor.getPos(); + int bound = cursor.getUpperBound(); + + if (bound >= index + 4 && "ICY ".equals(buffer.substring(index, index + 4))) { + return true; + } + + return super.hasProtocolVersion(buffer, cursor); + } + } + + public interface ConnectionManagerFactory { + HttpClientConnectionManager create( + HttpClientConnectionOperator operator, + HttpConnectionFactory connectionFactory + ); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/ExtendedHttpConfigurable.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/ExtendedHttpConfigurable.java index debd09c05..a3d5aebbd 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/ExtendedHttpConfigurable.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/ExtendedHttpConfigurable.java @@ -3,5 +3,5 @@ import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; public interface ExtendedHttpConfigurable extends HttpConfigurable { - void setHttpContextFilter(HttpContextFilter filter); + void setHttpContextFilter(HttpContextFilter filter); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/HttpContextFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/HttpContextFilter.java index 3a936d192..53a01c354 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/HttpContextFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/HttpContextFilter.java @@ -5,13 +5,13 @@ import org.apache.http.client.protocol.HttpClientContext; public interface HttpContextFilter { - void onContextOpen(HttpClientContext context); + void onContextOpen(HttpClientContext context); - void onContextClose(HttpClientContext context); + void onContextClose(HttpClientContext context); - void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition); + void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition); - boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response); + boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response); - boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error); + boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/HttpContextRetryCounter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/HttpContextRetryCounter.java index e165a82c7..274d5885b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/HttpContextRetryCounter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/HttpContextRetryCounter.java @@ -3,37 +3,37 @@ import org.apache.http.client.protocol.HttpClientContext; public class HttpContextRetryCounter { - private final String attributeName; + private final String attributeName; - public HttpContextRetryCounter(String attributeName) { - this.attributeName = attributeName; - } + public HttpContextRetryCounter(String attributeName) { + this.attributeName = attributeName; + } - public void handleUpdate(HttpClientContext context, boolean isRepetition) { - if (isRepetition) { - setRetryCount(context, getRetryCount(context) + 1); - } else { - setRetryCount(context, 0); + public void handleUpdate(HttpClientContext context, boolean isRepetition) { + if (isRepetition) { + setRetryCount(context, getRetryCount(context) + 1); + } else { + setRetryCount(context, 0); + } } - } - public void setRetryCount(HttpClientContext context, int value) { - RetryCount count = context.getAttribute(attributeName, RetryCount.class); + public void setRetryCount(HttpClientContext context, int value) { + RetryCount count = context.getAttribute(attributeName, RetryCount.class); - if (count == null) { - count = new RetryCount(); - context.setAttribute(attributeName, count); - } + if (count == null) { + count = new RetryCount(); + context.setAttribute(attributeName, count); + } - count.value = value; - } + count.value = value; + } - public int getRetryCount(HttpClientContext context) { - RetryCount count = context.getAttribute(attributeName, RetryCount.class); - return count != null ? count.value : 0; - } + public int getRetryCount(HttpClientContext context) { + RetryCount count = context.getAttribute(attributeName, RetryCount.class); + return count != null ? count.value : 0; + } - private static class RetryCount { - private int value; - } + private static class RetryCount { + private int value; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/HttpStreamTools.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/HttpStreamTools.java index 6636fb8de..8f9523ca4 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/HttpStreamTools.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/HttpStreamTools.java @@ -3,32 +3,33 @@ import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; -import java.io.IOException; -import java.io.InputStream; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; +import java.io.IOException; +import java.io.InputStream; + public class HttpStreamTools { - public static InputStream streamContent(HttpInterface httpInterface, HttpUriRequest request) { - CloseableHttpResponse response = null; - boolean success = false; + public static InputStream streamContent(HttpInterface httpInterface, HttpUriRequest request) { + CloseableHttpResponse response = null; + boolean success = false; - try { - response = httpInterface.execute(request); - int statusCode = response.getStatusLine().getStatusCode(); + try { + response = httpInterface.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); - if (!HttpClientTools.isSuccessWithContent(statusCode)) { - throw new IOException("Invalid status code from " + request.getURI() + " URL: " + statusCode); - } + if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new IOException("Invalid status code from " + request.getURI() + " URL: " + statusCode); + } - success = true; - return response.getEntity().getContent(); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - if (response != null && !success) { - ExceptionTools.closeWithWarnings(response); - } + success = true; + return response.getEntity().getContent(); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + if (response != null && !success) { + ExceptionTools.closeWithWarnings(response); + } + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/MultiHttpConfigurable.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/MultiHttpConfigurable.java index 9b4a6b1ee..c4bcb781b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/MultiHttpConfigurable.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/MultiHttpConfigurable.java @@ -1,36 +1,37 @@ package com.sedmelluq.discord.lavaplayer.tools.http; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.HttpClientBuilder; + import java.util.Collection; import java.util.function.Consumer; import java.util.function.Function; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.impl.client.HttpClientBuilder; public class MultiHttpConfigurable implements ExtendedHttpConfigurable { - private final Collection configurables; + private final Collection configurables; - public MultiHttpConfigurable(Collection configurables) { - this.configurables = configurables; - } + public MultiHttpConfigurable(Collection configurables) { + this.configurables = configurables; + } - @Override - public void setHttpContextFilter(HttpContextFilter filter) { - for (ExtendedHttpConfigurable configurable : configurables) { - configurable.setHttpContextFilter(filter); + @Override + public void setHttpContextFilter(HttpContextFilter filter) { + for (ExtendedHttpConfigurable configurable : configurables) { + configurable.setHttpContextFilter(filter); + } } - } - @Override - public void configureRequests(Function configurator) { - for (ExtendedHttpConfigurable configurable : configurables) { - configurable.configureRequests(configurator); + @Override + public void configureRequests(Function configurator) { + for (ExtendedHttpConfigurable configurable : configurables) { + configurable.configureRequests(configurator); + } } - } - @Override - public void configureBuilder(Consumer configurator) { - for (ExtendedHttpConfigurable configurable : configurables) { - configurable.configureBuilder(configurator); + @Override + public void configureBuilder(Consumer configurator) { + for (ExtendedHttpConfigurable configurable : configurables) { + configurable.configureBuilder(configurator); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/SettableHttpRequestFilter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/SettableHttpRequestFilter.java index f82e0c525..e1d045864 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/SettableHttpRequestFilter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/SettableHttpRequestFilter.java @@ -5,62 +5,62 @@ import org.apache.http.client.protocol.HttpClientContext; public class SettableHttpRequestFilter implements HttpContextFilter { - private HttpContextFilter filter; + private HttpContextFilter filter; - public HttpContextFilter get() { - return filter; - } + public HttpContextFilter get() { + return filter; + } - public void set(HttpContextFilter filter) { - this.filter = filter; - } + public void set(HttpContextFilter filter) { + this.filter = filter; + } - @Override - public void onContextOpen(HttpClientContext context) { - HttpContextFilter current = filter; + @Override + public void onContextOpen(HttpClientContext context) { + HttpContextFilter current = filter; - if (current != null) { - current.onContextOpen(context); + if (current != null) { + current.onContextOpen(context); + } } - } - @Override - public void onContextClose(HttpClientContext context) { - HttpContextFilter current = filter; + @Override + public void onContextClose(HttpClientContext context) { + HttpContextFilter current = filter; - if (current != null) { - current.onContextClose(context); + if (current != null) { + current.onContextClose(context); + } } - } - @Override - public void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition) { - HttpContextFilter current = filter; + @Override + public void onRequest(HttpClientContext context, HttpUriRequest request, boolean isRepetition) { + HttpContextFilter current = filter; - if (current != null) { - current.onRequest(context, request, isRepetition); + if (current != null) { + current.onRequest(context, request, isRepetition); + } } - } - @Override - public boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response) { - HttpContextFilter current = filter; + @Override + public boolean onRequestResponse(HttpClientContext context, HttpUriRequest request, HttpResponse response) { + HttpContextFilter current = filter; - if (current != null) { - return current.onRequestResponse(context, request, response); - } else { - return false; + if (current != null) { + return current.onRequestResponse(context, request, response); + } else { + return false; + } } - } - @Override - public boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error) { - HttpContextFilter current = filter; + @Override + public boolean onRequestException(HttpClientContext context, HttpUriRequest request, Throwable error) { + HttpContextFilter current = filter; - if (current != null) { - return current.onRequestException(context, request, error); - } else { - return false; + if (current != null) { + return current.onRequestException(context, request, error); + } else { + return false; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/SimpleHttpClientConnectionManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/SimpleHttpClientConnectionManager.java index 075093a4c..98324434e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/SimpleHttpClientConnectionManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/http/SimpleHttpClientConnectionManager.java @@ -1,118 +1,115 @@ package com.sedmelluq.discord.lavaplayer.tools.http; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.concurrent.TimeUnit; import org.apache.http.HttpClientConnection; import org.apache.http.HttpHost; import org.apache.http.config.ConnectionConfig; import org.apache.http.config.SocketConfig; -import org.apache.http.conn.ConnectionRequest; -import org.apache.http.conn.HttpClientConnectionManager; -import org.apache.http.conn.HttpClientConnectionOperator; -import org.apache.http.conn.HttpConnectionFactory; -import org.apache.http.conn.ManagedHttpClientConnection; +import org.apache.http.conn.*; import org.apache.http.conn.routing.HttpRoute; import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory; import org.apache.http.protocol.HttpContext; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + public class SimpleHttpClientConnectionManager implements HttpClientConnectionManager { - private final HttpClientConnectionOperator connectionOperator; - private final HttpConnectionFactory connectionFactory; - private volatile SocketConfig socketConfig = SocketConfig.DEFAULT; - private volatile ConnectionConfig connectionConfig = ConnectionConfig.DEFAULT; - - public SimpleHttpClientConnectionManager( - HttpClientConnectionOperator connectionOperator, - HttpConnectionFactory factory - ) { - this.connectionOperator = connectionOperator; - this.connectionFactory = factory != null ? factory : ManagedHttpClientConnectionFactory.INSTANCE; - } - - public void setSocketConfig(SocketConfig config) { - this.socketConfig = config; - } - - public void setConnectionConfig(ConnectionConfig config) { - this.connectionConfig = config; - } - - @Override - public ConnectionRequest requestConnection(HttpRoute route, Object state) { - return new ConnectionRequest() { - - @Override - public boolean cancel() { + private final HttpClientConnectionOperator connectionOperator; + private final HttpConnectionFactory connectionFactory; + private volatile SocketConfig socketConfig = SocketConfig.DEFAULT; + private volatile ConnectionConfig connectionConfig = ConnectionConfig.DEFAULT; + + public SimpleHttpClientConnectionManager( + HttpClientConnectionOperator connectionOperator, + HttpConnectionFactory factory + ) { + this.connectionOperator = connectionOperator; + this.connectionFactory = factory != null ? factory : ManagedHttpClientConnectionFactory.INSTANCE; + } + + public void setSocketConfig(SocketConfig config) { + this.socketConfig = config; + } + + public void setConnectionConfig(ConnectionConfig config) { + this.connectionConfig = config; + } + + @Override + public ConnectionRequest requestConnection(HttpRoute route, Object state) { + return new ConnectionRequest() { + + @Override + public boolean cancel() { + // Nothing to do. + return false; + } + + @Override + public HttpClientConnection get(final long timeout, final TimeUnit timeUnit) { + return connectionFactory.create(route, connectionConfig); + } + }; + } + + @Override + public void releaseConnection( + HttpClientConnection connection, + Object newState, + long validDuration, + TimeUnit timeUnit + ) { + try { + connection.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void connect( + HttpClientConnection connection, + HttpRoute route, + int connectTimeout, + HttpContext context + ) throws IOException { + HttpHost host; + + if (route.getProxyHost() != null) { + host = route.getProxyHost(); + } else { + host = route.getTargetHost(); + } + + InetSocketAddress localAddress = route.getLocalSocketAddress(); + + ManagedHttpClientConnection managed = (ManagedHttpClientConnection) connection; + this.connectionOperator.connect(managed, host, localAddress, connectTimeout, this.socketConfig, context); + } + + @Override + public void upgrade(HttpClientConnection connection, HttpRoute route, HttpContext context) throws IOException { + ManagedHttpClientConnection managed = (ManagedHttpClientConnection) connection; + this.connectionOperator.upgrade(managed, route.getTargetHost(), context); + } + + @Override + public void routeComplete(HttpClientConnection conn, HttpRoute route, HttpContext context) { // Nothing to do. - return false; - } - - @Override - public HttpClientConnection get(final long timeout, final TimeUnit timeUnit) { - return connectionFactory.create(route, connectionConfig); - } - }; - } - - @Override - public void releaseConnection( - HttpClientConnection connection, - Object newState, - long validDuration, - TimeUnit timeUnit - ) { - try { - connection.close(); - } catch (IOException e) { - throw new RuntimeException(e); } - } - - @Override - public void connect( - HttpClientConnection connection, - HttpRoute route, - int connectTimeout, - HttpContext context - ) throws IOException { - HttpHost host; - - if (route.getProxyHost() != null) { - host = route.getProxyHost(); - } else { - host = route.getTargetHost(); + + @Override + public void closeIdleConnections(long idletime, TimeUnit timeUnit) { + // Nothing to do. } - InetSocketAddress localAddress = route.getLocalSocketAddress(); - - ManagedHttpClientConnection managed = (ManagedHttpClientConnection) connection; - this.connectionOperator.connect(managed, host, localAddress, connectTimeout, this.socketConfig, context); - } - - @Override - public void upgrade(HttpClientConnection connection, HttpRoute route, HttpContext context) throws IOException { - ManagedHttpClientConnection managed = (ManagedHttpClientConnection) connection; - this.connectionOperator.upgrade(managed, route.getTargetHost(), context); - } - - @Override - public void routeComplete(HttpClientConnection conn, HttpRoute route, HttpContext context) { - // Nothing to do. - } - - @Override - public void closeIdleConnections(long idletime, TimeUnit timeUnit) { - // Nothing to do. - } - - @Override - public void closeExpiredConnections() { - // Nothing to do. - } - - @Override - public void shutdown() { - // Nothing to do. - } + @Override + public void closeExpiredConnections() { + // Nothing to do. + } + + @Override + public void shutdown() { + // Nothing to do. + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/AbstractHttpInterfaceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/AbstractHttpInterfaceManager.java index b7579e307..b99cc612a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/AbstractHttpInterfaceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/AbstractHttpInterfaceManager.java @@ -1,89 +1,90 @@ package com.sedmelluq.discord.lavaplayer.tools.io; -import java.io.IOException; -import java.util.function.Consumer; -import java.util.function.Function; import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.util.function.Consumer; +import java.util.function.Function; + /** * Base class for an HTTP interface manager with lazily initialized http client instance. */ public abstract class AbstractHttpInterfaceManager implements HttpInterfaceManager { - private static final Logger log = LoggerFactory.getLogger(AbstractHttpInterfaceManager.class); + private static final Logger log = LoggerFactory.getLogger(AbstractHttpInterfaceManager.class); - private final HttpClientBuilder clientBuilder; - private final Object lock; - private boolean closed; - private CloseableHttpClient sharedClient; - private RequestConfig requestConfig; + private final HttpClientBuilder clientBuilder; + private final Object lock; + private boolean closed; + private CloseableHttpClient sharedClient; + private RequestConfig requestConfig; - /** - * @param clientBuilder HTTP client builder to use for creating the client instance. - * @param requestConfig Request config used by the client builder - */ - public AbstractHttpInterfaceManager(HttpClientBuilder clientBuilder, RequestConfig requestConfig) { - this.clientBuilder = clientBuilder; - this.requestConfig = requestConfig; - this.lock = new Object(); - } + /** + * @param clientBuilder HTTP client builder to use for creating the client instance. + * @param requestConfig Request config used by the client builder + */ + public AbstractHttpInterfaceManager(HttpClientBuilder clientBuilder, RequestConfig requestConfig) { + this.clientBuilder = clientBuilder; + this.requestConfig = requestConfig; + this.lock = new Object(); + } - @Override - public void close() throws IOException { - synchronized (lock) { - closed = true; + @Override + public void close() throws IOException { + synchronized (lock) { + closed = true; - if (sharedClient != null) { - CloseableHttpClient client = sharedClient; - sharedClient = null; - client.close(); - } + if (sharedClient != null) { + CloseableHttpClient client = sharedClient; + sharedClient = null; + client.close(); + } + } } - } - @Override - public void configureRequests(Function configurator) { - synchronized (lock) { - try { - close(); - } catch (Exception e) { - log.warn("Failed to close HTTP client.", e); - } + @Override + public void configureRequests(Function configurator) { + synchronized (lock) { + try { + close(); + } catch (Exception e) { + log.warn("Failed to close HTTP client.", e); + } - closed = false; - requestConfig = configurator.apply(requestConfig); - clientBuilder.setDefaultRequestConfig(requestConfig); + closed = false; + requestConfig = configurator.apply(requestConfig); + clientBuilder.setDefaultRequestConfig(requestConfig); + } } - } - @Override - public void configureBuilder(Consumer configurator) { - synchronized (lock) { - try { - close(); - } catch (Exception e) { - log.warn("Failed to close HTTP client.", e); - } + @Override + public void configureBuilder(Consumer configurator) { + synchronized (lock) { + try { + close(); + } catch (Exception e) { + log.warn("Failed to close HTTP client.", e); + } - closed = false; - configurator.accept(clientBuilder); + closed = false; + configurator.accept(clientBuilder); + } } - } - protected CloseableHttpClient getSharedClient() { - synchronized (lock) { - if (closed) { - throw new IllegalStateException("Cannot get http client for a closed manager."); - } + protected CloseableHttpClient getSharedClient() { + synchronized (lock) { + if (closed) { + throw new IllegalStateException("Cannot get http client for a closed manager."); + } - if (sharedClient == null) { - sharedClient = clientBuilder.build(); - } + if (sharedClient == null) { + sharedClient = clientBuilder.build(); + } - return sharedClient; + return sharedClient; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/BitBufferReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/BitBufferReader.java index c2fd054c8..0f4f5f087 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/BitBufferReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/BitBufferReader.java @@ -7,37 +7,37 @@ * Helper for reading a specific number of bits at a time from a byte buffer. */ public class BitBufferReader extends BitStreamReader { - private final ByteBuffer buffer; + private final ByteBuffer buffer; - /** - * @param buffer Byte buffer to read bytes from - */ - public BitBufferReader(ByteBuffer buffer) { - super(null); + /** + * @param buffer Byte buffer to read bytes from + */ + public BitBufferReader(ByteBuffer buffer) { + super(null); - this.buffer = buffer; - } + this.buffer = buffer; + } - @Override - public long asLong(int bitsNeeded) { - try { - return super.asLong(bitsNeeded); - } catch (IOException e) { - throw new RuntimeException(e); + @Override + public long asLong(int bitsNeeded) { + try { + return super.asLong(bitsNeeded); + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - @Override - public int asInteger(int bitsNeeded) { - try { - return super.asInteger(bitsNeeded); - } catch (IOException e) { - throw new RuntimeException(e); + @Override + public int asInteger(int bitsNeeded) { + try { + return super.asInteger(bitsNeeded); + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - @Override - protected int readByte() throws IOException { - return buffer.get() & 0xFF; - } + @Override + protected int readByte() throws IOException { + return buffer.get() & 0xFF; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/BitStreamReader.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/BitStreamReader.java index 7bab94d99..eca50fa18 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/BitStreamReader.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/BitStreamReader.java @@ -8,117 +8,123 @@ * Helper for reading a specific number of bits at a time from a stream. */ public class BitStreamReader { - private final InputStream stream; - private int currentByte; - private int bitsLeft; - - /** - * @param stream The underlying stream - */ - public BitStreamReader(InputStream stream) { - this.stream = stream; - } - - /** - * Get the specified number of bits as a long value - * @param bitsNeeded Number of bits to retrieve - * @return The value of those bits as a long - * @throws IOException On read error - */ - public long asLong(int bitsNeeded) throws IOException { - long value = 0; - - while (bitsNeeded > 0) { - fill(); - - int chunk = Math.min(bitsNeeded, bitsLeft); - int mask = (1 << chunk) - 1; - - value <<= chunk; - value |= (currentByte >> (bitsLeft - chunk)) & mask; - - bitsNeeded -= chunk; - bitsLeft -= chunk; + private final InputStream stream; + private int currentByte; + private int bitsLeft; + + /** + * @param stream The underlying stream + */ + public BitStreamReader(InputStream stream) { + this.stream = stream; } - return value; - } - - /** - * Get the specific number of bits as a signed long value (highest order bit is sign) - * @param bitsNeeded Number of bits needed - * @return The signed value - * @throws IOException On read error - */ - public long asSignedLong(int bitsNeeded) throws IOException { - long value = asLong(bitsNeeded); - - if ((value & (1L << (bitsNeeded - 1))) != 0) { - return value | ~((1L << bitsNeeded) - 1); - } else { - return value; + /** + * Get the specified number of bits as a long value + * + * @param bitsNeeded Number of bits to retrieve + * @return The value of those bits as a long + * @throws IOException On read error + */ + public long asLong(int bitsNeeded) throws IOException { + long value = 0; + + while (bitsNeeded > 0) { + fill(); + + int chunk = Math.min(bitsNeeded, bitsLeft); + int mask = (1 << chunk) - 1; + + value <<= chunk; + value |= (currentByte >> (bitsLeft - chunk)) & mask; + + bitsNeeded -= chunk; + bitsLeft -= chunk; + } + + return value; + } + + /** + * Get the specific number of bits as a signed long value (highest order bit is sign) + * + * @param bitsNeeded Number of bits needed + * @return The signed value + * @throws IOException On read error + */ + public long asSignedLong(int bitsNeeded) throws IOException { + long value = asLong(bitsNeeded); + + if ((value & (1L << (bitsNeeded - 1))) != 0) { + return value | ~((1L << bitsNeeded) - 1); + } else { + return value; + } + } + + /** + * Get the specified number of bits as an integer value + * + * @param bitsNeeded Number of bits to retrieve + * @return The value of those bits as an integer + * @throws IOException On read error + */ + public int asInteger(int bitsNeeded) throws IOException { + return Math.toIntExact(asLong(bitsNeeded)); + } + + /** + * Get the specific number of bits as a signed integer value (highest order bit is sign) + * + * @param bitsNeeded Number of bits needed + * @return The signed value + * @throws IOException On read error + */ + public int asSignedInteger(int bitsNeeded) throws IOException { + return Math.toIntExact(asSignedLong(bitsNeeded)); + } + + /** + * Reads bits from the stream until a set bit is reached. + * + * @return The number of zeroes read + * @throws IOException On read error + */ + public int readAllZeroes() throws IOException { + int count = 0; + fill(); + + while ((currentByte & (1 << --bitsLeft)) == 0) { + count++; + fill(); + } + + return count; } - } - - /** - * Get the specified number of bits as an integer value - * @param bitsNeeded Number of bits to retrieve - * @return The value of those bits as an integer - * @throws IOException On read error - */ - public int asInteger(int bitsNeeded) throws IOException { - return Math.toIntExact(asLong(bitsNeeded)); - } - - /** - * Get the specific number of bits as a signed integer value (highest order bit is sign) - * @param bitsNeeded Number of bits needed - * @return The signed value - * @throws IOException On read error - */ - public int asSignedInteger(int bitsNeeded) throws IOException { - return Math.toIntExact(asSignedLong(bitsNeeded)); - } - - /** - * Reads bits from the stream until a set bit is reached. - * @return The number of zeroes read - * @throws IOException On read error - */ - public int readAllZeroes() throws IOException { - int count = 0; - fill(); - - while ((currentByte & (1 << --bitsLeft)) == 0) { - count++; - fill(); + + /** + * Reads the number of bits it requires to make the reader align on a byte. + * + * @return The read bits as an unsigned value + */ + public int readRemainingBits() { + int value = currentByte & ((1 << bitsLeft) - 1); + bitsLeft = 0; + return value; } - return count; - } - - /** - * Reads the number of bits it requires to make the reader align on a byte. - * @return The read bits as an unsigned value - */ - public int readRemainingBits() { - int value = currentByte & ((1 << bitsLeft) - 1); - bitsLeft = 0; - return value; - } - - private void fill() throws IOException { - if (bitsLeft == 0) { - currentByte = readByte(); - bitsLeft = 8; - - if (currentByte == -1) { - throw new EOFException("Bit stream needs more bytes"); - } + private void fill() throws IOException { + if (bitsLeft == 0) { + currentByte = readByte(); + bitsLeft = 8; + + if (currentByte == -1) { + throw new EOFException("Bit stream needs more bytes"); + } + } } - } - protected int readByte() throws IOException { - return stream.read(); - } + protected int readByte() throws IOException { + return stream.read(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/BitStreamWriter.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/BitStreamWriter.java index 4ae0cad06..72d6f4097 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/BitStreamWriter.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/BitStreamWriter.java @@ -7,57 +7,58 @@ * Helper for writing a specific number of bits at a time to a stream. */ public class BitStreamWriter { - private final OutputStream stream; - private int currentByte; - private int bitsUnused; - - /** - * @param stream The underlying stream - */ - public BitStreamWriter(OutputStream stream) { - this.stream = stream; - bitsUnused = 8; - } - - /** - * @param value The value to take the bits from (lower order bits first) - * @param bits Number of bits to write - * @throws IOException On write error - */ - public void write(long value, int bits) throws IOException { - int bitsToPush = bits; - - while (bitsToPush > 0) { - int chunk = Math.min(bitsUnused, bitsToPush); - int mask = (1 << chunk) - 1; - - currentByte |= (((int) (value >> (bitsToPush - chunk))) & mask) << (bitsUnused - chunk); - - sendOnFullByte(); - - bitsToPush -= chunk; - bitsUnused -= chunk; + private final OutputStream stream; + private int currentByte; + private int bitsUnused; + + /** + * @param stream The underlying stream + */ + public BitStreamWriter(OutputStream stream) { + this.stream = stream; + bitsUnused = 8; } - } - private void sendOnFullByte() throws IOException { - if (bitsUnused == 0) { - stream.write(currentByte); - bitsUnused = 8; - currentByte = 0; + /** + * @param value The value to take the bits from (lower order bits first) + * @param bits Number of bits to write + * @throws IOException On write error + */ + public void write(long value, int bits) throws IOException { + int bitsToPush = bits; + + while (bitsToPush > 0) { + int chunk = Math.min(bitsUnused, bitsToPush); + int mask = (1 << chunk) - 1; + + currentByte |= (((int) (value >> (bitsToPush - chunk))) & mask) << (bitsUnused - chunk); + + sendOnFullByte(); + + bitsToPush -= chunk; + bitsUnused -= chunk; + } } - } - - /** - * Flush the current byte even if there are remaining unused bits left - * @throws IOException On write error - */ - public void flush() throws IOException { - if (bitsUnused < 8) { - stream.write(currentByte); + + private void sendOnFullByte() throws IOException { + if (bitsUnused == 0) { + stream.write(currentByte); + bitsUnused = 8; + currentByte = 0; + } } - bitsUnused = 8; - currentByte = 0; - } + /** + * Flush the current byte even if there are remaining unused bits left + * + * @throws IOException On write error + */ + public void flush() throws IOException { + if (bitsUnused < 8) { + stream.write(currentByte); + } + + bitsUnused = 8; + currentByte = 0; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ByteBufferInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ByteBufferInputStream.java index df86ec61f..39ed6f888 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ByteBufferInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ByteBufferInputStream.java @@ -8,37 +8,37 @@ * A byte buffer exposed as an input stream. */ public class ByteBufferInputStream extends InputStream { - private final ByteBuffer buffer; + private final ByteBuffer buffer; - /** - * @param buffer The buffer to read from. - */ - public ByteBufferInputStream(ByteBuffer buffer) { - this.buffer = buffer; - } + /** + * @param buffer The buffer to read from. + */ + public ByteBufferInputStream(ByteBuffer buffer) { + this.buffer = buffer; + } - @Override - public int read() throws IOException { - if (buffer.hasRemaining()) { - return buffer.get() & 0xFF; - } else { - return -1; + @Override + public int read() throws IOException { + if (buffer.hasRemaining()) { + return buffer.get() & 0xFF; + } else { + return -1; + } } - } - @Override - public int read(byte[] array, int offset, int length) throws IOException { - if (buffer.hasRemaining()) { - int chunk = Math.min(buffer.remaining(), length); - buffer.get(array, offset, length); - return chunk; - } else { - return -1; + @Override + public int read(byte[] array, int offset, int length) throws IOException { + if (buffer.hasRemaining()) { + int chunk = Math.min(buffer.remaining(), length); + buffer.get(array, offset, length); + return chunk; + } else { + return -1; + } } - } - @Override - public int available() throws IOException { - return buffer.remaining(); - } + @Override + public int available() throws IOException { + return buffer.remaining(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ByteBufferOutputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ByteBufferOutputStream.java index 0f3f4f599..90ce11f53 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ByteBufferOutputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ByteBufferOutputStream.java @@ -8,22 +8,22 @@ * A byte buffer wrapped in an output stream. */ public class ByteBufferOutputStream extends OutputStream { - private final ByteBuffer buffer; + private final ByteBuffer buffer; - /** - * @param buffer The underlying byte buffer - */ - public ByteBufferOutputStream(ByteBuffer buffer) { - this.buffer = buffer; - } + /** + * @param buffer The underlying byte buffer + */ + public ByteBufferOutputStream(ByteBuffer buffer) { + this.buffer = buffer; + } - @Override - public void write(int b) throws IOException { - buffer.put((byte) b); - } + @Override + public void write(int b) throws IOException { + buffer.put((byte) b); + } - @Override - public void write(byte[] b, int off, int len) throws IOException { - buffer.put(b, off, len); - } + @Override + public void write(byte[] b, int off, int len) throws IOException { + buffer.put(b, off, len); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ChainedInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ChainedInputStream.java index b3a2251f6..4d9e1de0e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ChainedInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ChainedInputStream.java @@ -7,106 +7,106 @@ * Input stream which can swap the underlying input stream if the current one ends. */ public class ChainedInputStream extends InputStream { - private final Provider provider; - private InputStream currentStream; - private boolean streamEnded; - - /** - * @param provider Provider for input streams to chain. - */ - public ChainedInputStream(Provider provider) { - this.provider = provider; - } - - private boolean loadNextStream() throws IOException { - if (!streamEnded) { - close(); - - currentStream = provider.next(); - - if (currentStream == null) { - streamEnded = true; - } + private final Provider provider; + private InputStream currentStream; + private boolean streamEnded; + + /** + * @param provider Provider for input streams to chain. + */ + public ChainedInputStream(Provider provider) { + this.provider = provider; } - return !streamEnded; - } + private boolean loadNextStream() throws IOException { + if (!streamEnded) { + close(); - @Override - public int read() throws IOException { - if (streamEnded || (currentStream == null && !loadNextStream())) { - return -1; - } + currentStream = provider.next(); - int result; - int emptyStreamCount = 0; + if (currentStream == null) { + streamEnded = true; + } + } - while ((result = currentStream.read()) == -1 && ++emptyStreamCount < 5) { - if (!loadNextStream()) { - return -1; - } + return !streamEnded; } - return result; - } + @Override + public int read() throws IOException { + if (streamEnded || (currentStream == null && !loadNextStream())) { + return -1; + } - @Override - public int read(byte[] buffer, int offset, int length) throws IOException { - if (streamEnded || (currentStream == null && !loadNextStream())) { - return -1; - } + int result; + int emptyStreamCount = 0; - int result; - int emptyStreamCount = 0; + while ((result = currentStream.read()) == -1 && ++emptyStreamCount < 5) { + if (!loadNextStream()) { + return -1; + } + } - while ((result = currentStream.read(buffer, offset, length)) == -1 && ++emptyStreamCount < 5) { - if (!loadNextStream()) { - return -1; - } + return result; } - return result; - } + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + if (streamEnded || (currentStream == null && !loadNextStream())) { + return -1; + } - @Override - public long skip(long distance) throws IOException { - if (streamEnded || (currentStream == null && !loadNextStream())) { - return -1; - } + int result; + int emptyStreamCount = 0; - long result; - int emptyStreamCount = 0; + while ((result = currentStream.read(buffer, offset, length)) == -1 && ++emptyStreamCount < 5) { + if (!loadNextStream()) { + return -1; + } + } - while ((result = currentStream.skip(distance)) == 0 && ++emptyStreamCount < 5) { - if (!loadNextStream()) { - return 0; - } + return result; } - return result; - } + @Override + public long skip(long distance) throws IOException { + if (streamEnded || (currentStream == null && !loadNextStream())) { + return -1; + } - @Override - public void close() throws IOException { - if (currentStream != null) { - currentStream.close(); - currentStream = null; + long result; + int emptyStreamCount = 0; + + while ((result = currentStream.skip(distance)) == 0 && ++emptyStreamCount < 5) { + if (!loadNextStream()) { + return 0; + } + } + + return result; + } + + @Override + public void close() throws IOException { + if (currentStream != null) { + currentStream.close(); + currentStream = null; + } } - } - @Override - public boolean markSupported() { - return false; - } + @Override + public boolean markSupported() { + return false; + } - /** - * Provider for next input stream of a chained stream. - */ - public interface Provider { /** - * @return Next input stream, null to cause EOF on the chained stream. - * @throws IOException On read error. + * Provider for next input stream of a chained stream. */ - InputStream next() throws IOException; - } + public interface Provider { + /** + * @return Next input stream, null to cause EOF on the chained stream. + * @throws IOException On read error. + */ + InputStream next() throws IOException; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/DetachedByteChannel.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/DetachedByteChannel.java index e6083bc5c..be9f7101d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/DetachedByteChannel.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/DetachedByteChannel.java @@ -9,32 +9,32 @@ * Creates a readable byte channel which can be closed without closing the underlying channel. */ public class DetachedByteChannel implements ReadableByteChannel { - private final ReadableByteChannel delegate; - private boolean closed; + private final ReadableByteChannel delegate; + private boolean closed; - /** - * @param delegate The underlying channel - */ - public DetachedByteChannel(ReadableByteChannel delegate) { - this.delegate = delegate; - } - - @Override - public int read(ByteBuffer output) throws IOException { - if (closed) { - throw new ClosedChannelException(); + /** + * @param delegate The underlying channel + */ + public DetachedByteChannel(ReadableByteChannel delegate) { + this.delegate = delegate; } - return delegate.read(output); - } + @Override + public int read(ByteBuffer output) throws IOException { + if (closed) { + throw new ClosedChannelException(); + } - @Override - public boolean isOpen() { - return !closed && delegate.isOpen(); - } + return delegate.read(output); + } - @Override - public void close() throws IOException { - closed = true; - } + @Override + public boolean isOpen() { + return !closed && delegate.isOpen(); + } + + @Override + public void close() throws IOException { + closed = true; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/DirectBufferStreamBroker.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/DirectBufferStreamBroker.java index 56400f679..072804996 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/DirectBufferStreamBroker.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/DirectBufferStreamBroker.java @@ -9,103 +9,103 @@ * repeated several times, as it supports resetting. */ public class DirectBufferStreamBroker { - private final byte[] copyBuffer; - private final int initialSize; - private int readByteCount; - private ByteBuffer currentBuffer; - - /** - * @param initialSize Initial size of the underlying direct buffer. - */ - public DirectBufferStreamBroker(int initialSize) { - this.initialSize = initialSize; - this.copyBuffer = new byte[512]; - this.currentBuffer = ByteBuffer.allocateDirect(initialSize); - } - - /** - * Reset the buffer to its initial size. - */ - public void resetAndCompact() { - currentBuffer = ByteBuffer.allocateDirect(initialSize); - } - - /** - * Clear the underlying buffer. - */ - public void clear() { - currentBuffer.clear(); - } - - /** - * @return A duplicate of the underlying buffer. - */ - public ByteBuffer getBuffer() { - ByteBuffer buffer = currentBuffer.duplicate(); - buffer.flip(); - return buffer; - } - - public boolean isTruncated() { - return currentBuffer.position() < readByteCount; - } - - /** - * Copies the final state after a {@link #consumeNext(InputStream, int, int)} operation into a new byte array. - * - * @return New byte array containing consumed data. - */ - public byte[] extractBytes() { - byte[] data = new byte[currentBuffer.position()]; - currentBuffer.position(0); - currentBuffer.get(data, 0, data.length); - return data; - } - - /** - * Consume an entire stream and append it into the buffer (or clear first if clear parameter is true). - * - * @param inputStream The input stream to fully consume. - * @param maximumSavedBytes Maximum number of bytes to save internally. If this is exceeded, it will continue reading - * and discarding until maximum read byte count is reached. - * @param maximumReadBytes Maximum number of bytes to read. - * @return If stream was fully read before {@code maximumReadBytes} was reached, returns {@code true}. Returns - * {@code false} if the number of bytes read is {@code maximumReadBytes}, even if no more data is left in the - * stream. - * @throws IOException On read error - */ - public boolean consumeNext(InputStream inputStream, int maximumSavedBytes, int maximumReadBytes) throws IOException { - currentBuffer.clear(); - readByteCount = 0; - - ensureCapacity(Math.min(maximumSavedBytes, inputStream.available())); - - while (readByteCount < maximumReadBytes) { - int maximumReadFragment = Math.min(copyBuffer.length, maximumReadBytes - readByteCount); - int fragmentLength = inputStream.read(copyBuffer, 0, maximumReadFragment); - - if (fragmentLength == -1) { - return true; - } - - int bytesToSave = Math.min(fragmentLength, maximumSavedBytes - readByteCount); - - if (bytesToSave > 0) { - ensureCapacity(currentBuffer.position() + bytesToSave); - currentBuffer.put(copyBuffer, 0, bytesToSave); - } + private final byte[] copyBuffer; + private final int initialSize; + private int readByteCount; + private ByteBuffer currentBuffer; + + /** + * @param initialSize Initial size of the underlying direct buffer. + */ + public DirectBufferStreamBroker(int initialSize) { + this.initialSize = initialSize; + this.copyBuffer = new byte[512]; + this.currentBuffer = ByteBuffer.allocateDirect(initialSize); } - return false; - } + /** + * Reset the buffer to its initial size. + */ + public void resetAndCompact() { + currentBuffer = ByteBuffer.allocateDirect(initialSize); + } + + /** + * Clear the underlying buffer. + */ + public void clear() { + currentBuffer.clear(); + } + + /** + * @return A duplicate of the underlying buffer. + */ + public ByteBuffer getBuffer() { + ByteBuffer buffer = currentBuffer.duplicate(); + buffer.flip(); + return buffer; + } + + public boolean isTruncated() { + return currentBuffer.position() < readByteCount; + } + + /** + * Copies the final state after a {@link #consumeNext(InputStream, int, int)} operation into a new byte array. + * + * @return New byte array containing consumed data. + */ + public byte[] extractBytes() { + byte[] data = new byte[currentBuffer.position()]; + currentBuffer.position(0); + currentBuffer.get(data, 0, data.length); + return data; + } + + /** + * Consume an entire stream and append it into the buffer (or clear first if clear parameter is true). + * + * @param inputStream The input stream to fully consume. + * @param maximumSavedBytes Maximum number of bytes to save internally. If this is exceeded, it will continue reading + * and discarding until maximum read byte count is reached. + * @param maximumReadBytes Maximum number of bytes to read. + * @return If stream was fully read before {@code maximumReadBytes} was reached, returns {@code true}. Returns + * {@code false} if the number of bytes read is {@code maximumReadBytes}, even if no more data is left in the + * stream. + * @throws IOException On read error + */ + public boolean consumeNext(InputStream inputStream, int maximumSavedBytes, int maximumReadBytes) throws IOException { + currentBuffer.clear(); + readByteCount = 0; + + ensureCapacity(Math.min(maximumSavedBytes, inputStream.available())); + + while (readByteCount < maximumReadBytes) { + int maximumReadFragment = Math.min(copyBuffer.length, maximumReadBytes - readByteCount); + int fragmentLength = inputStream.read(copyBuffer, 0, maximumReadFragment); + + if (fragmentLength == -1) { + return true; + } + + int bytesToSave = Math.min(fragmentLength, maximumSavedBytes - readByteCount); + + if (bytesToSave > 0) { + ensureCapacity(currentBuffer.position() + bytesToSave); + currentBuffer.put(copyBuffer, 0, bytesToSave); + } + } + + return false; + } - private void ensureCapacity(int capacity) { - if (capacity > currentBuffer.capacity()) { - ByteBuffer newBuffer = ByteBuffer.allocateDirect(currentBuffer.capacity() << 1); - currentBuffer.flip(); + private void ensureCapacity(int capacity) { + if (capacity > currentBuffer.capacity()) { + ByteBuffer newBuffer = ByteBuffer.allocateDirect(currentBuffer.capacity() << 1); + currentBuffer.flip(); - newBuffer.put(currentBuffer); - currentBuffer = newBuffer; + newBuffer.put(currentBuffer); + currentBuffer = newBuffer; + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/EmptyInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/EmptyInputStream.java index 03f137b41..0454947f6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/EmptyInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/EmptyInputStream.java @@ -6,15 +6,15 @@ * Represents an empty input stream. */ public class EmptyInputStream extends InputStream { - public static final EmptyInputStream INSTANCE = new EmptyInputStream(); + public static final EmptyInputStream INSTANCE = new EmptyInputStream(); - @Override - public int available() { - return 0; - } + @Override + public int available() { + return 0; + } - @Override - public int read() { - return -1; - } + @Override + public int read() { + return -1; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ExtendedBufferedInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ExtendedBufferedInputStream.java index 4d3b78146..18e0a43ff 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ExtendedBufferedInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ExtendedBufferedInputStream.java @@ -8,33 +8,33 @@ * discarding the buffer. */ public class ExtendedBufferedInputStream extends BufferedInputStream { - /** - * @param in Underlying input stream - */ - public ExtendedBufferedInputStream(InputStream in) { - super(in); - } + /** + * @param in Underlying input stream + */ + public ExtendedBufferedInputStream(InputStream in) { + super(in); + } - /** - * @param in Underlying input stream - * @param size Size of the buffer - */ - public ExtendedBufferedInputStream(InputStream in, int size) { - super(in, size); - } + /** + * @param in Underlying input stream + * @param size Size of the buffer + */ + public ExtendedBufferedInputStream(InputStream in, int size) { + super(in, size); + } - /** - * @return The number of bytes left in the buffer. This is useful for calculating the actual position in the buffer - * if the position in the underlying buffer is known. - */ - public int getBufferedByteCount() { - return count - pos; - } + /** + * @return The number of bytes left in the buffer. This is useful for calculating the actual position in the buffer + * if the position in the underlying buffer is known. + */ + public int getBufferedByteCount() { + return count - pos; + } - /** - * Discard the remaining buffer. This should be called after seek has been performed on the underlying stream. - */ - public void discardBuffer() { - pos = count; - } + /** + * Discard the remaining buffer. This should be called after seek has been performed on the underlying stream. + */ + public void discardBuffer() { + pos = count; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/GreedyInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/GreedyInputStream.java index 0bb372bb8..c4abdb82c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/GreedyInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/GreedyInputStream.java @@ -8,44 +8,44 @@ * Input stream wrapper which reads or skips until EOF or requested length. */ public class GreedyInputStream extends FilterInputStream { - /** - * @param in Underlying input stream. - */ - public GreedyInputStream(InputStream in) { - super(in); - } - - @Override - public int read(byte[] buffer, int offset, int length) throws IOException { - int read = 0; - - while (read < length) { - int chunk = in.read(buffer, offset + read, length - read); - if (chunk == -1) { - return read == 0 ? -1 : read; - } - read += chunk; + /** + * @param in Underlying input stream. + */ + public GreedyInputStream(InputStream in) { + super(in); } - return read; - } + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + int read = 0; - @Override - public long skip(long maximum) throws IOException { - long skipped = 0; - - while (skipped < maximum) { - long chunk = in.skip(maximum - skipped); - if (chunk == 0) { - if (in.read() == -1) { - break; - } else { - chunk = 1; + while (read < length) { + int chunk = in.read(buffer, offset + read, length - read); + if (chunk == -1) { + return read == 0 ? -1 : read; + } + read += chunk; } - } - skipped += chunk; + + return read; } - return skipped; - } + @Override + public long skip(long maximum) throws IOException { + long skipped = 0; + + while (skipped < maximum) { + long chunk = in.skip(maximum - skipped); + if (chunk == 0) { + if (in.read() == -1) { + break; + } else { + chunk = 1; + } + } + skipped += chunk; + } + + return skipped; + } } 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 b22b008c3..c9a3099a2 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 @@ -5,13 +5,7 @@ import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; import com.sedmelluq.discord.lavaplayer.tools.http.ExtendedHttpClientBuilder; -import org.apache.http.ConnectionClosedException; -import org.apache.http.Header; -import org.apache.http.HttpHeaders; -import org.apache.http.HttpRequest; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.NoHttpResponseException; +import org.apache.http.*; import org.apache.http.client.CookieStore; import org.apache.http.client.RedirectStrategy; import org.apache.http.client.config.CookieSpecs; @@ -28,7 +22,6 @@ import org.slf4j.LoggerFactory; import javax.net.ssl.SSLException; - import java.io.IOException; import java.net.SocketException; import java.net.SocketTimeoutException; @@ -42,272 +35,272 @@ * Tools for working with HttpClient */ public class HttpClientTools { - private static final Logger log = LoggerFactory.getLogger(HttpClientTools.class); - - public static final RequestConfig DEFAULT_REQUEST_CONFIG = RequestConfig.custom() - .setConnectTimeout(3000) - .setCookieSpec(CookieSpecs.STANDARD) - .build(); - - private static final RequestConfig NO_COOKIES_REQUEST_CONFIG = RequestConfig.custom() - .setConnectTimeout(3000) - .setCookieSpec(CookieSpecs.IGNORE_COOKIES) - .build(); - - /** - * @return An HttpClientBuilder which uses the same cookie store for all clients - */ - public static HttpClientBuilder createSharedCookiesHttpBuilder() { - return createHttpBuilder(DEFAULT_REQUEST_CONFIG); - } - - /** - * @return Default HTTP interface manager with thread-local context - */ - public static HttpInterfaceManager createDefaultThreadLocalManager() { - return new ThreadLocalHttpInterfaceManager(createSharedCookiesHttpBuilder(), DEFAULT_REQUEST_CONFIG); - } - - /** - * @return HTTP interface manager with thread-local context, ignores cookies - */ - public static HttpInterfaceManager createCookielessThreadLocalManager() { - return new ThreadLocalHttpInterfaceManager(createHttpBuilder(NO_COOKIES_REQUEST_CONFIG), NO_COOKIES_REQUEST_CONFIG); - } - - private static HttpClientBuilder createHttpBuilder(RequestConfig requestConfig) { - CookieStore cookieStore = new BasicCookieStore(); - - return new ExtendedHttpClientBuilder() - .setDefaultCookieStore(cookieStore) - .setRetryHandler(NoResponseRetryHandler.RETRY_INSTANCE) - .setDefaultRequestConfig(requestConfig); - } - - /** - * A redirect strategy which does not follow any redirects. - */ - public static class NoRedirectsStrategy implements RedirectStrategy { - @Override - public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) { - return false; + private static final Logger log = LoggerFactory.getLogger(HttpClientTools.class); + + public static final RequestConfig DEFAULT_REQUEST_CONFIG = RequestConfig.custom() + .setConnectTimeout(3000) + .setCookieSpec(CookieSpecs.STANDARD) + .build(); + + private static final RequestConfig NO_COOKIES_REQUEST_CONFIG = RequestConfig.custom() + .setConnectTimeout(3000) + .setCookieSpec(CookieSpecs.IGNORE_COOKIES) + .build(); + + /** + * @return An HttpClientBuilder which uses the same cookie store for all clients + */ + public static HttpClientBuilder createSharedCookiesHttpBuilder() { + return createHttpBuilder(DEFAULT_REQUEST_CONFIG); } - @Override - public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context) { - return null; + /** + * @return Default HTTP interface manager with thread-local context + */ + public static HttpInterfaceManager createDefaultThreadLocalManager() { + return new ThreadLocalHttpInterfaceManager(createSharedCookiesHttpBuilder(), DEFAULT_REQUEST_CONFIG); } - } - - /** - * @param requestUrl URL of the original request. - * @param response Response object. - * @return A redirect location if the status code indicates a redirect and the Location header is present. - */ - public static String getRedirectLocation(String requestUrl, HttpResponse response) { - if (!isRedirectStatus(response.getStatusLine().getStatusCode())) { - return null; + + /** + * @return HTTP interface manager with thread-local context, ignores cookies + */ + public static HttpInterfaceManager createCookielessThreadLocalManager() { + return new ThreadLocalHttpInterfaceManager(createHttpBuilder(NO_COOKIES_REQUEST_CONFIG), NO_COOKIES_REQUEST_CONFIG); } - Header header = response.getFirstHeader("Location"); - if (header == null) { - return null; + private static HttpClientBuilder createHttpBuilder(RequestConfig requestConfig) { + CookieStore cookieStore = new BasicCookieStore(); + + return new ExtendedHttpClientBuilder() + .setDefaultCookieStore(cookieStore) + .setRetryHandler(NoResponseRetryHandler.RETRY_INSTANCE) + .setDefaultRequestConfig(requestConfig); } - String location = header.getValue(); + /** + * A redirect strategy which does not follow any redirects. + */ + public static class NoRedirectsStrategy implements RedirectStrategy { + @Override + public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) { + return false; + } + + @Override + public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context) { + return null; + } + } - try { - return new URI(requestUrl).resolve(location).toString(); - } catch (URISyntaxException e) { - log.debug("Failed to parse URI.", e); - return location; + /** + * @param requestUrl URL of the original request. + * @param response Response object. + * @return A redirect location if the status code indicates a redirect and the Location header is present. + */ + public static String getRedirectLocation(String requestUrl, HttpResponse response) { + if (!isRedirectStatus(response.getStatusLine().getStatusCode())) { + return null; + } + + Header header = response.getFirstHeader("Location"); + if (header == null) { + return null; + } + + String location = header.getValue(); + + try { + return new URI(requestUrl).resolve(location).toString(); + } catch (URISyntaxException e) { + log.debug("Failed to parse URI.", e); + return location; + } } - } - - private static boolean isRedirectStatus(int statusCode) { - switch (statusCode) { - case HttpStatus.SC_MOVED_PERMANENTLY: - case HttpStatus.SC_MOVED_TEMPORARILY: - case HttpStatus.SC_SEE_OTHER: - case HttpStatus.SC_TEMPORARY_REDIRECT: - return true; - default: - return false; + + private static boolean isRedirectStatus(int statusCode) { + switch (statusCode) { + case HttpStatus.SC_MOVED_PERMANENTLY: + case HttpStatus.SC_MOVED_TEMPORARILY: + case HttpStatus.SC_SEE_OTHER: + case HttpStatus.SC_TEMPORARY_REDIRECT: + return true; + default: + return false; + } } - } - - /** - * @param statusCode The status code of a response. - * @return True if this status code indicates a success with a response body - */ - public static boolean isSuccessWithContent(int statusCode) { - return statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_PARTIAL_CONTENT || - statusCode == HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION; - } - - /** - * @param response The response. - * @param context Additional string to include in exception message. - * @throws IOException if this status code indicates an error with a response body - */ - public static void assertSuccessWithContent(HttpResponse response, String context) throws IOException { - int statusCode = response.getStatusLine().getStatusCode(); - - if (!isSuccessWithContent(statusCode)) { - throw new IOException("Invalid status code for " + context + ": " + statusCode); + + /** + * @param statusCode The status code of a response. + * @return True if this status code indicates a success with a response body + */ + public static boolean isSuccessWithContent(int statusCode) { + return statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_PARTIAL_CONTENT || + statusCode == HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION; } - } - - /** - * @param response The response. - * @param context Additional string to include in exception message. - * @throws IOException if this status code indicates an error with a response body - */ - public static void assertSuccessWithRedirectContent(HttpResponse response, String context) throws IOException { - int statusCode = response.getStatusLine().getStatusCode(); - - if (!isRedirectStatus(statusCode)) { - throw new IOException("Invalid status code for " + context + ": " + statusCode); + + /** + * @param response The response. + * @param context Additional string to include in exception message. + * @throws IOException if this status code indicates an error with a response body + */ + public static void assertSuccessWithContent(HttpResponse response, String context) throws IOException { + int statusCode = response.getStatusLine().getStatusCode(); + + if (!isSuccessWithContent(statusCode)) { + throw new IOException("Invalid status code for " + context + ": " + statusCode); + } } - } - - public static String getRawContentType(HttpResponse response) { - Header header = response.getFirstHeader(HttpHeaders.CONTENT_TYPE); - return header != null ? header.getValue() : null; - } - - public static boolean hasJsonContentType(HttpResponse response) { - String contentType = getRawContentType(response); - return contentType != null && contentType.startsWith(ContentType.APPLICATION_JSON.getMimeType()); - } - - public static void assertJsonContentType(HttpResponse response) throws IOException { - if (!HttpClientTools.hasJsonContentType(response)) { - throw ExceptionTools.throwWithDebugInfo( - log, - null, - "Expected JSON content type, got " + HttpClientTools.getRawContentType(response), - "responseContent", - EntityUtils.toString(response.getEntity()) - ); + + /** + * @param response The response. + * @param context Additional string to include in exception message. + * @throws IOException if this status code indicates an error with a response body + */ + public static void assertSuccessWithRedirectContent(HttpResponse response, String context) throws IOException { + int statusCode = response.getStatusLine().getStatusCode(); + + if (!isRedirectStatus(statusCode)) { + throw new IOException("Invalid status code for " + context + ": " + statusCode); + } } - } - - /** - * @param exception Exception to check. - * @return True if retrying to connect after receiving this exception is likely to succeed. - */ - public static boolean isRetriableNetworkException(Throwable exception) { - return isConnectionResetException(exception) || - isSocketTimeoutException(exception) || - isIncorrectSslShutdownException(exception) || - isPrematureEndException(exception) || - isRetriableConscryptException(exception) || - isRetriableNestedSslException(exception); - } - - public static boolean isConnectionResetException(Throwable exception) { - return (exception instanceof SocketException || exception instanceof SSLException) - && "Connection reset".equals(exception.getMessage()); - } - - private static boolean isSocketTimeoutException(Throwable exception) { - return (exception instanceof SocketTimeoutException || exception instanceof SSLException) - && "Read timed out".equals(exception.getMessage()); - } - - private static boolean isIncorrectSslShutdownException(Throwable exception) { - return exception instanceof SSLException && "SSL peer shut down incorrectly".equals(exception.getMessage()); - } - - private static boolean isPrematureEndException(Throwable exception) { - return exception instanceof ConnectionClosedException && exception.getMessage() != null && - exception.getMessage().startsWith("Premature end of Content-Length"); - } - - private static boolean isRetriableConscryptException(Throwable exception) { - if (exception instanceof SSLException) { - String message = exception.getMessage(); - - if (message != null && message.contains("I/O error during system call")) { - return message.contains("No error") || - message.contains("Connection reset by peer") || - message.contains("Connection timed out"); - } + + public static String getRawContentType(HttpResponse response) { + Header header = response.getFirstHeader(HttpHeaders.CONTENT_TYPE); + return header != null ? header.getValue() : null; } - return false; - } - - private static boolean isRetriableNestedSslException(Throwable exception) { - return exception instanceof SSLException && isRetriableNetworkException(exception.getCause()); - } - - /** - * Executes an HTTP request and returns the response as a JsonBrowser instance. - * - * @param httpInterface HTTP interface to use for the request. - * @param request Request to perform. - * @return Response as a JsonBrowser instance. null in case of 404. - * @throws IOException On network error or for non-200 response code. - */ - public static JsonBrowser fetchResponseAsJson(HttpInterface httpInterface, HttpUriRequest request) throws IOException { - try (CloseableHttpResponse response = httpInterface.execute(request)) { - int statusCode = response.getStatusLine().getStatusCode(); - - if (statusCode == HttpStatus.SC_NOT_FOUND) { - return null; - } else if (!isSuccessWithContent(statusCode)) { - throw new FriendlyException("Server responded with an error.", SUSPICIOUS, - new IllegalStateException("Response code from channel info is " + statusCode)); - } - - return JsonBrowser.parse(response.getEntity().getContent()); + public static boolean hasJsonContentType(HttpResponse response) { + String contentType = getRawContentType(response); + return contentType != null && contentType.startsWith(ContentType.APPLICATION_JSON.getMimeType()); } - } - - /** - * Executes an HTTP request and returns the response as an array of lines. - * - * @param httpInterface HTTP interface to use for the request. - * @param request Request to perform. - * @param name Name of the operation to include in exception messages. - * @return Array of lines from the response - * @throws IOException On network error or for non-200 response code. - */ - public static String[] fetchResponseLines(HttpInterface httpInterface, HttpUriRequest request, String name) throws IOException { - try (CloseableHttpResponse response = httpInterface.execute(request)) { - int statusCode = response.getStatusLine().getStatusCode(); - if (!isSuccessWithContent(statusCode)) { - throw new IOException("Unexpected response code " + statusCode + " from " + name); - } - - return DataFormatTools.streamToLines(response.getEntity().getContent(), StandardCharsets.UTF_8); + + public static void assertJsonContentType(HttpResponse response) throws IOException { + if (!HttpClientTools.hasJsonContentType(response)) { + throw ExceptionTools.throwWithDebugInfo( + log, + null, + "Expected JSON content type, got " + HttpClientTools.getRawContentType(response), + "responseContent", + EntityUtils.toString(response.getEntity()) + ); + } + } + + /** + * @param exception Exception to check. + * @return True if retrying to connect after receiving this exception is likely to succeed. + */ + public static boolean isRetriableNetworkException(Throwable exception) { + return isConnectionResetException(exception) || + isSocketTimeoutException(exception) || + isIncorrectSslShutdownException(exception) || + isPrematureEndException(exception) || + isRetriableConscryptException(exception) || + isRetriableNestedSslException(exception); + } + + public static boolean isConnectionResetException(Throwable exception) { + return (exception instanceof SocketException || exception instanceof SSLException) + && "Connection reset".equals(exception.getMessage()); + } + + private static boolean isSocketTimeoutException(Throwable exception) { + return (exception instanceof SocketTimeoutException || exception instanceof SSLException) + && "Read timed out".equals(exception.getMessage()); + } + + private static boolean isIncorrectSslShutdownException(Throwable exception) { + return exception instanceof SSLException && "SSL peer shut down incorrectly".equals(exception.getMessage()); } - } - - /** - * @param response Http response to get the header value from. - * @param name Name of the header. - * @return Value if header was present, null otherwise. - */ - public static String getHeaderValue(HttpResponse response, String name) { - Header header = response.getFirstHeader(name); - return header != null ? header.getValue() : null; - } - - private static class NoResponseRetryHandler extends DefaultHttpRequestRetryHandler { - private static final NoResponseRetryHandler RETRY_INSTANCE = new NoResponseRetryHandler(); - - @Override - public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { - boolean retry = super.retryRequest(exception, executionCount, context); - - if (!retry && exception instanceof NoHttpResponseException && executionCount < 5) { - return true; - } else { - return retry; - } + + private static boolean isPrematureEndException(Throwable exception) { + return exception instanceof ConnectionClosedException && exception.getMessage() != null && + exception.getMessage().startsWith("Premature end of Content-Length"); + } + + private static boolean isRetriableConscryptException(Throwable exception) { + if (exception instanceof SSLException) { + String message = exception.getMessage(); + + if (message != null && message.contains("I/O error during system call")) { + return message.contains("No error") || + message.contains("Connection reset by peer") || + message.contains("Connection timed out"); + } + } + + return false; + } + + private static boolean isRetriableNestedSslException(Throwable exception) { + return exception instanceof SSLException && isRetriableNetworkException(exception.getCause()); + } + + /** + * Executes an HTTP request and returns the response as a JsonBrowser instance. + * + * @param httpInterface HTTP interface to use for the request. + * @param request Request to perform. + * @return Response as a JsonBrowser instance. null in case of 404. + * @throws IOException On network error or for non-200 response code. + */ + public static JsonBrowser fetchResponseAsJson(HttpInterface httpInterface, HttpUriRequest request) throws IOException { + try (CloseableHttpResponse response = httpInterface.execute(request)) { + int statusCode = response.getStatusLine().getStatusCode(); + + if (statusCode == HttpStatus.SC_NOT_FOUND) { + return null; + } else if (!isSuccessWithContent(statusCode)) { + throw new FriendlyException("Server responded with an error.", SUSPICIOUS, + new IllegalStateException("Response code from channel info is " + statusCode)); + } + + return JsonBrowser.parse(response.getEntity().getContent()); + } + } + + /** + * Executes an HTTP request and returns the response as an array of lines. + * + * @param httpInterface HTTP interface to use for the request. + * @param request Request to perform. + * @param name Name of the operation to include in exception messages. + * @return Array of lines from the response + * @throws IOException On network error or for non-200 response code. + */ + public static String[] fetchResponseLines(HttpInterface httpInterface, HttpUriRequest request, String name) throws IOException { + try (CloseableHttpResponse response = httpInterface.execute(request)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (!isSuccessWithContent(statusCode)) { + throw new IOException("Unexpected response code " + statusCode + " from " + name); + } + + return DataFormatTools.streamToLines(response.getEntity().getContent(), StandardCharsets.UTF_8); + } + } + + /** + * @param response Http response to get the header value from. + * @param name Name of the header. + * @return Value if header was present, null otherwise. + */ + public static String getHeaderValue(HttpResponse response, String name) { + Header header = response.getFirstHeader(name); + return header != null ? header.getValue() : null; + } + + private static class NoResponseRetryHandler extends DefaultHttpRequestRetryHandler { + private static final NoResponseRetryHandler RETRY_INSTANCE = new NoResponseRetryHandler(); + + @Override + public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { + boolean retry = super.retryRequest(exception, executionCount, context); + + if (!retry && exception instanceof NoHttpResponseException && executionCount < 5) { + return true; + } else { + return retry; + } + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpConfigurable.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpConfigurable.java index 5c823675c..8e28c8567 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpConfigurable.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpConfigurable.java @@ -10,13 +10,13 @@ * Represents a class where HTTP request configuration can be changed. */ public interface HttpConfigurable { - /** - * @param configurator Function to reconfigure request config. - */ - void configureRequests(Function configurator); + /** + * @param configurator Function to reconfigure request config. + */ + void configureRequests(Function configurator); - /** - * @param configurator Function to reconfigure HTTP builder. - */ - void configureBuilder(Consumer configurator); + /** + * @param configurator Function to reconfigure HTTP builder. + */ + void configureBuilder(Consumer configurator); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpInterface.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpInterface.java index 3e2affd0e..00684b460 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpInterface.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpInterface.java @@ -2,135 +2,136 @@ import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools; import com.sedmelluq.discord.lavaplayer.tools.http.HttpContextFilter; -import java.io.Closeable; -import java.io.IOException; -import java.net.URI; -import java.util.List; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.impl.client.CloseableHttpClient; +import java.io.Closeable; +import java.io.IOException; +import java.net.URI; +import java.util.List; + /** * An HTTP interface for performing HTTP requests in one specific thread. This also means it is not thread safe and should * not be used in a thread it was not obtained in. For multi-thread use {@link HttpInterfaceManager#getInterface()}, * should be called in each thread separately. */ public class HttpInterface implements Closeable { - private final CloseableHttpClient client; - private final HttpClientContext context; - private final boolean ownedClient; - private final HttpContextFilter filter; - private HttpUriRequest lastRequest; - private boolean available; - - /** - * @param client The http client instance used. - * @param context The http context instance used. - * @param ownedClient True if the client should be closed when this instance is closed. - * @param filter - */ - public HttpInterface(CloseableHttpClient client, HttpClientContext context, boolean ownedClient, - HttpContextFilter filter) { - - this.client = client; - this.context = context; - this.ownedClient = ownedClient; - this.filter = filter; - this.available = true; - } - - /** - * Acquire exclusive use of this instance. This is released by calling close. - * - * @return True if this instance was not exclusively used when this method was called. - */ - public boolean acquire() { - if (!available) { - return false; + private final CloseableHttpClient client; + private final HttpClientContext context; + private final boolean ownedClient; + private final HttpContextFilter filter; + private HttpUriRequest lastRequest; + private boolean available; + + /** + * @param client The http client instance used. + * @param context The http context instance used. + * @param ownedClient True if the client should be closed when this instance is closed. + * @param filter + */ + public HttpInterface(CloseableHttpClient client, HttpClientContext context, boolean ownedClient, + HttpContextFilter filter) { + + this.client = client; + this.context = context; + this.ownedClient = ownedClient; + this.filter = filter; + this.available = true; } - filter.onContextOpen(context); - available = false; - return true; - } - - /** - * Executes the given query using the client and context stored in this instance. - * - * @param request The request to execute. - * @return Closeable response from the server. - * @throws IOException On network error. - */ - public CloseableHttpResponse execute(HttpUriRequest request) throws IOException { - boolean isRepeated = false; - - while (true) { - filter.onRequest(context, request, isRepeated); - - try { - CloseableHttpResponse response = client.execute(request, context); - lastRequest = request; - - if (!filter.onRequestResponse(context, request, response)) { - return response; + /** + * Acquire exclusive use of this instance. This is released by calling close. + * + * @return True if this instance was not exclusively used when this method was called. + */ + public boolean acquire() { + if (!available) { + return false; } - } catch (Throwable e) { - if (!filter.onRequestException(context, request, e)) { - if (e instanceof Error) { - throw (Error) e; - } else if (e instanceof RuntimeException) { - throw (RuntimeException) e; - } else //noinspection ConstantConditions - if (e instanceof IOException) { - throw (IOException) e; - } else { - throw new RuntimeException(e); - } + + filter.onContextOpen(context); + available = false; + return true; + } + + /** + * Executes the given query using the client and context stored in this instance. + * + * @param request The request to execute. + * @return Closeable response from the server. + * @throws IOException On network error. + */ + public CloseableHttpResponse execute(HttpUriRequest request) throws IOException { + boolean isRepeated = false; + + while (true) { + filter.onRequest(context, request, isRepeated); + + try { + CloseableHttpResponse response = client.execute(request, context); + lastRequest = request; + + if (!filter.onRequestResponse(context, request, response)) { + return response; + } + } catch (Throwable e) { + if (!filter.onRequestException(context, request, e)) { + if (e instanceof Error) { + throw (Error) e; + } else if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else //noinspection ConstantConditions + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new RuntimeException(e); + } + } else { + ExceptionTools.rethrowErrors(e); + } + } + + isRepeated = true; + } + } + + /** + * @return The final URL after redirects for the last processed request. Original URL if no redirects were performed. + * Null if no requests have been executed. Undefined state if last request threw an exception. + */ + public URI getFinalLocation() { + List redirectLocations = context.getRedirectLocations(); + + if (redirectLocations != null && !redirectLocations.isEmpty()) { + return redirectLocations.get(redirectLocations.size() - 1); } else { - ExceptionTools.rethrowErrors(e); + return lastRequest != null ? lastRequest.getURI() : null; } - } + } - isRepeated = true; + /** + * @return Http client context used by this interface. + */ + public HttpClientContext getContext() { + return context; } - } - - /** - * @return The final URL after redirects for the last processed request. Original URL if no redirects were performed. - * Null if no requests have been executed. Undefined state if last request threw an exception. - */ - public URI getFinalLocation() { - List redirectLocations = context.getRedirectLocations(); - - if (redirectLocations != null && !redirectLocations.isEmpty()) { - return redirectLocations.get(redirectLocations.size() - 1); - } else { - return lastRequest != null ? lastRequest.getURI() : null; + + /** + * @return Http client instance used by this instance. + */ + public CloseableHttpClient getHttpClient() { + return client; } - } - - /** - * @return Http client context used by this interface. - */ - public HttpClientContext getContext() { - return context; - } - - /** - * @return Http client instance used by this instance. - */ - public CloseableHttpClient getHttpClient() { - return client; - } - - @Override - public void close() throws IOException { - available = true; - filter.onContextClose(context); - - if (ownedClient) { - client.close(); + + @Override + public void close() throws IOException { + available = true; + filter.onContextClose(context); + + if (ownedClient) { + client.close(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpInterfaceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpInterfaceManager.java index c3bffb951..4264f5139 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpInterfaceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/HttpInterfaceManager.java @@ -1,14 +1,15 @@ package com.sedmelluq.discord.lavaplayer.tools.io; import com.sedmelluq.discord.lavaplayer.tools.http.ExtendedHttpConfigurable; + import java.io.Closeable; /** * A thread-safe manager for HTTP interfaces. */ public interface HttpInterfaceManager extends ExtendedHttpConfigurable, Closeable { - /** - * @return An HTTP interface for use by the current thread. - */ - HttpInterface getInterface(); + /** + * @return An HTTP interface for use by the current thread. + */ + HttpInterface getInterface(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/MessageInput.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/MessageInput.java index e89f20d16..1f09de6d8 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/MessageInput.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/MessageInput.java @@ -13,57 +13,58 @@ * An input for messages with their size known so unknown messages can be skipped. */ public class MessageInput { - private final CountingInputStream countingInputStream; - private final DataInputStream dataInputStream; - private int messageSize; - private int messageFlags; + private final CountingInputStream countingInputStream; + private final DataInputStream dataInputStream; + private int messageSize; + private int messageFlags; - /** - * @param inputStream Input stream to read from. - */ - public MessageInput(InputStream inputStream) { - this.countingInputStream = new CountingInputStream(inputStream); - this.dataInputStream = new DataInputStream(inputStream); - } + /** + * @param inputStream Input stream to read from. + */ + public MessageInput(InputStream inputStream) { + this.countingInputStream = new CountingInputStream(inputStream); + this.dataInputStream = new DataInputStream(inputStream); + } + + /** + * @return Data input for the next message. Note that it does not automatically skip over the last message if it was + * not fully read, for that purpose, skipRemainingBytes() should be explicitly called after reading every + * message. A null return value indicates the position where MessageOutput#finish() had written the end + * marker. + * @throws IOException On IO error + */ + public DataInput nextMessage() throws IOException { + int value = dataInputStream.readInt(); + messageFlags = (int) ((value & 0xC0000000L) >> 30L); + messageSize = value & 0x3FFFFFFF; - /** - * @return Data input for the next message. Note that it does not automatically skip over the last message if it was - * not fully read, for that purpose, skipRemainingBytes() should be explicitly called after reading every - * message. A null return value indicates the position where MessageOutput#finish() had written the end - * marker. - * @throws IOException On IO error - */ - public DataInput nextMessage() throws IOException { - int value = dataInputStream.readInt(); - messageFlags = (int) ((value & 0xC0000000L) >> 30L); - messageSize = value & 0x3FFFFFFF; + if (messageSize == 0) { + return null; + } - if (messageSize == 0) { - return null; + return new DataInputStream(new BoundedInputStream(countingInputStream, messageSize)); } - return new DataInputStream(new BoundedInputStream(countingInputStream, messageSize)); - } + /** + * @return Flags (values 0-3) of the last message for which nextMessage() was called. + */ + public int getMessageFlags() { + return messageFlags; + } - /** - * @return Flags (values 0-3) of the last message for which nextMessage() was called. - */ - public int getMessageFlags() { - return messageFlags; - } + /** + * Skip the remaining bytes of the last message returned from nextMessage(). This must be called if it is not certain + * that all of the bytes of the message were consumed. + * + * @throws IOException On IO error + */ + public void skipRemainingBytes() throws IOException { + long count = countingInputStream.resetByteCount(); - /** - * Skip the remaining bytes of the last message returned from nextMessage(). This must be called if it is not certain - * that all of the bytes of the message were consumed. - * @throws IOException On IO error - */ - public void skipRemainingBytes() throws IOException { - long count = countingInputStream.resetByteCount(); + if (count < messageSize) { + IOUtils.skipFully(dataInputStream, messageSize - count); + } - if (count < messageSize) { - IOUtils.skipFully(dataInputStream, messageSize - count); + messageSize = 0; } - - messageSize = 0; - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/MessageOutput.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/MessageOutput.java index 7f5d3f6bf..2e6abb6ae 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/MessageOutput.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/MessageOutput.java @@ -1,63 +1,62 @@ package com.sedmelluq.discord.lavaplayer.tools.io; -import java.io.ByteArrayOutputStream; -import java.io.DataOutput; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.OutputStream; +import java.io.*; /** * An output for a series of messages which each have sizes specified before the start of the message. Even when the * decoder does not recognize some of the messages, it can skip over the message since it knows its size in advance. */ public class MessageOutput { - private final OutputStream outputStream; - private final DataOutputStream dataOutputStream; - private final ByteArrayOutputStream messageByteOutput; - private final DataOutputStream messageDataOutput; + private final OutputStream outputStream; + private final DataOutputStream dataOutputStream; + private final ByteArrayOutputStream messageByteOutput; + private final DataOutputStream messageDataOutput; - /** - * @param outputStream Output stream to write the messages to - */ - public MessageOutput(OutputStream outputStream) { - this.outputStream = outputStream; - this.dataOutputStream = new DataOutputStream(outputStream); - this.messageByteOutput = new ByteArrayOutputStream(); - this.messageDataOutput = new DataOutputStream(messageByteOutput); - } + /** + * @param outputStream Output stream to write the messages to + */ + public MessageOutput(OutputStream outputStream) { + this.outputStream = outputStream; + this.dataOutputStream = new DataOutputStream(outputStream); + this.messageByteOutput = new ByteArrayOutputStream(); + this.messageDataOutput = new DataOutputStream(messageByteOutput); + } - /** - * @return Data output for a new message - */ - public DataOutput startMessage() { - messageByteOutput.reset(); - return messageDataOutput; - } + /** + * @return Data output for a new message + */ + public DataOutput startMessage() { + messageByteOutput.reset(); + return messageDataOutput; + } - /** - * Commit previously started message to the underlying output stream. - * @throws IOException On IO error - */ - public void commitMessage() throws IOException { - dataOutputStream.writeInt(messageByteOutput.size()); - messageByteOutput.writeTo(outputStream); - } + /** + * Commit previously started message to the underlying output stream. + * + * @throws IOException On IO error + */ + public void commitMessage() throws IOException { + dataOutputStream.writeInt(messageByteOutput.size()); + messageByteOutput.writeTo(outputStream); + } - /** - * Commit previously started message to the underlying output stream. - * @param flags Flags to use when committing the message (0-3). - * @throws IOException On IO error - */ - public void commitMessage(int flags) throws IOException { - dataOutputStream.writeInt(messageByteOutput.size() | flags << 30); - messageByteOutput.writeTo(outputStream); - } + /** + * Commit previously started message to the underlying output stream. + * + * @param flags Flags to use when committing the message (0-3). + * @throws IOException On IO error + */ + public void commitMessage(int flags) throws IOException { + dataOutputStream.writeInt(messageByteOutput.size() | flags << 30); + messageByteOutput.writeTo(outputStream); + } - /** - * Write an end marker to the stream so that decoder knows to return null at this position. - * @throws IOException On IO error - */ - public void finish() throws IOException { - dataOutputStream.writeInt(0); - } + /** + * Write an end marker to the stream so that decoder knows to return null at this position. + * + * @throws IOException On IO error + */ + public void finish() throws IOException { + dataOutputStream.writeInt(0); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/NonSeekableInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/NonSeekableInputStream.java index 6be24e820..8956d7446 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/NonSeekableInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/NonSeekableInputStream.java @@ -10,45 +10,45 @@ import java.util.List; public class NonSeekableInputStream extends SeekableInputStream { - private final CountingInputStream delegate; - - public NonSeekableInputStream(InputStream delegate) { - super(Units.CONTENT_LENGTH_UNKNOWN, 0); - this.delegate = new CountingInputStream(delegate); - } - - @Override - public long getPosition() { - return delegate.getByteCount(); - } - - @Override - protected void seekHard(long position) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean canSeekHard() { - return false; - } - - @Override - public List getTrackInfoProviders() { - return Collections.emptyList(); - } - - @Override - public int read() throws IOException { - return delegate.read(); - } - - @Override - public int read(byte[] buffer, int offset, int length) throws IOException { - return delegate.read(buffer, offset, length); - } - - @Override - public void close() throws IOException { - delegate.close(); - } + private final CountingInputStream delegate; + + public NonSeekableInputStream(InputStream delegate) { + super(Units.CONTENT_LENGTH_UNKNOWN, 0); + this.delegate = new CountingInputStream(delegate); + } + + @Override + public long getPosition() { + return delegate.getByteCount(); + } + + @Override + protected void seekHard(long position) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean canSeekHard() { + return false; + } + + @Override + public List getTrackInfoProviders() { + return Collections.emptyList(); + } + + @Override + public int read() throws IOException { + return delegate.read(); + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + return delegate.read(buffer, offset, length); + } + + @Override + public void close() throws IOException { + delegate.close(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/PersistentHttpStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/PersistentHttpStream.java index 0b4eb8860..8f4e09154 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/PersistentHttpStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/PersistentHttpStream.java @@ -27,284 +27,284 @@ * and using a closed stream will just reopen the connection. */ public class PersistentHttpStream extends SeekableInputStream implements AutoCloseable { - private static final Logger log = LoggerFactory.getLogger(PersistentHttpStream.class); - - private static final long MAX_SKIP_DISTANCE = 512L * 1024L; - - private final HttpInterface httpInterface; - protected final URI contentUrl; - private int lastStatusCode; - private CloseableHttpResponse currentResponse; - protected InputStream currentContent; - protected long position; - - /** - * @param httpInterface The HTTP interface to use for requests - * @param contentUrl The URL of the resource - * @param contentLength The length of the resource in bytes - */ - public PersistentHttpStream(HttpInterface httpInterface, URI contentUrl, Long contentLength) { - super(contentLength == null ? Units.CONTENT_LENGTH_UNKNOWN : contentLength, MAX_SKIP_DISTANCE); - - this.httpInterface = httpInterface; - this.contentUrl = contentUrl; - this.position = 0; - } - - /** - * Connect and return status code or return last status code if already connected. This causes the internal status - * code checker to be disabled, so non-success status codes will be returned instead of being thrown as they would - * be otherwise. - * - * @return The status code when connecting to the URL - * @throws IOException On IO error - */ - public int checkStatusCode() throws IOException { - connect(true); - - return lastStatusCode; - } - - /** - * @return An HTTP response if one is currently open. - */ - public HttpResponse getCurrentResponse() { - return currentResponse; - } - - protected URI getConnectUrl() { - return contentUrl; - } - - protected boolean useHeadersForRange() { - return true; - } - - private static boolean validateStatusCode(HttpResponse response, boolean returnOnServerError) { - int statusCode = response.getStatusLine().getStatusCode(); - if (returnOnServerError && statusCode >= HttpStatus.SC_INTERNAL_SERVER_ERROR) { - return false; - } else if (!isSuccessWithContent(statusCode)) { - throw new RuntimeException("Not success status code: " + statusCode); + private static final Logger log = LoggerFactory.getLogger(PersistentHttpStream.class); + + private static final long MAX_SKIP_DISTANCE = 512L * 1024L; + + private final HttpInterface httpInterface; + protected final URI contentUrl; + private int lastStatusCode; + private CloseableHttpResponse currentResponse; + protected InputStream currentContent; + protected long position; + + /** + * @param httpInterface The HTTP interface to use for requests + * @param contentUrl The URL of the resource + * @param contentLength The length of the resource in bytes + */ + public PersistentHttpStream(HttpInterface httpInterface, URI contentUrl, Long contentLength) { + super(contentLength == null ? Units.CONTENT_LENGTH_UNKNOWN : contentLength, MAX_SKIP_DISTANCE); + + this.httpInterface = httpInterface; + this.contentUrl = contentUrl; + this.position = 0; } - return true; - } - private HttpGet getConnectRequest() { - HttpGet request = new HttpGet(getConnectUrl()); + /** + * Connect and return status code or return last status code if already connected. This causes the internal status + * code checker to be disabled, so non-success status codes will be returned instead of being thrown as they would + * be otherwise. + * + * @return The status code when connecting to the URL + * @throws IOException On IO error + */ + public int checkStatusCode() throws IOException { + connect(true); + + return lastStatusCode; + } + + /** + * @return An HTTP response if one is currently open. + */ + public HttpResponse getCurrentResponse() { + return currentResponse; + } - if (position > 0 && useHeadersForRange()) { - request.setHeader(HttpHeaders.RANGE, "bytes=" + position + "-"); + protected URI getConnectUrl() { + return contentUrl; } - return request; - } + protected boolean useHeadersForRange() { + return true; + } - protected void connect(boolean skipStatusCheck) throws IOException { - if (currentResponse == null) { - for (int i = 1; i >= 0; i--) { - if (attemptConnect(skipStatusCheck, i > 0)) { - break; + private static boolean validateStatusCode(HttpResponse response, boolean returnOnServerError) { + int statusCode = response.getStatusLine().getStatusCode(); + if (returnOnServerError && statusCode >= HttpStatus.SC_INTERNAL_SERVER_ERROR) { + return false; + } else if (!isSuccessWithContent(statusCode)) { + throw new RuntimeException("Not success status code: " + statusCode); } - } + return true; } - } - /** - * @return An InputStream implementation for the current http stream. - */ - public InputStream createContentInputStream(HttpResponse response) throws IOException { - return new BufferedInputStream(response.getEntity().getContent()); - } + private HttpGet getConnectRequest() { + HttpGet request = new HttpGet(getConnectUrl()); + + if (position > 0 && useHeadersForRange()) { + request.setHeader(HttpHeaders.RANGE, "bytes=" + position + "-"); + } - private boolean attemptConnect(boolean skipStatusCheck, boolean retryOnServerError) throws IOException { - currentResponse = httpInterface.execute(getConnectRequest()); - lastStatusCode = currentResponse.getStatusLine().getStatusCode(); + return request; + } - if (!skipStatusCheck && !validateStatusCode(currentResponse, retryOnServerError)) { - return false; + protected void connect(boolean skipStatusCheck) throws IOException { + if (currentResponse == null) { + for (int i = 1; i >= 0; i--) { + if (attemptConnect(skipStatusCheck, i > 0)) { + break; + } + } + } } - if (currentResponse.getEntity() == null) { - currentContent = EmptyInputStream.INSTANCE; - contentLength = 0; - return true; + /** + * @return An InputStream implementation for the current http stream. + */ + public InputStream createContentInputStream(HttpResponse response) throws IOException { + return new BufferedInputStream(response.getEntity().getContent()); } - currentContent = createContentInputStream(currentResponse); + private boolean attemptConnect(boolean skipStatusCheck, boolean retryOnServerError) throws IOException { + currentResponse = httpInterface.execute(getConnectRequest()); + lastStatusCode = currentResponse.getStatusLine().getStatusCode(); + + if (!skipStatusCheck && !validateStatusCode(currentResponse, retryOnServerError)) { + return false; + } + + if (currentResponse.getEntity() == null) { + currentContent = EmptyInputStream.INSTANCE; + contentLength = 0; + return true; + } - if (contentLength == Units.CONTENT_LENGTH_UNKNOWN) { - Header header = currentResponse.getFirstHeader("Content-Length"); + currentContent = createContentInputStream(currentResponse); - if (header != null) { - contentLength = Long.parseLong(header.getValue()); - } + if (contentLength == Units.CONTENT_LENGTH_UNKNOWN) { + Header header = currentResponse.getFirstHeader("Content-Length"); + + if (header != null) { + contentLength = Long.parseLong(header.getValue()); + } + } + + return true; } - return true; - } + private void handleNetworkException(IOException exception, boolean attemptReconnect) throws IOException { + if (!attemptReconnect || !HttpClientTools.isRetriableNetworkException(exception)) { + throw exception; + } + + close(); - private void handleNetworkException(IOException exception, boolean attemptReconnect) throws IOException { - if (!attemptReconnect || !HttpClientTools.isRetriableNetworkException(exception)) { - throw exception; + log.debug("Encountered retriable exception on url {}.", contentUrl, exception); } - close(); + private int internalRead(boolean attemptReconnect) throws IOException { + connect(false); + + try { + int result = currentContent.read(); + if (result >= 0) { + position++; + } + return result; + } catch (IOException e) { + handleNetworkException(e, attemptReconnect); + return internalRead(false); + } + } - log.debug("Encountered retriable exception on url {}.", contentUrl, exception); - } + @Override + public int read() throws IOException { + return internalRead(true); + } - private int internalRead(boolean attemptReconnect) throws IOException { - connect(false); + protected int internalRead(byte[] b, int off, int len, boolean attemptReconnect) throws IOException { + connect(false); + + try { + int result = currentContent.read(b, off, len); + if (result >= 0) { + position += result; + } + return result; + } catch (IOException e) { + handleNetworkException(e, attemptReconnect); + return internalRead(b, off, len, false); + } + } - try { - int result = currentContent.read(); - if (result >= 0) { - position++; - } - return result; - } catch (IOException e) { - handleNetworkException(e, attemptReconnect); - return internalRead(false); + @Override + public int read(byte[] b, int off, int len) throws IOException { + return internalRead(b, off, len, true); } - } - - @Override - public int read() throws IOException { - return internalRead(true); - } - - protected int internalRead(byte[] b, int off, int len, boolean attemptReconnect) throws IOException { - connect(false); - - try { - int result = currentContent.read(b, off, len); - if (result >= 0) { - position += result; - } - return result; - } catch (IOException e) { - handleNetworkException(e, attemptReconnect); - return internalRead(b, off, len, false); + + protected long internalSkip(long n, boolean attemptReconnect) throws IOException { + connect(false); + + try { + long result = currentContent.skip(n); + if (result >= 0) { + position += result; + } + return result; + } catch (IOException e) { + handleNetworkException(e, attemptReconnect); + return internalSkip(n, false); + } } - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - return internalRead(b, off, len, true); - } - - protected long internalSkip(long n, boolean attemptReconnect) throws IOException { - connect(false); - - try { - long result = currentContent.skip(n); - if (result >= 0) { - position += result; - } - return result; - } catch (IOException e) { - handleNetworkException(e, attemptReconnect); - return internalSkip(n, false); + + @Override + public long skip(long n) throws IOException { + return internalSkip(n, true); } - } - @Override - public long skip(long n) throws IOException { - return internalSkip(n, true); - } + private int internalAvailable(boolean attemptReconnect) throws IOException { + connect(false); + + try { + return currentContent.available(); + } catch (IOException e) { + handleNetworkException(e, attemptReconnect); + return internalAvailable(false); + } + } - private int internalAvailable(boolean attemptReconnect) throws IOException { - connect(false); + @Override + public int available() throws IOException { + return internalAvailable(true); + } - try { - return currentContent.available(); - } catch (IOException e) { - handleNetworkException(e, attemptReconnect); - return internalAvailable(false); + @Override + public synchronized void reset() throws IOException { + throw new IOException("mark/reset not supported"); + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public void close() throws IOException { + if (currentResponse != null) { + try { + currentResponse.close(); + } catch (IOException e) { + log.debug("Failed to close response.", e); + } + + currentResponse = null; + currentContent = null; + } } - } - - @Override - public int available() throws IOException { - return internalAvailable(true); - } - - @Override - public synchronized void reset() throws IOException { - throw new IOException("mark/reset not supported"); - } - - @Override - public boolean markSupported() { - return false; - } - - @Override - public void close() throws IOException { - if (currentResponse != null) { - try { - currentResponse.close(); - } catch (IOException e) { - log.debug("Failed to close response.", e); - } - - currentResponse = null; - currentContent = null; + + /** + * Detach from the current connection, making sure not to close the connection when the stream is closed. + */ + public void releaseConnection() { + if (currentContent != null) { + try { + currentContent.close(); + } catch (IOException e) { + log.debug("Failed to close response stream.", e); + } + } + + currentResponse = null; + currentContent = null; } - } - - /** - * Detach from the current connection, making sure not to close the connection when the stream is closed. - */ - public void releaseConnection() { - if (currentContent != null) { - try { - currentContent.close(); - } catch (IOException e) { - log.debug("Failed to close response stream.", e); - } + + @Override + public long getPosition() { + return position; } - currentResponse = null; - currentContent = null; - } - - @Override - public long getPosition() { - return position; - } - - @Override - protected void seekHard(long position) throws IOException { - close(); - - this.position = position; - } - - @Override - public boolean canSeekHard() { - return contentLength != Units.CONTENT_LENGTH_UNKNOWN; - } - - @Override - public List getTrackInfoProviders() { - if (currentResponse != null) { - return Collections.singletonList(createIceCastHeaderProvider()); - } else { - return Collections.emptyList(); + @Override + protected void seekHard(long position) throws IOException { + close(); + + this.position = position; } - } - private AudioTrackInfoProvider createIceCastHeaderProvider() { - AudioTrackInfoBuilder builder = AudioTrackInfoBuilder.empty() - .setTitle(getHeaderValue(currentResponse, "icy-description")) - .setAuthor(getHeaderValue(currentResponse, "icy-name")); + @Override + public boolean canSeekHard() { + return contentLength != Units.CONTENT_LENGTH_UNKNOWN; + } - if (builder.getTitle() == null) { - builder.setTitle(getHeaderValue(currentResponse, "icy-url")); + @Override + public List getTrackInfoProviders() { + if (currentResponse != null) { + return Collections.singletonList(createIceCastHeaderProvider()); + } else { + return Collections.emptyList(); + } } - return builder; - } + private AudioTrackInfoProvider createIceCastHeaderProvider() { + AudioTrackInfoBuilder builder = AudioTrackInfoBuilder.empty() + .setTitle(getHeaderValue(currentResponse, "icy-description")) + .setAuthor(getHeaderValue(currentResponse, "icy-name")); + + if (builder.getTitle() == null) { + builder.setTitle(getHeaderValue(currentResponse, "icy-url")); + } + + return builder; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ResettableBoundedInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ResettableBoundedInputStream.java index 8d0f43bf5..b43824f52 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ResettableBoundedInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ResettableBoundedInputStream.java @@ -9,80 +9,81 @@ * Bounded input stream where the limit can be set dynamically. */ public class ResettableBoundedInputStream extends InputStream { - private final InputStream delegate; - - private long limit; - private long position; - - /** - * @param delegate Underlying input stream. - */ - public ResettableBoundedInputStream(InputStream delegate) { - this.delegate = delegate; - this.limit = Long.MAX_VALUE; - this.position = 0; - } - - /** - * Make this input stream return EOF after the specified number of bytes. - * @param limit Maximum number of bytes that can be read. - */ - public void resetLimit(long limit) { - this.position = 0; - this.limit = limit; - } - - @Override - public int read() throws IOException { - if (position >= limit) { - return -1; + private final InputStream delegate; + + private long limit; + private long position; + + /** + * @param delegate Underlying input stream. + */ + public ResettableBoundedInputStream(InputStream delegate) { + this.delegate = delegate; + this.limit = Long.MAX_VALUE; + this.position = 0; } - int result = delegate.read(); - if (result != -1) { - position++; + /** + * Make this input stream return EOF after the specified number of bytes. + * + * @param limit Maximum number of bytes that can be read. + */ + public void resetLimit(long limit) { + this.position = 0; + this.limit = limit; } - return result; - } + @Override + public int read() throws IOException { + if (position >= limit) { + return -1; + } - @Override - public int read(byte[] buffer, int offset, int length) throws IOException { - if (position >= limit) { - return EOF; + int result = delegate.read(); + if (result != -1) { + position++; + } + + return result; + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + if (position >= limit) { + return EOF; + } + + int chunk = (int) Math.min(length, limit - position); + int read = delegate.read(buffer, offset, chunk); + + if (read == -1) { + return -1; + } + + position += read; + return read; } - int chunk = (int) Math.min(length, limit - position); - int read = delegate.read(buffer, offset, chunk); + @Override + public long skip(final long distance) throws IOException { + int chunk = (int) Math.min(distance, limit - position); + long skipped = delegate.skip(chunk); + position += skipped; + return skipped; + } - if (read == -1) { - return -1; + @Override + public int available() throws IOException { + return (int) Math.min(limit - position, delegate.available()); } - position += read; - return read; - } - - @Override - public long skip(final long distance) throws IOException { - int chunk = (int) Math.min(distance, limit - position); - long skipped = delegate.skip(chunk); - position += skipped; - return skipped; - } - - @Override - public int available() throws IOException { - return (int) Math.min(limit - position, delegate.available()); - } - - @Override - public void close() throws IOException { - // Nothing to do - } - - @Override - public boolean markSupported() { - return false; - } + @Override + public void close() throws IOException { + // Nothing to do + } + + @Override + public boolean markSupported() { + return false; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/SavedHeadSeekableInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/SavedHeadSeekableInputStream.java index 3cb982212..49323bda9 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/SavedHeadSeekableInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/SavedHeadSeekableInputStream.java @@ -10,131 +10,132 @@ * saved beginning does not cause any IO to be done on the underlying input stream. */ public class SavedHeadSeekableInputStream extends SeekableInputStream { - private final SeekableInputStream delegate; - private final byte[] savedHead; - private boolean usingHead; - private boolean allowDirectReads; - private long headPosition; - private long savedUntilPosition; - - /** - * @param delegate The seekable stream to delegate reading to - * @param savedSize Number of bytes to buffer - */ - public SavedHeadSeekableInputStream(SeekableInputStream delegate, int savedSize) { - super(delegate.getContentLength(), delegate.getMaxSkipDistance()); - this.delegate = delegate; - this.savedHead = new byte[savedSize]; - this.allowDirectReads = true; - } - - public void setAllowDirectReads(boolean allowDirectReads) { - this.allowDirectReads = allowDirectReads; - } - - /** - * Load the number of bytes specified in the constructor into the saved buffer. - * @throws IOException On IO error - */ - public void loadHead() throws IOException { - delegate.seek(0); - savedUntilPosition = read(savedHead, 0, savedHead.length); - usingHead = savedUntilPosition > 0; - headPosition = 0; - } - - @Override - public long getPosition() { - if (usingHead) { - return headPosition; - } else { - return delegate.getPosition(); + private final SeekableInputStream delegate; + private final byte[] savedHead; + private boolean usingHead; + private boolean allowDirectReads; + private long headPosition; + private long savedUntilPosition; + + /** + * @param delegate The seekable stream to delegate reading to + * @param savedSize Number of bytes to buffer + */ + public SavedHeadSeekableInputStream(SeekableInputStream delegate, int savedSize) { + super(delegate.getContentLength(), delegate.getMaxSkipDistance()); + this.delegate = delegate; + this.savedHead = new byte[savedSize]; + this.allowDirectReads = true; } - } - - @Override - protected void seekHard(long position) throws IOException { - if (position >= savedUntilPosition) { - if (allowDirectReads) { - usingHead = false; - delegate.seekHard(position); - } else { - throw new IndexOutOfBoundsException("Reads beyond saved head are disabled."); - } - } else { - usingHead = true; - headPosition = position; + + public void setAllowDirectReads(boolean allowDirectReads) { + this.allowDirectReads = allowDirectReads; } - } - - @Override - public boolean canSeekHard() { - return delegate.canSeekHard(); - } - - @Override - public List getTrackInfoProviders() { - return delegate.getTrackInfoProviders(); - } - - @Override - public int read() throws IOException { - if (usingHead) { - byte result = savedHead[(int) headPosition]; - - if (++headPosition == savedUntilPosition) { - delegate.seek(savedUntilPosition); - usingHead = false; - } - - return result & 0xFF; - } else if (allowDirectReads) { - return delegate.read(); - } else { - throw new IndexOutOfBoundsException("Reads beyond saved head are disabled."); + + /** + * Load the number of bytes specified in the constructor into the saved buffer. + * + * @throws IOException On IO error + */ + public void loadHead() throws IOException { + delegate.seek(0); + savedUntilPosition = read(savedHead, 0, savedHead.length); + usingHead = savedUntilPosition > 0; + headPosition = 0; } - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (usingHead) { - return super.read(b, off, len); - } else if (allowDirectReads) { - return delegate.read(b, off, len); - } else { - throw new IndexOutOfBoundsException("Reads beyond saved head are disabled."); + + @Override + public long getPosition() { + if (usingHead) { + return headPosition; + } else { + return delegate.getPosition(); + } } - } - - @Override - public long skip(long n) throws IOException { - if (usingHead) { - return super.skip(n); - } else if (allowDirectReads) { - return delegate.skip(n); - } else { - throw new IndexOutOfBoundsException("Reads beyond saved head are disabled."); + + @Override + protected void seekHard(long position) throws IOException { + if (position >= savedUntilPosition) { + if (allowDirectReads) { + usingHead = false; + delegate.seekHard(position); + } else { + throw new IndexOutOfBoundsException("Reads beyond saved head are disabled."); + } + } else { + usingHead = true; + headPosition = position; + } } - } - - @Override - public int available() throws IOException { - if (usingHead) { - return (int) (savedUntilPosition - headPosition); - } else if (allowDirectReads) { - return delegate.available(); - } else { - throw new IndexOutOfBoundsException("Reads beyond saved head are disabled."); + + @Override + public boolean canSeekHard() { + return delegate.canSeekHard(); } - } - @Override - public void close() throws IOException { - delegate.close(); - } + @Override + public List getTrackInfoProviders() { + return delegate.getTrackInfoProviders(); + } + + @Override + public int read() throws IOException { + if (usingHead) { + byte result = savedHead[(int) headPosition]; + + if (++headPosition == savedUntilPosition) { + delegate.seek(savedUntilPosition); + usingHead = false; + } + + return result & 0xFF; + } else if (allowDirectReads) { + return delegate.read(); + } else { + throw new IndexOutOfBoundsException("Reads beyond saved head are disabled."); + } + } - @Override - public boolean markSupported() { - return false; - } + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (usingHead) { + return super.read(b, off, len); + } else if (allowDirectReads) { + return delegate.read(b, off, len); + } else { + throw new IndexOutOfBoundsException("Reads beyond saved head are disabled."); + } + } + + @Override + public long skip(long n) throws IOException { + if (usingHead) { + return super.skip(n); + } else if (allowDirectReads) { + return delegate.skip(n); + } else { + throw new IndexOutOfBoundsException("Reads beyond saved head are disabled."); + } + } + + @Override + public int available() throws IOException { + if (usingHead) { + return (int) (savedUntilPosition - headPosition); + } else if (allowDirectReads) { + return delegate.available(); + } else { + throw new IndexOutOfBoundsException("Reads beyond saved head are disabled."); + } + } + + @Override + public void close() throws IOException { + delegate.close(); + } + + @Override + public boolean markSupported() { + return false; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/SeekableInputStream.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/SeekableInputStream.java index c98b28d91..9a2821972 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/SeekableInputStream.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/SeekableInputStream.java @@ -11,93 +11,95 @@ * An input stream that is seekable. */ public abstract class SeekableInputStream extends InputStream { - protected long contentLength; - private final long maxSkipDistance; - - /** - * @param contentLength Total stream length - * @param maxSkipDistance Maximum distance that should be skipped by reading and discarding - */ - public SeekableInputStream(long contentLength, long maxSkipDistance) { - this.contentLength = contentLength; - this.maxSkipDistance = maxSkipDistance; - } - - /** - * @return Length of the stream - */ - public long getContentLength() { - return contentLength; - } - - /** - * @return Maximum distance that this stream will skip without doing a direct seek on the underlying resource. - */ - public long getMaxSkipDistance() { - return maxSkipDistance; - } - - /** - * @return Current position in the stream - */ - public abstract long getPosition(); - - protected abstract void seekHard(long position) throws IOException; - - /** - * @return true if it is possible to seek to an arbitrary position in this stream, even when it is behind - * the current position. - */ - public abstract boolean canSeekHard(); - - /** - * Skip the specified number of bytes in the stream. The result is either that the requested number of bytes were - * skipped or an EOFException was thrown. - * @param distance The number of bytes to skip - * @throws IOException On IO error - */ - public void skipFully(long distance) throws IOException { - long current = getPosition(); - long target = current + distance; - - while (current < target) { - long skipped = skip(target - current); - - if (skipped == 0) { - if (read() == -1) { - throw new EOFException("Cannot skip any further."); - } else { - skipped = 1; - } - } + protected long contentLength; + private final long maxSkipDistance; + + /** + * @param contentLength Total stream length + * @param maxSkipDistance Maximum distance that should be skipped by reading and discarding + */ + public SeekableInputStream(long contentLength, long maxSkipDistance) { + this.contentLength = contentLength; + this.maxSkipDistance = maxSkipDistance; + } + + /** + * @return Length of the stream + */ + public long getContentLength() { + return contentLength; + } - current += skipped; + /** + * @return Maximum distance that this stream will skip without doing a direct seek on the underlying resource. + */ + public long getMaxSkipDistance() { + return maxSkipDistance; } - } - - /** - * Seek to the specified position - * @param position The position to seek to - * @throws IOException On a read error or if the position is beyond EOF - */ - public void seek(long position) throws IOException { - long current = getPosition(); - - if (current != position) { - if (current <= position && position - current <= maxSkipDistance) { - skipFully(position - current); - } else if (!canSeekHard()) { - if (current > position) { - seekHard(0); - skipFully(position); - } else { - skipFully(position - current); + + /** + * @return Current position in the stream + */ + public abstract long getPosition(); + + protected abstract void seekHard(long position) throws IOException; + + /** + * @return true if it is possible to seek to an arbitrary position in this stream, even when it is behind + * the current position. + */ + public abstract boolean canSeekHard(); + + /** + * Skip the specified number of bytes in the stream. The result is either that the requested number of bytes were + * skipped or an EOFException was thrown. + * + * @param distance The number of bytes to skip + * @throws IOException On IO error + */ + public void skipFully(long distance) throws IOException { + long current = getPosition(); + long target = current + distance; + + while (current < target) { + long skipped = skip(target - current); + + if (skipped == 0) { + if (read() == -1) { + throw new EOFException("Cannot skip any further."); + } else { + skipped = 1; + } + } + + current += skipped; + } + } + + /** + * Seek to the specified position + * + * @param position The position to seek to + * @throws IOException On a read error or if the position is beyond EOF + */ + public void seek(long position) throws IOException { + long current = getPosition(); + + if (current != position) { + if (current <= position && position - current <= maxSkipDistance) { + skipFully(position - current); + } else if (!canSeekHard()) { + if (current > position) { + seekHard(0); + skipFully(position); + } else { + skipFully(position - current); + } + } else { + seekHard(position); + } } - } else { - seekHard(position); - } } - } - public abstract List getTrackInfoProviders(); + public abstract List getTrackInfoProviders(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/SimpleHttpInterfaceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/SimpleHttpInterfaceManager.java index 42bcca52b..93b91f372 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/SimpleHttpInterfaceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/SimpleHttpInterfaceManager.java @@ -10,26 +10,26 @@ * HTTP interface manager which creates a new HTTP context for each interface. */ public class SimpleHttpInterfaceManager extends AbstractHttpInterfaceManager { - private final SettableHttpRequestFilter filterHolder; + private final SettableHttpRequestFilter filterHolder; - /** - * @param clientBuilder HTTP client builder to use for creating the client instance. - * @param requestConfig Request config used by the client builder - */ - public SimpleHttpInterfaceManager(HttpClientBuilder clientBuilder, RequestConfig requestConfig) { - super(clientBuilder, requestConfig); - this.filterHolder = new SettableHttpRequestFilter(); - } + /** + * @param clientBuilder HTTP client builder to use for creating the client instance. + * @param requestConfig Request config used by the client builder + */ + public SimpleHttpInterfaceManager(HttpClientBuilder clientBuilder, RequestConfig requestConfig) { + super(clientBuilder, requestConfig); + this.filterHolder = new SettableHttpRequestFilter(); + } - @Override - public HttpInterface getInterface() { - HttpInterface httpInterface = new HttpInterface(getSharedClient(), HttpClientContext.create(), false, filterHolder); - httpInterface.acquire(); - return httpInterface; - } + @Override + public HttpInterface getInterface() { + HttpInterface httpInterface = new HttpInterface(getSharedClient(), HttpClientContext.create(), false, filterHolder); + httpInterface.acquire(); + return httpInterface; + } - @Override - public void setHttpContextFilter(HttpContextFilter filter) { - filterHolder.set(filter); - } + @Override + public void setHttpContextFilter(HttpContextFilter filter) { + filterHolder.set(filter); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/StreamTools.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/StreamTools.java index 6bba3444f..01901645e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/StreamTools.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/StreamTools.java @@ -7,29 +7,29 @@ * Utility methods for streams. */ public class StreamTools { - /** - * Reads from the stream until either the length number of bytes is read, or the stream ends. Note that neither case - * throws an exception. - * - * @param in The stream to read from. - * @param buffer Buffer to write the data that is read from the stream. - * @param offset Offset in the buffer to start writing from. - * @param length Maximum number of bytes to read from the stream. - * @return The number of bytes read from the stream. - * @throws IOException On read error. - */ - public static int readUntilEnd(InputStream in, byte[] buffer, int offset, int length) throws IOException { - int position = 0; + /** + * Reads from the stream until either the length number of bytes is read, or the stream ends. Note that neither case + * throws an exception. + * + * @param in The stream to read from. + * @param buffer Buffer to write the data that is read from the stream. + * @param offset Offset in the buffer to start writing from. + * @param length Maximum number of bytes to read from the stream. + * @return The number of bytes read from the stream. + * @throws IOException On read error. + */ + public static int readUntilEnd(InputStream in, byte[] buffer, int offset, int length) throws IOException { + int position = 0; - while (position < length) { - int count = in.read(buffer, offset + position, length - position); - if (count < 0) { - break; - } + while (position < length) { + int count = in.read(buffer, offset + position, length - position); + if (count < 0) { + break; + } - position += count; - } + position += count; + } - return position; - } + return position; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ThreadLocalHttpInterfaceManager.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ThreadLocalHttpInterfaceManager.java index 06d0f61ee..9a3b64660 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ThreadLocalHttpInterfaceManager.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/ThreadLocalHttpInterfaceManager.java @@ -13,43 +13,43 @@ * client instance used is created lazily. */ public class ThreadLocalHttpInterfaceManager extends AbstractHttpInterfaceManager { - private final ThreadLocal httpInterfaces; - private final SettableHttpRequestFilter filter; - - /** - * @param clientBuilder HTTP client builder to use for creating the client instance. - * @param requestConfig Request config used by the client builder - */ - public ThreadLocalHttpInterfaceManager(HttpClientBuilder clientBuilder, RequestConfig requestConfig) { - super(clientBuilder, requestConfig); - - this.filter = new SettableHttpRequestFilter(); - this.httpInterfaces = ThreadLocal.withInitial(() -> - new HttpInterface(getSharedClient(), HttpClientContext.create(), false, filter) - ); - } - - @Override - public HttpInterface getInterface() { - CloseableHttpClient client = getSharedClient(); - - HttpInterface httpInterface = httpInterfaces.get(); - if (httpInterface.getHttpClient() != client) { - httpInterfaces.remove(); - httpInterface = httpInterfaces.get(); + private final ThreadLocal httpInterfaces; + private final SettableHttpRequestFilter filter; + + /** + * @param clientBuilder HTTP client builder to use for creating the client instance. + * @param requestConfig Request config used by the client builder + */ + public ThreadLocalHttpInterfaceManager(HttpClientBuilder clientBuilder, RequestConfig requestConfig) { + super(clientBuilder, requestConfig); + + this.filter = new SettableHttpRequestFilter(); + this.httpInterfaces = ThreadLocal.withInitial(() -> + new HttpInterface(getSharedClient(), HttpClientContext.create(), false, filter) + ); } - if (httpInterface.acquire()) { - return httpInterface; - } + @Override + public HttpInterface getInterface() { + CloseableHttpClient client = getSharedClient(); + + HttpInterface httpInterface = httpInterfaces.get(); + if (httpInterface.getHttpClient() != client) { + httpInterfaces.remove(); + httpInterface = httpInterfaces.get(); + } - httpInterface = new HttpInterface(client, HttpClientContext.create(), false, filter); - httpInterface.acquire(); - return httpInterface; - } + if (httpInterface.acquire()) { + return httpInterface; + } - @Override - public void setHttpContextFilter(HttpContextFilter modifier) { - filter.set(modifier); - } + httpInterface = new HttpInterface(client, HttpClientContext.create(), false, filter); + httpInterface.acquire(); + return httpInterface; + } + + @Override + public void setHttpContextFilter(HttpContextFilter modifier) { + filter.set(modifier); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/TrustManagerBuilder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/TrustManagerBuilder.java index b286dc14f..299d8101e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/TrustManagerBuilder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/TrustManagerBuilder.java @@ -20,118 +20,120 @@ * Builder for a trust manager with custom certificates. */ public class TrustManagerBuilder { - private static final Logger log = LoggerFactory.getLogger(TrustManagerBuilder.class); - - private final List certificates = new ArrayList<>(); - - /** - * Add certificates from the default trust store - * @return this - * @throws Exception In case anything explodes. - */ - public TrustManagerBuilder addBuiltinCertificates() throws Exception { - TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - factory.init((KeyStore) null); - - X509TrustManager builtInTrustManager = findFirstX509TrustManager(factory); - if (builtInTrustManager != null) { - addFromTrustManager(builtInTrustManager); + private static final Logger log = LoggerFactory.getLogger(TrustManagerBuilder.class); + + private final List certificates = new ArrayList<>(); + + /** + * Add certificates from the default trust store + * + * @return this + * @throws Exception In case anything explodes. + */ + public TrustManagerBuilder addBuiltinCertificates() throws Exception { + TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + factory.init((KeyStore) null); + + X509TrustManager builtInTrustManager = findFirstX509TrustManager(factory); + if (builtInTrustManager != null) { + addFromTrustManager(builtInTrustManager); + } + return this; } - return this; - } - - /** - * Add certificates from the specified resource directory, using {path}/bundled.txt and {path}/extended.txt as the - * list of JKS file names to laoad from that directory. - * @param path Path to the resource directory. - * @return this - * @throws Exception In case anything explodes. - */ - public TrustManagerBuilder addFromResourceDirectory(String path) throws Exception { - addFromResourceList(path, path + "/bundled.txt"); - addFromResourceList(path, path + "/extended.txt"); - return this; - } - - /** - * @return A trust manager with the loaded certificates. - * @throws Exception In case anything explodes. - */ - public X509TrustManager build() throws Exception { - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(null, null); - - for (int i = 0; i < certificates.size(); i++) { - keyStore.setCertificateEntry(String.valueOf(i), certificates.get(i)); + + /** + * Add certificates from the specified resource directory, using {path}/bundled.txt and {path}/extended.txt as the + * list of JKS file names to laoad from that directory. + * + * @param path Path to the resource directory. + * @return this + * @throws Exception In case anything explodes. + */ + public TrustManagerBuilder addFromResourceDirectory(String path) throws Exception { + addFromResourceList(path, path + "/bundled.txt"); + addFromResourceList(path, path + "/extended.txt"); + return this; } - TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - factory.init(keyStore); + /** + * @return A trust manager with the loaded certificates. + * @throws Exception In case anything explodes. + */ + public X509TrustManager build() throws Exception { + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + + for (int i = 0; i < certificates.size(); i++) { + keyStore.setCertificateEntry(String.valueOf(i), certificates.get(i)); + } - return findFirstX509TrustManager(factory); - } + TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + factory.init(keyStore); - private X509TrustManager findFirstX509TrustManager(TrustManagerFactory factory) { - for (TrustManager trustManager : factory.getTrustManagers()) { - if (trustManager instanceof X509TrustManager) { - return (X509TrustManager) trustManager; - } + return findFirstX509TrustManager(factory); } - return null; - } + private X509TrustManager findFirstX509TrustManager(TrustManagerFactory factory) { + for (TrustManager trustManager : factory.getTrustManagers()) { + if (trustManager instanceof X509TrustManager) { + return (X509TrustManager) trustManager; + } + } - private void addFromTrustManager(X509TrustManager trustManager) { - for (Certificate certificate : trustManager.getAcceptedIssuers()) { - certificates.add(certificate); + return null; } - } - - private void addFromResourceList(String basePath, String listPath) throws Exception { - InputStream listFileStream = TrustManagerBuilder.class.getResourceAsStream(listPath); - if (listFileStream == null) { - log.debug("Certificate list {} not present in classpath.", listPath); - return; + private void addFromTrustManager(X509TrustManager trustManager) { + for (Certificate certificate : trustManager.getAcceptedIssuers()) { + certificates.add(certificate); + } } - try { - for (String line : IOUtils.readLines(listFileStream, StandardCharsets.UTF_8)) { - String fileName = line.trim(); + private void addFromResourceList(String basePath, String listPath) throws Exception { + InputStream listFileStream = TrustManagerBuilder.class.getResourceAsStream(listPath); - if (!fileName.isEmpty()) { - addFromResourceFile(basePath + "/" + fileName); + if (listFileStream == null) { + log.debug("Certificate list {} not present in classpath.", listPath); + return; } - } - } finally { - ExceptionTools.closeWithWarnings(listFileStream); - } - } - private void addFromResourceFile(String resourcePath) throws Exception { - InputStream fileStream = TrustManagerBuilder.class.getResourceAsStream(resourcePath); + try { + for (String line : IOUtils.readLines(listFileStream, StandardCharsets.UTF_8)) { + String fileName = line.trim(); - if (fileStream == null) { - log.warn("Certificate {} not present in classpath.", resourcePath); - return; + if (!fileName.isEmpty()) { + addFromResourceFile(basePath + "/" + fileName); + } + } + } finally { + ExceptionTools.closeWithWarnings(listFileStream); + } } - try { - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(fileStream, null); - addFromKeyStore(keyStore); - } finally { - ExceptionTools.closeWithWarnings(fileStream); + private void addFromResourceFile(String resourcePath) throws Exception { + InputStream fileStream = TrustManagerBuilder.class.getResourceAsStream(resourcePath); + + if (fileStream == null) { + log.warn("Certificate {} not present in classpath.", resourcePath); + return; + } + + try { + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(fileStream, null); + addFromKeyStore(keyStore); + } finally { + ExceptionTools.closeWithWarnings(fileStream); + } } - } - private void addFromKeyStore(KeyStore keyStore) throws Exception { - for (Enumeration enumeration = keyStore.aliases(); enumeration.hasMoreElements(); ) { - String alias = enumeration.nextElement(); + private void addFromKeyStore(KeyStore keyStore) throws Exception { + for (Enumeration enumeration = keyStore.aliases(); enumeration.hasMoreElements(); ) { + String alias = enumeration.nextElement(); - if (keyStore.isCertificateEntry(alias)) { - certificates.add(keyStore.getCertificate(alias)); - } + if (keyStore.isCertificateEntry(alias)) { + certificates.add(keyStore.getCertificate(alias)); + } + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioPlaylist.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioPlaylist.java index ba49f1a3a..cf6e5e563 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioPlaylist.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioPlaylist.java @@ -6,23 +6,23 @@ * Playlist of audio tracks */ public interface AudioPlaylist extends AudioItem { - /** - * @return Name of the playlist - */ - String getName(); + /** + * @return Name of the playlist + */ + String getName(); - /** - * @return List of tracks in the playlist - */ - List getTracks(); + /** + * @return List of tracks in the playlist + */ + List getTracks(); - /** - * @return Track that is explicitly selected, may be null. This same instance occurs in the track list. - */ - AudioTrack getSelectedTrack(); + /** + * @return Track that is explicitly selected, may be null. This same instance occurs in the track list. + */ + AudioTrack getSelectedTrack(); - /** - * @return True if the playlist was created from search results. - */ - boolean isSearchResult(); + /** + * @return True if the playlist was created from search results. + */ + boolean isSearchResult(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioReference.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioReference.java index 472ccb073..94c49d487 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioReference.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioReference.java @@ -8,71 +8,71 @@ * which means that the item referred to in it is loaded instead. */ public class AudioReference implements AudioItem, AudioTrackInfoProvider { - public static final AudioReference NO_TRACK = new AudioReference(null, null, null); + public static final AudioReference NO_TRACK = new AudioReference(null, null, null); - /** - * The identifier of the other item. - */ - public final String identifier; - /** - * The title of the other item, if known. - */ - public final String title; - /** - * Known probe and probe settings of the item to be loaded. - */ - public final MediaContainerDescriptor containerDescriptor; + /** + * The identifier of the other item. + */ + public final String identifier; + /** + * The title of the other item, if known. + */ + public final String title; + /** + * Known probe and probe settings of the item to be loaded. + */ + public final MediaContainerDescriptor containerDescriptor; - /** - * @param identifier The identifier of the other item. - * @param title The title of the other item, if known. - */ - public AudioReference(String identifier, String title) { - this(identifier, title, null); - } + /** + * @param identifier The identifier of the other item. + * @param title The title of the other item, if known. + */ + public AudioReference(String identifier, String title) { + this(identifier, title, null); + } - /** - * @param identifier The identifier of the other item. - * @param title The title of the other item, if known. - */ - public AudioReference(String identifier, String title, MediaContainerDescriptor containerDescriptor) { - this.identifier = identifier; - this.title = title; - this.containerDescriptor = containerDescriptor; - } + /** + * @param identifier The identifier of the other item. + * @param title The title of the other item, if known. + */ + public AudioReference(String identifier, String title, MediaContainerDescriptor containerDescriptor) { + this.identifier = identifier; + this.title = title; + this.containerDescriptor = containerDescriptor; + } - @Override - public String getTitle() { - return title; - } + @Override + public String getTitle() { + return title; + } - @Override - public String getAuthor() { - return null; - } + @Override + public String getAuthor() { + return null; + } - @Override - public Long getLength() { - return null; - } + @Override + public Long getLength() { + return null; + } - @Override - public String getIdentifier() { - return identifier; - } + @Override + public String getIdentifier() { + return identifier; + } - @Override - public String getUri() { - return identifier; - } + @Override + public String getUri() { + return identifier; + } - @Override - public String getArtworkUrl() { - return null; - } + @Override + public String getArtworkUrl() { + return null; + } - @Override - public String getISRC() { - return null; - } + @Override + public String getISRC() { + return null; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrack.java index 083af27a3..ee175fce1 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrack.java @@ -6,80 +6,80 @@ * A playable audio track */ public interface AudioTrack extends AudioItem { - /** - * @return Track meta information - */ - AudioTrackInfo getInfo(); + /** + * @return Track meta information + */ + AudioTrackInfo getInfo(); - /** - * @return The identifier of the track - */ - String getIdentifier(); + /** + * @return The identifier of the track + */ + String getIdentifier(); - /** - * @return The current execution state of the track - */ - AudioTrackState getState(); + /** + * @return The current execution state of the track + */ + AudioTrackState getState(); - /** - * Stop the track if it is currently playing - */ - void stop(); + /** + * Stop the track if it is currently playing + */ + void stop(); - /** - * @return True if the track is seekable. - */ - boolean isSeekable(); + /** + * @return True if the track is seekable. + */ + boolean isSeekable(); - /** - * @return Get the current position of the track in milliseconds - */ - long getPosition(); + /** + * @return Get the current position of the track in milliseconds + */ + long getPosition(); - /** - * Seek to the specified position. - * - * @param position New position of the track in milliseconds - */ - void setPosition(long position); + /** + * Seek to the specified position. + * + * @param position New position of the track in milliseconds + */ + void setPosition(long position); - /** - * @param marker Track position marker to place - */ - void setMarker(TrackMarker marker); + /** + * @param marker Track position marker to place + */ + void setMarker(TrackMarker marker); - /** - * @return Duration of the track in milliseconds - */ - long getDuration(); + /** + * @return Duration of the track in milliseconds + */ + long getDuration(); - /** - * @return Clone of this track which does not share the execution state of this track - */ - AudioTrack makeClone(); + /** + * @return Clone of this track which does not share the execution state of this track + */ + AudioTrack makeClone(); - /** - * @return The source manager which created this track. Null if not created by a source manager directly. - */ - AudioSourceManager getSourceManager(); + /** + * @return The source manager which created this track. Null if not created by a source manager directly. + */ + AudioSourceManager getSourceManager(); - /** - * Attach an object with this track which can later be retrieved with {@link #getUserData()}. Useful for retrieving - * application-specific object from the track in callbacks. - * - * @param userData Object to store. - */ - void setUserData(Object userData); + /** + * Attach an object with this track which can later be retrieved with {@link #getUserData()}. Useful for retrieving + * application-specific object from the track in callbacks. + * + * @param userData Object to store. + */ + void setUserData(Object userData); - /** - * @return Object previously stored with {@link #setUserData(Object)} - */ - Object getUserData(); + /** + * @return Object previously stored with {@link #setUserData(Object)} + */ + Object getUserData(); - /** - * @param klass The expected class of the user data (or a superclass of it). - * @return Object previously stored with {@link #setUserData(Object)} if it is of the specified type. If it is set, - * but with a different type, null is returned. - */ - T getUserData(Class klass); + /** + * @param klass The expected class of the user data (or a superclass of it). + * @return Object previously stored with {@link #setUserData(Object)} if it is of the specified type. If it is set, + * but with a different type, null is returned. + */ + T getUserData(Class klass); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackEndReason.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackEndReason.java index b034e75ce..590943892 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackEndReason.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackEndReason.java @@ -4,38 +4,38 @@ * Reason why a track stopped playing. */ public enum AudioTrackEndReason { - /** - * This means that the track itself emitted a terminator. This is usually caused by the track reaching the end, - * however it will also be used when it ends due to an exception. - */ - FINISHED(true), - /** - * This means that the track failed to start, throwing an exception before providing any audio. - */ - LOAD_FAILED(true), - /** - * The track was stopped due to the player being stopped by either calling stop() or playTrack(null). - */ - STOPPED(false), - /** - * The track stopped playing because a new track started playing. Note that with this reason, the old track will still - * play until either its buffer runs out or audio from the new track is available. - */ - REPLACED(false), - /** - * The track was stopped because the cleanup threshold for the audio player was reached. This triggers when the amount - * of time passed since the last call to AudioPlayer#provide() has reached the threshold specified in player manager - * configuration. This may also indicate either a leaked audio player which was discarded, but not stopped. - */ - CLEANUP(false); + /** + * This means that the track itself emitted a terminator. This is usually caused by the track reaching the end, + * however it will also be used when it ends due to an exception. + */ + FINISHED(true), + /** + * This means that the track failed to start, throwing an exception before providing any audio. + */ + LOAD_FAILED(true), + /** + * The track was stopped due to the player being stopped by either calling stop() or playTrack(null). + */ + STOPPED(false), + /** + * The track stopped playing because a new track started playing. Note that with this reason, the old track will still + * play until either its buffer runs out or audio from the new track is available. + */ + REPLACED(false), + /** + * The track was stopped because the cleanup threshold for the audio player was reached. This triggers when the amount + * of time passed since the last call to AudioPlayer#provide() has reached the threshold specified in player manager + * configuration. This may also indicate either a leaked audio player which was discarded, but not stopped. + */ + CLEANUP(false); - /** - * Indicates whether a new track should be started on receiving this event. If this is false, either this event is - * already triggered because another track started (REPLACED) or because the player is stopped (STOPPED, CLEANUP). - */ - public final boolean mayStartNext; + /** + * Indicates whether a new track should be started on receiving this event. If this is false, either this event is + * already triggered because another track started (REPLACED) or because the player is stopped (STOPPED, CLEANUP). + */ + public final boolean mayStartNext; - AudioTrackEndReason(boolean mayStartNext) { - this.mayStartNext = mayStartNext; - } + AudioTrackEndReason(boolean mayStartNext) { + this.mayStartNext = mayStartNext; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackInfo.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackInfo.java index 2f863b282..13e6daaa8 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackInfo.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackInfo.java @@ -4,69 +4,69 @@ * Meta info for an audio track */ public class AudioTrackInfo { - /** - * Track title - */ - public final String title; - /** - * Track author, if known - */ - public final String author; - /** - * Length of the track in milliseconds, UnitConstants.DURATION_MS_UNKNOWN for streams - */ - public final long length; - /** - * Audio source specific track identifier - */ - public final String identifier; - /** - * True if this track is a stream - */ - public final boolean isStream; - /** - * URL of the track, or local path to the file - */ - public final String uri; - /** - * URL to thumbnail of the track - */ - public final String artworkUrl; - /** - * International Standard Recording Code - */ - public final String isrc; + /** + * Track title + */ + public final String title; + /** + * Track author, if known + */ + public final String author; + /** + * Length of the track in milliseconds, UnitConstants.DURATION_MS_UNKNOWN for streams + */ + public final long length; + /** + * Audio source specific track identifier + */ + public final String identifier; + /** + * True if this track is a stream + */ + public final boolean isStream; + /** + * URL of the track, or local path to the file + */ + public final String uri; + /** + * URL to thumbnail of the track + */ + public final String artworkUrl; + /** + * International Standard Recording Code + */ + public final String isrc; - /** - * @param title Track title - * @param author Track author, if known - * @param length Length of the track in milliseconds - * @param identifier Audio source specific track identifier - * @param isStream True if this track is a stream - * @param uri URL of the track or path to its file. - * @param artworkUrl Thumbnail of the track - * @param isrc International Standard Recording Code - */ - public AudioTrackInfo(String title, String author, long length, String identifier, boolean isStream, String uri, String artworkUrl, String isrc) { - this.title = title; - this.author = author; - this.length = length; - this.identifier = identifier; - this.isStream = isStream; - this.uri = uri; - this.artworkUrl = artworkUrl; - this.isrc = isrc; - } + /** + * @param title Track title + * @param author Track author, if known + * @param length Length of the track in milliseconds + * @param identifier Audio source specific track identifier + * @param isStream True if this track is a stream + * @param uri URL of the track or path to its file. + * @param artworkUrl Thumbnail of the track + * @param isrc International Standard Recording Code + */ + public AudioTrackInfo(String title, String author, long length, String identifier, boolean isStream, String uri, String artworkUrl, String isrc) { + this.title = title; + this.author = author; + this.length = length; + this.identifier = identifier; + this.isStream = isStream; + this.uri = uri; + this.artworkUrl = artworkUrl; + this.isrc = isrc; + } - /** - * @param title Track title - * @param author Track author, if known - * @param length Length of the track in milliseconds - * @param identifier Audio source specific track identifier - * @param isStream True if this track is a stream - * @param uri URL of the track or path to its file. - */ - public AudioTrackInfo(String title, String author, long length, String identifier, boolean isStream, String uri) { - this(title, author, length, identifier, isStream, uri, null, null); - } -} \ No newline at end of file + /** + * @param title Track title + * @param author Track author, if known + * @param length Length of the track in milliseconds + * @param identifier Audio source specific track identifier + * @param isStream True if this track is a stream + * @param uri URL of the track or path to its file. + */ + public AudioTrackInfo(String title, String author, long length, String identifier, boolean isStream, String uri) { + this(title, author, length, identifier, isStream, uri, null, null); + } +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackState.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackState.java index 053733e92..c1bdc76a8 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackState.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrackState.java @@ -4,10 +4,10 @@ * The execution state of an audio track */ public enum AudioTrackState { - INACTIVE, - LOADING, - PLAYING, - SEEKING, - STOPPING, - FINISHED + INACTIVE, + LOADING, + PLAYING, + SEEKING, + STOPPING, + FINISHED } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/BaseAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/BaseAudioTrack.java index fa45b108f..b2565e47f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/BaseAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/BaseAudioTrack.java @@ -16,155 +16,155 @@ * Abstract base for all audio tracks with an executor */ public abstract class BaseAudioTrack implements InternalAudioTrack { - private final PrimordialAudioTrackExecutor initialExecutor; - private final AtomicBoolean executorAssigned; - private volatile AudioTrackExecutor activeExecutor; - protected final AudioTrackInfo trackInfo; - protected final AtomicLong accurateDuration; - private volatile Object userData; - - /** - * @param trackInfo Track info - */ - public BaseAudioTrack(AudioTrackInfo trackInfo) { - this.initialExecutor = new PrimordialAudioTrackExecutor(trackInfo); - this.executorAssigned = new AtomicBoolean(); - this.activeExecutor = null; - this.trackInfo = trackInfo; - this.accurateDuration = new AtomicLong(); - } - - @Override - public void assignExecutor(AudioTrackExecutor executor, boolean applyPrimordialState) { - if (executorAssigned.compareAndSet(false, true)) { - if (applyPrimordialState) { - initialExecutor.applyStateToExecutor(executor); - } - activeExecutor = executor; - } else { - throw new IllegalStateException("Cannot play the same instance of a track twice, use track.makeClone()."); - } - } - - @Override - public AudioTrackExecutor getActiveExecutor() { - AudioTrackExecutor executor = activeExecutor; - return executor != null ? executor : initialExecutor; - } - - @Override - public void stop() { - getActiveExecutor().stop(); - } - - @Override - public AudioTrackState getState() { - return getActiveExecutor().getState(); - } - - @Override - public String getIdentifier() { - return trackInfo.identifier; - } - - @Override - public boolean isSeekable() { - return !trackInfo.isStream; - } - - @Override - public long getPosition() { - return getActiveExecutor().getPosition(); - } - - @Override - public void setPosition(long position) { - getActiveExecutor().setPosition(position); - } - - @Override - public void setMarker(TrackMarker marker) { - getActiveExecutor().setMarker(marker); - } - - @Override - public AudioFrame provide() { - return getActiveExecutor().provide(); - } - - @Override - public AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { - return getActiveExecutor().provide(timeout, unit); - } - - @Override - public boolean provide(MutableAudioFrame targetFrame) { - return getActiveExecutor().provide(targetFrame); - } - - @Override - public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) - throws TimeoutException, InterruptedException { - - return getActiveExecutor().provide(targetFrame, timeout, unit); - } - - @Override - public AudioTrackInfo getInfo() { - return trackInfo; - } - - @Override - public long getDuration() { - long accurate = accurateDuration.get(); - - if (accurate == 0) { - return trackInfo.length; - } else { - return accurate; - } - } - - @Override - public AudioTrack makeClone() { - AudioTrack track = makeShallowClone(); - track.setUserData(userData); - return track; - } - - @Override - public AudioSourceManager getSourceManager() { - return null; - } - - @Override - public AudioTrackExecutor createLocalExecutor(AudioPlayerManager playerManager) { - return null; - } - - @Override - public void setUserData(Object userData) { - this.userData = userData; - } - - @Override - public Object getUserData() { - return userData; - } - - @Override - @SuppressWarnings("unchecked") - public T getUserData(Class klass) { - Object data = userData; - - if (data != null && klass.isAssignableFrom(data.getClass())) { - return (T) data; - } else { - return null; - } - } - - protected AudioTrack makeShallowClone() { - throw new UnsupportedOperationException(); - } + private final PrimordialAudioTrackExecutor initialExecutor; + private final AtomicBoolean executorAssigned; + private volatile AudioTrackExecutor activeExecutor; + protected final AudioTrackInfo trackInfo; + protected final AtomicLong accurateDuration; + private volatile Object userData; + + /** + * @param trackInfo Track info + */ + public BaseAudioTrack(AudioTrackInfo trackInfo) { + this.initialExecutor = new PrimordialAudioTrackExecutor(trackInfo); + this.executorAssigned = new AtomicBoolean(); + this.activeExecutor = null; + this.trackInfo = trackInfo; + this.accurateDuration = new AtomicLong(); + } + + @Override + public void assignExecutor(AudioTrackExecutor executor, boolean applyPrimordialState) { + if (executorAssigned.compareAndSet(false, true)) { + if (applyPrimordialState) { + initialExecutor.applyStateToExecutor(executor); + } + activeExecutor = executor; + } else { + throw new IllegalStateException("Cannot play the same instance of a track twice, use track.makeClone()."); + } + } + + @Override + public AudioTrackExecutor getActiveExecutor() { + AudioTrackExecutor executor = activeExecutor; + return executor != null ? executor : initialExecutor; + } + + @Override + public void stop() { + getActiveExecutor().stop(); + } + + @Override + public AudioTrackState getState() { + return getActiveExecutor().getState(); + } + + @Override + public String getIdentifier() { + return trackInfo.identifier; + } + + @Override + public boolean isSeekable() { + return !trackInfo.isStream; + } + + @Override + public long getPosition() { + return getActiveExecutor().getPosition(); + } + + @Override + public void setPosition(long position) { + getActiveExecutor().setPosition(position); + } + + @Override + public void setMarker(TrackMarker marker) { + getActiveExecutor().setMarker(marker); + } + + @Override + public AudioFrame provide() { + return getActiveExecutor().provide(); + } + + @Override + public AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { + return getActiveExecutor().provide(timeout, unit); + } + + @Override + public boolean provide(MutableAudioFrame targetFrame) { + return getActiveExecutor().provide(targetFrame); + } + + @Override + public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) + throws TimeoutException, InterruptedException { + + return getActiveExecutor().provide(targetFrame, timeout, unit); + } + + @Override + public AudioTrackInfo getInfo() { + return trackInfo; + } + + @Override + public long getDuration() { + long accurate = accurateDuration.get(); + + if (accurate == 0) { + return trackInfo.length; + } else { + return accurate; + } + } + + @Override + public AudioTrack makeClone() { + AudioTrack track = makeShallowClone(); + track.setUserData(userData); + return track; + } + + @Override + public AudioSourceManager getSourceManager() { + return null; + } + + @Override + public AudioTrackExecutor createLocalExecutor(AudioPlayerManager playerManager) { + return null; + } + + @Override + public void setUserData(Object userData) { + this.userData = userData; + } + + @Override + public Object getUserData() { + return userData; + } + + @Override + @SuppressWarnings("unchecked") + public T getUserData(Class klass) { + Object data = userData; + + if (data != null && klass.isAssignableFrom(data.getClass())) { + return (T) data; + } else { + return null; + } + } + + protected AudioTrack makeShallowClone() { + throw new UnsupportedOperationException(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/BasicAudioPlaylist.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/BasicAudioPlaylist.java index 84a4887e9..d73b8831d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/BasicAudioPlaylist.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/BasicAudioPlaylist.java @@ -6,41 +6,41 @@ * The basic implementation of AudioPlaylist */ public class BasicAudioPlaylist implements AudioPlaylist { - private final String name; - private final List tracks; - private final AudioTrack selectedTrack; - private final boolean isSearchResult; + private final String name; + private final List tracks; + private final AudioTrack selectedTrack; + private final boolean isSearchResult; - /** - * @param name Name of the playlist - * @param tracks List of tracks in the playlist - * @param selectedTrack Track that is explicitly selected - * @param isSearchResult True if the playlist was created from search results - */ - public BasicAudioPlaylist(String name, List tracks, AudioTrack selectedTrack, boolean isSearchResult) { - this.name = name; - this.tracks = tracks; - this.selectedTrack = selectedTrack; - this.isSearchResult = isSearchResult; - } + /** + * @param name Name of the playlist + * @param tracks List of tracks in the playlist + * @param selectedTrack Track that is explicitly selected + * @param isSearchResult True if the playlist was created from search results + */ + public BasicAudioPlaylist(String name, List tracks, AudioTrack selectedTrack, boolean isSearchResult) { + this.name = name; + this.tracks = tracks; + this.selectedTrack = selectedTrack; + this.isSearchResult = isSearchResult; + } - @Override - public String getName() { - return name; - } + @Override + public String getName() { + return name; + } - @Override - public List getTracks() { - return tracks; - } + @Override + public List getTracks() { + return tracks; + } - @Override - public AudioTrack getSelectedTrack() { - return selectedTrack; - } + @Override + public AudioTrack getSelectedTrack() { + return selectedTrack; + } - @Override - public boolean isSearchResult() { - return isSearchResult; - } + @Override + public boolean isSearchResult() { + return isSearchResult; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/DecodedTrackHolder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/DecodedTrackHolder.java index f4603e768..b0d456194 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/DecodedTrackHolder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/DecodedTrackHolder.java @@ -4,16 +4,16 @@ * The result of decoding a track. */ public class DecodedTrackHolder { - /** - * The decoded track. This may be null if there was a track to decode, but the decoding could not be performed because - * of an older serialization version or because the track source it used is not loaded. - */ - public final AudioTrack decodedTrack; + /** + * The decoded track. This may be null if there was a track to decode, but the decoding could not be performed because + * of an older serialization version or because the track source it used is not loaded. + */ + public final AudioTrack decodedTrack; - /** - * @param decodedTrack The decoded track - */ - public DecodedTrackHolder(AudioTrack decodedTrack) { - this.decodedTrack = decodedTrack; - } + /** + * @param decodedTrack The decoded track + */ + public DecodedTrackHolder(AudioTrack decodedTrack) { + this.decodedTrack = decodedTrack; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/DelegatedAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/DelegatedAudioTrack.java index 68c3d86d6..f9d23cb98 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/DelegatedAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/DelegatedAudioTrack.java @@ -7,66 +7,66 @@ * track is created, but is passed when processDelegate() is called. */ public abstract class DelegatedAudioTrack extends BaseAudioTrack { - private InternalAudioTrack delegate; + private InternalAudioTrack delegate; - /** - * @param trackInfo Track info - */ - public DelegatedAudioTrack(AudioTrackInfo trackInfo) { - super(trackInfo); - } + /** + * @param trackInfo Track info + */ + public DelegatedAudioTrack(AudioTrackInfo trackInfo) { + super(trackInfo); + } - protected synchronized void processDelegate(InternalAudioTrack delegate, LocalAudioTrackExecutor localExecutor) - throws Exception { + protected synchronized void processDelegate(InternalAudioTrack delegate, LocalAudioTrackExecutor localExecutor) + throws Exception { - this.delegate = delegate; + this.delegate = delegate; - delegate.assignExecutor(localExecutor, false); - delegate.process(localExecutor); - } + delegate.assignExecutor(localExecutor, false); + delegate.process(localExecutor); + } - @Override - public void setPosition(long position) { - if (delegate != null) { - delegate.setPosition(position); - } else { - synchronized (this) { + @Override + public void setPosition(long position) { if (delegate != null) { - delegate.setPosition(position); + delegate.setPosition(position); } else { - super.setPosition(position); + synchronized (this) { + if (delegate != null) { + delegate.setPosition(position); + } else { + super.setPosition(position); + } + } } - } } - } - @Override - public long getDuration() { - if (delegate != null) { - return delegate.getDuration(); - } else { - synchronized (this) { + @Override + public long getDuration() { if (delegate != null) { - return delegate.getDuration(); + return delegate.getDuration(); } else { - return super.getDuration(); + synchronized (this) { + if (delegate != null) { + return delegate.getDuration(); + } else { + return super.getDuration(); + } + } } - } } - } - @Override - public long getPosition() { - if (delegate != null) { - return delegate.getPosition(); - } else { - synchronized (this) { + @Override + public long getPosition() { if (delegate != null) { - return delegate.getPosition(); + return delegate.getPosition(); } else { - return super.getPosition(); + synchronized (this) { + if (delegate != null) { + return delegate.getPosition(); + } else { + return super.getPosition(); + } + } } - } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/InternalAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/InternalAudioTrack.java index beff57ef6..332195b93 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/InternalAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/InternalAudioTrack.java @@ -9,28 +9,29 @@ * Methods of an audio track that should not be visible outside of the library */ public interface InternalAudioTrack extends AudioTrack, AudioFrameProvider { - /** - * @param executor Executor to assign to the track - * @param applyPrimordialState True if the state previously applied to this track should be copied to new executor. - */ - void assignExecutor(AudioTrackExecutor executor, boolean applyPrimordialState); + /** + * @param executor Executor to assign to the track + * @param applyPrimordialState True if the state previously applied to this track should be copied to new executor. + */ + void assignExecutor(AudioTrackExecutor executor, boolean applyPrimordialState); - /** - * @return Get the active track executor - */ - AudioTrackExecutor getActiveExecutor(); + /** + * @return Get the active track executor + */ + AudioTrackExecutor getActiveExecutor(); - /** - * Perform any necessary loading and then enter the read/seek loop - * @param executor The local executor which processes this track - * @throws Exception In case anything explodes. - */ - void process(LocalAudioTrackExecutor executor) throws Exception; + /** + * Perform any necessary loading and then enter the read/seek loop + * + * @param executor The local executor which processes this track + * @throws Exception In case anything explodes. + */ + void process(LocalAudioTrackExecutor executor) throws Exception; - /** - * @param playerManager The player manager which is executing this track - * @return A custom local executor for this track. Unless this track requires a special executor, this should return - * null as the default one will be used in that case. - */ - AudioTrackExecutor createLocalExecutor(AudioPlayerManager playerManager); + /** + * @param playerManager The player manager which is executing this track + * @return A custom local executor for this track. Unless this track requires a special executor, this should return + * null as the default one will be used in that case. + */ + AudioTrackExecutor createLocalExecutor(AudioPlayerManager playerManager); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarker.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarker.java index bdf79d69c..e8ce897fc 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarker.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarker.java @@ -6,22 +6,22 @@ * its handler will always be called. */ public class TrackMarker { - /** - * The position of the track in milliseconds when this marker should trigger. - */ - public final long timecode; - /** - * The handler for the marker. The handler is guaranteed to be never called more than once, and guaranteed to be - * called at least once if the track is started on a player. - */ - public final TrackMarkerHandler handler; + /** + * The position of the track in milliseconds when this marker should trigger. + */ + public final long timecode; + /** + * The handler for the marker. The handler is guaranteed to be never called more than once, and guaranteed to be + * called at least once if the track is started on a player. + */ + public final TrackMarkerHandler handler; - /** - * @param timecode The position of the track in milliseconds when this marker should trigger. - * @param handler The handler for the marker. - */ - public TrackMarker(long timecode, TrackMarkerHandler handler) { - this.timecode = timecode; - this.handler = handler; - } + /** + * @param timecode The position of the track in milliseconds when this marker should trigger. + * @param handler The handler for the marker. + */ + public TrackMarker(long timecode, TrackMarkerHandler handler) { + this.timecode = timecode; + this.handler = handler; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarkerHandler.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarkerHandler.java index a9cda26be..4b479faf3 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarkerHandler.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarkerHandler.java @@ -4,43 +4,43 @@ * A track marker handler. */ public interface TrackMarkerHandler { - /** - * @param state The state of the marker when it is triggered. - */ - void handle(MarkerState state); - - /** - * The state of the marker at the moment the handle method is called. - */ - enum MarkerState { - /** - * The specified position has been reached with normal playback. - */ - REACHED, - /** - * The marker has been removed by setting the marker of the track to null. - */ - REMOVED, - /** - * The marker has been overwritten by setting the marker of the track to another non-null marker. - */ - OVERWRITTEN, - /** - * A seek was performed which jumped over the marked position. - */ - BYPASSED, /** - * The track was stopped before it ended, before the marked position was reached. + * @param state The state of the marker when it is triggered. */ - STOPPED, - /** - * The playback position was already beyond the marked position when the marker was placed. - */ - LATE, + void handle(MarkerState state); + /** - * The track ended without the marker being triggered (either due to an exception or because the track duration was - * smaller than the marked position). + * The state of the marker at the moment the handle method is called. */ - ENDED - } + enum MarkerState { + /** + * The specified position has been reached with normal playback. + */ + REACHED, + /** + * The marker has been removed by setting the marker of the track to null. + */ + REMOVED, + /** + * The marker has been overwritten by setting the marker of the track to another non-null marker. + */ + OVERWRITTEN, + /** + * A seek was performed which jumped over the marked position. + */ + BYPASSED, + /** + * The track was stopped before it ended, before the marked position was reached. + */ + STOPPED, + /** + * The playback position was already beyond the marked position when the marker was placed. + */ + LATE, + /** + * The track ended without the marker being triggered (either due to an exception or because the track duration was + * smaller than the marked position). + */ + ENDED + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarkerTracker.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarkerTracker.java index d51db7e37..777b6e188 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarkerTracker.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarkerTracker.java @@ -2,82 +2,83 @@ import java.util.concurrent.atomic.AtomicReference; -import static com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler.MarkerState.BYPASSED; -import static com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler.MarkerState.LATE; -import static com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler.MarkerState.OVERWRITTEN; -import static com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler.MarkerState.REACHED; -import static com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler.MarkerState.REMOVED; +import static com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler.MarkerState.*; /** * Tracks the state of a track position marker. */ public class TrackMarkerTracker { - private final AtomicReference current = new AtomicReference<>(); + private final AtomicReference current = new AtomicReference<>(); - /** - * Set a new track position marker. - * @param marker Marker - * @param currentTimecode Current timecode of the track when this marker is set - */ - public void set(TrackMarker marker, long currentTimecode) { - TrackMarker previous = current.getAndSet(marker); + /** + * Set a new track position marker. + * + * @param marker Marker + * @param currentTimecode Current timecode of the track when this marker is set + */ + public void set(TrackMarker marker, long currentTimecode) { + TrackMarker previous = current.getAndSet(marker); - if (previous != null) { - previous.handler.handle(marker != null ? OVERWRITTEN : REMOVED); - } + if (previous != null) { + previous.handler.handle(marker != null ? OVERWRITTEN : REMOVED); + } - if (marker != null && currentTimecode >= marker.timecode) { - trigger(marker, LATE); + if (marker != null && currentTimecode >= marker.timecode) { + trigger(marker, LATE); + } } - } - /** - * Remove the current marker. - * @return The removed marker. - */ - public TrackMarker remove() { - return current.getAndSet(null); - } + /** + * Remove the current marker. + * + * @return The removed marker. + */ + public TrackMarker remove() { + return current.getAndSet(null); + } - /** - * Trigger and remove the marker with the specified state. - * @param state The state of the marker to pass to the handler. - */ - public void trigger(TrackMarkerHandler.MarkerState state) { - TrackMarker marker = current.getAndSet(null); + /** + * Trigger and remove the marker with the specified state. + * + * @param state The state of the marker to pass to the handler. + */ + public void trigger(TrackMarkerHandler.MarkerState state) { + TrackMarker marker = current.getAndSet(null); - if (marker != null) { - marker.handler.handle(state); + if (marker != null) { + marker.handler.handle(state); + } } - } - /** - * Check a timecode which was reached by normal playback, trigger REACHED if necessary. - * @param timecode Timecode which was reached by normal playback. - */ - public void checkPlaybackTimecode(long timecode) { - TrackMarker marker = current.get(); + /** + * Check a timecode which was reached by normal playback, trigger REACHED if necessary. + * + * @param timecode Timecode which was reached by normal playback. + */ + public void checkPlaybackTimecode(long timecode) { + TrackMarker marker = current.get(); - if (marker != null && timecode >= marker.timecode) { - trigger(marker, REACHED); + if (marker != null && timecode >= marker.timecode) { + trigger(marker, REACHED); + } } - } - /** - * Check a timecode which was reached by seeking, trigger BYPASSED if necessary. - * @param timecode Timecode which was reached by seeking. - */ - public void checkSeekTimecode(long timecode) { - TrackMarker marker = current.get(); + /** + * Check a timecode which was reached by seeking, trigger BYPASSED if necessary. + * + * @param timecode Timecode which was reached by seeking. + */ + public void checkSeekTimecode(long timecode) { + TrackMarker marker = current.get(); - if (marker != null && timecode >= marker.timecode) { - trigger(marker, BYPASSED); + if (marker != null && timecode >= marker.timecode) { + trigger(marker, BYPASSED); + } } - } - private void trigger(TrackMarker marker, TrackMarkerHandler.MarkerState state) { - if (current.compareAndSet(marker, null)) { - marker.handler.handle(state); + private void trigger(TrackMarker marker, TrackMarkerHandler.MarkerState state) { + if (current.compareAndSet(marker, null)) { + marker.handler.handle(state); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackStateListener.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackStateListener.java index 6b4b2d48c..11208f03f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackStateListener.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackStateListener.java @@ -6,21 +6,21 @@ * Listener of track execution events. */ public interface TrackStateListener { - /** - * Called when an exception occurs while a track is playing or loading. This is always fatal, but it may have left - * some data in the audio buffer which can still play until the buffer clears out. - * - * @param track The audio track for which the exception occurred - * @param exception The exception that occurred - */ - void onTrackException(AudioTrack track, FriendlyException exception); + /** + * Called when an exception occurs while a track is playing or loading. This is always fatal, but it may have left + * some data in the audio buffer which can still play until the buffer clears out. + * + * @param track The audio track for which the exception occurred + * @param exception The exception that occurred + */ + void onTrackException(AudioTrack track, FriendlyException exception); - /** - * Called when an exception occurs while a track is playing or loading. This is always fatal, but it may have left - * some data in the audio buffer which can still play until the buffer clears out. - * - * @param track The audio track for which the exception occurred - * @param thresholdMs The wait threshold that was exceeded for this event to trigger - */ - void onTrackStuck(AudioTrack track, long thresholdMs); + /** + * Called when an exception occurs while a track is playing or loading. This is always fatal, but it may have left + * some data in the audio buffer which can still play until the buffer clears out. + * + * @param track The audio track for which the exception occurred + * @param thresholdMs The wait threshold that was exceeded for this event to trigger + */ + void onTrackStuck(AudioTrack track, long thresholdMs); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/info/AudioTrackInfoBuilder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/info/AudioTrackInfoBuilder.java index e5c4d7167..d42f099a3 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/info/AudioTrackInfoBuilder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/info/AudioTrackInfoBuilder.java @@ -11,161 +11,161 @@ * Builder for {@link AudioTrackInfo}. */ public class AudioTrackInfoBuilder implements AudioTrackInfoProvider { - private static final String UNKNOWN_TITLE = "Unknown title"; - private static final String UNKNOWN_ARTIST = "Unknown artist"; - - private String title; - private String author; - private Long length; - private String identifier; - private String uri; - private String artworkUrl; - private Boolean isStream; - private String isrc; - - private AudioTrackInfoBuilder() { - - } - - @Override - public String getTitle() { - return title; - } - - @Override - public String getAuthor() { - return author; - } - - @Override - public Long getLength() { - return length; - } - - @Override - public String getIdentifier() { - return identifier; - } - - @Override - public String getUri() { - return uri; - } - - @Override - public String getArtworkUrl() { - return artworkUrl; - } - - @Override - public String getISRC() { - return isrc; - } - - public AudioTrackInfoBuilder setTitle(String value) { - title = DataFormatTools.defaultOnNull(value, title); - return this; - } - - public AudioTrackInfoBuilder setAuthor(String value) { - author = DataFormatTools.defaultOnNull(value, author); - return this; - } - - public AudioTrackInfoBuilder setLength(Long value) { - length = DataFormatTools.defaultOnNull(value, length); - return this; - } - - public AudioTrackInfoBuilder setIdentifier(String value) { - identifier = DataFormatTools.defaultOnNull(value, identifier); - return this; - } - - public AudioTrackInfoBuilder setUri(String value) { - uri = DataFormatTools.defaultOnNull(value, uri); - return this; - } - - public AudioTrackInfoBuilder setArtworkUrl(String value) { - artworkUrl = DataFormatTools.defaultOnNull(value, artworkUrl); - return this; - } - - public AudioTrackInfoBuilder setIsStream(Boolean stream) { - isStream = stream; - return this; - } - - public AudioTrackInfoBuilder setISRC(String value) { - isrc = DataFormatTools.defaultOnNull(value, isrc); - return this; - } - - /** - * @param provider The track info provider to apply to the builder. - * @return this - */ - public AudioTrackInfoBuilder apply(AudioTrackInfoProvider provider) { - if (provider == null) { - return this; - } - - return setTitle(provider.getTitle()) - .setAuthor(provider.getAuthor()) - .setLength(provider.getLength()) - .setIdentifier(provider.getIdentifier()) - .setUri(provider.getUri()) - .setArtworkUrl(provider.getArtworkUrl()) - .setISRC(provider.getISRC()); - } - - /** - * @return Audio track info instance. - */ - public AudioTrackInfo build() { - long finalLength = DataFormatTools.defaultOnNull(length, DURATION_MS_UNKNOWN); - - return new AudioTrackInfo( - title, - author, - finalLength, - identifier, - DataFormatTools.defaultOnNull(isStream, finalLength == DURATION_MS_UNKNOWN), - uri, - artworkUrl, - isrc - ); - } - - /** - * Creates an instance of an audio track builder based on an audio reference and a stream. - * - * @param reference Audio reference to use as the starting point for the builder. - * @param stream Stream to get additional data from. - * @return An instance of the builder with the reference and track info providers from the stream preapplied. - */ - public static AudioTrackInfoBuilder create(AudioReference reference, SeekableInputStream stream) { - AudioTrackInfoBuilder builder = new AudioTrackInfoBuilder() - .setAuthor(UNKNOWN_ARTIST) - .setTitle(UNKNOWN_TITLE) - .setLength(DURATION_MS_UNKNOWN); - - builder.apply(reference); - - if (stream != null) { - for (AudioTrackInfoProvider provider : stream.getTrackInfoProviders()) { - builder.apply(provider); - } - } - - return builder; - } - - /** - * @return Empty instance of audio track builder. - */ - public static AudioTrackInfoBuilder empty() { - return new AudioTrackInfoBuilder(); - } + private static final String UNKNOWN_TITLE = "Unknown title"; + private static final String UNKNOWN_ARTIST = "Unknown artist"; + + private String title; + private String author; + private Long length; + private String identifier; + private String uri; + private String artworkUrl; + private Boolean isStream; + private String isrc; + + private AudioTrackInfoBuilder() { + + } + + @Override + public String getTitle() { + return title; + } + + @Override + public String getAuthor() { + return author; + } + + @Override + public Long getLength() { + return length; + } + + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public String getUri() { + return uri; + } + + @Override + public String getArtworkUrl() { + return artworkUrl; + } + + @Override + public String getISRC() { + return isrc; + } + + public AudioTrackInfoBuilder setTitle(String value) { + title = DataFormatTools.defaultOnNull(value, title); + return this; + } + + public AudioTrackInfoBuilder setAuthor(String value) { + author = DataFormatTools.defaultOnNull(value, author); + return this; + } + + public AudioTrackInfoBuilder setLength(Long value) { + length = DataFormatTools.defaultOnNull(value, length); + return this; + } + + public AudioTrackInfoBuilder setIdentifier(String value) { + identifier = DataFormatTools.defaultOnNull(value, identifier); + return this; + } + + public AudioTrackInfoBuilder setUri(String value) { + uri = DataFormatTools.defaultOnNull(value, uri); + return this; + } + + public AudioTrackInfoBuilder setArtworkUrl(String value) { + artworkUrl = DataFormatTools.defaultOnNull(value, artworkUrl); + return this; + } + + public AudioTrackInfoBuilder setIsStream(Boolean stream) { + isStream = stream; + return this; + } + + public AudioTrackInfoBuilder setISRC(String value) { + isrc = DataFormatTools.defaultOnNull(value, isrc); + return this; + } + + /** + * @param provider The track info provider to apply to the builder. + * @return this + */ + public AudioTrackInfoBuilder apply(AudioTrackInfoProvider provider) { + if (provider == null) { + return this; + } + + return setTitle(provider.getTitle()) + .setAuthor(provider.getAuthor()) + .setLength(provider.getLength()) + .setIdentifier(provider.getIdentifier()) + .setUri(provider.getUri()) + .setArtworkUrl(provider.getArtworkUrl()) + .setISRC(provider.getISRC()); + } + + /** + * @return Audio track info instance. + */ + public AudioTrackInfo build() { + long finalLength = DataFormatTools.defaultOnNull(length, DURATION_MS_UNKNOWN); + + return new AudioTrackInfo( + title, + author, + finalLength, + identifier, + DataFormatTools.defaultOnNull(isStream, finalLength == DURATION_MS_UNKNOWN), + uri, + artworkUrl, + isrc + ); + } + + /** + * Creates an instance of an audio track builder based on an audio reference and a stream. + * + * @param reference Audio reference to use as the starting point for the builder. + * @param stream Stream to get additional data from. + * @return An instance of the builder with the reference and track info providers from the stream preapplied. + */ + public static AudioTrackInfoBuilder create(AudioReference reference, SeekableInputStream stream) { + AudioTrackInfoBuilder builder = new AudioTrackInfoBuilder() + .setAuthor(UNKNOWN_ARTIST) + .setTitle(UNKNOWN_TITLE) + .setLength(DURATION_MS_UNKNOWN); + + builder.apply(reference); + + if (stream != null) { + for (AudioTrackInfoProvider provider : stream.getTrackInfoProviders()) { + builder.apply(provider); + } + } + + return builder; + } + + /** + * @return Empty instance of audio track builder. + */ + public static AudioTrackInfoBuilder empty() { + return new AudioTrackInfoBuilder(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/info/AudioTrackInfoProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/info/AudioTrackInfoProvider.java index 836d9ba9d..14bf64155 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/info/AudioTrackInfoProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/info/AudioTrackInfoProvider.java @@ -4,32 +4,32 @@ * Provider for audio track info. */ public interface AudioTrackInfoProvider { - /** - * @return Track title, or null if this provider does not know it. - */ - String getTitle(); + /** + * @return Track title, or null if this provider does not know it. + */ + String getTitle(); - /** - * @return Track author, or null if this provider does not know it. - */ - String getAuthor(); + /** + * @return Track author, or null if this provider does not know it. + */ + String getAuthor(); - /** - * @return Track length in milliseconds, or null if this provider does not know it. - */ - Long getLength(); + /** + * @return Track length in milliseconds, or null if this provider does not know it. + */ + Long getLength(); - /** - * @return Track identifier, or null if this provider does not know it. - */ - String getIdentifier(); + /** + * @return Track identifier, or null if this provider does not know it. + */ + String getIdentifier(); - /** - * @return Track URI, or null if this provider does not know it. - */ - String getUri(); + /** + * @return Track URI, or null if this provider does not know it. + */ + String getUri(); - String getArtworkUrl(); + String getArtworkUrl(); - String getISRC(); + String getISRC(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AbstractAudioFrameBuffer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AbstractAudioFrameBuffer.java index 4f8c30755..ae9bd251b 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AbstractAudioFrameBuffer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AbstractAudioFrameBuffer.java @@ -6,71 +6,71 @@ * Common parts of a frame buffer which are not likely to depend on the specific implementation. */ public abstract class AbstractAudioFrameBuffer implements AudioFrameBuffer { - protected final AudioDataFormat format; - protected final Object synchronizer; - protected volatile boolean locked; - protected volatile boolean receivedFrames; - protected boolean terminated; - protected boolean terminateOnEmpty; - protected boolean clearOnInsert; + protected final AudioDataFormat format; + protected final Object synchronizer; + protected volatile boolean locked; + protected volatile boolean receivedFrames; + protected boolean terminated; + protected boolean terminateOnEmpty; + protected boolean clearOnInsert; - protected AbstractAudioFrameBuffer(AudioDataFormat format) { - this.format = format; - this.synchronizer = new Object(); - locked = false; - receivedFrames = false; - terminated = false; - terminateOnEmpty = false; - clearOnInsert = false; - } + protected AbstractAudioFrameBuffer(AudioDataFormat format) { + this.format = format; + this.synchronizer = new Object(); + locked = false; + receivedFrames = false; + terminated = false; + terminateOnEmpty = false; + clearOnInsert = false; + } - @Override - public void waitForTermination() throws InterruptedException { - synchronized (synchronizer) { - while (!terminated) { - synchronizer.wait(); - } + @Override + public void waitForTermination() throws InterruptedException { + synchronized (synchronizer) { + while (!terminated) { + synchronizer.wait(); + } + } } - } - @Override - public void setTerminateOnEmpty() { - synchronized (synchronizer) { - // Count this also as inserting the terminator frame, hence trigger clearOnInsert - if (clearOnInsert) { - clear(); - clearOnInsert = false; - } + @Override + public void setTerminateOnEmpty() { + synchronized (synchronizer) { + // Count this also as inserting the terminator frame, hence trigger clearOnInsert + if (clearOnInsert) { + clear(); + clearOnInsert = false; + } - if (!terminated) { - terminateOnEmpty = true; - signalWaiters(); - } + if (!terminated) { + terminateOnEmpty = true; + signalWaiters(); + } + } } - } - @Override - public void setClearOnInsert() { - synchronized (synchronizer) { - clearOnInsert = true; - terminateOnEmpty = false; + @Override + public void setClearOnInsert() { + synchronized (synchronizer) { + clearOnInsert = true; + terminateOnEmpty = false; + } } - } - @Override - public boolean hasClearOnInsert() { - return clearOnInsert; - } + @Override + public boolean hasClearOnInsert() { + return clearOnInsert; + } - @Override - public void lockBuffer() { - locked = true; - } + @Override + public void lockBuffer() { + locked = true; + } - @Override - public boolean hasReceivedFrames() { - return receivedFrames; - } + @Override + public boolean hasReceivedFrames() { + return receivedFrames; + } - protected abstract void signalWaiters(); + protected abstract void signalWaiters(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AbstractMutableAudioFrame.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AbstractMutableAudioFrame.java index 10f9a7fe0..14f1f9a28 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AbstractMutableAudioFrame.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AbstractMutableAudioFrame.java @@ -6,51 +6,51 @@ * Base class for mutable audio frames. */ public abstract class AbstractMutableAudioFrame implements AudioFrame { - private long timecode; - private int volume; - private AudioDataFormat format; - private boolean terminator; - - @Override - public long getTimecode() { - return timecode; - } - - public void setTimecode(long timecode) { - this.timecode = timecode; - } - - @Override - public int getVolume() { - return volume; - } - - public void setVolume(int volume) { - this.volume = volume; - } - - @Override - public AudioDataFormat getFormat() { - return format; - } - - public void setFormat(AudioDataFormat format) { - this.format = format; - } - - @Override - public boolean isTerminator() { - return terminator; - } - - public void setTerminator(boolean terminator) { - this.terminator = terminator; - } - - /** - * @return An immutable instance created from this mutable audio frame. In an ideal flow, this should never be called. - */ - public ImmutableAudioFrame freeze() { - return new ImmutableAudioFrame(timecode, getData(), volume, format); - } + private long timecode; + private int volume; + private AudioDataFormat format; + private boolean terminator; + + @Override + public long getTimecode() { + return timecode; + } + + public void setTimecode(long timecode) { + this.timecode = timecode; + } + + @Override + public int getVolume() { + return volume; + } + + public void setVolume(int volume) { + this.volume = volume; + } + + @Override + public AudioDataFormat getFormat() { + return format; + } + + public void setFormat(AudioDataFormat format) { + this.format = format; + } + + @Override + public boolean isTerminator() { + return terminator; + } + + public void setTerminator(boolean terminator) { + this.terminator = terminator; + } + + /** + * @return An immutable instance created from this mutable audio frame. In an ideal flow, this should never be called. + */ + public ImmutableAudioFrame freeze() { + return new ImmutableAudioFrame(timecode, getData(), volume, format); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AllocatingAudioFrameBuffer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AllocatingAudioFrameBuffer.java index 185b7421f..fbb332537 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AllocatingAudioFrameBuffer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AllocatingAudioFrameBuffer.java @@ -16,193 +16,193 @@ * Consumes frames in a blocking manner and provides frames in a non-blocking manner. */ public class AllocatingAudioFrameBuffer extends AbstractAudioFrameBuffer { - private static final Logger log = LoggerFactory.getLogger(AudioFrameBuffer.class); - - private final int fullCapacity; - private final ArrayBlockingQueue audioFrames; - private final AtomicBoolean stopping; - - /** - * @param bufferDuration The length of the internal buffer in milliseconds - * @param format The format of the frames held in this buffer - * @param stopping Atomic boolean which has true value when the track is in a state of pending stop. - */ - public AllocatingAudioFrameBuffer(int bufferDuration, AudioDataFormat format, AtomicBoolean stopping) { - super(format); - this.fullCapacity = bufferDuration / 20 + 1; - this.audioFrames = new ArrayBlockingQueue<>(fullCapacity); - this.stopping = stopping; - } - - /** - * @return Number of frames that can be added to the buffer without blocking. - */ - @Override - public int getRemainingCapacity() { - return audioFrames.remainingCapacity(); - } - - /** - * @return Total number of frames that the buffer can hold. - */ - @Override - public int getFullCapacity() { - return fullCapacity; - } - - @Override - public AudioFrame provide() { - AudioFrame frame = audioFrames.poll(); - - if (frame == null) { - return fetchPendingTerminator(); - } else if (frame.isTerminator()) { - fetchPendingTerminator(); - return frame; + private static final Logger log = LoggerFactory.getLogger(AudioFrameBuffer.class); + + private final int fullCapacity; + private final ArrayBlockingQueue audioFrames; + private final AtomicBoolean stopping; + + /** + * @param bufferDuration The length of the internal buffer in milliseconds + * @param format The format of the frames held in this buffer + * @param stopping Atomic boolean which has true value when the track is in a state of pending stop. + */ + public AllocatingAudioFrameBuffer(int bufferDuration, AudioDataFormat format, AtomicBoolean stopping) { + super(format); + this.fullCapacity = bufferDuration / 20 + 1; + this.audioFrames = new ArrayBlockingQueue<>(fullCapacity); + this.stopping = stopping; } - return filterFrame(frame); - } + /** + * @return Number of frames that can be added to the buffer without blocking. + */ + @Override + public int getRemainingCapacity() { + return audioFrames.remainingCapacity(); + } + + /** + * @return Total number of frames that the buffer can hold. + */ + @Override + public int getFullCapacity() { + return fullCapacity; + } - @Override - public AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { - AudioFrame frame = audioFrames.poll(); + @Override + public AudioFrame provide() { + AudioFrame frame = audioFrames.poll(); - if (frame == null) { - AudioFrame terminator = fetchPendingTerminator(); - if (terminator != null) { - return terminator; - } + if (frame == null) { + return fetchPendingTerminator(); + } else if (frame.isTerminator()) { + fetchPendingTerminator(); + return frame; + } - if (timeout > 0) { - frame = audioFrames.poll(timeout, unit); + return filterFrame(frame); + } - if (frame == null || frame.isTerminator()) { - terminator = fetchPendingTerminator(); - return terminator != null ? terminator : frame; + @Override + public AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { + AudioFrame frame = audioFrames.poll(); + + if (frame == null) { + AudioFrame terminator = fetchPendingTerminator(); + if (terminator != null) { + return terminator; + } + + if (timeout > 0) { + frame = audioFrames.poll(timeout, unit); + + if (frame == null || frame.isTerminator()) { + terminator = fetchPendingTerminator(); + return terminator != null ? terminator : frame; + } + } + } else if (frame.isTerminator()) { + fetchPendingTerminator(); + return frame; } - } - } else if (frame.isTerminator()) { - fetchPendingTerminator(); - return frame; + + return filterFrame(frame); } - return filterFrame(frame); - } - - @Override - public boolean provide(MutableAudioFrame targetFrame) { - return passToMutable(provide(), targetFrame); - } - - @Override - public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) - throws TimeoutException, InterruptedException { - - return passToMutable(provide(timeout, unit), targetFrame); - } - - private boolean passToMutable(AudioFrame frame, MutableAudioFrame targetFrame) { - if (targetFrame != null && frame != null) { - if (frame.isTerminator()) { - targetFrame.setTerminator(true); - } else { - targetFrame.setTimecode(frame.getTimecode()); - targetFrame.setVolume(frame.getVolume()); - targetFrame.store(frame.getData(), 0, frame.getDataLength()); - targetFrame.setTerminator(false); - } - - return true; + @Override + public boolean provide(MutableAudioFrame targetFrame) { + return passToMutable(provide(), targetFrame); } - return false; - } + @Override + public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) + throws TimeoutException, InterruptedException { - @Override - public void clear() { - audioFrames.clear(); - } + return passToMutable(provide(timeout, unit), targetFrame); + } - @Override - public void rebuild(AudioFrameRebuilder rebuilder) { - List frames = new ArrayList<>(); - int frameCount = audioFrames.drainTo(frames); + private boolean passToMutable(AudioFrame frame, MutableAudioFrame targetFrame) { + if (targetFrame != null && frame != null) { + if (frame.isTerminator()) { + targetFrame.setTerminator(true); + } else { + targetFrame.setTimecode(frame.getTimecode()); + targetFrame.setVolume(frame.getVolume()); + targetFrame.store(frame.getData(), 0, frame.getDataLength()); + targetFrame.setTerminator(false); + } + + return true; + } - log.debug("Running rebuilder {} on {} buffered frames.", rebuilder.getClass().getSimpleName(), frameCount); + return false; + } - for (AudioFrame frame : frames) { - audioFrames.add(rebuilder.rebuild(frame)); + @Override + public void clear() { + audioFrames.clear(); } - } - - /** - * @return The timecode of the last frame in the buffer, null if the buffer is empty or is marked to be cleared upon - * receiving the next frame. - */ - @Override - public Long getLastInputTimecode() { - Long lastTimecode = null; - - synchronized (synchronizer) { - if (!clearOnInsert) { - for (AudioFrame frame : audioFrames) { - lastTimecode = frame.getTimecode(); + + @Override + public void rebuild(AudioFrameRebuilder rebuilder) { + List frames = new ArrayList<>(); + int frameCount = audioFrames.drainTo(frames); + + log.debug("Running rebuilder {} on {} buffered frames.", rebuilder.getClass().getSimpleName(), frameCount); + + for (AudioFrame frame : frames) { + audioFrames.add(rebuilder.rebuild(frame)); } - } } - return lastTimecode; - } + /** + * @return The timecode of the last frame in the buffer, null if the buffer is empty or is marked to be cleared upon + * receiving the next frame. + */ + @Override + public Long getLastInputTimecode() { + Long lastTimecode = null; + + synchronized (synchronizer) { + if (!clearOnInsert) { + for (AudioFrame frame : audioFrames) { + lastTimecode = frame.getTimecode(); + } + } + } - @Override - public void consume(AudioFrame frame) throws InterruptedException { - // If an interrupt sent along with setting the stopping status was silently consumed elsewhere, this check should - // still trigger. Guarantees that stopped tracks cannot get stuck in this method. Possible performance improvement: - // offer with timeout, check stopping if timed out, then put? - if (stopping != null && stopping.get()) { - throw new InterruptedException(); + return lastTimecode; } - if (!locked) { - receivedFrames = true; + @Override + public void consume(AudioFrame frame) throws InterruptedException { + // If an interrupt sent along with setting the stopping status was silently consumed elsewhere, this check should + // still trigger. Guarantees that stopped tracks cannot get stuck in this method. Possible performance improvement: + // offer with timeout, check stopping if timed out, then put? + if (stopping != null && stopping.get()) { + throw new InterruptedException(); + } + + if (!locked) { + receivedFrames = true; - if (clearOnInsert) { - audioFrames.clear(); - clearOnInsert = false; - } + if (clearOnInsert) { + audioFrames.clear(); + clearOnInsert = false; + } - if (frame instanceof AbstractMutableAudioFrame) { - frame = ((AbstractMutableAudioFrame) frame).freeze(); - } + if (frame instanceof AbstractMutableAudioFrame) { + frame = ((AbstractMutableAudioFrame) frame).freeze(); + } - audioFrames.put(frame); - } - } - - private AudioFrame fetchPendingTerminator() { - synchronized (synchronizer) { - if (terminateOnEmpty) { - terminateOnEmpty = false; - terminated = true; - synchronizer.notifyAll(); - return TerminatorAudioFrame.INSTANCE; - } + audioFrames.put(frame); + } } - return null; - } + private AudioFrame fetchPendingTerminator() { + synchronized (synchronizer) { + if (terminateOnEmpty) { + terminateOnEmpty = false; + terminated = true; + synchronizer.notifyAll(); + return TerminatorAudioFrame.INSTANCE; + } + } - private AudioFrame filterFrame(AudioFrame frame) { - if (frame != null && frame.getVolume() == 0) { - return new ImmutableAudioFrame(frame.getTimecode(), format.silenceBytes(), 0, format); + return null; } - return frame; - } + private AudioFrame filterFrame(AudioFrame frame) { + if (frame != null && frame.getVolume() == 0) { + return new ImmutableAudioFrame(frame.getTimecode(), format.silenceBytes(), 0, format); + } + + return frame; + } - @Override - protected void signalWaiters() { - audioFrames.offer(TerminatorAudioFrame.INSTANCE); - } + @Override + protected void signalWaiters() { + audioFrames.offer(TerminatorAudioFrame.INSTANCE); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrame.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrame.java index f6b37bda3..56d7c490a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrame.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrame.java @@ -6,43 +6,43 @@ * Represents an audio frame. */ public interface AudioFrame { - /** - * @return Absolute timecode of the frame in milliseconds. - */ - long getTimecode(); - - /** - * @return Volume of the current frame. - */ - int getVolume(); - - /** - * @return Length of the data of this frame. - */ - int getDataLength(); - - /** - * @return Byte array with the frame data. - */ - byte[] getData(); - - /** - * Before calling this method, the caller should verify that the data fits in the buffer using - * {@link #getDataLength()}. - * - * @param buffer Buffer to write the frame data to. - * @param offset Offset in the buffer to start writing at. - */ - void getData(byte[] buffer, int offset); - - /** - * @return The data format of this buffer. - */ - AudioDataFormat getFormat(); - - /** - * @return Whether this frame is a terminator. This is an internal concept of the player and should never be - * true in any frames received by the user. - */ - boolean isTerminator(); + /** + * @return Absolute timecode of the frame in milliseconds. + */ + long getTimecode(); + + /** + * @return Volume of the current frame. + */ + int getVolume(); + + /** + * @return Length of the data of this frame. + */ + int getDataLength(); + + /** + * @return Byte array with the frame data. + */ + byte[] getData(); + + /** + * Before calling this method, the caller should verify that the data fits in the buffer using + * {@link #getDataLength()}. + * + * @param buffer Buffer to write the frame data to. + * @param offset Offset in the buffer to start writing at. + */ + void getData(byte[] buffer, int offset); + + /** + * @return The data format of this buffer. + */ + AudioDataFormat getFormat(); + + /** + * @return Whether this frame is a terminator. This is an internal concept of the player and should never be + * true in any frames received by the user. + */ + boolean isTerminator(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameBuffer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameBuffer.java index abf8abe86..29858234c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameBuffer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameBuffer.java @@ -6,57 +6,58 @@ */ public interface AudioFrameBuffer extends AudioFrameProvider, AudioFrameConsumer { - /** - * @return Number of frames that can be added to the buffer without blocking. - */ - int getRemainingCapacity(); - - /** - * @return Total number of frames that the buffer can hold. - */ - int getFullCapacity(); - - /** - * Wait until another thread has consumed a terminator frame from this buffer - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void waitForTermination() throws InterruptedException; - - /** - * Signal that no more input is expected and if the content frames have been consumed, emit a terminator frame. - */ - void setTerminateOnEmpty(); - - /** - * Signal that the next frame provided to the buffer will clear the frames before it. This is useful when the next - * data is not contiguous with the current frame buffer, but the remaining frames in the buffer should be used until - * the next data arrives to prevent a situation where the buffer cannot provide any frames for a while. - */ - void setClearOnInsert(); - - /** - * @return Whether the next frame is set to clear the buffer. - */ - boolean hasClearOnInsert(); - - /** - * Clear the buffer. - */ - void clear(); - - /** - * Lock the buffer so no more incoming frames are accepted. - */ - void lockBuffer(); - - /** - * @return True if this buffer has received any input frames. - */ - boolean hasReceivedFrames(); - - /** - * @return The timecode of the last frame in the buffer, null if the buffer is empty or is marked to be cleared upon - * receiving the next frame. - */ - Long getLastInputTimecode(); + /** + * @return Number of frames that can be added to the buffer without blocking. + */ + int getRemainingCapacity(); + + /** + * @return Total number of frames that the buffer can hold. + */ + int getFullCapacity(); + + /** + * Wait until another thread has consumed a terminator frame from this buffer + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void waitForTermination() throws InterruptedException; + + /** + * Signal that no more input is expected and if the content frames have been consumed, emit a terminator frame. + */ + void setTerminateOnEmpty(); + + /** + * Signal that the next frame provided to the buffer will clear the frames before it. This is useful when the next + * data is not contiguous with the current frame buffer, but the remaining frames in the buffer should be used until + * the next data arrives to prevent a situation where the buffer cannot provide any frames for a while. + */ + void setClearOnInsert(); + + /** + * @return Whether the next frame is set to clear the buffer. + */ + boolean hasClearOnInsert(); + + /** + * Clear the buffer. + */ + void clear(); + + /** + * Lock the buffer so no more incoming frames are accepted. + */ + void lockBuffer(); + + /** + * @return True if this buffer has received any input frames. + */ + boolean hasReceivedFrames(); + + /** + * @return The timecode of the last frame in the buffer, null if the buffer is empty or is marked to be cleared upon + * receiving the next frame. + */ + Long getLastInputTimecode(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameBufferFactory.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameBufferFactory.java index 1208e53d0..5210988ce 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameBufferFactory.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameBufferFactory.java @@ -8,12 +8,12 @@ * Factory for audio frame buffers. */ public interface AudioFrameBufferFactory { - /** - * @param bufferDuration Maximum duration of the buffer. The buffer may actually hold less in case the average size of - * frames exceeds {@link AudioDataFormat#expectedChunkSize()}. - * @param format The format of the frames held in this buffer. - * @param stopping Atomic boolean which has true value when the track is in a state of pending stop. - * @return A new frame buffer instance. - */ - AudioFrameBuffer create(int bufferDuration, AudioDataFormat format, AtomicBoolean stopping); + /** + * @param bufferDuration Maximum duration of the buffer. The buffer may actually hold less in case the average size of + * frames exceeds {@link AudioDataFormat#expectedChunkSize()}. + * @param format The format of the frames held in this buffer. + * @param stopping Atomic boolean which has true value when the track is in a state of pending stop. + * @return A new frame buffer instance. + */ + AudioFrameBuffer create(int bufferDuration, AudioDataFormat format, AtomicBoolean stopping); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameConsumer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameConsumer.java index 6d6e10614..e86899bcb 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameConsumer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameConsumer.java @@ -4,16 +4,18 @@ * A consumer for audio frames */ public interface AudioFrameConsumer { - /** - * Consumes the frame, may block - * @param frame The frame to consume - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - void consume(AudioFrame frame) throws InterruptedException; + /** + * Consumes the frame, may block + * + * @param frame The frame to consume + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void consume(AudioFrame frame) throws InterruptedException; - /** - * Rebuild all caches frames - * @param rebuilder The rebuilder to use - */ - void rebuild(AudioFrameRebuilder rebuilder); + /** + * Rebuild all caches frames + * + * @param rebuilder The rebuilder to use + */ + void rebuild(AudioFrameRebuilder rebuilder); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameProvider.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameProvider.java index 872ccc1cf..f0c6a957a 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameProvider.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameProvider.java @@ -7,35 +7,35 @@ * A provider for audio frames */ public interface AudioFrameProvider { - /** - * @return Provided frame, or null if none available - */ - AudioFrame provide(); + /** + * @return Provided frame, or null if none available + */ + AudioFrame provide(); - /** - * @param timeout Specifies the maximum time to wait for data. Pass 0 for non-blocking mode. - * @param unit Specifies the time unit of the maximum wait time. - * @return Provided frame. In case wait time is above zero, null indicates that no data is not available at the - * current moment, otherwise null means the end of the track. - * @throws TimeoutException When wait time is above zero, but no track info is found in that time. - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException; + /** + * @param timeout Specifies the maximum time to wait for data. Pass 0 for non-blocking mode. + * @param unit Specifies the time unit of the maximum wait time. + * @return Provided frame. In case wait time is above zero, null indicates that no data is not available at the + * current moment, otherwise null means the end of the track. + * @throws TimeoutException When wait time is above zero, but no track info is found in that time. + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException; - /** - * @param targetFrame Frame to update with the details and data of the provided frame. - * @return true if a frame was provided. - */ - boolean provide(MutableAudioFrame targetFrame); + /** + * @param targetFrame Frame to update with the details and data of the provided frame. + * @return true if a frame was provided. + */ + boolean provide(MutableAudioFrame targetFrame); - /** - * @param targetFrame Frame to update with the details and data of the provided frame. - * @param timeout Timeout. - * @param unit Time unit for the timeout value. - * @return true if a frame was provided. - * @throws TimeoutException If no frame became available within the timeout. - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) - throws TimeoutException, InterruptedException; + /** + * @param targetFrame Frame to update with the details and data of the provided frame. + * @param timeout Timeout. + * @param unit Time unit for the timeout value. + * @return true if a frame was provided. + * @throws TimeoutException If no frame became available within the timeout. + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) + throws TimeoutException, InterruptedException; } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameProviderTools.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameProviderTools.java index a8023db6f..12df1ffe8 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameProviderTools.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameProviderTools.java @@ -9,16 +9,16 @@ * Encapsulates common behavior shared by different audio frame providers. */ public class AudioFrameProviderTools { - /** - * @param provider Delegates a call to frame provide without timeout to the timed version of it. - * @return The audio frame from provide method. - */ - public static AudioFrame delegateToTimedProvide(AudioFrameProvider provider) { - try { - return provider.provide(0, TimeUnit.MILLISECONDS); - } catch (TimeoutException | InterruptedException e) { - ExceptionTools.keepInterrupted(e); - throw new RuntimeException(e); + /** + * @param provider Delegates a call to frame provide without timeout to the timed version of it. + * @return The audio frame from provide method. + */ + public static AudioFrame delegateToTimedProvide(AudioFrameProvider provider) { + try { + return provider.provide(0, TimeUnit.MILLISECONDS); + } catch (TimeoutException | InterruptedException e) { + ExceptionTools.keepInterrupted(e); + throw new RuntimeException(e); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameRebuilder.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameRebuilder.java index f49d28de2..66e6cadf1 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameRebuilder.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioFrameRebuilder.java @@ -4,10 +4,11 @@ * Interface for classes which can rebuild audio frames. */ public interface AudioFrameRebuilder { - /** - * Rebuilds a frame (for example by reencoding) - * @param frame The audio frame - * @return The new frame (may be the same as input) - */ - AudioFrame rebuild(AudioFrame frame); + /** + * Rebuilds a frame (for example by reencoding) + * + * @param frame The audio frame + * @return The new frame (may be the same as input) + */ + AudioFrame rebuild(AudioFrame frame); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioProcessingContext.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioProcessingContext.java index acf689628..022c2f10d 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioProcessingContext.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioProcessingContext.java @@ -8,40 +8,40 @@ * Context for processing audio. Contains configuration for encoding and the output where the frames go to. */ public class AudioProcessingContext { - /** - * Audio encoding or filtering related configuration - */ - public final AudioConfiguration configuration; - /** - * Consumer for the produced audio frames - */ - public final AudioFrameBuffer frameBuffer; - /** - * Mutable volume level for the audio - */ - public final AudioPlayerOptions playerOptions; - /** - * Output format to use throughout this processing cycle - */ - public final AudioDataFormat outputFormat; - /** - * Whether filter factory change is applied to already playing tracks. - */ - public final boolean filterHotSwapEnabled; + /** + * Audio encoding or filtering related configuration + */ + public final AudioConfiguration configuration; + /** + * Consumer for the produced audio frames + */ + public final AudioFrameBuffer frameBuffer; + /** + * Mutable volume level for the audio + */ + public final AudioPlayerOptions playerOptions; + /** + * Output format to use throughout this processing cycle + */ + public final AudioDataFormat outputFormat; + /** + * Whether filter factory change is applied to already playing tracks. + */ + public final boolean filterHotSwapEnabled; - /** - * @param configuration Audio encoding or filtering related configuration - * @param frameBuffer Frame buffer for the produced audio frames - * @param playerOptions State of the audio player. - * @param outputFormat Output format to use throughout this processing cycle - */ - public AudioProcessingContext(AudioConfiguration configuration, AudioFrameBuffer frameBuffer, - AudioPlayerOptions playerOptions, AudioDataFormat outputFormat) { + /** + * @param configuration Audio encoding or filtering related configuration + * @param frameBuffer Frame buffer for the produced audio frames + * @param playerOptions State of the audio player. + * @param outputFormat Output format to use throughout this processing cycle + */ + public AudioProcessingContext(AudioConfiguration configuration, AudioFrameBuffer frameBuffer, + AudioPlayerOptions playerOptions, AudioDataFormat outputFormat) { - this.configuration = configuration; - this.frameBuffer = frameBuffer; - this.playerOptions = playerOptions; - this.outputFormat = outputFormat; - this.filterHotSwapEnabled = configuration.isFilterHotSwapEnabled(); - } + this.configuration = configuration; + this.frameBuffer = frameBuffer; + this.playerOptions = playerOptions; + this.outputFormat = outputFormat; + this.filterHotSwapEnabled = configuration.isFilterHotSwapEnabled(); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioTrackExecutor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioTrackExecutor.java index b9221c23f..7ecb1bcd4 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioTrackExecutor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioTrackExecutor.java @@ -8,46 +8,49 @@ * Executor which handles track execution and all operations on playing tracks. */ public interface AudioTrackExecutor extends AudioFrameProvider { - /** - * @return The audio buffer of this executor. - */ - AudioFrameBuffer getAudioBuffer(); - - /** - * Execute the track, which means that this thread will fill the frame buffer until the track finishes or is stopped. - * @param listener Listener for track state events - */ - void execute(TrackStateListener listener); - - /** - * Stop playing the track, terminating the thread that is filling the frame buffer. - */ - void stop(); - - /** - * @return Timecode of the last played frame or in case a seek is in progress, the timecode of the frame being seeked to. - */ - long getPosition(); - - /** - * Perform seek to the specified timecode. - * @param timecode The timecode in milliseconds - */ - void setPosition(long timecode); - - /** - * @return Current state of the executor - */ - AudioTrackState getState(); - - /** - * Set track position marker. - * @param marker Track position marker to set. - */ - void setMarker(TrackMarker marker); - - /** - * @return True if this track threw an exception before it provided any audio. - */ - boolean failedBeforeLoad(); + /** + * @return The audio buffer of this executor. + */ + AudioFrameBuffer getAudioBuffer(); + + /** + * Execute the track, which means that this thread will fill the frame buffer until the track finishes or is stopped. + * + * @param listener Listener for track state events + */ + void execute(TrackStateListener listener); + + /** + * Stop playing the track, terminating the thread that is filling the frame buffer. + */ + void stop(); + + /** + * @return Timecode of the last played frame or in case a seek is in progress, the timecode of the frame being seeked to. + */ + long getPosition(); + + /** + * Perform seek to the specified timecode. + * + * @param timecode The timecode in milliseconds + */ + void setPosition(long timecode); + + /** + * @return Current state of the executor + */ + AudioTrackState getState(); + + /** + * Set track position marker. + * + * @param marker Track position marker to set. + */ + void setMarker(TrackMarker marker); + + /** + * @return True if this track threw an exception before it provided any audio. + */ + boolean failedBeforeLoad(); } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/ImmutableAudioFrame.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/ImmutableAudioFrame.java index e32366e82..108d3cba9 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/ImmutableAudioFrame.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/ImmutableAudioFrame.java @@ -6,73 +6,73 @@ * A single audio frame. */ public class ImmutableAudioFrame implements AudioFrame { - /** - * Timecode of this frame in milliseconds. - */ - public final long timecode; + /** + * Timecode of this frame in milliseconds. + */ + public final long timecode; - /** - * Buffer for this frame, in the format specified in the format field. - */ - public final byte[] data; + /** + * Buffer for this frame, in the format specified in the format field. + */ + public final byte[] data; - /** - * Volume level of the audio in this frame. Internally when this value is 0, the data may actually contain a - * non-silent frame. This is to allow frames with 0 volume to be modified later. These frames should still be - * handled as silent frames. - */ - public final int volume; + /** + * Volume level of the audio in this frame. Internally when this value is 0, the data may actually contain a + * non-silent frame. This is to allow frames with 0 volume to be modified later. These frames should still be + * handled as silent frames. + */ + public final int volume; - /** - * Specifies the format of audio in the data buffer. - */ - public final AudioDataFormat format; + /** + * Specifies the format of audio in the data buffer. + */ + public final AudioDataFormat format; - /** - * @param timecode Timecode of this frame in milliseconds. - * @param data Buffer for this frame, in the format specified in the format field. - * @param volume Volume level of the audio in this frame. - * @param format Specifies the format of audio in the data buffer. - */ - public ImmutableAudioFrame(long timecode, byte[] data, int volume, AudioDataFormat format) { - this.timecode = timecode; - this.data = data; - this.volume = volume; - this.format = format; - } + /** + * @param timecode Timecode of this frame in milliseconds. + * @param data Buffer for this frame, in the format specified in the format field. + * @param volume Volume level of the audio in this frame. + * @param format Specifies the format of audio in the data buffer. + */ + public ImmutableAudioFrame(long timecode, byte[] data, int volume, AudioDataFormat format) { + this.timecode = timecode; + this.data = data; + this.volume = volume; + this.format = format; + } - @Override - public long getTimecode() { - return timecode; - } + @Override + public long getTimecode() { + return timecode; + } - @Override - public int getVolume() { - return volume; - } + @Override + public int getVolume() { + return volume; + } - @Override - public int getDataLength() { - return data.length; - } + @Override + public int getDataLength() { + return data.length; + } - @Override - public byte[] getData() { - return data; - } + @Override + public byte[] getData() { + return data; + } - @Override - public void getData(byte[] buffer, int offset) { - System.arraycopy(data, 0, buffer, offset, data.length); - } + @Override + public void getData(byte[] buffer, int offset) { + System.arraycopy(data, 0, buffer, offset, data.length); + } - @Override - public AudioDataFormat getFormat() { - return format; - } + @Override + public AudioDataFormat getFormat() { + return format; + } - @Override - public boolean isTerminator() { - return false; - } -} \ No newline at end of file + @Override + public boolean isTerminator() { + return false; + } +} diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/LocalAudioTrackExecutor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/LocalAudioTrackExecutor.java index 5f7f04875..da19e5801 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/LocalAudioTrackExecutor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/LocalAudioTrackExecutor.java @@ -5,11 +5,7 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayerOptions; import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; -import com.sedmelluq.discord.lavaplayer.track.AudioTrackState; -import com.sedmelluq.discord.lavaplayer.track.InternalAudioTrack; -import com.sedmelluq.discord.lavaplayer.track.TrackMarker; -import com.sedmelluq.discord.lavaplayer.track.TrackMarkerTracker; -import com.sedmelluq.discord.lavaplayer.track.TrackStateListener; +import com.sedmelluq.discord.lavaplayer.track.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,471 +26,474 @@ * Handles the execution and output buffering of an audio track. */ public class LocalAudioTrackExecutor implements AudioTrackExecutor { - private static final Logger log = LoggerFactory.getLogger(LocalAudioTrackExecutor.class); - - private final InternalAudioTrack audioTrack; - private final AudioProcessingContext processingContext; - private final boolean useSeekGhosting; - private final AudioFrameBuffer frameBuffer; - private final AtomicReference playingThread = new AtomicReference<>(); - private final AtomicBoolean queuedStop = new AtomicBoolean(false); - private final AtomicLong queuedSeek = new AtomicLong(-1); - private final AtomicLong lastFrameTimecode = new AtomicLong(0); - private final AtomicReference state = new AtomicReference<>(AudioTrackState.INACTIVE); - private final Object actionSynchronizer = new Object(); - private final TrackMarkerTracker markerTracker = new TrackMarkerTracker(); - private long externalSeekPosition = -1; - private boolean interruptibleForSeek = false; - private volatile Throwable trackException; - - /** - * @param audioTrack The audio track that this executor executes - * @param configuration Configuration to use for audio processing - * @param playerOptions Mutable player options (for example volume). - * @param useSeekGhosting Whether to keep providing old frames continuing from the previous position during a seek - * until frames from the new position arrive. - * @param bufferDuration The size of the frame buffer in milliseconds - */ - public LocalAudioTrackExecutor(InternalAudioTrack audioTrack, AudioConfiguration configuration, - AudioPlayerOptions playerOptions, boolean useSeekGhosting, int bufferDuration) { - - this.audioTrack = audioTrack; - AudioDataFormat currentFormat = configuration.getOutputFormat(); - this.frameBuffer = configuration.getFrameBufferFactory().create(bufferDuration, currentFormat, queuedStop); - this.processingContext = new AudioProcessingContext(configuration, frameBuffer, playerOptions, currentFormat); - this.useSeekGhosting = useSeekGhosting; - } - - public AudioProcessingContext getProcessingContext() { - return processingContext; - } - - public StackTraceElement[] getStackTrace() { - Thread thread = playingThread.get(); - - if (thread != null) { - StackTraceElement[] trace = thread.getStackTrace(); - - if (playingThread.get() == thread) { - return trace; - } - } - - return null; - } - - @Override - public AudioFrameBuffer getAudioBuffer() { - return frameBuffer; - } - - @Override - public void execute(TrackStateListener listener) { - InterruptedException interrupt = null; - - if (Thread.interrupted()) { - log.debug("Cleared a stray interrupt."); - } - - if (playingThread.compareAndSet(null, Thread.currentThread())) { - log.debug("Starting to play track {} locally with listener {}", audioTrack.getInfo().identifier, listener); - - state.set(AudioTrackState.LOADING); - - try { - audioTrack.process(this); - - log.debug("Playing track {} finished or was stopped.", audioTrack.getIdentifier()); - } catch (Throwable e) { - // Temporarily clear the interrupted status so it would not disrupt listener methods. - interrupt = findInterrupt(e); - - if (interrupt != null && checkStopped()) { - log.debug("Track {} was interrupted outside of execution loop.", audioTrack.getIdentifier()); - } else { - frameBuffer.setTerminateOnEmpty(); + private static final Logger log = LoggerFactory.getLogger(LocalAudioTrackExecutor.class); + + private final InternalAudioTrack audioTrack; + private final AudioProcessingContext processingContext; + private final boolean useSeekGhosting; + private final AudioFrameBuffer frameBuffer; + private final AtomicReference playingThread = new AtomicReference<>(); + private final AtomicBoolean queuedStop = new AtomicBoolean(false); + private final AtomicLong queuedSeek = new AtomicLong(-1); + private final AtomicLong lastFrameTimecode = new AtomicLong(0); + private final AtomicReference state = new AtomicReference<>(AudioTrackState.INACTIVE); + private final Object actionSynchronizer = new Object(); + private final TrackMarkerTracker markerTracker = new TrackMarkerTracker(); + private long externalSeekPosition = -1; + private boolean interruptibleForSeek = false; + private volatile Throwable trackException; - FriendlyException exception = ExceptionTools.wrapUnfriendlyExceptions("Something broke when playing the track.", FAULT, e); - ExceptionTools.log(log, exception, "playback of " + audioTrack.getIdentifier()); + /** + * @param audioTrack The audio track that this executor executes + * @param configuration Configuration to use for audio processing + * @param playerOptions Mutable player options (for example volume). + * @param useSeekGhosting Whether to keep providing old frames continuing from the previous position during a seek + * until frames from the new position arrive. + * @param bufferDuration The size of the frame buffer in milliseconds + */ + public LocalAudioTrackExecutor(InternalAudioTrack audioTrack, AudioConfiguration configuration, + AudioPlayerOptions playerOptions, boolean useSeekGhosting, int bufferDuration) { + + this.audioTrack = audioTrack; + AudioDataFormat currentFormat = configuration.getOutputFormat(); + this.frameBuffer = configuration.getFrameBufferFactory().create(bufferDuration, currentFormat, queuedStop); + this.processingContext = new AudioProcessingContext(configuration, frameBuffer, playerOptions, currentFormat); + this.useSeekGhosting = useSeekGhosting; + } - trackException = exception; - listener.onTrackException(audioTrack, exception); + public AudioProcessingContext getProcessingContext() { + return processingContext; + } - ExceptionTools.rethrowErrors(e); - } - } finally { - synchronized (actionSynchronizer) { - interrupt = interrupt != null ? interrupt : findInterrupt(null); + public StackTraceElement[] getStackTrace() { + Thread thread = playingThread.get(); - playingThread.compareAndSet(Thread.currentThread(), null); + if (thread != null) { + StackTraceElement[] trace = thread.getStackTrace(); - markerTracker.trigger(ENDED); - state.set(AudioTrackState.FINISHED); + if (playingThread.get() == thread) { + return trace; + } } - if (interrupt != null) { - Thread.currentThread().interrupt(); - } - } - } else { - log.warn("Tried to start an already playing track {}", audioTrack.getIdentifier()); - } - } - - @Override - public void stop() { - synchronized (actionSynchronizer) { - Thread thread = playingThread.get(); - - if (thread != null) { - log.debug("Requesting stop for track {}", audioTrack.getIdentifier()); - - queuedStop.compareAndSet(false, true); - thread.interrupt(); - } else { - log.debug("Tried to stop track {} which is not playing.", audioTrack.getIdentifier()); - } - } - } - - /** - * @return True if the track has been scheduled to stop and then clears the scheduled stop bit. - */ - public boolean checkStopped() { - if (queuedStop.compareAndSet(true, false)) { - state.set(AudioTrackState.STOPPING); - return true; - } - - return false; - } - - /** - * Wait until all the frames from the frame buffer have been consumed. Keeps the buffering thread alive to keep it - * interruptible for seeking until buffer is empty. - * - * @throws InterruptedException When interrupted externally (or for seek/stop). - */ - public void waitOnEnd() throws InterruptedException { - frameBuffer.setTerminateOnEmpty(); - frameBuffer.waitForTermination(); - } - - /** - * Interrupt the buffering thread, either stop or seek should have been set beforehand. - * @return True if there was a thread to interrupt. - */ - public boolean interrupt() { - synchronized (actionSynchronizer) { - Thread thread = playingThread.get(); - - if (thread != null) { - thread.interrupt(); - return true; - } + return null; + } - return false; + @Override + public AudioFrameBuffer getAudioBuffer() { + return frameBuffer; } - } - @Override - public long getPosition() { - long seek = queuedSeek.get(); - return seek != -1 ? seek : lastFrameTimecode.get(); - } + @Override + public void execute(TrackStateListener listener) { + InterruptedException interrupt = null; - @Override - public void setPosition(long timecode) { - if (!audioTrack.isSeekable()) { - return; - } + if (Thread.interrupted()) { + log.debug("Cleared a stray interrupt."); + } - synchronized (actionSynchronizer) { - if (timecode < 0) { - timecode = 0; - } + if (playingThread.compareAndSet(null, Thread.currentThread())) { + log.debug("Starting to play track {} locally with listener {}", audioTrack.getInfo().identifier, listener); - queuedSeek.set(timecode); + state.set(AudioTrackState.LOADING); - if (!useSeekGhosting) { - frameBuffer.clear(); - } + try { + audioTrack.process(this); - interruptForSeek(); - } - } + log.debug("Playing track {} finished or was stopped.", audioTrack.getIdentifier()); + } catch (Throwable e) { + // Temporarily clear the interrupted status so it would not disrupt listener methods. + interrupt = findInterrupt(e); - @Override - public AudioTrackState getState() { - return state.get(); - } + if (interrupt != null && checkStopped()) { + log.debug("Track {} was interrupted outside of execution loop.", audioTrack.getIdentifier()); + } else { + frameBuffer.setTerminateOnEmpty(); - /** - * @return True if this track is currently in the middle of a seek. - */ - private boolean isPerformingSeek() { - return queuedSeek.get() != -1 || (useSeekGhosting && frameBuffer.hasClearOnInsert()); - } + FriendlyException exception = ExceptionTools.wrapUnfriendlyExceptions("Something broke when playing the track.", FAULT, e); + ExceptionTools.log(log, exception, "playback of " + audioTrack.getIdentifier()); - @Override - public void setMarker(TrackMarker marker) { - markerTracker.set(marker, getPosition()); - } + trackException = exception; + listener.onTrackException(audioTrack, exception); - @Override - public boolean failedBeforeLoad() { - return trackException != null && !frameBuffer.hasReceivedFrames(); - } + ExceptionTools.rethrowErrors(e); + } + } finally { + synchronized (actionSynchronizer) { + interrupt = interrupt != null ? interrupt : findInterrupt(null); - public void executeProcessingLoop(ReadExecutor readExecutor, SeekExecutor seekExecutor) { - executeProcessingLoop(readExecutor, seekExecutor, true); - } + playingThread.compareAndSet(Thread.currentThread(), null); - /** - * Execute the read and seek loop for the track. - * @param readExecutor Callback for reading the track - * @param seekExecutor Callback for performing a seek on the track, may be null on a non-seekable track - */ - public void executeProcessingLoop(ReadExecutor readExecutor, SeekExecutor seekExecutor, boolean waitOnEnd) { - boolean proceed = true; + markerTracker.trigger(ENDED); + state.set(AudioTrackState.FINISHED); + } - if (checkPendingSeek(seekExecutor) == SeekResult.EXTERNAL_SEEK) { - return; + if (interrupt != null) { + Thread.currentThread().interrupt(); + } + } + } else { + log.warn("Tried to start an already playing track {}", audioTrack.getIdentifier()); + } } - while (proceed) { - state.set(AudioTrackState.PLAYING); - proceed = false; + @Override + public void stop() { + synchronized (actionSynchronizer) { + Thread thread = playingThread.get(); + + if (thread != null) { + log.debug("Requesting stop for track {}", audioTrack.getIdentifier()); - try { - // An interrupt may have been placed while we were handling the previous one. - if (Thread.interrupted() && !handlePlaybackInterrupt(null, seekExecutor)) { - break; + queuedStop.compareAndSet(false, true); + thread.interrupt(); + } else { + log.debug("Tried to stop track {} which is not playing.", audioTrack.getIdentifier()); + } } + } - setInterruptibleForSeek(true); - readExecutor.performRead(); - setInterruptibleForSeek(false); - - if (seekExecutor != null && externalSeekPosition != -1) { - long nextPosition = externalSeekPosition; - externalSeekPosition = -1; - performSeek(seekExecutor, nextPosition); - proceed = true; - } else if (waitOnEnd) { - waitOnEnd(); + /** + * @return True if the track has been scheduled to stop and then clears the scheduled stop bit. + */ + public boolean checkStopped() { + if (queuedStop.compareAndSet(true, false)) { + state.set(AudioTrackState.STOPPING); + return true; } - } catch (Exception e) { - setInterruptibleForSeek(false); - InterruptedException interruption = findInterrupt(e); - if (interruption != null) { - proceed = handlePlaybackInterrupt(interruption, seekExecutor); - } else { - throw ExceptionTools.wrapUnfriendlyExceptions("Something went wrong when decoding the track.", FAULT, e); + return false; + } + + /** + * Wait until all the frames from the frame buffer have been consumed. Keeps the buffering thread alive to keep it + * interruptible for seeking until buffer is empty. + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + public void waitOnEnd() throws InterruptedException { + frameBuffer.setTerminateOnEmpty(); + frameBuffer.waitForTermination(); + } + + /** + * Interrupt the buffering thread, either stop or seek should have been set beforehand. + * + * @return True if there was a thread to interrupt. + */ + public boolean interrupt() { + synchronized (actionSynchronizer) { + Thread thread = playingThread.get(); + + if (thread != null) { + thread.interrupt(); + return true; + } + + return false; } - } } - } - private void setInterruptibleForSeek(boolean state) { - synchronized (actionSynchronizer) { - interruptibleForSeek = state; + @Override + public long getPosition() { + long seek = queuedSeek.get(); + return seek != -1 ? seek : lastFrameTimecode.get(); } - } - private void interruptForSeek() { - boolean interrupted = false; + @Override + public void setPosition(long timecode) { + if (!audioTrack.isSeekable()) { + return; + } + + synchronized (actionSynchronizer) { + if (timecode < 0) { + timecode = 0; + } - synchronized (actionSynchronizer) { - if (interruptibleForSeek) { - interruptibleForSeek = false; - Thread thread = playingThread.get(); + queuedSeek.set(timecode); - if (thread != null) { - thread.interrupt(); - interrupted = true; + if (!useSeekGhosting) { + frameBuffer.clear(); + } + + interruptForSeek(); } - } } - if (interrupted) { - log.debug("Interrupting playing thread to perform a seek {}", audioTrack.getIdentifier()); - } else { - log.debug("Seeking on track {} while not in playback loop.", audioTrack.getIdentifier()); + @Override + public AudioTrackState getState() { + return state.get(); } - } - private boolean handlePlaybackInterrupt(InterruptedException interruption, SeekExecutor seekExecutor) { - Thread.interrupted(); + /** + * @return True if this track is currently in the middle of a seek. + */ + private boolean isPerformingSeek() { + return queuedSeek.get() != -1 || (useSeekGhosting && frameBuffer.hasClearOnInsert()); + } - if (checkStopped()) { - markerTracker.trigger(STOPPED); - return false; + @Override + public void setMarker(TrackMarker marker) { + markerTracker.set(marker, getPosition()); } - SeekResult seekResult = checkPendingSeek(seekExecutor); + @Override + public boolean failedBeforeLoad() { + return trackException != null && !frameBuffer.hasReceivedFrames(); + } - if (seekResult != SeekResult.NO_SEEK) { - // Double-check, might have received a stop request while seeking - if (checkStopped()) { - markerTracker.trigger(STOPPED); - return false; - } else { - return seekResult == SeekResult.INTERNAL_SEEK; - } - } else if (interruption != null) { - Thread.currentThread().interrupt(); - throw new FriendlyException("The track was unexpectedly terminated.", SUSPICIOUS, interruption); - } else { - return true; + public void executeProcessingLoop(ReadExecutor readExecutor, SeekExecutor seekExecutor) { + executeProcessingLoop(readExecutor, seekExecutor, true); } - } - private InterruptedException findInterrupt(Throwable throwable) { - InterruptedException exception = findDeepException(throwable, InterruptedException.class); + /** + * Execute the read and seek loop for the track. + * + * @param readExecutor Callback for reading the track + * @param seekExecutor Callback for performing a seek on the track, may be null on a non-seekable track + */ + public void executeProcessingLoop(ReadExecutor readExecutor, SeekExecutor seekExecutor, boolean waitOnEnd) { + boolean proceed = true; - if (exception == null) { - InterruptedIOException ioException = findDeepException(throwable, InterruptedIOException.class); + if (checkPendingSeek(seekExecutor) == SeekResult.EXTERNAL_SEEK) { + return; + } - if (ioException != null && (ioException.getMessage() == null || !ioException.getMessage().contains("timed out"))) { - exception = new InterruptedException(ioException.getMessage()); - } + while (proceed) { + state.set(AudioTrackState.PLAYING); + proceed = false; + + try { + // An interrupt may have been placed while we were handling the previous one. + if (Thread.interrupted() && !handlePlaybackInterrupt(null, seekExecutor)) { + break; + } + + setInterruptibleForSeek(true); + readExecutor.performRead(); + setInterruptibleForSeek(false); + + if (seekExecutor != null && externalSeekPosition != -1) { + long nextPosition = externalSeekPosition; + externalSeekPosition = -1; + performSeek(seekExecutor, nextPosition); + proceed = true; + } else if (waitOnEnd) { + waitOnEnd(); + } + } catch (Exception e) { + setInterruptibleForSeek(false); + InterruptedException interruption = findInterrupt(e); + + if (interruption != null) { + proceed = handlePlaybackInterrupt(interruption, seekExecutor); + } else { + throw ExceptionTools.wrapUnfriendlyExceptions("Something went wrong when decoding the track.", FAULT, e); + } + } + } } - if (exception == null && Thread.interrupted()) { - return new InterruptedException(); + private void setInterruptibleForSeek(boolean state) { + synchronized (actionSynchronizer) { + interruptibleForSeek = state; + } } - return exception; - } + private void interruptForSeek() { + boolean interrupted = false; + + synchronized (actionSynchronizer) { + if (interruptibleForSeek) { + interruptibleForSeek = false; + Thread thread = playingThread.get(); + + if (thread != null) { + thread.interrupt(); + interrupted = true; + } + } + } + + if (interrupted) { + log.debug("Interrupting playing thread to perform a seek {}", audioTrack.getIdentifier()); + } else { + log.debug("Seeking on track {} while not in playback loop.", audioTrack.getIdentifier()); + } + } + + private boolean handlePlaybackInterrupt(InterruptedException interruption, SeekExecutor seekExecutor) { + Thread.interrupted(); + + if (checkStopped()) { + markerTracker.trigger(STOPPED); + return false; + } - /** - * Performs a seek if it scheduled. - * @param seekExecutor Callback for performing a seek on the track - * @return True if a seek was performed - */ - private SeekResult checkPendingSeek(SeekExecutor seekExecutor) { - if (!audioTrack.isSeekable()) { - return SeekResult.NO_SEEK; + SeekResult seekResult = checkPendingSeek(seekExecutor); + + if (seekResult != SeekResult.NO_SEEK) { + // Double-check, might have received a stop request while seeking + if (checkStopped()) { + markerTracker.trigger(STOPPED); + return false; + } else { + return seekResult == SeekResult.INTERNAL_SEEK; + } + } else if (interruption != null) { + Thread.currentThread().interrupt(); + throw new FriendlyException("The track was unexpectedly terminated.", SUSPICIOUS, interruption); + } else { + return true; + } } - long seekPosition; + private InterruptedException findInterrupt(Throwable throwable) { + InterruptedException exception = findDeepException(throwable, InterruptedException.class); - synchronized (actionSynchronizer) { - seekPosition = queuedSeek.get(); + if (exception == null) { + InterruptedIOException ioException = findDeepException(throwable, InterruptedIOException.class); - if (seekPosition == -1) { - return SeekResult.NO_SEEK; - } + if (ioException != null && (ioException.getMessage() == null || !ioException.getMessage().contains("timed out"))) { + exception = new InterruptedException(ioException.getMessage()); + } + } - log.debug("Track {} interrupted for seeking to {}.", audioTrack.getIdentifier(), seekPosition); - applySeekState(seekPosition); + if (exception == null && Thread.interrupted()) { + return new InterruptedException(); + } + + return exception; } - if (seekExecutor != null) { - performSeek(seekExecutor, seekPosition); - return SeekResult.INTERNAL_SEEK; - } else { - externalSeekPosition = seekPosition; - return SeekResult.EXTERNAL_SEEK; + /** + * Performs a seek if it scheduled. + * + * @param seekExecutor Callback for performing a seek on the track + * @return True if a seek was performed + */ + private SeekResult checkPendingSeek(SeekExecutor seekExecutor) { + if (!audioTrack.isSeekable()) { + return SeekResult.NO_SEEK; + } + + long seekPosition; + + synchronized (actionSynchronizer) { + seekPosition = queuedSeek.get(); + + if (seekPosition == -1) { + return SeekResult.NO_SEEK; + } + + log.debug("Track {} interrupted for seeking to {}.", audioTrack.getIdentifier(), seekPosition); + applySeekState(seekPosition); + } + + if (seekExecutor != null) { + performSeek(seekExecutor, seekPosition); + return SeekResult.INTERNAL_SEEK; + } else { + externalSeekPosition = seekPosition; + return SeekResult.EXTERNAL_SEEK; + } } - } - private void performSeek(SeekExecutor seekExecutor, long seekPosition) { - try { - seekExecutor.performSeek(seekPosition); - } catch (Exception e) { - throw ExceptionTools.wrapUnfriendlyExceptions("Something went wrong when seeking to a position.", FAULT, e); + private void performSeek(SeekExecutor seekExecutor, long seekPosition) { + try { + seekExecutor.performSeek(seekPosition); + } catch (Exception e) { + throw ExceptionTools.wrapUnfriendlyExceptions("Something went wrong when seeking to a position.", FAULT, e); + } } - } - private void applySeekState(long seekPosition) { - state.set(AudioTrackState.SEEKING); + private void applySeekState(long seekPosition) { + state.set(AudioTrackState.SEEKING); - if (useSeekGhosting) { - frameBuffer.setClearOnInsert(); - } else { - frameBuffer.clear(); + if (useSeekGhosting) { + frameBuffer.setClearOnInsert(); + } else { + frameBuffer.clear(); + } + + queuedSeek.set(-1); + markerTracker.checkSeekTimecode(seekPosition); } - queuedSeek.set(-1); - markerTracker.checkSeekTimecode(seekPosition); - } + @Override + public AudioFrame provide() { + AudioFrame frame = frameBuffer.provide(); + processProvidedFrame(frame); + return frame; + } - @Override - public AudioFrame provide() { - AudioFrame frame = frameBuffer.provide(); - processProvidedFrame(frame); - return frame; - } + @Override + public AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { + AudioFrame frame = frameBuffer.provide(timeout, unit); + processProvidedFrame(frame); + return frame; + } - @Override - public AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { - AudioFrame frame = frameBuffer.provide(timeout, unit); - processProvidedFrame(frame); - return frame; - } + @Override + public boolean provide(MutableAudioFrame targetFrame) { + if (frameBuffer.provide(targetFrame)) { + processProvidedFrame(targetFrame); + return true; + } - @Override - public boolean provide(MutableAudioFrame targetFrame) { - if (frameBuffer.provide(targetFrame)) { - processProvidedFrame(targetFrame); - return true; + return false; } - return false; - } + @Override + public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) + throws TimeoutException, InterruptedException { - @Override - public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) - throws TimeoutException, InterruptedException { + if (frameBuffer.provide(targetFrame, timeout, unit)) { + processProvidedFrame(targetFrame); + return true; + } - if (frameBuffer.provide(targetFrame, timeout, unit)) { - processProvidedFrame(targetFrame); - return true; + return true; } - return true; - } + private void processProvidedFrame(AudioFrame frame) { + if (frame != null && !frame.isTerminator()) { + if (!isPerformingSeek()) { + markerTracker.checkPlaybackTimecode(frame.getTimecode()); + } - private void processProvidedFrame(AudioFrame frame) { - if (frame != null && !frame.isTerminator()) { - if (!isPerformingSeek()) { - markerTracker.checkPlaybackTimecode(frame.getTimecode()); - } - - lastFrameTimecode.set(frame.getTimecode()); + lastFrameTimecode.set(frame.getTimecode()); + } } - } - /** - * Read executor, see method description - */ - public interface ReadExecutor { /** - * Reads until interrupted or EOF. - * - * @throws InterruptedException When interrupted externally (or for seek/stop). + * Read executor, see method description */ - void performRead() throws Exception; - } + public interface ReadExecutor { + /** + * Reads until interrupted or EOF. + * + * @throws InterruptedException When interrupted externally (or for seek/stop). + */ + void performRead() throws Exception; + } - /** - * Seek executor, see method description - */ - public interface SeekExecutor { /** - * Perform a seek to the specified position - * - * @param position Position in milliseconds + * Seek executor, see method description */ - void performSeek(long position) throws Exception; - } - - private enum SeekResult { - NO_SEEK, - INTERNAL_SEEK, - EXTERNAL_SEEK - } + public interface SeekExecutor { + /** + * Perform a seek to the specified position + * + * @param position Position in milliseconds + */ + void performSeek(long position) throws Exception; + } + + private enum SeekResult { + NO_SEEK, + INTERNAL_SEEK, + EXTERNAL_SEEK + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/MutableAudioFrame.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/MutableAudioFrame.java index 26e8ff610..7c325c0cc 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/MutableAudioFrame.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/MutableAudioFrame.java @@ -6,52 +6,52 @@ * A mutable audio frame. */ public class MutableAudioFrame extends AbstractMutableAudioFrame { - private ByteBuffer frameBuffer; - private int framePosition; - private int frameLength; + private ByteBuffer frameBuffer; + private int framePosition; + private int frameLength; - /** - * This should be called only by the requester of a frame. - * - * @param frameBuffer Buffer to use internally. - */ - public void setBuffer(ByteBuffer frameBuffer) { - this.frameBuffer = frameBuffer; - this.framePosition = frameBuffer.position(); - this.frameLength = frameBuffer.remaining(); - } + /** + * This should be called only by the requester of a frame. + * + * @param frameBuffer Buffer to use internally. + */ + public void setBuffer(ByteBuffer frameBuffer) { + this.frameBuffer = frameBuffer; + this.framePosition = frameBuffer.position(); + this.frameLength = frameBuffer.remaining(); + } - /** - * This should be called only by the provider of a frame. - * - * @param buffer Buffer to copy data from into the internal buffer of this instance. - * @param offset Offset in the buffer. - * @param length Length of the data to copy. - */ - public void store(byte[] buffer, int offset, int length) { - frameBuffer.position(framePosition); - frameBuffer.limit(frameBuffer.capacity()); - frameBuffer.put(buffer, offset, length); - frameLength = length; - } + /** + * This should be called only by the provider of a frame. + * + * @param buffer Buffer to copy data from into the internal buffer of this instance. + * @param offset Offset in the buffer. + * @param length Length of the data to copy. + */ + public void store(byte[] buffer, int offset, int length) { + frameBuffer.position(framePosition); + frameBuffer.limit(frameBuffer.capacity()); + frameBuffer.put(buffer, offset, length); + frameLength = length; + } - @Override - public int getDataLength() { - return frameLength; - } + @Override + public int getDataLength() { + return frameLength; + } - @Override - public byte[] getData() { - byte[] data = new byte[getDataLength()]; - getData(data, 0); - return data; - } + @Override + public byte[] getData() { + byte[] data = new byte[getDataLength()]; + getData(data, 0); + return data; + } - @Override - public void getData(byte[] buffer, int offset) { - int previous = frameBuffer.position(); - frameBuffer.position(framePosition); - frameBuffer.get(buffer, offset, frameLength); - frameBuffer.position(previous); - } + @Override + public void getData(byte[] buffer, int offset) { + int previous = frameBuffer.position(); + frameBuffer.position(framePosition); + frameBuffer.get(buffer, offset, frameLength); + frameBuffer.position(previous); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/NonAllocatingAudioFrameBuffer.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/NonAllocatingAudioFrameBuffer.java index c79ec46f9..439b778af 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/NonAllocatingAudioFrameBuffer.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/NonAllocatingAudioFrameBuffer.java @@ -14,307 +14,307 @@ * frames are preallocated, and for the data there is one byte buffer which is used as a ring buffer for the frame data. */ public class NonAllocatingAudioFrameBuffer extends AbstractAudioFrameBuffer { - private static final Logger log = LoggerFactory.getLogger(NonAllocatingAudioFrameBuffer.class); - - private final int worstCaseFrameCount; - private final ReferenceMutableAudioFrame[] frames; - private final ReferenceMutableAudioFrame silentFrame; - private final AtomicBoolean stopping; - private MutableAudioFrame bridgeFrame; - - private final byte[] frameBuffer; - private int firstFrame; - private int frameCount; - - /** - * @param bufferDuration The length of the internal buffer in milliseconds - * @param format The format of the frames held in this buffer - * @param stopping Atomic boolean which has true value when the track is in a state of pending stop. - */ - public NonAllocatingAudioFrameBuffer(int bufferDuration, AudioDataFormat format, AtomicBoolean stopping) { - super(format); - int maximumFrameCount = bufferDuration / (int) format.frameDuration() + 1; - frames = createFrames(maximumFrameCount, format); - silentFrame = createSilentFrame(format); - this.frameBuffer = new byte[format.expectedChunkSize() * maximumFrameCount]; - worstCaseFrameCount = frameBuffer.length / format.maximumChunkSize(); - this.stopping = stopping; - } - - /** - * @return Number of frames that can be added to the buffer without blocking. - */ - @Override - public int getRemainingCapacity() { - synchronized (synchronizer) { - if (frameCount == 0) { - return worstCaseFrameCount; - } + private static final Logger log = LoggerFactory.getLogger(NonAllocatingAudioFrameBuffer.class); + + private final int worstCaseFrameCount; + private final ReferenceMutableAudioFrame[] frames; + private final ReferenceMutableAudioFrame silentFrame; + private final AtomicBoolean stopping; + private MutableAudioFrame bridgeFrame; + + private final byte[] frameBuffer; + private int firstFrame; + private int frameCount; + + /** + * @param bufferDuration The length of the internal buffer in milliseconds + * @param format The format of the frames held in this buffer + * @param stopping Atomic boolean which has true value when the track is in a state of pending stop. + */ + public NonAllocatingAudioFrameBuffer(int bufferDuration, AudioDataFormat format, AtomicBoolean stopping) { + super(format); + int maximumFrameCount = bufferDuration / (int) format.frameDuration() + 1; + frames = createFrames(maximumFrameCount, format); + silentFrame = createSilentFrame(format); + this.frameBuffer = new byte[format.expectedChunkSize() * maximumFrameCount]; + worstCaseFrameCount = frameBuffer.length / format.maximumChunkSize(); + this.stopping = stopping; + } - int lastFrame = wrappedFrameIndex(firstFrame + frameCount - 1); + /** + * @return Number of frames that can be added to the buffer without blocking. + */ + @Override + public int getRemainingCapacity() { + synchronized (synchronizer) { + if (frameCount == 0) { + return worstCaseFrameCount; + } - int bufferHead = frames[firstFrame].getFrameOffset(); - int bufferTail = frames[lastFrame].getFrameEndOffset(); + int lastFrame = wrappedFrameIndex(firstFrame + frameCount - 1); - int maximumFrameSize = format.maximumChunkSize(); + int bufferHead = frames[firstFrame].getFrameOffset(); + int bufferTail = frames[lastFrame].getFrameEndOffset(); - if (bufferHead < bufferTail) { - return (frameBuffer.length - bufferTail) / maximumFrameSize + bufferHead / maximumFrameSize; - } else { - return (bufferHead - bufferTail) / maximumFrameSize; - } - } - } - - /** - * @return Total number of frames that the buffer can hold. - */ - @Override - public int getFullCapacity() { - return worstCaseFrameCount; - } - - @Override - public void consume(AudioFrame frame) throws InterruptedException { - // If an interrupt sent along with setting the stopping status was silently consumed elsewhere, this check should - // still trigger. Guarantees that stopped tracks cannot get stuck in this method. Possible performance improvement: - // offer with timeout, check stopping if timed out, then put? - if (stopping != null && stopping.get()) { - throw new InterruptedException(); + int maximumFrameSize = format.maximumChunkSize(); + + if (bufferHead < bufferTail) { + return (frameBuffer.length - bufferTail) / maximumFrameSize + bufferHead / maximumFrameSize; + } else { + return (bufferHead - bufferTail) / maximumFrameSize; + } + } } - synchronized (synchronizer) { - if (!locked) { - receivedFrames = true; + /** + * @return Total number of frames that the buffer can hold. + */ + @Override + public int getFullCapacity() { + return worstCaseFrameCount; + } - if (clearOnInsert) { - clear(); - clearOnInsert = false; + @Override + public void consume(AudioFrame frame) throws InterruptedException { + // If an interrupt sent along with setting the stopping status was silently consumed elsewhere, this check should + // still trigger. Guarantees that stopped tracks cannot get stuck in this method. Possible performance improvement: + // offer with timeout, check stopping if timed out, then put? + if (stopping != null && stopping.get()) { + throw new InterruptedException(); } - while (!attemptStore(frame)) { - synchronizer.wait(); - } + synchronized (synchronizer) { + if (!locked) { + receivedFrames = true; + + if (clearOnInsert) { + clear(); + clearOnInsert = false; + } + + while (!attemptStore(frame)) { + synchronizer.wait(); + } - synchronizer.notifyAll(); - } + synchronizer.notifyAll(); + } + } } - } - @Override - public AudioFrame provide() { - synchronized (synchronizer) { - if (provide(getBridgeFrame())) { - return unwrapBridgeFrame(); - } + @Override + public AudioFrame provide() { + synchronized (synchronizer) { + if (provide(getBridgeFrame())) { + return unwrapBridgeFrame(); + } - return null; + return null; + } } - } - @Override - public AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { - synchronized (synchronizer) { - if (provide(getBridgeFrame(), timeout, unit)) { - return unwrapBridgeFrame(); - } + @Override + public AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { + synchronized (synchronizer) { + if (provide(getBridgeFrame(), timeout, unit)) { + return unwrapBridgeFrame(); + } - return null; + return null; + } } - } - - @Override - public boolean provide(MutableAudioFrame targetFrame) { - synchronized (synchronizer) { - if (frameCount == 0) { - if (terminateOnEmpty) { - popPendingTerminator(targetFrame); - synchronizer.notifyAll(); - return true; + + @Override + public boolean provide(MutableAudioFrame targetFrame) { + synchronized (synchronizer) { + if (frameCount == 0) { + if (terminateOnEmpty) { + popPendingTerminator(targetFrame); + synchronizer.notifyAll(); + return true; + } + return false; + } else { + popFrame(targetFrame); + synchronizer.notifyAll(); + return true; + } } - return false; - } else { - popFrame(targetFrame); - synchronizer.notifyAll(); - return true; - } } - } - @Override - public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) - throws TimeoutException, InterruptedException { + @Override + public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) + throws TimeoutException, InterruptedException { - long currentTime = System.nanoTime(); - long endTime = currentTime + unit.toMillis(timeout); + long currentTime = System.nanoTime(); + long endTime = currentTime + unit.toMillis(timeout); - synchronized (synchronizer) { - while (frameCount == 0) { - if (terminateOnEmpty) { - popPendingTerminator(targetFrame); - synchronizer.notifyAll(); - return true; - } + synchronized (synchronizer) { + while (frameCount == 0) { + if (terminateOnEmpty) { + popPendingTerminator(targetFrame); + synchronizer.notifyAll(); + return true; + } - synchronizer.wait(endTime - currentTime); - currentTime = System.nanoTime(); + synchronizer.wait(endTime - currentTime); + currentTime = System.nanoTime(); - if (currentTime >= endTime) { - throw new TimeoutException(); - } - } + if (currentTime >= endTime) { + throw new TimeoutException(); + } + } - popFrame(targetFrame); - synchronizer.notifyAll(); - return true; + popFrame(targetFrame); + synchronizer.notifyAll(); + return true; + } } - } - private void popFrame(MutableAudioFrame targetFrame) { - ReferenceMutableAudioFrame frame = frames[firstFrame]; + private void popFrame(MutableAudioFrame targetFrame) { + ReferenceMutableAudioFrame frame = frames[firstFrame]; - if (frame.getVolume() == 0) { - silentFrame.setTimecode(frame.getTimecode()); - frame = silentFrame; - } + if (frame.getVolume() == 0) { + silentFrame.setTimecode(frame.getTimecode()); + frame = silentFrame; + } - targetFrame.setTimecode(frame.getTimecode()); - targetFrame.setVolume(frame.getVolume()); - targetFrame.setTerminator(false); - targetFrame.store(frame.getFrameBuffer(), frame.getFrameOffset(), frame.getDataLength()); + targetFrame.setTimecode(frame.getTimecode()); + targetFrame.setVolume(frame.getVolume()); + targetFrame.setTerminator(false); + targetFrame.store(frame.getFrameBuffer(), frame.getFrameOffset(), frame.getDataLength()); - firstFrame = wrappedFrameIndex(firstFrame + 1); - frameCount--; - } + firstFrame = wrappedFrameIndex(firstFrame + 1); + frameCount--; + } - private void popPendingTerminator(MutableAudioFrame frame) { - terminateOnEmpty = false; - terminated = true; + private void popPendingTerminator(MutableAudioFrame frame) { + terminateOnEmpty = false; + terminated = true; - frame.setTerminator(true); - } + frame.setTerminator(true); + } - @Override - public void clear() { - synchronized (synchronizer) { - frameCount = 0; + @Override + public void clear() { + synchronized (synchronizer) { + frameCount = 0; + } } - } - - @Override - public void rebuild(AudioFrameRebuilder rebuilder) { - log.debug("Frame rebuild not supported on non-allocating frame buffer yet."); - } - - @Override - public Long getLastInputTimecode() { - synchronized (synchronizer) { - if (!clearOnInsert && frameCount > 0) { - return frames[wrappedFrameIndex(firstFrame + frameCount - 1)].getTimecode(); - } + + @Override + public void rebuild(AudioFrameRebuilder rebuilder) { + log.debug("Frame rebuild not supported on non-allocating frame buffer yet."); } - return null; - } + @Override + public Long getLastInputTimecode() { + synchronized (synchronizer) { + if (!clearOnInsert && frameCount > 0) { + return frames[wrappedFrameIndex(firstFrame + frameCount - 1)].getTimecode(); + } + } - private boolean attemptStore(AudioFrame frame) { - if (frameCount >= frames.length) { - return false; + return null; } - int frameLength = frame.getDataLength(); - int frameBufferLength = frameBuffer.length; - - if (frameCount == 0) { - firstFrame = 0; + private boolean attemptStore(AudioFrame frame) { + if (frameCount >= frames.length) { + return false; + } - if (frameLength > frameBufferLength) { - throw new IllegalArgumentException("Frame is too big for buffer."); - } + int frameLength = frame.getDataLength(); + int frameBufferLength = frameBuffer.length; - store(frame, 0, 0, frameLength); - } else { - int lastFrame = wrappedFrameIndex(firstFrame + frameCount - 1); - int nextFrame = wrappedFrameIndex(lastFrame + 1); + if (frameCount == 0) { + firstFrame = 0; - int bufferHead = frames[firstFrame].getFrameOffset(); - int bufferTail = frames[lastFrame].getFrameEndOffset(); + if (frameLength > frameBufferLength) { + throw new IllegalArgumentException("Frame is too big for buffer."); + } - if (bufferHead < bufferTail) { - if (bufferTail + frameLength <= frameBufferLength) { - store(frame, nextFrame, bufferTail, frameLength); - } else if (bufferHead >= frameLength) { - store(frame, nextFrame, 0, frameLength); + store(frame, 0, 0, frameLength); } else { - return false; + int lastFrame = wrappedFrameIndex(firstFrame + frameCount - 1); + int nextFrame = wrappedFrameIndex(lastFrame + 1); + + int bufferHead = frames[firstFrame].getFrameOffset(); + int bufferTail = frames[lastFrame].getFrameEndOffset(); + + if (bufferHead < bufferTail) { + if (bufferTail + frameLength <= frameBufferLength) { + store(frame, nextFrame, bufferTail, frameLength); + } else if (bufferHead >= frameLength) { + store(frame, nextFrame, 0, frameLength); + } else { + return false; + } + } else if (bufferTail + frameLength <= bufferHead) { + store(frame, nextFrame, bufferTail, frameLength); + } else { + return false; + } } - } else if (bufferTail + frameLength <= bufferHead) { - store(frame, nextFrame, bufferTail, frameLength); - } else { - return false; - } + + return true; + } + + private int wrappedFrameIndex(int index) { + int maximumFrameCount = frames.length; + return index >= maximumFrameCount ? index - maximumFrameCount : index; } - return true; - } + private void store(AudioFrame frame, int index, int frameOffset, int frameLength) { + ReferenceMutableAudioFrame targetFrame = frames[index]; + targetFrame.setTimecode(frame.getTimecode()); + targetFrame.setVolume(frame.getVolume()); + targetFrame.setDataReference(frameBuffer, frameOffset, frameLength); - private int wrappedFrameIndex(int index) { - int maximumFrameCount = frames.length; - return index >= maximumFrameCount ? index - maximumFrameCount : index; - } + frame.getData(frameBuffer, frameOffset); - private void store(AudioFrame frame, int index, int frameOffset, int frameLength) { - ReferenceMutableAudioFrame targetFrame = frames[index]; - targetFrame.setTimecode(frame.getTimecode()); - targetFrame.setVolume(frame.getVolume()); - targetFrame.setDataReference(frameBuffer, frameOffset, frameLength); + frameCount++; + } - frame.getData(frameBuffer, frameOffset); + private MutableAudioFrame getBridgeFrame() { + if (bridgeFrame == null) { + bridgeFrame = new MutableAudioFrame(); + bridgeFrame.setBuffer(ByteBuffer.allocate(format.maximumChunkSize())); + } - frameCount++; - } + return bridgeFrame; + } - private MutableAudioFrame getBridgeFrame() { - if (bridgeFrame == null) { - bridgeFrame = new MutableAudioFrame(); - bridgeFrame.setBuffer(ByteBuffer.allocate(format.maximumChunkSize())); + private AudioFrame unwrapBridgeFrame() { + if (bridgeFrame.isTerminator()) { + return TerminatorAudioFrame.INSTANCE; + } else { + return new ImmutableAudioFrame(bridgeFrame.getTimecode(), bridgeFrame.getData(), bridgeFrame.getVolume(), + bridgeFrame.getFormat()); + } } - return bridgeFrame; - } + private static ReferenceMutableAudioFrame[] createFrames(int frameCount, AudioDataFormat format) { + ReferenceMutableAudioFrame[] frames = new ReferenceMutableAudioFrame[frameCount]; - private AudioFrame unwrapBridgeFrame() { - if (bridgeFrame.isTerminator()) { - return TerminatorAudioFrame.INSTANCE; - } else { - return new ImmutableAudioFrame(bridgeFrame.getTimecode(), bridgeFrame.getData(), bridgeFrame.getVolume(), - bridgeFrame.getFormat()); - } - } + for (int i = 0; i < frames.length; i++) { + frames[i] = new ReferenceMutableAudioFrame(); + frames[i].setFormat(format); + } - private static ReferenceMutableAudioFrame[] createFrames(int frameCount, AudioDataFormat format) { - ReferenceMutableAudioFrame[] frames = new ReferenceMutableAudioFrame[frameCount]; + return frames; + } - for (int i = 0; i < frames.length; i++) { - frames[i] = new ReferenceMutableAudioFrame(); - frames[i].setFormat(format); + private static ReferenceMutableAudioFrame createSilentFrame(AudioDataFormat format) { + ReferenceMutableAudioFrame frame = new ReferenceMutableAudioFrame(); + frame.setFormat(format); + frame.setDataReference(format.silenceBytes(), 0, format.silenceBytes().length); + frame.setVolume(0); + return frame; } - return frames; - } - - private static ReferenceMutableAudioFrame createSilentFrame(AudioDataFormat format) { - ReferenceMutableAudioFrame frame = new ReferenceMutableAudioFrame(); - frame.setFormat(format); - frame.setDataReference(format.silenceBytes(), 0, format.silenceBytes().length); - frame.setVolume(0); - return frame; - } - - @Override - protected void signalWaiters() { - synchronized (synchronizer) { - synchronizer.notifyAll(); + @Override + protected void signalWaiters() { + synchronized (synchronizer) { + synchronizer.notifyAll(); + } } - } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/PrimordialAudioTrackExecutor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/PrimordialAudioTrackExecutor.java index 3ddf1d77e..d19d7da0c 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/PrimordialAudioTrackExecutor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/PrimordialAudioTrackExecutor.java @@ -1,10 +1,6 @@ package com.sedmelluq.discord.lavaplayer.track.playback; -import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; -import com.sedmelluq.discord.lavaplayer.track.AudioTrackState; -import com.sedmelluq.discord.lavaplayer.track.TrackMarker; -import com.sedmelluq.discord.lavaplayer.track.TrackMarkerTracker; -import com.sedmelluq.discord.lavaplayer.track.TrackStateListener; +import com.sedmelluq.discord.lavaplayer.track.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,93 +12,94 @@ * information, which is applied to the actual executor when one is attached. */ public class PrimordialAudioTrackExecutor implements AudioTrackExecutor { - private static final Logger log = LoggerFactory.getLogger(LocalAudioTrackExecutor.class); - - private final AudioTrackInfo trackInfo; - private final TrackMarkerTracker markerTracker; - - private volatile long position; - - /** - * @param trackInfo Information of the track this executor is used with - */ - public PrimordialAudioTrackExecutor(AudioTrackInfo trackInfo) { - this.trackInfo = trackInfo; - this.markerTracker = new TrackMarkerTracker(); - } - - @Override - public AudioFrameBuffer getAudioBuffer() { - return null; - } - - @Override - public void execute(TrackStateListener listener) { - throw new UnsupportedOperationException(); - } - - @Override - public void stop() { - log.info("Tried to stop track {} which is not playing.", trackInfo.identifier); - } - - @Override - public long getPosition() { - return position; - } - - @Override - public void setPosition(long timecode) { - position = timecode; - markerTracker.checkSeekTimecode(timecode); - } - - @Override - public AudioTrackState getState() { - return AudioTrackState.INACTIVE; - } - - @Override - public void setMarker(TrackMarker marker) { - markerTracker.set(marker, position); - } - - @Override - public boolean failedBeforeLoad() { - return false; - } - - @Override - public AudioFrame provide() { - return provide(0, TimeUnit.MILLISECONDS); - } - - @Override - public AudioFrame provide(long timeout, TimeUnit unit) { - return null; - } - - @Override - public boolean provide(MutableAudioFrame targetFrame) { - return false; - } - - @Override - public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) - throws TimeoutException, InterruptedException { - - return false; - } - - /** - * Apply the position and loop state that had been set on this executor to an actual executor. - * @param executor The executor to apply the state to - */ - public void applyStateToExecutor(AudioTrackExecutor executor) { - if (position != 0) { - executor.setPosition(position); + private static final Logger log = LoggerFactory.getLogger(LocalAudioTrackExecutor.class); + + private final AudioTrackInfo trackInfo; + private final TrackMarkerTracker markerTracker; + + private volatile long position; + + /** + * @param trackInfo Information of the track this executor is used with + */ + public PrimordialAudioTrackExecutor(AudioTrackInfo trackInfo) { + this.trackInfo = trackInfo; + this.markerTracker = new TrackMarkerTracker(); + } + + @Override + public AudioFrameBuffer getAudioBuffer() { + return null; + } + + @Override + public void execute(TrackStateListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void stop() { + log.info("Tried to stop track {} which is not playing.", trackInfo.identifier); + } + + @Override + public long getPosition() { + return position; + } + + @Override + public void setPosition(long timecode) { + position = timecode; + markerTracker.checkSeekTimecode(timecode); + } + + @Override + public AudioTrackState getState() { + return AudioTrackState.INACTIVE; } - executor.setMarker(markerTracker.remove()); - } + @Override + public void setMarker(TrackMarker marker) { + markerTracker.set(marker, position); + } + + @Override + public boolean failedBeforeLoad() { + return false; + } + + @Override + public AudioFrame provide() { + return provide(0, TimeUnit.MILLISECONDS); + } + + @Override + public AudioFrame provide(long timeout, TimeUnit unit) { + return null; + } + + @Override + public boolean provide(MutableAudioFrame targetFrame) { + return false; + } + + @Override + public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) + throws TimeoutException, InterruptedException { + + return false; + } + + /** + * Apply the position and loop state that had been set on this executor to an actual executor. + * + * @param executor The executor to apply the state to + */ + public void applyStateToExecutor(AudioTrackExecutor executor) { + if (position != 0) { + executor.setPosition(position); + } + + executor.setMarker(markerTracker.remove()); + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/ReferenceMutableAudioFrame.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/ReferenceMutableAudioFrame.java index 63e44faab..1bf683727 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/ReferenceMutableAudioFrame.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/ReferenceMutableAudioFrame.java @@ -4,56 +4,56 @@ * Mutable audio frame which contains no dedicated buffer, but refers to a segment in a specified byte buffer. */ public class ReferenceMutableAudioFrame extends AbstractMutableAudioFrame { - private byte[] frameBuffer; - private int frameOffset; - private int frameLength; - - /** - * @return The underlying byte buffer. - */ - public byte[] getFrameBuffer() { - return frameBuffer; - } - - /** - * @return Offset of the frame data in the underlying byte buffer. - */ - public int getFrameOffset() { - return frameOffset; - } - - /** - * @return Offset of the end of frame data in the underlying byte buffer. - */ - public int getFrameEndOffset() { - return frameOffset + frameLength; - } - - @Override - public int getDataLength() { - return frameLength; - } - - @Override - public byte[] getData() { - byte[] data = new byte[frameLength]; - getData(data, 0); - return data; - } - - @Override - public void getData(byte[] buffer, int offset) { - System.arraycopy(frameBuffer, frameOffset, buffer, offset, frameLength); - } - - /** - * @param frameBuffer See {@link #getFrameBuffer()}. - * @param frameOffset See {@link #getFrameOffset()}. - * @param frameLength See {@link #getDataLength()}. - */ - public void setDataReference(byte[] frameBuffer, int frameOffset, int frameLength) { - this.frameBuffer = frameBuffer; - this.frameOffset = frameOffset; - this.frameLength = frameLength; - } + private byte[] frameBuffer; + private int frameOffset; + private int frameLength; + + /** + * @return The underlying byte buffer. + */ + public byte[] getFrameBuffer() { + return frameBuffer; + } + + /** + * @return Offset of the frame data in the underlying byte buffer. + */ + public int getFrameOffset() { + return frameOffset; + } + + /** + * @return Offset of the end of frame data in the underlying byte buffer. + */ + public int getFrameEndOffset() { + return frameOffset + frameLength; + } + + @Override + public int getDataLength() { + return frameLength; + } + + @Override + public byte[] getData() { + byte[] data = new byte[frameLength]; + getData(data, 0); + return data; + } + + @Override + public void getData(byte[] buffer, int offset) { + System.arraycopy(frameBuffer, frameOffset, buffer, offset, frameLength); + } + + /** + * @param frameBuffer See {@link #getFrameBuffer()}. + * @param frameOffset See {@link #getFrameOffset()}. + * @param frameLength See {@link #getDataLength()}. + */ + public void setDataReference(byte[] frameBuffer, int frameOffset, int frameLength) { + this.frameBuffer = frameBuffer; + this.frameOffset = frameOffset; + this.frameLength = frameLength; + } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/TerminatorAudioFrame.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/TerminatorAudioFrame.java index 56ad94d54..d93523e82 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/TerminatorAudioFrame.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/TerminatorAudioFrame.java @@ -6,40 +6,40 @@ * Audio frame where {@link #isTerminator()} is true. */ public class TerminatorAudioFrame implements AudioFrame { - public static final TerminatorAudioFrame INSTANCE = new TerminatorAudioFrame(); - - @Override - public long getTimecode() { - throw new UnsupportedOperationException(); - } - - @Override - public int getVolume() { - throw new UnsupportedOperationException(); - } - - @Override - public int getDataLength() { - throw new UnsupportedOperationException(); - } - - @Override - public byte[] getData() { - throw new UnsupportedOperationException(); - } - - @Override - public void getData(byte[] buffer, int offset) { - throw new UnsupportedOperationException(); - } - - @Override - public AudioDataFormat getFormat() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isTerminator() { - return true; - } + public static final TerminatorAudioFrame INSTANCE = new TerminatorAudioFrame(); + + @Override + public long getTimecode() { + throw new UnsupportedOperationException(); + } + + @Override + public int getVolume() { + throw new UnsupportedOperationException(); + } + + @Override + public int getDataLength() { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] getData() { + throw new UnsupportedOperationException(); + } + + @Override + public void getData(byte[] buffer, int offset) { + throw new UnsupportedOperationException(); + } + + @Override + public AudioDataFormat getFormat() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isTerminator() { + return true; + } } diff --git a/natives/build.gradle b/natives/build.gradle index 93afe562e..ee31cf4c1 100644 --- a/natives/build.gradle +++ b/natives/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'de.undercouch.download' + id 'de.undercouch.download' } import org.apache.tools.ant.taskdefs.condition.Os @@ -15,260 +15,262 @@ ext.vorbisVersion = '1.3.6' ext.sampleRateVersion = '0.1.9' ext.fdkAacVersion = '2.0.0' -task load { doLast { - if (!file("$projectDir/samplerate/src").exists()) { - def downloadPath = "$buildDir/tmp/libsamplerate.tar.gz" - def unpackPath = "$buildDir/tmp" - - download.run { - src "https://www.mega-nerd.com/SRC/libsamplerate-${sampleRateVersion}.tar.gz" - dest downloadPath - } - - copy { - from tarTree(resources.gzip(downloadPath)) - into unpackPath - } - - copy { - from "$unpackPath/libsamplerate-${sampleRateVersion}/src" - into "$projectDir/samplerate/src" - } - } - - if (!file("$projectDir/fdk-aac/libAACdec").exists()) { - def downloadPath = "$buildDir/tmp/fdk-aac-v${fdkAacVersion}.zip" - def unpackPath = "$buildDir" - - download.run { - src "https://github.com/mstorsjo/fdk-aac/archive/v${fdkAacVersion}.zip" - dest downloadPath - } - - copy { - from zipTree(file(downloadPath)) - into unpackPath - } - - copy { - from "$unpackPath/fdk-aac-${fdkAacVersion}" - into "$projectDir/fdk-aac" - } - } - - if (!file("$projectDir/vorbis/libogg-${oggVersion}").exists()) { - def downloadPath = "$buildDir/tmp/temp.zip" - def unpackPath = "$buildDir" - - download.run { - src "https://downloads.xiph.org/releases/ogg/libogg-${oggVersion}.zip" - dest downloadPath - } - - copy { - from zipTree(file(downloadPath)) - into "$projectDir/vorbis" - } - - download.run { - src "https://downloads.xiph.org/releases/vorbis/libvorbis-${vorbisVersion}.zip" - dest downloadPath - } - - copy { - from zipTree(file(downloadPath)) - into "$projectDir/vorbis" +task load { + doLast { + if (!file("$projectDir/samplerate/src").exists()) { + def downloadPath = "$buildDir/tmp/libsamplerate.tar.gz" + def unpackPath = "$buildDir/tmp" + + download.run { + src "https://www.mega-nerd.com/SRC/libsamplerate-${sampleRateVersion}.tar.gz" + dest downloadPath + } + + copy { + from tarTree(resources.gzip(downloadPath)) + into unpackPath + } + + copy { + from "$unpackPath/libsamplerate-${sampleRateVersion}/src" + into "$projectDir/samplerate/src" + } + } + + if (!file("$projectDir/fdk-aac/libAACdec").exists()) { + def downloadPath = "$buildDir/tmp/fdk-aac-v${fdkAacVersion}.zip" + def unpackPath = "$buildDir" + + download.run { + src "https://github.com/mstorsjo/fdk-aac/archive/v${fdkAacVersion}.zip" + dest downloadPath + } + + copy { + from zipTree(file(downloadPath)) + into unpackPath + } + + copy { + from "$unpackPath/fdk-aac-${fdkAacVersion}" + into "$projectDir/fdk-aac" + } + } + + if (!file("$projectDir/vorbis/libogg-${oggVersion}").exists()) { + def downloadPath = "$buildDir/tmp/temp.zip" + def unpackPath = "$buildDir" + + download.run { + src "https://downloads.xiph.org/releases/ogg/libogg-${oggVersion}.zip" + dest downloadPath + } + + copy { + from zipTree(file(downloadPath)) + into "$projectDir/vorbis" + } + + download.run { + src "https://downloads.xiph.org/releases/vorbis/libvorbis-${vorbisVersion}.zip" + dest downloadPath + } + + copy { + from zipTree(file(downloadPath)) + into "$projectDir/vorbis" + } + } + + if (!file("$projectDir/opus/opus-${opusVersion}").exists()) { + def downloadPath = "$buildDir/tmp/temp.tar.gz" + def unpackPath = "$buildDir" + + download.run { + src "https://downloads.xiph.org/releases/opus/opus-${opusVersion}.tar.gz" + dest downloadPath + } + + copy { + from tarTree(file(downloadPath)) + into "$projectDir/opus" + } + } + + if (!Os.isFamily(Os.FAMILY_WINDOWS) && !file("$projectDir/mp3/mpg123-${mpg123Version}").exists()) { + def downloadPath = "$buildDir/tmp/temp.tar.bz2" + def unpackPath = "$buildDir" + + download.run { + src "https://www.mpg123.de/download/mpg123-${mpg123Version}.tar.bz2" + dest downloadPath + } + + copy { + from tarTree(file(downloadPath)) + into "$projectDir/mp3" + } + } } - } - - if (!file("$projectDir/opus/opus-${opusVersion}").exists()) { - def downloadPath = "$buildDir/tmp/temp.tar.gz" - def unpackPath = "$buildDir" - - download.run { - src "https://downloads.xiph.org/releases/opus/opus-${opusVersion}.tar.gz" - dest downloadPath - } - - copy { - from tarTree(file(downloadPath)) - into "$projectDir/opus" - } - } - - if (!Os.isFamily(Os.FAMILY_WINDOWS) && !file("$projectDir/mp3/mpg123-${mpg123Version}").exists()) { - def downloadPath = "$buildDir/tmp/temp.tar.bz2" - def unpackPath = "$buildDir" - - download.run { - src "https://www.mpg123.de/download/mpg123-${mpg123Version}.tar.bz2" - dest downloadPath - } - - copy { - from tarTree(file(downloadPath)) - into "$projectDir/mp3" - } - } -}} +} ext.devEnvPath = null def getDevEnvLocation() { - if (ext.devEnvPath == null) { - def testDirectory = file("$buildDir/tmp/vsloc") - testDirectory.deleteDir() - testDirectory.mkdirs() + if (ext.devEnvPath == null) { + def testDirectory = file("$buildDir/tmp/vsloc") + testDirectory.deleteDir() + testDirectory.mkdirs() - new File(testDirectory, "CMakeLists.txt") << "" + new File(testDirectory, "CMakeLists.txt") << "" - def outStream = new StringBuilder() - def cmakeProcess = "cmake .".execute(null as String[], testDirectory) - cmakeProcess.consumeProcessOutput(outStream, outStream) - cmakeProcess.waitFor() + def outStream = new StringBuilder() + def cmakeProcess = "cmake .".execute(null as String[], testDirectory) + cmakeProcess.consumeProcessOutput(outStream, outStream) + cmakeProcess.waitFor() - def matcher = Pattern.compile("working C compiler: ([^\\n]*) -- works").matcher(outStream) - matcher.find() + def matcher = Pattern.compile("working C compiler: ([^\\n]*) -- works").matcher(outStream) + matcher.find() - def baseDirectory = file(matcher.group(1)) + def baseDirectory = file(matcher.group(1)) - while (!new File(baseDirectory, 'Common7').directory && baseDirectory.parentFile?.directory) { - baseDirectory = baseDirectory.parentFile - } + while (!new File(baseDirectory, 'Common7').directory && baseDirectory.parentFile?.directory) { + baseDirectory = baseDirectory.parentFile + } - ext.devEnvPath = new File(baseDirectory, "Common7/IDE/devenv.exe").absolutePath - } + ext.devEnvPath = new File(baseDirectory, "Common7/IDE/devenv.exe").absolutePath + } - return ext.devEnvPath + return ext.devEnvPath } def buildOpusOnWindows(force) { - def base = "$projectDir/libs/64" - def libs = ['opus'] - def present = libs.every { file("${base}/${it}.lib").exists() } + def base = "$projectDir/libs/64" + def libs = ['opus'] + def present = libs.every { file("${base}/${it}.lib").exists() } - if (force || !present) { - file(base).deleteDir() - file(base).mkdirs() + if (force || !present) { + file(base).deleteDir() + file(base).mkdirs() - def devenv = getDevEnvLocation() - def platformName = "x64" + def devenv = getDevEnvLocation() + def platformName = "x64" - def process = [devenv, "opus.sln", "/Project", "opus", "/Build", "Release|${platformName}"]. - execute(null as String[], file("$projectDir/opus/opus-${opusVersion}/win32/VS2015")) + def process = [devenv, "opus.sln", "/Project", "opus", "/Build", "Release|${platformName}"]. + execute(null as String[], file("$projectDir/opus/opus-${opusVersion}/win32/VS2015")) - process.waitForProcessOutput(System.out as Appendable, System.err) + process.waitForProcessOutput(System.out as Appendable, System.err) - libs.each { - assert file("$projectDir/opus/opus-${opusVersion}/win32/VS2015/${platformName}/Release/${it}.lib").renameTo("${base}/${it}.lib") + libs.each { + assert file("$projectDir/opus/opus-${opusVersion}/win32/VS2015/${platformName}/Release/${it}.lib").renameTo("${base}/${it}.lib") + } } - } } def waitForAndCheckSuccess(process, name) { - process.waitForProcessOutput(System.out as Appendable, System.err) + process.waitForProcessOutput(System.out as Appendable, System.err) - if (process.exitValue() != 0) { - throw new IllegalStateException("${name} failed.") - } + if (process.exitValue() != 0) { + throw new IllegalStateException("${name} failed.") + } } def buildOpusOnLinux(force) { - def present = file("${projectDir}/libs/64/libopus.a").exists() + def present = file("${projectDir}/libs/64/libopus.a").exists() - if (force || !present) { - def flags = "-fPIC -O3 -fdata-sections -ffunction-sections" - def process = ["./configure", "--enable-static", "--with-cpu=x64-64", "--with-pic", "CFLAGS=${flags}", "CXXFLAGS=${flags}", "LDFLAGS=${flags}"]. - execute(null as String[], file("$projectDir/opus/opus-${opusVersion}")) + if (force || !present) { + def flags = "-fPIC -O3 -fdata-sections -ffunction-sections" + def process = ["./configure", "--enable-static", "--with-cpu=x64-64", "--with-pic", "CFLAGS=${flags}", "CXXFLAGS=${flags}", "LDFLAGS=${flags}"]. + execute(null as String[], file("$projectDir/opus/opus-${opusVersion}")) - waitForAndCheckSuccess(process, "Opus ./configure") + waitForAndCheckSuccess(process, "Opus ./configure") - process = ["make", "clean"].execute(null as String[], file("$projectDir/opus/opus-${opusVersion}")) - waitForAndCheckSuccess(process, "Opus make clean") + process = ["make", "clean"].execute(null as String[], file("$projectDir/opus/opus-${opusVersion}")) + waitForAndCheckSuccess(process, "Opus make clean") - process = ["make"].execute(null as String[], file("$projectDir/opus/opus-${opusVersion}")) - waitForAndCheckSuccess(process, "Opus make") + process = ["make"].execute(null as String[], file("$projectDir/opus/opus-${opusVersion}")) + waitForAndCheckSuccess(process, "Opus make") - copy { - from "$projectDir/opus/opus-${opusVersion}/.libs/libopus.a" - into "$projectDir/libs/64" + copy { + from "$projectDir/opus/opus-${opusVersion}/.libs/libopus.a" + into "$projectDir/libs/64" + } } - } } def extractVersionPrefix(fullVersion, partCount) { - def parts = fullVersion.split('\\.').toList() - return parts.subList(0, Math.min(partCount, parts.size())).join('.') + def parts = fullVersion.split('\\.').toList() + return parts.subList(0, Math.min(partCount, parts.size())).join('.') } def replaceAutotoolsVersion(directory, originalVersion) { - def localVersion = (['automake', '--version'].execute().text =~ /\(GNU automake\) ([0-9.]+)/)[0][1] - def localMajorVersion = extractVersionPrefix(localVersion, 2) - def originalMajorVersion = extractVersionPrefix(originalVersion, 2) + def localVersion = (['automake', '--version'].execute().text =~ /\(GNU automake\) ([0-9.]+)/)[0][1] + def localMajorVersion = extractVersionPrefix(localVersion, 2) + def originalMajorVersion = extractVersionPrefix(originalVersion, 2) - [file("$directory/aclocal.m4"), file("$directory/configure")].each { - it.text = it.text.replace("am__api_version='$originalMajorVersion'", "am__api_version='$localMajorVersion'").replace(originalVersion, localVersion) - } + [file("$directory/aclocal.m4"), file("$directory/configure")].each { + it.text = it.text.replace("am__api_version='$originalMajorVersion'", "am__api_version='$localMajorVersion'").replace(originalVersion, localVersion) + } } def buildMpg123OnLinux(force) { - def present = file("${projectDir}/libs/64/libmpg123.a").exists() + def present = file("${projectDir}/libs/64/libmpg123.a").exists() - if (force || !present) { - replaceAutotoolsVersion("$projectDir/mp3/mpg123-${mpg123Version}", '1.15.1') + if (force || !present) { + replaceAutotoolsVersion("$projectDir/mp3/mpg123-${mpg123Version}", '1.15.1') - def flags = "-fPIC -O3 -fdata-sections -ffunction-sections" - def process = ['./configure', '--enable-static', '--with-cpu=x86-64', '--with-pic', "CFLAGS=$flags", "CXXFLAGS=$flags"]. - execute(null as String[], file("$projectDir/mp3/mpg123-${mpg123Version}")) + def flags = "-fPIC -O3 -fdata-sections -ffunction-sections" + def process = ['./configure', '--enable-static', '--with-cpu=x86-64', '--with-pic', "CFLAGS=$flags", "CXXFLAGS=$flags"]. + execute(null as String[], file("$projectDir/mp3/mpg123-${mpg123Version}")) - waitForAndCheckSuccess(process, "Mpg123 ./configure") + waitForAndCheckSuccess(process, "Mpg123 ./configure") - process = ["make", "clean"].execute(null as String[], file("$projectDir/mp3/mpg123-${mpg123Version}")) - waitForAndCheckSuccess(process, "make clean") + process = ["make", "clean"].execute(null as String[], file("$projectDir/mp3/mpg123-${mpg123Version}")) + waitForAndCheckSuccess(process, "make clean") - process = ["make"].execute(null as String[], file("$projectDir/mp3/mpg123-${mpg123Version}")) - waitForAndCheckSuccess(process, "make") + process = ["make"].execute(null as String[], file("$projectDir/mp3/mpg123-${mpg123Version}")) + waitForAndCheckSuccess(process, "make") - copy { - from "$projectDir/mp3/mpg123-${mpg123Version}/src/libmpg123/.libs/libmpg123.a" - into "$projectDir/libs/64" + copy { + from "$projectDir/mp3/mpg123-${mpg123Version}/src/libmpg123/.libs/libmpg123.a" + into "$projectDir/libs/64" + } } - } } def prepareOggOnLinux(force) { - def configured = file("$projectDir/vorbis/libogg-${oggVersion}/include/ogg/config_types.h").exists() + def configured = file("$projectDir/vorbis/libogg-${oggVersion}/include/ogg/config_types.h").exists() - if (force || !configured) { - def process = ["./configure", "--enable-static", "--build=x86_64-pc-linux-gnu", "--with-pic"]. - execute(null as String[], file("$projectDir/vorbis/libogg-${oggVersion}")) + if (force || !configured) { + def process = ["./configure", "--enable-static", "--build=x86_64-pc-linux-gnu", "--with-pic"]. + execute(null as String[], file("$projectDir/vorbis/libogg-${oggVersion}")) - process.waitForProcessOutput(System.out as Appendable, System.err) - } + process.waitForProcessOutput(System.out as Appendable, System.err) + } } def prepareBuilds(force) { - if (Os.isFamily(Os.FAMILY_WINDOWS)) { - buildOpusOnWindows(force) - } else { - prepareOggOnLinux(force) - buildOpusOnLinux(force) - buildMpg123OnLinux(force) - } + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + buildOpusOnWindows(force) + } else { + prepareOggOnLinux(force) + buildOpusOnLinux(force) + buildMpg123OnLinux(force) + } } -task compileNatives() { } -task checkNatives() { } +task compileNatives() {} +task checkNatives() {} def buildTaskConfig = [ - buildBase: buildDir, - projectBase: projectDir, - deployBase: project(':natives-publish').projectDir, + buildBase : buildDir, + projectBase : projectDir, + deployBase : project(':natives-publish').projectDir, setupDependency: tasks.load, - setupDoFirst: { config -> prepareBuilds(false) }, - compileTask: tasks.compileNatives, - checkTask: tasks.checkNatives, - name: 'connector' + setupDoFirst : { config -> prepareBuilds(false) }, + compileTask : tasks.compileNatives, + checkTask : tasks.checkNatives, + name : 'connector' ] createBuildTask(tasks, buildTaskConfig) diff --git a/natives/natives.gradle b/natives/natives.gradle index 93fd8fdad..fb3cdfa26 100644 --- a/natives/natives.gradle +++ b/natives/natives.gradle @@ -1,98 +1,102 @@ import org.apache.tools.ant.taskdefs.condition.Os def getBuildParameters(base) { - if (Os.isFamily(Os.FAMILY_WINDOWS)) { - return [ - 'identifier': "win-x86-64", - 'library': "${base}.dll", - 'setupArguments': ['-DCMAKE_BUILD_TYPE=Release', '-A', 'x64'], - 'buildArguments': ['--config', 'Release'], - 'env': [:] - ] - } else if (Os.isFamily(Os.FAMILY_MAC)) { - return [ - 'identifier': 'darwin', - 'library': "lib${base}.dylib", - 'setupArguments': ['-DCMAKE_BUILD_TYPE=Release'], - 'buildArguments': [], - 'env': [:] - ] - } else { - return [ - 'identifier': "linux-x86-64", - 'library': "lib${base}.so", - 'setupArguments': ['-DCMAKE_BUILD_TYPE=Release'], - 'buildArguments': [], - 'env': [:] - ] - } + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + return [ + 'identifier' : "win-x86-64", + 'library' : "${base}.dll", + 'setupArguments': ['-DCMAKE_BUILD_TYPE=Release', '-A', 'x64'], + 'buildArguments': ['--config', 'Release'], + 'env' : [:] + ] + } else if (Os.isFamily(Os.FAMILY_MAC)) { + return [ + 'identifier' : 'darwin', + 'library' : "lib${base}.dylib", + 'setupArguments': ['-DCMAKE_BUILD_TYPE=Release'], + 'buildArguments': [], + 'env' : [:] + ] + } else { + return [ + 'identifier' : "linux-x86-64", + 'library' : "lib${base}.so", + 'setupArguments': ['-DCMAKE_BUILD_TYPE=Release'], + 'buildArguments': [], + 'env' : [:] + ] + } } def getHomeDirectory() { - def directory = file(System.getProperty('java.home')) - return directory.name == 'jre' ? directory.parentFile.absolutePath : directory.absolutePath + def directory = file(System.getProperty('java.home')) + return directory.name == 'jre' ? directory.parentFile.absolutePath : directory.absolutePath } def createBuildTask(tasksHolder, config) { - def parameters = getBuildParameters(config.name) - def buildDirectory = "${config.buildBase}/${parameters.identifier}" - def distDirectory = "${config.projectBase}/dist/${parameters.identifier}" - def deployDirectory = "${config.deployBase}/src/main/resources/natives/${parameters.identifier}" - def taskBase = "${config.name}-64" + def parameters = getBuildParameters(config.name) + def buildDirectory = "${config.buildBase}/${parameters.identifier}" + def distDirectory = "${config.projectBase}/dist/${parameters.identifier}" + def deployDirectory = "${config.deployBase}/src/main/resources/natives/${parameters.identifier}" + def taskBase = "${config.name}-64" - parameters.env.put('DIST_DIR', distDirectory) - parameters.env.put('JAVA_HOME', getHomeDirectory()) + parameters.env.put('DIST_DIR', distDirectory) + parameters.env.put('JAVA_HOME', getHomeDirectory()) - def setupTask = tasksHolder.create("${taskBase}-setup", Exec) { - doFirst { - if (config.setupDoFirst) { - config.setupDoFirst(config) - } + def setupTask = tasksHolder.create("${taskBase}-setup", Exec) { + doFirst { + if (config.setupDoFirst) { + config.setupDoFirst(config) + } - file(buildDirectory).with { - it.deleteDir() - it.mkdirs() - } - } + file(buildDirectory).with { + it.deleteDir() + it.mkdirs() + } + } - workingDir buildDirectory - executable 'cmake' - args(parameters.setupArguments + ['../..']) - environment parameters.env - } + workingDir buildDirectory + executable 'cmake' + args(parameters.setupArguments + ['../..']) + environment parameters.env + } - def buildTask = tasksHolder.create("${taskBase}-build", Exec) { - workingDir buildDirectory - executable 'cmake' - args(['--build', '.'] + parameters.buildArguments) - environment parameters.env - } + def buildTask = tasksHolder.create("${taskBase}-build", Exec) { + workingDir buildDirectory + executable 'cmake' + args(['--build', '.'] + parameters.buildArguments) + environment parameters.env + } - def deployTask = tasksHolder.create("${taskBase}-deploy") { doLast { - copy { - from distDirectory - into deployDirectory + def deployTask = tasksHolder.create("${taskBase}-deploy") { + doLast { + copy { + from distDirectory + into deployDirectory + } + } } - }} - tasksHolder.create("${taskBase}-deploy-only") { doLast { - copy { - from distDirectory - into deployDirectory + tasksHolder.create("${taskBase}-deploy-only") { + doLast { + copy { + from distDirectory + into deployDirectory + } + } } - }} - if (config.setupDependency) { - setupTask.dependsOn(config.setupDependency) - } + if (config.setupDependency) { + setupTask.dependsOn(config.setupDependency) + } - buildTask.dependsOn(setupTask) - deployTask.dependsOn(buildTask) - config.compileTask.dependsOn(deployTask) + buildTask.dependsOn(setupTask) + deployTask.dependsOn(buildTask) + config.compileTask.dependsOn(deployTask) - if (!file("${deployDirectory}/${parameters.library}").exists()) { - config.checkTask.dependsOn(deployTask) - } + if (!file("${deployDirectory}/${parameters.library}").exists()) { + config.checkTask.dependsOn(deployTask) + } } ext.createBuildTask = { tasksHolder, config -> createBuildTask(tasksHolder, config) } diff --git a/settings.gradle.kts b/settings.gradle.kts index e85ef42bb..aa678dad8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,13 +2,13 @@ rootProject.name = "lavaplayer" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include( - ":common", - ":main", - ":extensions", - ":extensions:youtube-rotator", - ":extensions:format-xm", - ":natives", - ":natives-publish" + ":common", + ":main", + ":extensions", + ":extensions:youtube-rotator", + ":extensions:format-xm", + ":natives", + ":natives-publish" ) // https://github.com/gradle/gradle/issues/19254 @@ -57,4 +57,4 @@ fun VersionCatalogBuilder.test() { library("groovy", "org.apache.groovy", "groovy").version("4.0.13") library("spock-core", "org.spockframework", "spock-core").version("2.4-M1-groovy-4.0") library("logback-classic", "ch.qos.logback", "logback-classic").version("1.4.8") -} \ No newline at end of file +} From 980a1f62316efd828a5196d220aa71ef1727cda0 Mon Sep 17 00:00:00 2001 From: Nik Date: Thu, 3 Aug 2023 19:46:33 -0700 Subject: [PATCH 2/2] Ignore 4 space indent commit in git blames --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..b9a97dc94 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Reformat code to 4 space indents +9565a0d600a7ab8e302b840b93d2d4e05de7b573