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/consumer/frame/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
37 #include <tbb/concurrent_queue.h>
\r
39 #include <boost/circular_buffer.hpp>
\r
40 #include <boost/timer.hpp>
\r
65 struct configuration
\r
67 size_t device_index;
\r
68 bool embedded_audio;
\r
71 output_pixels output;
\r
75 , embedded_audio(false)
\r
76 , keyer(default_key)
\r
77 , latency(default_latency)
\r
78 , output(fill_and_key){}
\r
81 class decklink_frame_adapter : public IDeckLinkVideoFrame
\r
83 const safe_ptr<const core::read_frame> frame_;
\r
84 const core::video_format_desc format_desc_;
\r
86 decklink_frame_adapter(const safe_ptr<const core::read_frame>& frame, const core::video_format_desc& format_desc)
\r
88 , format_desc_(format_desc){}
\r
90 STDMETHOD (QueryInterface(REFIID, LPVOID*)) {return E_NOINTERFACE;}
\r
91 STDMETHOD_(ULONG, AddRef()) {return 1;}
\r
92 STDMETHOD_(ULONG, Release()) {return 1;}
\r
94 STDMETHOD_(long, GetWidth()) {return format_desc_.width;}
\r
95 STDMETHOD_(long, GetHeight()) {return format_desc_.height;}
\r
96 STDMETHOD_(long, GetRowBytes()) {return format_desc_.width*4;}
\r
97 STDMETHOD_(BMDPixelFormat, GetPixelFormat()){return bmdFormat8BitBGRA;}
\r
98 STDMETHOD_(BMDFrameFlags, GetFlags()) {return bmdFrameFlagDefault;}
\r
100 STDMETHOD(GetBytes(void** buffer))
\r
102 static std::vector<unsigned char> zeros(1920*1080*4, 0);
\r
103 *buffer = const_cast<unsigned char*>(frame_->image_data().begin());
\r
104 if(static_cast<size_t>(frame_->image_data().size()) != format_desc_.size)
\r
105 *buffer = zeros.data();
\r
109 STDMETHOD(GetTimecode(BMDTimecodeFormat format, IDeckLinkTimecode** timecode)){return S_FALSE;}
\r
110 STDMETHOD(GetAncillaryData(IDeckLinkVideoFrameAncillary** ancillary)) {return S_FALSE;}
\r
113 std::shared_ptr<IDeckLinkVideoFrame> make_alpha_only_frame(const CComQIPtr<IDeckLinkOutput>& decklink, const safe_ptr<const core::read_frame>& frame, const core::video_format_desc& format_desc)
\r
115 IDeckLinkMutableVideoFrame* result;
\r
117 if(FAILED(decklink->CreateVideoFrame(format_desc.width, format_desc.height, format_desc.size/format_desc.height, bmdFormat8BitBGRA, bmdFrameFlagDefault, &result)))
\r
118 BOOST_THROW_EXCEPTION(caspar_exception());
\r
120 void* bytes = nullptr;
\r
121 if(FAILED(result->GetBytes(&bytes)))
\r
122 BOOST_THROW_EXCEPTION(caspar_exception());
\r
124 unsigned char* data = reinterpret_cast<unsigned char*>(bytes);
\r
126 if(static_cast<size_t>(frame->image_data().size()) == format_desc.size)
\r
128 tbb::parallel_for(tbb::blocked_range<int>(0, frame->image_data().size()/4), [&](const tbb::blocked_range<int>& r)
\r
130 for(int n = r.begin(); n != r.end(); ++n)
\r
132 data[n*4+0] = frame->image_data()[n*4+3];
\r
133 data[n*4+1] = frame->image_data()[n*4+3];
\r
134 data[n*4+2] = frame->image_data()[n*4+3];
\r
140 memset(data, 0, format_desc.size);
\r
142 return std::shared_ptr<IDeckLinkVideoFrame>(result, [](IDeckLinkMutableVideoFrame* p) {p->Release();});
\r
145 std::shared_ptr<IDeckLinkVideoFrame> make_fill_only_frame(const CComQIPtr<IDeckLinkOutput>& decklink, const safe_ptr<const core::read_frame>& frame, const core::video_format_desc& format_desc)
\r
147 IDeckLinkMutableVideoFrame* result;
\r
149 if(FAILED(decklink->CreateVideoFrame(format_desc.width, format_desc.height, format_desc.size/format_desc.height, bmdFormat8BitBGRA, bmdFrameFlagDefault, &result)))
\r
150 BOOST_THROW_EXCEPTION(caspar_exception());
\r
152 void* bytes = nullptr;
\r
153 if(FAILED(result->GetBytes(&bytes)))
\r
154 BOOST_THROW_EXCEPTION(caspar_exception());
\r
156 unsigned char* data = reinterpret_cast<unsigned char*>(bytes);
\r
158 if(static_cast<size_t>(frame->image_data().size()) == format_desc.size)
\r
160 tbb::parallel_for(tbb::blocked_range<int>(0, frame->image_data().size()/4), [&](const tbb::blocked_range<int>& r)
\r
162 for(int n = r.begin(); n != r.end(); ++n)
\r
164 data[n*4+0] = frame->image_data()[n*4+0];
\r
165 data[n*4+1] = frame->image_data()[n*4+1];
\r
166 data[n*4+2] = frame->image_data()[n*4+2];
\r
172 memset(data, 0, format_desc.size);
\r
174 return std::shared_ptr<IDeckLinkVideoFrame>(result, [](IDeckLinkMutableVideoFrame* p) {p->Release();});
\r
177 struct decklink_consumer : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback, boost::noncopyable
\r
179 const configuration config_;
\r
181 CComPtr<IDeckLink> decklink_;
\r
182 CComQIPtr<IDeckLinkOutput> output_;
\r
183 CComQIPtr<IDeckLinkConfiguration> configuration_;
\r
184 CComQIPtr<IDeckLinkKeyer> keyer_;
\r
186 std::exception_ptr exception_;
\r
188 tbb::atomic<bool> is_running_;
\r
190 const std::wstring model_name_;
\r
191 const core::video_format_desc format_desc_;
\r
192 const size_t buffer_size_;
\r
194 unsigned long frames_scheduled_;
\r
195 unsigned long audio_scheduled_;
\r
197 std::list<std::shared_ptr<IDeckLinkVideoFrame>> frame_container_; // Must be std::list in order to guarantee that pointers are always valid.
\r
198 boost::circular_buffer<std::vector<short>> audio_container_;
\r
200 tbb::concurrent_bounded_queue<std::shared_ptr<const core::read_frame>> video_frame_buffer_;
\r
201 tbb::concurrent_bounded_queue<std::shared_ptr<const core::read_frame>> audio_frame_buffer_;
\r
203 std::shared_ptr<diagnostics::graph> graph_;
\r
204 boost::timer tick_timer_;
\r
207 decklink_consumer(const configuration& config, const core::video_format_desc& format_desc)
\r
209 , decklink_(get_device(config.device_index))
\r
210 , output_(decklink_)
\r
211 , configuration_(decklink_)
\r
212 , keyer_(decklink_)
\r
213 , model_name_(get_model_name(decklink_))
\r
214 , format_desc_(format_desc)
\r
215 , buffer_size_(config.embedded_audio ? 5 : 4) // Minimum buffer-size (3 + 1 tolerance).
\r
216 , frames_scheduled_(0)
\r
217 , audio_scheduled_(0)
\r
218 , audio_container_(buffer_size_+1)
\r
220 is_running_ = true;
\r
222 video_frame_buffer_.set_capacity(1);
\r
223 audio_frame_buffer_.set_capacity(1);
\r
225 graph_ = diagnostics::create_graph(narrow(print()));
\r
226 graph_->add_guide("tick-time", 0.5);
\r
227 graph_->set_color("tick-time", diagnostics::color(0.1f, 0.7f, 0.8f));
\r
228 graph_->set_color("late-frame", diagnostics::color(0.6f, 0.3f, 0.3f));
\r
229 graph_->set_color("dropped-frame", diagnostics::color(0.3f, 0.6f, 0.3f));
\r
230 graph_->set_color("flushed-frame", diagnostics::color(0.3f, 0.3f, 0.6f));
\r
232 enable_video(get_display_mode(output_, format_desc_.format, bmdFormat8BitBGRA, bmdVideoOutputFlagDefault));
\r
234 if(config.embedded_audio)
\r
237 set_latency(config.latency);
\r
238 set_keyer(config.keyer);
\r
240 for(size_t n = 0; n < buffer_size_; ++n)
\r
241 schedule_next_video(core::read_frame::empty());
\r
243 if(config.embedded_audio)
\r
244 output_->BeginAudioPreroll();
\r
248 CASPAR_LOG(info) << print() << L" Buffer depth: " << buffer_size_;
\r
249 CASPAR_LOG(info) << print() << L" Successfully initialized for " << format_desc_.name;
\r
252 ~decklink_consumer()
\r
254 is_running_ = false;
\r
255 video_frame_buffer_.try_push(core::read_frame::empty());
\r
256 audio_frame_buffer_.try_push(core::read_frame::empty());
\r
258 if(output_ != nullptr)
\r
260 output_->StopScheduledPlayback(0, nullptr, 0);
\r
261 if(config_.embedded_audio)
\r
262 output_->DisableAudioOutput();
\r
263 output_->DisableVideoOutput();
\r
267 void set_latency(latency latency)
\r
269 if(latency == normal_latency)
\r
271 configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, false);
\r
272 CASPAR_LOG(info) << print() << L" Enabled normal-latency mode";
\r
274 else if(latency == low_latency)
\r
276 configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, true);
\r
277 CASPAR_LOG(info) << print() << L" Enabled low-latency mode";
\r
280 CASPAR_LOG(info) << print() << L" Uses driver latency settings.";
\r
283 void set_keyer(key keyer)
\r
285 if(keyer == internal_key)
\r
287 if(FAILED(keyer_->Enable(FALSE)))
\r
288 CASPAR_LOG(error) << print() << L" Failed to enable internal keyer.";
\r
289 else if(FAILED(keyer_->SetLevel(255)))
\r
290 CASPAR_LOG(error) << print() << L" Failed to set key-level to max.";
\r
292 CASPAR_LOG(info) << print() << L" Enabled internal keyer.";
\r
294 else if(keyer == external_key)
\r
296 if(FAILED(keyer_->Enable(TRUE)))
\r
297 CASPAR_LOG(error) << print() << L" Failed to enable external keyer.";
\r
298 else if(FAILED(keyer_->SetLevel(255)))
\r
299 CASPAR_LOG(error) << print() << L" Failed to set key-level to max.";
\r
301 CASPAR_LOG(info) << print() << L" Enabled external keyer.";
\r
304 CASPAR_LOG(info) << print() << L" Uses driver keyer settings.";
\r
307 void enable_audio()
\r
309 if(FAILED(output_->EnableAudioOutput(bmdAudioSampleRate48kHz, bmdAudioSampleType16bitInteger, 2, bmdAudioOutputStreamTimestamped)))
\r
310 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not enable audio output."));
\r
312 if(FAILED(output_->SetAudioCallback(this)))
\r
313 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not set audio callback."));
\r
315 CASPAR_LOG(info) << print() << L" Enabled embedded-audio.";
\r
318 void enable_video(BMDDisplayMode display_mode)
\r
320 if(FAILED(output_->EnableVideoOutput(display_mode, bmdVideoOutputFlagDefault)))
\r
321 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not enable video output."));
\r
323 if(FAILED(output_->SetScheduledFrameCompletionCallback(this)))
\r
324 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Failed to set playback completion callback."));
\r
327 void start_playback()
\r
329 if(FAILED(output_->StartScheduledPlayback(0, format_desc_.time_scale, 1.0)))
\r
330 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Failed to schedule playback."));
\r
333 STDMETHOD (QueryInterface(REFIID, LPVOID*)) {return E_NOINTERFACE;}
\r
334 STDMETHOD_(ULONG, AddRef()) {return 1;}
\r
335 STDMETHOD_(ULONG, Release()) {return 1;}
\r
337 STDMETHOD(ScheduledPlaybackHasStopped())
\r
339 is_running_ = false;
\r
340 CASPAR_LOG(info) << print() << L" Scheduled playback has stopped.";
\r
344 STDMETHOD(ScheduledFrameCompleted(IDeckLinkVideoFrame* completed_frame, BMDOutputFrameCompletionResult result))
\r
351 if(result == bmdOutputFrameDisplayedLate)
\r
352 graph_->add_tag("late-frame");
\r
353 else if(result == bmdOutputFrameDropped)
\r
354 graph_->add_tag("dropped-frame");
\r
355 else if(result == bmdOutputFrameFlushed)
\r
356 graph_->add_tag("flushed-frame");
\r
358 frame_container_.erase(std::find_if(frame_container_.begin(), frame_container_.end(), [&](const std::shared_ptr<IDeckLinkVideoFrame> frame)
\r
360 return frame.get() == completed_frame;
\r
363 std::shared_ptr<const core::read_frame> frame;
\r
364 video_frame_buffer_.pop(frame);
\r
365 schedule_next_video(safe_ptr<const core::read_frame>(frame));
\r
369 exception_ = std::current_exception();
\r
376 STDMETHOD(RenderAudioSamples(BOOL preroll))
\r
383 std::shared_ptr<const core::read_frame> frame;
\r
384 audio_frame_buffer_.pop(frame);
\r
385 schedule_next_audio(safe_ptr<const core::read_frame>(frame));
\r
392 exception_ = std::current_exception();
\r
399 void schedule_next_audio(const safe_ptr<const core::read_frame>& frame)
\r
401 static std::vector<short> silence(48000, 0);
\r
403 const int sample_count = format_desc_.audio_samples_per_frame;
\r
404 const int sample_frame_count = sample_count/2;
\r
406 const short* frame_audio_data = frame->audio_data().size() == sample_count ? frame->audio_data().begin() : silence.data();
\r
407 audio_container_.push_back(std::vector<short>(frame_audio_data, frame_audio_data+sample_count));
\r
409 if(FAILED(output_->ScheduleAudioSamples(audio_container_.back().data(), sample_frame_count, (audio_scheduled_++) * sample_frame_count, format_desc_.audio_sample_rate, nullptr)))
\r
410 CASPAR_LOG(error) << print() << L" Failed to schedule audio.";
\r
413 void schedule_next_video(const safe_ptr<const core::read_frame>& frame)
\r
415 std::shared_ptr<IDeckLinkVideoFrame> deck_frame;
\r
416 if(config_.output == key_only)
\r
417 deck_frame = make_alpha_only_frame(output_, frame, format_desc_);
\r
418 else if(config_.output == fill_only)
\r
419 deck_frame = make_fill_only_frame(output_, frame, format_desc_);
\r
421 deck_frame = std::make_shared<decklink_frame_adapter>(frame, format_desc_);
\r
423 frame_container_.push_back(deck_frame);
\r
424 if(FAILED(output_->ScheduleVideoFrame(frame_container_.back().get(), (frames_scheduled_++) * format_desc_.duration, format_desc_.duration, format_desc_.time_scale)))
\r
425 CASPAR_LOG(error) << print() << L" Failed to schedule video.";
\r
427 graph_->update_value("tick-time", tick_timer_.elapsed()*format_desc_.fps*0.5);
\r
428 tick_timer_.restart();
\r
431 void send(const safe_ptr<const core::read_frame>& frame)
\r
433 if(exception_ != nullptr)
\r
434 std::rethrow_exception(exception_);
\r
437 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Is not running."));
\r
439 if(config_.embedded_audio)
\r
440 audio_frame_buffer_.push(frame);
\r
441 video_frame_buffer_.push(frame);
\r
444 std::wstring print() const
\r
446 return model_name_ + L" [" + boost::lexical_cast<std::wstring>(config_.device_index) + L"]";
\r
450 struct decklink_consumer_proxy : public core::frame_consumer
\r
452 const configuration config_;
\r
454 com_context<decklink_consumer> context_;
\r
457 decklink_consumer_proxy(const configuration& config)
\r
459 , context_(L"decklink_consumer[" + boost::lexical_cast<std::wstring>(config.device_index) + L"]"){}
\r
461 virtual void initialize(const core::video_format_desc& format_desc)
\r
463 context_.reset([&]{return new decklink_consumer(config_, format_desc);});
\r
466 virtual void send(const safe_ptr<const core::read_frame>& frame)
\r
468 context_->send(frame);
\r
471 virtual std::wstring print() const
\r
473 return context_->print();
\r
477 safe_ptr<core::frame_consumer> create_decklink_consumer(const std::vector<std::wstring>& params)
\r
479 if(params.size() < 1 || params[0] != L"DECKLINK")
\r
480 return core::frame_consumer::empty();
\r
482 configuration config;
\r
484 if(params.size() > 1)
\r
485 config.device_index = lexical_cast_or_default<int>(params[1], config.device_index);
\r
487 if(std::find(params.begin(), params.end(), L"INTERNAL_KEY") != params.end())
\r
488 config.keyer = internal_key;
\r
489 else if(std::find(params.begin(), params.end(), L"EXTERNAL_KEY") != params.end())
\r
490 config.keyer = external_key;
\r
492 if(std::find(params.begin(), params.end(), L"LOW_LATENCY") != params.end())
\r
493 config.latency = low_latency;
\r
494 else if(std::find(params.begin(), params.end(), L"NORMAL_LATENCY") != params.end())
\r
495 config.latency = normal_latency;
\r
497 config.embedded_audio = std::find(params.begin(), params.end(), L"EMBEDDED_AUDIO") != params.end();
\r
499 return make_safe<decklink_consumer_proxy>(config);
\r
502 safe_ptr<core::frame_consumer> create_decklink_consumer(const boost::property_tree::ptree& ptree)
\r
504 configuration config;
\r
506 auto key_str = ptree.get("key", "default");
\r
507 if(key_str == "internal")
\r
508 config.keyer = internal_key;
\r
509 else if(key_str == "external")
\r
510 config.keyer = external_key;
\r
512 auto latency_str = ptree.get("latency", "default");
\r
513 if(latency_str == "normal")
\r
514 config.latency = normal_latency;
\r
515 else if(latency_str == "low")
\r
516 config.latency = low_latency;
\r
518 auto output_str = ptree.get("output", "fill_and_key");
\r
519 if(output_str == "fill_only")
\r
520 config.output = fill_only;
\r
521 else if(output_str == "key_only")
\r
522 config.output = key_only;
\r
524 config.device_index = ptree.get("device", 0);
\r
525 config.embedded_audio = ptree.get("embedded-audio", false);
\r
527 return make_safe<decklink_consumer_proxy>(config);
\r
533 ##############################################################################
\r
539 BMD Developer Support
\r
540 developer@blackmagic-design.com
\r
542 -----------------------------------------------------------------------------
\r
544 Thanks for your inquiry. The minimum number of frames that you can preroll
\r
545 for scheduled playback is three frames for video and four frames for audio.
\r
546 As you mentioned if you preroll less frames then playback will not start or
\r
547 playback will be very sporadic. From our experience with Media Express, we
\r
548 recommended that at least seven frames are prerolled for smooth playback.
\r
550 Regarding the bmdDeckLinkConfigLowLatencyVideoOutput flag:
\r
551 There can be around 3 frames worth of latency on scheduled output.
\r
552 When the bmdDeckLinkConfigLowLatencyVideoOutput flag is used this latency is
\r
553 reduced or removed for scheduled playback. If the DisplayVideoFrameSync()
\r
554 method is used, the bmdDeckLinkConfigLowLatencyVideoOutput setting will
\r
555 guarantee that the provided frame will be output as soon the previous
\r
556 frame output has been completed.
\r
557 ################################################################################
\r
561 ##############################################################################
\r
562 Async DMA Transfer without redundant copying
\r
567 BMD Developer Support
\r
568 developer@blackmagic-design.com
\r
570 -----------------------------------------------------------------------------
\r
572 Thanks for your inquiry. You could try subclassing IDeckLinkMutableVideoFrame
\r
573 and providing a pointer to your video buffer when GetBytes() is called.
\r
574 This may help to keep copying to a minimum. Please ensure that the pixel
\r
575 format is in bmdFormat10BitYUV, otherwise the DeckLink API / driver will
\r
576 have to colourspace convert which may result in additional copying.
\r
577 ################################################################################
\r