]> git.sesse.net Git - casparcg/blob - modules/decklink/consumer/decklink_consumer.cpp
601d875175c1a61da4864983787a1f73d062de1b
[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 \r
42 #include <boost/circular_buffer.hpp>\r
43 #include <boost/timer.hpp>\r
44 \r
45 namespace caspar { \r
46         \r
47 struct configuration\r
48 {\r
49         size_t  device_index;\r
50         bool    embedded_audio;\r
51         bool    internal_key;\r
52         bool    low_latency;\r
53         bool    key_only;\r
54         size_t  buffer_depth;\r
55         \r
56         configuration()\r
57                 : device_index(1)\r
58                 , embedded_audio(false)\r
59                 , internal_key(false)\r
60                 , low_latency(false)\r
61                 , key_only(false)\r
62                 , buffer_depth(core::consumer_buffer_depth()){}\r
63 };\r
64 \r
65 class decklink_frame : public IDeckLinkVideoFrame\r
66 {\r
67         const safe_ptr<core::read_frame>        frame_;\r
68         const core::video_format_desc           format_desc_;\r
69 public:\r
70         decklink_frame(const safe_ptr<core::read_frame>& frame, const core::video_format_desc& format_desc)\r
71                 : frame_(frame)\r
72                 , format_desc_(format_desc){}\r
73         \r
74         STDMETHOD (QueryInterface(REFIID, LPVOID*))     {return E_NOINTERFACE;}\r
75         STDMETHOD_(ULONG, AddRef())                                     {return 1;}\r
76         STDMETHOD_(ULONG, Release())                            {return 1;}\r
77 \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
83         \r
84     STDMETHOD(GetBytes(void** buffer))\r
85         {\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
90                 return S_OK;\r
91         }\r
92         \r
93     STDMETHOD(GetTimecode(BMDTimecodeFormat format, IDeckLinkTimecode** timecode)){return S_FALSE;}        \r
94     STDMETHOD(GetAncillaryData(IDeckLinkVideoFrameAncillary** ancillary))                 {return S_FALSE;}\r
95 };\r
96 \r
97 struct decklink_consumer : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback, boost::noncopyable\r
98 {               \r
99         const configuration config_;\r
100 \r
101         CComPtr<IDeckLink>                                      decklink_;\r
102         CComQIPtr<IDeckLinkOutput>                      output_;\r
103         CComQIPtr<IDeckLinkConfiguration>       configuration_;\r
104         CComQIPtr<IDeckLinkKeyer>                       keyer_;\r
105 \r
106         tbb::spin_mutex                                         exception_mutex_;\r
107         std::exception_ptr                                      exception_;\r
108 \r
109         tbb::atomic<bool>                                       is_running_;\r
110                 \r
111         const std::wstring                                      model_name_;\r
112         const core::video_format_desc           format_desc_;\r
113         const size_t                                            buffer_size_;\r
114 \r
115         long long                                                       frames_scheduled_;\r
116         long long                                                       audio_scheduled_;\r
117 \r
118         size_t                                                          preroll_count_;\r
119                 \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
122 \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
125         \r
126         std::shared_ptr<diagnostics::graph> graph_;\r
127         boost::timer tick_timer_;\r
128 \r
129 public:\r
130         decklink_consumer(const configuration& config, const core::video_format_desc& format_desc) \r
131                 : config_(config)\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
143         {\r
144                 is_running_ = true;\r
145                                 \r
146                 video_frame_buffer_.set_capacity(1);\r
147                 audio_frame_buffer_.set_capacity(1);\r
148 \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
155                 \r
156                 enable_video(get_display_mode(output_, format_desc_.format, bmdFormat8BitBGRA, bmdVideoOutputFlagDefault));\r
157                                 \r
158                 if(config.embedded_audio)\r
159                         enable_audio();\r
160 \r
161                 set_latency(config.low_latency);                                \r
162                 set_keyer(config.internal_key);\r
163                                 \r
164                 if(config.embedded_audio)               \r
165                         output_->BeginAudioPreroll();           \r
166                 \r
167                 for(size_t n = 0; n < buffer_size_; ++n)\r
168                         schedule_next_video(make_safe<core::read_frame>());\r
169 \r
170                 if(!config.embedded_audio)\r
171                         start_playback();\r
172         }\r
173 \r
174         ~decklink_consumer()\r
175         {               \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
179 \r
180                 if(output_ != nullptr) \r
181                 {\r
182                         output_->StopScheduledPlayback(0, nullptr, 0);\r
183                         if(config_.embedded_audio)\r
184                                 output_->DisableAudioOutput();\r
185                         output_->DisableVideoOutput();\r
186                 }\r
187         }\r
188                         \r
189         const core::video_format_desc& get_video_format_desc() const\r
190         {\r
191                 return format_desc_;\r
192         }\r
193 \r
194         void set_latency(bool low_latency)\r
195         {               \r
196                 if(!low_latency)\r
197                 {\r
198                         configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, false);\r
199                         CASPAR_LOG(info) << print() << L" Enabled normal-latency mode";\r
200                 }\r
201                 else\r
202                 {                       \r
203                         configuration_->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, true);\r
204                         CASPAR_LOG(info) << print() << L" Enabled low-latency mode";\r
205                 }\r
206         }\r
207 \r
208         void set_keyer(bool internal_key)\r
209         {\r
210                 if(internal_key) \r
211                 {\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
216                         else\r
217                                 CASPAR_LOG(info) << print() << L" Enabled internal keyer.";             \r
218                 }\r
219                 else\r
220                 {\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
225                         else\r
226                                 CASPAR_LOG(info) << print() << L" Enabled external keyer.";                     \r
227                 }\r
228         }\r
229         \r
230         void enable_audio()\r
231         {\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
234                                 \r
235                 if(FAILED(output_->SetAudioCallback(this)))\r
236                         BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not set audio callback."));\r
237 \r
238                 CASPAR_LOG(info) << print() << L" Enabled embedded-audio.";\r
239         }\r
240 \r
241         void enable_video(BMDDisplayMode display_mode)\r
242         {\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
245                 \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
250         }\r
251 \r
252         void start_playback()\r
253         {\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
256         }\r
257         \r
258         STDMETHOD (QueryInterface(REFIID, LPVOID*))     {return E_NOINTERFACE;}\r
259         STDMETHOD_(ULONG, AddRef())                                     {return 1;}\r
260         STDMETHOD_(ULONG, Release())                            {return 1;}\r
261         \r
262         STDMETHOD(ScheduledPlaybackHasStopped())\r
263         {\r
264                 is_running_ = false;\r
265                 CASPAR_LOG(info) << print() << L" Scheduled playback has stopped.";\r
266                 return S_OK;\r
267         }\r
268 \r
269         STDMETHOD(ScheduledFrameCompleted(IDeckLinkVideoFrame* completed_frame, BMDOutputFrameCompletionResult result))\r
270         {\r
271                 if(!is_running_)\r
272                         return E_FAIL;\r
273                 \r
274                 try\r
275                 {\r
276                         if(result == bmdOutputFrameDisplayedLate)\r
277                         {\r
278                                 graph_->add_tag("late-frame");\r
279                                 ++frames_scheduled_;\r
280                                 ++audio_scheduled_;\r
281                         }\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
286 \r
287                         frame_container_.erase(std::find_if(frame_container_.begin(), frame_container_.end(), [&](const std::shared_ptr<IDeckLinkVideoFrame>& frame)\r
288                         {\r
289                                 return frame.get() == completed_frame;\r
290                         }));\r
291 \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
295                 }\r
296                 catch(...)\r
297                 {\r
298                         tbb::spin_mutex::scoped_lock lock(exception_mutex_);\r
299                         exception_ = std::current_exception();\r
300                         return E_FAIL;\r
301                 }\r
302 \r
303                 return S_OK;\r
304         }\r
305                 \r
306         STDMETHOD(RenderAudioSamples(BOOL preroll))\r
307         {\r
308                 if(!is_running_)\r
309                         return E_FAIL;\r
310                 \r
311                 try\r
312                 {       \r
313                         if(preroll)\r
314                         {\r
315                                 if(++preroll_count_ >= buffer_size_)\r
316                                 {\r
317                                         output_->EndAudioPreroll();\r
318                                         start_playback();                               \r
319                                 }\r
320                                 else\r
321                                         schedule_next_audio(make_safe<core::read_frame>());     \r
322                         }\r
323                         else\r
324                         {\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
328                         }\r
329                 }\r
330                 catch(...)\r
331                 {\r
332                         tbb::spin_mutex::scoped_lock lock(exception_mutex_);\r
333                         exception_ = std::current_exception();\r
334                         return E_FAIL;\r
335                 }\r
336 \r
337                 return S_OK;\r
338         }\r
339 \r
340         void schedule_next_audio(const safe_ptr<core::read_frame>& frame)\r
341         {\r
342                 const int sample_frame_count = frame->audio_data().size()/format_desc_.audio_channels;\r
343 \r
344                 audio_container_.push_back(std::vector<int16_t>(frame->audio_data().begin(), frame->audio_data().end()));\r
345 \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
348         }\r
349                         \r
350         void schedule_next_video(const safe_ptr<core::read_frame>& frame)\r
351         {\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
355 \r
356                 graph_->update_value("tick-time", tick_timer_.elapsed()*format_desc_.fps*0.5);\r
357                 tick_timer_.restart();\r
358         }\r
359 \r
360         void send(const safe_ptr<core::read_frame>& frame)\r
361         {\r
362                 {\r
363                         tbb::spin_mutex::scoped_lock lock(exception_mutex_);\r
364                         if(exception_ != nullptr)\r
365                                 std::rethrow_exception(exception_);\r
366                 }\r
367 \r
368                 if(!is_running_)\r
369                         BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Is not running."));\r
370                 \r
371                 if(config_.embedded_audio)\r
372                         audio_frame_buffer_.push(frame);        \r
373                 video_frame_buffer_.push(frame);        \r
374         }\r
375         \r
376         std::wstring print() const\r
377         {\r
378                 return model_name_ + L" [" + boost::lexical_cast<std::wstring>(config_.device_index) + L"|" +  format_desc_.name + L"]";\r
379         }\r
380 };\r
381 \r
382 struct decklink_consumer_proxy : public core::frame_consumer\r
383 {\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
388 public:\r
389 \r
390         decklink_consumer_proxy(const configuration& config)\r
391                 : config_(config)\r
392                 , context_(L"decklink_consumer[" + boost::lexical_cast<std::wstring>(config.device_index) + L"]")\r
393                 , fail_count_(0)\r
394         {\r
395         }\r
396 \r
397         ~decklink_consumer_proxy()\r
398         {\r
399                 auto str = print();\r
400                 context_.reset();\r
401                 CASPAR_LOG(info) << str << L" Successfully Uninitialized.";     \r
402         }\r
403         \r
404         virtual void initialize(const core::video_format_desc& format_desc)\r
405         {\r
406                 format_desc_ = format_desc;\r
407                 context_.reset([&]{return new decklink_consumer(config_, format_desc_);});              \r
408                                 \r
409                 CASPAR_LOG(info) << print() << L" Successfully Initialized.";   \r
410         }\r
411         \r
412         virtual bool send(const safe_ptr<core::read_frame>& frame)\r
413         {\r
414                 if(!context_)\r
415                         context_.reset([&]{return new decklink_consumer(config_, format_desc_);});\r
416 \r
417                 try\r
418                 {\r
419                         context_->send(frame);\r
420                         fail_count_ = 0;\r
421                 }\r
422                 catch(...)\r
423                 {\r
424                         context_.reset();\r
425 \r
426                         if(fail_count_++ > 3)\r
427                                 return false;  // Outside didn't handle exception properly, just give up.\r
428                         \r
429                         throw;\r
430                 }\r
431 \r
432                 return true;\r
433         }\r
434         \r
435         virtual std::wstring print() const\r
436         {\r
437                 return context_ ? context_->print() : L"decklink_consumer";\r
438         }\r
439 \r
440         virtual bool key_only() const\r
441         {\r
442                 return config_.key_only;\r
443         }\r
444                 \r
445         virtual const core::video_format_desc& get_video_format_desc() const\r
446         {\r
447                 return format_desc_;\r
448         }\r
449 };      \r
450 \r
451 safe_ptr<core::frame_consumer> create_decklink_consumer(const std::vector<std::wstring>& params) \r
452 {\r
453         if(params.size() < 1 || params[0] != L"DECKLINK")\r
454                 return core::frame_consumer::empty();\r
455         \r
456         configuration config;\r
457                 \r
458         if(params.size() > 1)\r
459                 config.device_index = lexical_cast_or_default<int>(params[1], config.device_index);\r
460         \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
465 \r
466         return make_safe<decklink_consumer_proxy>(config);\r
467 }\r
468 \r
469 safe_ptr<core::frame_consumer> create_decklink_consumer(const boost::property_tree::ptree& ptree) \r
470 {\r
471         configuration config;\r
472 \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
478 \r
479         return make_safe<decklink_consumer_proxy>(config);\r
480 }\r
481 \r
482 }\r
483 \r
484 /*\r
485 ##############################################################################\r
486 Pre-rolling\r
487 \r
488 Mail: 2011-05-09\r
489 \r
490 Yoshan\r
491 BMD Developer Support\r
492 developer@blackmagic-design.com\r
493 \r
494 -----------------------------------------------------------------------------\r
495 \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
501 \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
510 */\r
511 \r
512 /*\r
513 ##############################################################################\r
514 Async DMA Transfer without redundant copying\r
515 \r
516 Mail: 2011-05-10\r
517 \r
518 Yoshan\r
519 BMD Developer Support\r
520 developer@blackmagic-design.com\r
521 \r
522 -----------------------------------------------------------------------------\r
523 \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
530 */