2 * Copyright (c) 2011 Sveriges Television AB <info@casparcg.com>
\r
4 * This file is part of CasparCG (www.casparcg.com).
\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
19 * Author: Robert Nagy, ronag89@gmail.com
\r
22 #include "../StdAfx.h"
\r
24 #include "decklink_consumer.h"
\r
26 #include "../util/util.h"
\r
28 #include "../interop/DeckLinkAPI_h.h"
\r
30 #include <core/mixer/read_frame.h>
\r
32 #include <common/concurrency/com_context.h>
\r
33 #include <common/diagnostics/graph.h>
\r
34 #include <common/exception/exceptions.h>
\r
35 #include <common/memory/memcpy.h>
\r
36 #include <common/memory/memclr.h>
\r
37 #include <common/memory/memshfl.h>
\r
39 #include <core/consumer/frame_consumer.h>
\r
41 #include <tbb/concurrent_queue.h>
\r
42 #include <tbb/cache_aligned_allocator.h>
\r
44 #include <boost/circular_buffer.hpp>
\r
45 #include <boost/timer.hpp>
\r
46 #include <boost/property_tree/ptree.hpp>
\r
48 namespace caspar { namespace decklink {
\r
50 struct configuration
\r
52 size_t device_index;
\r
53 bool embedded_audio;
\r
57 size_t base_buffer_depth;
\r
58 size_t buffer_depth;
\r
62 , embedded_audio(false)
\r
63 , internal_key(false)
\r
66 , base_buffer_depth(3)
\r
67 , buffer_depth(base_buffer_depth + (low_latency ? 0 : 1) + (embedded_audio ? 1 : 0)){}
\r
70 class decklink_frame : public IDeckLinkVideoFrame
\r
72 tbb::atomic<int> ref_count_;
\r
73 std::shared_ptr<core::read_frame> frame_;
\r
74 const core::video_format_desc format_desc_;
\r
76 const bool key_only_;
\r
77 std::vector<uint8_t, tbb::cache_aligned_allocator<uint8_t>> key_data_;
\r
79 decklink_frame(const safe_ptr<core::read_frame>& frame, const core::video_format_desc& format_desc, bool key_only)
\r
81 , format_desc_(format_desc)
\r
82 , key_only_(key_only)
\r
87 const boost::iterator_range<const int32_t*> audio_data()
\r
89 return frame_->audio_data();
\r
92 STDMETHOD (QueryInterface(REFIID, LPVOID*)) {return E_NOINTERFACE;}
\r
93 STDMETHOD_(ULONG, AddRef())
\r
95 return ++ref_count_;
\r
97 STDMETHOD_(ULONG, Release())
\r
100 if(ref_count_ == 0)
\r
105 STDMETHOD_(long, GetWidth()) {return format_desc_.width;}
\r
106 STDMETHOD_(long, GetHeight()) {return format_desc_.height;}
\r
107 STDMETHOD_(long, GetRowBytes()) {return format_desc_.width*4;}
\r
108 STDMETHOD_(BMDPixelFormat, GetPixelFormat()) {return bmdFormat8BitBGRA;}
\r
109 STDMETHOD_(BMDFrameFlags, GetFlags()) {return bmdFrameFlagDefault;}
\r
111 STDMETHOD(GetBytes(void** buffer))
\r
113 static std::vector<uint8_t, tbb::cache_aligned_allocator<uint8_t>> zeros(1920*1080*4, 0);
\r
114 if(static_cast<size_t>(frame_->image_data().size()) != format_desc_.size)
\r
116 *buffer = zeros.data();
\r
121 *buffer = const_cast<uint8_t*>(frame_->image_data().begin());
\r
124 if(key_data_.empty())
\r
126 key_data_.resize(frame_->image_data().size());
\r
127 fast_memshfl(key_data_.data(), frame_->image_data().begin(), frame_->image_data().size(), 0x0F0F0F0F, 0x0B0B0B0B, 0x07070707, 0x03030303);
\r
130 *buffer = key_data_.data();
\r
136 STDMETHOD(GetTimecode(BMDTimecodeFormat format, IDeckLinkTimecode** timecode)){return S_FALSE;}
\r
137 STDMETHOD(GetAncillaryData(IDeckLinkVideoFrameAncillary** ancillary)) {return S_FALSE;}
\r
140 struct decklink_consumer : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback, boost::noncopyable
\r
142 const int channel_index_;
\r
143 const configuration config_;
\r
145 CComPtr<IDeckLink> decklink_;
\r
146 CComQIPtr<IDeckLinkOutput> output_;
\r
147 CComQIPtr<IDeckLinkConfiguration> configuration_;
\r
148 CComQIPtr<IDeckLinkKeyer> keyer_;
\r
150 tbb::spin_mutex exception_mutex_;
\r
151 std::exception_ptr exception_;
\r
153 tbb::atomic<bool> is_running_;
\r
155 const std::wstring model_name_;
\r
156 const core::video_format_desc format_desc_;
\r
157 const size_t buffer_size_;
\r
159 long long video_scheduled_;
\r
160 long long audio_scheduled_;
\r
162 size_t preroll_count_;
\r
164 boost::circular_buffer<std::vector<int32_t>> audio_container_;
\r
166 tbb::concurrent_bounded_queue<std::shared_ptr<core::read_frame>> video_frame_buffer_;
\r
167 tbb::concurrent_bounded_queue<std::shared_ptr<core::read_frame>> audio_frame_buffer_;
\r
169 safe_ptr<diagnostics::graph> graph_;
\r
170 boost::timer tick_timer_;
\r
173 decklink_consumer(const configuration& config, const core::video_format_desc& format_desc, int channel_index)
\r
174 : channel_index_(channel_index)
\r
176 , decklink_(get_device(config.device_index))
\r
177 , output_(decklink_)
\r
178 , configuration_(decklink_)
\r
179 , keyer_(decklink_)
\r
180 , model_name_(get_model_name(decklink_))
\r
181 , format_desc_(format_desc)
\r
182 , buffer_size_(config.buffer_depth) // Minimum buffer-size 3.
\r
183 , video_scheduled_(0)
\r
184 , audio_scheduled_(0)
\r
185 , preroll_count_(0)
\r
186 , audio_container_(buffer_size_+1)
\r
188 is_running_ = true;
\r
190 video_frame_buffer_.set_capacity(1);
\r
191 audio_frame_buffer_.set_capacity(1);
\r
193 graph_->add_guide("tick-time", 0.5);
\r
194 graph_->set_color("tick-time", diagnostics::color(0.0f, 0.6f, 0.9f));
\r
195 graph_->set_color("late-frame", diagnostics::color(0.6f, 0.3f, 0.3f));
\r
196 graph_->set_color("dropped-frame", diagnostics::color(0.3f, 0.6f, 0.3f));
\r
197 graph_->set_color("flushed-frame", diagnostics::color(0.4f, 0.3f, 0.8f));
\r
198 graph_->set_color("buffered-audio", diagnostics::color(0.9f, 0.9f, 0.5f));
\r
199 graph_->set_color("buffered-video", diagnostics::color(0.2f, 0.9f, 0.9f));
\r
200 graph_->set_text(print());
\r
201 diagnostics::register_graph(graph_);
\r
203 enable_video(get_display_mode(output_, format_desc_.format, bmdFormat8BitBGRA, bmdVideoOutputFlagDefault));
\r
205 if(config.embedded_audio)
\r
208 set_latency(config.low_latency);
\r
209 set_keyer(config.internal_key);
\r
211 if(config.embedded_audio)
\r
212 output_->BeginAudioPreroll();
\r
214 for(size_t n = 0; n < buffer_size_; ++n)
\r
215 schedule_next_video(make_safe<core::read_frame>());
\r
217 if(!config.embedded_audio)
\r
221 ~decklink_consumer()
\r
223 is_running_ = false;
\r
224 video_frame_buffer_.try_push(std::make_shared<core::read_frame>());
\r
225 audio_frame_buffer_.try_push(std::make_shared<core::read_frame>());
\r
227 if(output_ != nullptr)
\r
229 output_->StopScheduledPlayback(0, nullptr, 0);
\r
230 if(config_.embedded_audio)
\r
231 output_->DisableAudioOutput();
\r
232 output_->DisableVideoOutput();
\r
236 void set_latency(bool low_latency)
\r
240 configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, false);
\r
241 CASPAR_LOG(info) << print() << L" Enabled normal-latency mode";
\r
245 configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, true);
\r
246 CASPAR_LOG(info) << print() << L" Enabled low-latency mode";
\r
250 void set_keyer(bool internal_key)
\r
254 if(FAILED(keyer_->Enable(FALSE)))
\r
255 CASPAR_LOG(error) << print() << L" Failed to enable internal keyer.";
\r
256 else if(FAILED(keyer_->SetLevel(255)))
\r
257 CASPAR_LOG(error) << print() << L" Failed to set key-level to max.";
\r
259 CASPAR_LOG(info) << print() << L" Enabled internal keyer.";
\r
263 if(FAILED(keyer_->Enable(TRUE)))
\r
264 CASPAR_LOG(error) << print() << L" Failed to enable external keyer.";
\r
265 else if(FAILED(keyer_->SetLevel(255)))
\r
266 CASPAR_LOG(error) << print() << L" Failed to set key-level to max.";
\r
268 CASPAR_LOG(info) << print() << L" Enabled external keyer.";
\r
272 void enable_audio()
\r
274 if(FAILED(output_->EnableAudioOutput(bmdAudioSampleRate48kHz, bmdAudioSampleType32bitInteger, 2, bmdAudioOutputStreamTimestamped)))
\r
275 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not enable audio output."));
\r
277 if(FAILED(output_->SetAudioCallback(this)))
\r
278 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not set audio callback."));
\r
280 CASPAR_LOG(info) << print() << L" Enabled embedded-audio.";
\r
283 void enable_video(BMDDisplayMode display_mode)
\r
285 if(FAILED(output_->EnableVideoOutput(display_mode, bmdVideoOutputFlagDefault)))
\r
286 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not enable video output."));
\r
288 if(FAILED(output_->SetScheduledFrameCompletionCallback(this)))
\r
289 BOOST_THROW_EXCEPTION(caspar_exception()
\r
290 << msg_info(narrow(print()) + " Failed to set playback completion callback.")
\r
291 << boost::errinfo_api_function("SetScheduledFrameCompletionCallback"));
\r
294 void start_playback()
\r
296 if(FAILED(output_->StartScheduledPlayback(0, format_desc_.time_scale, 1.0)))
\r
297 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Failed to schedule playback."));
\r
300 STDMETHOD (QueryInterface(REFIID, LPVOID*)) {return E_NOINTERFACE;}
\r
301 STDMETHOD_(ULONG, AddRef()) {return 1;}
\r
302 STDMETHOD_(ULONG, Release()) {return 1;}
\r
304 STDMETHOD(ScheduledPlaybackHasStopped())
\r
306 is_running_ = false;
\r
307 CASPAR_LOG(info) << print() << L" Scheduled playback has stopped.";
\r
311 STDMETHOD(ScheduledFrameCompleted(IDeckLinkVideoFrame* completed_frame, BMDOutputFrameCompletionResult result))
\r
318 if(result == bmdOutputFrameDisplayedLate)
\r
320 graph_->add_tag("late-frame");
\r
321 video_scheduled_ += format_desc_.duration;
\r
322 audio_scheduled_ += reinterpret_cast<decklink_frame*>(completed_frame)->audio_data().size()/format_desc_.audio_channels;
\r
323 //++video_scheduled_;
\r
324 //audio_scheduled_ += format_desc_.audio_cadence[0];
\r
325 //++audio_scheduled_;
\r
327 else if(result == bmdOutputFrameDropped)
\r
328 graph_->add_tag("dropped-frame");
\r
329 else if(result == bmdOutputFrameFlushed)
\r
330 graph_->add_tag("flushed-frame");
\r
332 std::shared_ptr<core::read_frame> frame;
\r
333 video_frame_buffer_.pop(frame);
\r
334 schedule_next_video(make_safe_ptr(frame));
\r
336 unsigned long buffered;
\r
337 output_->GetBufferedVideoFrameCount(&buffered);
\r
338 graph_->update_value("buffered-video", static_cast<double>(buffered)/format_desc_.fps);
\r
342 tbb::spin_mutex::scoped_lock lock(exception_mutex_);
\r
343 exception_ = std::current_exception();
\r
350 STDMETHOD(RenderAudioSamples(BOOL preroll))
\r
359 if(++preroll_count_ >= buffer_size_)
\r
361 output_->EndAudioPreroll();
\r
365 schedule_next_audio(core::audio_buffer(format_desc_.audio_cadence[preroll % format_desc_.audio_cadence.size()], 0));
\r
369 std::shared_ptr<core::read_frame> frame;
\r
370 audio_frame_buffer_.pop(frame);
\r
371 schedule_next_audio(frame->audio_data());
\r
374 unsigned long buffered;
\r
375 output_->GetBufferedAudioSampleFrameCount(&buffered);
\r
376 graph_->update_value("buffered-audio", static_cast<double>(buffered)/(format_desc_.audio_cadence[0]*2));
\r
380 tbb::spin_mutex::scoped_lock lock(exception_mutex_);
\r
381 exception_ = std::current_exception();
\r
388 template<typename T>
\r
389 void schedule_next_audio(const T& audio_data)
\r
391 const int sample_frame_count = audio_data.size()/format_desc_.audio_channels;
\r
393 audio_container_.push_back(std::vector<int32_t>(audio_data.begin(), audio_data.end()));
\r
395 if(FAILED(output_->ScheduleAudioSamples(audio_container_.back().data(), sample_frame_count, audio_scheduled_, format_desc_.audio_sample_rate, nullptr)))
\r
396 CASPAR_LOG(error) << print() << L" Failed to schedule audio.";
\r
398 audio_scheduled_ += sample_frame_count;
\r
401 void schedule_next_video(const safe_ptr<core::read_frame>& frame)
\r
403 CComPtr<IDeckLinkVideoFrame> frame2(new decklink_frame(frame, format_desc_, config_.key_only));
\r
404 if(FAILED(output_->ScheduleVideoFrame(frame2, video_scheduled_, format_desc_.duration, format_desc_.time_scale)))
\r
405 CASPAR_LOG(error) << print() << L" Failed to schedule video.";
\r
407 video_scheduled_ += format_desc_.duration;
\r
409 graph_->update_value("tick-time", tick_timer_.elapsed()*format_desc_.fps*0.5);
\r
410 tick_timer_.restart();
\r
413 void send(const safe_ptr<core::read_frame>& frame)
\r
416 tbb::spin_mutex::scoped_lock lock(exception_mutex_);
\r
417 if(exception_ != nullptr)
\r
418 std::rethrow_exception(exception_);
\r
422 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Is not running."));
\r
424 if(config_.embedded_audio)
\r
425 audio_frame_buffer_.push(frame);
\r
426 video_frame_buffer_.push(frame);
\r
429 std::wstring print() const
\r
431 return model_name_ + L" [" + boost::lexical_cast<std::wstring>(channel_index_) + L"|device " +
\r
432 boost::lexical_cast<std::wstring>(config_.device_index) + L"|" + format_desc_.name + L"]";
\r
436 struct decklink_consumer_proxy : public core::frame_consumer
\r
438 const configuration config_;
\r
439 com_context<decklink_consumer> context_;
\r
440 std::vector<size_t> audio_cadence_;
\r
443 decklink_consumer_proxy(const configuration& config)
\r
445 , context_(L"decklink_consumer[" + boost::lexical_cast<std::wstring>(config.device_index) + L"]")
\r
449 ~decklink_consumer_proxy()
\r
453 auto str = print();
\r
455 CASPAR_LOG(info) << str << L" Successfully Uninitialized.";
\r
461 virtual void initialize(const core::video_format_desc& format_desc, int channel_index) override
\r
463 context_.reset([&]{return new decklink_consumer(config_, format_desc, channel_index);});
\r
464 audio_cadence_ = format_desc.audio_cadence;
\r
466 CASPAR_LOG(info) << print() << L" Successfully Initialized.";
\r
469 virtual bool send(const safe_ptr<core::read_frame>& frame) override
\r
471 CASPAR_VERIFY(audio_cadence_.front() == static_cast<size_t>(frame->audio_data().size()));
\r
472 boost::range::rotate(audio_cadence_, std::begin(audio_cadence_)+1);
\r
474 context_->send(frame);
\r
478 virtual std::wstring print() const override
\r
480 return context_ ? context_->print() : L"[decklink_consumer]";
\r
483 virtual boost::property_tree::wptree info() const override
\r
485 boost::property_tree::wptree info;
\r
486 info.add(L"type", L"decklink-consumer");
\r
487 info.add(L"key-only", config_.key_only);
\r
488 info.add(L"device", config_.device_index);
\r
489 info.add(L"low-latency", config_.low_latency);
\r
490 info.add(L"embedded-audio", config_.embedded_audio);
\r
491 info.add(L"low-latency", config_.low_latency);
\r
492 info.add(L"internal-key", config_.internal_key);
\r
496 virtual size_t buffer_depth() const override
\r
498 return config_.buffer_depth;
\r
501 virtual int index() const override
\r
503 return 300 + config_.device_index;
\r
507 safe_ptr<core::frame_consumer> create_consumer(const std::vector<std::wstring>& params)
\r
509 if(params.size() < 1 || params[0] != L"DECKLINK")
\r
510 return core::frame_consumer::empty();
\r
512 configuration config;
\r
514 if(params.size() > 1)
\r
515 config.device_index = lexical_cast_or_default<int>(params[1], config.device_index);
\r
517 config.internal_key = std::find(params.begin(), params.end(), L"INTERNAL_KEY") != params.end();
\r
518 config.low_latency = std::find(params.begin(), params.end(), L"LOW_LATENCY") != params.end();
\r
519 config.embedded_audio = std::find(params.begin(), params.end(), L"EMBEDDED_AUDIO") != params.end();
\r
520 config.key_only = std::find(params.begin(), params.end(), L"KEY_ONLY") != params.end();
\r
522 return make_safe<decklink_consumer_proxy>(config);
\r
525 safe_ptr<core::frame_consumer> create_consumer(const boost::property_tree::wptree& ptree)
\r
527 configuration config;
\r
529 config.internal_key = ptree.get(L"internal-key", config.internal_key);
\r
530 config.low_latency = ptree.get(L"low-latency", config.low_latency);
\r
531 config.key_only = ptree.get(L"key-only", config.key_only);
\r
532 config.device_index = ptree.get(L"device", config.device_index);
\r
533 config.embedded_audio = ptree.get(L"embedded-audio", config.embedded_audio);
\r
534 config.base_buffer_depth = ptree.get(L"buffer-depth", config.base_buffer_depth);
\r
536 return make_safe<decklink_consumer_proxy>(config);
\r
542 ##############################################################################
\r
548 BMD Developer Support
\r
549 developer@blackmagic-design.com
\r
551 -----------------------------------------------------------------------------
\r
553 Thanks for your inquiry. The minimum number of frames that you can preroll
\r
554 for scheduled playback is three frames for video and four frames for audio.
\r
555 As you mentioned if you preroll less frames then playback will not start or
\r
556 playback will be very sporadic. From our experience with Media Express, we
\r
557 recommended that at least seven frames are prerolled for smooth playback.
\r
559 Regarding the bmdDeckLinkConfigLowLatencyVideoOutput flag:
\r
560 There can be around 3 frames worth of latency on scheduled output.
\r
561 When the bmdDeckLinkConfigLowLatencyVideoOutput flag is used this latency is
\r
562 reduced or removed for scheduled playback. If the DisplayVideoFrameSync()
\r
563 method is used, the bmdDeckLinkConfigLowLatencyVideoOutput setting will
\r
564 guarantee that the provided frame will be output as soon the previous
\r
565 frame output has been completed.
\r
566 ################################################################################
\r
570 ##############################################################################
\r
571 Async DMA Transfer without redundant copying
\r
576 BMD Developer Support
\r
577 developer@blackmagic-design.com
\r
579 -----------------------------------------------------------------------------
\r
581 Thanks for your inquiry. You could try subclassing IDeckLinkMutableVideoFrame
\r
582 and providing a pointer to your video buffer when GetBytes() is called.
\r
583 This may help to keep copying to a minimum. Please ensure that the pixel
\r
584 format is in bmdFormat10BitYUV, otherwise the DeckLink API / driver will
\r
585 have to colourspace convert which may result in additional copying.
\r
586 ################################################################################
\r