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 "obs-browser-source.hpp"
20 #include "browser-client.hpp"
21 #include "browser-scheme.hpp"
22 #include "wide-string.hpp"
23 #include "json11/json11.hpp"
24 #include <util/threading.h>
25 #include <QApplication>
26 #include <util/dstr.h>
27 #include <functional>
28 #include <thread>
29 #include <mutex>
30 
31 #ifdef __linux__
32 #include "linux-keyboard-helpers.hpp"
33 #endif
34 
35 #ifdef USE_QT_LOOP
36 #include <QEventLoop>
37 #include <QThread>
38 #endif
39 
40 using namespace std;
41 using namespace json11;
42 
43 extern bool QueueCEFTask(std::function<void()> task);
44 
45 static mutex browser_list_mutex;
46 static BrowserSource *first_browser = nullptr;
47 
SendBrowserVisibility(CefRefPtr<CefBrowser> browser,bool isVisible)48 static void SendBrowserVisibility(CefRefPtr<CefBrowser> browser, bool isVisible)
49 {
50 	if (!browser)
51 		return;
52 
53 #if ENABLE_WASHIDDEN
54 	if (isVisible) {
55 		browser->GetHost()->WasHidden(false);
56 		browser->GetHost()->Invalidate(PET_VIEW);
57 	} else {
58 		browser->GetHost()->WasHidden(true);
59 	}
60 #endif
61 
62 	CefRefPtr<CefProcessMessage> msg =
63 		CefProcessMessage::Create("Visibility");
64 	CefRefPtr<CefListValue> args = msg->GetArgumentList();
65 	args->SetBool(0, isVisible);
66 	SendBrowserProcessMessage(browser, PID_RENDERER, msg);
67 }
68 
69 void DispatchJSEvent(std::string eventName, std::string jsonString,
70 		     BrowserSource *browser = nullptr);
71 
BrowserSource(obs_data_t *,obs_source_t * source_)72 BrowserSource::BrowserSource(obs_data_t *, obs_source_t *source_)
73 	: source(source_)
74 {
75 	/* defer update */
76 	obs_source_update(source, nullptr);
77 
78 	lock_guard<mutex> lock(browser_list_mutex);
79 	p_prev_next = &first_browser;
80 	next = first_browser;
81 	if (first_browser)
82 		first_browser->p_prev_next = &next;
83 	first_browser = this;
84 }
85 
~BrowserSource()86 BrowserSource::~BrowserSource()
87 {
88 	DestroyBrowser();
89 	DestroyTextures();
90 
91 	lock_guard<mutex> lock(browser_list_mutex);
92 	if (next)
93 		next->p_prev_next = p_prev_next;
94 	*p_prev_next = next;
95 }
96 
ExecuteOnBrowser(BrowserFunc func,bool async)97 void BrowserSource::ExecuteOnBrowser(BrowserFunc func, bool async)
98 {
99 	if (!async) {
100 #ifdef USE_QT_LOOP
101 		if (QThread::currentThread() == qApp->thread()) {
102 			if (!!cefBrowser)
103 				func(cefBrowser);
104 			return;
105 		}
106 #endif
107 		os_event_t *finishedEvent;
108 		os_event_init(&finishedEvent, OS_EVENT_TYPE_AUTO);
109 		bool success = QueueCEFTask([&]() {
110 			if (!!cefBrowser)
111 				func(cefBrowser);
112 			os_event_signal(finishedEvent);
113 		});
114 		if (success) {
115 			os_event_wait(finishedEvent);
116 		}
117 		os_event_destroy(finishedEvent);
118 	} else {
119 		CefRefPtr<CefBrowser> browser = cefBrowser;
120 		if (!!browser) {
121 #ifdef USE_QT_LOOP
122 			QueueBrowserTask(cefBrowser, func);
123 #else
124 			QueueCEFTask([=]() { func(browser); });
125 #endif
126 		}
127 	}
128 }
129 
CreateBrowser()130 bool BrowserSource::CreateBrowser()
131 {
132 	return QueueCEFTask([this]() {
133 #ifdef SHARED_TEXTURE_SUPPORT_ENABLED
134 		if (hwaccel) {
135 			obs_enter_graphics();
136 			tex_sharing_avail = gs_shared_texture_available();
137 			obs_leave_graphics();
138 		}
139 #else
140 		bool hwaccel = false;
141 #endif
142 
143 		CefRefPtr<BrowserClient> browserClient = new BrowserClient(
144 			this, hwaccel && tex_sharing_avail, reroute_audio);
145 
146 		CefWindowInfo windowInfo;
147 #if CHROME_VERSION_BUILD < 3071
148 		windowInfo.transparent_painting_enabled = true;
149 #endif
150 		windowInfo.width = width;
151 		windowInfo.height = height;
152 		windowInfo.windowless_rendering_enabled = true;
153 
154 #ifdef SHARED_TEXTURE_SUPPORT_ENABLED
155 		windowInfo.shared_texture_enabled = hwaccel;
156 #endif
157 
158 		CefBrowserSettings cefBrowserSettings;
159 
160 #ifdef SHARED_TEXTURE_SUPPORT_ENABLED
161 #ifdef _WIN32
162 		if (!fps_custom) {
163 			windowInfo.external_begin_frame_enabled = true;
164 			cefBrowserSettings.windowless_frame_rate = 0;
165 		} else {
166 			cefBrowserSettings.windowless_frame_rate = fps;
167 		}
168 #else
169 		double video_fps = obs_get_active_fps();
170 		cefBrowserSettings.windowless_frame_rate =
171 			(fps_custom) ? fps : video_fps;
172 #endif
173 #else
174 		cefBrowserSettings.windowless_frame_rate = fps;
175 #endif
176 
177 		cefBrowserSettings.default_font_size = 16;
178 		cefBrowserSettings.default_fixed_font_size = 16;
179 
180 #if ENABLE_LOCAL_FILE_URL_SCHEME
181 		if (is_local) {
182 			/* Disable web security for file:// URLs to allow
183 			 * local content access to remote APIs */
184 			cefBrowserSettings.web_security = STATE_DISABLED;
185 		}
186 #endif
187 
188 		cefBrowser = CefBrowserHost::CreateBrowserSync(
189 			windowInfo, browserClient, url, cefBrowserSettings,
190 #if CHROME_VERSION_BUILD >= 3770
191 			CefRefPtr<CefDictionaryValue>(),
192 #endif
193 			nullptr);
194 #if CHROME_VERSION_BUILD >= 3683
195 		if (reroute_audio)
196 			cefBrowser->GetHost()->SetAudioMuted(true);
197 #endif
198 
199 		SendBrowserVisibility(cefBrowser, is_showing);
200 	});
201 }
202 
DestroyBrowser(bool async)203 void BrowserSource::DestroyBrowser(bool async)
204 {
205 	ExecuteOnBrowser(
206 		[](CefRefPtr<CefBrowser> cefBrowser) {
207 			CefRefPtr<CefClient> client =
208 				cefBrowser->GetHost()->GetClient();
209 			BrowserClient *bc =
210 				reinterpret_cast<BrowserClient *>(client.get());
211 			if (bc) {
212 				bc->bs = nullptr;
213 			}
214 
215 			/*
216 		 * This stops rendering
217 		 * http://magpcss.org/ceforum/viewtopic.php?f=6&t=12079
218 		 * https://bitbucket.org/chromiumembedded/cef/issues/1363/washidden-api-got-broken-on-branch-2062)
219 		 */
220 			cefBrowser->GetHost()->WasHidden(true);
221 			cefBrowser->GetHost()->CloseBrowser(true);
222 		},
223 		async);
224 
225 	cefBrowser = nullptr;
226 }
227 #if CHROME_VERSION_BUILD < 4103 && CHROME_VERSION_BUILD >= 3683
ClearAudioStreams()228 void BrowserSource::ClearAudioStreams()
229 {
230 	QueueCEFTask([this]() {
231 		audio_streams.clear();
232 		std::lock_guard<std::mutex> lock(audio_sources_mutex);
233 		audio_sources.clear();
234 	});
235 }
236 #endif
SendMouseClick(const struct obs_mouse_event * event,int32_t type,bool mouse_up,uint32_t click_count)237 void BrowserSource::SendMouseClick(const struct obs_mouse_event *event,
238 				   int32_t type, bool mouse_up,
239 				   uint32_t click_count)
240 {
241 	uint32_t modifiers = event->modifiers;
242 	int32_t x = event->x;
243 	int32_t y = event->y;
244 
245 	ExecuteOnBrowser(
246 		[=](CefRefPtr<CefBrowser> cefBrowser) {
247 			CefMouseEvent e;
248 			e.modifiers = modifiers;
249 			e.x = x;
250 			e.y = y;
251 			CefBrowserHost::MouseButtonType buttonType =
252 				(CefBrowserHost::MouseButtonType)type;
253 			cefBrowser->GetHost()->SendMouseClickEvent(
254 				e, buttonType, mouse_up, click_count);
255 		},
256 		true);
257 }
258 
SendMouseMove(const struct obs_mouse_event * event,bool mouse_leave)259 void BrowserSource::SendMouseMove(const struct obs_mouse_event *event,
260 				  bool mouse_leave)
261 {
262 	uint32_t modifiers = event->modifiers;
263 	int32_t x = event->x;
264 	int32_t y = event->y;
265 
266 	ExecuteOnBrowser(
267 		[=](CefRefPtr<CefBrowser> cefBrowser) {
268 			CefMouseEvent e;
269 			e.modifiers = modifiers;
270 			e.x = x;
271 			e.y = y;
272 			cefBrowser->GetHost()->SendMouseMoveEvent(e,
273 								  mouse_leave);
274 		},
275 		true);
276 }
277 
SendMouseWheel(const struct obs_mouse_event * event,int x_delta,int y_delta)278 void BrowserSource::SendMouseWheel(const struct obs_mouse_event *event,
279 				   int x_delta, int y_delta)
280 {
281 	uint32_t modifiers = event->modifiers;
282 	int32_t x = event->x;
283 	int32_t y = event->y;
284 
285 	ExecuteOnBrowser(
286 		[=](CefRefPtr<CefBrowser> cefBrowser) {
287 			CefMouseEvent e;
288 			e.modifiers = modifiers;
289 			e.x = x;
290 			e.y = y;
291 			cefBrowser->GetHost()->SendMouseWheelEvent(e, x_delta,
292 								   y_delta);
293 		},
294 		true);
295 }
296 
SendFocus(bool focus)297 void BrowserSource::SendFocus(bool focus)
298 {
299 	ExecuteOnBrowser(
300 		[=](CefRefPtr<CefBrowser> cefBrowser) {
301 			cefBrowser->GetHost()->SendFocusEvent(focus);
302 		},
303 		true);
304 }
305 
SendKeyClick(const struct obs_key_event * event,bool key_up)306 void BrowserSource::SendKeyClick(const struct obs_key_event *event, bool key_up)
307 {
308 	std::string text = event->text;
309 #ifdef __linux__
310 	uint32_t native_vkey = KeyboardCodeFromXKeysym(event->native_vkey);
311 	uint32_t modifiers = event->native_modifiers;
312 #elif defined(_WIN32)
313 	uint32_t native_vkey = event->native_vkey;
314 	uint32_t modifiers = event->modifiers;
315 #else
316 	uint32_t native_vkey = event->native_vkey;
317 	uint32_t native_scancode = event->native_scancode;
318 	uint32_t modifiers = event->native_modifiers;
319 #endif
320 
321 	ExecuteOnBrowser(
322 		[=](CefRefPtr<CefBrowser> cefBrowser) {
323 			CefKeyEvent e;
324 			e.windows_key_code = native_vkey;
325 #ifdef __APPLE__
326 			e.native_key_code = native_scancode;
327 #endif
328 
329 			e.type = key_up ? KEYEVENT_KEYUP : KEYEVENT_RAWKEYDOWN;
330 
331 			if (!text.empty()) {
332 				wstring wide = to_wide(text);
333 				if (wide.size())
334 					e.character = wide[0];
335 			}
336 
337 			//e.native_key_code = native_vkey;
338 			e.modifiers = modifiers;
339 
340 			cefBrowser->GetHost()->SendKeyEvent(e);
341 			if (!text.empty() && !key_up) {
342 				e.type = KEYEVENT_CHAR;
343 #ifdef __linux__
344 				e.windows_key_code =
345 					KeyboardCodeFromXKeysym(e.character);
346 #elif defined(_WIN32)
347 				e.windows_key_code = e.character;
348 #else
349 				e.native_key_code = native_scancode;
350 #endif
351 				cefBrowser->GetHost()->SendKeyEvent(e);
352 			}
353 		},
354 		true);
355 }
356 
SetShowing(bool showing)357 void BrowserSource::SetShowing(bool showing)
358 {
359 	is_showing = showing;
360 
361 	if (shutdown_on_invisible) {
362 		if (showing) {
363 			Update();
364 		} else {
365 			DestroyBrowser(true);
366 		}
367 	} else {
368 		ExecuteOnBrowser(
369 			[=](CefRefPtr<CefBrowser> cefBrowser) {
370 				CefRefPtr<CefProcessMessage> msg =
371 					CefProcessMessage::Create("Visibility");
372 				CefRefPtr<CefListValue> args =
373 					msg->GetArgumentList();
374 				args->SetBool(0, showing);
375 				SendBrowserProcessMessage(cefBrowser,
376 							  PID_RENDERER, msg);
377 			},
378 			true);
379 		Json json = Json::object{{"visible", showing}};
380 		DispatchJSEvent("obsSourceVisibleChanged", json.dump(), this);
381 #if defined(_WIN32) && defined(SHARED_TEXTURE_SUPPORT_ENABLED)
382 		if (showing && !fps_custom) {
383 			reset_frame = false;
384 		}
385 #endif
386 
387 		SendBrowserVisibility(cefBrowser, showing);
388 	}
389 }
390 
SetActive(bool active)391 void BrowserSource::SetActive(bool active)
392 {
393 	ExecuteOnBrowser(
394 		[=](CefRefPtr<CefBrowser> cefBrowser) {
395 			CefRefPtr<CefProcessMessage> msg =
396 				CefProcessMessage::Create("Active");
397 			CefRefPtr<CefListValue> args = msg->GetArgumentList();
398 			args->SetBool(0, active);
399 			SendBrowserProcessMessage(cefBrowser, PID_RENDERER,
400 						  msg);
401 		},
402 		true);
403 	Json json = Json::object{{"active", active}};
404 	DispatchJSEvent("obsSourceActiveChanged", json.dump(), this);
405 }
406 
Refresh()407 void BrowserSource::Refresh()
408 {
409 	ExecuteOnBrowser(
410 		[](CefRefPtr<CefBrowser> cefBrowser) {
411 			cefBrowser->ReloadIgnoreCache();
412 		},
413 		true);
414 }
415 #ifdef SHARED_TEXTURE_SUPPORT_ENABLED
416 #ifdef _WIN32
SignalBeginFrame()417 inline void BrowserSource::SignalBeginFrame()
418 {
419 	if (reset_frame) {
420 		ExecuteOnBrowser(
421 			[](CefRefPtr<CefBrowser> cefBrowser) {
422 				cefBrowser->GetHost()->SendExternalBeginFrame();
423 			},
424 			true);
425 
426 		reset_frame = false;
427 	}
428 }
429 #endif
430 #endif
431 
Update(obs_data_t * settings)432 void BrowserSource::Update(obs_data_t *settings)
433 {
434 	if (settings) {
435 		bool n_is_local;
436 		int n_width;
437 		int n_height;
438 		bool n_fps_custom;
439 		int n_fps;
440 		bool n_shutdown;
441 		bool n_restart;
442 		bool n_reroute;
443 		std::string n_url;
444 		std::string n_css;
445 
446 		n_is_local = obs_data_get_bool(settings, "is_local_file");
447 		n_width = (int)obs_data_get_int(settings, "width");
448 		n_height = (int)obs_data_get_int(settings, "height");
449 		n_fps_custom = obs_data_get_bool(settings, "fps_custom");
450 		n_fps = (int)obs_data_get_int(settings, "fps");
451 		n_shutdown = obs_data_get_bool(settings, "shutdown");
452 		n_restart = obs_data_get_bool(settings, "restart_when_active");
453 		n_css = obs_data_get_string(settings, "css");
454 		n_url = obs_data_get_string(settings,
455 					    n_is_local ? "local_file" : "url");
456 		n_reroute = obs_data_get_bool(settings, "reroute_audio");
457 
458 		if (n_is_local && !n_url.empty()) {
459 			n_url = CefURIEncode(n_url, false);
460 
461 #ifdef _WIN32
462 			size_t slash = n_url.find("%2F");
463 			size_t colon = n_url.find("%3A");
464 
465 			if (slash != std::string::npos &&
466 			    colon != std::string::npos && colon < slash)
467 				n_url.replace(colon, 3, ":");
468 #endif
469 
470 			while (n_url.find("%5C") != std::string::npos)
471 				n_url.replace(n_url.find("%5C"), 3, "/");
472 
473 			while (n_url.find("%2F") != std::string::npos)
474 				n_url.replace(n_url.find("%2F"), 3, "/");
475 
476 #if !ENABLE_LOCAL_FILE_URL_SCHEME
477 			/* http://absolute/ based mapping for older CEF */
478 			n_url = "http://absolute/" + n_url;
479 #elif defined(_WIN32)
480 			/* Widows-style local file URL:
481 			 * file:///C:/file/path.webm */
482 			n_url = "file:///" + n_url;
483 #else
484 			/* UNIX-style local file URL:
485 			 * file:///home/user/file.webm */
486 			n_url = "file://" + n_url;
487 #endif
488 		}
489 
490 #if ENABLE_LOCAL_FILE_URL_SCHEME
491 		if (astrcmpi_n(n_url.c_str(), "http://absolute/", 16) == 0) {
492 			/* Replace http://absolute/ URLs with file://
493 			 * URLs if file:// URLs are enabled */
494 			n_url = "file:///" + n_url.substr(16);
495 			n_is_local = true;
496 		}
497 #endif
498 
499 		if (n_is_local == is_local && n_width == width &&
500 		    n_height == height && n_fps_custom == fps_custom &&
501 		    n_fps == fps && n_shutdown == shutdown_on_invisible &&
502 		    n_restart == restart && n_css == css && n_url == url &&
503 		    n_reroute == reroute_audio) {
504 			return;
505 		}
506 
507 		is_local = n_is_local;
508 		width = n_width;
509 		height = n_height;
510 		fps = n_fps;
511 		fps_custom = n_fps_custom;
512 		shutdown_on_invisible = n_shutdown;
513 		reroute_audio = n_reroute;
514 		restart = n_restart;
515 		css = n_css;
516 		url = n_url;
517 
518 		obs_source_set_audio_active(source, reroute_audio);
519 	}
520 
521 	DestroyBrowser(true);
522 	DestroyTextures();
523 #if CHROME_VERSION_BUILD < 4103 && CHROME_VERSION_BUILD >= 3683
524 	ClearAudioStreams();
525 #endif
526 	if (!shutdown_on_invisible || obs_source_showing(source))
527 		create_browser = true;
528 
529 	first_update = false;
530 }
531 
Tick()532 void BrowserSource::Tick()
533 {
534 	if (create_browser && CreateBrowser())
535 		create_browser = false;
536 #if defined(_WIN32) && defined(SHARED_TEXTURE_SUPPORT_ENABLED)
537 	if (!fps_custom)
538 		reset_frame = true;
539 #endif
540 }
541 
542 extern void ProcessCef();
543 
Render()544 void BrowserSource::Render()
545 {
546 	bool flip = false;
547 #ifdef SHARED_TEXTURE_SUPPORT_ENABLED
548 	flip = hwaccel;
549 #endif
550 
551 	if (texture) {
552 #ifdef __APPLE__
553 		gs_effect_t *effect = obs_get_base_effect(
554 			(hwaccel) ? OBS_EFFECT_DEFAULT_RECT
555 				  : OBS_EFFECT_PREMULTIPLIED_ALPHA);
556 #else
557 		gs_effect_t *effect =
558 			obs_get_base_effect(OBS_EFFECT_PREMULTIPLIED_ALPHA);
559 #endif
560 
561 		const bool current =
562 			gs_is_srgb_format(gs_texture_get_color_format(texture));
563 		const bool previous = gs_set_linear_srgb(current);
564 		while (gs_effect_loop(effect, "Draw"))
565 			obs_source_draw(texture, 0, 0, 0, 0, flip);
566 		gs_set_linear_srgb(previous);
567 	}
568 
569 #if defined(_WIN32) && defined(SHARED_TEXTURE_SUPPORT_ENABLED)
570 	SignalBeginFrame();
571 #elif USE_QT_LOOP
572 	ProcessCef();
573 #endif
574 }
575 
ExecuteOnBrowser(BrowserFunc func,BrowserSource * bs)576 static void ExecuteOnBrowser(BrowserFunc func, BrowserSource *bs)
577 {
578 	lock_guard<mutex> lock(browser_list_mutex);
579 
580 	if (bs) {
581 		BrowserSource *bsw = reinterpret_cast<BrowserSource *>(bs);
582 		bsw->ExecuteOnBrowser(func, true);
583 	}
584 }
585 
ExecuteOnAllBrowsers(BrowserFunc func)586 static void ExecuteOnAllBrowsers(BrowserFunc func)
587 {
588 	lock_guard<mutex> lock(browser_list_mutex);
589 
590 	BrowserSource *bs = first_browser;
591 	while (bs) {
592 		BrowserSource *bsw = reinterpret_cast<BrowserSource *>(bs);
593 		bsw->ExecuteOnBrowser(func, true);
594 		bs = bs->next;
595 	}
596 }
597 
DispatchJSEvent(std::string eventName,std::string jsonString,BrowserSource * browser)598 void DispatchJSEvent(std::string eventName, std::string jsonString,
599 		     BrowserSource *browser)
600 {
601 	const auto jsEvent = [=](CefRefPtr<CefBrowser> cefBrowser) {
602 		CefRefPtr<CefProcessMessage> msg =
603 			CefProcessMessage::Create("DispatchJSEvent");
604 		CefRefPtr<CefListValue> args = msg->GetArgumentList();
605 
606 		args->SetString(0, eventName);
607 		args->SetString(1, jsonString);
608 		SendBrowserProcessMessage(cefBrowser, PID_RENDERER, msg);
609 	};
610 
611 	if (!browser)
612 		ExecuteOnAllBrowsers(jsEvent);
613 	else
614 		ExecuteOnBrowser(jsEvent, browser);
615 }
616