local medialib do -- Note: build file expects these exact lines for them to be automatically replaced, so please don't change anything local VERSION = "git@8c99637e" local DISTRIBUTABLE = true medialib = {} medialib.VERSION = VERSION medialib.DISTRIBUTABLE = DISTRIBUTABLE medialib.Modules = {} local cvar_debug = CreateConVar("medialib_debug", "0", FCVAR_ARCHIVE) cvars.AddChangeCallback(cvar_debug:GetName(), function(_, _, val) medialib.DEBUG = val == "1" end) medialib.DEBUG = cvar_debug:GetBool() function medialib.modulePlaceholder(name) medialib.Modules[name] = {} end function medialib.module(name, opts) if medialib.DEBUG then print("[MediaLib] Creating module " .. name) end local mod = medialib.Modules[name] or { name = name, options = opts, } medialib.Modules[name] = mod return mod end -- AddCSLuaFile all medialib modules if SERVER then for _,fname in pairs(file.Find("medialib/*", "LUA")) do AddCSLuaFile("medialib/" .. fname) end end local file_Exists = file.Exists function medialib.tryInclude(file) if file_Exists(file, "LUA") then include(file) return true end if medialib.DEBUG then print("[MediaLib] Attempted to include nonexistent file " .. file) end return false end function medialib.load(name) local mod = medialib.Modules[name] if mod then return mod end if medialib.DEBUG then print("[MediaLib] Loading unreferenced module " .. name) end local file = "medialib/" .. name .. ".lua" if not medialib.tryInclude(file) then return nil end return medialib.Modules[name] end local medialibg = setmetatable({medialib = medialib}, {__index = _G}) local real_file_meta = { read = function(self) return file.Read(self.lua_path, "LUA") end, load = function(self) --local str = self:read() --if not str then error("MedialibDynLoad: could not load " .. self.lua_path) end -- TODO this does not function correctly; embedded medialib loading real_file will use global medialib as its 'medialib' instance return include(self.lua_path) end, addcs = function(self) AddCSLuaFile(self.lua_path) end, } real_file_meta.__index = real_file_meta local virt_file_meta = { read = function(self) return self.source end, load = function(self) local compiled = CompileString(self:read(), "MediaLib_DynFile_" .. self.name) setfenv(compiled, medialibg) return compiled() end, addcs = function() end } virt_file_meta.__index = virt_file_meta -- Used for medialib packed into a single file medialib.FolderItems = {} -- Returns an iterator for files in folder function medialib.folderIterator(folder) local files = {} for _,fname in pairs(file.Find("medialib/" .. folder .. "/*.lua", "LUA")) do table.insert(files, setmetatable({ name = fname, lua_path = "medialib/" .. folder .. "/" .. fname }, real_file_meta)) end for k,item in pairs(medialib.FolderItems) do local mfolder = k:match("^([^/]*).+") if mfolder == folder then table.insert(files, setmetatable({ name = k:match("^[^/]*/(.+)"), source = item }, virt_file_meta)) end end return pairs(files) end if CLIENT then local function Rainbow() for i=1, 30 do MsgC(HSVToColor(30*i, 0.5, 0.9), " " .. string.rep("SEE BELOW FOR INSTRUCTIONS ", 3) .. "\n") end end concommand.Add("medialib_noflash", function(_, _, args) if args[1] == "rainbow" then Rainbow() end SetClipboardText("http://get.adobe.com/flashplayer/otherversions/") MsgN("[ MediaLib: How to get Flash Player ]") MsgN("1. Open this website in your browser (not the ingame Steam browser): http://get.adobe.com/flashplayer/otherversions/") MsgN(" (the link has been automatically copied to your clipboard)") MsgN("2. Download and install the NSAPI (for Firefox) version") MsgN("3. Restart your Garry's Mod and rejoin this server") MsgN("[ ======================= ]") end) concommand.Add("medialib_lowaudio", function(_, _, args) if args[1] == "rainbow" then Rainbow() end SetClipboardText("http://windows.microsoft.com/en-us/windows7/adjust-the-sound-level-on-your-computer") MsgN("[ MediaLib: How to fix muted sound ]") MsgN("1. Follow instructions here: http://windows.microsoft.com/en-us/windows7/adjust-the-sound-level-on-your-computer") MsgN(" (the link has been automatically copied to your clipboard, you can open it in the steam ingame browser)") MsgN("2. Increase the volume of a process called 'Awesomium Core'") MsgN("3. You should immediately start hearing sound if a mediaclip is playing") MsgN("[ ======================= ]") end) hook.Add("OnPlayerChat", "MediaLib.ShowInstructions", function(ply, text) if text:match("!ml_noflash") then RunConsoleCommand("medialib_noflash", "rainbow") RunConsoleCommand("showconsole") elseif text:match("!ml_lowvolume") then RunConsoleCommand("medialib_lowaudio", "rainbow") RunConsoleCommand("showconsole") end end) end end -- 'oop'; CodeLen/MinifiedLen 2927/2927; Dependencies [] medialib.modulePlaceholder("oop") do local oop = medialib.module("oop") oop.Classes = oop.Classes or {} function oop.class(name, parent) local cls = oop.Classes[name] if not cls then cls = oop.createClass(name, parent) oop.Classes[name] = cls if medialib.DEBUG then print("[MediaLib] Registering oopclass " .. name) end end return cls end function oop.resolveClass(obj) if obj == nil then return oop.Object end local t = type(obj) if t == "string" then local clsobj = oop.Classes[obj] if clsobj then return clsobj end error("Resolving class from inexistent class string '" .. tostring(obj) .. "'") end if t == "table" then return obj end error("Resolving class from invalid object '" .. tostring(obj) .. "'") end -- This is a special parent used to prevent oop.Object being parent of itself local NIL_PARENT = {} -- Credits to Middleclass local metamethods = {'__add', '__call', '__concat', '__div', '__ipairs', '__le', '__len', '__lt', '__mod', '__mul', '__pairs', '__pow', '__sub', '__tostring', '__unm'} function oop.createClass(name, parent) local cls = {} -- Get parent class local par_cls if parent ~= NIL_PARENT then par_cls = oop.resolveClass(parent) end -- Add metadata cls.name = name cls.super = par_cls -- Add a subtable for class members ie methods and class/super handles cls.members = setmetatable({}, {__index = cls.super}) -- Add built-in "keywords" that Instances can access cls.members.class = cls cls.members.super = cls.super -- Instance metatable local cls_instance_meta = {} do cls_instance_meta.__index = cls.members -- Add metamethods. The class does not have members yet, so we need to use runtime lookup for _,name in pairs(metamethods) do cls_instance_meta[name] = function(...) local method = cls.members[name] if method then return method(...) end end end end -- Class metatable local class_meta = {} do class_meta.__index = cls.members class_meta.__newindex = cls.members class_meta.__tostring = function(self) return "class " .. self.name end -- Make the Class object a constructor. -- ie calling Class() creates a new instance function class_meta:__call(...) local instance = {} setmetatable(instance, cls_instance_meta) -- Call constructor if exists local ctor = instance.initialize if ctor then ctor(instance, ...) end return instance end end -- Set meta functions setmetatable(cls, class_meta) return cls end oop.Object = oop.createClass("Object", NIL_PARENT) -- Get the hash code ie the value Lua prints when you call __tostring() function oop.Object:hashCode() local meta = getmetatable(self) local old_tostring = meta.__tostring meta.__tostring = nil local hash = tostring(self):match("table: 0x(.*)") meta.__tostring = old_tostring return hash end function oop.Object:__tostring() return string.format("%s@%s", self.class.name, self:hashCode()) end end -- 'mediabase'; CodeLen/MinifiedLen 4305/4305; Dependencies [oop] medialib.modulePlaceholder("mediabase") do local oop = medialib.load("oop") local Media = oop.class("Media") function Media:on(event, callback) self._events = self._events or {} self._events[event] = self._events[event] or {} self._events[event][callback] = true end function Media:emit(event, ...) if not self._events then return end local callbacks = self._events[event] if not callbacks then return end for k,_ in pairs(callbacks) do k(...) end end function Media:getServiceBase() error("Media:getServiceBase() not implemented!") end function Media:getService() return self._service end function Media:getUrl() return self._unresolvedUrl end -- If metadata is cached: return it -- Otherwise either start a new metadata query, or if one is already going on -- do nothing function Media:lookupMetadata() local md = self._metadata -- Already fetched if type(md) == "table" then return md end -- Fetching or there was an error (TODO make error available to user) if md == true or type(md) == "string" then return nil end self._metadata = true self:getService():query(self:getUrl(), function(err, data) if err then self._metadata = err else self._metadata = data end end) return nil end -- True returned from this function does not imply anything related to how -- ready media is to play, just that it exists somewhere in memory and should -- at least in some point in the future be playable, but even that is not guaranteed function Media:isValid() return false end -- The GMod global IsValid requires the uppercase version function Media:IsValid() return self:isValid() end -- vol must be a float between 0 and 1 function Media:setVolume(vol) end function Media:getVolume() end -- "Quality" must be one of following strings: "low", "medium", "high", "veryhigh" -- Qualities do not map equally between services (ie "low" in youtube might be "medium" in twitch) -- Services are not guaranteed to change to the exact provided quality, or even to do anything at all function Media:setQuality(quality) end -- time must be an integer between 0 and duration function Media:seek(time) end function Media:getTime() return 0 end -- This method can be called repeatedly to keep the media somewhat in sync -- with given time, which makes it a great function to keep eg. synchronized -- televisions in sync. function Media:sync(time, margin) -- Only sync at most once per five seconds if self._lastSync and self._lastSync > CurTime() - 5 then return end local shouldSync = self:shouldSync(time, margin) if not shouldSync then return end self:seek(time + 0.1) -- Assume 0.1 sec loading time self._lastSync = CurTime() end function Media:shouldSync(time, margin) -- Check for invalid syncing state if not self:isValid() or not self:isPlaying() then return false end margin = margin or 2 local curTime = self:getTime() local diff = math.abs(curTime - time) return diff > margin end -- Must return one of following strings: "error", "loading", "buffering", "playing", "paused", "ended" -- Can also return nil if state is unknown or cannot be represented properly -- If getState does not return nil, it should be assumed to be the correct current state function Media:getState() end -- Simplified function of above; simply returns boolean indicating playing state function Media:isPlaying() return self:getState() == "playing" end function Media:play() end function Media:pause() end function Media:stop() end -- Queue a function to run after media is loaded. The function should run immediately -- if media is already loaded. function Media:runCommand(fn) end function Media:draw(x, y, w, h) end function Media:getTag() return self._tag end function Media:setTag(tag) self._tag = tag end function Media:guessDefaultTag() for i=1, 10 do local info = debug.getinfo(i, "S") if not info then break end local src = info.short_src local addon = src:match("addons/(.-)/") if addon and addon ~= "medialib" then return string.format("addon:%s", addon) end end return "addon:medialib" end function Media:setDefaultTag() self:setTag(self:guessDefaultTag()) end function Media:getDebugInfo() return string.format("[%s] Media [%s] valid:%s state:%s url:%s time:%d", self:getTag(), self.class.name, tostring(self:isValid()), self:getState(), self:getUrl(), self:getTime()) end end -- 'media'; CodeLen/MinifiedLen 746/746; Dependencies [] medialib.modulePlaceholder("media") do local media = medialib.module("media") media.Services = {} function media.registerService(name, cls) media.Services[name] = cls() end media.RegisterService = media.registerService -- alias function media.service(name) return media.Services[name] end media.Service = media.service -- alias function media.guessService(url, opts) for name,service in pairs(media.Services) do local isViable = true if opts and opts.whitelist then isViable = isViable and table.HasValue(opts.whitelist, name) end if opts and opts.blacklist then isViable = isViable and not table.HasValue(opts.blacklist, name) end if isViable and service:isValidUrl(url) then return service end end end media.GuessService = media.guessService -- alias end -- 'mediaregistry'; CodeLen/MinifiedLen 1248/1248; Dependencies [] medialib.modulePlaceholder("mediaregistry") do local mediaregistry = medialib.module("mediaregistry") local cache = setmetatable({}, {__mode = "v"}) function mediaregistry.add(media) table.insert(cache, media) end function mediaregistry.get() return cache end concommand.Add("medialib_listall", function() hook.Run("MediaLib_ListAll") end) local vers = medialib.VERSION hook.Add("MediaLib_ListAll", "MediaLib_" .. vers, function() print("Media for medialib version " .. vers .. ":") for _,v in pairs(cache) do print(v:getDebugInfo()) end end) concommand.Add("medialib_stopall", function() hook.Run("MediaLib_StopAll") end) hook.Add("MediaLib_StopAll", "MediaLib_" .. medialib.VERSION, function() for _,v in pairs(cache) do v:stop() end table.Empty(cache) end) local cvar_debug = CreateConVar("medialib_debugmedia", "0") hook.Add("HUDPaint", "MediaLib_G_DebugMedia", function() if not cvar_debug:GetBool() then return end local counter = {0} hook.Run("MediaLib_DebugPaint", counter) end) hook.Add("MediaLib_DebugPaint", "MediaLib_" .. medialib.VERSION, function(counter) local i = counter[1] for _,media in pairs(cache) do local t = string.format("#%d %s", i, media:getDebugInfo()) draw.SimpleText(t, "DermaDefault", 10, 10 + i*15) i=i+1 end counter[1] = i end) end -- 'servicebase'; CodeLen/MinifiedLen 2234/2234; Dependencies [oop,mediaregistry] medialib.modulePlaceholder("servicebase") do local oop = medialib.load("oop") local mediaregistry = medialib.load("mediaregistry") local Service = oop.class("Service") function Service:on(event, callback) self._events = {} self._events[event] = self._events[event] or {} self._events[event][callback] = true end function Service:emit(event, ...) for k,_ in pairs(self._events[event] or {}) do k(...) end if event == "error" then MsgN("[MediaLib] Video error: " .. table.ToString{...}) end end function Service:load(url, opts) end function Service:loadMediaObject(media, url, opts) media._unresolvedUrl = url media._service = self media:setDefaultTag() hook.Run("Medialib_ProcessOpts", media, opts or {}) mediaregistry.add(media) self:resolveUrl(url, function(resolvedUrl, resolvedData) media:openUrl(resolvedUrl) if resolvedData and resolvedData.start and (not opts or not opts.dontSeek) then media:seek(resolvedData.start) end end) end function Service:isValidUrl(url) end -- Sub-services should override this function Service:directQuery(url, callback) end -- A metatable for the callback chain local _service_cbchain_meta = {} _service_cbchain_meta.__index = _service_cbchain_meta function _service_cbchain_meta:addCallback(cb) table.insert(self._callbacks, cb) end function _service_cbchain_meta:run(err, data) local first = table.remove(self._callbacks, 1) if not first then return end first(err, data, function(err, data) self:run(err, data) end) end -- Query calls direct query and then passes the data through a medialib hook function Service:query(url, callback) local cbchain = setmetatable({_callbacks = {}}, _service_cbchain_meta) -- First add the data gotten from the service itself cbchain:addCallback(function(_, _, cb) return self:directQuery(url, cb) end) -- Then add custom callbacks hook.Run("Medialib_ExtendQuery", url, cbchain) -- Then add the user callback cbchain:addCallback(function(err, data) callback(err, data) end) -- Finally run the chain cbchain:run(url) end function Service:parseUrl(url) end -- the second argument to cb() function call has some standard keys: -- `start` the time at which to start media in seconds function Service:resolveUrl(url, cb) cb(url, self:parseUrl(url)) end end -- 'timekeeper'; CodeLen/MinifiedLen 1016/1016; Dependencies [oop] medialib.modulePlaceholder("timekeeper") do -- The purpose of TimeKeeper is to keep time where it is not easily available synchronously (ie. HTML based services) local oop = medialib.load("oop") local TimeKeeper = oop.class("TimeKeeper") function TimeKeeper:initialize() self:reset() end function TimeKeeper:reset() self.cachedTime = 0 self.running = false self.runningTimeStart = 0 end function TimeKeeper:getTime() local time = self.cachedTime if self.running then time = time + (RealTime() - self.runningTimeStart) end return time end function TimeKeeper:isRunning() return self.running end function TimeKeeper:play() if self.running then return end self.runningTimeStart = RealTime() self.running = true end function TimeKeeper:pause() if not self.running then return end local runningTime = RealTime() - self.runningTimeStart self.cachedTime = self.cachedTime + runningTime self.running = false end function TimeKeeper:seek(time) self.cachedTime = time if self.running then self.runningTimeStart = RealTime() end end end -- 'service_html'; CodeLen/MinifiedLen 7390/7390; Dependencies [oop,timekeeper] medialib.modulePlaceholder("service_html") do local oop = medialib.load("oop") medialib.load("timekeeper") local HTMLService = oop.class("HTMLService", "Service") function HTMLService:load(url, opts) local media = oop.class("HTMLMedia")() self:loadMediaObject(media, url, opts) return media end -- Whether or not we can trust that the HTML panel will send 'playing', 'paused' -- and other playback related events. If this returns true, 'timekeeper' will -- not be updated in playback related methods (except stop). function HTMLService:hasReliablePlaybackEvents(media) return false end local AwesomiumPool = {instances = {}} concommand.Add("medialib_awepoolinfo", function() print("AwesomiumPool> Free instance count: " .. #AwesomiumPool.instances) end) -- If there's bunch of awesomium instances in pool, we clean one up every 30 seconds timer.Create("MediaLib.AwesomiumPoolCleaner", 30, 0, function() if #AwesomiumPool.instances < 3 then return end local inst = table.remove(AwesomiumPool.instances, 1) if IsValid(inst) then inst:Remove() end end) function AwesomiumPool.get() local inst = table.remove(AwesomiumPool.instances, 1) if not IsValid(inst) then local pnl = vgui.Create("DHTML") return pnl end return inst end function AwesomiumPool.free(inst) if not IsValid(inst) then return end inst:SetHTML("") table.insert(AwesomiumPool.instances, inst) end local cvar_showAllMessages = CreateConVar("medialib_showallmessages", "0") local HTMLMedia = oop.class("HTMLMedia", "Media") local panel_width, panel_height = 1280, 720 function HTMLMedia:initialize() self.timeKeeper = oop.class("TimeKeeper")() self.panel = AwesomiumPool.get() local pnl = self.panel pnl:SetPos(0, 0) pnl:SetSize(panel_width, panel_height) local hookid = "MediaLib.HTMLMedia.FakeThink-" .. self:hashCode() hook.Add("Think", hookid, function() if not IsValid(self.panel) then hook.Remove("Think", hookid) return end self.panel:Think() end) local oldcm = pnl._OldCM or pnl.ConsoleMessage pnl._OldCM = oldcm pnl.ConsoleMessage = function(pself, msg) if msg and not cvar_showAllMessages:GetBool() then -- Filter some things out if string.find(msg, "XMLHttpRequest", nil, true) then return end if string.find(msg, "Unsafe JavaScript attempt to access", nil, true) then return end if string.find(msg, "Unable to post message to", nil, true) then return end if string.find(msg, "ran insecure content from", nil, true) then return end end return oldcm(pself, msg) end pnl:AddFunction("console", "warn", function(param) -- Youtube seems to spam lots of useless stuff here (that requires this function still?), so block by default if not cvar_showAllMessages:GetBool() then return end pnl:ConsoleMessage(param) end) pnl:SetPaintedManually(true) pnl:SetVisible(false) pnl:AddFunction("medialiblua", "Event", function(id, jsonstr) self:handleHTMLEvent(id, util.JSONToTable(jsonstr)) end) end function HTMLMedia:getBaseService() return "html" end function HTMLMedia:openUrl(url) self.panel:OpenURL(url) self.URLChanged = CurTime() end function HTMLMedia:runJS(js, ...) local code = string.format(js, ...) self.panel:QueueJavascript(code) end function HTMLMedia:handleHTMLEvent(id, event) if medialib.DEBUG then MsgN("[MediaLib] HTML Event: " .. id .. " (" .. table.ToString(event) .. ")") end if id == "stateChange" then local state = event.state local setToState if event.time then self.timeKeeper:seek(event.time) end if state == "playing" then setToState = "playing" self.timeKeeper:play() elseif state == "ended" or state == "paused" or state == "buffering" then setToState = state self.timeKeeper:pause() end if setToState then self.state = setToState self:emit(setToState) end elseif id == "playerLoaded" then for _,fn in pairs(self.commandQueue or {}) do fn() end elseif id == "error" then self:emit("error", {errorId = "service_error", errorName = "Error from service: " .. tostring(event.message)}) else MsgN("[MediaLib] Unhandled HTML event " .. tostring(id)) end end function HTMLMedia:getState() return self.state end local cvar_updatestride = CreateConVar("medialib_html_updatestride", "1", FCVAR_ARCHIVE) function HTMLMedia:updateTexture() local framenumber = FrameNumber() local framesSinceUpdate = (framenumber - (self.lastUpdatedFrame or 0)) if framesSinceUpdate >= cvar_updatestride:GetInt() then self.panel:UpdateHTMLTexture() self.lastUpdatedFrame = framenumber end end function HTMLMedia:getHTMLMaterial() if self._htmlMat then return self._htmlMat end local mat = self.panel:GetHTMLMaterial() self._htmlMat = mat return mat end function HTMLMedia:draw(x, y, w, h) self:updateTexture() local mat = self:getHTMLMaterial() surface.SetMaterial(mat) surface.SetDrawColor(255, 255, 255) local w_frac, h_frac = panel_width / mat:Width(), panel_height / mat:Height() surface.DrawTexturedRectUV(x or 0, y or 0, w or panel_width, h or panel_height, 0, 0, w_frac, h_frac) end function HTMLMedia:getTime() return self.timeKeeper:getTime() end function HTMLMedia:setQuality(qual) if self.lastSetQuality and self.lastSetQuality == qual then return end self.lastSetQuality = qual self:runJS("medialibDelegate.run('setQuality', {quality: %q})", qual) end -- This applies the volume to the HTML panel -- There is a undocumented 'internalVolume' variable, that can be used by eg 3d vol function HTMLMedia:applyVolume() local ivol = self.internalVolume or 1 local rvol = self.volume or 1 local vol = ivol * rvol if self.lastSetVolume and self.lastSetVolume == vol then return end self.lastSetVolume = vol self:runJS("medialibDelegate.run('setVolume', {vol: %f})", vol) end -- This sets a volume variable function HTMLMedia:setVolume(vol) self.volume = vol self:applyVolume() end function HTMLMedia:getVolume() -- could cookies potentially set the volume to something other than 1? return self.volume or 1 end -- if we dont rely on callbacks from JS, this will be the 'buffer' time for seeks local SEEK_BUFFER = 0.2 function HTMLMedia:seek(time) -- Youtube does not seem to properly callback with a 'seek' event (works in Chrome) -- Workaround by setting timeseeker time instantly here with a small buffer -- Using workaround is ok because if we somehow get event later, it'll correct -- the time that was set wrongly here self.timeKeeper:seek(time - SEEK_BUFFER) self:runJS("medialibDelegate.run('seek', {time: %.1f})", time) end -- See HTMLService:hasReliablePlaybackEvents() function HTMLMedia:hasReliablePlaybackEvents() local service = self:getService() return service and service:hasReliablePlaybackEvents(self) end function HTMLMedia:play() if not self:hasReliablePlaybackEvents() then self.timeKeeper:play() end self:runJS("medialibDelegate.run('play')") end function HTMLMedia:pause() if not self:hasReliablePlaybackEvents() then self.timeKeeper:pause() end self:runJS("medialibDelegate.run('pause')") end function HTMLMedia:stop() AwesomiumPool.free(self.panel) self.panel = nil self.timeKeeper:pause() self:emit("ended", {stopped = true}) self:emit("destroyed") end function HTMLMedia:runCommand(fn) if self._playerLoaded then fn() else self.commandQueue = self.commandQueue or {} self.commandQueue[#self.commandQueue+1] = fn end end function HTMLMedia:isValid() return IsValid(self.panel) end end -- 'service_bass'; CodeLen/MinifiedLen 6685/6685; Dependencies [oop,mediaregistry] medialib.modulePlaceholder("service_bass") do local oop = medialib.load("oop") local BASSService = oop.class("BASSService", "Service") function BASSService:load(url, opts) local media = oop.class("BASSMedia")() self:loadMediaObject(media, url, opts) return media end local BASSMedia = oop.class("BASSMedia", "Media") function BASSMedia:initialize() self.bassPlayOptions = {"noplay", "noblock"} self.commandQueue = {} end function BASSMedia:getBaseService() return "bass" end function BASSMedia:updateFFT() local curFrame = FrameNumber() if self._lastFFTUpdate and self._lastFFTUpdate == curFrame then return end self._lastFFTUpdate = curFrame local chan = self.chan if not IsValid(chan) then return end self.fftValues = self.fftValues or {} chan:FFT(self.fftValues, FFT_512) end function BASSMedia:getFFT() return self.fftValues end function BASSMedia:draw(x, y, w, h) surface.SetDrawColor(0, 0, 0) surface.DrawRect(x, y, w, h) self:updateFFT() local fftValues = self:getFFT() if not fftValues then return end local valCount = #fftValues local valsPerX = (valCount == 0 and 1 or (w/valCount)) local barw = w / (valCount) for i=1, valCount do surface.SetDrawColor(HSVToColor(i, 0.9, 0.5)) local barh = fftValues[i]*h surface.DrawRect(x + i*barw, y + (h-barh), barw, barh) end end function BASSMedia:openUrl(url) self._openingInfo = {"url", url} local flags = table.concat(self.bassPlayOptions, " ") sound.PlayURL(url, flags, function(chan, errId, errName) self:bassCallback(chan, errId, errName) end) end function BASSMedia:openFile(path) self._openingInfo = {"file", path} local flags = table.concat(self.bassPlayOptions, " ") sound.PlayFile(path, flags, function(chan, errId, errName) self:bassCallback(chan, errId, errName) end) end -- Attempts to reload the stream function BASSMedia:reload() local type, resource = unpack(self._openingInfo or {}) if not type then MsgN("[Medialib] Attempting to reload BASS stream that was never started the first time!") return end -- stop existing channel if it exists if IsValid(self.chan) then self.chan:Stop() self.chan = nil end -- Remove stop flag, clear cmd queue, stop state checker self._stopped = false self:stopStateChecker() self.commandQueue = {} MsgN("[Medialib] Attempting to reload BASS stream ", type, resource) if type == "url" then self:openUrl(resource) elseif type == "file" then self:openFile(resource) elseif type then MsgN("[Medialib] Failed to reload audio resource ", type, resource) return end self:applyVolume(true) if self._commandState == "play" then self:play() end end function BASSMedia:bassCallback(chan, errId, errName) if not IsValid(chan) then ErrorNoHalt("[MediaLib] BassMedia play failed: ", errName) self._stopped = true self:emit("error", "loading_failed", string.format("BASS error id: %s; name: %s", errId, errName)) return end -- Check if media was stopped before loading if self._stopped then MsgN("[MediaLib] Loading BASS media aborted; stop flag was enabled") chan:Stop() return end self.chan = chan for _,c in pairs(self.commandQueue) do c(chan) end -- Empty queue self.commandQueue = {} self:startStateChecker() end function BASSMedia:startStateChecker() timer.Create("MediaLib_BASS_EndChecker_" .. self:hashCode(), 1, 0, function() if IsValid(self.chan) and self.chan:GetState() == GMOD_CHANNEL_STOPPED then self:emit("ended") self:stopStateChecker() end end) end function BASSMedia:stopStateChecker() timer.Remove("MediaLib_BASS_EndChecker_" .. self:hashCode()) end function BASSMedia:runCommand(fn) if IsValid(self.chan) then fn(self.chan) else self.commandQueue[#self.commandQueue+1] = fn end end -- This applies the volume to the HTML panel -- There is a undocumented 'internalVolume' variable, that can be used by eg 3d vol function BASSMedia:applyVolume(force) local ivol = self.internalVolume or 1 local rvol = self.volume or 1 local vol = ivol * rvol if not force and self.lastSetVolume and self.lastSetVolume == vol then return end self.lastSetVolume = vol self:runCommand(function(chan) chan:SetVolume(vol) end) end function BASSMedia:setVolume(vol) self.volume = vol self:applyVolume() end function BASSMedia:getVolume() return self.volume or 1 end function BASSMedia:seek(time) self:runCommand(function(chan) if chan:IsBlockStreamed() then return end self._seekingTo = time local timerId = "MediaLib_BASSMedia_Seeker_" .. self:hashCode() local function AttemptSeek() -- someone used :seek with other time if self._seekingTo ~= time or -- chan not valid not IsValid(chan) then timer.Destroy(timerId) return end chan:SetTime(time) -- seek succeeded if math.abs(chan:GetTime() - time) < 0.25 then timer.Destroy(timerId) end end timer.Create(timerId, 0.2, 0, AttemptSeek) AttemptSeek() end) end function BASSMedia:getTime() if self:isValid() and IsValid(self.chan) then return self.chan:GetTime() end return 0 end function BASSMedia:getState() if not self:isValid() then return "error" end if not IsValid(self.chan) then return "loading" end local bassState = self.chan:GetState() if bassState == GMOD_CHANNEL_PLAYING then return "playing" end if bassState == GMOD_CHANNEL_PAUSED then return "paused" end if bassState == GMOD_CHANNEL_STALLED then return "buffering" end if bassState == GMOD_CHANNEL_STOPPED then return "paused" end -- umm?? return end function BASSMedia:play() self:runCommand(function(chan) chan:Play() self:emit("playing") self._commandState = "play" end) end function BASSMedia:pause() self:runCommand(function(chan) chan:Pause() self:emit("paused") self._commandState = "pause" end) end function BASSMedia:stop() self._stopped = true self:runCommand(function(chan) chan:Stop() self:emit("ended", {stopped = true}) self:emit("destroyed") self:stopStateChecker() end) end function BASSMedia:isValid() return not self._stopped end local mediaregistry = medialib.load("mediaregistry") local netmsgid = "ML_MapCleanHack_" .. medialib.VERSION if CLIENT then -- Logic for reloading BASS streams after map cleanups -- Workaround until gmod issue #2874 gets fixed net.Receive(netmsgid, function() for _,v in pairs(mediaregistry.get()) do -- BASS media that should play, yet does not if v:getBaseService() == "bass" and v:isValid() and IsValid(v.chan) and v.chan:GetState() == GMOD_CHANNEL_STOPPED then v:reload() end end end) end if SERVER then util.AddNetworkString(netmsgid) hook.Add("PostCleanupMap", "MediaLib_BassReload" .. medialib.VERSION, function() net.Start(netmsgid) net.Broadcast() end) end end medialib.FolderItems["services/gdrive.lua"] = "local oop = medialib.load(\"oop\")\n\nlocal GDriveService = oop.class(\"GDriveService\", \"HTMLService\")\nGDriveService.identifier = \"GDrive\"\n\nlocal all_patterns = {\"^https?://drive.google.com/file/d/([^/]*)/edit\"}\n\nfunction GDriveService:parseUrl(url)\n\tfor _,pattern in pairs(all_patterns) do\n\t\tlocal id = string.match(url, pattern)\n\t\tif id then\n\t\t\treturn {id = id}\n\t\tend\n\tend\nend\n\nfunction GDriveService:isValidUrl(url)\n\treturn self:parseUrl(url) ~= nil\nend\n\nlocal function urlencode(str)\n if (str) then\n str = string.gsub (str, \"\\n\", \"\\r\\n\")\n str = string.gsub (str, \"([^%w ])\",\n function (c) return string.format (\"%%%02X\", string.byte(c)) end)\n str = string.gsub (str, \" \", \"+\")\n end\n return str \nend\n\nlocal player_url = \"https://wyozi.github.io/gmod-medialib/mp4.html?id=%s\"\nlocal gdrive_stream_url = \"https://drive.google.com/uc?export=download&confirm=yTib&id=%s\"\nfunction GDriveService:resolveUrl(url, callback)\n\tlocal urlData = self:parseUrl(url)\n\tlocal playerUrl = string.format(player_url, urlencode(string.format(gdrive_stream_url, urlData.id)))\n\n\tcallback(playerUrl, {start = urlData.start})\nend\n\nfunction GDriveService:directQuery(url, callback)\n\tcallback(nil, {\n\t\ttitle = url:match(\"([^/]+)$\")\n\t})\nend\n\nfunction GDriveService:hasReliablePlaybackEvents(media)\n\treturn true\nend\n\nreturn GDriveService" medialib.FolderItems["services/mp4.lua"] = "local oop = medialib.load(\"oop\")\n\nlocal Mp4Service = oop.class(\"Mp4Service\", \"HTMLService\")\nMp4Service.identifier = \"mp4\"\n\nlocal all_patterns = {\"^https?://.*%.mp4\"}\n\nfunction Mp4Service:parseUrl(url)\n\tfor _,pattern in pairs(all_patterns) do\n\t\tlocal id = string.match(url, pattern)\n\t\tif id then\n\t\t\treturn {id = id}\n\t\tend\n\tend\nend\n\nfunction Mp4Service:isValidUrl(url)\n\treturn self:parseUrl(url) ~= nil\nend\n\nlocal player_url = \"https://wyozi.github.io/gmod-medialib/mp4.html?id=%s\"\nfunction Mp4Service:resolveUrl(url, callback)\n\tlocal urlData = self:parseUrl(url)\n\tlocal playerUrl = string.format(player_url, urlData.id)\n\n\tcallback(playerUrl, {start = urlData.start})\nend\n\nfunction Mp4Service:directQuery(url, callback)\n\tcallback(nil, {\n\t\ttitle = url:match(\"([^/]+)$\")\n\t})\nend\n\nfunction Mp4Service:hasReliablePlaybackEvents(media)\n\treturn true\nend\n\nreturn Mp4Service" medialib.FolderItems["services/soundcloud.lua"] = "local oop = medialib.load(\"oop\")\n\nlocal SoundcloudService = oop.class(\"SoundcloudService\", \"BASSService\")\nSoundcloudService.identifier = \"soundcloud\"\n\nlocal all_patterns = {\n\t\"^https?://www.soundcloud.com/([A-Za-z0-9_%-]+/[A-Za-z0-9_%-]+)/?.*$\",\n\t\"^https?://soundcloud.com/([A-Za-z0-9_%-]+/[A-Za-z0-9_%-]+)/?.*$\",\n}\n\n-- Support url that passes track id directly\nlocal id_pattern = \"^https?://api.soundcloud.com/tracks/(%d+)\"\n\nfunction SoundcloudService:parseUrl(url)\n\tfor _,pattern in pairs(all_patterns) do\n\t\tlocal path = string.match(url, pattern)\n\t\tif path then\n\t\t\treturn {path = path}\n\t\tend\n\tend\n\n\tlocal id = string.match(url, id_pattern)\n\tif id then\n\t\treturn {id = id}\n\tend\nend\n\nfunction SoundcloudService:isValidUrl(url)\n\treturn self:parseUrl(url) ~= nil\nend\n\nfunction SoundcloudService:resolveUrl(url, callback)\n\tlocal apiKey = medialib.SOUNDCLOUD_API_KEY\n\tif not apiKey then\n\t\tErrorNoHalt(\"SoundCloud error: Missing SoundCloud API key\")\n\t\treturn\n\tend\n\n\tlocal urlData = self:parseUrl(url)\n\n\tif urlData.id then\n\t\t-- id passed directly; nice, we can skip resolve.json\n\t\tcallback(string.format(\"https://api.soundcloud.com/tracks/%s/stream?client_id=%s\", urlData.id, apiKey), {})\n\telse\n\t\thttp.Fetch(\n\t\t\tstring.format(\"https://api.soundcloud.com/resolve.json?url=http://soundcloud.com/%s&client_id=%s\", urlData.path, apiKey),\n\t\t\tfunction(data)\n\t\t\t\tlocal jsonTable = util.JSONToTable(data)\n\t\t\t\tif not jsonTable then\n\t\t\t\t\tErrorNoHalt(\"Failed to retrieve SC track id for \" .. urlData.path .. \": empty JSON\")\n\t\t\t\t\treturn\n\t\t\t\tend\n\n\t\t\t\tlocal id = jsonTable.id\n\t\t\t\tcallback(string.format(\"https://api.soundcloud.com/tracks/%s/stream?client_id=%s\", id, apiKey), {})\n\t\t\tend)\n\tend\nend\n\nfunction SoundcloudService:directQuery(url, callback)\n\tlocal apiKey = medialib.SOUNDCLOUD_API_KEY\n\tif not apiKey then\n\t\tcallback(\"Missing SoundCloud API key\")\n\t\treturn\n\tend\n\n\tlocal urlData = self:parseUrl(url)\n\n\tlocal metaurl\n\tif urlData.path then\n\t\tmetaurl = string.format(\"https://api.soundcloud.com/resolve.json?url=http://soundcloud.com/%s&client_id=%s\", urlData.path, apiKey)\n\telse\n\t\tmetaurl = string.format(\"https://api.soundcloud.com/tracks/%s?client_id=%s\", urlData.id, apiKey)\n\tend\n\n\thttp.Fetch(metaurl, function(result, size)\n\t\tif size == 0 then\n\t\t\tcallback(\"http body size = 0\")\n\t\t\treturn\n\t\tend\n\n\t\tlocal entry = util.JSONToTable(result)\n\n\t\tif entry.errors then\n\t\t\tlocal msg = entry.errors[1].error_message or \"error\"\n\n\t\t\tlocal translated = msg\n\t\t\tif string.StartWith(msg, \"404\") then\n\t\t\t\ttranslated = \"Invalid id\"\n\t\t\tend\n\n\t\t\tcallback(translated)\n\t\t\treturn\n\t\tend\n\n\t\tcallback(nil, {\n\t\t\ttitle = entry.title,\n\t\t\tduration = tonumber(entry.duration) / 1000\n\t\t})\n\tend, function(err) callback(\"HTTP: \" .. err) end)\nend\n\nreturn SoundcloudService\n" medialib.FolderItems["services/twitch.lua"] = "local oop = medialib.load(\"oop\")\n\nlocal TwitchService = oop.class(\"TwitchService\", \"HTMLService\")\nTwitchService.identifier = \"twitch\"\n\nlocal all_patterns = {\n\t\"https?://www.twitch.tv/([A-Za-z0-9_%-]+)\",\n\t\"https?://twitch.tv/([A-Za-z0-9_%-]+)\"\n}\n\nfunction TwitchService:parseUrl(url)\n\tfor _,pattern in pairs(all_patterns) do\n\t\tlocal id = string.match(url, pattern)\n\t\tif id then\n\t\t\treturn {id = id}\n\t\tend\n\tend\nend\n\nfunction TwitchService:isValidUrl(url)\n\treturn self:parseUrl(url) ~= nil\nend\n\nlocal player_url = \"https://wyozi.github.io/gmod-medialib/twitch.html?channel=%s\"\nfunction TwitchService:resolveUrl(url, callback)\n\tlocal urlData = self:parseUrl(url)\n\tlocal playerUrl = string.format(player_url, urlData.id)\n\n\tcallback(playerUrl, {start = urlData.start})\nend\n\nfunction TwitchService:directQuery(url, callback)\n\tlocal urlData = self:parseUrl(url)\n\tlocal metaurl = string.format(\"https://api.twitch.tv/kraken/channels/%s\", urlData.id)\n\n\thttp.Fetch(metaurl, function(result, size)\n\t\tif size == 0 then\n\t\t\tcallback(\"http body size = 0\")\n\t\t\treturn\n\t\tend\n\n\t\tlocal data = {}\n\t\tdata.id = urlData.id\n\n\t\tlocal jsontbl = util.JSONToTable(result)\n\n\t\tif jsontbl then\n\t\t\tif jsontbl.error then\n\t\t\t\tcallback(jsontbl.message)\n\t\t\t\treturn\n\t\t\telse\n\t\t\t\tdata.title = jsontbl.display_name .. \": \" .. jsontbl.status\n\t\t\tend\n\t\telse\n\t\t\tdata.title = \"ERROR\"\n\t\tend\n\n\t\tcallback(nil, data)\n\tend, function(err) callback(\"HTTP: \" .. err) end)\nend\n\nreturn TwitchService" medialib.FolderItems["services/vimeo.lua"] = "local oop = medialib.load(\"oop\")\n\nlocal VimeoService = oop.class(\"VimeoService\", \"HTMLService\")\nVimeoService.identifier = \"vimeo\"\n\nlocal all_patterns = {\n\t\"https?://www.vimeo.com/([0-9]+)\",\n\t\"https?://vimeo.com/([0-9]+)\",\n\t\"https?://www.vimeo.com/channels/staffpicks/([0-9]+)\",\n\t\"https?://vimeo.com/channels/staffpicks/([0-9]+)\",\n}\n\nfunction VimeoService:parseUrl(url)\n\tfor _,pattern in pairs(all_patterns) do\n\t\tlocal id = string.match(url, pattern)\n\t\tif id then\n\t\t\treturn {id = id}\n\t\tend\n\tend\nend\n\nfunction VimeoService:isValidUrl(url)\n\treturn self:parseUrl(url) ~= nil\nend\n\nlocal player_url = \"http://wyozi.github.io/gmod-medialib/vimeo.html?id=%s\"\nfunction VimeoService:resolveUrl(url, callback)\n\tlocal urlData = self:parseUrl(url)\n\tlocal playerUrl = string.format(player_url, urlData.id)\n\n\tcallback(playerUrl, {start = urlData.start})\nend\n\nfunction VimeoService:directQuery(url, callback)\n\tlocal urlData = self:parseUrl(url)\n\tlocal metaurl = string.format(\"http://vimeo.com/api/v2/video/%s.json\", urlData.id)\n\n\thttp.Fetch(metaurl, function(result, size, headers, httpcode)\n\t\tif size == 0 then\n\t\t\tcallback(\"http body size = 0\")\n\t\t\treturn\n\t\tend\n\n\t\tif httpcode == 404 then\n\t\t\tcallback(\"Invalid id\")\n\t\t\treturn\n\t\tend\n\n\t\tlocal data = {}\n\t\tdata.id = urlData.id\n\n\t\tlocal jsontbl = util.JSONToTable(result)\n\n\t\tif jsontbl then\n\t\t\tdata.title = jsontbl[1].title\n\t\t\tdata.duration = jsontbl[1].duration\n\t\telse\n\t\t\tdata.title = \"ERROR\"\n\t\tend\n\n\t\tcallback(nil, data)\n\tend, function(err) callback(\"HTTP: \" .. err) end)\nend\n\nfunction VimeoService:hasReliablePlaybackEvents(media)\n\treturn true\nend\n\nreturn VimeoService\n" medialib.FolderItems["services/webaudio.lua"] = "local oop = medialib.load(\"oop\")\nlocal WebAudioService = oop.class(\"WebAudioService\", \"BASSService\")\nWebAudioService.identifier = \"webaudio\"\n\nlocal all_patterns = {\n\t\"^https?://(.*)%.mp3\",\n\t\"^https?://(.*)%.ogg\",\n}\n\nfunction WebAudioService:parseUrl(url)\n\tfor _,pattern in pairs(all_patterns) do\n\t\tlocal id = string.match(url, pattern)\n\t\tif id then\n\t\t\treturn {id = id}\n\t\tend\n\tend\nend\n\nfunction WebAudioService:isValidUrl(url)\n\treturn self:parseUrl(url) ~= nil\nend\n\nfunction WebAudioService:resolveUrl(url, callback)\n\tcallback(url, {})\nend\n\nfunction WebAudioService:directQuery(url, callback)\n\tcallback(nil, {\n\t\ttitle = url:match(\"([^/]+)$\")\n\t})\nend\n\nreturn WebAudioService" medialib.FolderItems["services/webm.lua"] = "local oop = medialib.load(\"oop\")\n\nlocal WebmService = oop.class(\"WebmService\", \"HTMLService\")\nWebmService.identifier = \"webm\"\n\nlocal all_patterns = {\"^https?://.*%.webm\"}\n\nfunction WebmService:parseUrl(url)\n\tfor _,pattern in pairs(all_patterns) do\n\t\tlocal id = string.match(url, pattern)\n\t\tif id then\n\t\t\treturn {id = id}\n\t\tend\n\tend\nend\n\nfunction WebmService:isValidUrl(url)\n\treturn self:parseUrl(url) ~= nil\nend\n\nlocal player_url = \"http://wyozi.github.io/gmod-medialib/webm.html?id=%s\"\nfunction WebmService:resolveUrl(url, callback)\n\tlocal urlData = self:parseUrl(url)\n\tlocal playerUrl = string.format(player_url, urlData.id)\n\n\tcallback(playerUrl, {start = urlData.start})\nend\n\nfunction WebmService:directQuery(url, callback)\n\tcallback(nil, {\n\t\ttitle = url:match(\"([^/]+)$\")\n\t})\nend\n\nreturn WebmService" medialib.FolderItems["services/webradio.lua"] = "local oop = medialib.load(\"oop\")\nlocal WebRadioService = oop.class(\"WebRadioService\", \"BASSService\")\nWebRadioService.identifier = \"webradio\"\n\nlocal all_patterns = {\n\t\"^https?://(.*)%.pls\",\n\t\"^https?://(.*)%.m3u\"\n}\n\nfunction WebRadioService:parseUrl(url)\n\tfor _,pattern in pairs(all_patterns) do\n\t\tlocal id = string.match(url, pattern)\n\t\tif id then\n\t\t\treturn {id = id}\n\t\tend\n\tend\nend\n\nfunction WebRadioService:isValidUrl(url)\n\treturn self:parseUrl(url) ~= nil\nend\n\nfunction WebRadioService:resolveUrl(url, callback)\n\tcallback(url, {})\nend\n\nfunction WebRadioService:directQuery(url, callback)\n\tcallback(nil, {\n\t\ttitle = url:match(\"([^/]+)$\") -- the filename is the best we can get (unless we parse pls?)\n\t})\nend\n\nreturn WebRadioService" medialib.FolderItems["services/youtube.lua"] = "local oop = medialib.load(\"oop\")\n\nlocal YoutubeService = oop.class(\"YoutubeService\", \"HTMLService\")\nYoutubeService.identifier = \"youtube\"\n\nlocal raw_patterns = {\n\t\"^https?://[A-Za-z0-9%.%-]*%.?youtu%.be/([A-Za-z0-9_%-]+)\",\n\t\"^https?://[A-Za-z0-9%.%-]*%.?youtube%.com/watch%?.*v=([A-Za-z0-9_%-]+)\",\n\t\"^https?://[A-Za-z0-9%.%-]*%.?youtube%.com/v/([A-Za-z0-9_%-]+)\",\n}\nlocal all_patterns = {}\n\n-- Appends time modifier patterns to each pattern\nfor k,p in pairs(raw_patterns) do\n\tlocal function with_sep(sep)\n\t\ttable.insert(all_patterns, p .. sep .. \"t=(%d+)m(%d+)s\")\n\t\ttable.insert(all_patterns, p .. sep .. \"t=(%d+)s?\")\n\tend\n\n\t-- We probably support more separators than youtube itself, but that does not matter\n\twith_sep(\"#\")\n\twith_sep(\"&\")\n\twith_sep(\"?\")\n\n\ttable.insert(all_patterns, p)\nend\n\nfunction YoutubeService:parseUrl(url)\n\tfor _,pattern in pairs(all_patterns) do\n\t\tlocal id, time1, time2 = string.match(url, pattern)\n\t\tif id then\n\t\t\tlocal time_sec = 0\n\t\t\tif time1 and time2 then\n\t\t\t\ttime_sec = tonumber(time1)*60 + tonumber(time2)\n\t\t\telse\n\t\t\t\ttime_sec = tonumber(time1)\n\t\t\tend\n\n\t\t\treturn {\n\t\t\t\tid = id,\n\t\t\t\tstart = time_sec\n\t\t\t}\n\t\tend\n\tend\nend\n\nfunction YoutubeService:isValidUrl(url)\n\treturn self:parseUrl(url) ~= nil\nend\n\nlocal player_url = \"https://wyozi.github.io/gmod-medialib/youtube.html?id=%s\"\nfunction YoutubeService:resolveUrl(url, callback)\n\tlocal urlData = self:parseUrl(url)\n\tlocal playerUrl = string.format(player_url, urlData.id)\n\n\tcallback(playerUrl, {start = urlData.start})\nend\n\n-- http://en.wikipedia.org/wiki/ISO_8601#Durations\n-- Cheers wiox :))\nlocal function PTToSeconds(str)\n\tlocal h = str:match(\"(%d+)H\") or 0\n\tlocal m = str:match(\"(%d+)M\") or 0\n\tlocal s = str:match(\"(%d+)S\") or 0\n\treturn h*(60*60) + m*60 + s\nend\n\nlocal API_KEY = \"AIzaSyBmQHvMSiOTrmBKJ0FFJ2LmNtc4YHyUJaQ\"\nfunction YoutubeService:directQuery(url, callback)\n\tlocal urlData = self:parseUrl(url)\n\tlocal metaurl = string.format(\"https://www.googleapis.com/youtube/v3/videos?part=snippet%%2CcontentDetails&id=%s&key=%s\", urlData.id, API_KEY)\n\n\thttp.Fetch(metaurl, function(result, size)\n\t\tif size == 0 then\n\t\t\tcallback(\"http body size = 0\")\n\t\t\treturn\n\t\tend\n\n\t\tlocal data = {}\n\t\tdata.id = urlData.id\n\n\t\tlocal jsontbl = util.JSONToTable(result)\n\n\t\tif jsontbl and jsontbl.items then\n\t\t\tlocal item = jsontbl.items[1]\n\t\t\tif not item then\n\t\t\t\tcallback(\"No video id found\")\n\t\t\t\treturn\n\t\t\tend\n\n\t\t\tdata.title = item.snippet.title\n\t\t\tdata.duration = tonumber(PTToSeconds(item.contentDetails.duration))\n\t\telse\n\t\t\tcallback(result)\n\t\t\treturn\n\t\tend\n\n\t\tcallback(nil, data)\n\tend, function(err) callback(\"HTTP: \" .. err) end)\nend\n\nfunction YoutubeService:hasReliablePlaybackEvents(media)\n\treturn true\nend\n\nreturn YoutubeService\n" -- 'serviceloader'; CodeLen/MinifiedLen 533/533; Dependencies [servicebase,service_html,service_bass,media,oop] medialib.modulePlaceholder("serviceloader") do medialib.load("servicebase") medialib.load("service_html") medialib.load("service_bass") local media = medialib.load("media") -- Load the actual service files for _,file in medialib.folderIterator("services") do if medialib.DEBUG then print("[MediaLib] Registering service " .. file.name) end if SERVER then file:addcs() end local status, err = pcall(function() return file:load() end) if status then media.registerService(err.identifier, err) else print("[MediaLib] Failed to load service ", file, ": ", err) end end end -- '__loader'; CodeLen/MinifiedLen 325/325; Dependencies [mediabase,media,serviceloader] medialib.modulePlaceholder("__loader") do -- This file loads required modules in the correct order. -- For development version: this file is automatically called after autorun/medialib.lua -- For distributable: this file is loaded after packed modules have been added to medialib medialib.load("mediabase") medialib.load("media") medialib.load("serviceloader") end return medialib