]> git.sesse.net Git - casparcg/blob - modules/html/producer/html_producer.cpp
551b5a83ba1e9fdc0be01d00b9651dffe17594fa
[casparcg] / modules / html / producer / html_producer.cpp
1 /*
2 * Copyright 2013 Sveriges Television AB http://casparcg.com/
3 *
4 * This file is part of CasparCG (www.casparcg.com).
5 *
6 * CasparCG is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * CasparCG is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with CasparCG. If not, see <http://www.gnu.org/licenses/>.
18 *
19 * Author: Robert Nagy, ronag89@gmail.com
20 */
21
22 #include "html_producer.h"
23
24 #include <core/video_format.h>
25
26 #include <core/monitor/monitor.h>
27 #include <core/frame/draw_frame.h>
28 #include <core/frame/frame_factory.h>
29 #include <core/producer/frame_producer.h>
30 #include <core/interaction/interaction_event.h>
31 #include <core/frame/frame.h>
32 #include <core/frame/pixel_format.h>
33 #include <core/frame/audio_channel_layout.h>
34 #include <core/frame/geometry.h>
35 #include <core/help/help_repository.h>
36 #include <core/help/help_sink.h>
37
38 #include <common/assert.h>
39 #include <common/env.h>
40 #include <common/executor.h>
41 #include <common/lock.h>
42 #include <common/future.h>
43 #include <common/diagnostics/graph.h>
44 #include <common/prec_timer.h>
45 #include <common/linq.h>
46 #include <common/os/filesystem.h>
47 #include <common/memcpy.h>
48
49 #include <boost/algorithm/string/predicate.hpp>
50 #include <boost/filesystem.hpp>
51 #include <boost/timer.hpp>
52 #include <boost/log/trivial.hpp>
53 #include <boost/property_tree/ptree.hpp>
54
55 #include <tbb/atomic.h>
56 #include <tbb/concurrent_queue.h>
57
58 #pragma warning(push)
59 #pragma warning(disable: 4458)
60 #include <cef_task.h>
61 #include <cef_app.h>
62 #include <cef_client.h>
63 #include <cef_render_handler.h>
64 #pragma warning(pop)
65
66 #include <asmlib.h>
67
68 #include <queue>
69
70 #include "../html.h"
71
72 #pragma comment (lib, "libcef.lib")
73 #pragma comment (lib, "libcef_dll_wrapper.lib")
74
75 namespace caspar { namespace html {
76                 
77 class html_client
78         : public CefClient
79         , public CefRenderHandler
80         , public CefLifeSpanHandler
81         , public CefLoadHandler
82 {
83         std::wstring                                                    url_;
84         spl::shared_ptr<diagnostics::graph>             graph_;
85         boost::timer                                                    tick_timer_;
86         boost::timer                                                    frame_timer_;
87         boost::timer                                                    paint_timer_;
88
89         spl::shared_ptr<core::frame_factory>    frame_factory_;
90         core::video_format_desc                                 format_desc_;
91         tbb::concurrent_queue<std::wstring>             javascript_before_load_;
92         tbb::atomic<bool>                                               loaded_;
93         tbb::atomic<bool>                                               removed_;
94         std::queue<core::draw_frame>                    frames_;
95         mutable boost::mutex                                    frames_mutex_;
96
97         core::draw_frame                                                last_frame_;
98         core::draw_frame                                                last_progressive_frame_;
99         mutable boost::mutex                                    last_frame_mutex_;
100
101         CefRefPtr<CefBrowser>                                   browser_;
102
103         executor                                                                executor_;
104
105 public:
106
107         html_client(
108                         spl::shared_ptr<core::frame_factory> frame_factory,
109                         const core::video_format_desc& format_desc,
110                         const std::wstring& url)
111                 : url_(url)
112                 , frame_factory_(std::move(frame_factory))
113                 , format_desc_(format_desc)
114                 , last_frame_(core::draw_frame::empty())
115                 , last_progressive_frame_(core::draw_frame::empty())
116                 , executor_(L"html_producer")
117         {
118                 graph_->set_color("browser-tick-time", diagnostics::color(0.1f, 1.0f, 0.1f));
119                 graph_->set_color("tick-time", diagnostics::color(0.0f, 0.6f, 0.9f));
120                 graph_->set_color("late-frame", diagnostics::color(0.6f, 0.3f, 0.9f));
121                 graph_->set_text(print());
122                 diagnostics::register_graph(graph_);
123
124                 loaded_ = false;
125                 removed_ = false;
126                 executor_.begin_invoke([&]{ update(); });
127         }
128
129         core::draw_frame receive()
130         {
131                 auto frame = last_frame();
132                 executor_.begin_invoke([&]{ update(); });
133                 return frame;
134         }
135
136         core::draw_frame last_frame() const
137         {
138                 return lock(last_frame_mutex_, [&]
139                 {
140                         return last_frame_;
141                 });
142         }
143
144         void execute_javascript(const std::wstring& javascript)
145         {
146                 if (!loaded_)
147                 {
148                         javascript_before_load_.push(javascript);
149                 }
150                 else
151                 {
152                         execute_queued_javascript();
153                         do_execute_javascript(javascript);
154                 }
155         }
156
157         CefRefPtr<CefBrowserHost> get_browser_host()
158         {
159                 return browser_->GetHost();
160         }
161
162         void close()
163         {
164                 html::invoke([=]
165                 {
166                         if (browser_ != nullptr)
167                         {
168                                 browser_->GetHost()->CloseBrowser(true);
169                         }
170                 });
171         }
172
173         void remove()
174         {
175                 close();
176                 removed_ = true;
177         }
178
179         bool is_removed() const
180         {
181                 return removed_;
182         }
183
184 private:
185
186         bool GetViewRect(CefRefPtr<CefBrowser> browser, CefRect &rect)
187         {
188                 CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
189
190                 rect = CefRect(0, 0, format_desc_.square_width, format_desc_.square_height);
191                 return true;
192         }
193
194         void OnPaint(
195                         CefRefPtr<CefBrowser> browser,
196                         PaintElementType type,
197                         const RectList &dirtyRects,
198                         const void *buffer,
199                         int width,
200                         int height)
201         {
202                 graph_->set_value("browser-tick-time", paint_timer_.elapsed()
203                                 * format_desc_.fps
204                                 * format_desc_.field_count
205                                 * 0.5);
206                 paint_timer_.restart();
207                 CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
208
209                 boost::timer copy_timer;
210                 core::pixel_format_desc pixel_desc;
211                         pixel_desc.format = core::pixel_format::bgra;
212                         pixel_desc.planes.push_back(
213                                 core::pixel_format_desc::plane(width, height, 4));
214                 auto frame = frame_factory_->create_frame(this, pixel_desc, core::audio_channel_layout::invalid());
215                 fast_memcpy(frame.image_data().begin(), buffer, width * height * 4);
216
217                 lock(frames_mutex_, [&]
218                 {
219                         frames_.push(core::draw_frame(std::move(frame)));
220
221                         size_t max_in_queue = format_desc_.field_count + 1;
222
223                         while (frames_.size() > max_in_queue)
224                         {
225                                 frames_.pop();
226                                 graph_->set_tag(diagnostics::tag_severity::WARNING, "dropped-frame");
227                         }
228                 });
229                 graph_->set_value("copy-time", copy_timer.elapsed()
230                                 * format_desc_.fps
231                                 * format_desc_.field_count
232                                 * 0.5);
233         }
234
235         void OnAfterCreated(CefRefPtr<CefBrowser> browser) override
236         {
237                 CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
238
239                 browser_ = browser;
240         }
241
242         void OnBeforeClose(CefRefPtr<CefBrowser> browser) override
243         {
244                 CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
245
246                 browser_ = nullptr;
247         }
248
249         bool DoClose(CefRefPtr<CefBrowser> browser) override
250         {
251                 CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
252
253                 return false;
254         }
255
256         CefRefPtr<CefRenderHandler> GetRenderHandler() override
257         {
258                 return this;
259         }
260
261         CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override
262         {
263                 return this;
264         }
265
266         CefRefPtr<CefLoadHandler> GetLoadHandler() override {
267                 return this;
268         }
269
270         void OnLoadEnd(
271                         CefRefPtr<CefBrowser> browser,
272                         CefRefPtr<CefFrame> frame,
273                         int httpStatusCode) override
274         {
275                 loaded_ = true;
276                 execute_queued_javascript();
277         }
278
279         bool OnProcessMessageReceived(
280                         CefRefPtr<CefBrowser> browser,
281                         CefProcessId source_process,
282                         CefRefPtr<CefProcessMessage> message) override
283         {
284                 auto name = message->GetName().ToString();
285
286                 if (name == REMOVE_MESSAGE_NAME)
287                 {
288                         remove();
289
290                         return true;
291                 }
292                 else if (name == LOG_MESSAGE_NAME)
293                 {
294                         auto args = message->GetArgumentList();
295                         auto severity =
296                                 static_cast<boost::log::trivial::severity_level>(args->GetInt(0));
297                         auto msg = args->GetString(1).ToWString();
298
299                         BOOST_LOG_STREAM_WITH_PARAMS(
300                                         log::logger::get(),
301                                         (boost::log::keywords::severity = severity))
302                                 << print() << L" [renderer_process] " << msg;
303                 }
304
305                 return false;
306         }
307
308         void invoke_requested_animation_frames()
309         {
310                 if (browser_)
311                         browser_->SendProcessMessage(
312                                         CefProcessId::PID_RENDERER,
313                                         CefProcessMessage::Create(TICK_MESSAGE_NAME));
314
315                 graph_->set_value("tick-time", tick_timer_.elapsed()
316                                 * format_desc_.fps
317                                 * format_desc_.field_count
318                                 * 0.5);
319                 tick_timer_.restart();
320         }
321
322         bool try_pop(core::draw_frame& result)
323         {
324                 return lock(frames_mutex_, [&]() -> bool
325                 {
326                         if (!frames_.empty())
327                         {
328                                 result = std::move(frames_.front());
329                                 frames_.pop();
330
331                                 return true;
332                         }
333
334                         return false;
335                 });
336         }
337
338         core::draw_frame pop()
339         {
340                 core::draw_frame frame;
341
342                 if (!try_pop(frame))
343                 {
344                         CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " No frame in buffer"));
345                 }
346
347                 return frame;
348         }
349
350         void update()
351         {
352                 invoke_requested_animation_frames();
353
354                 prec_timer timer;
355                 timer.tick(0.0);
356
357                 auto num_frames = lock(frames_mutex_, [&]
358                 {
359                         return frames_.size();
360                 });
361
362                 if (num_frames >= format_desc_.field_count)
363                 {
364                         if (format_desc_.field_mode != core::field_mode::progressive)
365                         {
366                                 auto frame1 = pop();
367
368                                 executor_.yield(caspar::task_priority::lowest_priority);
369                                 timer.tick(1.0 / (format_desc_.fps * format_desc_.field_count));
370                                 invoke_requested_animation_frames();
371
372                                 auto frame2 = pop();
373
374                                 lock(last_frame_mutex_, [&]
375                                 {
376                                         last_progressive_frame_ = frame2;
377                                         last_frame_ = core::draw_frame::interlace(frame1, frame2, format_desc_.field_mode);
378                                 });
379                         }
380                         else
381                         {
382                                 auto frame = pop();
383
384                                 lock(last_frame_mutex_, [&]
385                                 {
386                                         last_frame_ = frame;
387                                 });
388                         }
389                 }
390                 else if (num_frames == 1) // Interlaced but only one frame
391                 {                         // available. Probably the last frame
392                                           // of some animation sequence.
393                         auto frame = pop();
394
395                         lock(last_frame_mutex_, [&]
396                         {
397                                 last_progressive_frame_ = frame;
398                                 last_frame_ = frame;
399                         });
400
401                         timer.tick(1.0 / (format_desc_.fps * format_desc_.field_count));
402                         invoke_requested_animation_frames();
403                 }
404                 else
405                 {
406                         graph_->set_tag(diagnostics::tag_severity::INFO, "late-frame");
407
408                         if (format_desc_.field_mode != core::field_mode::progressive)
409                                 lock(last_frame_mutex_, [&]
410                                 {
411                                         last_frame_ = last_progressive_frame_;
412                                 });
413                 }
414         }
415
416         void do_execute_javascript(const std::wstring& javascript)
417         {
418                 html::begin_invoke([=]
419                 {
420                         if (browser_ != nullptr)
421                                 browser_->GetMainFrame()->ExecuteJavaScript(u8(javascript).c_str(), browser_->GetMainFrame()->GetURL(), 0);
422                 });
423         }
424
425         void execute_queued_javascript()
426         {
427                 std::wstring javascript;
428
429                 while (javascript_before_load_.try_pop(javascript))
430                         do_execute_javascript(javascript);
431         }
432
433         std::wstring print() const
434         {
435                 return L"html[" + url_ + L"]";
436         }
437
438         IMPLEMENT_REFCOUNTING(html_client);
439 };
440
441 class html_producer
442         : public core::frame_producer_base
443 {
444         core::monitor::subject  monitor_subject_;
445         const std::wstring              url_;
446         core::constraints               constraints_;
447
448         CefRefPtr<html_client>  client_;
449
450 public:
451         html_producer(
452                 const spl::shared_ptr<core::frame_factory>& frame_factory,
453                 const core::video_format_desc& format_desc,
454                 const std::wstring& url)
455                 : url_(url)
456         {
457                 constraints_.width.set(format_desc.square_width);
458                 constraints_.height.set(format_desc.square_height);
459
460                 html::invoke([&]
461                 {
462                         client_ = new html_client(frame_factory, format_desc, url_);
463
464                         CefWindowInfo window_info;
465
466                         window_info.SetTransparentPainting(true);
467                         window_info.SetAsOffScreen(nullptr);
468                         //window_info.SetAsWindowless(nullptr, true);
469                                         
470                         CefBrowserSettings browser_settings;
471                         browser_settings.web_security = cef_state_t::STATE_DISABLED;
472                         CefBrowserHost::CreateBrowser(window_info, client_.get(), url, browser_settings, nullptr);
473                 });
474         }
475
476         ~html_producer()
477         {
478                 if (client_)
479                         client_->close();
480         }
481
482         // frame_producer
483
484         std::wstring name() const override
485         {
486                 return L"html";
487         }
488
489         void on_interaction(const core::interaction_event::ptr& event) override
490         {
491                 if (core::is<core::mouse_move_event>(event))
492                 {
493                         auto move = core::as<core::mouse_move_event>(event);
494                         int x = static_cast<int>(move->x * constraints_.width.get());
495                         int y = static_cast<int>(move->y * constraints_.height.get());
496
497                         CefMouseEvent e;
498                         e.x = x;
499                         e.y = y;
500                         client_->get_browser_host()->SendMouseMoveEvent(e, false);
501                 }
502                 else if (core::is<core::mouse_button_event>(event))
503                 {
504                         auto button = core::as<core::mouse_button_event>(event);
505                         int x = static_cast<int>(button->x * constraints_.width.get());
506                         int y = static_cast<int>(button->y * constraints_.height.get());
507
508                         CefMouseEvent e;
509                         e.x = x;
510                         e.y = y;
511                         client_->get_browser_host()->SendMouseClickEvent(
512                                         e,
513                                         static_cast<CefBrowserHost::MouseButtonType>(button->button),
514                                         !button->pressed,
515                                         1);
516                 }
517                 else if (core::is<core::mouse_wheel_event>(event))
518                 {
519                         auto wheel = core::as<core::mouse_wheel_event>(event);
520                         int x = static_cast<int>(wheel->x * constraints_.width.get());
521                         int y = static_cast<int>(wheel->y * constraints_.height.get());
522
523                         CefMouseEvent e;
524                         e.x = x;
525                         e.y = y;
526                         static const int WHEEL_TICKS_AMPLIFICATION = 40;
527                         client_->get_browser_host()->SendMouseWheelEvent(
528                                         e,
529                                         0,                                               // delta_x
530                                         wheel->ticks_delta * WHEEL_TICKS_AMPLIFICATION); // delta_y
531                 }
532         }
533
534         bool collides(double x, double y) const override
535         {
536                 return true;
537         }
538
539         core::draw_frame receive_impl() override
540         {
541                 if (client_)
542                 {
543                         if (client_->is_removed())
544                         {
545                                 client_ = nullptr;
546                                 return core::draw_frame::empty();
547                         }
548
549                         return client_->receive();
550                 }
551
552                 return core::draw_frame::empty();
553         }
554
555         std::future<std::wstring> call(const std::vector<std::wstring>& params) override
556         {
557                 if (!client_)
558                         return make_ready_future(std::wstring(L""));
559
560                 auto javascript = params.at(0);
561
562                 client_->execute_javascript(javascript);
563
564                 return make_ready_future(std::wstring(L""));
565         }
566
567         std::wstring print() const override
568         {
569                 return L"html[" + url_ + L"]";
570         }
571
572         boost::property_tree::wptree info() const override
573         {
574                 boost::property_tree::wptree info;
575                 info.add(L"type", L"html-producer");
576                 return info;
577         }
578
579         core::constraints& pixel_constraints() override
580         {
581                 return constraints_;
582         }
583
584         core::monitor::subject& monitor_output()
585         {
586                 return monitor_subject_;
587         }
588 };
589
590 void describe_producer(core::help_sink& sink, const core::help_repository& repo)
591 {
592         sink.short_description(L"Renders a web page in real time.");
593         sink.syntax(L"{[html_filename:string]},{[HTML] [url:string]}");
594         sink.para()->text(L"Embeds an actual web browser and renders the content in realtime.");
595         sink.para()
596                 ->text(L"HTML content can either be stored locally under the ")->code(L"templates")
597                 ->text(L" folder or fetched directly via an URL. If a .html file is found with the name ")
598                 ->code(L"html_filename")->text(L" under the ")->code(L"templates")->text(L" folder it will be rendered. If the ")
599                 ->code(L"[HTML] url")->text(L" syntax is used instead, the URL will be loaded.");
600         sink.para()->text(L"Examples:");
601         sink.example(L">> PLAY 1-10 [HTML] http://www.casparcg.com");
602         sink.example(L">> PLAY 1-10 folder/html_file");
603 }
604
605 spl::shared_ptr<core::frame_producer> create_producer(
606                 const core::frame_producer_dependencies& dependencies,
607                 const std::vector<std::wstring>& params)
608 {
609         const auto filename = env::template_folder() + params.at(0) + L".html";
610         const auto found_filename = find_case_insensitive(filename);
611
612         if (!found_filename && !boost::iequals(params.at(0), L"[HTML]"))
613                 return core::frame_producer::empty();
614
615         const auto url = found_filename 
616                 ? L"file://" + *found_filename
617                 : params.at(1);
618                 
619         if (!boost::algorithm::contains(url, ".") || boost::algorithm::ends_with(url, "_A") || boost::algorithm::ends_with(url, "_ALPHA"))
620                 return core::frame_producer::empty();
621
622         return core::create_destroy_proxy(spl::make_shared<html_producer>(
623                         dependencies.frame_factory,
624                         dependencies.format_desc,
625                         url));
626 }
627
628 }}