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