]> git.sesse.net Git - casparcg/blob - modules/decklink/consumer/decklink_consumer.cpp
d0949fd807a1ef4b43e5aa8650a32e0ca2ddd70c
[casparcg] / modules / decklink / consumer / decklink_consumer.cpp
1 /*\r
2 * copyright (c) 2010 Sveriges Television AB <info@casparcg.com>\r
3 *\r
4 *  This file is part of CasparCG.\r
5 *\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
10 *\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
15 \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
18 *\r
19 */\r
20 \r
21 #include "../StdAfx.h"\r
22  \r
23 #include "decklink_consumer.h"\r
24 \r
25 #include "../util/util.h"\r
26 \r
27 #include "../interop/DeckLinkAPI_h.h"\r
28 \r
29 #include <core/mixer/read_frame.h>\r
30 \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
37 \r
38 #include <core/consumer/frame_consumer.h>\r
39 \r
40 #include <tbb/concurrent_queue.h>\r
41 #include <tbb/cache_aligned_allocator.h>\r
42 \r
43 #include <boost/circular_buffer.hpp>\r
44 #include <boost/timer.hpp>\r
45 \r
46 namespace caspar { \r
47         \r
48 struct configuration\r
49 {\r
50         size_t  device_index;\r
51         bool    embedded_audio;\r
52         bool    internal_key;\r
53         bool    low_latency;\r
54         bool    key_only;\r
55         size_t  buffer_depth;\r
56         \r
57         configuration()\r
58                 : device_index(1)\r
59                 , embedded_audio(false)\r
60                 , internal_key(false)\r
61                 , low_latency(false)\r
62                 , key_only(false)\r
63                 , buffer_depth(core::consumer_buffer_depth()){}\r
64 };\r
65 \r
66 class decklink_frame : public IDeckLinkVideoFrame\r
67 {\r
68         tbb::atomic<int>                                                                                        ref_count_;\r
69         std::shared_ptr<core::read_frame>                                                       frame_;\r
70         const core::video_format_desc                                                           format_desc_;\r
71 \r
72         bool                                                                                                            key_only_;\r
73         std::vector<uint8_t, tbb::cache_aligned_allocator<uint8_t>> key_data_;\r
74 public:\r
75         decklink_frame(const safe_ptr<core::read_frame>& frame, const core::video_format_desc& format_desc, bool key_only)\r
76                 : frame_(frame)\r
77                 , format_desc_(format_desc)\r
78                 , key_only_(key_only)\r
79         {\r
80                 ref_count_ = 0;\r
81         }\r
82         \r
83         STDMETHOD (QueryInterface(REFIID, LPVOID*))             {return E_NOINTERFACE;}\r
84         STDMETHOD_(ULONG,                       AddRef())                       \r
85         {\r
86                 return ++ref_count_;\r
87         }\r
88         STDMETHOD_(ULONG,                       Release())                      \r
89         {\r
90                 --ref_count_;\r
91                 if(ref_count_ == 0)\r
92                         delete this;\r
93                 return ref_count_;\r
94         }\r
95 \r
96         STDMETHOD_(long,                        GetWidth())                     {return format_desc_.width;}        \r
97     STDMETHOD_(long,                    GetHeight())            {return format_desc_.height;}        \r
98     STDMETHOD_(long,                    GetRowBytes())          {return format_desc_.width*4;}        \r
99         STDMETHOD_(BMDPixelFormat,      GetPixelFormat())       {return bmdFormat8BitBGRA;}        \r
100     STDMETHOD_(BMDFrameFlags,   GetFlags())                     {return bmdFrameFlagDefault;}\r
101         \r
102     STDMETHOD(GetBytes(void** buffer))\r
103         {\r
104                 static std::vector<uint8_t> zeros(1920*1080*4, 0);\r
105                 if(static_cast<size_t>(frame_->image_data().size()) != format_desc_.size)\r
106                 {\r
107                         *buffer = zeros.data();\r
108                         return S_OK;\r
109                 }\r
110 \r
111                 if(!key_only_)\r
112                         *buffer = const_cast<uint8_t*>(frame_->image_data().begin());\r
113                 else\r
114                 {\r
115                         if(key_data_.empty())\r
116                         {\r
117                                 key_data_.resize(frame_->image_data().size());\r
118                                 fast_memshfl(key_data_.data(), frame_->image_data().begin(), frame_->image_data().size(), 0x0F0F0F0F, 0x0B0B0B0B, 0x07070707, 0x03030303);\r
119                                 frame_.reset();\r
120                         }\r
121                         *buffer = key_data_.data();\r
122                 }\r
123 \r
124                 return S_OK;\r
125         }\r
126         \r
127     STDMETHOD(GetTimecode(BMDTimecodeFormat format, IDeckLinkTimecode** timecode)){return S_FALSE;}        \r
128     STDMETHOD(GetAncillaryData(IDeckLinkVideoFrameAncillary** ancillary))                 {return S_FALSE;}\r
129 };\r
130 \r
131 struct decklink_consumer : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback, boost::noncopyable\r
132 {               \r
133         const configuration                                     config_;\r
134 \r
135         CComPtr<IDeckLink>                                      decklink_;\r
136         CComQIPtr<IDeckLinkOutput>                      output_;\r
137         CComQIPtr<IDeckLinkConfiguration>       configuration_;\r
138         CComQIPtr<IDeckLinkKeyer>                       keyer_;\r
139 \r
140         tbb::spin_mutex                                         exception_mutex_;\r
141         std::exception_ptr                                      exception_;\r
142 \r
143         tbb::atomic<bool>                                       is_running_;\r
144                 \r
145         const std::wstring                                      model_name_;\r
146         const core::video_format_desc           format_desc_;\r
147         const size_t                                            buffer_size_;\r
148 \r
149         long long                                                       frames_scheduled_;\r
150         long long                                                       audio_scheduled_;\r
151 \r
152         size_t                                                          preroll_count_;\r
153                 \r
154         boost::circular_buffer<std::vector<int16_t>>    audio_container_;\r
155 \r
156         tbb::concurrent_bounded_queue<std::shared_ptr<core::read_frame>> video_frame_buffer_;\r
157         tbb::concurrent_bounded_queue<std::shared_ptr<core::read_frame>> audio_frame_buffer_;\r
158         \r
159         std::shared_ptr<diagnostics::graph> graph_;\r
160         boost::timer tick_timer_;\r
161 \r
162 public:\r
163         decklink_consumer(const configuration& config, const core::video_format_desc& format_desc) \r
164                 : config_(config)\r
165                 , decklink_(get_device(config.device_index))\r
166                 , output_(decklink_)\r
167                 , configuration_(decklink_)\r
168                 , keyer_(decklink_)\r
169                 , model_name_(get_model_name(decklink_))\r
170                 , format_desc_(format_desc)\r
171                 , buffer_size_(config.embedded_audio ? config.buffer_depth + 1 : config.buffer_depth) // Minimum buffer-size 3.\r
172                 , frames_scheduled_(0)\r
173                 , audio_scheduled_(0)\r
174                 , preroll_count_(0)\r
175                 , audio_container_(buffer_size_+1)\r
176         {\r
177                 is_running_ = true;\r
178                                 \r
179                 video_frame_buffer_.set_capacity(1);\r
180                 audio_frame_buffer_.set_capacity(1);\r
181 \r
182                 graph_ = diagnostics::create_graph(narrow(print()));\r
183                 graph_->add_guide("tick-time", 0.5);\r
184                 graph_->set_color("tick-time", diagnostics::color(0.0f, 0.6f, 0.9f));   \r
185                 graph_->set_color("late-frame", diagnostics::color(0.6f, 0.3f, 0.3f));\r
186                 graph_->set_color("dropped-frame", diagnostics::color(0.3f, 0.6f, 0.3f));\r
187                 graph_->set_color("flushed-frame", diagnostics::color(0.4f, 0.3f, 0.8f));\r
188                 \r
189                 enable_video(get_display_mode(output_, format_desc_.format, bmdFormat8BitBGRA, bmdVideoOutputFlagDefault));\r
190                                 \r
191                 if(config.embedded_audio)\r
192                         enable_audio();\r
193 \r
194                 set_latency(config.low_latency);                                \r
195                 set_keyer(config.internal_key);\r
196                                 \r
197                 if(config.embedded_audio)               \r
198                         output_->BeginAudioPreroll();           \r
199                 \r
200                 for(size_t n = 0; n < buffer_size_; ++n)\r
201                         schedule_next_video(make_safe<core::read_frame>());\r
202 \r
203                 if(!config.embedded_audio)\r
204                         start_playback();\r
205         }\r
206 \r
207         ~decklink_consumer()\r
208         {               \r
209                 is_running_ = false;\r
210                 video_frame_buffer_.try_push(std::make_shared<core::read_frame>());\r
211                 audio_frame_buffer_.try_push(std::make_shared<core::read_frame>());\r
212 \r
213                 if(output_ != nullptr) \r
214                 {\r
215                         output_->StopScheduledPlayback(0, nullptr, 0);\r
216                         if(config_.embedded_audio)\r
217                                 output_->DisableAudioOutput();\r
218                         output_->DisableVideoOutput();\r
219                 }\r
220         }\r
221                         \r
222         const core::video_format_desc& get_video_format_desc() const\r
223         {\r
224                 return format_desc_;\r
225         }\r
226 \r
227         void set_latency(bool low_latency)\r
228         {               \r
229                 if(!low_latency)\r
230                 {\r
231                         configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, false);\r
232                         CASPAR_LOG(info) << print() << L" Enabled normal-latency mode";\r
233                 }\r
234                 else\r
235                 {                       \r
236                         configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, true);\r
237                         CASPAR_LOG(info) << print() << L" Enabled low-latency mode";\r
238                 }\r
239         }\r
240 \r
241         void set_keyer(bool internal_key)\r
242         {\r
243                 if(internal_key) \r
244                 {\r
245                         if(FAILED(keyer_->Enable(FALSE)))                       \r
246                                 CASPAR_LOG(error) << print() << L" Failed to enable internal keyer.";                   \r
247                         else if(FAILED(keyer_->SetLevel(255)))                  \r
248                                 CASPAR_LOG(error) << print() << L" Failed to set key-level to max.";\r
249                         else\r
250                                 CASPAR_LOG(info) << print() << L" Enabled internal keyer.";             \r
251                 }\r
252                 else\r
253                 {\r
254                         if(FAILED(keyer_->Enable(TRUE)))                        \r
255                                 CASPAR_LOG(error) << print() << L" Failed to enable external keyer.";   \r
256                         else if(FAILED(keyer_->SetLevel(255)))                  \r
257                                 CASPAR_LOG(error) << print() << L" Failed to set key-level to max.";\r
258                         else\r
259                                 CASPAR_LOG(info) << print() << L" Enabled external keyer.";                     \r
260                 }\r
261         }\r
262         \r
263         void enable_audio()\r
264         {\r
265                 if(FAILED(output_->EnableAudioOutput(bmdAudioSampleRate48kHz, bmdAudioSampleType16bitInteger, 2, bmdAudioOutputStreamTimestamped)))\r
266                                 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not enable audio output."));\r
267                                 \r
268                 if(FAILED(output_->SetAudioCallback(this)))\r
269                         BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not set audio callback."));\r
270 \r
271                 CASPAR_LOG(info) << print() << L" Enabled embedded-audio.";\r
272         }\r
273 \r
274         void enable_video(BMDDisplayMode display_mode)\r
275         {\r
276                 if(FAILED(output_->EnableVideoOutput(display_mode, bmdVideoOutputFlagDefault))) \r
277                         BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not enable video output."));\r
278                 \r
279                 if(FAILED(output_->SetScheduledFrameCompletionCallback(this)))\r
280                         BOOST_THROW_EXCEPTION(caspar_exception() \r
281                                                                         << msg_info(narrow(print()) + " Failed to set playback completion callback.")\r
282                                                                         << boost::errinfo_api_function("SetScheduledFrameCompletionCallback"));\r
283         }\r
284 \r
285         void start_playback()\r
286         {\r
287                 if(FAILED(output_->StartScheduledPlayback(0, format_desc_.time_scale, 1.0))) \r
288                         BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Failed to schedule playback."));\r
289         }\r
290         \r
291         STDMETHOD (QueryInterface(REFIID, LPVOID*))     {return E_NOINTERFACE;}\r
292         STDMETHOD_(ULONG, AddRef())                                     {return 1;}\r
293         STDMETHOD_(ULONG, Release())                            {return 1;}\r
294         \r
295         STDMETHOD(ScheduledPlaybackHasStopped())\r
296         {\r
297                 is_running_ = false;\r
298                 CASPAR_LOG(info) << print() << L" Scheduled playback has stopped.";\r
299                 return S_OK;\r
300         }\r
301 \r
302         STDMETHOD(ScheduledFrameCompleted(IDeckLinkVideoFrame* completed_frame, BMDOutputFrameCompletionResult result))\r
303         {\r
304                 if(!is_running_)\r
305                         return E_FAIL;\r
306                 \r
307                 try\r
308                 {\r
309                         if(result == bmdOutputFrameDisplayedLate)\r
310                         {\r
311                                 graph_->add_tag("late-frame");\r
312                                 ++frames_scheduled_;\r
313                                 ++audio_scheduled_;\r
314                         }\r
315                         else if(result == bmdOutputFrameDropped)\r
316                                 graph_->add_tag("dropped-frame");\r
317                         else if(result == bmdOutputFrameFlushed)\r
318                                 graph_->add_tag("flushed-frame");\r
319 \r
320                         std::shared_ptr<core::read_frame> frame;        \r
321                         video_frame_buffer_.pop(frame);                                 \r
322                         schedule_next_video(make_safe(frame));  \r
323                 }\r
324                 catch(...)\r
325                 {\r
326                         tbb::spin_mutex::scoped_lock lock(exception_mutex_);\r
327                         exception_ = std::current_exception();\r
328                         return E_FAIL;\r
329                 }\r
330 \r
331                 return S_OK;\r
332         }\r
333                 \r
334         STDMETHOD(RenderAudioSamples(BOOL preroll))\r
335         {\r
336                 if(!is_running_)\r
337                         return E_FAIL;\r
338                 \r
339                 try\r
340                 {       \r
341                         if(preroll)\r
342                         {\r
343                                 if(++preroll_count_ >= buffer_size_)\r
344                                 {\r
345                                         output_->EndAudioPreroll();\r
346                                         start_playback();                               \r
347                                 }\r
348                                 else\r
349                                         schedule_next_audio(make_safe<core::read_frame>());     \r
350                         }\r
351                         else\r
352                         {\r
353                                 std::shared_ptr<core::read_frame> frame;\r
354                                 audio_frame_buffer_.pop(frame);\r
355                                 schedule_next_audio(make_safe(frame));  \r
356                         }\r
357                 }\r
358                 catch(...)\r
359                 {\r
360                         tbb::spin_mutex::scoped_lock lock(exception_mutex_);\r
361                         exception_ = std::current_exception();\r
362                         return E_FAIL;\r
363                 }\r
364 \r
365                 return S_OK;\r
366         }\r
367 \r
368         void schedule_next_audio(const safe_ptr<core::read_frame>& frame)\r
369         {\r
370                 const int sample_frame_count = frame->audio_data().size()/format_desc_.audio_channels;\r
371 \r
372                 audio_container_.push_back(std::vector<int16_t>(frame->audio_data().begin(), frame->audio_data().end()));\r
373 \r
374                 if(FAILED(output_->ScheduleAudioSamples(audio_container_.back().data(), sample_frame_count, (audio_scheduled_++) * sample_frame_count, format_desc_.audio_sample_rate, nullptr)))\r
375                         CASPAR_LOG(error) << print() << L" Failed to schedule audio.";\r
376         }\r
377                         \r
378         void schedule_next_video(const safe_ptr<core::read_frame>& frame)\r
379         {\r
380                 CComPtr<IDeckLinkVideoFrame> frame2(new decklink_frame(frame, format_desc_, config_.key_only));\r
381                 if(FAILED(output_->ScheduleVideoFrame(frame2, (frames_scheduled_++) * format_desc_.duration, format_desc_.duration, format_desc_.time_scale)))\r
382                         CASPAR_LOG(error) << print() << L" Failed to schedule video.";\r
383 \r
384                 graph_->update_value("tick-time", tick_timer_.elapsed()*format_desc_.fps*0.5);\r
385                 tick_timer_.restart();\r
386         }\r
387 \r
388         void send(const safe_ptr<core::read_frame>& frame)\r
389         {\r
390                 {\r
391                         tbb::spin_mutex::scoped_lock lock(exception_mutex_);\r
392                         if(exception_ != nullptr)\r
393                                 std::rethrow_exception(exception_);\r
394                 }\r
395 \r
396                 if(!is_running_)\r
397                         BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Is not running."));\r
398                 \r
399                 if(config_.embedded_audio)\r
400                         audio_frame_buffer_.push(frame);        \r
401                 video_frame_buffer_.push(frame);        \r
402         }\r
403         \r
404         std::wstring print() const\r
405         {\r
406                 return model_name_ + L" [" + boost::lexical_cast<std::wstring>(config_.device_index) + L"|" +  format_desc_.name + L"]";\r
407         }\r
408 };\r
409 \r
410 struct decklink_consumer_proxy : public core::frame_consumer\r
411 {\r
412         const configuration                             config_;\r
413         com_context<decklink_consumer>  context_;\r
414         core::video_format_desc                 format_desc_;\r
415         size_t                                                  fail_count_;\r
416 public:\r
417 \r
418         decklink_consumer_proxy(const configuration& config)\r
419                 : config_(config)\r
420                 , context_(L"decklink_consumer[" + boost::lexical_cast<std::wstring>(config.device_index) + L"]")\r
421                 , fail_count_(0)\r
422         {\r
423         }\r
424 \r
425         ~decklink_consumer_proxy()\r
426         {\r
427                 auto str = print();\r
428                 context_.reset();\r
429                 CASPAR_LOG(info) << str << L" Successfully Uninitialized.";     \r
430         }\r
431         \r
432         virtual void initialize(const core::video_format_desc& format_desc)\r
433         {\r
434                 format_desc_ = format_desc;\r
435                 context_.reset([&]{return new decklink_consumer(config_, format_desc_);});              \r
436                                 \r
437                 CASPAR_LOG(info) << print() << L" Successfully Initialized.";   \r
438         }\r
439         \r
440         virtual bool send(const safe_ptr<core::read_frame>& frame)\r
441         {\r
442                 if(!context_)\r
443                         context_.reset([&]{return new decklink_consumer(config_, format_desc_);});\r
444 \r
445                 try\r
446                 {\r
447                         context_->send(frame);\r
448                         fail_count_ = 0;\r
449                 }\r
450                 catch(...)\r
451                 {\r
452                         context_.reset();\r
453 \r
454                         if(fail_count_++ > 3)\r
455                                 return false;  // Outside didn't handle exception properly, just give up.\r
456                         \r
457                         throw;\r
458                 }\r
459 \r
460                 return true;\r
461         }\r
462         \r
463         virtual std::wstring print() const\r
464         {\r
465                 return context_ ? context_->print() : L"decklink_consumer";\r
466         }\r
467                         \r
468         virtual const core::video_format_desc& get_video_format_desc() const\r
469         {\r
470                 return format_desc_;\r
471         }\r
472 };      \r
473 \r
474 safe_ptr<core::frame_consumer> create_decklink_consumer(const std::vector<std::wstring>& params) \r
475 {\r
476         if(params.size() < 1 || params[0] != L"DECKLINK")\r
477                 return core::frame_consumer::empty();\r
478         \r
479         configuration config;\r
480                 \r
481         if(params.size() > 1)\r
482                 config.device_index = lexical_cast_or_default<int>(params[1], config.device_index);\r
483         \r
484         config.internal_key             = std::find(params.begin(), params.end(), L"INTERNAL_KEY")       != params.end();\r
485         config.low_latency              = std::find(params.begin(), params.end(), L"LOW_LATENCY")        != params.end();\r
486         config.embedded_audio   = std::find(params.begin(), params.end(), L"EMBEDDED_AUDIO") != params.end();\r
487         config.key_only                 = std::find(params.begin(), params.end(), L"KEY_ONLY")           != params.end();\r
488 \r
489         return make_safe<decklink_consumer_proxy>(config);\r
490 }\r
491 \r
492 safe_ptr<core::frame_consumer> create_decklink_consumer(const boost::property_tree::ptree& ptree) \r
493 {\r
494         configuration config;\r
495 \r
496         config.internal_key             = ptree.get("internal-key",       config.internal_key);\r
497         config.low_latency              = ptree.get("low-latency",        config.low_latency);\r
498         config.key_only                 = ptree.get("key-only",           config.key_only);\r
499         config.device_index             = ptree.get("device",             config.device_index);\r
500         config.embedded_audio   = ptree.get("embedded-audio", config.embedded_audio);\r
501 \r
502         return make_safe<decklink_consumer_proxy>(config);\r
503 }\r
504 \r
505 }\r
506 \r
507 /*\r
508 ##############################################################################\r
509 Pre-rolling\r
510 \r
511 Mail: 2011-05-09\r
512 \r
513 Yoshan\r
514 BMD Developer Support\r
515 developer@blackmagic-design.com\r
516 \r
517 -----------------------------------------------------------------------------\r
518 \r
519 Thanks for your inquiry. The minimum number of frames that you can preroll \r
520 for scheduled playback is three frames for video and four frames for audio. \r
521 As you mentioned if you preroll less frames then playback will not start or\r
522 playback will be very sporadic. From our experience with Media Express, we \r
523 recommended that at least seven frames are prerolled for smooth playback. \r
524 \r
525 Regarding the bmdDeckLinkConfigLowLatencyVideoOutput flag:\r
526 There can be around 3 frames worth of latency on scheduled output.\r
527 When the bmdDeckLinkConfigLowLatencyVideoOutput flag is used this latency is\r
528 reduced  or removed for scheduled playback. If the DisplayVideoFrameSync() \r
529 method is used, the bmdDeckLinkConfigLowLatencyVideoOutput setting will \r
530 guarantee that the provided frame will be output as soon the previous \r
531 frame output has been completed.\r
532 ################################################################################\r
533 */\r
534 \r
535 /*\r
536 ##############################################################################\r
537 Async DMA Transfer without redundant copying\r
538 \r
539 Mail: 2011-05-10\r
540 \r
541 Yoshan\r
542 BMD Developer Support\r
543 developer@blackmagic-design.com\r
544 \r
545 -----------------------------------------------------------------------------\r
546 \r
547 Thanks for your inquiry. You could try subclassing IDeckLinkMutableVideoFrame \r
548 and providing a pointer to your video buffer when GetBytes() is called. \r
549 This may help to keep copying to a minimum. Please ensure that the pixel \r
550 format is in bmdFormat10BitYUV, otherwise the DeckLink API / driver will \r
551 have to colourspace convert which may result in additional copying.\r
552 ################################################################################\r
553 */