2 * copyright (c) 2010 Sveriges Television AB <info@casparcg.com>
\r
4 * This file is part of CasparCG.
\r
6 * CasparCG is free software: you can redistribute it and/or modify
\r
7 * it under the terms of the GNU General Public License as published by
\r
8 * the Free Software Foundation, either version 3 of the License, or
\r
9 * (at your option) any later version.
\r
11 * CasparCG is distributed in the hope that it will be useful,
\r
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
\r
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
\r
14 * GNU General Public License for more details.
\r
16 * You should have received a copy of the GNU General Public License
\r
17 * along with CasparCG. If not, see <http://www.gnu.org/licenses/>.
\r
21 #include "../StdAfx.h"
\r
23 #include "decklink_consumer.h"
\r
25 #include "../util/util.h"
\r
27 #include "../interop/DeckLinkAPI_h.h"
\r
29 #include <core/mixer/read_frame.h>
\r
31 #include <common/concurrency/com_context.h>
\r
32 #include <common/diagnostics/graph.h>
\r
33 #include <common/exception/exceptions.h>
\r
34 #include <common/memory/memcpy.h>
\r
35 #include <common/memory/memclr.h>
\r
36 #include <common/memory/memshfl.h>
\r
38 #include <core/consumer/frame_consumer.h>
\r
40 #include <tbb/concurrent_queue.h>
\r
42 #include <boost/circular_buffer.hpp>
\r
43 #include <boost/timer.hpp>
\r
47 struct configuration
\r
49 size_t device_index;
\r
50 bool embedded_audio;
\r
54 size_t buffer_depth;
\r
58 , embedded_audio(false)
\r
59 , internal_key(false)
\r
60 , low_latency(false)
\r
62 , buffer_depth(core::consumer_buffer_depth()){}
\r
65 class decklink_frame : public IDeckLinkVideoFrame
\r
67 const safe_ptr<core::read_frame> frame_;
\r
68 const core::video_format_desc format_desc_;
\r
70 decklink_frame(const safe_ptr<core::read_frame>& frame, const core::video_format_desc& format_desc)
\r
72 , format_desc_(format_desc){}
\r
74 STDMETHOD (QueryInterface(REFIID, LPVOID*)) {return E_NOINTERFACE;}
\r
75 STDMETHOD_(ULONG, AddRef()) {return 1;}
\r
76 STDMETHOD_(ULONG, Release()) {return 1;}
\r
78 STDMETHOD_(long, GetWidth()) {return format_desc_.width;}
\r
79 STDMETHOD_(long, GetHeight()) {return format_desc_.height;}
\r
80 STDMETHOD_(long, GetRowBytes()) {return format_desc_.width*4;}
\r
81 STDMETHOD_(BMDPixelFormat, GetPixelFormat()){return bmdFormat8BitBGRA;}
\r
82 STDMETHOD_(BMDFrameFlags, GetFlags()) {return bmdFrameFlagDefault;}
\r
84 STDMETHOD(GetBytes(void** buffer))
\r
86 static std::vector<uint8_t> zeros(1920*1080*4, 0);
\r
87 *buffer = const_cast<uint8_t*>(frame_->image_data().begin());
\r
88 if(static_cast<size_t>(frame_->image_data().size()) != format_desc_.size)
\r
89 *buffer = zeros.data();
\r
93 STDMETHOD(GetTimecode(BMDTimecodeFormat format, IDeckLinkTimecode** timecode)){return S_FALSE;}
\r
94 STDMETHOD(GetAncillaryData(IDeckLinkVideoFrameAncillary** ancillary)) {return S_FALSE;}
\r
97 struct decklink_consumer : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback, boost::noncopyable
\r
99 const configuration config_;
\r
101 CComPtr<IDeckLink> decklink_;
\r
102 CComQIPtr<IDeckLinkOutput> output_;
\r
103 CComQIPtr<IDeckLinkConfiguration> configuration_;
\r
104 CComQIPtr<IDeckLinkKeyer> keyer_;
\r
106 tbb::spin_mutex exception_mutex_;
\r
107 std::exception_ptr exception_;
\r
109 tbb::atomic<bool> is_running_;
\r
111 const std::wstring model_name_;
\r
112 const core::video_format_desc format_desc_;
\r
113 const size_t buffer_size_;
\r
115 long long frames_scheduled_;
\r
116 long long audio_scheduled_;
\r
118 size_t preroll_count_;
\r
120 std::list<std::shared_ptr<IDeckLinkVideoFrame>> frame_container_; // Must be std::list in order to guarantee that pointers are always valid.
\r
121 boost::circular_buffer<std::vector<int16_t>> audio_container_;
\r
123 tbb::concurrent_bounded_queue<std::shared_ptr<core::read_frame>> video_frame_buffer_;
\r
124 tbb::concurrent_bounded_queue<std::shared_ptr<core::read_frame>> audio_frame_buffer_;
\r
126 std::shared_ptr<diagnostics::graph> graph_;
\r
127 boost::timer tick_timer_;
\r
130 decklink_consumer(const configuration& config, const core::video_format_desc& format_desc)
\r
132 , decklink_(get_device(config.device_index))
\r
133 , output_(decklink_)
\r
134 , configuration_(decklink_)
\r
135 , keyer_(decklink_)
\r
136 , model_name_(get_model_name(decklink_))
\r
137 , format_desc_(format_desc)
\r
138 , buffer_size_(config.embedded_audio ? config.buffer_depth + 1 : config.buffer_depth) // Minimum buffer-size 3.
\r
139 , frames_scheduled_(0)
\r
140 , audio_scheduled_(0)
\r
141 , preroll_count_(0)
\r
142 , audio_container_(buffer_size_+1)
\r
144 is_running_ = true;
\r
146 video_frame_buffer_.set_capacity(1);
\r
147 audio_frame_buffer_.set_capacity(1);
\r
149 graph_ = diagnostics::create_graph(narrow(print()));
\r
150 graph_->add_guide("tick-time", 0.5);
\r
151 graph_->set_color("tick-time", diagnostics::color(0.0f, 0.6f, 0.9f));
\r
152 graph_->set_color("late-frame", diagnostics::color(0.6f, 0.3f, 0.3f));
\r
153 graph_->set_color("dropped-frame", diagnostics::color(0.3f, 0.6f, 0.3f));
\r
154 graph_->set_color("flushed-frame", diagnostics::color(0.4f, 0.3f, 0.8f));
\r
156 enable_video(get_display_mode(output_, format_desc_.format, bmdFormat8BitBGRA, bmdVideoOutputFlagDefault));
\r
158 if(config.embedded_audio)
\r
161 set_latency(config.low_latency);
\r
162 set_keyer(config.internal_key);
\r
164 if(config.embedded_audio)
\r
165 output_->BeginAudioPreroll();
\r
167 for(size_t n = 0; n < buffer_size_; ++n)
\r
168 schedule_next_video(make_safe<core::read_frame>());
\r
170 if(!config.embedded_audio)
\r
174 ~decklink_consumer()
\r
176 is_running_ = false;
\r
177 video_frame_buffer_.try_push(std::make_shared<core::read_frame>());
\r
178 audio_frame_buffer_.try_push(std::make_shared<core::read_frame>());
\r
180 if(output_ != nullptr)
\r
182 output_->StopScheduledPlayback(0, nullptr, 0);
\r
183 if(config_.embedded_audio)
\r
184 output_->DisableAudioOutput();
\r
185 output_->DisableVideoOutput();
\r
189 const core::video_format_desc& get_video_format_desc() const
\r
191 return format_desc_;
\r
194 void set_latency(bool low_latency)
\r
198 configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, false);
\r
199 CASPAR_LOG(info) << print() << L" Enabled normal-latency mode";
\r
203 configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, true);
\r
204 CASPAR_LOG(info) << print() << L" Enabled low-latency mode";
\r
208 void set_keyer(bool internal_key)
\r
212 if(FAILED(keyer_->Enable(FALSE)))
\r
213 CASPAR_LOG(error) << print() << L" Failed to enable internal keyer.";
\r
214 else if(FAILED(keyer_->SetLevel(255)))
\r
215 CASPAR_LOG(error) << print() << L" Failed to set key-level to max.";
\r
217 CASPAR_LOG(info) << print() << L" Enabled internal keyer.";
\r
221 if(FAILED(keyer_->Enable(TRUE)))
\r
222 CASPAR_LOG(error) << print() << L" Failed to enable external keyer.";
\r
223 else if(FAILED(keyer_->SetLevel(255)))
\r
224 CASPAR_LOG(error) << print() << L" Failed to set key-level to max.";
\r
226 CASPAR_LOG(info) << print() << L" Enabled external keyer.";
\r
230 void enable_audio()
\r
232 if(FAILED(output_->EnableAudioOutput(bmdAudioSampleRate48kHz, bmdAudioSampleType16bitInteger, 2, bmdAudioOutputStreamTimestamped)))
\r
233 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not enable audio output."));
\r
235 if(FAILED(output_->SetAudioCallback(this)))
\r
236 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not set audio callback."));
\r
238 CASPAR_LOG(info) << print() << L" Enabled embedded-audio.";
\r
241 void enable_video(BMDDisplayMode display_mode)
\r
243 if(FAILED(output_->EnableVideoOutput(display_mode, bmdVideoOutputFlagDefault)))
\r
244 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not enable video output."));
\r
246 if(FAILED(output_->SetScheduledFrameCompletionCallback(this)))
\r
247 BOOST_THROW_EXCEPTION(caspar_exception()
\r
248 << msg_info(narrow(print()) + " Failed to set playback completion callback.")
\r
249 << boost::errinfo_api_function("SetScheduledFrameCompletionCallback"));
\r
252 void start_playback()
\r
254 if(FAILED(output_->StartScheduledPlayback(0, format_desc_.time_scale, 1.0)))
\r
255 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Failed to schedule playback."));
\r
258 STDMETHOD (QueryInterface(REFIID, LPVOID*)) {return E_NOINTERFACE;}
\r
259 STDMETHOD_(ULONG, AddRef()) {return 1;}
\r
260 STDMETHOD_(ULONG, Release()) {return 1;}
\r
262 STDMETHOD(ScheduledPlaybackHasStopped())
\r
264 is_running_ = false;
\r
265 CASPAR_LOG(info) << print() << L" Scheduled playback has stopped.";
\r
269 STDMETHOD(ScheduledFrameCompleted(IDeckLinkVideoFrame* completed_frame, BMDOutputFrameCompletionResult result))
\r
276 if(result == bmdOutputFrameDisplayedLate)
\r
278 graph_->add_tag("late-frame");
\r
279 ++frames_scheduled_;
\r
280 ++audio_scheduled_;
\r
282 else if(result == bmdOutputFrameDropped)
\r
283 graph_->add_tag("dropped-frame");
\r
284 else if(result == bmdOutputFrameFlushed)
\r
285 graph_->add_tag("flushed-frame");
\r
287 frame_container_.erase(std::find_if(frame_container_.begin(), frame_container_.end(), [&](const std::shared_ptr<IDeckLinkVideoFrame>& frame)
\r
289 return frame.get() == completed_frame;
\r
292 std::shared_ptr<core::read_frame> frame;
\r
293 video_frame_buffer_.pop(frame);
\r
294 schedule_next_video(make_safe(frame));
\r
298 tbb::spin_mutex::scoped_lock lock(exception_mutex_);
\r
299 exception_ = std::current_exception();
\r
306 STDMETHOD(RenderAudioSamples(BOOL preroll))
\r
315 if(++preroll_count_ >= buffer_size_)
\r
317 output_->EndAudioPreroll();
\r
321 schedule_next_audio(make_safe<core::read_frame>());
\r
325 std::shared_ptr<core::read_frame> frame;
\r
326 audio_frame_buffer_.pop(frame);
\r
327 schedule_next_audio(make_safe(frame));
\r
332 tbb::spin_mutex::scoped_lock lock(exception_mutex_);
\r
333 exception_ = std::current_exception();
\r
340 void schedule_next_audio(const safe_ptr<core::read_frame>& frame)
\r
342 const int sample_frame_count = frame->audio_data().size()/format_desc_.audio_channels;
\r
344 audio_container_.push_back(std::vector<int16_t>(frame->audio_data().begin(), frame->audio_data().end()));
\r
346 if(FAILED(output_->ScheduleAudioSamples(audio_container_.back().data(), sample_frame_count, (audio_scheduled_++) * sample_frame_count, format_desc_.audio_sample_rate, nullptr)))
\r
347 CASPAR_LOG(error) << print() << L" Failed to schedule audio.";
\r
350 void schedule_next_video(const safe_ptr<core::read_frame>& frame)
\r
352 frame_container_.push_back(std::make_shared<decklink_frame>(frame, format_desc_));
\r
353 if(FAILED(output_->ScheduleVideoFrame(frame_container_.back().get(), (frames_scheduled_++) * format_desc_.duration, format_desc_.duration, format_desc_.time_scale)))
\r
354 CASPAR_LOG(error) << print() << L" Failed to schedule video.";
\r
356 graph_->update_value("tick-time", tick_timer_.elapsed()*format_desc_.fps*0.5);
\r
357 tick_timer_.restart();
\r
360 void send(const safe_ptr<core::read_frame>& frame)
\r
363 tbb::spin_mutex::scoped_lock lock(exception_mutex_);
\r
364 if(exception_ != nullptr)
\r
365 std::rethrow_exception(exception_);
\r
369 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Is not running."));
\r
371 if(config_.embedded_audio)
\r
372 audio_frame_buffer_.push(frame);
\r
373 video_frame_buffer_.push(frame);
\r
376 std::wstring print() const
\r
378 return model_name_ + L" [" + boost::lexical_cast<std::wstring>(config_.device_index) + L"|" + format_desc_.name + L"]";
\r
382 struct decklink_consumer_proxy : public core::frame_consumer
\r
384 const configuration config_;
\r
385 com_context<decklink_consumer> context_;
\r
386 core::video_format_desc format_desc_;
\r
387 size_t fail_count_;
\r
390 decklink_consumer_proxy(const configuration& config)
\r
392 , context_(L"decklink_consumer[" + boost::lexical_cast<std::wstring>(config.device_index) + L"]")
\r
397 ~decklink_consumer_proxy()
\r
399 auto str = print();
\r
401 CASPAR_LOG(info) << str << L" Successfully Uninitialized.";
\r
404 virtual void initialize(const core::video_format_desc& format_desc)
\r
406 format_desc_ = format_desc;
\r
407 context_.reset([&]{return new decklink_consumer(config_, format_desc_);});
\r
409 CASPAR_LOG(info) << print() << L" Successfully Initialized.";
\r
412 virtual bool send(const safe_ptr<core::read_frame>& frame)
\r
415 context_.reset([&]{return new decklink_consumer(config_, format_desc_);});
\r
419 context_->send(frame);
\r
426 if(fail_count_++ > 3)
\r
427 return false; // Outside didn't handle exception properly, just give up.
\r
435 virtual std::wstring print() const
\r
437 return context_ ? context_->print() : L"decklink_consumer";
\r
440 virtual bool key_only() const
\r
442 return config_.key_only;
\r
445 virtual const core::video_format_desc& get_video_format_desc() const
\r
447 return format_desc_;
\r
451 safe_ptr<core::frame_consumer> create_decklink_consumer(const std::vector<std::wstring>& params)
\r
453 if(params.size() < 1 || params[0] != L"DECKLINK")
\r
454 return core::frame_consumer::empty();
\r
456 configuration config;
\r
458 if(params.size() > 1)
\r
459 config.device_index = lexical_cast_or_default<int>(params[1], config.device_index);
\r
461 config.internal_key = std::find(params.begin(), params.end(), L"INTERNAL_KEY") != params.end();
\r
462 config.low_latency = std::find(params.begin(), params.end(), L"LOW_LATENCY") != params.end();
\r
463 config.embedded_audio = std::find(params.begin(), params.end(), L"EMBEDDED_AUDIO") != params.end();
\r
464 config.key_only = std::find(params.begin(), params.end(), L"KEY_ONLY") != params.end();
\r
466 return make_safe<decklink_consumer_proxy>(config);
\r
469 safe_ptr<core::frame_consumer> create_decklink_consumer(const boost::property_tree::ptree& ptree)
\r
471 configuration config;
\r
473 config.internal_key = ptree.get("internal-key", config.internal_key);
\r
474 config.low_latency = ptree.get("low-latency", config.low_latency);
\r
475 config.key_only = ptree.get("key-only", config.key_only);
\r
476 config.device_index = ptree.get("device", config.device_index);
\r
477 config.embedded_audio = ptree.get("embedded-audio", config.embedded_audio);
\r
479 return make_safe<decklink_consumer_proxy>(config);
\r
485 ##############################################################################
\r
491 BMD Developer Support
\r
492 developer@blackmagic-design.com
\r
494 -----------------------------------------------------------------------------
\r
496 Thanks for your inquiry. The minimum number of frames that you can preroll
\r
497 for scheduled playback is three frames for video and four frames for audio.
\r
498 As you mentioned if you preroll less frames then playback will not start or
\r
499 playback will be very sporadic. From our experience with Media Express, we
\r
500 recommended that at least seven frames are prerolled for smooth playback.
\r
502 Regarding the bmdDeckLinkConfigLowLatencyVideoOutput flag:
\r
503 There can be around 3 frames worth of latency on scheduled output.
\r
504 When the bmdDeckLinkConfigLowLatencyVideoOutput flag is used this latency is
\r
505 reduced or removed for scheduled playback. If the DisplayVideoFrameSync()
\r
506 method is used, the bmdDeckLinkConfigLowLatencyVideoOutput setting will
\r
507 guarantee that the provided frame will be output as soon the previous
\r
508 frame output has been completed.
\r
509 ################################################################################
\r
513 ##############################################################################
\r
514 Async DMA Transfer without redundant copying
\r
519 BMD Developer Support
\r
520 developer@blackmagic-design.com
\r
522 -----------------------------------------------------------------------------
\r
524 Thanks for your inquiry. You could try subclassing IDeckLinkMutableVideoFrame
\r
525 and providing a pointer to your video buffer when GetBytes() is called.
\r
526 This may help to keep copying to a minimum. Please ensure that the pixel
\r
527 format is in bmdFormat10BitYUV, otherwise the DeckLink API / driver will
\r
528 have to colourspace convert which may result in additional copying.
\r
529 ################################################################################
\r