2 * Copyright 2013 Sveriges Television AB http://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
27 #include "../util/decklink_allocator.h"
\r
29 #include "../interop/DeckLinkAPI_h.h"
\r
31 #include <core/mixer/read_frame.h>
\r
33 #include <common/concurrency/com_context.h>
\r
34 #include <common/concurrency/future_util.h>
\r
35 #include <common/diagnostics/graph.h>
\r
36 #include <common/exception/exceptions.h>
\r
37 #include <common/exception/win32_exception.h>
\r
38 #include <common/utility/assert.h>
\r
40 #include <core/parameters/parameters.h>
\r
41 #include <core/consumer/frame_consumer.h>
\r
42 #include <core/mixer/audio/audio_util.h>
\r
44 #include <tbb/concurrent_queue.h>
\r
45 #include <tbb/cache_aligned_allocator.h>
\r
47 #include <boost/circular_buffer.hpp>
\r
48 #include <boost/timer.hpp>
\r
49 #include <boost/property_tree/ptree.hpp>
\r
50 #include <boost/algorithm/string.hpp>
\r
52 namespace caspar { namespace decklink {
\r
54 struct decklink_consumer : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback, boost::noncopyable
\r
56 const int channel_index_;
\r
57 const configuration config_;
\r
59 std::unique_ptr<thread_safe_decklink_allocator> allocator_;
\r
60 CComPtr<IDeckLink> decklink_;
\r
61 CComQIPtr<IDeckLinkOutput> output_;
\r
62 CComQIPtr<IDeckLinkKeyer> keyer_;
\r
63 CComQIPtr<IDeckLinkAttributes> attributes_;
\r
64 CComQIPtr<IDeckLinkConfiguration> configuration_;
\r
66 tbb::spin_mutex exception_mutex_;
\r
67 std::exception_ptr exception_;
\r
69 tbb::atomic<bool> is_running_;
\r
71 const std::wstring model_name_;
\r
72 const core::video_format_desc format_desc_;
\r
73 const size_t buffer_size_;
\r
75 long long video_scheduled_;
\r
76 long long audio_scheduled_;
\r
78 size_t preroll_count_;
\r
80 boost::circular_buffer<std::vector<int32_t>> audio_container_;
\r
82 tbb::concurrent_bounded_queue<std::shared_ptr<core::read_frame>> video_frame_buffer_;
\r
83 tbb::concurrent_bounded_queue<std::shared_ptr<core::read_frame>> audio_frame_buffer_;
\r
85 safe_ptr<diagnostics::graph> graph_;
\r
86 boost::timer tick_timer_;
\r
87 retry_task<bool> send_completion_;
\r
88 reference_signal_detector reference_signal_detector_;
\r
90 tbb::atomic<int64_t> current_presentation_delay_;
\r
93 decklink_consumer(const configuration& config, const core::video_format_desc& format_desc, int channel_index)
\r
94 : channel_index_(channel_index)
\r
96 , decklink_(get_device(config.device_index))
\r
97 , output_(decklink_)
\r
99 , attributes_(decklink_)
\r
100 , configuration_(decklink_)
\r
101 , model_name_(get_model_name(decklink_))
\r
102 , format_desc_(format_desc)
\r
103 , buffer_size_(config.buffer_depth()) // Minimum buffer-size 3.
\r
104 , video_scheduled_(0)
\r
105 , audio_scheduled_(0)
\r
106 , preroll_count_(0)
\r
107 , audio_container_(buffer_size_+1)
\r
108 , reference_signal_detector_(output_)
\r
110 is_running_ = true;
\r
111 current_presentation_delay_ = 0;
\r
113 video_frame_buffer_.set_capacity(1);
\r
115 if (format_desc.fps > 50.0)
\r
116 // Blackmagic calls RenderAudioSamples() 50 times per second
\r
117 // regardless of video mode so we sometimes need to give them
\r
118 // samples from 2 frames in order to keep up
\r
119 audio_frame_buffer_.set_capacity(2);
\r
121 audio_frame_buffer_.set_capacity(1);
\r
123 graph_->set_color("tick-time", diagnostics::color(0.0f, 0.6f, 0.9f));
\r
124 graph_->set_color("late-frame", diagnostics::color(0.6f, 0.3f, 0.3f));
\r
125 graph_->set_color("dropped-frame", diagnostics::color(0.3f, 0.6f, 0.3f));
\r
126 graph_->set_color("flushed-frame", diagnostics::color(0.4f, 0.3f, 0.8f));
\r
127 graph_->set_color("buffered-audio", diagnostics::color(0.9f, 0.9f, 0.5f));
\r
128 graph_->set_color("buffered-video", diagnostics::color(0.2f, 0.9f, 0.9f));
\r
129 graph_->set_text(print());
\r
130 diagnostics::register_graph(graph_);
\r
132 enable_video(get_display_mode(output_, format_desc_.format, bmdFormat8BitBGRA, bmdVideoOutputFlagDefault));
\r
134 if(config.embedded_audio)
\r
137 set_latency(configuration_, config.latency, print());
\r
138 set_keyer(attributes_, keyer_, config.keyer, print());
\r
140 if(config.embedded_audio)
\r
141 output_->BeginAudioPreroll();
\r
143 for(size_t n = 0; n < buffer_size_; ++n)
\r
144 schedule_next_video(make_safe<core::read_frame>());
\r
146 if(!config.embedded_audio)
\r
150 ~decklink_consumer()
\r
152 is_running_ = false;
\r
153 video_frame_buffer_.try_push(std::make_shared<core::read_frame>());
\r
154 audio_frame_buffer_.try_push(std::make_shared<core::read_frame>());
\r
156 if(output_ != nullptr)
\r
158 output_->StopScheduledPlayback(0, nullptr, 0);
\r
159 if(config_.embedded_audio)
\r
160 output_->DisableAudioOutput();
\r
161 output_->DisableVideoOutput();
\r
165 void enable_audio()
\r
167 if(FAILED(output_->EnableAudioOutput(bmdAudioSampleRate48kHz, bmdAudioSampleType32bitInteger, config_.num_out_channels(), bmdAudioOutputStreamTimestamped)))
\r
168 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not enable audio output."));
\r
170 if(FAILED(output_->SetAudioCallback(this)))
\r
171 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not set audio callback."));
\r
173 CASPAR_LOG(info) << print() << L" Enabled embedded-audio.";
\r
176 void enable_video(BMDDisplayMode display_mode)
\r
178 if (config_.custom_allocator)
\r
180 allocator_.reset(new thread_safe_decklink_allocator(print()));
\r
182 if (FAILED(output_->SetVideoOutputFrameMemoryAllocator(allocator_.get())))
\r
183 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not set custom memory allocator."));
\r
185 CASPAR_LOG(info) << print() << L" Using custom allocator.";
\r
188 if(FAILED(output_->EnableVideoOutput(display_mode, bmdVideoOutputFlagDefault)))
\r
189 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Could not enable video output."));
\r
191 if(FAILED(output_->SetScheduledFrameCompletionCallback(this)))
\r
192 BOOST_THROW_EXCEPTION(caspar_exception()
\r
193 << msg_info(narrow(print()) + " Failed to set playback completion callback.")
\r
194 << boost::errinfo_api_function("SetScheduledFrameCompletionCallback"));
\r
197 void start_playback()
\r
199 if(FAILED(output_->StartScheduledPlayback(0, format_desc_.time_scale, 1.0)))
\r
200 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Failed to schedule playback."));
\r
203 STDMETHOD (QueryInterface(REFIID, LPVOID*)) {return E_NOINTERFACE;}
\r
204 STDMETHOD_(ULONG, AddRef()) {return 1;}
\r
205 STDMETHOD_(ULONG, Release()) {return 1;}
\r
207 STDMETHOD(ScheduledPlaybackHasStopped())
\r
209 is_running_ = false;
\r
210 CASPAR_LOG(info) << print() << L" Scheduled playback has stopped.";
\r
214 STDMETHOD(ScheduledFrameCompleted(IDeckLinkVideoFrame* completed_frame, BMDOutputFrameCompletionResult result))
\r
216 win32_exception::ensure_handler_installed_for_thread("decklink-ScheduledFrameCompleted");
\r
222 auto dframe = reinterpret_cast<decklink_frame*>(completed_frame);
\r
223 current_presentation_delay_ = dframe->get_age_millis();
\r
225 if(result == bmdOutputFrameDisplayedLate)
\r
227 graph_->set_tag("late-frame");
\r
228 video_scheduled_ += format_desc_.duration;
\r
229 audio_scheduled_ += dframe->audio_data().size()/config_.num_out_channels();
\r
230 //++video_scheduled_;
\r
231 //audio_scheduled_ += format_desc_.audio_cadence[0];
\r
232 //++audio_scheduled_;
\r
234 else if(result == bmdOutputFrameDropped)
\r
235 graph_->set_tag("dropped-frame");
\r
236 else if(result == bmdOutputFrameFlushed)
\r
237 graph_->set_tag("flushed-frame");
\r
239 std::shared_ptr<core::read_frame> frame;
\r
240 video_frame_buffer_.pop(frame);
\r
241 send_completion_.try_completion();
\r
242 schedule_next_video(make_safe_ptr(frame));
\r
244 unsigned long buffered;
\r
245 output_->GetBufferedVideoFrameCount(&buffered);
\r
246 graph_->set_value("buffered-video", static_cast<double>(buffered)/format_desc_.fps);
\r
250 tbb::spin_mutex::scoped_lock lock(exception_mutex_);
\r
251 exception_ = std::current_exception();
\r
258 STDMETHOD(RenderAudioSamples(BOOL preroll))
\r
260 win32_exception::ensure_handler_installed_for_thread("decklink-RenderAudioSamples");
\r
269 if(++preroll_count_ >= buffer_size_)
\r
271 output_->EndAudioPreroll();
\r
276 core::audio_buffer silent_audio(format_desc_.audio_cadence[preroll_count_ % format_desc_.audio_cadence.size()] * config_.num_out_channels(), 0);
\r
277 auto view = core::make_multichannel_view<int32_t>(silent_audio.begin(), silent_audio.end(), config_.audio_layout, config_.num_out_channels());
\r
278 schedule_next_audio(view);
\r
283 std::shared_ptr<core::read_frame> frame;
\r
285 while (audio_frame_buffer_.try_pop(frame))
\r
287 send_completion_.try_completion();
\r
288 schedule_next_audio(frame->multichannel_view());
\r
292 unsigned long buffered;
\r
293 output_->GetBufferedAudioSampleFrameCount(&buffered);
\r
294 graph_->set_value("buffered-audio", static_cast<double>(buffered)/(format_desc_.audio_cadence[0] * config_.num_out_channels() * 2));
\r
298 tbb::spin_mutex::scoped_lock lock(exception_mutex_);
\r
299 exception_ = std::current_exception();
\r
306 template<typename View>
\r
307 void schedule_next_audio(const View& view)
\r
309 const int sample_frame_count = view.num_samples();
\r
311 if (core::needs_rearranging(
\r
312 view, config_.audio_layout, config_.num_out_channels()))
\r
314 std::vector<int32_t> resulting_audio_data;
\r
315 resulting_audio_data.resize(
\r
316 sample_frame_count * config_.num_out_channels());
\r
318 auto dest_view = core::make_multichannel_view<int32_t>(
\r
319 resulting_audio_data.begin(),
\r
320 resulting_audio_data.end(),
\r
321 config_.audio_layout,
\r
322 config_.num_out_channels());
\r
324 core::rearrange_or_rearrange_and_mix(
\r
325 view, dest_view, core::default_mix_config_repository());
\r
327 if (config_.audio_layout.num_channels == 1) // mono
\r
328 boost::copy( // duplicate L to R
\r
329 dest_view.channel(0),
\r
330 dest_view.channel(1).begin());
\r
332 audio_container_.push_back(std::move(resulting_audio_data));
\r
336 audio_container_.push_back(
\r
337 std::vector<int32_t>(view.raw_begin(), view.raw_end()));
\r
340 if(FAILED(output_->ScheduleAudioSamples(
\r
341 audio_container_.back().data(),
\r
342 sample_frame_count,
\r
344 format_desc_.audio_sample_rate,
\r
346 CASPAR_LOG(error) << print() << L" Failed to schedule audio.";
\r
348 audio_scheduled_ += sample_frame_count;
\r
351 void schedule_next_video(const safe_ptr<core::read_frame>& frame)
\r
353 CComPtr<IDeckLinkVideoFrame> frame2(new decklink_frame(frame, format_desc_, config_.key_only));
\r
354 if(FAILED(output_->ScheduleVideoFrame(frame2, video_scheduled_, format_desc_.duration, format_desc_.time_scale)))
\r
355 CASPAR_LOG(error) << print() << L" Failed to schedule video.";
\r
357 video_scheduled_ += format_desc_.duration;
\r
359 graph_->set_value("tick-time", tick_timer_.elapsed()*format_desc_.fps*0.5);
\r
360 tick_timer_.restart();
\r
362 reference_signal_detector_.detect_change([this]() { return print(); });
\r
365 boost::unique_future<bool> send(const safe_ptr<core::read_frame>& frame)
\r
368 tbb::spin_mutex::scoped_lock lock(exception_mutex_);
\r
369 if(exception_ != nullptr)
\r
370 std::rethrow_exception(exception_);
\r
374 BOOST_THROW_EXCEPTION(caspar_exception() << msg_info(narrow(print()) + " Is not running."));
\r
376 bool audio_ready = !config_.embedded_audio;
\r
377 bool video_ready = false;
\r
379 auto enqueue_task = [audio_ready, video_ready, frame, this]() mutable -> boost::optional<bool>
\r
382 audio_ready = audio_frame_buffer_.try_push(frame);
\r
385 video_ready = video_frame_buffer_.try_push(frame);
\r
387 if (audio_ready && video_ready)
\r
390 return boost::optional<bool>();
\r
393 if (enqueue_task())
\r
394 return wrap_as_future(true);
\r
396 send_completion_.set_task(enqueue_task);
\r
398 return send_completion_.get_future();
\r
401 std::wstring print() const
\r
403 return model_name_ + L" [" + boost::lexical_cast<std::wstring>(channel_index_) + L"-" +
\r
404 boost::lexical_cast<std::wstring>(config_.device_index) + L"|" + format_desc_.name + L"]";
\r
408 struct decklink_consumer_proxy : public core::frame_consumer
\r
410 const configuration config_;
\r
411 com_context<decklink_consumer> context_;
\r
412 std::vector<size_t> audio_cadence_;
\r
413 core::video_format_desc format_desc_;
\r
416 decklink_consumer_proxy(const configuration& config)
\r
418 , context_(L"decklink_consumer[" + boost::lexical_cast<std::wstring>(config.device_index) + L"]")
\r
422 ~decklink_consumer_proxy()
\r
426 auto str = print();
\r
428 CASPAR_LOG(info) << str << L" Successfully Uninitialized.";
\r
434 virtual void initialize(const core::video_format_desc& format_desc, int channel_index) override
\r
436 context_.reset([&]{return new decklink_consumer(config_, format_desc, channel_index);});
\r
437 audio_cadence_ = format_desc.audio_cadence;
\r
438 format_desc_ = format_desc;
\r
440 CASPAR_LOG(info) << print() << L" Successfully Initialized.";
\r
443 virtual boost::unique_future<bool> send(const safe_ptr<core::read_frame>& frame) override
\r
445 CASPAR_VERIFY(audio_cadence_.front() * frame->num_channels() == static_cast<size_t>(frame->audio_data().size()));
\r
446 boost::range::rotate(audio_cadence_, std::begin(audio_cadence_)+1);
\r
448 return context_->send(frame);
\r
451 virtual std::wstring print() const override
\r
453 return context_ ? context_->print() : L"[decklink_consumer]";
\r
456 virtual boost::property_tree::wptree info() const override
\r
458 boost::property_tree::wptree info;
\r
459 info.add(L"type", L"decklink-consumer");
\r
460 info.add(L"key-only", config_.key_only);
\r
461 info.add(L"device", config_.device_index);
\r
462 info.add(L"low-latency", config_.low_latency);
\r
463 info.add(L"embedded-audio", config_.embedded_audio);
\r
464 info.add(L"presentation-frame-age", presentation_frame_age_millis());
\r
465 //info.add(L"internal-key", config_.internal_key);
\r
469 virtual size_t buffer_depth() const override
\r
471 return config_.buffer_depth();
\r
474 virtual int index() const override
\r
476 return 300 + config_.device_index;
\r
479 virtual int64_t presentation_frame_age_millis() const
\r
481 return context_ ? context_->current_presentation_delay_ : 0;
\r
485 safe_ptr<core::frame_consumer> create_consumer(const core::parameters& params)
\r
487 if(params.size() < 1 || params[0] != L"DECKLINK")
\r
488 return core::frame_consumer::empty();
\r
490 configuration config;
\r
492 if(params.size() > 1)
\r
493 config.device_index = lexical_cast_or_default<int>(params[1], config.device_index);
\r
495 if(std::find(params.begin(), params.end(), L"INTERNAL_KEY") != params.end())
\r
496 config.keyer = configuration::internal_keyer;
\r
497 else if(std::find(params.begin(), params.end(), L"EXTERNAL_KEY") != params.end())
\r
498 config.keyer = configuration::external_keyer;
\r
500 config.keyer = configuration::default_keyer;
\r
502 if(std::find(params.begin(), params.end(), L"LOW_LATENCY") != params.end())
\r
503 config.latency = configuration::low_latency;
\r
505 config.embedded_audio = std::find(params.begin(), params.end(), L"EMBEDDED_AUDIO") != params.end();
\r
506 config.key_only = std::find(params.begin(), params.end(), L"KEY_ONLY") != params.end();
\r
507 config.audio_layout = core::default_channel_layout_repository().get_by_name(
\r
508 params.get(L"CHANNEL_LAYOUT", L"STEREO"));
\r
510 return make_safe<decklink_consumer_proxy>(config);
\r
513 safe_ptr<core::frame_consumer> create_consumer(const boost::property_tree::wptree& ptree)
\r
515 configuration config;
\r
517 auto keyer = ptree.get(L"keyer", L"external");
\r
518 if(keyer == L"external")
\r
519 config.keyer = configuration::external_keyer;
\r
520 else if(keyer == L"internal")
\r
521 config.keyer = configuration::internal_keyer;
\r
523 auto latency = ptree.get(L"latency", L"normal");
\r
524 if(latency == L"low")
\r
525 config.latency = configuration::low_latency;
\r
526 else if(latency == L"normal")
\r
527 config.latency = configuration::normal_latency;
\r
529 config.key_only = ptree.get(L"key-only", config.key_only);
\r
530 config.device_index = ptree.get(L"device", config.device_index);
\r
531 config.embedded_audio = ptree.get(L"embedded-audio", config.embedded_audio);
\r
532 config.base_buffer_depth = ptree.get(L"buffer-depth", config.base_buffer_depth);
\r
533 config.custom_allocator = ptree.get(L"custom-allocator", config.custom_allocator);
\r
534 config.audio_layout =
\r
535 core::default_channel_layout_repository().get_by_name(
\r
536 boost::to_upper_copy(ptree.get(L"channel-layout", L"STEREO")));
\r
538 return make_safe<decklink_consumer_proxy>(config);
\r
544 ##############################################################################
\r
550 BMD Developer Support
\r
551 developer@blackmagic-design.com
\r
553 -----------------------------------------------------------------------------
\r
555 Thanks for your inquiry. The minimum number of frames that you can preroll
\r
556 for scheduled playback is three frames for video and four frames for audio.
\r
557 As you mentioned if you preroll less frames then playback will not start or
\r
558 playback will be very sporadic. From our experience with Media Express, we
\r
559 recommended that at least seven frames are prerolled for smooth playback.
\r
561 Regarding the bmdDeckLinkConfigLowLatencyVideoOutput flag:
\r
562 There can be around 3 frames worth of latency on scheduled output.
\r
563 When the bmdDeckLinkConfigLowLatencyVideoOutput flag is used this latency is
\r
564 reduced or removed for scheduled playback. If the DisplayVideoFrameSync()
\r
565 method is used, the bmdDeckLinkConfigLowLatencyVideoOutput setting will
\r
566 guarantee that the provided frame will be output as soon the previous
\r
567 frame output has been completed.
\r
568 ################################################################################
\r
572 ##############################################################################
\r
573 Async DMA Transfer without redundant copying
\r
578 BMD Developer Support
\r
579 developer@blackmagic-design.com
\r
581 -----------------------------------------------------------------------------
\r
583 Thanks for your inquiry. You could try subclassing IDeckLinkMutableVideoFrame
\r
584 and providing a pointer to your video buffer when GetBytes() is called.
\r
585 This may help to keep copying to a minimum. Please ensure that the pixel
\r
586 format is in bmdFormat10BitYUV, otherwise the DeckLink API / driver will
\r
587 have to colourspace convert which may result in additional copying.
\r
588 ################################################################################
\r