-/*\r
-* Copyright (c) 2011 Sveriges Television AB <info@casparcg.com>\r
-*\r
-* This file is part of CasparCG (www.casparcg.com).\r
-*\r
-* CasparCG is free software: you can redistribute it and/or modify\r
-* it under the terms of the GNU General Public License as published by\r
-* the Free Software Foundation, either version 3 of the License, or\r
-* (at your option) any later version.\r
-*\r
-* CasparCG is distributed in the hope that it will be useful,\r
-* but WITHOUT ANY WARRANTY; without even the implied warranty of\r
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
-* GNU General Public License for more details.\r
-*\r
-* You should have received a copy of the GNU General Public License\r
-* along with CasparCG. If not, see <http://www.gnu.org/licenses/>.\r
-*\r
-* Author: Robert Nagy, ronag89@gmail.com\r
-*/\r
-\r
-#include "../StdAfx.h"\r
- \r
-#include "decklink_consumer.h"\r
-\r
-#include "../util/util.h"\r
-\r
-#include "../interop/DeckLinkAPI_h.h"\r
-\r
-#include <core/frame/frame.h>\r
-#include <core/mixer/audio/audio_mixer.h>\r
-\r
-#include <common/executor.h>\r
-#include <common/lock.h>\r
-#include <common/diagnostics/graph.h>\r
-#include <common/except.h>\r
-#include <common/memshfl.h>\r
-#include <common/array.h>\r
-\r
-#include <core/consumer/frame_consumer.h>\r
-\r
-#include <tbb/concurrent_queue.h>\r
-#include <tbb/cache_aligned_allocator.h>\r
-\r
-#include <common/assert.h>\r
-#include <boost/lexical_cast.hpp>\r
-#include <boost/circular_buffer.hpp>\r
-#include <boost/timer.hpp>\r
-#include <boost/property_tree/ptree.hpp>\r
-\r
-namespace caspar { namespace decklink { \r
- \r
-struct configuration\r
-{\r
- enum keyer_t\r
- {\r
- internal_keyer,\r
- external_keyer,\r
- default_keyer\r
- };\r
-\r
- enum latency_t\r
- {\r
- low_latency,\r
- normal_latency,\r
- default_latency\r
- };\r
-\r
- int device_index;\r
- bool embedded_audio;\r
- keyer_t keyer;\r
- latency_t latency;\r
- bool key_only;\r
- int base_buffer_depth;\r
- \r
- configuration()\r
- : device_index(1)\r
- , embedded_audio(true)\r
- , keyer(default_keyer)\r
- , latency(default_latency)\r
- , key_only(false)\r
- , base_buffer_depth(3)\r
- {\r
- }\r
- \r
- int buffer_depth() const\r
- {\r
- return base_buffer_depth + (latency == low_latency ? 0 : 1) + (embedded_audio ? 1 : 0);\r
- }\r
-};\r
-\r
-class decklink_frame : public IDeckLinkVideoFrame\r
-{\r
- tbb::atomic<int> ref_count_;\r
- core::const_frame frame_;\r
- const core::video_format_desc format_desc_;\r
-\r
- const bool key_only_;\r
- std::vector<uint8_t, tbb::cache_aligned_allocator<uint8_t>> data_;\r
-public:\r
- decklink_frame(core::const_frame frame, const core::video_format_desc& format_desc, bool key_only)\r
- : frame_(frame)\r
- , format_desc_(format_desc)\r
- , key_only_(key_only)\r
- {\r
- ref_count_ = 0;\r
- }\r
- \r
- // IUnknown\r
-\r
- STDMETHOD (QueryInterface(REFIID, LPVOID*)) \r
- {\r
- return E_NOINTERFACE;\r
- }\r
- \r
- STDMETHOD_(ULONG, AddRef()) \r
- {\r
- return ++ref_count_;\r
- }\r
-\r
- STDMETHOD_(ULONG, Release()) \r
- {\r
- if(--ref_count_ == 0)\r
- delete this;\r
- return ref_count_;\r
- }\r
-\r
- // IDecklinkVideoFrame\r
-\r
- STDMETHOD_(long, GetWidth()) {return static_cast<long>(format_desc_.width);} \r
- STDMETHOD_(long, GetHeight()) {return static_cast<long>(format_desc_.height);} \r
- STDMETHOD_(long, GetRowBytes()) {return static_cast<long>(format_desc_.width*4);} \r
- STDMETHOD_(BMDPixelFormat, GetPixelFormat()) {return bmdFormat8BitBGRA;} \r
- STDMETHOD_(BMDFrameFlags, GetFlags()) {return bmdFrameFlagDefault;}\r
- \r
- STDMETHOD(GetBytes(void** buffer))\r
- {\r
- try\r
- {\r
- if(static_cast<int>(frame_.image_data().size()) != format_desc_.size)\r
- {\r
- data_.resize(format_desc_.size, 0);\r
- *buffer = data_.data();\r
- }\r
- else if(key_only_)\r
- {\r
- if(data_.empty())\r
- {\r
- data_.resize(frame_.image_data().size());\r
- aligned_memshfl(data_.data(), frame_.image_data().begin(), frame_.image_data().size(), 0x0F0F0F0F, 0x0B0B0B0B, 0x07070707, 0x03030303);\r
- }\r
- *buffer = data_.data();\r
- }\r
- else\r
- *buffer = const_cast<uint8_t*>(frame_.image_data().begin());\r
- }\r
- catch(...)\r
- {\r
- CASPAR_LOG_CURRENT_EXCEPTION();\r
- return E_FAIL;\r
- }\r
-\r
- return S_OK;\r
- }\r
- \r
- STDMETHOD(GetTimecode(BMDTimecodeFormat format, IDeckLinkTimecode** timecode)){return S_FALSE;} \r
- STDMETHOD(GetAncillaryData(IDeckLinkVideoFrameAncillary** ancillary)) {return S_FALSE;}\r
-\r
- // decklink_frame \r
-\r
- const core::audio_buffer& audio_data()\r
- {\r
- return frame_.audio_data();\r
- }\r
-};\r
-\r
-struct decklink_consumer : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback, boost::noncopyable\r
-{ \r
- const int channel_index_;\r
- const configuration config_;\r
-\r
- CComPtr<IDeckLink> decklink_;\r
- CComQIPtr<IDeckLinkOutput> output_;\r
- CComQIPtr<IDeckLinkConfiguration> configuration_;\r
- CComQIPtr<IDeckLinkKeyer> keyer_;\r
- CComQIPtr<IDeckLinkAttributes> attributes_;\r
-\r
- tbb::spin_mutex exception_mutex_;\r
- std::exception_ptr exception_;\r
-\r
- tbb::atomic<bool> is_running_;\r
- \r
- const std::wstring model_name_;\r
- const core::video_format_desc format_desc_;\r
- const int buffer_size_;\r
-\r
- long long video_scheduled_;\r
- long long audio_scheduled_;\r
-\r
- int preroll_count_;\r
- \r
- boost::circular_buffer<std::vector<int32_t>> audio_container_;\r
-\r
- tbb::concurrent_bounded_queue<core::const_frame> video_frame_buffer_;\r
- tbb::concurrent_bounded_queue<core::const_frame> audio_frame_buffer_;\r
- \r
- spl::shared_ptr<diagnostics::graph> graph_;\r
- boost::timer tick_timer_;\r
-\r
-public:\r
- decklink_consumer(const configuration& config, const core::video_format_desc& format_desc, int channel_index) \r
- : channel_index_(channel_index)\r
- , config_(config)\r
- , decklink_(get_device(config.device_index))\r
- , output_(decklink_)\r
- , configuration_(decklink_)\r
- , keyer_(decklink_)\r
- , attributes_(decklink_)\r
- , model_name_(get_model_name(decklink_))\r
- , format_desc_(format_desc)\r
- , buffer_size_(config.buffer_depth()) // Minimum buffer-size 3.\r
- , video_scheduled_(0)\r
- , audio_scheduled_(0)\r
- , preroll_count_(0)\r
- , audio_container_(buffer_size_+1)\r
- {\r
- is_running_ = true;\r
- \r
- video_frame_buffer_.set_capacity(1);\r
- audio_frame_buffer_.set_capacity(1);\r
-\r
- graph_->set_color("tick-time", diagnostics::color(0.0f, 0.6f, 0.9f)); \r
- graph_->set_color("late-frame", diagnostics::color(0.6f, 0.3f, 0.3f));\r
- graph_->set_color("dropped-frame", diagnostics::color(0.3f, 0.6f, 0.3f));\r
- graph_->set_color("flushed-frame", diagnostics::color(0.4f, 0.3f, 0.8f));\r
- graph_->set_color("buffered-audio", diagnostics::color(0.9f, 0.9f, 0.5f));\r
- graph_->set_color("buffered-video", diagnostics::color(0.2f, 0.9f, 0.9f));\r
- graph_->set_text(print());\r
- diagnostics::register_graph(graph_);\r
- \r
- enable_video(get_display_mode(output_, format_desc_.format, bmdFormat8BitBGRA, bmdVideoOutputFlagDefault));\r
- \r
- if(config.embedded_audio)\r
- enable_audio();\r
- \r
- set_latency(config.latency); \r
- set_keyer(config.keyer);\r
- \r
- if(config.embedded_audio) \r
- output_->BeginAudioPreroll(); \r
- \r
- for(int n = 0; n < buffer_size_; ++n)\r
- schedule_next_video(core::const_frame::empty());\r
-\r
- if(!config.embedded_audio)\r
- start_playback();\r
- }\r
-\r
- ~decklink_consumer()\r
- { \r
- is_running_ = false;\r
- video_frame_buffer_.try_push(core::const_frame::empty());\r
- audio_frame_buffer_.try_push(core::const_frame::empty());\r
-\r
- if(output_ != nullptr) \r
- {\r
- output_->StopScheduledPlayback(0, nullptr, 0);\r
- if(config_.embedded_audio)\r
- output_->DisableAudioOutput();\r
- output_->DisableVideoOutput();\r
- }\r
- }\r
- \r
- void set_latency(configuration::latency_t latency)\r
- { \r
- if(latency == configuration::low_latency)\r
- {\r
- configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, true);\r
- CASPAR_LOG(info) << print() << L" Enabled low-latency mode.";\r
- }\r
- else if(latency == configuration::normal_latency)\r
- { \r
- configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, false);\r
- CASPAR_LOG(info) << print() << L" Disabled low-latency mode.";\r
- }\r
- }\r
-\r
- void set_keyer(configuration::keyer_t keyer)\r
- {\r
- if(keyer == configuration::internal_keyer) \r
- {\r
- BOOL value = true;\r
- if(SUCCEEDED(attributes_->GetFlag(BMDDeckLinkSupportsInternalKeying, &value)) && !value)\r
- CASPAR_LOG(error) << print() << L" Failed to enable internal keyer."; \r
- else if(FAILED(keyer_->Enable(FALSE))) \r
- CASPAR_LOG(error) << print() << L" Failed to enable internal keyer."; \r
- else if(FAILED(keyer_->SetLevel(255))) \r
- CASPAR_LOG(error) << print() << L" Failed to set key-level to max.";\r
- else\r
- CASPAR_LOG(info) << print() << L" Enabled internal keyer."; \r
- }\r
- else if(keyer == configuration::external_keyer)\r
- {\r
- BOOL value = true;\r
- if(SUCCEEDED(attributes_->GetFlag(BMDDeckLinkSupportsExternalKeying, &value)) && !value)\r
- CASPAR_LOG(error) << print() << L" Failed to enable external keyer."; \r
- else if(FAILED(keyer_->Enable(TRUE))) \r
- CASPAR_LOG(error) << print() << L" Failed to enable external keyer."; \r
- else if(FAILED(keyer_->SetLevel(255))) \r
- CASPAR_LOG(error) << print() << L" Failed to set key-level to max.";\r
- else\r
- CASPAR_LOG(info) << print() << L" Enabled external keyer."; \r
- }\r
- }\r
- \r
- void enable_audio()\r
- {\r
- if(FAILED(output_->EnableAudioOutput(bmdAudioSampleRate48kHz, bmdAudioSampleType32bitInteger, 2, bmdAudioOutputStreamTimestamped)))\r
- CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Could not enable audio output."));\r
- \r
- if(FAILED(output_->SetAudioCallback(this)))\r
- CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Could not set audio callback."));\r
-\r
- CASPAR_LOG(info) << print() << L" Enabled embedded-audio.";\r
- }\r
-\r
- void enable_video(BMDDisplayMode display_mode)\r
- {\r
- if(FAILED(output_->EnableVideoOutput(display_mode, bmdVideoOutputFlagDefault))) \r
- CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Could not enable video output."));\r
- \r
- if(FAILED(output_->SetScheduledFrameCompletionCallback(this)))\r
- CASPAR_THROW_EXCEPTION(caspar_exception() \r
- << msg_info(u8(print()) + " Failed to set playback completion callback.")\r
- << boost::errinfo_api_function("SetScheduledFrameCompletionCallback"));\r
- }\r
-\r
- void start_playback()\r
- {\r
- if(FAILED(output_->StartScheduledPlayback(0, format_desc_.time_scale, 1.0))) \r
- CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Failed to schedule playback."));\r
- }\r
- \r
- STDMETHOD (QueryInterface(REFIID, LPVOID*)) {return E_NOINTERFACE;}\r
- STDMETHOD_(ULONG, AddRef()) {return 1;}\r
- STDMETHOD_(ULONG, Release()) {return 1;}\r
- \r
- STDMETHOD(ScheduledPlaybackHasStopped())\r
- {\r
- is_running_ = false;\r
- CASPAR_LOG(info) << print() << L" Scheduled playback has stopped.";\r
- return S_OK;\r
- }\r
-\r
- STDMETHOD(ScheduledFrameCompleted(IDeckLinkVideoFrame* completed_frame, BMDOutputFrameCompletionResult result))\r
- {\r
- if(!is_running_)\r
- return E_FAIL;\r
- \r
- try\r
- {\r
- if(result == bmdOutputFrameDisplayedLate)\r
- {\r
- graph_->set_tag("late-frame");\r
- video_scheduled_ += format_desc_.duration;\r
- audio_scheduled_ += reinterpret_cast<decklink_frame*>(completed_frame)->audio_data().size()/format_desc_.audio_channels;\r
- //++video_scheduled_;\r
- //audio_scheduled_ += format_desc_.audio_cadence[0];\r
- //++audio_scheduled_;\r
- }\r
- else if(result == bmdOutputFrameDropped)\r
- graph_->set_tag("dropped-frame");\r
- else if(result == bmdOutputFrameFlushed)\r
- graph_->set_tag("flushed-frame");\r
-\r
- auto frame = core::const_frame::empty(); \r
- video_frame_buffer_.pop(frame); \r
- schedule_next_video(frame); \r
- \r
- unsigned long buffered;\r
- output_->GetBufferedVideoFrameCount(&buffered);\r
- graph_->set_value("buffered-video", static_cast<double>(buffered)/format_desc_.fps);\r
- }\r
- catch(...)\r
- {\r
- lock(exception_mutex_, [&]\r
- {\r
- exception_ = std::current_exception();\r
- });\r
- return E_FAIL;\r
- }\r
-\r
- return S_OK;\r
- }\r
- \r
- STDMETHOD(RenderAudioSamples(BOOL preroll))\r
- {\r
- if(!is_running_)\r
- return E_FAIL;\r
- \r
- try\r
- { \r
- if(preroll)\r
- {\r
- if(++preroll_count_ >= buffer_size_)\r
- {\r
- output_->EndAudioPreroll();\r
- start_playback(); \r
- }\r
- else\r
- schedule_next_audio(core::audio_buffer(format_desc_.audio_cadence[preroll % format_desc_.audio_cadence.size()], 0)); \r
- }\r
- else\r
- {\r
- auto frame = core::const_frame::empty();\r
- audio_frame_buffer_.pop(frame);\r
- schedule_next_audio(frame.audio_data());\r
- }\r
-\r
- unsigned long buffered;\r
- output_->GetBufferedAudioSampleFrameCount(&buffered);\r
- graph_->set_value("buffered-audio", static_cast<double>(buffered)/(format_desc_.audio_cadence[0]*2));\r
- }\r
- catch(...)\r
- {\r
- tbb::spin_mutex::scoped_lock lock(exception_mutex_);\r
- exception_ = std::current_exception();\r
- return E_FAIL;\r
- }\r
-\r
- return S_OK;\r
- }\r
-\r
- template<typename T>\r
- void schedule_next_audio(const T& audio_data)\r
- {\r
- auto sample_frame_count = static_cast<int>(audio_data.size()/format_desc_.audio_channels);\r
-\r
- audio_container_.push_back(std::vector<int32_t>(audio_data.begin(), audio_data.end()));\r
-\r
- if(FAILED(output_->ScheduleAudioSamples(audio_container_.back().data(), sample_frame_count, audio_scheduled_, format_desc_.audio_sample_rate, nullptr)))\r
- CASPAR_LOG(error) << print() << L" Failed to schedule audio.";\r
-\r
- audio_scheduled_ += sample_frame_count;\r
- }\r
- \r
- void schedule_next_video(core::const_frame frame)\r
- {\r
- CComPtr<IDeckLinkVideoFrame> frame2(new decklink_frame(frame, format_desc_, config_.key_only));\r
- if(FAILED(output_->ScheduleVideoFrame(frame2, video_scheduled_, format_desc_.duration, format_desc_.time_scale)))\r
- CASPAR_LOG(error) << print() << L" Failed to schedule video.";\r
-\r
- video_scheduled_ += format_desc_.duration;\r
-\r
- graph_->set_value("tick-time", tick_timer_.elapsed()*format_desc_.fps*0.5);\r
- tick_timer_.restart();\r
- }\r
-\r
- void send(core::const_frame frame)\r
- {\r
- auto exception = lock(exception_mutex_, [&]\r
- {\r
- return exception_;\r
- });\r
-\r
- if(exception != nullptr)\r
- std::rethrow_exception(exception); \r
-\r
- if(!is_running_)\r
- CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Is not running."));\r
- \r
- if(config_.embedded_audio)\r
- audio_frame_buffer_.push(frame); \r
- video_frame_buffer_.push(frame); \r
- }\r
- \r
- std::wstring print() const\r
- {\r
- return model_name_ + L" [" + boost::lexical_cast<std::wstring>(channel_index_) + L"-" +\r
- boost::lexical_cast<std::wstring>(config_.device_index) + L"|" + format_desc_.name + L"]";\r
- }\r
-};\r
-\r
-struct decklink_consumer_proxy : public core::frame_consumer\r
-{\r
- const configuration config_;\r
- std::unique_ptr<decklink_consumer> consumer_;\r
- executor executor_;\r
-public:\r
-\r
- decklink_consumer_proxy(const configuration& config)\r
- : config_(config)\r
- , executor_(L"decklink_consumer[" + boost::lexical_cast<std::wstring>(config.device_index) + L"]")\r
- {\r
- executor_.begin_invoke([=]\r
- {\r
- ::CoInitialize(nullptr);\r
- });\r
- }\r
-\r
- ~decklink_consumer_proxy()\r
- {\r
- executor_.invoke([=]\r
- {\r
- ::CoUninitialize();\r
- });\r
- }\r
-\r
- // frame_consumer\r
- \r
- void initialize(const core::video_format_desc& format_desc, int channel_index) override\r
- {\r
- executor_.invoke([=]\r
- {\r
- consumer_.reset();\r
- consumer_.reset(new decklink_consumer(config_, format_desc, channel_index)); \r
- });\r
- }\r
- \r
- bool send(core::const_frame frame) override\r
- {\r
- consumer_->send(frame);\r
- return true;\r
- }\r
- \r
- std::wstring print() const override\r
- {\r
- return consumer_ ? consumer_->print() : L"[decklink_consumer]";\r
- } \r
-\r
- std::wstring name() const override\r
- {\r
- return L"decklink";\r
- }\r
-\r
- boost::property_tree::wptree info() const override\r
- {\r
- boost::property_tree::wptree info;\r
- info.add(L"type", L"decklink");\r
- info.add(L"key-only", config_.key_only);\r
- info.add(L"device", config_.device_index);\r
- info.add(L"low-latency", config_.low_latency);\r
- info.add(L"embedded-audio", config_.embedded_audio);\r
- info.add(L"low-latency", config_.low_latency);\r
- //info.add(L"internal-key", config_.internal_key);\r
- return info;\r
- }\r
-\r
- int buffer_depth() const override\r
- {\r
- return config_.buffer_depth();\r
- }\r
-\r
- int index() const override\r
- {\r
- return 300 + config_.device_index;\r
- }\r
-\r
- void subscribe(const monitor::observable::observer_ptr& o) override\r
- {\r
- }\r
-\r
- void unsubscribe(const monitor::observable::observer_ptr& o) override\r
- {\r
- } \r
-}; \r
-\r
-spl::shared_ptr<core::frame_consumer> create_consumer(const std::vector<std::wstring>& params) \r
-{\r
- if(params.size() < 1 || params[0] != L"DECKLINK")\r
- return core::frame_consumer::empty();\r
- \r
- configuration config;\r
- \r
- if(params.size() > 1)\r
- config.device_index = boost::lexical_cast<int>(params[1]);\r
- \r
- if(std::find(params.begin(), params.end(), L"INTERNAL_KEY") != params.end())\r
- config.keyer = configuration::internal_keyer;\r
- else if(std::find(params.begin(), params.end(), L"EXTERNAL_KEY") != params.end())\r
- config.keyer = configuration::external_keyer;\r
- else\r
- config.keyer = configuration::default_keyer;\r
-\r
- if(std::find(params.begin(), params.end(), L"LOW_LATENCY") != params.end())\r
- config.latency = configuration::low_latency;\r
-\r
- config.embedded_audio = std::find(params.begin(), params.end(), L"EMBEDDED_AUDIO") != params.end();\r
- config.key_only = std::find(params.begin(), params.end(), L"KEY_ONLY") != params.end();\r
-\r
- return spl::make_shared<decklink_consumer_proxy>(config);\r
-}\r
-\r
-spl::shared_ptr<core::frame_consumer> create_consumer(const boost::property_tree::wptree& ptree) \r
-{\r
- configuration config;\r
-\r
- auto keyer = ptree.get(L"keyer", L"default");\r
- if(keyer == L"external")\r
- config.keyer = configuration::external_keyer;\r
- else if(keyer == L"internal")\r
- config.keyer = configuration::internal_keyer;\r
-\r
- auto latency = ptree.get(L"latency", L"normal");\r
- if(latency == L"low")\r
- config.latency = configuration::low_latency;\r
- else if(latency == L"normal")\r
- config.latency = configuration::normal_latency;\r
-\r
- config.key_only = ptree.get(L"key-only", config.key_only);\r
- config.device_index = ptree.get(L"device", config.device_index);\r
- config.embedded_audio = ptree.get(L"embedded-audio", config.embedded_audio);\r
- config.base_buffer_depth = ptree.get(L"buffer-depth", config.base_buffer_depth);\r
-\r
- return spl::make_shared<decklink_consumer_proxy>(config);\r
-}\r
-\r
-}}\r
-\r
-/*\r
-##############################################################################\r
-Pre-rolling\r
-\r
-Mail: 2011-05-09\r
-\r
-Yoshan\r
-BMD Developer Support\r
-developer@blackmagic-design.com\r
-\r
------------------------------------------------------------------------------\r
-\r
-Thanks for your inquiry. The minimum number of frames that you can preroll \r
-for scheduled playback is three frames for video and four frames for audio. \r
-As you mentioned if you preroll less frames then playback will not start or\r
-playback will be very sporadic. From our experience with Media Express, we \r
-recommended that at least seven frames are prerolled for smooth playback. \r
-\r
-Regarding the bmdDeckLinkConfigLowLatencyVideoOutput flag:\r
-There can be around 3 frames worth of latency on scheduled output.\r
-When the bmdDeckLinkConfigLowLatencyVideoOutput flag is used this latency is\r
-reduced or removed for scheduled playback. If the DisplayVideoFrameSync() \r
-method is used, the bmdDeckLinkConfigLowLatencyVideoOutput setting will \r
-guarantee that the provided frame will be output as soon the previous \r
-frame output has been completed.\r
-################################################################################\r
-*/\r
-\r
-/*\r
-##############################################################################\r
-Async DMA Transfer without redundant copying\r
-\r
-Mail: 2011-05-10\r
-\r
-Yoshan\r
-BMD Developer Support\r
-developer@blackmagic-design.com\r
-\r
------------------------------------------------------------------------------\r
-\r
-Thanks for your inquiry. You could try subclassing IDeckLinkMutableVideoFrame \r
-and providing a pointer to your video buffer when GetBytes() is called. \r
-This may help to keep copying to a minimum. Please ensure that the pixel \r
-format is in bmdFormat10BitYUV, otherwise the DeckLink API / driver will \r
-have to colourspace convert which may result in additional copying.\r
-################################################################################\r
+/*
+* Copyright (c) 2011 Sveriges Television AB <info@casparcg.com>
+*
+* This file is part of CasparCG (www.casparcg.com).
+*
+* CasparCG is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* CasparCG is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with CasparCG. If not, see <http://www.gnu.org/licenses/>.
+*
+* Author: Robert Nagy, ronag89@gmail.com
+*/
+
+#include "../StdAfx.h"
+
+#include "decklink_consumer.h"
+
+#include "../util/util.h"
+
+#include "../interop/DeckLinkAPI_h.h"
+
+#include <core/frame/frame.h>
+#include <core/mixer/audio/audio_mixer.h>
+
+#include <common/executor.h>
+#include <common/lock.h>
+#include <common/diagnostics/graph.h>
+#include <common/except.h>
+#include <common/memshfl.h>
+#include <common/array.h>
+
+#include <core/consumer/frame_consumer.h>
+
+#include <tbb/concurrent_queue.h>
+#include <tbb/cache_aligned_allocator.h>
+
+#include <common/assert.h>
+#include <boost/lexical_cast.hpp>
+#include <boost/circular_buffer.hpp>
+#include <boost/timer.hpp>
+#include <boost/property_tree/ptree.hpp>
+
+namespace caspar { namespace decklink {
+
+struct configuration
+{
+ enum keyer_t
+ {
+ internal_keyer,
+ external_keyer,
+ default_keyer
+ };
+
+ enum latency_t
+ {
+ low_latency,
+ normal_latency,
+ default_latency
+ };
+
+ int device_index;
+ bool embedded_audio;
+ keyer_t keyer;
+ latency_t latency;
+ bool key_only;
+ int base_buffer_depth;
+
+ configuration()
+ : device_index(1)
+ , embedded_audio(true)
+ , keyer(default_keyer)
+ , latency(default_latency)
+ , key_only(false)
+ , base_buffer_depth(3)
+ {
+ }
+
+ int buffer_depth() const
+ {
+ return base_buffer_depth + (latency == low_latency ? 0 : 1) + (embedded_audio ? 1 : 0);
+ }
+};
+
+class decklink_frame : public IDeckLinkVideoFrame
+{
+ tbb::atomic<int> ref_count_;
+ core::const_frame frame_;
+ const core::video_format_desc format_desc_;
+
+ const bool key_only_;
+ std::vector<uint8_t, tbb::cache_aligned_allocator<uint8_t>> data_;
+public:
+ decklink_frame(core::const_frame frame, const core::video_format_desc& format_desc, bool key_only)
+ : frame_(frame)
+ , format_desc_(format_desc)
+ , key_only_(key_only)
+ {
+ ref_count_ = 0;
+ }
+
+ // IUnknown
+
+ STDMETHOD (QueryInterface(REFIID, LPVOID*))
+ {
+ return E_NOINTERFACE;
+ }
+
+ STDMETHOD_(ULONG, AddRef())
+ {
+ return ++ref_count_;
+ }
+
+ STDMETHOD_(ULONG, Release())
+ {
+ if(--ref_count_ == 0)
+ delete this;
+ return ref_count_;
+ }
+
+ // IDecklinkVideoFrame
+
+ STDMETHOD_(long, GetWidth()) {return static_cast<long>(format_desc_.width);}
+ STDMETHOD_(long, GetHeight()) {return static_cast<long>(format_desc_.height);}
+ STDMETHOD_(long, GetRowBytes()) {return static_cast<long>(format_desc_.width*4);}
+ STDMETHOD_(BMDPixelFormat, GetPixelFormat()) {return bmdFormat8BitBGRA;}
+ STDMETHOD_(BMDFrameFlags, GetFlags()) {return bmdFrameFlagDefault;}
+
+ STDMETHOD(GetBytes(void** buffer))
+ {
+ try
+ {
+ if(static_cast<int>(frame_.image_data().size()) != format_desc_.size)
+ {
+ data_.resize(format_desc_.size, 0);
+ *buffer = data_.data();
+ }
+ else if(key_only_)
+ {
+ if(data_.empty())
+ {
+ data_.resize(frame_.image_data().size());
+ aligned_memshfl(data_.data(), frame_.image_data().begin(), frame_.image_data().size(), 0x0F0F0F0F, 0x0B0B0B0B, 0x07070707, 0x03030303);
+ }
+ *buffer = data_.data();
+ }
+ else
+ *buffer = const_cast<uint8_t*>(frame_.image_data().begin());
+ }
+ catch(...)
+ {
+ CASPAR_LOG_CURRENT_EXCEPTION();
+ return E_FAIL;
+ }
+
+ return S_OK;
+ }
+
+ STDMETHOD(GetTimecode(BMDTimecodeFormat format, IDeckLinkTimecode** timecode)){return S_FALSE;}
+ STDMETHOD(GetAncillaryData(IDeckLinkVideoFrameAncillary** ancillary)) {return S_FALSE;}
+
+ // decklink_frame
+
+ const core::audio_buffer& audio_data()
+ {
+ return frame_.audio_data();
+ }
+};
+
+struct decklink_consumer : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback, boost::noncopyable
+{
+ const int channel_index_;
+ const configuration config_;
+
+ CComPtr<IDeckLink> decklink_;
+ CComQIPtr<IDeckLinkOutput> output_;
+ CComQIPtr<IDeckLinkConfiguration> configuration_;
+ CComQIPtr<IDeckLinkKeyer> keyer_;
+ CComQIPtr<IDeckLinkAttributes> attributes_;
+
+ tbb::spin_mutex exception_mutex_;
+ std::exception_ptr exception_;
+
+ tbb::atomic<bool> is_running_;
+
+ const std::wstring model_name_;
+ const core::video_format_desc format_desc_;
+ const int buffer_size_;
+
+ long long video_scheduled_;
+ long long audio_scheduled_;
+
+ int preroll_count_;
+
+ boost::circular_buffer<std::vector<int32_t>> audio_container_;
+
+ tbb::concurrent_bounded_queue<core::const_frame> video_frame_buffer_;
+ tbb::concurrent_bounded_queue<core::const_frame> audio_frame_buffer_;
+
+ spl::shared_ptr<diagnostics::graph> graph_;
+ boost::timer tick_timer_;
+
+public:
+ decklink_consumer(const configuration& config, const core::video_format_desc& format_desc, int channel_index)
+ : channel_index_(channel_index)
+ , config_(config)
+ , decklink_(get_device(config.device_index))
+ , output_(decklink_)
+ , configuration_(decklink_)
+ , keyer_(decklink_)
+ , attributes_(decklink_)
+ , model_name_(get_model_name(decklink_))
+ , format_desc_(format_desc)
+ , buffer_size_(config.buffer_depth()) // Minimum buffer-size 3.
+ , video_scheduled_(0)
+ , audio_scheduled_(0)
+ , preroll_count_(0)
+ , audio_container_(buffer_size_+1)
+ {
+ is_running_ = true;
+
+ video_frame_buffer_.set_capacity(1);
+ audio_frame_buffer_.set_capacity(1);
+
+ 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.3f));
+ graph_->set_color("dropped-frame", diagnostics::color(0.3f, 0.6f, 0.3f));
+ graph_->set_color("flushed-frame", diagnostics::color(0.4f, 0.3f, 0.8f));
+ graph_->set_color("buffered-audio", diagnostics::color(0.9f, 0.9f, 0.5f));
+ graph_->set_color("buffered-video", diagnostics::color(0.2f, 0.9f, 0.9f));
+ graph_->set_text(print());
+ diagnostics::register_graph(graph_);
+
+ enable_video(get_display_mode(output_, format_desc_.format, bmdFormat8BitBGRA, bmdVideoOutputFlagDefault));
+
+ if(config.embedded_audio)
+ enable_audio();
+
+ set_latency(config.latency);
+ set_keyer(config.keyer);
+
+ if(config.embedded_audio)
+ output_->BeginAudioPreroll();
+
+ for(int n = 0; n < buffer_size_; ++n)
+ schedule_next_video(core::const_frame::empty());
+
+ if(!config.embedded_audio)
+ start_playback();
+ }
+
+ ~decklink_consumer()
+ {
+ is_running_ = false;
+ video_frame_buffer_.try_push(core::const_frame::empty());
+ audio_frame_buffer_.try_push(core::const_frame::empty());
+
+ if(output_ != nullptr)
+ {
+ output_->StopScheduledPlayback(0, nullptr, 0);
+ if(config_.embedded_audio)
+ output_->DisableAudioOutput();
+ output_->DisableVideoOutput();
+ }
+ }
+
+ void set_latency(configuration::latency_t latency)
+ {
+ if(latency == configuration::low_latency)
+ {
+ configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, true);
+ CASPAR_LOG(info) << print() << L" Enabled low-latency mode.";
+ }
+ else if(latency == configuration::normal_latency)
+ {
+ configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, false);
+ CASPAR_LOG(info) << print() << L" Disabled low-latency mode.";
+ }
+ }
+
+ void set_keyer(configuration::keyer_t keyer)
+ {
+ if(keyer == configuration::internal_keyer)
+ {
+ BOOL value = true;
+ if(SUCCEEDED(attributes_->GetFlag(BMDDeckLinkSupportsInternalKeying, &value)) && !value)
+ CASPAR_LOG(error) << print() << L" Failed to enable internal keyer.";
+ else if(FAILED(keyer_->Enable(FALSE)))
+ CASPAR_LOG(error) << print() << L" Failed to enable internal keyer.";
+ else if(FAILED(keyer_->SetLevel(255)))
+ CASPAR_LOG(error) << print() << L" Failed to set key-level to max.";
+ else
+ CASPAR_LOG(info) << print() << L" Enabled internal keyer.";
+ }
+ else if(keyer == configuration::external_keyer)
+ {
+ BOOL value = true;
+ if(SUCCEEDED(attributes_->GetFlag(BMDDeckLinkSupportsExternalKeying, &value)) && !value)
+ CASPAR_LOG(error) << print() << L" Failed to enable external keyer.";
+ else if(FAILED(keyer_->Enable(TRUE)))
+ CASPAR_LOG(error) << print() << L" Failed to enable external keyer.";
+ else if(FAILED(keyer_->SetLevel(255)))
+ CASPAR_LOG(error) << print() << L" Failed to set key-level to max.";
+ else
+ CASPAR_LOG(info) << print() << L" Enabled external keyer.";
+ }
+ }
+
+ void enable_audio()
+ {
+ if(FAILED(output_->EnableAudioOutput(bmdAudioSampleRate48kHz, bmdAudioSampleType32bitInteger, 2, bmdAudioOutputStreamTimestamped)))
+ CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Could not enable audio output."));
+
+ if(FAILED(output_->SetAudioCallback(this)))
+ CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Could not set audio callback."));
+
+ CASPAR_LOG(info) << print() << L" Enabled embedded-audio.";
+ }
+
+ void enable_video(BMDDisplayMode display_mode)
+ {
+ if(FAILED(output_->EnableVideoOutput(display_mode, bmdVideoOutputFlagDefault)))
+ CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Could not enable video output."));
+
+ if(FAILED(output_->SetScheduledFrameCompletionCallback(this)))
+ CASPAR_THROW_EXCEPTION(caspar_exception()
+ << msg_info(u8(print()) + " Failed to set playback completion callback.")
+ << boost::errinfo_api_function("SetScheduledFrameCompletionCallback"));
+ }
+
+ void start_playback()
+ {
+ if(FAILED(output_->StartScheduledPlayback(0, format_desc_.time_scale, 1.0)))
+ CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Failed to schedule playback."));
+ }
+
+ STDMETHOD (QueryInterface(REFIID, LPVOID*)) {return E_NOINTERFACE;}
+ STDMETHOD_(ULONG, AddRef()) {return 1;}
+ STDMETHOD_(ULONG, Release()) {return 1;}
+
+ STDMETHOD(ScheduledPlaybackHasStopped())
+ {
+ is_running_ = false;
+ CASPAR_LOG(info) << print() << L" Scheduled playback has stopped.";
+ return S_OK;
+ }
+
+ STDMETHOD(ScheduledFrameCompleted(IDeckLinkVideoFrame* completed_frame, BMDOutputFrameCompletionResult result))
+ {
+ if(!is_running_)
+ return E_FAIL;
+
+ try
+ {
+ if(result == bmdOutputFrameDisplayedLate)
+ {
+ graph_->set_tag("late-frame");
+ video_scheduled_ += format_desc_.duration;
+ audio_scheduled_ += reinterpret_cast<decklink_frame*>(completed_frame)->audio_data().size()/format_desc_.audio_channels;
+ //++video_scheduled_;
+ //audio_scheduled_ += format_desc_.audio_cadence[0];
+ //++audio_scheduled_;
+ }
+ else if(result == bmdOutputFrameDropped)
+ graph_->set_tag("dropped-frame");
+ else if(result == bmdOutputFrameFlushed)
+ graph_->set_tag("flushed-frame");
+
+ auto frame = core::const_frame::empty();
+ video_frame_buffer_.pop(frame);
+ schedule_next_video(frame);
+
+ unsigned long buffered;
+ output_->GetBufferedVideoFrameCount(&buffered);
+ graph_->set_value("buffered-video", static_cast<double>(buffered)/format_desc_.fps);
+ }
+ catch(...)
+ {
+ lock(exception_mutex_, [&]
+ {
+ exception_ = std::current_exception();
+ });
+ return E_FAIL;
+ }
+
+ return S_OK;
+ }
+
+ STDMETHOD(RenderAudioSamples(BOOL preroll))
+ {
+ if(!is_running_)
+ return E_FAIL;
+
+ try
+ {
+ if(preroll)
+ {
+ if(++preroll_count_ >= buffer_size_)
+ {
+ output_->EndAudioPreroll();
+ start_playback();
+ }
+ else
+ schedule_next_audio(core::audio_buffer(format_desc_.audio_cadence[preroll % format_desc_.audio_cadence.size()], 0));
+ }
+ else
+ {
+ auto frame = core::const_frame::empty();
+ audio_frame_buffer_.pop(frame);
+ schedule_next_audio(frame.audio_data());
+ }
+
+ unsigned long buffered;
+ output_->GetBufferedAudioSampleFrameCount(&buffered);
+ graph_->set_value("buffered-audio", static_cast<double>(buffered)/(format_desc_.audio_cadence[0]*2));
+ }
+ catch(...)
+ {
+ tbb::spin_mutex::scoped_lock lock(exception_mutex_);
+ exception_ = std::current_exception();
+ return E_FAIL;
+ }
+
+ return S_OK;
+ }
+
+ template<typename T>
+ void schedule_next_audio(const T& audio_data)
+ {
+ auto sample_frame_count = static_cast<int>(audio_data.size()/format_desc_.audio_channels);
+
+ audio_container_.push_back(std::vector<int32_t>(audio_data.begin(), audio_data.end()));
+
+ if(FAILED(output_->ScheduleAudioSamples(audio_container_.back().data(), sample_frame_count, audio_scheduled_, format_desc_.audio_sample_rate, nullptr)))
+ CASPAR_LOG(error) << print() << L" Failed to schedule audio.";
+
+ audio_scheduled_ += sample_frame_count;
+ }
+
+ void schedule_next_video(core::const_frame frame)
+ {
+ CComPtr<IDeckLinkVideoFrame> frame2(new decklink_frame(frame, format_desc_, config_.key_only));
+ if(FAILED(output_->ScheduleVideoFrame(frame2, video_scheduled_, format_desc_.duration, format_desc_.time_scale)))
+ CASPAR_LOG(error) << print() << L" Failed to schedule video.";
+
+ video_scheduled_ += format_desc_.duration;
+
+ graph_->set_value("tick-time", tick_timer_.elapsed()*format_desc_.fps*0.5);
+ tick_timer_.restart();
+ }
+
+ void send(core::const_frame frame)
+ {
+ auto exception = lock(exception_mutex_, [&]
+ {
+ return exception_;
+ });
+
+ if(exception != nullptr)
+ std::rethrow_exception(exception);
+
+ if(!is_running_)
+ CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info(u8(print()) + " Is not running."));
+
+ if(config_.embedded_audio)
+ audio_frame_buffer_.push(frame);
+ video_frame_buffer_.push(frame);
+ }
+
+ std::wstring print() const
+ {
+ return model_name_ + L" [" + boost::lexical_cast<std::wstring>(channel_index_) + L"-" +
+ boost::lexical_cast<std::wstring>(config_.device_index) + L"|" + format_desc_.name + L"]";
+ }
+};
+
+struct decklink_consumer_proxy : public core::frame_consumer
+{
+ const configuration config_;
+ std::unique_ptr<decklink_consumer> consumer_;
+ executor executor_;
+public:
+
+ decklink_consumer_proxy(const configuration& config)
+ : config_(config)
+ , executor_(L"decklink_consumer[" + boost::lexical_cast<std::wstring>(config.device_index) + L"]")
+ {
+ executor_.begin_invoke([=]
+ {
+ ::CoInitialize(nullptr);
+ });
+ }
+
+ ~decklink_consumer_proxy()
+ {
+ executor_.invoke([=]
+ {
+ ::CoUninitialize();
+ });
+ }
+
+ // frame_consumer
+
+ void initialize(const core::video_format_desc& format_desc, int channel_index) override
+ {
+ executor_.invoke([=]
+ {
+ consumer_.reset();
+ consumer_.reset(new decklink_consumer(config_, format_desc, channel_index));
+ });
+ }
+
+ bool send(core::const_frame frame) override
+ {
+ consumer_->send(frame);
+ return true;
+ }
+
+ std::wstring print() const override
+ {
+ return consumer_ ? consumer_->print() : L"[decklink_consumer]";
+ }
+
+ std::wstring name() const override
+ {
+ return L"decklink";
+ }
+
+ boost::property_tree::wptree info() const override
+ {
+ boost::property_tree::wptree info;
+ info.add(L"type", L"decklink");
+ info.add(L"key-only", config_.key_only);
+ info.add(L"device", config_.device_index);
+ info.add(L"low-latency", config_.low_latency);
+ info.add(L"embedded-audio", config_.embedded_audio);
+ info.add(L"low-latency", config_.low_latency);
+ //info.add(L"internal-key", config_.internal_key);
+ return info;
+ }
+
+ int buffer_depth() const override
+ {
+ return config_.buffer_depth();
+ }
+
+ int index() const override
+ {
+ return 300 + config_.device_index;
+ }
+
+ void subscribe(const monitor::observable::observer_ptr& o) override
+ {
+ }
+
+ void unsubscribe(const monitor::observable::observer_ptr& o) override
+ {
+ }
+};
+
+spl::shared_ptr<core::frame_consumer> create_consumer(const std::vector<std::wstring>& params)
+{
+ if(params.size() < 1 || params[0] != L"DECKLINK")
+ return core::frame_consumer::empty();
+
+ configuration config;
+
+ if(params.size() > 1)
+ config.device_index = boost::lexical_cast<int>(params[1]);
+
+ if(std::find(params.begin(), params.end(), L"INTERNAL_KEY") != params.end())
+ config.keyer = configuration::internal_keyer;
+ else if(std::find(params.begin(), params.end(), L"EXTERNAL_KEY") != params.end())
+ config.keyer = configuration::external_keyer;
+ else
+ config.keyer = configuration::default_keyer;
+
+ if(std::find(params.begin(), params.end(), L"LOW_LATENCY") != params.end())
+ config.latency = configuration::low_latency;
+
+ config.embedded_audio = std::find(params.begin(), params.end(), L"EMBEDDED_AUDIO") != params.end();
+ config.key_only = std::find(params.begin(), params.end(), L"KEY_ONLY") != params.end();
+
+ return spl::make_shared<decklink_consumer_proxy>(config);
+}
+
+spl::shared_ptr<core::frame_consumer> create_consumer(const boost::property_tree::wptree& ptree)
+{
+ configuration config;
+
+ auto keyer = ptree.get(L"keyer", L"default");
+ if(keyer == L"external")
+ config.keyer = configuration::external_keyer;
+ else if(keyer == L"internal")
+ config.keyer = configuration::internal_keyer;
+
+ auto latency = ptree.get(L"latency", L"normal");
+ if(latency == L"low")
+ config.latency = configuration::low_latency;
+ else if(latency == L"normal")
+ config.latency = configuration::normal_latency;
+
+ config.key_only = ptree.get(L"key-only", config.key_only);
+ config.device_index = ptree.get(L"device", config.device_index);
+ config.embedded_audio = ptree.get(L"embedded-audio", config.embedded_audio);
+ config.base_buffer_depth = ptree.get(L"buffer-depth", config.base_buffer_depth);
+
+ return spl::make_shared<decklink_consumer_proxy>(config);
+}
+
+}}
+
+/*
+##############################################################################
+Pre-rolling
+
+Mail: 2011-05-09
+
+Yoshan
+BMD Developer Support
+developer@blackmagic-design.com
+
+-----------------------------------------------------------------------------
+
+Thanks for your inquiry. The minimum number of frames that you can preroll
+for scheduled playback is three frames for video and four frames for audio.
+As you mentioned if you preroll less frames then playback will not start or
+playback will be very sporadic. From our experience with Media Express, we
+recommended that at least seven frames are prerolled for smooth playback.
+
+Regarding the bmdDeckLinkConfigLowLatencyVideoOutput flag:
+There can be around 3 frames worth of latency on scheduled output.
+When the bmdDeckLinkConfigLowLatencyVideoOutput flag is used this latency is
+reduced or removed for scheduled playback. If the DisplayVideoFrameSync()
+method is used, the bmdDeckLinkConfigLowLatencyVideoOutput setting will
+guarantee that the provided frame will be output as soon the previous
+frame output has been completed.
+################################################################################
+*/
+
+/*
+##############################################################################
+Async DMA Transfer without redundant copying
+
+Mail: 2011-05-10
+
+Yoshan
+BMD Developer Support
+developer@blackmagic-design.com
+
+-----------------------------------------------------------------------------
+
+Thanks for your inquiry. You could try subclassing IDeckLinkMutableVideoFrame
+and providing a pointer to your video buffer when GetBytes() is called.
+This may help to keep copying to a minimum. Please ensure that the pixel
+format is in bmdFormat10BitYUV, otherwise the DeckLink API / driver will
+have to colourspace convert which may result in additional copying.
+################################################################################
*/
\ No newline at end of file