#include <core/producer/frame_producer.h>
#include <core/mixer/write_frame.h>
+#include <common/utility/assert.h>
#include <common/env.h>
#include <common/concurrency/executor.h>
#include <common/concurrency/lock.h>
+#include <common/concurrency/future_util.h>
#include <common/diagnostics/graph.h>
#include <common/utility/timer.h>
-
-#include <berkelium/Berkelium.hpp>
-#include <berkelium/Context.hpp>
-#include <berkelium/Window.hpp>
-#include <berkelium/WindowDelegate.hpp>
-#include <berkelium/Rect.hpp>
+#include <common/memory/memcpy.h>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/trim.hpp>
+#include <boost/algorithm/string/replace.hpp>
#include <boost/filesystem.hpp>
#include <boost/format.hpp>
+#include <boost/timer.hpp>
#include <tbb/atomic.h>
-#include <tbb/parallel_for.h>
+#include <tbb/concurrent_queue.h>
+
+#include <cef_task.h>
+#include <cef_app.h>
+#include <cef_client.h>
+#include <cef_render_handler.h>
+
+#include <queue>
#include "html.h"
-namespace caspar { namespace html {
+#pragma comment (lib, "libcef.lib")
+#pragma comment (lib, "libcef_dll_wrapper.lib")
+
+namespace caspar {
+ namespace html {
-class html_producer
- : public core::frame_producer
- , public Berkelium::WindowDelegate
-{
- core::monitor::subject monitor_subject_;
- const std::wstring url_;
- safe_ptr<diagnostics::graph> graph_;
-
- const safe_ptr<core::frame_factory> frame_factory_;
+ class html_client
+ : public CefClient
+ , public CefRenderHandler
+ , public CefLifeSpanHandler
+ , public CefLoadHandler
+ {
+ std::wstring url_;
+ safe_ptr<diagnostics::graph> graph_;
+ boost::timer tick_timer_;
+ boost::timer frame_timer_;
+ boost::timer paint_timer_;
+
+ safe_ptr<core::frame_factory> frame_factory_;
+ tbb::concurrent_queue<std::wstring> javascript_before_load_;
+ tbb::atomic<bool> loaded_;
+ tbb::atomic<bool> removed_;
+ tbb::atomic<bool> animation_frame_requested_;
+ std::queue<safe_ptr<core::basic_frame>> frames_;
+ mutable boost::mutex frames_mutex_;
+
+ safe_ptr<core::basic_frame> last_frame_;
+ safe_ptr<core::basic_frame> last_progressive_frame_;
+ mutable boost::mutex last_frame_mutex_;
+
+ CefRefPtr<CefBrowser> browser_;
+
+ executor executor_;
+
+ public:
+
+ html_client(safe_ptr<core::frame_factory> frame_factory, const std::wstring& url)
+ : url_(url)
+ , frame_factory_(frame_factory)
+ , last_frame_(core::basic_frame::empty())
+ , last_progressive_frame_(core::basic_frame::empty())
+ , executor_(L"html_producer")
+ {
+ graph_->set_color("browser-tick-time", diagnostics::color(0.1f, 1.0f, 0.1f));
+ graph_->set_color("tick-time", diagnostics::color(0.0f, 0.6f, 0.9f));
+ graph_->set_color("late-frame", diagnostics::color(0.6f, 0.3f, 0.9f));
+ graph_->set_text(print());
+ diagnostics::register_graph(graph_);
+
+ loaded_ = false;
+ removed_ = false;
+ animation_frame_requested_ = false;
+ executor_.begin_invoke([&]{ update(); });
+ }
- safe_ptr<core::basic_frame> last_frame_;
- mutable boost::mutex last_frame_mutex_;
-
- std::vector<unsigned char> frame_;
- mutable boost::mutex frame_mutex_;
+ safe_ptr<core::basic_frame> receive()
+ {
+ auto frame = last_frame();
+ executor_.begin_invoke([&]{ update(); });
+ return frame;
+ }
+
+ safe_ptr<core::basic_frame> last_frame() const
+ {
+ return lock(last_frame_mutex_, [&]
+ {
+ return last_frame_;
+ });
+ }
- tbb::atomic<bool> invalidated_;
+ void execute_javascript(const std::wstring& javascript)
+ {
+ if (!loaded_)
+ {
+ javascript_before_load_.push(javascript);
+ }
+ else
+ {
+ execute_queued_javascript();
+ do_execute_javascript(javascript);
+ }
+ }
- std::unique_ptr<Berkelium::Window> window_;
+ void close()
+ {
+ if (!animation_frame_requested_)
+ CASPAR_LOG(warning) << print()
+ << " window.requestAnimationFrame() never called. "
+ << "Animations might have been laggy";
- high_prec_timer timer_;
+ html::invoke([=]
+ {
+ if (browser_ != nullptr)
+ {
+ browser_->GetHost()->CloseBrowser(true);
+ }
+ });
+ }
- executor executor_;
-
-public:
- html_producer(
- const safe_ptr<core::frame_factory>& frame_factory,
- const std::wstring& url)
- : url_(url)
- , frame_factory_(frame_factory)
- , last_frame_(core::basic_frame::empty())
- , frame_(frame_factory->get_video_format_desc().width * frame_factory->get_video_format_desc().height * 4, 0)
- , executor_(L"html_producer")
- {
- invalidated_ = true;
-
- graph_->set_text(print());
- diagnostics::register_graph(graph_);
-
- html::invoke([&]
- {
+ void remove()
{
- std::unique_ptr<Berkelium::Context> context(Berkelium::Context::create());
- window_.reset(Berkelium::Window::create(context.get()));
- }
-
- window_->resize(
- frame_factory->get_video_format_desc().width,
- frame_factory->get_video_format_desc().height);
- window_->setDelegate(this);
- window_->setTransparent(true);
-
- const auto narrow_url = narrow(url);
-
- if(!window_->navigateTo(
- Berkelium::URLString::point_to(
- narrow_url.data(),
- narrow_url.length())))
+ close();
+ removed_ = true;
+ }
+
+ bool is_removed() const
{
- BOOST_THROW_EXCEPTION(caspar_exception() << msg_info("Failed to navigate."));
+ return removed_;
}
- });
- tick();
- }
+ private:
- ~html_producer()
- {
- html::invoke([=]
- {
- window_.reset();
- });
- }
-
- // frame_producer
-
- safe_ptr<core::basic_frame> receive(int) override
- {
- return last_frame();
- }
+ bool GetViewRect(CefRefPtr<CefBrowser> browser, CefRect &rect)
+ {
+ CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
- safe_ptr<core::basic_frame> last_frame() const override
- {
- return lock(last_frame_mutex_, [&]
- {
- return last_frame_;
- });
- }
-
- boost::unique_future<std::wstring> call(const std::wstring& param) override
- {
- static const boost::wregex play_exp(L"PLAY\\s*(\\d+)?", boost::regex::icase);
- static const boost::wregex stop_exp(L"STOP\\s*(\\d+)?", boost::regex::icase);
- static const boost::wregex next_exp(L"NEXT\\s*(\\d+)?", boost::regex::icase);
- static const boost::wregex remove_exp(L"REMOVE\\s*(\\d+)?", boost::regex::icase);
- static const boost::wregex update_exp(L"UPDATE\\s+(\\d+)?\"?(?<VALUE>.*)\"?", boost::regex::icase);
- static const boost::wregex invoke_exp(L"INVOKE\\s+(\\d+)?\"?(?<VALUE>.*)\"?", boost::regex::icase);
-
- auto command = [=]
- {
- auto javascript = param;
+ rect = CefRect(0, 0, frame_factory_->get_video_format_desc().square_width, frame_factory_->get_video_format_desc().square_height);
+ return true;
+ }
- boost::wsmatch what;
+ void OnPaint(
+ CefRefPtr<CefBrowser> browser,
+ PaintElementType type,
+ const RectList &dirtyRects,
+ const void *buffer,
+ int width,
+ int height)
+ {
+ graph_->set_value("browser-tick-time", paint_timer_.elapsed()
+ * frame_factory_->get_video_format_desc().fps
+ * frame_factory_->get_video_format_desc().field_count
+ * 0.5);
+ paint_timer_.restart();
+ CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
+
+ boost::timer copy_timer;
+ core::pixel_format_desc pixel_desc;
+ pixel_desc.pix_fmt = core::pixel_format::bgra;
+ pixel_desc.planes.push_back(
+ core::pixel_format_desc::plane(width, height, 4));
+ auto frame = frame_factory_->create_frame(this, pixel_desc);
+ fast_memcpy(frame->image_data().begin(), buffer, width * height * 4);
+ frame->commit();
+
+ lock(frames_mutex_, [&]
+ {
+ frames_.push(frame);
- if (boost::regex_match(param, what, play_exp))
+ size_t max_in_queue = frame_factory_->get_video_format_desc().field_count;
+
+ while (frames_.size() > max_in_queue)
+ {
+ frames_.pop();
+ graph_->set_tag("dropped-frame");
+ }
+ });
+ graph_->set_value("copy-time", copy_timer.elapsed()
+ * frame_factory_->get_video_format_desc().fps
+ * frame_factory_->get_video_format_desc().field_count
+ * 0.5);
+ }
+
+ void OnAfterCreated(CefRefPtr<CefBrowser> browser) override
{
- javascript = (boost::wformat(L"play()")).str();
+ CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
+
+ browser_ = browser;
}
- else if (boost::regex_match(param, what, stop_exp))
+
+ void OnBeforeClose(CefRefPtr<CefBrowser> browser) override
{
- javascript = (boost::wformat(L"stop()")).str();
+ CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
+
+ browser_ = nullptr;
}
- else if (boost::regex_match(param, what, next_exp))
+
+ bool DoClose(CefRefPtr<CefBrowser> browser) override
{
- javascript = (boost::wformat(L"next()")).str();
+ CASPAR_ASSERT(CefCurrentlyOn(TID_UI));
+
+ return false;
}
- else if (boost::regex_match(param, what, remove_exp))
+
+ CefRefPtr<CefRenderHandler> GetRenderHandler() override
{
- javascript = (boost::wformat(L"remove()")).str();
+ return this;
}
- else if (boost::regex_match(param, what, update_exp))
+
+ CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override
{
- javascript = (boost::wformat(L"update(\"%1%\")") % boost::algorithm::trim_copy_if(what["VALUE"].str(), boost::is_any_of(" \""))).str();
+ return this;
+ }
+
+ CefRefPtr<CefLoadHandler> GetLoadHandler() override {
+ return this;
}
- else if (boost::regex_match(param, what, invoke_exp))
+
+ void OnLoadEnd(
+ CefRefPtr<CefBrowser> browser,
+ CefRefPtr<CefFrame> frame,
+ int httpStatusCode) override
{
- javascript = (boost::wformat(L"invoke(\"%1%\")") % boost::algorithm::trim_copy_if(what["VALUE"].str(), boost::is_any_of(" \""))).str();
+ loaded_ = true;
+ execute_queued_javascript();
}
-
- window_->executeJavascript(Berkelium::WideString::point_to(javascript.data(), javascript.length()));
- };
-
- boost::packaged_task<std::wstring> task([=]() -> std::wstring
- {
- html::invoke(command);
- return L"";
- });
+ bool OnProcessMessageReceived(
+ CefRefPtr<CefBrowser> browser,
+ CefProcessId source_process,
+ CefRefPtr<CefProcessMessage> message) override
+ {
+ auto name = message->GetName().ToString();
- task();
+ if (name == ANIMATION_FRAME_REQUESTED_MESSAGE_NAME)
+ {
+ CASPAR_LOG(trace)
+ << print() << L" Requested animation frame";
+ animation_frame_requested_ = true;
- return task.get_future();
- }
-
- std::wstring print() const override
- {
- return L"html[" + url_ + L"]";
- }
-
- boost::property_tree::wptree info() const override
- {
- boost::property_tree::wptree info;
- info.add(L"type", L"html-producer");
- return info;
- }
-
- core::monitor::subject& monitor_output()
- {
- return monitor_subject_;
- }
-
- // Berkelium::WindowDelegate
-
- void onPaint(
- Berkelium::Window* wini,
- const unsigned char* bitmap_in,
- const Berkelium::Rect& bitmap_rect,
- size_t num_copy_rects,
- const Berkelium::Rect* copy_rects,
- int dx,
- int dy,
- const Berkelium::Rect& scroll_rect) override
- {
- lock(frame_mutex_, [&]
- {
- invalidated_ = true;
-
- tbb::parallel_for<int>(0, num_copy_rects, 1, [&](int i)
- {
- tbb::parallel_for<int>(0, copy_rects[i].height(), 1, [&](int y)
+ return true;
+ }
+ else if (name == REMOVE_MESSAGE_NAME)
+ {
+ remove();
+
+ return true;
+ }
+ else if (name == LOG_MESSAGE_NAME)
{
- memcpy(
- frame_.data() + ((y + copy_rects[i].top()) * frame_factory_->get_video_format_desc().width + copy_rects[i].left()) * 4,
- bitmap_in + ((y + copy_rects[i].top() - bitmap_rect.top()) * bitmap_rect.width() + copy_rects[i].left() - bitmap_rect.left()) * 4,
- copy_rects[i].width() * 4);
+ auto args = message->GetArgumentList();
+ auto severity =
+ static_cast<log::severity_level>(args->GetInt(0));
+ auto msg = args->GetString(1).ToWString();
+
+ BOOST_LOG_STREAM_WITH_PARAMS(
+ log::get_logger(),
+ (boost::log::keywords::severity = severity))
+ << print() << L" [renderer_process] " << msg;
+ }
+
+ return false;
+ }
+
+ void invoke_requested_animation_frames()
+ {
+ if (browser_)
+ browser_->SendProcessMessage(
+ CefProcessId::PID_RENDERER,
+ CefProcessMessage::Create(TICK_MESSAGE_NAME));
+ graph_->set_value("tick-time", tick_timer_.elapsed()
+ * frame_factory_->get_video_format_desc().fps
+ * frame_factory_->get_video_format_desc().field_count
+ * 0.5);
+ tick_timer_.restart();
+ }
+
+ bool try_pop(safe_ptr<core::basic_frame>& result)
+ {
+ return lock(frames_mutex_, [&]() -> bool
+ {
+ if (!frames_.empty())
+ {
+ result = frames_.front();
+ frames_.pop();
+
+ return true;
+ }
+
+ return false;
});
- });
- });
- }
+ }
- void onExternalHost(
- Berkelium::Window *win,
- Berkelium::WideString message,
- Berkelium::URLString origin,
- Berkelium::URLString target) override
- {
- }
+ safe_ptr<core::basic_frame> pop()
+ {
+ safe_ptr<core::basic_frame> frame;
- // html_producer
-
- safe_ptr<core::basic_frame> draw(
- safe_ptr<core::write_frame> frame,
- core::field_mode::type field_mode)
- {
- const auto& pixel_desc = frame->get_pixel_format_desc();
-
- CASPAR_ASSERT(pixel_desc.pix_fmt == core::pixel_format::bgra);
-
- const auto& height = pixel_desc.planes[0].height;
- const auto& linesize = pixel_desc.planes[0].linesize;
-
- lock(frame_mutex_, [&]
- {
- tbb::parallel_for<int>(
- field_mode == core::field_mode::upper ? 0 : 1,
- height,
- field_mode == core::field_mode::progressive ? 1 : 2,
- [&](int y)
+ if (!try_pop(frame))
+ {
+ BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + "No frame in buffer"));
+ }
+
+ return frame;
+ }
+
+ void update()
+ {
+ invoke_requested_animation_frames();
+
+ high_prec_timer timer;
+ timer.tick(0.0);
+ const auto& format_desc = frame_factory_->get_video_format_desc();
+
+ auto num_frames = lock(frames_mutex_, [&]
+ {
+ return frames_.size();
+ });
+
+ if (num_frames >= format_desc.field_count)
+ {
+ if (format_desc.field_mode != core::field_mode::progressive)
+ {
+ auto frame1 = pop();
+
+ executor_.yield();
+ timer.tick(1.0 / (format_desc.fps * format_desc.field_count));
+ invoke_requested_animation_frames();
+
+ auto frame2 = pop();
+
+ lock(last_frame_mutex_, [&]
+ {
+ last_progressive_frame_ = frame2;
+ last_frame_ = core::basic_frame::interlace(frame1, frame2, format_desc.field_mode);
+ });
+ }
+ else
+ {
+ auto frame = pop();
+
+ lock(last_frame_mutex_, [&]
+ {
+ last_frame_ = frame;
+ });
+ }
+ }
+ else if (num_frames == 1) // Interlaced but only one frame
+ { // available. Probably the last frame
+ // of some animation sequence.
+ auto frame = pop();
+
+ lock(last_frame_mutex_, [&]
+ {
+ last_progressive_frame_ = frame;
+ last_frame_ = frame;
+ });
+
+ timer.tick(1.0 / (format_desc.fps * format_desc.field_count));
+ invoke_requested_animation_frames();
+ }
+ else
+ {
+ graph_->set_tag("late-frame");
+
+ if (format_desc.field_mode != core::field_mode::progressive)
+ {
+ lock(last_frame_mutex_, [&]
+ {
+ last_frame_ = last_progressive_frame_;
+ });
+
+ timer.tick(1.0 / (format_desc.fps * format_desc.field_count));
+ invoke_requested_animation_frames();
+ }
+ }
+ }
+
+ void do_execute_javascript(const std::wstring& javascript)
{
- memcpy(
- frame->image_data().begin() + y * linesize,
- frame_.data() + y * linesize,
- linesize);
- });
- });
-
- return frame;
- }
-
- void tick()
- {
- if(invalidated_.fetch_and_store(false))
+ html::begin_invoke([=]
+ {
+ if (browser_ != nullptr)
+ browser_->GetMainFrame()->ExecuteJavaScript(narrow(javascript).c_str(), browser_->GetMainFrame()->GetURL(), 0);
+ });
+ }
+
+ void execute_queued_javascript()
+ {
+ std::wstring javascript;
+
+ while (javascript_before_load_.try_pop(javascript))
+ do_execute_javascript(javascript);
+ }
+
+ std::wstring print() const
+ {
+ return L"html[" + url_ + L"]";
+ }
+
+ IMPLEMENT_REFCOUNTING(html_client);
+ };
+
+ class html_producer
+ : public core::frame_producer
{
- core::pixel_format_desc pixel_desc;
- pixel_desc.pix_fmt = core::pixel_format::bgra;
- pixel_desc.planes.push_back(
- core::pixel_format_desc::plane(
- frame_factory_->get_video_format_desc().width,
- frame_factory_->get_video_format_desc().height,
- 4));
+ core::monitor::subject monitor_subject_;
+ const std::wstring url_;
- auto frame = frame_factory_->create_frame(this, pixel_desc);
+ CefRefPtr<html_client> client_;
- const auto& format_desc = frame_factory_->get_video_format_desc();
+ public:
+ html_producer(
+ const safe_ptr<core::frame_factory>& frame_factory,
+ const std::wstring& url)
+ : url_(url)
+ {
+ html::invoke([&]
+ {
+ client_ = new html_client(frame_factory, url_);
+
+ CefWindowInfo window_info;
- if (format_desc.field_mode != core::field_mode::progressive)
+ window_info.SetTransparentPainting(TRUE);
+ window_info.SetAsOffScreen(nullptr);
+ //window_info.SetAsWindowless(nullptr, true);
+
+ CefBrowserSettings browser_settings;
+ CefBrowserHost::CreateBrowser(window_info, client_.get(), url, browser_settings, nullptr);
+ });
+ }
+
+ ~html_producer()
{
- draw(frame, format_desc.field_mode);
-
- executor_.yield();
- timer_.tick(1.0 / (format_desc.fps * format_desc.field_count));
-
- draw(frame, static_cast<core::field_mode::type>(format_desc.field_mode ^ core::field_mode::progressive));
+ if (client_)
+ client_->close();
}
- else
- {
- draw(frame, format_desc.field_mode);
- }
-
- frame->commit();
- lock(last_frame_mutex_, [&]
- {
- last_frame_ = frame;
- });
-
- executor_.yield();
+ // frame_producer
- timer_.tick(1.0 / (format_desc.fps * format_desc.field_count));
- }
+ safe_ptr<core::basic_frame> receive(int) override
+ {
+ if (client_)
+ {
+ if (client_->is_removed())
+ {
+ client_ = nullptr;
+ return core::basic_frame::empty();
+ }
- executor_.begin_invoke([this]{ tick(); });
- }
-};
+ return client_->receive();
+ }
+
+ return core::basic_frame::empty();
+ }
+
+ safe_ptr<core::basic_frame> last_frame() const override
+ {
+ return client_
+ ? client_->last_frame()
+ : core::basic_frame::empty();
+ }
+
+ boost::unique_future<std::wstring> call(const std::wstring& param) override
+ {
+ static const boost::wregex play_exp(L"PLAY\\s*(\\d+)?", boost::regex::icase);
+ static const boost::wregex stop_exp(L"STOP\\s*(\\d+)?", boost::regex::icase);
+ static const boost::wregex next_exp(L"NEXT\\s*(\\d+)?", boost::regex::icase);
+ static const boost::wregex remove_exp(L"REMOVE\\s*(\\d+)?", boost::regex::icase);
+ static const boost::wregex update_exp(L"UPDATE\\s+(\\d+)?\"?(?<VALUE>.*)\"?", boost::regex::icase);
+ static const boost::wregex invoke_exp(L"INVOKE\\s+(\\d+)?\"?(?<VALUE>.*)\"?", boost::regex::icase);
+
+ if (!client_)
+ return wrap_as_future(std::wstring(L""));
+
+ auto javascript = param;
+
+ boost::wsmatch what;
+
+ if (boost::regex_match(param, what, play_exp))
+ {
+ javascript = (boost::wformat(L"play()")).str();
+ }
+ else if (boost::regex_match(param, what, stop_exp))
+ {
+ javascript = (boost::wformat(L"stop()")).str();
+ }
+ else if (boost::regex_match(param, what, next_exp))
+ {
+ javascript = (boost::wformat(L"next()")).str();
+ }
+ else if (boost::regex_match(param, what, remove_exp))
+ {
+ client_->remove();
+ return wrap_as_future(std::wstring(L""));
+ }
+ else if (boost::regex_match(param, what, update_exp))
+ {
+ javascript = (boost::wformat(L"update(\"%1%\")") % boost::algorithm::replace_all_copy(boost::algorithm::trim_copy_if(what["VALUE"].str(), boost::is_any_of(" \"")), "\"", "\\\"")).str();
+ }
+ else if (boost::regex_match(param, what, invoke_exp))
+ {
+ auto function_call = boost::algorithm::trim_copy_if(what["VALUE"].str(), boost::is_any_of(" \""));
-safe_ptr<core::frame_producer> create_producer(
- const safe_ptr<core::frame_factory>& frame_factory,
- const core::parameters& params)
-{
- const auto filename = env::template_folder() + L"\\" + params.at_original(0) + L".html";
+ // Append empty () if no parameter list has been given
+ javascript = boost::ends_with(function_call, ")") ? function_call : function_call + L"()";
+ }
+
+ client_->execute_javascript(javascript);
+
+ return wrap_as_future(std::wstring(L""));
+ }
+
+ std::wstring print() const override
+ {
+ return L"html[" + url_ + L"]";
+ }
+
+ boost::property_tree::wptree info() const override
+ {
+ boost::property_tree::wptree info;
+ info.add(L"type", L"html-producer");
+ return info;
+ }
+
+ core::monitor::subject& monitor_output()
+ {
+ return monitor_subject_;
+ }
+ };
+
+ safe_ptr<core::frame_producer> create_producer(
+ const safe_ptr<core::frame_factory>& frame_factory,
+ const core::parameters& params)
+ {
+ const auto filename = env::template_folder() + L"\\" + params.at_original(0) + L".html";
- const auto url = boost::filesystem::exists(filename)
- ? filename
- : params.at_original(0);
-
- if(!boost::algorithm::contains(url, ".") || boost::algorithm::ends_with(url, "_A") || boost::algorithm::ends_with(url, "_ALPHA"))
- return core::frame_producer::empty();
+ if (!boost::filesystem::exists(filename) && params.at(0) != L"[HTML]")
+ return core::frame_producer::empty();
- return core::create_producer_destroy_proxy(
- core::create_producer_print_proxy(
- make_safe<html_producer>(
- frame_factory,
- url)));
-}
+ const auto url = boost::filesystem::exists(filename)
+ ? filename
+ : params.at_original(1);
+
+ if(!boost::algorithm::contains(url, ".") || boost::algorithm::ends_with(url, "_A") || boost::algorithm::ends_with(url, "_ALPHA"))
+ return core::frame_producer::empty();
+
+ return core::create_producer_destroy_proxy(
+ core::create_producer_print_proxy(
+ make_safe<html_producer>(
+ frame_factory,
+ url)));
+ }
-}}
\ No newline at end of file
+ }
+}
\ No newline at end of file