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