1 /******************************************************************************
2 Copyright (C) 2014 by John R. Bradley <jrb@turrettech.com>
3 Copyright (C) 2018 by Hugh Bailey ("Jim") <jim@obsproject.com>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 2 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 ******************************************************************************/
18
19 #include "browser-app.hpp"
20 #include "browser-version.h"
21 #include <json11/json11.hpp>
22
23 #ifdef _WIN32
24 #include <windows.h>
25 #endif
26
27 #ifdef USE_QT_LOOP
28 #include <util/base.h>
29 #include <util/platform.h>
30 #include <util/threading.h>
31 #include <QTimer>
32 #endif
33
34 #define UNUSED_PARAMETER(x) \
35 { \
36 (void)x; \
37 }
38
39 using namespace json11;
40
GetRenderProcessHandler()41 CefRefPtr<CefRenderProcessHandler> BrowserApp::GetRenderProcessHandler()
42 {
43 return this;
44 }
45
GetBrowserProcessHandler()46 CefRefPtr<CefBrowserProcessHandler> BrowserApp::GetBrowserProcessHandler()
47 {
48 return this;
49 }
50
OnRegisterCustomSchemes(CefRawPtr<CefSchemeRegistrar> registrar)51 void BrowserApp::OnRegisterCustomSchemes(CefRawPtr<CefSchemeRegistrar> registrar)
52 {
53 #if CHROME_VERSION_BUILD >= 3683
54 registrar->AddCustomScheme("http",
55 CEF_SCHEME_OPTION_STANDARD |
56 CEF_SCHEME_OPTION_CORS_ENABLED);
57 #elif CHROME_VERSION_BUILD >= 3029
58 registrar->AddCustomScheme("http", true, false, false, false, true,
59 false);
60 #else
61 registrar->AddCustomScheme("http", true, false, false, false, true);
62 #endif
63 }
64
OnBeforeChildProcessLaunch(CefRefPtr<CefCommandLine> command_line)65 void BrowserApp::OnBeforeChildProcessLaunch(
66 CefRefPtr<CefCommandLine> command_line)
67 {
68 #ifdef _WIN32
69 std::string pid = std::to_string(GetCurrentProcessId());
70 command_line->AppendSwitchWithValue("parent_pid", pid);
71 #else
72 (void)command_line;
73 #endif
74 }
75
OnBeforeCommandLineProcessing(const CefString &,CefRefPtr<CefCommandLine> command_line)76 void BrowserApp::OnBeforeCommandLineProcessing(
77 const CefString &, CefRefPtr<CefCommandLine> command_line)
78 {
79 if (!shared_texture_available) {
80 bool enableGPU = command_line->HasSwitch("enable-gpu");
81 CefString type = command_line->GetSwitchValue("type");
82
83 if (!enableGPU && type.empty()) {
84 command_line->AppendSwitch("disable-gpu-compositing");
85 }
86 }
87
88 if (command_line->HasSwitch("disable-features")) {
89 // Don't override existing, as this can break OSR
90 std::string disableFeatures =
91 command_line->GetSwitchValue("disable-features");
92 disableFeatures += ",HardwareMediaKeyHandling";
93 command_line->AppendSwitchWithValue("disable-features",
94 disableFeatures);
95 } else {
96 command_line->AppendSwitchWithValue("disable-features",
97 "HardwareMediaKeyHandling");
98 }
99
100 command_line->AppendSwitchWithValue("autoplay-policy",
101 "no-user-gesture-required");
102 #ifdef __APPLE__
103 command_line->AppendSwitch("use-mock-keychain");
104 #endif
105 }
106
OnContextCreated(CefRefPtr<CefBrowser> browser,CefRefPtr<CefFrame>,CefRefPtr<CefV8Context> context)107 void BrowserApp::OnContextCreated(CefRefPtr<CefBrowser> browser,
108 CefRefPtr<CefFrame>,
109 CefRefPtr<CefV8Context> context)
110 {
111 CefRefPtr<CefV8Value> globalObj = context->GetGlobal();
112
113 CefRefPtr<CefV8Value> obsStudioObj = CefV8Value::CreateObject(0, 0);
114 globalObj->SetValue("obsstudio", obsStudioObj,
115 V8_PROPERTY_ATTRIBUTE_NONE);
116
117 CefRefPtr<CefV8Value> pluginVersion =
118 CefV8Value::CreateString(OBS_BROWSER_VERSION_STRING);
119 obsStudioObj->SetValue("pluginVersion", pluginVersion,
120 V8_PROPERTY_ATTRIBUTE_NONE);
121
122 CefRefPtr<CefV8Value> getCurrentScene =
123 CefV8Value::CreateFunction("getCurrentScene", this);
124 obsStudioObj->SetValue("getCurrentScene", getCurrentScene,
125 V8_PROPERTY_ATTRIBUTE_NONE);
126
127 CefRefPtr<CefV8Value> getStatus =
128 CefV8Value::CreateFunction("getStatus", this);
129 obsStudioObj->SetValue("getStatus", getStatus,
130 V8_PROPERTY_ATTRIBUTE_NONE);
131
132 CefRefPtr<CefV8Value> saveReplayBuffer =
133 CefV8Value::CreateFunction("saveReplayBuffer", this);
134 obsStudioObj->SetValue("saveReplayBuffer", saveReplayBuffer,
135 V8_PROPERTY_ATTRIBUTE_NONE);
136
137 #if !ENABLE_WASHIDDEN
138 int id = browser->GetIdentifier();
139 if (browserVis.find(id) != browserVis.end()) {
140 SetDocumentVisibility(browser, browserVis[id]);
141 }
142 #endif
143 }
144
ExecuteJSFunction(CefRefPtr<CefBrowser> browser,const char * functionName,CefV8ValueList arguments)145 void BrowserApp::ExecuteJSFunction(CefRefPtr<CefBrowser> browser,
146 const char *functionName,
147 CefV8ValueList arguments)
148 {
149 CefRefPtr<CefV8Context> context =
150 browser->GetMainFrame()->GetV8Context();
151
152 context->Enter();
153
154 CefRefPtr<CefV8Value> globalObj = context->GetGlobal();
155 CefRefPtr<CefV8Value> obsStudioObj = globalObj->GetValue("obsstudio");
156 CefRefPtr<CefV8Value> jsFunction = obsStudioObj->GetValue(functionName);
157
158 if (jsFunction && jsFunction->IsFunction())
159 jsFunction->ExecuteFunction(NULL, arguments);
160
161 context->Exit();
162 }
163
164 #if !ENABLE_WASHIDDEN
SetFrameDocumentVisibility(CefRefPtr<CefBrowser> browser,CefRefPtr<CefFrame> frame,bool isVisible)165 void BrowserApp::SetFrameDocumentVisibility(CefRefPtr<CefBrowser> browser,
166 CefRefPtr<CefFrame> frame,
167 bool isVisible)
168 {
169 UNUSED_PARAMETER(browser);
170
171 CefRefPtr<CefV8Context> context = frame->GetV8Context();
172
173 context->Enter();
174
175 CefRefPtr<CefV8Value> globalObj = context->GetGlobal();
176
177 CefRefPtr<CefV8Value> documentObject = globalObj->GetValue("document");
178
179 if (!!documentObject) {
180 documentObject->SetValue("hidden",
181 CefV8Value::CreateBool(!isVisible),
182 V8_PROPERTY_ATTRIBUTE_READONLY);
183
184 documentObject->SetValue(
185 "visibilityState",
186 CefV8Value::CreateString(isVisible ? "visible"
187 : "hidden"),
188 V8_PROPERTY_ATTRIBUTE_READONLY);
189
190 std::string script = "new CustomEvent('visibilitychange', {});";
191
192 CefRefPtr<CefV8Value> returnValue;
193 CefRefPtr<CefV8Exception> exception;
194
195 /* Create the CustomEvent object
196 * We have to use eval to invoke the new operator */
197 bool success = context->Eval(script, frame->GetURL(), 0,
198 returnValue, exception);
199
200 if (success) {
201 CefV8ValueList arguments;
202 arguments.push_back(returnValue);
203
204 CefRefPtr<CefV8Value> dispatchEvent =
205 documentObject->GetValue("dispatchEvent");
206
207 /* Dispatch visibilitychange event on the document
208 * object */
209 dispatchEvent->ExecuteFunction(documentObject,
210 arguments);
211 }
212 }
213
214 context->Exit();
215 }
216
SetDocumentVisibility(CefRefPtr<CefBrowser> browser,bool isVisible)217 void BrowserApp::SetDocumentVisibility(CefRefPtr<CefBrowser> browser,
218 bool isVisible)
219 {
220 /* This method might be called before OnContextCreated
221 * call is made. We'll save the requested visibility
222 * state here, and use it later in OnContextCreated to
223 * set initial page visibility state. */
224 browserVis[browser->GetIdentifier()] = isVisible;
225
226 std::vector<int64> frameIdentifiers;
227 /* Set visibility state for every frame in the browser
228 *
229 * According to the Page Visibility API documentation:
230 * https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
231 *
232 * "Visibility states of an <iframe> are the same as
233 * the parent document. Hiding an <iframe> using CSS
234 * properties (such as display: none;) doesn't trigger
235 * visibility events or change the state of the document
236 * contained within the frame."
237 *
238 * Thus, we set the same visibility state for every frame of the browser.
239 */
240 browser->GetFrameIdentifiers(frameIdentifiers);
241
242 for (int64 frameId : frameIdentifiers) {
243 CefRefPtr<CefFrame> frame = browser->GetFrame(frameId);
244
245 SetFrameDocumentVisibility(browser, frame, isVisible);
246 }
247 }
248 #endif
249
OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,CefRefPtr<CefFrame> frame,CefProcessId source_process,CefRefPtr<CefProcessMessage> message)250 bool BrowserApp::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
251 #if CHROME_VERSION_BUILD >= 3770
252 CefRefPtr<CefFrame> frame,
253 #endif
254 CefProcessId source_process,
255 CefRefPtr<CefProcessMessage> message)
256 {
257 UNUSED_PARAMETER(frame);
258 DCHECK(source_process == PID_BROWSER);
259
260 CefRefPtr<CefListValue> args = message->GetArgumentList();
261
262 if (message->GetName() == "Visibility") {
263 CefV8ValueList arguments;
264 arguments.push_back(CefV8Value::CreateBool(args->GetBool(0)));
265
266 ExecuteJSFunction(browser, "onVisibilityChange", arguments);
267
268 #if !ENABLE_WASHIDDEN
269 SetDocumentVisibility(browser, args->GetBool(0));
270 #endif
271
272 } else if (message->GetName() == "Active") {
273 CefV8ValueList arguments;
274 arguments.push_back(CefV8Value::CreateBool(args->GetBool(0)));
275
276 ExecuteJSFunction(browser, "onActiveChange", arguments);
277
278 } else if (message->GetName() == "DispatchJSEvent") {
279 CefRefPtr<CefV8Context> context =
280 browser->GetMainFrame()->GetV8Context();
281
282 context->Enter();
283
284 CefRefPtr<CefV8Value> globalObj = context->GetGlobal();
285
286 std::string err;
287 auto payloadJson =
288 Json::parse(args->GetString(1).ToString(), err);
289
290 Json::object wrapperJson;
291 if (args->GetSize() > 1)
292 wrapperJson["detail"] = payloadJson;
293 std::string wrapperJsonString = Json(wrapperJson).dump();
294 std::string script;
295
296 script += "new CustomEvent('";
297 script += args->GetString(0).ToString();
298 script += "', ";
299 script += wrapperJsonString;
300 script += ");";
301
302 CefRefPtr<CefV8Value> returnValue;
303 CefRefPtr<CefV8Exception> exception;
304
305 /* Create the CustomEvent object
306 * We have to use eval to invoke the new operator */
307 context->Eval(script, browser->GetMainFrame()->GetURL(), 0,
308 returnValue, exception);
309
310 CefV8ValueList arguments;
311 arguments.push_back(returnValue);
312
313 CefRefPtr<CefV8Value> dispatchEvent =
314 globalObj->GetValue("dispatchEvent");
315 dispatchEvent->ExecuteFunction(NULL, arguments);
316
317 context->Exit();
318
319 } else if (message->GetName() == "executeCallback") {
320 CefRefPtr<CefV8Context> context =
321 browser->GetMainFrame()->GetV8Context();
322 CefRefPtr<CefV8Value> retval;
323 CefRefPtr<CefV8Exception> exception;
324
325 context->Enter();
326
327 CefRefPtr<CefListValue> arguments = message->GetArgumentList();
328 int callbackID = arguments->GetInt(0);
329 CefString jsonString = arguments->GetString(1);
330
331 std::string script;
332 script += "JSON.parse('";
333 script += arguments->GetString(1).ToString();
334 script += "');";
335
336 CefRefPtr<CefV8Value> callback = callbackMap[callbackID];
337 CefV8ValueList args;
338
339 context->Eval(script, browser->GetMainFrame()->GetURL(), 0,
340 retval, exception);
341
342 args.push_back(retval);
343
344 if (callback)
345 callback->ExecuteFunction(NULL, args);
346
347 context->Exit();
348
349 callbackMap.erase(callbackID);
350
351 } else {
352 return false;
353 }
354
355 return true;
356 }
357
Execute(const CefString & name,CefRefPtr<CefV8Value>,const CefV8ValueList & arguments,CefRefPtr<CefV8Value> &,CefString &)358 bool BrowserApp::Execute(const CefString &name, CefRefPtr<CefV8Value>,
359 const CefV8ValueList &arguments,
360 CefRefPtr<CefV8Value> &, CefString &)
361 {
362 if (name == "getCurrentScene" || name == "getStatus" ||
363 name == "saveReplayBuffer") {
364 if (arguments.size() == 1 && arguments[0]->IsFunction()) {
365 callbackId++;
366 callbackMap[callbackId] = arguments[0];
367 }
368
369 CefRefPtr<CefProcessMessage> msg =
370 CefProcessMessage::Create(name);
371 CefRefPtr<CefListValue> args = msg->GetArgumentList();
372 args->SetInt(0, callbackId);
373
374 CefRefPtr<CefBrowser> browser =
375 CefV8Context::GetCurrentContext()->GetBrowser();
376 SendBrowserProcessMessage(browser, PID_BROWSER, msg);
377
378 } else {
379 /* Function does not exist. */
380 return false;
381 }
382
383 return true;
384 }
385
386 #ifdef USE_QT_LOOP
387 Q_DECLARE_METATYPE(MessageTask);
388 MessageObject messageObject;
389
QueueBrowserTask(CefRefPtr<CefBrowser> browser,BrowserFunc func)390 void QueueBrowserTask(CefRefPtr<CefBrowser> browser, BrowserFunc func)
391 {
392 std::lock_guard<std::mutex> lock(messageObject.browserTaskMutex);
393 messageObject.browserTasks.emplace_back(browser, func);
394
395 QMetaObject::invokeMethod(&messageObject, "ExecuteNextBrowserTask",
396 Qt::QueuedConnection);
397 }
398
ExecuteNextBrowserTask()399 bool MessageObject::ExecuteNextBrowserTask()
400 {
401 Task nextTask;
402 {
403 std::lock_guard<std::mutex> lock(browserTaskMutex);
404 if (!browserTasks.size())
405 return false;
406
407 nextTask = browserTasks[0];
408 browserTasks.pop_front();
409 }
410
411 nextTask.func(nextTask.browser);
412 return true;
413 }
414
ExecuteTask(MessageTask task)415 void MessageObject::ExecuteTask(MessageTask task)
416 {
417 task();
418 }
419
DoCefMessageLoop(int ms)420 void MessageObject::DoCefMessageLoop(int ms)
421 {
422 if (ms)
423 QTimer::singleShot((int)ms + 2,
424 []() { CefDoMessageLoopWork(); });
425 else
426 CefDoMessageLoopWork();
427 }
428
Process()429 void MessageObject::Process()
430 {
431 CefDoMessageLoopWork();
432 }
433
ProcessCef()434 void ProcessCef()
435 {
436 QMetaObject::invokeMethod(&messageObject, "DoCefMessageLoop",
437 Qt::QueuedConnection, Q_ARG(int, (int)0));
438 }
439
440 #define MAX_DELAY (1000 / 30)
441
OnScheduleMessagePumpWork(int64 delay_ms)442 void BrowserApp::OnScheduleMessagePumpWork(int64 delay_ms)
443 {
444 if (delay_ms < 0)
445 delay_ms = 0;
446 else if (delay_ms > MAX_DELAY)
447 delay_ms = MAX_DELAY;
448
449 if (!frameTimer.isActive()) {
450 QObject::connect(&frameTimer, &QTimer::timeout, &messageObject,
451 &MessageObject::Process);
452 frameTimer.setSingleShot(false);
453 frameTimer.start(33);
454 }
455
456 QMetaObject::invokeMethod(&messageObject, "DoCefMessageLoop",
457 Qt::QueuedConnection,
458 Q_ARG(int, (int)delay_ms));
459 }
460 #endif
461