diff --git a/.gitignore b/.gitignore index e08cd6d..c19a498 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,8 @@ tests/ vcred.json vdk.log -workspace/ \ No newline at end of file +workspace/ + +.vscode/ +.idea/ +vdk/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 6fcdb76..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Lua.diagnostics.globals": [ - "p" - ] -} \ No newline at end of file diff --git a/bin/ffmpeg-linux-arm64.bin b/bin/ffmpeg-linux-arm64.bin new file mode 100644 index 0000000..630c71c Binary files /dev/null and b/bin/ffmpeg-linux-arm64.bin differ diff --git a/bin/ffmpeg-linux-x64.bin b/bin/ffmpeg-linux-x64.bin new file mode 100644 index 0000000..5250b3d Binary files /dev/null and b/bin/ffmpeg-linux-x64.bin differ diff --git a/bin/ffmpeg-win32-arm64.exe b/bin/ffmpeg-win32-arm64.exe new file mode 100644 index 0000000..b1d81de Binary files /dev/null and b/bin/ffmpeg-win32-arm64.exe differ diff --git a/bin/ffmpeg-win32-x64.exe b/bin/ffmpeg-win32-x64.exe new file mode 100644 index 0000000..97ba1ec Binary files /dev/null and b/bin/ffmpeg-win32-x64.exe differ diff --git a/bin/mpg123-win32-x64.dll b/bin/mpg123-win32-x64.dll deleted file mode 100644 index 0b2a99f..0000000 Binary files a/bin/mpg123-win32-x64.dll and /dev/null differ diff --git a/bin/mpg123-win32-x86.dll b/bin/mpg123-win32-x86.dll deleted file mode 100644 index 5792254..0000000 Binary files a/bin/mpg123-win32-x86.dll and /dev/null differ diff --git a/bin/opus-linux-x64.so b/bin/opus-linux-x64.so new file mode 100644 index 0000000..a1e8a89 Binary files /dev/null and b/bin/opus-linux-x64.so differ diff --git a/bin/opus-linux-x86.so b/bin/opus-linux-x86.so new file mode 100644 index 0000000..efab256 Binary files /dev/null and b/bin/opus-linux-x86.so differ diff --git a/bin/LICENSES/OPUS b/docs/BINARY_LICENSES/OPUS similarity index 100% rename from bin/LICENSES/OPUS rename to docs/BINARY_LICENSES/OPUS diff --git a/bin/LICENSES/SODIUM b/docs/BINARY_LICENSES/SODIUM similarity index 100% rename from bin/LICENSES/SODIUM rename to docs/BINARY_LICENSES/SODIUM diff --git a/bin/README.md b/docs/BINARY_README.md similarity index 100% rename from bin/README.md rename to docs/BINARY_README.md diff --git a/HISTORY.md b/docs/HISTORY.md similarity index 100% rename from HISTORY.md rename to docs/HISTORY.md diff --git a/README.md b/docs/README.md similarity index 100% rename from README.md rename to docs/README.md diff --git a/example.app.yml b/example.app.yml index 1597fc0..4fc7fef 100644 --- a/example.app.yml +++ b/example.app.yml @@ -31,7 +31,7 @@ sources: # Available in YouTube website in Authorization header. Requires intercepting the website's requests. authorization: 'DISABLED' # Available in YouTube website in Cookie header. Requires intercepting the website's requests. - cookie: 'DISABLED', + cookie: 'DISABLED' # Available in YouTube website in X-Goog-Visitor-Id header. Requires intercepting the website's requests. visitorId: 'DISABLED' soundcloud: @@ -48,4 +48,4 @@ audio: quality: 'high' encryption: 'aead_aes256_gcm_rtpsize' # best, medium, fastest, zero order holder, linear - resamplingQuality: 'best' \ No newline at end of file + resamplingQuality: 'best' diff --git a/libs/quickmedia b/libs/quickmedia index 22870ea..879a744 160000 --- a/libs/quickmedia +++ b/libs/quickmedia @@ -1 +1 @@ -Subproject commit 22870eae34fe295a0e86e05428ed88d71bde5eb9 +Subproject commit 879a744f81356706a26bd698e0a370d3bb8cdf27 diff --git a/manifest.json b/manifest.json index 606a21a..2878449 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"codename":"IA","license":"MIT","homepage":"https://github.com/LunaticSea/LunaStream","name":"LunaStream","version":{"preRelease":"dev","semver":"1.0.2-dev","major":"1","build":"","minor":"0","patch":"2"},"runtime":{"rex":"8.37","luvi":"v2.14.0","winsvc":"1.0.0","luvit":"2.18.1","libuv":"1.44.2"},"buildTime":1740154346,"git":{"commit":"79d52771c9df430dbe8acb68b52b709b70705f48","commitTime":"1740146777","branch":"add/voice"},"author":{"name":"RainyXeon","email":"minh15052008@gmail.com"}} \ No newline at end of file +{"homepage":"https://github.com/LunaticSea/LunaStream","codename":"IA","name":"LunaStream","version":{"patch":"2","build":"","preRelease":"dev","semver":"1.0.2-dev","major":"1","minor":"0"},"license":"MIT","author":{"name":"RainyXeon","email":"minh15052008@gmail.com"},"runtime":{"winsvc":"1.0.0","luvit":"2.18.1","libuv":"1.48.0","luvi":"v2.15.0"},"buildTime":1746283890,"git":{"branch":"developments","commitTime":"1746283540","commit":"f9919add877567d2eebefdad164a0b441126ee0f"}} \ No newline at end of file diff --git a/src/managers/player.lua b/src/managers/player.lua index b3efe68..3c3b921 100644 --- a/src/managers/player.lua +++ b/src/managers/player.lua @@ -3,7 +3,6 @@ local class = require('class') local voice = require('../voice') local Player = class('Player') local decoder = require('../track/decoder') -local quickmedia = require("quickmedia") local json = require('json') function Player:__init(luna, guildId, sessionId) @@ -78,11 +77,7 @@ function Player:play(track) end end - if format == "mp3" then - self._stream = stream - else - self._stream = stream:pipe(quickmedia.opus.Decoder:new(self.voice._opus)) - end + self._stream = stream if self.voice then self.voice:play(self._stream, { encoder = true }) diff --git a/src/router/decodetrack.lua b/src/router/decodetrack.lua index ce6830c..ce7e8e3 100644 --- a/src/router/decodetrack.lua +++ b/src/router/decodetrack.lua @@ -20,5 +20,5 @@ return function(req, res, answer) ) end - answer(json.encode(result), 200, { ["Content-Type"] = "text/plain" }) + answer(json.encode(result), 200, { ["Content-Type"] = "application/json" }) end diff --git a/src/sources/deezer.lua b/src/sources/deezer.lua index 041163c..f45f579 100644 --- a/src/sources/deezer.lua +++ b/src/sources/deezer.lua @@ -1,450 +1 @@ -local http = require("coro-http") -local urlp = require("url-param") -local json = require("json") -local openssl = require("openssl") -local Transform = require("stream").Transform -local cipher = openssl.cipher -local digest = openssl.digest - -local AbstractSource = require('./abstract.lua') -local encoder = require("../track/encoder.lua") -local class = require('class') - -local Decrypt = Transform:extend() - -local function toHex(str) - return (str:gsub('.', function(c) - return string.format("%02x", string.byte(c)) - end)) -end - -local function bxor(a, b) - local res = 0 - for i = 0, 7 do - local bitA = a % 2 - local bitB = b % 2 - local xorBit = (bitA + bitB) % 2 - res = res + xorBit * (2 ^ i) - a = math.floor(a / 2) - b = math.floor(b / 2) - end - return res -end - -local function calculateKey(songId, decryptionKey) - local md5 = digest.new("md5") - md5:update(songId) - local binaryHash = md5:final() - local songIdHash = toHex(binaryHash) - local keyBytes = {} - for i = 1, 16 do - local a = string.byte(songIdHash, i) - local b = string.byte(songIdHash, i + 16) - local c = string.byte(decryptionKey, i) - local xorVal = bxor(bxor(a, b), c) - keyBytes[i] = string.char(xorVal) - end - return table.concat(keyBytes) -end - -local IV = string.char(0, 1, 2, 3, 4, 5, 6, 7) - -local function decryptAudioBlock(block, trackKey, blockIndex) - if blockIndex % 3 == 0 then - local deciph = cipher.new("bf-cbc", trackKey, IV) - deciph:setPadding(false) - local decrypted = deciph:update(block) or "" - local final = deciph:final() or "" - return decrypted .. final - else - return block - end -end - -function Decrypt:initialize(id) - Transform.initialize(self, { objectMode = true }) - self.trackKey = calculateKey(id, "g4el58wc0zvf9na1") - self.blockIndex = 0 -end - -function Decrypt:_transform(chunk, done) - self.blockIndex = self.blockIndex + 1 - - if self.blockIndex % 3 == 0 then - self:push(decryptAudioBlock(chunk, self.trackKey, self.blockIndex)) - else - self:push(chunk) - end - - done() -end - -local Deezer = class('Deezer', AbstractSource) - -function Deezer:__init(luna) - AbstractSource.__init(self) - self._luna = luna - self._license_token = nil - self._form_validation = nil - self._cookie = nil - self._search_id = 'dzsearch' -end - -function Deezer:setup() - local random_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - local api_token = "" - for i = 1, 16 do - local rand_index = math.random(1, #random_chars) - api_token = api_token .. random_chars:sub(rand_index, rand_index) - end - local url = string.format( - "https://www.deezer.com/ajax/gw-light.php?method=deezer.getUserData&input=3&api_version=1.0&api_token=%s", api_token - ) - - local success, response, data = pcall(http.request, "GET", url) - - if not success then - self._luna.logger:error('Deezer', 'Internal error: ' .. response) - return nil - end - - if response.code ~= 200 then - self._luna.logger:error('Deezer', 'Failed initializing Deezer source: ' .. response) - return nil - end - - if response.code ~= 200 then - self._luna.logger:error('Deezer', 'Failed initializing Deezer source') - return nil - end - - data = json.decode(data) - - if data.error == true then - self._luna.logger:error('Deezer', 'Failed initializing Deezer source') - return nil - end - - self._license_token = data.results.USER.OPTIONS.license_token - self._check_form = data.results.checkForm - - self._cookie = nil - for _, header in ipairs(response) do - if header[1] == 'Set-Cookie' then - if self._cookie then - self._cookie = self._cookie .. "; " .. header[2] - else - self._cookie = header[2] - end - end - end - - if not self._cookie then - self._luna.logger:error('Deezer', 'Cookie not found in response headers') - end - - return self -end - -function Deezer:search(query) - self._luna.logger:debug('Deezer', 'Searching: ' .. query) - local query_link = string.format("https://api.deezer.com/2.0/search?q=%s", urlp.encode(query)) - local success, response, data = pcall(http.request, "GET", query_link) - - if not success then - local error_message = string.format("Internal error: %s", response) - self._luna.logger:error('Deezer', error_message) - return self:buildError(error_message, "fault", "Deezer Source") - end - - if response.code ~= 200 then - local error_message = string.format("Server response error: %s | On query: %s", response.code, query) - self._luna.logger:error('Deezer', error_message) - return self:buildError(error_message, "fault", "Deezer Source") - end - - data = json.decode(data) - - if data.error then - local api_error_message = string.format("API error: %s | On query: %s", data.error.message, query) - self._luna.logger:error('Deezer', api_error_message) - return self:buildError(api_error_message, "fault", "Deezer Source") - end - - if data.total == 0 then - self._luna.logger:debug('Deezer', string.format("No results found for query: %s", query)) - return { loadType = "empty", data = {} } - end - - local max_results = self._luna.config.sources.maxSearchResults - if data.total > max_results then - data.data = { table.unpack(data.data, 1, max_results) } - end - - local tracks = {} - - for _, track in ipairs(data.data) do - local trackinfo = { - identifier = track.id, - uri = track.link, - title = track.title, - author = track.artist.name, - length = track.duration * 1000, - isSeekable = true, - isStream = false, - isrc = track.isrc, - artworkUrl = data.cover_xl or data.picture_xl, - sourceName = "deezer", - } - - table.insert( - tracks, { encoded = encoder(trackinfo), info = trackinfo, pluginInfo = {} } - ) - end - - return { loadType = "search", data = tracks } -end - -function Deezer:getLinkType(query) - local type, id = string.match(query, "/(%a+)/(%d+)$") - return type, id -end - -function Deezer:isLinkMatch(query) - local valid = string.match(query, "^https?://www%.deezer%.com/") and - (string.match(query, "/album/%d+$") or string.match(query, "/track/%d+$") or - string.match(query, "/playlist/%d+$")) - return valid ~= nil -end - -function Deezer:loadForm(query) - local type, id = self:getLinkType(query) - if not type then - self._luna.logger:error('Deezer', 'Type not supported') - return { - loadType = "error", - data = {}, - error = { - message = "Type not supported", - type = "fault", - source = "Deezer Source", - }, - } - end - - local url = string.format("https://api.deezer.com/%s/%s", type, id) - local success, response, data = pcall(http.request, "GET", url) - - if not success then - self._luna.logger:error('Deezer', 'Failed loading form: ' .. response) - return { - loadType = "error", - data = {}, - error = { - message = 'Failed loading form: ' .. response, - type = "fault", - source = "Deezer Source", - }, - } - end - - if response.code ~= 200 then - self._luna.logger:error('Deezer', 'Failed loading form') - return { - loadType = "error", - data = {}, - error = { - message = "Failed loading form", - type = "fault", - source = "Deezer Source", - }, - } - end - - data = json.decode(data) - - if data.error then - self._luna.logger:error('Deezer', 'Failed loading form') - return { - loadType = "error", - data = {}, - error = { - message = "Failed loading form", - type = "fault", - source = "Deezer Source", - }, - } - end - - local tracks = {} - if type == "track" then - local trackinfo = { - identifier = data.id, - uri = data.link, - title = data.title, - author = data.artist.name, - length = data.duration * 1000, - isSeekable = true, - isStream = false, - isrc = data.isrc, - artworkUrl = data.album.cover_xl or data.album.picture_xl, - sourceName = "deezer", - } - - return { - loadType = "track", - data = { - encoded = encoder(trackinfo), - info = trackinfo, - pluginInfo = {}, - }, - } - end - - if type == "album" then - for _, track in ipairs(data.tracks.data) do - local trackinfo = { - identifier = track.id, - uri = track.link, - title = track.title, - author = track.artist.name, - length = track.duration * 1000, - isSeekable = true, - isStream = false, - isrc = track.isrc, - artworkUrl = data.cover_xl or data.picture_xl, - sourceName = "deezer", - } - - table.insert( - tracks, { - encoded = encoder(trackinfo), - info = trackinfo, - pluginInfo = {}, - } - ) - end - - return { - loadType = "playlist", - data = { info = { name = data.title, selectedTrack = 0 }, tracks = tracks }, - } - end - - if type == "playlist" then - for _, track in ipairs(data.tracks.data) do - local trackinfo = { - identifier = track.id, - uri = track.link, - title = track.title, - author = track.artist.name, - length = track.duration * 1000, - isSeekable = true, - isStream = false, - isrc = track.isrc, - artworkUrl = data.picture_xl, - sourceName = "deezer", - } - - table.insert( - tracks, { - encoded = encoder(trackinfo), - info = trackinfo, - pluginInfo = {}, - } - ) - end - - return { - loadType = "playlist", - data = { info = { name = data.title, selectedTrack = 0 }, tracks = tracks }, - } - end - - self._luna.logger:error('Deezer', 'Type not supported') - return { - loadType = "error", - data = {}, - error = { - message = "Type not supported", - type = "fault", - source = "Deezer Source", - }, - } -end - -function Deezer:loadStream(track) - local song = { SNG_IDS = { track.info.identifier } } - - local url = string.format( - "https://www.deezer.com/ajax/gw-light.php?method=song.getListData&input=3&api_version=1.0&api_token=%s", - self._check_form - ) - local success, response, data = pcall(http.request, "POST", url, { { "Cookie", self._cookie } }, json.encode(song)) - - if not success then - self._luna.logger:error('Deezer', 'Failed loading stream: ' .. response) - return self:buildError('Failed loading stream: ' .. response, "fault", "Deezer Source") - end - - if response.code ~= 200 then - self._luna.logger:error('Deezer', 'Failed loading stream') - return self:buildError("Failed loading stream", "fault", "Deezer Source") - end - - data = json.decode(data) - - local formats = { 'MP3_64', 'MP3_128', 'MP3_256', 'MP3_320', 'FLAC' } - - local mediaData = { { type = "FULL", formats = {} } } - - for _, format in ipairs(formats) do - if tonumber(data.results.data[1]['FILESIZE_' .. format]) > 0 then - table.insert( - mediaData[1].formats, { cipher = 'BF_CBC_STRIPE', format = format } - ) - end - end - - local success, _, body = pcall(http.request, - "POST", "https://media.deezer.com/v1/get_url", {}, json.encode( - { - license_token = self._license_token, - media = mediaData, - track_tokens = { data.results.data[1].TRACK_TOKEN }, - } - ) - ) - - if not success then - return { - license_token = self._license_token, - media = mediaData, - track_tokens = { data.results.data[1].TRACK_TOKEN }, - } - end - - body = json.decode(body) - - if not body then - return { - license_token = self._license_token, - media = mediaData, - track_tokens = { data.results.data[1].TRACK_TOKEN }, - } - end - - return { - url = body.data[1].media[1].sources[1].url, - format = (string.sub(body.data[1].media[1].format, 1, string.len("MP3")) == "MP3") and 'mp3' or 'flac', - protocol = 'http', - extra = data.results.data[1], - keepAlive = true - } -end - -function Deezer:decryptAudio() - return Decrypt -end - -return Deezer +-- TODO: Refactor this code \ No newline at end of file diff --git a/src/sources/init.lua b/src/sources/init.lua index e00e92f..e0ccf72 100644 --- a/src/sources/init.lua +++ b/src/sources/init.lua @@ -1,6 +1,5 @@ local http = require("coro-http") -local stream = require("stream") -local PassThrough = stream.PassThrough +local Readable = require("stream").Readable local quickmedia = require("quickmedia") local config = require("../utils/config") local decoder = require("../track/decoder") @@ -9,7 +8,7 @@ local decoder = require("../track/decoder") local youtube = require("../sources/youtube") local avaliable_sources = { bandcamp = require("../sources/bandcamp.lua"), - deezer = require("../sources/deezer.lua"), + -- deezer = require("../sources/deezer.lua"), http = require("../sources/http.lua"), local_file = require("../sources/local_file.lua"), nicovideo = require("../sources/nicovideo.lua"), @@ -31,6 +30,19 @@ function Sources:__init(luna) self._luna.logger:info("SourceManager", "Setting up all avaliable source...") self._search_avaliables = {} self._source_avaliables = {} + self._ffmpeg_config = { + path = self:getBinaryPath('ffmpeg'), + args = { + '-loglevel', 'error', + '-analyzeduration', '0', + '-i', 'pipe:0', + '-f', 's16le', + '-ar', '48000', + '-ac', '2', + '-strict', '-2', + 'pipe:1' + } + } local is_yt = false local is_ytm = false @@ -145,11 +157,12 @@ function Sources:getStream(track) end if streamInfo.protocol == "file" then - local fstream = quickmedia.stream.file:new(streamInfo.url):pipe(quickmedia.opus.WebmDemuxer:new()) + local fstream = quickmedia.stream.file:new(streamInfo.url):pipe(quickmedia.core.FFmpeg:new(self._ffmpeg_config)) return fstream, streamInfo.format end + p(streamInfo.url, streamInfo.type, streamInfo.format) - if streamInfo.format == "hls" then + if streamInfo.protocol == "hls" then return self:loadHLS(streamInfo.url, streamInfo.type), streamInfo.type end @@ -168,17 +181,11 @@ function Sources:getStream(track) if track.info.sourceName == "deezer" then local source = self._source_avaliables["deezer"] - request:pipe(source:decryptAudio():new(track.info.identifier)):pipe(self:getBinaryPath('mpg123')) + request:pipe(source:decryptAudio():new(track.info.identifier)):pipe(quickmedia.core.FFmpeg:new(self._ffmpeg_config)) return request, streamInfo.format end - if streamInfo.format == "mp3" then - self._luna.logger:debug('Current stream is mp3') - return request:pipe(quickmedia.mpeg.Mp3Decoder:new(self:getBinaryPath('mpg123'))), streamInfo.format - else - self._luna.logger:debug('Current stream is opus') - return request:pipe(quickmedia.opus.WebmDemuxer:new()), streamInfo.format - end + return request:pipe(quickmedia.core.FFmpeg:new(self._ffmpeg_config)) end --------------------------------------------------------------- @@ -191,88 +198,85 @@ end function Sources:getBinaryPath(name) local os_name = require('los').type() local arch = os_name == 'darwin' and 'universal' or jit.arch - local lib_name_list = { win32 = '.dll', linux = '.so', darwin = '.dylib' } + local lib_name_list = { win32 = '.exe', linux = '.bin', darwin = '.macos' } return string.format('./bin/%s-%s-%s%s', name, os_name, arch, lib_name_list[os_name]) end + function Sources:loadHLS(url, type) - local stream = PassThrough:new() + local stream = Readable:new() + print("Loading HLS stream from URL: " .. url) + + local function processPlaylist(playlistUrl) + local res, body = http.request("GET", playlistUrl) + if res.code ~= 200 then + self._luna.logger:error("loadHLS", "HTTP error in playlist: " .. res.code) + stream:push(nil) + return + end - if type == "segment" then - coroutine.wrap(function() - local success, res, body = pcall(http.request, "GET", url) - if not success then - self._luna.logger:error("loadHLS", "Internal error: " .. res) - stream:close() - return - end - if res.code ~= 200 then - self._luna.logger:error("loadHLS", "HTTP error in segment: " .. res.code) - stream:close() - return - end - local chunkSize = 16 * 1024 - local bodyLength = #body - for i = 1, bodyLength, chunkSize do - local chunk = body:sub(i, math.min(i + chunkSize - 1, bodyLength)) - stream:write(chunk) - coroutine.yield() + local isMasterPlaylist = body:match("#EXT%-X%-STREAM%-INF") + if isMasterPlaylist then + local playlistUrls = {} + for line in body:gmatch("[^\r\n]+") do + if not line:match("^#") and line:match("%S") then + if not line:match("^https?://") then + local baseUrl = playlistUrl:match("(.*/)") + line = baseUrl .. line + end + table.insert(playlistUrls, line) + end end - stream:close() - end)() - return stream - - elseif type == "playlist" then - coroutine.wrap(function() - local success, res, playlistBody = pcall(http.request, "GET", url) - if not success then - self._luna.logger:error("loadHLS", "Internal error: " .. res) - stream:close() - return + if #playlistUrls > 0 then + processPlaylist(playlistUrls[1]) + else + self._luna.logger:error("loadHLS", "No valid playlist URLs found") + stream:push(nil) end - - if res.code ~= 200 then - self._luna.logger:error("loadHLS", "HTTP error in playlist: " .. res.code) - stream:close() - return - end - + else local segments = {} - for line in playlistBody:gmatch("[^\r\n]+") do + for line in body:gmatch("[^\r\n]+") do if not line:match("^#") and line:match("%S") then + if not line:match("^https?://") then + local baseUrl = playlistUrl:match("(.*/)") + line = baseUrl .. line + end table.insert(segments, line) end end for _, segUrl in ipairs(segments) do - if not segUrl:match("^https?://") then - local baseUrl = url:match("(.*/)") - segUrl = baseUrl .. segUrl - end - - local success, segRes, segBody = pcall(http.request, "GET", segUrl) - if success and segRes.code == 200 then - local chunkSize = 16 * 1024 - local segLength = #segBody - for i = 1, segLength, chunkSize do - local chunk = segBody:sub(i, math.min(i + chunkSize - 1, segLength)) - stream:write(chunk) - coroutine.yield() - end - else - if type(segRes) == "string" then return - self._luna.logger:error("loadHLS", "Internal error: " .. segRes) - end + print("Fetching segment: " .. segUrl) + local segRes, segBody = http.request("GET", segUrl) + if segRes.code ~= 200 then self._luna.logger:error("loadHLS", "HTTP error in segment: " .. segRes.code) + stream:push(nil) + else + stream:push(segBody) end end + end + end - stream:close() + if type == "segment" then + coroutine.wrap(function() + local res, body = http.request("GET", url) + if res.code ~= 200 then + self._luna.logger:error("loadHLS", "HTTP error in segment: " .. res.code) + stream:push(nil) + else + stream:push(body) + stream:push(nil) + end + end)() + elseif type == "playlist" then + coroutine.wrap(function() + processPlaylist(url) + stream:push(nil) end)() - return stream end - return stream + return stream:pipe(quickmedia.core.FFmpeg:new(self._ffmpeg_config)) end return Sources diff --git a/src/sources/youtube/ClientManager.lua b/src/sources/youtube/ClientManager.lua index b17ea3b..89237c2 100644 --- a/src/sources/youtube/ClientManager.lua +++ b/src/sources/youtube/ClientManager.lua @@ -1,5 +1,6 @@ local class = require('class') - +local http = require('coro-http') +local json = require('json') local YouTubeClientManager, get = class('YouTubeClientManager') function YouTubeClientManager:__init(luna) @@ -17,6 +18,18 @@ function YouTubeClientManager:__init(luna) gl = 'US', androidSdkVersion = '30', }, + IOS = { + clientName = 'IOS', + clientVersion = '19.47.7', + userAgent = 'com.google.ios.youtube/19.47.7 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X;)', + deviceMake = 'Apple', + deviceModel = 'iPhone 13', + osName = 'iOS', + osVersion = '17.5.1.21F90', + hl = 'en', + gl = 'US', + utcOffsetMinutes = 0 + }, ANDROID_MUSIC = { clientName = 'ANDROID_MUSIC', clientVersion = '8.02.53', @@ -28,10 +41,11 @@ function YouTubeClientManager:__init(luna) hl = 'en', gl = 'US', androidSdkVersion = '30', - }, + } } self._ytContext = {} self._currentClient = '' + self._visitorData = nil end function get:ytContext() @@ -48,6 +62,32 @@ function YouTubeClientManager:buildContext() self:switchClient('ANDROID') end +function YouTubeClientManager:_fetchVisitorData() + if self._visitorData then + return self._visitorData + end + + local success, response, data = pcall(http.request, + "GET", "https://www.youtube.com/sw.js_data", { + { "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" } + } + ) + + if success and response.code == 200 then + data = data:gsub("^%)]}'\n", "") + local ok, parsed = pcall(json.decode, data) + if ok and parsed then + self._visitorData = parsed[1][3][1][1][14] + self._luna.logger:debug('YouTube', string.format('Visitor data: %s', self._visitorData:gsub("%%", "%%%%"))) + return self._visitorData + else + self._luna.logger:warn('YouTube', 'Failed to parse visitorData') + end + else + self._luna.logger:warn('YouTube', 'Failed to fetch visitorData') + end +end + function YouTubeClientManager:switchClient(clientName) if not self._avaliableClients[clientName] then self._luna.logger:error('YouTubeClientManager', 'Client %s not found!', clientName) @@ -60,6 +100,7 @@ function YouTubeClientManager:switchClient(clientName) self._luna.logger:debug('YouTubeClientManager', 'Switching to client: ' .. clientName) self._ytContext.client = self._avaliableClients[clientName] + self._ytContext.client.visitorData = self:_fetchVisitorData() end return YouTubeClientManager diff --git a/src/sources/youtube/forms/video.lua b/src/sources/youtube/forms/video.lua index 45c01f6..189a6f7 100644 --- a/src/sources/youtube/forms/video.lua +++ b/src/sources/youtube/forms/video.lua @@ -3,8 +3,8 @@ local http = require("coro-http") local encoder = require("../../../track/encoder.lua") return function(query, src_type, youtube) - local videoId = query:match("v=([%w%-]+)") or query:match("https?://youtu%.be/(.+)%?si=.+") - + local videoId = query:match("v=([^&]+)") or query:match("https?://youtu%.be/(.+)%?si=.+") + p(videoId) local success, response, data = pcall(http.request, "POST", string.format("https://%s/youtubei/v1/player", youtube:baseHostRequest(src_type)), { { "User-Agent", youtube._clientManager.ytContext.client.userAgent }, { "X-GOOG-API-FORMAT-VERSION", "2" } }, @@ -17,7 +17,6 @@ return function(query, src_type, youtube) } ) ) - if not success then youtube._luna.logger:error('YouTube', "Internal error: %s | On query: %s", response, query) return youtube:buildError("Internal error: " .. response, "fault", "YouTube Source") diff --git a/src/sources/youtube/init.lua b/src/sources/youtube/init.lua index b1e6938..f84c825 100644 --- a/src/sources/youtube/init.lua +++ b/src/sources/youtube/init.lua @@ -36,6 +36,10 @@ function YouTube:search(query, src_type) end self._luna.logger:debug('YouTube', 'Searching: ' .. query) + if self._clientManager.ytContext.visitorData then + self._clientManager.ytContext.visitorData = nil -- + end + local success, response, data = pcall(http.request, "POST", string.format("https://%s/youtubei/v1/search", self:baseHostRequest(src_type)), { { "User-Agent", self._clientManager.ytContext.userAgent }, @@ -60,10 +64,11 @@ function YouTube:search(query, src_type) local baseUrl if type == "ytmsearch" then videos = data.contents.tabbedSearchResultsRenderer.tabs[1].tabRenderer.content.musicSplitViewRenderer.mainContent - .sectionListRenderer.contents[0].musicShelfRenderer.contents + .sectionListRenderer.contents[1].musicShelfRenderer.contents else - videos = data.contents.sectionListRenderer.contents[#data.contents.sectionListRenderer.contents].itemSectionRenderer - .contents + videos = data.contents.sectionListRenderer.contents[#data.contents.sectionListRenderer.contents].itemSectionRenderer and data.contents.sectionListRenderer.contents[#data.contents.sectionListRenderer.contents].itemSectionRenderer + .contents or data.contents.sectionListRenderer.contents[#data.contents.sectionListRenderer.contents].shelfRenderer.content.verticalListRenderer.items + end if #videos > config.sources.maxSearchResults then @@ -129,9 +134,7 @@ function YouTube:search(query, src_type) return { loadType = "search", data = tracks } end - function YouTube:checkURLType(inp_url, src_type) - local patterns = { ytmsearch = { video = "https?://music%.youtube%.com/watch%?v=[%w%-]+", @@ -147,9 +150,10 @@ function YouTube:checkURLType(inp_url, src_type) }, } local selectedPatterns = patterns[src_type] or patterns.default - - if string.match(inp_url, selectedPatterns.selectedVideo) or string.match(inp_url, selectedPatterns.playlist) then + if string.match(inp_url, selectedPatterns.playlist) then return 'playlist' + elseif string.match(inp_url, selectedPatterns.selectedVideo) then + return 'video' elseif src_type ~= 'ytmsearch' and string.match(inp_url, selectedPatterns.shorts) then return 'shorts' elseif string.match(inp_url, selectedPatterns.video) or string.match(inp_url, selectedPatterns.shortenedVideo) then @@ -178,23 +182,21 @@ function YouTube:isLinkMatch(query) end end - return false, nil + return false, nil end +-- index.lua (loadForm function remains unchanged) function YouTube:loadForm(query, src_type) if src_type == "ytmsearch" then self._clientManager:switchClient('ANDROID_MUSIC') end + if self._clientManager._currentClient ~= "ANDROID" then self._clientManager:switchClient('ANDROID') end local urlType = self:checkURLType(query, src_type) - - local formFile = - urlType == "video" and "video.lua" or urlType == "playlist" and "playlist.lua" or urlType == "shorts" and - "shorts.lua" - + local formFile = urlType == "video" and "video.lua" or urlType == "playlist" and "playlist.lua" or urlType == "shorts" and "shorts.lua" p('Form file: ', urlType, formFile) if formFile then @@ -220,7 +222,7 @@ function YouTube:loadStream(track) self._clientManager:switchClient('ANDROID_MUSIC') end if self._clientManager._currentClient ~= "ANDROID" then - self._clientManager:switchClient('ANDROID') + self._clientManager:switchClient('IOS') end self._luna.logger:debug('YouTube', 'Loading stream url for ' .. track.info.title) @@ -317,7 +319,23 @@ function YouTube:loadStream(track) ) ) - return { url = url, format = "webm/opus", protocol = "http", keepAlive = true } + local result = { + url = url or data.streamingData.hlsManifestUrl, + protocol = url and "http" or "hls_playlist", + format = url + and (audio.mimeType == 'audio/webm; codecs="opus"' and "webm/opus" or "arbitrary") + or "arbitrary", + keepAlive = true + } + if track.info.isStream then + result.protocol = "hls" + result.type = "playlist" + result.keepAlive = false + result.url = data.streamingData.hlsManifestUrl + result.format = "arbitrary" + end + + return result end return YouTube diff --git a/src/track/decoder.lua b/src/track/decoder.lua index 47aa806..2d84e2a 100644 --- a/src/track/decoder.lua +++ b/src/track/decoder.lua @@ -1,5 +1,4 @@ local class = require('class') -local Buffer = require('buffer').Buffer local openssl = require('openssl') local Decoder = class('Decoder') @@ -7,7 +6,7 @@ local Decoder = class('Decoder') function Decoder:__init(track) self._position = 1 self._track = track - self._buffer = Buffer:new(openssl.base64(self._track, false)) + self._buffer = openssl.base64(self._track, false) end function Decoder:changeBytes(bytes) @@ -17,17 +16,32 @@ end function Decoder:readByte() local byte = self:changeBytes(1) - return self._buffer[byte] + return string.byte(self._buffer, byte) end function Decoder:readUnsignedShort() local byte = self:changeBytes(2) - return self._buffer:readUInt16BE(byte) + -- local highOrderByte = self._buffer:readByte(byte) + -- local lowOrderByte = self._buffer:readByte(byte + 1) + -- First byte (b1) is multiplied by 256 (2^8) since it represents the high-order byte + -- Second byte (b2) is added as-is since it represents the low-order byte, + -- implementing big-endian byte ordering (most significant byte first) + local b1, b2 = string.byte(self._buffer, byte, byte + 1) + return b1 * 256 + b2 end function Decoder:readInt() local byte = self:changeBytes(4) - return self._buffer:readInt32BE(byte) + -- The first byte (b1) is most significant bit (MSB) + local b1, b2, b3, b4 = string.byte(self._buffer, byte, byte + 3) + local num = b1 * 256^3 + b2 * 256^2 + b3 * 256 + b4 + + -- using this as total bytes are calculated already + -- instead of MSB sign detection (b1 > 127). + if num > 2147483647 then + num = num - 4294967296 + end + return num end function Decoder:readLong() @@ -40,13 +54,13 @@ end function Decoder:readUTF() local len = self:readUnsignedShort() local start = self:changeBytes(len) - local result = self._buffer:toString(start, start + len - 1) - return result + return string.sub(self._buffer, start, start + len - 1) end function Decoder:getTrack() local success, result = pcall(Decoder.getTrackUnsafe, self) if not success then + print("Error:", result) return nil end return result @@ -90,6 +104,8 @@ function Decoder:trackVersionOne() ) if not success then + -- TODO: Use luna's logger + p('Error while decoding track version 1', result) return nil end return result @@ -119,6 +135,8 @@ function Decoder:trackVersionTwo() ) if not success then + -- TODO: Use luna's logger + p('Error while decoding track version 2', result) return nil end return result @@ -148,6 +166,8 @@ function Decoder:trackVersionThree() ) if not success then + -- TODO: Use luna's logger + p('Error while decoding track version 3', result) return nil end return result diff --git a/src/voice/init.lua b/src/voice/init.lua index eb296cb..dfc0d29 100644 --- a/src/voice/init.lua +++ b/src/voice/init.lua @@ -571,13 +571,21 @@ function VoiceManager:cacheReader() return data end + data = self._buffer .. data + self._buffer = '' + if #data == OPUS_CHUNK_STRING_SIZE then return data - else - local caculation = round_then_truncate(#data / OPUS_CHUNK_STRING_SIZE) - for _, mini_chunk in pairs(splitByChunk(data, round_then_truncate(#data / caculation))) do - table.insert(self._chunk_cache, mini_chunk) + elseif #data > OPUS_CHUNK_STRING_SIZE then + for i = 1, #data, OPUS_CHUNK_STRING_SIZE do + table.insert(self._chunk_cache, string.sub(data, i, i + OPUS_CHUNK_STRING_SIZE - 1)) end + if #self._chunk_cache[#self._chunk_cache] < OPUS_CHUNK_STRING_SIZE then + self._buffer = table.remove(self._chunk_cache) + end + else + self._buffer = data + return '' end res = table.remove(self._chunk_cache, 1) diff --git a/vdk/README.md b/vdk/README.md deleted file mode 100644 index bcea180..0000000 --- a/vdk/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# LunaStream's Voice Development Kit - (L)VDK -A tools for LunaStream's testing voice library focus on usable and convenience - -## Command usage -Please use this command to check the command usage: -``` -luvit vdk help -``` - -## Add credentials to (L)VDK -(L)VDK use vcred.json to handle credentials to connect to the voice, below is a template for vcred.json - -```json -{ - "guild_id": "813815427892641832", - "user_id": "1120309844117815326", - "token":"8fcd178a1b687623", - "session_id":"a9c85257c7f6fa174f40040a9e906e61", - "endpoint":"japan4479.discord.media:443" -} -``` - -## Add audio for testing -W.I.P \ No newline at end of file diff --git a/vdk/audio/kz_livetune_decorator.mp3 b/vdk/audio/kz_livetune_decorator.mp3 deleted file mode 100644 index 7768025..0000000 Binary files a/vdk/audio/kz_livetune_decorator.mp3 and /dev/null differ diff --git a/vdk/audio/kz_livetune_decorator.opus.ogg b/vdk/audio/kz_livetune_decorator.opus.ogg deleted file mode 100644 index 2e3c794..0000000 Binary files a/vdk/audio/kz_livetune_decorator.opus.ogg and /dev/null differ diff --git a/vdk/audio/kz_livetune_decorator.opus.weba b/vdk/audio/kz_livetune_decorator.opus.weba deleted file mode 100644 index 0d38402..0000000 Binary files a/vdk/audio/kz_livetune_decorator.opus.weba and /dev/null differ diff --git a/vdk/audio/kz_livetune_decorator.vorbis.ogg b/vdk/audio/kz_livetune_decorator.vorbis.ogg deleted file mode 100644 index b9f7549..0000000 Binary files a/vdk/audio/kz_livetune_decorator.vorbis.ogg and /dev/null differ diff --git a/vdk/audio/kz_livetune_decorator.vorbis.weba b/vdk/audio/kz_livetune_decorator.vorbis.weba deleted file mode 100644 index 6bd612b..0000000 Binary files a/vdk/audio/kz_livetune_decorator.vorbis.weba and /dev/null differ diff --git a/vdk/file/mpeg/mp3.lua b/vdk/file/mpeg/mp3.lua deleted file mode 100644 index 9a70714..0000000 --- a/vdk/file/mpeg/mp3.lua +++ /dev/null @@ -1,9 +0,0 @@ -local FileStream = require('../../../src/voice/stream/FileStream') -local quickmedia = require('quickmedia') - -return function (vdk) - vdk:log(false, 'Song Infomation: kz_livetune - Decorator (ft. Hatsune Miku), format: mp3') - - return FileStream:new('./vdk/audio/kz_livetune_decorator.mp3') - :pipe(quickmedia.mpeg.Mp3Decoder:new('./bin/mpg123/win32/x64.dll')) -end \ No newline at end of file diff --git a/vdk/file/ogg/opus.lua b/vdk/file/ogg/opus.lua deleted file mode 100644 index 50601d2..0000000 --- a/vdk/file/ogg/opus.lua +++ /dev/null @@ -1,11 +0,0 @@ -local quickmedia = require('quickmedia') - -local FileStream = require('../../../src/voice/stream/FileStream') - -return function (vdk) - vdk:log(false, 'Song Infomation: kz_livetune - Decorator (ft. Hatsune Miku), format: ogg (opus)') - - return FileStream:new('./vdk/audio/kz_livetune_decorator.opus.ogg') - :pipe(quickmedia.opus.OggDemuxer:new()) - :pipe(quickmedia.opus.Decoder:new(vdk._voice._opus)) -end \ No newline at end of file diff --git a/vdk/file/ogg/vorbis.lua b/vdk/file/ogg/vorbis.lua deleted file mode 100644 index aa501e5..0000000 --- a/vdk/file/ogg/vorbis.lua +++ /dev/null @@ -1 +0,0 @@ -return function () end \ No newline at end of file diff --git a/vdk/file/webm/opus.lua b/vdk/file/webm/opus.lua deleted file mode 100644 index c94ebac..0000000 --- a/vdk/file/webm/opus.lua +++ /dev/null @@ -1,11 +0,0 @@ -local quickmedia = require('quickmedia') - -local FileStream = require('../../../src/voice/stream/FileStream') - -return function (vdk) - vdk:log(false, 'Song Infomation: kz_livetune - Decorator (ft. Hatsune Miku), format: webm (opus)') - - return FileStream:new('./vdk/audio/kz_livetune_decorator.opus.weba') - :pipe(quickmedia.opus.WebmDemuxer:new()) - :pipe(quickmedia.opus.Decoder:new(vdk._voice._opus)) -end \ No newline at end of file diff --git a/vdk/file/webm/vorbis.lua b/vdk/file/webm/vorbis.lua deleted file mode 100644 index aa501e5..0000000 --- a/vdk/file/webm/vorbis.lua +++ /dev/null @@ -1 +0,0 @@ -return function () end \ No newline at end of file diff --git a/vdk/init.lua b/vdk/init.lua deleted file mode 100644 index 47ec624..0000000 --- a/vdk/init.lua +++ /dev/null @@ -1,44 +0,0 @@ -local childprocess = require("childprocess") -local fs = require('fs') - -local function slice(t, first, last) - local sliced = {} - for i = first, last do - sliced[#sliced + 1] = t[i] - end - return sliced -end - -local fd = nil -local cmd = "luvit" -- Change this to the command you want to capture logs from -local input_arg = slice(process.argv, 2, #process.argv) -- Arguments for the command - -if input_arg[1] == "--save-log" then - table.remove(input_arg, 1) - fd = fs.openSync('./vdk.log', 'w+') -end - -local args = { './vdk/main', table.unpack(input_arg) } - -local proc = childprocess.spawn(cmd, args) - -proc.stdout:on("data", function(chunk) - print(chunk:sub(1, -2)) - if fd then - fs.writeSync(fd, -1, chunk) - end -end) - -proc.stderr:on("data", function(chunk) - print(chunk:sub(1, -2)) - if fd then - fs.writeSync(fd, -1, chunk) - end -end) - -proc:on("exit", function(code, signal) - if fd then - fs.writeSync(fd, -1, "----- EOS -----") - fs.closeSync(fd) - end -end) \ No newline at end of file diff --git a/vdk/main.lua b/vdk/main.lua deleted file mode 100644 index ab595b8..0000000 --- a/vdk/main.lua +++ /dev/null @@ -1,125 +0,0 @@ --- External library -local timer = require('timer') -local json = require('json') -local fs = require('fs') -local class = require('class') - --- Internal library -local Voice = require('../src/voice') - --- Main code -local VoiceDevelopmentKit = class('VoiceDevelopmentKit') - -function VoiceDevelopmentKit:__init() - self._stream = nil - self._voice = nil - print('-------------------------------------------') - print("LunaStream's Voice Development Kit - (L)VDK") - print("Version: 1.0.0-internal") - print('-------------------------------------------') - self:commandHandling() -end - -function VoiceDevelopmentKit:commandHandling() - if process.argv[2] == "help" or not process.argv[2] then return self:commandManual() end - - local mode, format, encoding = process.argv[2], process.argv[3], process.argv[4] - - local str_template = encoding and './%s/%s/%s.lua' or './%s/%s.lua' - local require_string = string.format(str_template, mode, format, encoding) - - local success, global_stream = pcall(require, require_string) - if not success then - self:commandManual() - self:errorPrint(global_stream) - return - end - - self:log(false, 'Currently running mode: %s, type: %s, encoding: %s', mode, format, encoding or 'Not Specified') - - self._stream = global_stream - self:log(false, 'Now running voice library with credentials gets from vcred.json...') - self:voiceManager() -end - -function VoiceDevelopmentKit:commandManual() - print('Usage: luvit vdk (--save-log) [mode] [format/type] [encoding]') - print('├── mode: stream, file') - print('├── format/type: mpeg, ogg, webm, aac') - print('└── encoding: vorbis, mp3, opus') - print('Note: --save-log is optional for save log file, log file is vdk.log') -end - -function VoiceDevelopmentKit:voiceManager() - local vcred_file = fs.readFileSync('./vdk/vcred.json') - if not vcred_file then - self:errorPrint('vcred.json not found, please check vcred.example.json') - os.exit() - end - - local vcred = json.decode(vcred_file) - if not vcred then - self:errorPrint('vcred.json invalid, please check vcred.example.json') - os.exit() - end - - self:log(false, 'Voice Infomation:') - self:log(false, '├── user_id: %s', vcred.user_id, vcred.guild_id) - self:log(false, '├── guild_id: %s', vcred.guild_id) - self:log(false, '├── endpoint: %s', vcred.endpoint) - self:log(false, '├── session_id: %s', vcred.session_id) - self:log(false, '└── token: %s', vcred.token) - - self._voice = Voice(vcred.guild_id, vcred.user_id) - self:voiceEventListener() - self._voice:voiceCredential(vcred.session_id, vcred.endpoint, vcred.token) - self._voice:connect() - - self:log(false, 'Audio will play after 5s') - - timer.setTimeout(5000, coroutine.wrap(function () - self:log(false, 'Now play the song') - - local get_stream = self._stream(self) - if not get_stream then - self:log(false, '[---Error---]: Stream not found or invalid return') - os.exit() - end - - self._voice:play(get_stream, { encoder = true }) - self:playAudio() - end)) -end - -function VoiceDevelopmentKit:voiceEventListener() - self._voice:on('ready', function () - self:log(false, 'Voice is ready!') - end) - - self._voice:on('debug', function (log, ...) - if ... then - p(log, ...) - else - print(log) - end - end) -end - -function VoiceDevelopmentKit:playAudio() - self:log(false, 'Now play the song') -end - -function VoiceDevelopmentKit:errorPrint(data) - print('-------------------------------------------') - print('Error: ' .. data) - print('-------------------------------------------') -end - -function VoiceDevelopmentKit:log(inspect, data, ...) - if not inspect then - return print(string.format(data, ...)) - end - return p(data) -end - -VoiceDevelopmentKit() \ No newline at end of file diff --git a/vdk/stream/large.lua b/vdk/stream/large.lua deleted file mode 100644 index ac7306d..0000000 --- a/vdk/stream/large.lua +++ /dev/null @@ -1,30 +0,0 @@ -local quickmedia = require('quickmedia') - -local HTTPStream = require('../../src/voice/stream/HTTPStream') - -local stream_link = 'https://media.githubusercontent.com/media/LunaStream/StreamEmulator/refs/heads/main/large_audio_100mb.webm' - -return function (vdk) - local streamClient = HTTPStream:new('GET', stream_link) - local requestStream = streamClient:setup() - local code = requestStream.res and requestStream.res.code or 'nil' - local reason = requestStream.res and requestStream.res.reason or 'nil' - local version = requestStream.res and requestStream.res.version or 'nil' - local keepAlive = requestStream.res and requestStream.res.keepAlive or 'nil' - - vdk:log(false, '[HTTPStream]: HTTP/%s %s %s | keepAlive: %s ', version, code, reason, keepAlive) - - if code ~= 200 then return end - - vdk:log(false, 'Song Infomation: 11h whale sound, mode: stream') - - -- requestStream:on('ECONNREFUSED', function () - -- p('[HTTPStream]: Connection terminated') - -- end) - - local audioStream = requestStream - :pipe(quickmedia.opus.WebmDemuxer:new()) - :pipe(quickmedia.opus.Decoder:new(vdk._voice._opus)) - - return audioStream -end \ No newline at end of file diff --git a/vdk/stream/small.lua b/vdk/stream/small.lua deleted file mode 100644 index c014a90..0000000 --- a/vdk/stream/small.lua +++ /dev/null @@ -1,30 +0,0 @@ -local quickmedia = require('quickmedia') - -local HTTPStream = require('../../src/voice/stream/HTTPStream') - -local stream_link = 'https://raw.githubusercontent.com/LunaStream/StreamEmulator/refs/heads/main/livetune_decorator.weba' - -return function (vdk) - local streamClient = HTTPStream:new('GET', stream_link) - local requestStream = streamClient:setup() - local code = requestStream.res and requestStream.res.code or 'nil' - local reason = requestStream.res and requestStream.res.reason or 'nil' - local version = requestStream.res and requestStream.res.version or 'nil' - local keepAlive = requestStream.res and requestStream.res.keepAlive or 'nil' - - vdk:log(false, '[HTTPStream]: HTTP/%s %s %s | keepAlive: %s ', version, code, reason, keepAlive) - - if code ~= 200 then return end - - vdk:log(false, 'Song Infomation: kz_livetune - Decorator (ft. Hatsune Miku), mode: stream') - - -- requestStream:on('ECONNREFUSED', function () - -- p('[HTTPStream]: Connection terminated') - -- end) - - local audioStream = requestStream - :pipe(quickmedia.opus.WebmDemuxer:new()) - :pipe(quickmedia.opus.Decoder:new(vdk._voice._opus)) - - return audioStream -end \ No newline at end of file