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