-- -- DMediaPlayerHTML -- -- Improves upon the Cinema HTML control by adding: -- Window object callbacks, HTML and JS console filtering, URL polling, -- simulated mouse clicks, and proper fullscreen resizing. -- -- Cinema HTML control reference: -- https://github.com/pixeltailgames/cinema/blob/master/cinema/gamemode/modules/scoreboard/controls/cl_html.lua -- local PANEL = {} DEFINE_BASECLASS( "Panel" ) local DEBUG = false local JS_CallbackHack = [[(function(){ var funcname = '%s'; window[funcname] = function(){ _gm[funcname].apply(_gm,arguments); } })();]] local FilterCVar = CreateClientConVar( "js_console_filter", "0", true, false ) local FILTER_ALL = 0 local FILTER_NONE = 1 --[[--------------------------------------------------------- -----------------------------------------------------------]] function PANEL:Init() self.JS = {} self.Callbacks = {} self.MouseActions = {} self.URL = "data:text/html," hook.Add( "HUDPaint", self, function() self:HUDPaint() end ) end function PANEL:OnDocumentReady( url ) -- Detect CEF error pages via centralized helper if MediaPlayer.ChromeError then local hostname = MediaPlayer.ChromeError.ExtractHostname(self.URL) local handled = MediaPlayer.ChromeError.HandleDocumentReady(self, url, { hostname = hostname, }) if handled then self._chromeErrorHostname = hostname return end end self._chromeErrorHandled = nil self._chromeErrorHostname = nil -- -- Implement a console - because awesomium doesn't provide it for us anymore -- local console_funcs = {"log","error","debug","warn","info"} for _, func in pairs(console_funcs) do self:AddFunction( "console", func, function(param) self:ConsoleMessage( param, func ) end ) end self:AddFunction( "gmod", "getUrl", function( url ) self:SetURL( url ) end ) end function PANEL:Think() if self:IsLoading() then -- Call started loading if not self._loading then -- Get the page URL self:FetchPageURL() self._loading = true self:OnStartLoading() end else -- Call finished loading if self._loading then -- Get the page URL self:FetchPageURL() -- Hack to add window object callbacks if self.Callbacks.window then for funcname, callback in pairs(self.Callbacks.window) do self:RunJavascript( JS_CallbackHack:format(funcname) ) end end self._loading = nil self:OnFinishLoading() end -- Run queued javascript if self.JS then for k, v in pairs( self.JS ) do self:RunJavascript( v ) end self.JS = nil end end -- HACK: Poll page for URL change if not self._nextUrlPoll or self._nextUrlPoll < RealTime() then self:FetchPageURL() self._nextUrlPoll = RealTime() + 1 end end function PANEL:FetchPageURL() local js = [[ if (typeof gmod === 'object' && typeof gmod.getUrl === 'function') { gmod.getUrl(window.location.href); } ]] self:RunJavascript(js) end function PANEL:GetURL() return self.URL end function PANEL:SetURL( url ) local current = self.URL if current ~= url then self:OnURLChanged( url, current ) end self.URL = url end function PANEL:OnURLChanged( new, old ) if FilterCVar:GetInt() > FILTER_ALL then print( "URL Changed: " .. tostring(new) ) end -- Track the last known good URL for hostname extraction if isstring(new) and not new:StartWith("chrome-error://") then self._lastGoodURL = new end end --[[--------------------------------------------------------- Awesomium Override Functions -----------------------------------------------------------]] function PANEL:SetSize( w, h, fullscreen ) local keyboardEnabled = self:IsKeyboardInputEnabled() local mouseEnabled = self:IsMouseInputEnabled() if fullscreen then -- Cache fullscreen size local cw, ch = self:GetSize() self._OrigSize = { w = cw, h = ch } -- Render before the HUD self:ParentToHUD() elseif self._OrigSize then -- Restore cached size w = self._OrigSize.w h = self._OrigSize.h self._OrigSize = nil -- Reparent due to hud parented panels sometimes being inaccessible -- from Lua. self:SetParent( vgui.GetWorldPanel() ) else self._OrigSize = nil end self:SetKeyboardInputEnabled( keyboardEnabled ) self:SetMouseInputEnabled( mouseEnabled ) if not (w and h) then return end BaseClass.SetSize( self, w, h ) end function PANEL:OpenURL( url ) if DEBUG then print("DMediaPlayerHTML.OpenURL", url) end self:SetURL( url ) BaseClass.OpenURL( self, url ) end function PANEL:SetHTML( html ) if DEBUG then print("DMediaPlayerHTML.SetHTML") print(html) end BaseClass.SetHTML( self, html ) end --[[--------------------------------------------------------- Window loading events -----------------------------------------------------------]] -- -- Called when the page begins loading -- function PANEL:OnStartLoading() end -- -- Called when the page finishes loading all assets -- function PANEL:OnFinishLoading() end function PANEL:OnPlayerReady() -- Override in child panels or services to handle player ready events end --[[--------------------------------------------------------- Lua => JavaScript queue This code only runs when the page is finished loading; this means all assets (images, CSS, etc.) must load first! -----------------------------------------------------------]] function PANEL:QueueJavascript( js ) -- -- Can skip using the queue if there's nothing else in it -- if not ( self.JS or self:IsLoading() ) then return self:RunJavascript( js ) end self.JS = self.JS or {} table.insert( self.JS, js ) self:Think() end PANEL.QueueJavaScript = PANEL.QueueJavascript PANEL.Call = PANEL.QueueJavascript --[[--------------------------------------------------------- Handle console logging from JavaScript -----------------------------------------------------------]] PANEL.ConsoleColors = { ["default"] = Color(255,160,255), ["text"] = Color(255,255,255), ["error"] = Color(235,57,65), ["warn"] = Color(227,181,23), ["info"] = Color(100,173,229), } function PANEL:ConsoleMessage( ... ) local filterLevel = FilterCVar:GetInt() local args = {...} local msg = args[1] -- Three arguments are passed in if an error occured if #args == 3 and filterLevel > FILTER_ALL then local script = args[2] local linenum = args[3] local col = self.ConsoleColors.error local out = { "[JavaScript]", msg, ",", script, ":", linenum, "\n" } MsgC( col, table.concat(out, " ") ) else if not isstring( msg ) then msg = "*js variable* (" .. type(msg) .. ": " .. tostring(msg) .. ")" end -- Play a sound from JavaScript console logging if msg:StartWith( "PLAY:" ) then local soundpath = msg:sub( 7 ) surface.PlaySound( soundpath ) return end -- Browser player is ready — invalidate volume cache to force re-push if msg:StartWith( "READY:" ) then self:OnPlayerReady() return end -- Handle Chrome/CEF error signals via centralized helper if MediaPlayer.ChromeError then local hostname = self._chromeErrorHostname or MediaPlayer.ChromeError.ExtractHostname(self._lastGoodURL) local handled = MediaPlayer.ChromeError.HandleConsoleMessage(msg, hostname, function() self._chromeErrorHandled = true end) if handled then return end end if filterLevel == FILTER_ALL then return end local func = args[2] -- Output console message with prefix local prefixColor = self.ConsoleColors.default local prefix = "[HTML" if func and func:len() > 0 and func ~= "log" then if self.ConsoleColors[func] then prefixColor = self.ConsoleColors[func] end prefix = prefix .. ":" .. func:upper() end prefix = prefix .. "] " MsgC( prefixColor, prefix ) MsgC( self.ConsoleColors.text, msg, "\n" ) end end --[[--------------------------------------------------------- JavaScript callbacks -----------------------------------------------------------]] local JSObjects = { window = "_gm", this = "_gm", _gm = "window" } -- -- Called by the engine when a callback function is called -- function PANEL:OnCallback( obj, func, args ) -- Hack for adding window callbacks obj = JSObjects[obj] or obj if not self.Callbacks[ obj ] then return end -- -- Use AddFunction to add functions to this. -- local f = self.Callbacks[ obj ][ func ] if ( f ) then return f( unpack( args ) ) end end -- -- Add a function to Javascript -- function PANEL:AddFunction( obj, funcname, func ) -- Hack for adding window callbacks -- obj = JSObjects[obj] or obj if obj == "this" then obj = "window" end -- Create the `object` if it doesn't exist if not self.Callbacks[ obj ] then self:NewObject( obj ) self.Callbacks[ obj ] = {} end -- This creates the function in javascript (which redirects to c++ which calls OnCallback here) self:NewObjectCallback( JSObjects[obj] or obj, funcname ) -- Store the function so OnCallback can find it and call it self.Callbacks[ obj ][ funcname ] = func end --[[--------------------------------------------------------- Scrolling -----------------------------------------------------------]] local JS_RemoveScrollbars = "document.body.style.overflow = 'hidden';" local JS_ScrollBy = "window.scrollBy(0, %d);" function PANEL:RemoveScrollbars() self:QueueJavascript(JS_RemoveScrollbars) end function PANEL:Scroll( amount ) self:QueueJavascript( JS_ScrollBy:format(amount) ) end --[[--------------------------------------------------------- Compatibility functions -----------------------------------------------------------]] function PANEL:OpeningURL( url ) end function PANEL:FinishedURL( url ) end --[[--------------------------------------------------------- Simulated mouse clicks -----------------------------------------------------------]] function PANEL:HUDPaint() self:HandleMouseActions() end function PANEL:InjectMouseClick( x, y ) if self._handlingMouseAction then return end local w, h = self:GetSize() table.insert( self.MouseActions, { x = math.Round(x * w), y = math.Round(y * h), tick = 0 } ) end function PANEL:HandleMouseActions() if #self.MouseActions == 0 then return end local action = self.MouseActions[1] action.tick = action.tick + 1 if action.tick == 1 then -- show cursor self._handlingMouseAction = true self:SetZPos( 32767 ) self:MoveToCursor( action.x, action.y ) self:MakePopup() gui.EnableScreenClicker( true ) gui.InternalCursorMoved( 0, 0 ) elseif action.tick == 2 then local cx, cy = input.GetCursorPos() gui.InternalCursorMoved( cx, cy ) elseif action.tick == 3 then -- simulate click; need to wait at least one frame gui.InternalMousePressed( MOUSE_LEFT ) gui.InternalMouseReleased( MOUSE_LEFT ) elseif action.tick > 3 then -- hide cursor gui.EnableScreenClicker( false ) self:SetKeyboardInputEnabled( false ) self:SetMouseInputEnabled( false ) self:SetZPos( -32768 ) table.remove( self.MouseActions, 1 ) self._handlingMouseAction = nil end end function PANEL:OnRemove() -- Clean up if removed mid-action to prevent stuck screen clicker if self._handlingMouseAction then gui.EnableScreenClicker( false ) self._handlingMouseAction = nil end end function PANEL:MoveToCursor( xoffset, yoffset ) xoffset = xoffset or 0 yoffset = yoffset or 0 local cx, cy = input.GetCursorPos() self:SetPos( cx - xoffset, cy - yoffset ) end derma.DefineControl( "DMediaPlayerHTML", "", PANEL, "DHTML" )