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