mirror of
https://github.com/outbackdingo/UltraGrid.git
synced 2026-03-21 22:40:30 +00:00
2586 lines
119 KiB
C++
2586 lines
119 KiB
C++
/**
|
|
* @file video_display/decklink.cpp
|
|
* @author Martin Benes <martinbenesh@gmail.com>
|
|
* @author Lukas Hejtmanek <xhejtman@ics.muni.cz>
|
|
* @author Petr Holub <hopet@ics.muni.cz>
|
|
* @author Milos Liska <xliska@fi.muni.cz>
|
|
* @author Jiri Matela <matela@ics.muni.cz>
|
|
* @author Dalibor Matura <255899@mail.muni.cz>
|
|
* @author Martin Pulec <pulec@cesnet.cz>
|
|
*/
|
|
/*
|
|
* Copyright (c) 2010-2021 CESNET, z. s. p. o.
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, is permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
*
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* 3. Neither the name of CESNET nor the names of its contributors may be
|
|
* used to endorse or promote products derived from this software without
|
|
* specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING,
|
|
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
|
* EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
|
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
|
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#include "config_unix.h"
|
|
#include "config_win32.h"
|
|
#endif // HAVE_CONFIG_H
|
|
|
|
#define MOD_NAME "[Decklink display] "
|
|
|
|
#include "audio/types.h"
|
|
#include "blackmagic_common.hpp"
|
|
#include "compat/platform_time.h"
|
|
#include "debug.h"
|
|
#include "host.h"
|
|
#include "lib_common.h"
|
|
#include "messaging.h"
|
|
#include "module.h"
|
|
#include "rang.hpp"
|
|
#include "rtp/audio_decoders.h"
|
|
#include "tv.h"
|
|
#include "ug_runtime_error.hpp"
|
|
#include "utils/misc.h"
|
|
#include "utils/text.h" // is_prefix_of
|
|
#include "video.h"
|
|
#include "video_display.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cinttypes>
|
|
#include <cstdint>
|
|
#include <iomanip>
|
|
#include <mutex>
|
|
#include <queue>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "DeckLinkAPIVersion.h"
|
|
|
|
#ifndef WIN32
|
|
#define STDMETHODCALLTYPE
|
|
#endif
|
|
|
|
#define MAX_RESAMPLE_DELTA_DEFAULT 30
|
|
#define MIN_RESAMPLE_DELTA_DEFAULT 1
|
|
#define TARGET_BUFFER_DEFAULT 2700
|
|
|
|
static void print_output_modes(IDeckLink *);
|
|
static void display_decklink_done(void *state);
|
|
|
|
// performs command, if failed, displays error and jumps to error label
|
|
#define EXIT_IF_FAILED(cmd, name) \
|
|
do {\
|
|
HRESULT result = cmd;\
|
|
if (FAILED(result)) {;\
|
|
LOG(LOG_LEVEL_ERROR) << MOD_NAME << name << ": " << bmd_hresult_to_string(result) << "\n";\
|
|
goto error;\
|
|
}\
|
|
} while (0)
|
|
|
|
// similar as above, but only displays warning
|
|
#define CALL_AND_CHECK(cmd, name) \
|
|
do {\
|
|
HRESULT result = cmd;\
|
|
if (FAILED(result)) {;\
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME << name << ": " << bmd_hresult_to_string(result) << "\n";\
|
|
}\
|
|
} while (0)
|
|
|
|
using namespace std;
|
|
using rang::fg;
|
|
using rang::style;
|
|
|
|
static int display_decklink_putf(void *state, struct video_frame *frame, long long nonblock);
|
|
|
|
namespace {
|
|
class MovingAverage {
|
|
public:
|
|
MovingAverage(unsigned int period) :
|
|
period(period),
|
|
window(new double[period]),
|
|
head(NULL),
|
|
tail(NULL),
|
|
total(0) {
|
|
|
|
assert(period >= 1);
|
|
}
|
|
|
|
~MovingAverage() {
|
|
delete[] window;
|
|
}
|
|
|
|
void add(double val) {
|
|
// Init
|
|
if (head == NULL) {
|
|
head = window;
|
|
*head = val;
|
|
tail = head;
|
|
inc(tail);
|
|
total = val;
|
|
return;
|
|
}
|
|
// full?
|
|
if (head == tail) {
|
|
total -= *head;
|
|
inc(head);
|
|
}
|
|
|
|
*tail = val;
|
|
inc(tail);
|
|
total += val;
|
|
}
|
|
|
|
// Returns the average of the last P elements added.
|
|
// If no elements have been added yet, returns 0.0
|
|
double avg() const {
|
|
ptrdiff_t size = this->size();
|
|
if (size == 0) {
|
|
return 0; // No entries => 0 average
|
|
}
|
|
return total / (double)size;
|
|
}
|
|
|
|
ptrdiff_t size() const {
|
|
if (head == NULL)
|
|
return 0;
|
|
if (head == tail)
|
|
return period;
|
|
return (period + tail - head) % period;
|
|
}
|
|
// returns true if we have filled the period with samples
|
|
ptrdiff_t filled(){
|
|
bool filled = false;
|
|
if (this->size() >= this->period ){
|
|
filled = true;
|
|
}
|
|
return filled;
|
|
}
|
|
|
|
double getTotal() const {
|
|
return total;
|
|
}
|
|
|
|
private:
|
|
unsigned int period;
|
|
double* window; // Holds the values to calculate the average of.
|
|
double* head;
|
|
double* tail;
|
|
double total; // Cache the total
|
|
|
|
void inc(double* &p) {
|
|
if (++p >= window + period) {
|
|
p = window;
|
|
}
|
|
}
|
|
};
|
|
class PlaybackDelegate : public IDeckLinkVideoOutputCallback // , public IDeckLinkAudioOutputCallback
|
|
{
|
|
private:
|
|
uint64_t frames_dropped = 0;
|
|
uint64_t frames_flushed = 0;
|
|
uint64_t frames_late = 0;
|
|
|
|
friend int ::display_decklink_putf(void *state, struct video_frame *frame, long long nonblock);
|
|
public:
|
|
virtual ~PlaybackDelegate() = default;
|
|
// IUnknown needs only a dummy implementation
|
|
virtual HRESULT STDMETHODCALLTYPE QueryInterface (REFIID , LPVOID *) { return E_NOINTERFACE;}
|
|
virtual ULONG STDMETHODCALLTYPE AddRef () {return 1;}
|
|
virtual ULONG STDMETHODCALLTYPE Release () {return 1;}
|
|
|
|
virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted (IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result)
|
|
{
|
|
if (result == bmdOutputFrameDisplayedLate){
|
|
frames_late += 1;
|
|
LOG(LOG_LEVEL_VERBOSE) << MOD_NAME "Late frame (total: " << frames_late << ")\n";
|
|
} else if (result == bmdOutputFrameDropped){
|
|
frames_dropped += 1;
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME "Dropped frame (total: " << frames_dropped << ")\n";
|
|
} else if (result == bmdOutputFrameFlushed){
|
|
frames_flushed += 1;
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME "Flushed frame (total: " << frames_flushed << ")\n";
|
|
}
|
|
|
|
if (log_level >= LOG_LEVEL_DEBUG) {
|
|
IDeckLinkTimecode *timecode = NULL;
|
|
if (completedFrame->GetTimecode ((BMDTimecodeFormat) 0, &timecode) == S_OK) {
|
|
BMD_STR timecode_str;
|
|
if (timecode && timecode->GetString(&timecode_str) == S_OK) {
|
|
char *timecode_cstr = get_cstr_from_bmd_api_str(timecode_str);
|
|
LOG(LOG_LEVEL_DEBUG) << "Frame " << timecode_cstr << " output at " << time_since_epoch_in_ms() / (double) 1e3 << '\n';
|
|
release_bmd_api_str(timecode_str);
|
|
free(timecode_cstr);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
completedFrame->Release();
|
|
return S_OK;
|
|
}
|
|
|
|
virtual HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped (){
|
|
return S_OK;
|
|
}
|
|
//virtual HRESULT RenderAudioSamples (bool preroll);
|
|
};
|
|
|
|
class DeckLinkFrame;
|
|
|
|
struct buffer_pool_t {
|
|
queue<DeckLinkFrame *> frame_queue;
|
|
mutex lock;
|
|
};
|
|
|
|
class DeckLinkTimecode : public IDeckLinkTimecode{
|
|
BMDTimecodeBCD timecode;
|
|
public:
|
|
DeckLinkTimecode() : timecode(0) {}
|
|
virtual ~DeckLinkTimecode() = default;
|
|
/* IDeckLinkTimecode */
|
|
virtual BMDTimecodeBCD STDMETHODCALLTYPE GetBCD (void) { return timecode; }
|
|
virtual HRESULT STDMETHODCALLTYPE GetComponents (/* out */ uint8_t *hours, /* out */ uint8_t *minutes, /* out */ uint8_t *seconds, /* out */ uint8_t *frames) {
|
|
*frames = (timecode & 0xf) + ((timecode & 0xf0) >> 4) * 10;
|
|
*seconds = ((timecode & 0xf00) >> 8) + ((timecode & 0xf000) >> 12) * 10;
|
|
*minutes = ((timecode & 0xf0000) >> 16) + ((timecode & 0xf00000) >> 20) * 10;
|
|
*hours = ((timecode & 0xf000000) >> 24) + ((timecode & 0xf0000000) >> 28) * 10;
|
|
return S_OK;
|
|
}
|
|
virtual HRESULT STDMETHODCALLTYPE GetString (/* out */ BMD_STR *timecode) {
|
|
uint8_t hours, minutes, seconds, frames;
|
|
GetComponents(&hours, &minutes, &seconds, &frames);
|
|
char timecode_c[16];
|
|
assert(hours <= 99 && minutes <= 59 && seconds <= 60 && frames <= 99);
|
|
sprintf(timecode_c, "%02" PRIu8 ":%02" PRIu8 ":%02" PRIu8 ":%02" PRIu8, hours, minutes, seconds, frames);
|
|
*timecode = get_bmd_api_str_from_cstr(timecode_c);
|
|
return *timecode ? S_OK : E_FAIL;
|
|
}
|
|
virtual BMDTimecodeFlags STDMETHODCALLTYPE GetFlags (void) { return bmdTimecodeFlagDefault; }
|
|
virtual HRESULT STDMETHODCALLTYPE GetTimecodeUserBits (/* out */ BMDTimecodeUserBits *userBits) { if (!userBits) return E_POINTER; else return S_OK; }
|
|
|
|
/* IUnknown */
|
|
virtual HRESULT STDMETHODCALLTYPE QueryInterface (REFIID , LPVOID *) {return E_NOINTERFACE;}
|
|
virtual ULONG STDMETHODCALLTYPE AddRef () {return 1;}
|
|
virtual ULONG STDMETHODCALLTYPE Release () {return 1;}
|
|
|
|
void STDMETHODCALLTYPE SetBCD(BMDTimecodeBCD timecode) { this->timecode = timecode; }
|
|
};
|
|
|
|
struct ChromaticityCoordinates
|
|
{
|
|
double RedX;
|
|
double RedY;
|
|
double GreenX;
|
|
double GreenY;
|
|
double BlueX;
|
|
double BlueY;
|
|
double WhiteX;
|
|
double WhiteY;
|
|
};
|
|
|
|
constexpr ChromaticityCoordinates kDefaultRec2020Colorimetrics = { 0.708, 0.292, 0.170, 0.797, 0.131, 0.046, 0.3127, 0.3290 };
|
|
constexpr double kDefaultMaxDisplayMasteringLuminance = 1000.0;
|
|
constexpr double kDefaultMinDisplayMasteringLuminance = 0.0001;
|
|
constexpr double kDefaultMaxCLL = 1000.0;
|
|
constexpr double kDefaultMaxFALL = 50.0;
|
|
enum class HDR_EOTF { NONE = -1, SDR = 0, HDR = 1, PQ = 2, HLG = 3 };
|
|
|
|
struct HDRMetadata
|
|
{
|
|
int64_t EOTF{static_cast<int64_t>(HDR_EOTF::NONE)};
|
|
ChromaticityCoordinates referencePrimaries{kDefaultRec2020Colorimetrics};
|
|
double maxDisplayMasteringLuminance{kDefaultMaxDisplayMasteringLuminance};
|
|
double minDisplayMasteringLuminance{kDefaultMinDisplayMasteringLuminance};
|
|
double maxCLL{kDefaultMaxCLL};
|
|
double maxFALL{kDefaultMaxFALL};
|
|
|
|
void Init(const string & fmt);
|
|
};
|
|
|
|
class DeckLinkFrame : public IDeckLinkMutableVideoFrame, public IDeckLinkVideoFrameMetadataExtensions
|
|
{
|
|
long width;
|
|
long height;
|
|
long rawBytes;
|
|
BMDPixelFormat pixelFormat;
|
|
unique_ptr<char []> data;
|
|
|
|
IDeckLinkTimecode *timecode;
|
|
|
|
long ref;
|
|
|
|
buffer_pool_t &buffer_pool;
|
|
struct HDRMetadata m_metadata;
|
|
protected:
|
|
DeckLinkFrame(long w, long h, long rb, BMDPixelFormat pf, buffer_pool_t & bp, HDRMetadata const & hdr_metadata);
|
|
|
|
public:
|
|
virtual ~DeckLinkFrame();
|
|
static DeckLinkFrame *Create(long width, long height, long rawBytes, BMDPixelFormat pixelFormat, buffer_pool_t & buffer_pool, HDRMetadata const & hdr_metadata);
|
|
|
|
/* IUnknown */
|
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**) override;
|
|
ULONG STDMETHODCALLTYPE AddRef() override;
|
|
ULONG STDMETHODCALLTYPE Release() override;
|
|
|
|
/* IDeckLinkVideoFrame */
|
|
long STDMETHODCALLTYPE GetWidth (void) override;
|
|
long STDMETHODCALLTYPE GetHeight (void) override;
|
|
long STDMETHODCALLTYPE GetRowBytes (void) override;
|
|
BMDPixelFormat STDMETHODCALLTYPE GetPixelFormat (void) override;
|
|
BMDFrameFlags STDMETHODCALLTYPE GetFlags (void) override;
|
|
HRESULT STDMETHODCALLTYPE GetBytes (/* out */ void **buffer) override;
|
|
HRESULT STDMETHODCALLTYPE GetTimecode (/* in */ BMDTimecodeFormat format, /* out */ IDeckLinkTimecode **timecode) override;
|
|
HRESULT STDMETHODCALLTYPE GetAncillaryData (/* out */ IDeckLinkVideoFrameAncillary **ancillary) override;
|
|
|
|
/* IDeckLinkMutableVideoFrame */
|
|
HRESULT STDMETHODCALLTYPE SetFlags(BMDFrameFlags) override;
|
|
HRESULT STDMETHODCALLTYPE SetTimecode(BMDTimecodeFormat, IDeckLinkTimecode*) override;
|
|
HRESULT STDMETHODCALLTYPE SetTimecodeFromComponents(BMDTimecodeFormat, uint8_t, uint8_t, uint8_t, uint8_t, BMDTimecodeFlags) override;
|
|
HRESULT STDMETHODCALLTYPE SetAncillaryData(IDeckLinkVideoFrameAncillary*) override;
|
|
HRESULT STDMETHODCALLTYPE SetTimecodeUserBits(BMDTimecodeFormat, BMDTimecodeUserBits) override;
|
|
|
|
// IDeckLinkVideoFrameMetadataExtensions interface
|
|
HRESULT STDMETHODCALLTYPE GetInt(BMDDeckLinkFrameMetadataID metadataID, int64_t* value) override;
|
|
HRESULT STDMETHODCALLTYPE GetFloat(BMDDeckLinkFrameMetadataID metadataID, double* value) override;
|
|
HRESULT STDMETHODCALLTYPE GetFlag(BMDDeckLinkFrameMetadataID metadataID, BMD_BOOL* value) override;
|
|
HRESULT STDMETHODCALLTYPE GetString(BMDDeckLinkFrameMetadataID metadataID, BMD_STR * value) override;
|
|
HRESULT STDMETHODCALLTYPE GetBytes(BMDDeckLinkFrameMetadataID metadataID, void* buffer, uint32_t* bufferSize) override;
|
|
|
|
};
|
|
|
|
class DeckLink3DFrame : public DeckLinkFrame, public IDeckLinkVideoFrame3DExtensions
|
|
{
|
|
private:
|
|
using DeckLinkFrame::DeckLinkFrame;
|
|
DeckLink3DFrame(long w, long h, long rb, BMDPixelFormat pf, buffer_pool_t & buffer_pool, HDRMetadata const & hdr_metadata);
|
|
unique_ptr<DeckLinkFrame> rightEye; // rightEye ref count is always >= 1 therefore deleted by owner (this class)
|
|
|
|
public:
|
|
~DeckLink3DFrame();
|
|
static DeckLink3DFrame *Create(long width, long height, long rawBytes, BMDPixelFormat pixelFormat, buffer_pool_t & buffer_pool, HDRMetadata const & hdr_metadata);
|
|
|
|
/* IUnknown */
|
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**) override;
|
|
ULONG STDMETHODCALLTYPE AddRef() override;
|
|
ULONG STDMETHODCALLTYPE Release() override;
|
|
|
|
/* IDeckLinkVideoFrame3DExtensions */
|
|
BMDVideo3DPackingFormat STDMETHODCALLTYPE Get3DPackingFormat() override;
|
|
HRESULT STDMETHODCALLTYPE GetFrameForRightEye(IDeckLinkVideoFrame**) override;
|
|
};
|
|
} // end of unnamed namespace
|
|
|
|
class DecklinkAudioSummary {
|
|
public:
|
|
/**
|
|
* @brief This will detail out the longer running stats of the Decklink. It should be called on every audio frame
|
|
* but will only print out the report once every 30 seconds.
|
|
*/
|
|
void report() {
|
|
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
|
if(std::chrono::duration_cast<std::chrono::seconds>(now - this->last_summary).count() > 10) {
|
|
LOG(LOG_LEVEL_INFO) << rang::style::underline << "Decklink stats (cumulative)"
|
|
<< rang::style::reset << " - Total Audio Frames Played: "
|
|
<< rang::style::bold << this->frames_played
|
|
<< rang::style::reset << " / Missing Audio Frames: "
|
|
<< rang::style::bold << this->frames_missed
|
|
<< rang::style::reset << " / Buffer Underflows: "
|
|
<< rang::style::bold << this->buffer_underflow
|
|
<< rang::style::reset << " / Buffer Overflows: "
|
|
<< rang::style::bold << this->buffer_overflow
|
|
<< rang::style::reset << " / Resample (Higher Hz): "
|
|
<< rang::style::bold << this->resample_high
|
|
<< rang::style::reset << " / Resample (Lower Hz): "
|
|
<< rang::style::bold << this->resample_low
|
|
<< rang::style::reset << " / Average Buffer: "
|
|
<< rang::style::bold << this->buffer_average
|
|
<< rang::style::reset << " / Average Added Frames: "
|
|
<< rang::style::bold << this->avg_added_frames.avg()
|
|
<< rang::style::reset << " / Max time diff audio (ms): "
|
|
<< rang::style::bold << this->audio_time_diff_max
|
|
<< rang::style::reset << " / Min time diff audio (ms): "
|
|
<< rang::style::bold << this->audio_time_diff_min
|
|
<< "\n";
|
|
// Reset some of the variables
|
|
this->audio_time_diff_max = 0;
|
|
this->audio_time_diff_min = std::numeric_limits<long long>().max();
|
|
// Ensure that the summary gets called 30 seconds from now
|
|
this->last_summary = now;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief This should be called when a resample is requested that is lower than the
|
|
* original sample rate.
|
|
*/
|
|
void increment_resample_low() {
|
|
this->resample_low++;
|
|
}
|
|
|
|
/**
|
|
* @brief This should be called when a resample is requested that is higher than the
|
|
* original sample rate.
|
|
*/
|
|
void increment_resample_high() {
|
|
this->resample_high++;
|
|
}
|
|
|
|
/**
|
|
* @brief This should be called when an overflow has occured.
|
|
*/
|
|
void increment_buffer_overflow() {
|
|
this->buffer_overflow++;
|
|
}
|
|
|
|
/**
|
|
* @brief This should be called when an underflow has occured.
|
|
*/
|
|
void increment_buffer_underflow() {
|
|
this->buffer_underflow++;
|
|
}
|
|
|
|
/**
|
|
* @brief This should be called when an call to audio put has been called.
|
|
*/
|
|
void increment_audio_frames_played() {
|
|
this->frames_played++;
|
|
}
|
|
|
|
/**
|
|
* @brief Set the buffer average object
|
|
*
|
|
* @param buffer_average The average samples in the buffer per channel
|
|
*/
|
|
void set_buffer_average(double buffer_average) {
|
|
this->buffer_average = (int32_t)round(buffer_average);
|
|
}
|
|
|
|
/**
|
|
* @brief A quick way of roughly calculating if the buffer has emptied by the size of a single audio frame
|
|
* to keep track of missing audio frames. This doesn't mean that the audio frame was not played, just
|
|
* that the length of time between audio put calls caused the buffer to empty by half of the average
|
|
* size of a frame.
|
|
*
|
|
* @param buffer_samples The amount of audio samples in the buffer.
|
|
* @param samples The amount of samples that will be written to the buffer.
|
|
*/
|
|
void calculate_missing(uint32_t buffer_samples, uint32_t samples) {
|
|
this->avg_added_frames.add(samples);
|
|
if(this->avg_added_frames.filled()) {
|
|
samples = (uint32_t)this->avg_added_frames.avg();
|
|
}
|
|
// Check to see if the amount in the buffer has dropped by over half the average
|
|
// number of samples being written. If so, we likely dropped a frame.
|
|
if(prev_buffer_samples >= 0 && (uint32_t)this->prev_buffer_samples > buffer_samples + (samples / 2)) {
|
|
this->frames_missed++;
|
|
}
|
|
this->prev_buffer_samples = buffer_samples;
|
|
}
|
|
|
|
/**
|
|
* @brief This function should be called at the beginning of put audio to record the
|
|
* difference between calls.
|
|
*
|
|
*/
|
|
void record_audio_time_diff() {
|
|
// CHeck the previous time has been initialised
|
|
if(this->prev_audio_end.time_since_epoch().count() != 0) {
|
|
// Collect the time now and do a comparison to the time when we ended the previous function call
|
|
std::chrono::high_resolution_clock::time_point audio_begin = std::chrono::high_resolution_clock::now();
|
|
std::chrono::milliseconds time_diff = std::chrono::duration_cast<std::chrono::milliseconds>(audio_begin - this->prev_audio_end);
|
|
|
|
// Set a max or min if the timing is outside of whats already been collected
|
|
long long duration_diff = time_diff.count();
|
|
if(duration_diff > this->audio_time_diff_max) {
|
|
this->audio_time_diff_max = duration_diff;
|
|
}
|
|
else if(duration_diff < this->audio_time_diff_min) {
|
|
this->audio_time_diff_min = duration_diff;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Mark the end of the put audio function
|
|
*
|
|
*/
|
|
void mark_audio_time_end() {
|
|
this->prev_audio_end = std::chrono::high_resolution_clock::now();
|
|
}
|
|
private:
|
|
// Keep a track of the amount in the decklink buffer
|
|
int32_t prev_buffer_samples = -1;
|
|
// How many frames have been successfully written
|
|
uint32_t frames_played = 0;
|
|
// How many times the buffer dropped avg amount of frames being added
|
|
uint32_t frames_missed = 0;
|
|
MovingAverage avg_added_frames{250};
|
|
// How many buffer underflows and overflows have occured.
|
|
uint32_t buffer_underflow = 0;
|
|
uint32_t buffer_overflow = 0;
|
|
// How many times it was requested a higher or lower sample rate
|
|
uint32_t resample_high = 0;
|
|
uint32_t resample_low = 0;
|
|
// Sample count average
|
|
uint32_t buffer_average = 0;
|
|
// Timing between calls of audio put
|
|
std::chrono::high_resolution_clock::time_point prev_audio_end{};
|
|
long long audio_time_diff_max = 0;
|
|
long long audio_time_diff_min = std::numeric_limits<long long>().max();
|
|
// We want to the summary to be outputted every 30 or so seconds. So keep track of
|
|
// the last we outputted data.
|
|
std::chrono::steady_clock::time_point last_summary = std::chrono::steady_clock::now();
|
|
};
|
|
|
|
|
|
/**
|
|
* @todo
|
|
* - handle network losses
|
|
* - handle underruns
|
|
* - what about jitter - while computing the dst sample rate, the sampling interval (m_total) must be "long"
|
|
*/
|
|
class AudioDriftFixer {
|
|
public:
|
|
AudioDriftFixer(int buffer_samples, int delta_samples,
|
|
int target_buffer_fill = 0, int positive_jitter = 0,
|
|
int negative_jitter = 0) : average_buffer_samples(buffer_samples),average_delta(delta_samples),
|
|
target_buffer_fill(target_buffer_fill), pos_jitter(positive_jitter),
|
|
neg_jitter(negative_jitter){}
|
|
|
|
bool m_enabled = false;
|
|
|
|
/**
|
|
* @brief Set the max hz object
|
|
*
|
|
* @param max_hz The maximum hz delta that can be applied to fix the drift
|
|
*/
|
|
void set_max_hz(uint32_t max_hz) {
|
|
this->max_hz = max_hz;
|
|
}
|
|
|
|
/**
|
|
* @brief Set the min hz object
|
|
*
|
|
* @param min_hz The minimum hz delta that can be applied to fix the drift
|
|
*/
|
|
void set_min_hz(uint32_t min_hz) {
|
|
this->min_hz = min_hz;
|
|
}
|
|
|
|
/**
|
|
* @brief Set the target buffer object
|
|
*
|
|
* @param target_buffer The target buffer of samples per channel
|
|
*/
|
|
void set_target_buffer(uint32_t target_buffer) {
|
|
this->target_buffer_fill = target_buffer;
|
|
}
|
|
|
|
/**
|
|
* @brief Set the summary object
|
|
*
|
|
* @param audio_summary The audio summary pointer
|
|
*/
|
|
void set_summary(DecklinkAudioSummary *audio_summary) {
|
|
this->audio_summary = audio_summary;
|
|
}
|
|
|
|
/**
|
|
* @brief Set the root object
|
|
*
|
|
* @param root The root module
|
|
*/
|
|
void set_root(module *root) {
|
|
m_root = root;
|
|
}
|
|
|
|
/**
|
|
* @brief Get the average sample count per channel
|
|
*
|
|
* @return double The average of the buffer over the last X frames
|
|
*/
|
|
double get_buffer_avg() {
|
|
return this->average_buffer_samples.avg();
|
|
}
|
|
|
|
/**
|
|
* @brief This function will check the buffer delta and will return a delta in the sample rate
|
|
* that is required in order to offset the delta. This is scaled between the class
|
|
* members of min_hz and max_hz. The delta also has a max and min for the scaling which
|
|
* are defined by the min_buffer and the max_buffer. This ensures that very large deltas
|
|
* cannot cause large jumps in the resample rate that are audible (and that small deltas)
|
|
* do not create a resampling rate that is too small to have impact on the buffer.
|
|
*
|
|
* @param delta The delta between the average buffer size and the target buffer size to calculate a resample
|
|
* delta to offset the difference.
|
|
* @return double A resample delta that can be added or subtracted from the original resample rate to move the
|
|
* average buffer size to the target buffer size.
|
|
*/
|
|
double scale_buffer_delta(int delta) {
|
|
// Get a positive delta so that the scale can be calculated properly
|
|
delta = abs(delta);
|
|
// Check the boundaries for the scaling calculation
|
|
if((uint32_t)delta > this->max_buffer) {
|
|
delta = this->max_buffer;
|
|
}
|
|
else if ((uint32_t)delta < this->min_buffer) {
|
|
delta = this->min_buffer;
|
|
}
|
|
return (((this->max_hz - this->min_hz) * (delta - this->min_buffer)) / (this->max_buffer - this->min_buffer)) + this->min_hz;
|
|
}
|
|
|
|
/// @retval flag if the audio frame should be written
|
|
bool update(int buffered_count) {
|
|
if (!this->m_enabled) {
|
|
return true;
|
|
}
|
|
|
|
// Add the amount currently in the buffer to the moving average, and calculate the delta between that and the previous amount
|
|
// Store the previous buffer count so we can calculate this next frame.
|
|
this->average_buffer_samples.add((double)buffered_count);
|
|
this->average_delta.add((double)abs((int32_t)buffered_count - (int32_t)previous_buffer));
|
|
this->previous_buffer = buffered_count;
|
|
|
|
long long dst_frame_rate = 0;
|
|
// Calculate the average
|
|
uint32_t average_buffer_depth = (uint32_t)(this->average_buffer_samples.avg());
|
|
|
|
int resample_hz = dst_frame_rate = (bmdAudioSampleRate48kHz) * BASE;
|
|
|
|
// Check to see if our buffered samples has enough to calculate a good average
|
|
if (this->average_buffer_samples.filled()) {
|
|
// @todo might be worth trying to make this more dynamic so that certain input values
|
|
// for different cards can be applied
|
|
// Check to see if we have a target amount of the buffer we'd like to fill
|
|
if(this->pos_jitter == 0) {
|
|
this->pos_jitter = AudioDriftFixer::POS_JITTER_DEFAULT;
|
|
}
|
|
if(this->neg_jitter == 0) {
|
|
this->neg_jitter = AudioDriftFixer::NEG_JITTER_DEFAULT;
|
|
}
|
|
|
|
// Check whether there needs to be any resampling
|
|
if (average_buffer_depth > target_buffer_fill + this->pos_jitter)
|
|
{
|
|
// The buffer is too large, so we need to resample down to remove some frames
|
|
resample_hz = (int)this->scale_buffer_delta(average_buffer_depth - target_buffer_fill - this->pos_jitter);
|
|
dst_frame_rate = (bmdAudioSampleRate48kHz - resample_hz) * BASE;
|
|
this->audio_summary->increment_resample_low();
|
|
} else if(average_buffer_depth < target_buffer_fill - this->neg_jitter) {
|
|
// The buffer is too small, so we need to resample up to generate some additional frames
|
|
resample_hz = (int)this->scale_buffer_delta(target_buffer_fill - average_buffer_depth - this->neg_jitter);
|
|
dst_frame_rate = (bmdAudioSampleRate48kHz + resample_hz) * BASE;
|
|
this->audio_summary->increment_resample_high();
|
|
} else {
|
|
dst_frame_rate = (bmdAudioSampleRate48kHz) * BASE;
|
|
}
|
|
}
|
|
|
|
LOG(LOG_LEVEL_DEBUG) << MOD_NAME << " UPDATE playing speed " << average_buffer_depth << " vs " << buffered_count << " " << average_delta.avg() << " average_velocity " << resample_hz << " resample_hz\n";
|
|
|
|
|
|
if (dst_frame_rate != 0) {
|
|
auto *m = new msg_universal((string(MSG_UNIVERSAL_TAG_AUDIO_DECODER) + to_string(dst_frame_rate << ADEC_CH_RATE_SHIFT | BASE)).c_str());
|
|
LOG(LOG_LEVEL_VERBOSE) << MOD_NAME "Sending resample request " << dst_frame_rate << "/" << BASE << "\n";
|
|
assert(m_root != nullptr);
|
|
auto *response = send_message_sync(m_root, "audio.receiver.decoder", reinterpret_cast<message *>(m), 100, SEND_MESSAGE_FLAG_NO_STORE);
|
|
if (!RESPONSE_SUCCESSFUL(response_get_status(response))) {
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME "Unable to send resample message: " << response_get_text(response) << " (" << response_get_status(response) << ")\n";
|
|
}
|
|
free_response(response);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
static constexpr unsigned long BASE = (1U<<8U);
|
|
struct module *m_root = nullptr;
|
|
|
|
MovingAverage average_buffer_samples;
|
|
MovingAverage average_delta;
|
|
|
|
uint32_t target_buffer_fill = TARGET_BUFFER_DEFAULT;
|
|
uint32_t previous_buffer = 0;
|
|
|
|
// The min and max Hz changes we can resample between
|
|
uint32_t min_hz = MIN_RESAMPLE_DELTA_DEFAULT;
|
|
uint32_t max_hz = MAX_RESAMPLE_DELTA_DEFAULT;
|
|
// The min and max values to scale between
|
|
uint32_t min_buffer = 100;
|
|
uint32_t max_buffer = 600;
|
|
// Calculate the jitter so that we're within an acceptable range
|
|
uint32_t pos_jitter = 0;
|
|
uint32_t neg_jitter = 0;
|
|
// Currently unused but might form a part of a more dynamic
|
|
// solution to finding good jitter values in the future. @todo
|
|
uint32_t max_avg = 3650;
|
|
uint32_t min_avg = 1800;
|
|
|
|
// Store a audio_summary of resampling
|
|
DecklinkAudioSummary *audio_summary = nullptr;
|
|
|
|
static const uint32_t POS_JITTER_DEFAULT = 600;
|
|
static const uint32_t NEG_JITTER_DEFAULT = 600;
|
|
};
|
|
|
|
#define DECKLINK_MAGIC 0x12de326b
|
|
|
|
struct device_state {
|
|
PlaybackDelegate *delegate;
|
|
IDeckLink *deckLink;
|
|
IDeckLinkOutput *deckLinkOutput;
|
|
IDeckLinkConfiguration *deckLinkConfiguration;
|
|
IDeckLinkProfileAttributes *deckLinkAttributes;
|
|
};
|
|
|
|
struct state_decklink {
|
|
uint32_t magic = DECKLINK_MAGIC;
|
|
chrono::high_resolution_clock::time_point t0 = chrono::high_resolution_clock::now();
|
|
|
|
vector<struct device_state> state;
|
|
|
|
BMDTimeValue frameRateDuration{};
|
|
BMDTimeScale frameRateScale{};
|
|
|
|
DeckLinkTimecode *timecode{}; ///< @todo Should be actually allocated dynamically and
|
|
///< its lifespan controlled by AddRef()/Release() methods
|
|
|
|
struct video_desc vid_desc{};
|
|
struct audio_desc aud_desc{};
|
|
|
|
unsigned long int frames = 0;
|
|
bool stereo = false;
|
|
bool initialized_audio = false;
|
|
bool initialized_video = false;
|
|
bool emit_timecode = false;
|
|
int devices_cnt = 1;
|
|
bool play_audio = false; ///< the BMD device will be used also for output audio
|
|
|
|
BMDPixelFormat pixelFormat{};
|
|
|
|
uint32_t link_req = BMD_OPT_DEFAULT;
|
|
uint32_t profile_req = BMD_OPT_DEFAULT; // BMD_OPT_DEFAULT, BMD_OPT_KEEP, bmdDuplexHalf or one of BMDProfileID
|
|
char sdi_dual_channel_level = BMD_OPT_DEFAULT; // 'A' - level A, 'B' - level B
|
|
bool quad_square_division_split = true;
|
|
BMDVideoOutputConversionMode conversion_mode{};
|
|
HDRMetadata requested_hdr_mode{};
|
|
|
|
buffer_pool_t buffer_pool;
|
|
|
|
bool low_latency = true;
|
|
|
|
mutex reconfiguration_lock; ///< for audio and video reconf to be mutually exclusive
|
|
bool keep_device_defaults = false;
|
|
|
|
AudioDriftFixer audio_drift_fixer{250, 25, 2700, 5, 5};
|
|
DecklinkAudioSummary audio_summary{};
|
|
|
|
};
|
|
|
|
static void show_help(bool full);
|
|
|
|
static void show_help(bool full)
|
|
{
|
|
IDeckLinkIterator* deckLinkIterator;
|
|
IDeckLink* deckLink;
|
|
int numDevices = 0;
|
|
|
|
printf("Decklink (output) options:\n");
|
|
cout << style::bold << fg::red << "\t-d decklink" << fg::reset << "[:device=<device(s)>][:Level{A|B}][:3D][:audio_level={line|mic}][:half-duplex][:HDR[=<t>][:maxresample=<N>][:minresample=<N>][:targetbuffer=<N>][:drift_fix]]\n" << style::reset;
|
|
cout << style::bold << fg::red << "\t-d decklink" << fg::reset << ":[full]help\n" << style::reset;
|
|
cout << "Options:\n";
|
|
cout << style::bold << "\tfullhelp" << style::reset << "\tdisplay additional options and more details\n";
|
|
cout << style::bold << "\tdevice" << style::reset << "\t\tindex or name of output device (or comma-separated list of multple devices)\n";
|
|
cout << style::bold << "\tLevelA/LevelB" << style::reset << "\tspecifies 3G-SDI output level\n";
|
|
cout << style::bold << "\t3D" << style::reset << "\t\t3D stream will be received (see also HDMI3DPacking option)\n";
|
|
cout << style::bold << "\taudio_level" << style::reset << "\tset maximum attenuation for mic\n";
|
|
cout << style::bold << "\thalf-duplex" << style::reset
|
|
<< "\tset a profile that allows maximal number of simultaneous IOs\n";
|
|
cout << style::bold << "\tHDR[=HDR|PQ|HLG|<int>|help]" << style::reset << " - enable HDR metadata (optionally specifying EOTF, int 0-7 as per CEA 861.), help for extended help\n";
|
|
cout << style::bold << "\tdrift_fix" << style::reset << " activates a drift fix for the Decklink cards. The decklink card clocks will slowly drift overtime which can eventually cause a\n";
|
|
cout << "\tbuffer underflow or overflow. A dynamic resampler is utilised in order to stretch (or reduce) audio by a small amount to counter the drift.\n";
|
|
cout << style::bold << "\tmaxresample=<N> - " << style::reset << "The maximum amount the resample delta can be when scaling is applied. Measured in Hz.\n";
|
|
cout << style::bold << "\tminresample=<N> - " << style::reset << "The minimum amount the resample delta can be when scaling is applied. Measured in Hz.\n";
|
|
cout << style::bold << "\ttargetbuffer=<N> - " << style::reset << "The target amount of samples to have in the buffer (per channel).\n";
|
|
if (!full) {
|
|
cout << style::bold << "\tconversion" << style::reset << "\toutput size conversion, use '-d decklink:fullhelp' for list of conversions\n";
|
|
cout << "\t(other options available, use \"fullhelp\" to see complete list of options)\n";
|
|
} else {
|
|
cout << style::bold << "\tsingle-link/dual-link/quad-link" << style::reset << "\tspecifies if the video output will be in a single-link (HD/3G/6G/12G), dual-link HD-SDI mode or quad-link HD/3G/6G/12G\n";
|
|
cout << style::bold << "\ttimecode" << style::reset << "\temit timecode\n";
|
|
cout << style::bold << "\t[no-]quad-square" << style::reset << " set Quad-link SDI is output in Square Division Quad Split mode\n";
|
|
cout << style::bold << "\t[no-]low-latency" << style::reset << " do not use low-latency mode (use regular scheduled mode; low-latency is default)\n";
|
|
cout << style::bold << "\tconversion" << style::reset << "\toutput size conversion, can be:\n" <<
|
|
style::bold << "\t\tnone" << style::reset << " - no conversion\n" <<
|
|
style::bold << "\t\tltbx" << style::reset << " - down-converted letterbox SD\n" <<
|
|
style::bold << "\t\tamph" << style::reset << " - down-converted anamorphic SD\n" <<
|
|
style::bold << "\t\t720c" << style::reset << " - HD720 to HD1080 conversion\n" <<
|
|
style::bold << "\t\tHWlb" << style::reset << " - simultaneous output of HD and down-converted letterbox SD\n" <<
|
|
style::bold << "\t\tHWam" << style::reset << " - simultaneous output of HD and down-converted anamorphic SD\n" <<
|
|
style::bold << "\t\tHWcc" << style::reset << " - simultaneous output of HD and center cut SD\n" <<
|
|
style::bold << "\t\txcap" << style::reset << " - simultaneous output of 720p and 1080p cross-conversion\n" <<
|
|
style::bold << "\t\tua7p" << style::reset << " - simultaneous output of SD and up-converted anamorphic 720p\n" <<
|
|
style::bold << "\t\tua1i" << style::reset << " - simultaneous output of SD and up-converted anamorphic 1080i\n" <<
|
|
style::bold << "\t\tu47p" << style::reset << " - simultaneous output of SD and up-converted anamorphic widescreen aspect ratio 14:9 to 720p\n" <<
|
|
style::bold << "\t\tu41i" << style::reset << " - simultaneous output of SD and up-converted anamorphic widescreen aspect ratio 14:9 to 1080i\n" <<
|
|
style::bold << "\t\tup7p" << style::reset << " - simultaneous output of SD and up-converted pollarbox 720p\n" <<
|
|
style::bold << "\t\tup1i" << style::reset << " - simultaneous output of SD and up-converted pollarbox 1080i\n";
|
|
cout << style::bold << "\tHDMI3DPacking" << style::reset << " can be (used in conjunction with \"3D\" option):\n" <<
|
|
style::bold << "\t\tSideBySideHalf, LineByLine, TopAndBottom, FramePacking, LeftOnly, RightOnly\n" << style::reset;
|
|
cout << style::bold << "\tUse1080PsF[=true|false|keep]" << style::reset << " flag sets use of PsF on output instead of progressive (default is false)\n";
|
|
cout << style::bold << "\tprofile=<P>\n" << style::reset;
|
|
cout << "\t\tUse desired device profile: " << style::bold << "1dfd" << style::reset << ", "
|
|
<< style::bold << "1dhd" << style::reset << ", "
|
|
<< style::bold << "2dfd" << style::reset << ", "
|
|
<< style::bold << "2dhd" << style::reset << " or "
|
|
<< style::bold << "4dhd" << style::reset << ". See SDK manual for details. Use "
|
|
<< style::bold << "keep" << style::reset << " to disable automatic selection.\n";
|
|
cout << style::bold << "\tkeep-settings" << style::reset << "\tdo not apply any DeckLink settings by UG than required (keep user-selected defaults)\n";
|
|
}
|
|
|
|
cout << "Recognized pixel formats:";
|
|
for_each(uv_to_bmd_codec_map.cbegin(), uv_to_bmd_codec_map.cend(), [](auto const &i) { cout << " " << style::bold << get_codec_name(i.first) << style::reset; } );
|
|
cout << "\n";
|
|
|
|
// Create an IDeckLinkIterator object to enumerate all DeckLink cards in the system
|
|
deckLinkIterator = create_decklink_iterator(true);
|
|
if (deckLinkIterator == NULL) {
|
|
return;
|
|
}
|
|
|
|
// Enumerate all cards in this system
|
|
while (deckLinkIterator->Next(&deckLink) == S_OK)
|
|
{
|
|
string deviceName = bmd_get_device_name(deckLink);
|
|
if (deviceName.empty()) {
|
|
deviceName = "(unable to get name)";
|
|
}
|
|
|
|
// *** Print the model name of the DeckLink card
|
|
cout << "\ndevice: " << style::bold << numDevices << style::reset << ") "
|
|
<< style::bold << deviceName << style::reset << "\n";
|
|
print_output_modes(deckLink);
|
|
|
|
// Increment the total number of DeckLink cards found
|
|
numDevices++;
|
|
|
|
// Release the IDeckLink instance when we've finished with it to prevent leaks
|
|
deckLink->Release();
|
|
}
|
|
|
|
deckLinkIterator->Release();
|
|
|
|
decklink_uninitialize();
|
|
|
|
// If no DeckLink cards were found in the system, inform the user
|
|
if (numDevices == 0)
|
|
{
|
|
log_msg(LOG_LEVEL_WARNING, "\nNo Blackmagic Design devices were found.\n");
|
|
return;
|
|
}
|
|
|
|
printf("\n");
|
|
if (full) {
|
|
print_decklink_version();
|
|
printf("\n");
|
|
}
|
|
}
|
|
|
|
|
|
static struct video_frame *
|
|
display_decklink_getf(void *state)
|
|
{
|
|
struct state_decklink *s = (struct state_decklink *)state;
|
|
assert(s->magic == DECKLINK_MAGIC);
|
|
|
|
if (!s->initialized_video) {
|
|
return nullptr;
|
|
}
|
|
|
|
struct video_frame *out = vf_alloc_desc(s->vid_desc);
|
|
auto deckLinkFrames = new vector<IDeckLinkMutableVideoFrame *>(s->devices_cnt);
|
|
out->callbacks.dispose_udata = (void *) deckLinkFrames;
|
|
static auto dispose = [](struct video_frame *frame) {
|
|
delete (vector<IDeckLinkMutableVideoFrame *> *) frame->callbacks.dispose_udata;
|
|
vf_free(frame);
|
|
};
|
|
out->callbacks.dispose = dispose;
|
|
|
|
for (unsigned int i = 0; i < s->vid_desc.tile_count; ++i) {
|
|
const int linesize = vc_get_linesize(s->vid_desc.width, s->vid_desc.color_spec);
|
|
IDeckLinkMutableVideoFrame *deckLinkFrame = nullptr;
|
|
lock_guard<mutex> lg(s->buffer_pool.lock);
|
|
|
|
while (!s->buffer_pool.frame_queue.empty()) {
|
|
auto tmp = s->buffer_pool.frame_queue.front();
|
|
IDeckLinkMutableVideoFrame *frame;
|
|
if (s->stereo)
|
|
frame = dynamic_cast<DeckLink3DFrame *>(tmp);
|
|
else
|
|
frame = dynamic_cast<DeckLinkFrame *>(tmp);
|
|
s->buffer_pool.frame_queue.pop();
|
|
if (!frame || // wrong type
|
|
frame->GetWidth() != (long) s->vid_desc.width ||
|
|
frame->GetHeight() != (long) s->vid_desc.height ||
|
|
frame->GetRowBytes() != linesize ||
|
|
frame->GetPixelFormat() != s->pixelFormat) {
|
|
delete tmp;
|
|
} else {
|
|
deckLinkFrame = frame;
|
|
deckLinkFrame->AddRef();
|
|
break;
|
|
}
|
|
}
|
|
if (!deckLinkFrame) {
|
|
if (s->stereo)
|
|
deckLinkFrame = DeckLink3DFrame::Create(s->vid_desc.width,
|
|
s->vid_desc.height, linesize,
|
|
s->pixelFormat, s->buffer_pool, s->requested_hdr_mode);
|
|
else
|
|
deckLinkFrame = DeckLinkFrame::Create(s->vid_desc.width,
|
|
s->vid_desc.height, linesize,
|
|
s->pixelFormat, s->buffer_pool, s->requested_hdr_mode);
|
|
}
|
|
(*deckLinkFrames)[i] = deckLinkFrame;
|
|
|
|
deckLinkFrame->GetBytes((void **) &out->tiles[i].data);
|
|
|
|
if (s->stereo) {
|
|
IDeckLinkVideoFrame *deckLinkFrameRight = nullptr;
|
|
DeckLink3DFrame *frame3D = dynamic_cast<DeckLink3DFrame *>(deckLinkFrame);
|
|
assert(frame3D != nullptr);
|
|
frame3D->GetFrameForRightEye(&deckLinkFrameRight);
|
|
deckLinkFrameRight->GetBytes((void **) &out->tiles[1].data);
|
|
// release immedieatelly (parent still holds the reference)
|
|
deckLinkFrameRight->Release();
|
|
|
|
++i;
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
static void update_timecode(DeckLinkTimecode *tc, double fps)
|
|
{
|
|
const float epsilon = 0.005;
|
|
uint8_t hours, minutes, seconds, frames;
|
|
BMDTimecodeBCD bcd;
|
|
bool dropFrame = false;
|
|
|
|
if(ceil(fps) - fps > epsilon) { /* NTSCi drop framecode */
|
|
dropFrame = true;
|
|
}
|
|
|
|
tc->GetComponents (&hours, &minutes, &seconds, &frames);
|
|
frames++;
|
|
|
|
if((double) frames > fps - epsilon) {
|
|
frames = 0;
|
|
seconds++;
|
|
if(seconds >= 60) {
|
|
seconds = 0;
|
|
minutes++;
|
|
if(dropFrame) {
|
|
if(minutes % 10 != 0)
|
|
seconds = 2;
|
|
}
|
|
if(minutes >= 60) {
|
|
minutes = 0;
|
|
hours++;
|
|
if(hours >= 24) {
|
|
hours = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bcd = (frames % 10) | (frames / 10) << 4 | (seconds % 10) << 8 | (seconds / 10) << 12 | (minutes % 10) << 16 | (minutes / 10) << 20 |
|
|
(hours % 10) << 24 | (hours / 10) << 28;
|
|
|
|
tc->SetBCD(bcd);
|
|
}
|
|
|
|
static int display_decklink_putf(void *state, struct video_frame *frame, long long timeout_ns)
|
|
{
|
|
struct state_decklink *s = (struct state_decklink *)state;
|
|
|
|
if (frame == NULL)
|
|
return FALSE;
|
|
|
|
assert(s->magic == DECKLINK_MAGIC);
|
|
/*
|
|
timeInFrame is not dcoumented in the SDK, so putting this here for information.
|
|
The timeInFrame loops in range from 0 to ticksPerFrame-1 for each frame
|
|
The timeInFrame is reset by EOF interrupt from DeckLink hardware on output
|
|
After IDeckLinkOutput::EnableVideoOutput, there is some relocking of output to requested video mode,
|
|
in this time there are no EOF signals, so the timeInFrame is not reset.
|
|
There is slight variability in the lock time between runs as EnableVideoOutput is called from different timepoint in frame period.
|
|
The below will give an idea of the skew between the source clock and the blackmagic hardware clock.
|
|
*/
|
|
BMDTimeValue blk_start_time = 0;
|
|
BMDTimeValue blk_start_timeInFrame = 0;
|
|
BMDTimeValue blk_start_ticksPerFrame =0;
|
|
s->state.at(0).deckLinkOutput->GetHardwareReferenceClock(s->frameRateScale, &blk_start_time, &blk_start_timeInFrame, &blk_start_ticksPerFrame);
|
|
|
|
uint32_t i;
|
|
s->state.at(0).deckLinkOutput->GetBufferedVideoFrameCount(&i);
|
|
long long max_frames = DIV_ROUNDED_UP(timeout_ns, (long long)(NS_IN_SEC / frame->fps));
|
|
if (timeout_ns == PUTF_DISCARD || i > max_frames) {
|
|
if (timeout_ns != PUTF_DISCARD) {
|
|
log_msg(LOG_LEVEL_WARNING, MOD_NAME "Frame dropped!\n");
|
|
}
|
|
for (int j = 0; j < s->devices_cnt; ++j) {
|
|
IDeckLinkMutableVideoFrame *deckLinkFrame =
|
|
(*((vector<IDeckLinkMutableVideoFrame *> *) frame->callbacks.dispose_udata))[j];
|
|
deckLinkFrame->Release();
|
|
}
|
|
frame->callbacks.dispose(frame);
|
|
return 1;
|
|
}
|
|
|
|
auto t0 = chrono::high_resolution_clock::now();
|
|
|
|
if (frame->color_spec == R10k && get_commandline_param(R10K_FULL_OPT) == nullptr) {
|
|
for (unsigned i = 0; i < frame->tile_count; ++i) {
|
|
r10k_full_to_limited(frame->tiles[i].data, frame->tiles[i].data, frame->tiles[i].data_len);
|
|
}
|
|
}
|
|
|
|
for (int j = 0; j < s->devices_cnt; ++j) {
|
|
IDeckLinkMutableVideoFrame *deckLinkFrame =
|
|
(*((vector<IDeckLinkMutableVideoFrame *> *) frame->callbacks.dispose_udata))[j];
|
|
if(s->emit_timecode) {
|
|
deckLinkFrame->SetTimecode(bmdTimecodeRP188Any, s->timecode);
|
|
}
|
|
|
|
if (s->low_latency) {
|
|
s->state[j].deckLinkOutput->DisplayVideoFrameSync(deckLinkFrame);
|
|
deckLinkFrame->Release();
|
|
} else {
|
|
s->state[j].deckLinkOutput->ScheduleVideoFrame(deckLinkFrame,
|
|
s->frames * s->frameRateDuration, s->frameRateDuration, s->frameRateScale);
|
|
}
|
|
}
|
|
s->frames++;
|
|
if(s->emit_timecode) {
|
|
update_timecode(s->timecode, s->vid_desc.fps);
|
|
}
|
|
|
|
frame->callbacks.dispose(frame);
|
|
|
|
BMDTimeValue blk_end_time = 0;
|
|
BMDTimeValue blk_end_timeInFrame = 0;
|
|
BMDTimeValue blk_end_ticksPerFrame =0;
|
|
BMDTimeValue blk_write_duration =0;
|
|
|
|
s->state.at(0).deckLinkOutput->GetHardwareReferenceClock(s->frameRateScale, &blk_end_time, &blk_end_timeInFrame, &blk_end_ticksPerFrame);
|
|
if (blk_end_timeInFrame >= blk_start_timeInFrame){
|
|
blk_write_duration = blk_end_timeInFrame - blk_start_timeInFrame;
|
|
} else{
|
|
// we have wrapped
|
|
BMDTimeValue end_time = blk_end_timeInFrame + blk_end_ticksPerFrame;
|
|
blk_write_duration = end_time - blk_start_timeInFrame;
|
|
}
|
|
LOG(LOG_LEVEL_DEBUG) << MOD_NAME << " putf Video inframe " << blk_start_timeInFrame << " start, "
|
|
<< blk_end_timeInFrame << " end, "
|
|
<< blk_write_duration << " duration.\n";
|
|
if (blk_end_timeInFrame - blk_start_timeInFrame > 80) {
|
|
LOG(LOG_LEVEL_DEBUG) << MOD_NAME << " Video Inframe took longer than expected " << blk_write_duration<<"\n";
|
|
}
|
|
LOG(LOG_LEVEL_DEBUG) << MOD_NAME << " putf Video BlkMagic clock " << blk_start_time << " start, "
|
|
<< blk_end_time << " end, "
|
|
<< blk_end_time - blk_start_time <<" duration.\n";
|
|
auto t1 = chrono::high_resolution_clock::now();
|
|
LOG(LOG_LEVEL_DEBUG) << MOD_NAME "putf - " << i << " frames buffered, lasted " << setprecision(2) << chrono::duration_cast<chrono::duration<double>>(t1 - t0).count() * 1000.0 << " ms.\n";
|
|
|
|
if (chrono::duration_cast<chrono::seconds>(t1 - s->t0).count() > 5) {
|
|
LOG(LOG_LEVEL_VERBOSE) << MOD_NAME << s->state.at(0).delegate->frames_late << " frames late, "
|
|
<< s->state.at(0).delegate->frames_dropped << " dropped, "
|
|
<< s->state.at(0).delegate->frames_flushed << " flushed cumulative\n";
|
|
s->t0 = t1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static BMDDisplayMode get_mode(IDeckLinkOutput *deckLinkOutput, struct video_desc desc, BMDTimeValue *frameRateDuration,
|
|
BMDTimeScale *frameRateScale)
|
|
{ IDeckLinkDisplayModeIterator *displayModeIterator;
|
|
IDeckLinkDisplayMode* deckLinkDisplayMode;
|
|
BMDDisplayMode displayMode = bmdModeUnknown;
|
|
|
|
// Populate the display mode combo with a list of display modes supported by the installed DeckLink card
|
|
if (FAILED(deckLinkOutput->GetDisplayModeIterator(&displayModeIterator)))
|
|
{
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Fatal: cannot create display mode iterator.\n");
|
|
return bmdModeUnknown;
|
|
}
|
|
|
|
while (displayModeIterator->Next(&deckLinkDisplayMode) == S_OK)
|
|
{
|
|
BMD_STR modeNameString;
|
|
if (deckLinkDisplayMode->GetName(&modeNameString) == S_OK)
|
|
{
|
|
char *modeNameCString = get_cstr_from_bmd_api_str(modeNameString);
|
|
if (deckLinkDisplayMode->GetWidth() == (long) desc.width &&
|
|
deckLinkDisplayMode->GetHeight() == (long) desc.height)
|
|
{
|
|
double displayFPS;
|
|
BMDFieldDominance dominance;
|
|
bool interlaced;
|
|
|
|
dominance = deckLinkDisplayMode->GetFieldDominance();
|
|
if (dominance == bmdLowerFieldFirst ||
|
|
dominance == bmdUpperFieldFirst) {
|
|
if (dominance == bmdLowerFieldFirst) {
|
|
log_msg(LOG_LEVEL_WARNING, MOD_NAME "Lower field first format detected, fields can be switched! If so, please report a bug to " PACKAGE_BUGREPORT "\n");
|
|
}
|
|
interlaced = true;
|
|
} else { // progressive, psf, unknown
|
|
interlaced = false;
|
|
}
|
|
|
|
deckLinkDisplayMode->GetFrameRate(frameRateDuration,
|
|
frameRateScale);
|
|
displayFPS = (double) *frameRateScale / *frameRateDuration;
|
|
if(fabs(desc.fps - displayFPS) < 0.01 && (desc.interlacing == INTERLACED_MERGED ? interlaced : !interlaced)
|
|
)
|
|
{
|
|
log_msg(LOG_LEVEL_INFO, MOD_NAME "Selected mode: %s\n", modeNameCString);
|
|
displayMode = deckLinkDisplayMode->GetDisplayMode();
|
|
release_bmd_api_str(modeNameString);
|
|
free(modeNameCString);
|
|
deckLinkDisplayMode->Release();
|
|
break;
|
|
}
|
|
}
|
|
release_bmd_api_str(modeNameString);
|
|
free((void *) modeNameCString);
|
|
}
|
|
deckLinkDisplayMode->Release();
|
|
}
|
|
displayModeIterator->Release();
|
|
|
|
return displayMode;
|
|
}
|
|
|
|
/**
|
|
* @todo
|
|
* In non-low-latency mode, StopScheduledPlayback should be called. However, since this
|
|
* function is called from different thread than audio-related stuff and these things
|
|
* are not synchronized in any way, it looks like to be more appropriate not to call it,
|
|
* as it doesn't break things up. In low latency mode, this is not an issue.
|
|
*/
|
|
static int
|
|
display_decklink_reconfigure_video(void *state, struct video_desc desc)
|
|
{
|
|
struct state_decklink *s = (struct state_decklink *)state;
|
|
|
|
BMDDisplayMode displayMode;
|
|
BMD_BOOL supported;
|
|
HRESULT result;
|
|
|
|
unique_lock<mutex> lk(s->reconfiguration_lock);
|
|
|
|
assert(s->magic == DECKLINK_MAGIC);
|
|
|
|
s->vid_desc = desc;
|
|
|
|
if (desc.color_spec == R10k && get_commandline_param(R10K_FULL_OPT) == nullptr) {
|
|
log_msg(LOG_LEVEL_WARNING, MOD_NAME "Using limited range R10k as specified by BMD, use '--param "
|
|
R10K_FULL_OPT "' to override.\n");
|
|
}
|
|
|
|
auto it = std::find_if(uv_to_bmd_codec_map.begin(),
|
|
uv_to_bmd_codec_map.end(),
|
|
[&desc](const std::pair<codec_t, BMDPixelFormat>& el){ return el.first == desc.color_spec; });
|
|
if (it == uv_to_bmd_codec_map.end()) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Unsupported pixel format!\n");
|
|
goto error;
|
|
}
|
|
s->pixelFormat = it->second;
|
|
|
|
if (s->initialized_video) {
|
|
for (int i = 0; i < s->devices_cnt; ++i) {
|
|
CALL_AND_CHECK(s->state.at(i).deckLinkOutput->DisableVideoOutput(),
|
|
"DisableVideoOutput");
|
|
}
|
|
s->initialized_video = false;
|
|
}
|
|
|
|
if (s->stereo) {
|
|
bmd_check_stereo_profile(s->state.at(0).deckLink);
|
|
if ((int) desc.tile_count != 2) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "In stereo mode exactly "
|
|
"2 streams expected, %d received.\n", desc.tile_count);
|
|
goto error;
|
|
}
|
|
} else {
|
|
if ((int) desc.tile_count == 2) {
|
|
log_msg(LOG_LEVEL_WARNING, MOD_NAME "Received 2 streams but stereo mode is not enabled! Did you forget a \"3D\" parameter?\n");
|
|
}
|
|
if ((int) desc.tile_count > s->devices_cnt) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Expected at most %d streams. Got %d.\n", s->devices_cnt,
|
|
desc.tile_count);
|
|
goto error;
|
|
} else if ((int) desc.tile_count < s->devices_cnt) {
|
|
log_msg(LOG_LEVEL_WARNING, MOD_NAME "Received %d streams but %d devices are used!.\n", desc.tile_count, s->devices_cnt);
|
|
}
|
|
}
|
|
|
|
|
|
for (int i = 0; i < s->devices_cnt; ++i) {
|
|
BMDVideoOutputFlags outputFlags= bmdVideoOutputFlagDefault;
|
|
BMDSupportedVideoModeFlags supportedFlags = bmdSupportedVideoModeDefault;
|
|
|
|
displayMode = get_mode(s->state.at(i).deckLinkOutput, desc, &s->frameRateDuration,
|
|
&s->frameRateScale);
|
|
if (displayMode == bmdModeUnknown) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Could not find suitable video mode.\n");
|
|
goto error;
|
|
}
|
|
|
|
if (s->emit_timecode) {
|
|
outputFlags = (BMDVideoOutputFlags) (outputFlags | bmdVideoOutputRP188);
|
|
}
|
|
|
|
if (s->stereo) {
|
|
outputFlags = (BMDVideoOutputFlags) (outputFlags | bmdVideoOutputDualStream3D);
|
|
supportedFlags = (BMDSupportedVideoModeFlags) (supportedFlags | bmdSupportedVideoModeDualStream3D);
|
|
}
|
|
|
|
BMD_BOOL subsampling_444 = codec_is_a_rgb(desc.color_spec); // we don't have pixfmt for 444 YCbCr
|
|
CALL_AND_CHECK(s->state.at(i).deckLinkConfiguration->SetFlag(bmdDeckLinkConfig444SDIVideoOutput, subsampling_444),
|
|
"SDI subsampling");
|
|
|
|
uint32_t link = s->link_req;
|
|
|
|
if (!s->keep_device_defaults && s->link_req == BMD_OPT_DEFAULT) {
|
|
if (desc.width != 7680) {
|
|
link = bmdLinkConfigurationSingleLink;
|
|
LOG(LOG_LEVEL_NOTICE) << MOD_NAME "Setting single link by default.\n";
|
|
} else {
|
|
link = bmdLinkConfigurationQuadLink;
|
|
LOG(LOG_LEVEL_NOTICE) << MOD_NAME "Setting quad-link for 8K by default.\n";
|
|
}
|
|
}
|
|
CALL_AND_CHECK(s->state.at(i).deckLinkConfiguration->SetInt(bmdDeckLinkConfigSDIOutputLinkConfiguration, link), "Unable set output SDI link mode");
|
|
|
|
if (!s->keep_device_defaults && s->profile_req == BMD_OPT_DEFAULT && link == bmdLinkConfigurationQuadLink) {
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME "Quad-link detected - setting 1-subdevice-1/2-duplex profile automatically, use 'profile=keep' to override.\n";
|
|
decklink_set_duplex(s->state.at(i).deckLink, bmdProfileOneSubDeviceHalfDuplex);
|
|
} else if (link == bmdLinkConfigurationQuadLink && (s->profile_req != BMD_OPT_KEEP && s->profile_req == bmdProfileOneSubDeviceHalfDuplex)) {
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME "Setting quad-link and an incompatible device profile may not be supported!\n";
|
|
}
|
|
|
|
BMD_BOOL quad_link_supp;
|
|
if (s->state.at(i).deckLinkAttributes != nullptr && s->state.at(i).deckLinkAttributes->GetFlag(BMDDeckLinkSupportsQuadLinkSDI, &quad_link_supp) == S_OK && quad_link_supp == BMD_TRUE) {
|
|
CALL_AND_CHECK(s->state.at(i).deckLinkConfiguration->SetFlag(bmdDeckLinkConfigQuadLinkSDIVideoOutputSquareDivisionSplit, s->quad_square_division_split),
|
|
"Quad-link SDI Square Division Quad Split mode");
|
|
}
|
|
|
|
EXIT_IF_FAILED(s->state.at(i).deckLinkOutput->DoesSupportVideoMode(bmdVideoConnectionUnspecified, displayMode, s->pixelFormat,
|
|
IF_NOT_NULL_ELSE(s->conversion_mode, static_cast<BMDVideoOutputConversionMode>(bmdNoVideoOutputConversion)), supportedFlags, nullptr, &supported), "DoesSupportVideoMode");
|
|
if (!supported) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Requested parameters "
|
|
"combination not supported - %d * %dx%d@%f, timecode %s.\n",
|
|
desc.tile_count, desc.width, desc.height, desc.fps,
|
|
(outputFlags & bmdVideoOutputRP188 ? "ON": "OFF"));
|
|
goto error;
|
|
}
|
|
|
|
result = s->state.at(i).deckLinkOutput->EnableVideoOutput(displayMode, outputFlags);
|
|
if (FAILED(result)) {
|
|
if (result == E_ACCESSDENIED) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Unable to access the hardware or output "
|
|
"stream currently active (another application using it?).\n");
|
|
} else {
|
|
LOG(LOG_LEVEL_ERROR) << MOD_NAME << "EnableVideoOutput: " << bmd_hresult_to_string(result) << "\n";\
|
|
}
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
// This workaround is needed (at least) for Decklink Extreme 4K when capturing
|
|
// (possibly from another process) and when playback is in low-latency mode.
|
|
// When video is enabled after audio, audio playback becomes silent without
|
|
// an error.
|
|
if (s->initialized_audio) {
|
|
EXIT_IF_FAILED(s->state[0].deckLinkOutput->DisableAudioOutput(), "DisableAudioOutput");
|
|
EXIT_IF_FAILED(s->state[0].deckLinkOutput->EnableAudioOutput(bmdAudioSampleRate48kHz,
|
|
s->aud_desc.bps == 2 ? bmdAudioSampleType16bitInteger : bmdAudioSampleType32bitInteger,
|
|
s->aud_desc.ch_count,
|
|
bmdAudioOutputStreamContinuous),
|
|
"EnableAudioOutput");
|
|
}
|
|
|
|
if (!s->low_latency) {
|
|
for(int i = 0; i < s->devices_cnt; ++i) {
|
|
EXIT_IF_FAILED(s->state.at(i).deckLinkOutput->StartScheduledPlayback(0, s->frameRateScale, (double) s->frameRateDuration), "StartScheduledPlayback (video)");
|
|
}
|
|
}
|
|
|
|
s->initialized_video = true;
|
|
return TRUE;
|
|
|
|
error:
|
|
// in case we are partially initialized, deinitialize
|
|
for (int i = 0; i < s->devices_cnt; ++i) {
|
|
if (!s->low_latency) {
|
|
s->state.at(i).deckLinkOutput->StopScheduledPlayback (0, nullptr, 0);
|
|
}
|
|
s->state.at(i).deckLinkOutput->DisableVideoOutput();
|
|
}
|
|
s->initialized_video = false;
|
|
return FALSE;
|
|
}
|
|
|
|
static void display_decklink_probe(struct device_info **available_cards, int *count, void (**deleter)(void *))
|
|
{
|
|
UNUSED(deleter);
|
|
IDeckLinkIterator* deckLinkIterator;
|
|
IDeckLink* deckLink;
|
|
|
|
*count = 0;
|
|
*available_cards = nullptr;
|
|
|
|
deckLinkIterator = create_decklink_iterator(false);
|
|
if (deckLinkIterator == NULL) {
|
|
return;
|
|
}
|
|
|
|
// Enumerate all cards in this system
|
|
while (deckLinkIterator->Next(&deckLink) == S_OK)
|
|
{
|
|
string deviceName = bmd_get_device_name(deckLink);
|
|
if (deviceName.empty()) {
|
|
deviceName = "(unknown)";
|
|
}
|
|
|
|
*count += 1;
|
|
*available_cards = (struct device_info *)
|
|
realloc(*available_cards, *count * sizeof(struct device_info));
|
|
memset(*available_cards + *count - 1, 0, sizeof(struct device_info));
|
|
sprintf((*available_cards)[*count - 1].dev, ":device=%d", *count - 1);
|
|
sprintf((*available_cards)[*count - 1].extra, "\"embeddedAudioAvailable\":\"t\"");
|
|
(*available_cards)[*count - 1].repeatable = false;
|
|
|
|
strncpy((*available_cards)[*count - 1].name, deviceName.c_str(),
|
|
sizeof (*available_cards)[*count - 1].name - 1);
|
|
|
|
// Release the IDeckLink instance when we've finished with it to prevent leaks
|
|
deckLink->Release();
|
|
}
|
|
|
|
deckLinkIterator->Release();
|
|
decklink_uninitialize();
|
|
}
|
|
|
|
|
|
/**
|
|
* @brief A helper function for parsing unsigned integers out of the command line parameters. This will not allow negative numbers
|
|
* or numbers that are longer than 9 digits long (this stops undefined behaviour occuring). Any error case should apply a
|
|
* default value.
|
|
*
|
|
* @param value_str The string that is being parsed.
|
|
* @param value_name The name of the parameter that is being parsed
|
|
* @param value A pointer to a uint32 to write the parsed value into
|
|
* @param default_value The default value that should be applied in any of the error cases.
|
|
*/
|
|
static void parse_uint32(const char *value_str, const char *value_name, uint32_t *value, uint32_t default_value) {
|
|
int value_len = strlen(value_str);
|
|
if(value_len == 0) {
|
|
LOG(LOG_LEVEL_ERROR) << MOD_NAME << "Empty string for option - " << value_name << " - Setting to default " << default_value << "\n";
|
|
*value = default_value;
|
|
return;
|
|
}
|
|
else if(value_len > 9) {
|
|
LOG(LOG_LEVEL_ERROR) << MOD_NAME << "Inputted string too large - " << value_name << " - Setting to default " << default_value << "\n";
|
|
*value = default_value;
|
|
return;
|
|
}
|
|
int tmp_value = atoi(value_str);
|
|
if(tmp_value < 1) {
|
|
LOG(LOG_LEVEL_ERROR) << MOD_NAME << "Inputted resample string negative number or not a valid number - " << value_name << " - Setting to default " << default_value << "\n";
|
|
*value = default_value;
|
|
}
|
|
else {
|
|
*value = tmp_value;
|
|
}
|
|
}
|
|
|
|
static auto parse_devices(const char *devices_str, vector<string> *cardId) {
|
|
if (strlen(devices_str) == 0) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Empty device string!\n");
|
|
return false;
|
|
}
|
|
char *save_ptr;
|
|
char *tmp = strdup(devices_str);
|
|
char *ptr = tmp;
|
|
char *item;
|
|
while ((item = strtok_r(ptr, ",", &save_ptr))) {
|
|
cardId->push_back(item);
|
|
ptr = NULL;
|
|
}
|
|
free(tmp);
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool settings_init(struct state_decklink *s, const char *fmt,
|
|
vector<string> *cardId,
|
|
BMDVideo3DPackingFormat *HDMI3DPacking,
|
|
int *audio_consumer_levels,
|
|
int *use1080psf) {
|
|
if (strlen(fmt) == 0) {
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME "Card number unset, using first found (see -d decklink:help)!\n";
|
|
return true;
|
|
}
|
|
|
|
auto tmp = static_cast<char *>(alloca(strlen(fmt) + 1));
|
|
strcpy(tmp, fmt);
|
|
char *ptr;
|
|
char *save_ptr = nullptr;
|
|
|
|
ptr = strtok_r(tmp, ":", &save_ptr);
|
|
assert(ptr != nullptr);
|
|
int i = 0;
|
|
bool first_option_is_device = true;
|
|
while (ptr[i] != '\0') {
|
|
if (!isdigit(ptr[i]) && ptr[i] != ',') {
|
|
first_option_is_device = false;
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
if (first_option_is_device) {
|
|
log_msg(LOG_LEVEL_WARNING, MOD_NAME "Unnamed device index "
|
|
"deprecated. Use \"device=%s\" instead.\n", ptr);
|
|
if (!parse_devices(ptr, cardId)) {
|
|
return false;
|
|
}
|
|
ptr = strtok_r(nullptr, ":", &save_ptr);
|
|
}
|
|
|
|
while (ptr != nullptr) {
|
|
if (strncasecmp(ptr, "device=", strlen("device=")) == 0) {
|
|
if (!parse_devices(ptr + strlen("device="), cardId)) {
|
|
return false;
|
|
}
|
|
|
|
} else if (strcasecmp(ptr, "3D") == 0) {
|
|
s->stereo = true;
|
|
} else if (strcasecmp(ptr, "timecode") == 0) {
|
|
s->emit_timecode = true;
|
|
} else if (strcasecmp(ptr, "single-link") == 0) {
|
|
s->link_req = bmdLinkConfigurationSingleLink;
|
|
} else if (strcasecmp(ptr, "dual-link") == 0) {
|
|
s->link_req = bmdLinkConfigurationDualLink;
|
|
} else if (strcasecmp(ptr, "quad-link") == 0) {
|
|
s->link_req = bmdLinkConfigurationQuadLink;
|
|
} else if (strstr(ptr, "profile=") == ptr) {
|
|
ptr += strlen("profile=");
|
|
if (strcmp(ptr, "keep") == 0) {
|
|
s->profile_req = BMD_OPT_KEEP;
|
|
} else {
|
|
s->profile_req = (BMDProfileID) bmd_read_fourcc(ptr);
|
|
}
|
|
} else if (strcasecmp(ptr, "half-duplex") == 0) {
|
|
s->profile_req = bmdDuplexHalf;
|
|
} else if (strcasecmp(ptr, "LevelA") == 0) {
|
|
s->sdi_dual_channel_level = 'A';
|
|
} else if (strcasecmp(ptr, "LevelB") == 0) {
|
|
s->sdi_dual_channel_level = 'B';
|
|
} else if (strncasecmp(ptr, "HDMI3DPacking=", strlen("HDMI3DPacking=")) == 0) {
|
|
char *packing = ptr + strlen("HDMI3DPacking=");
|
|
if (strcasecmp(packing, "SideBySideHalf") == 0) {
|
|
*HDMI3DPacking = bmdVideo3DPackingSidebySideHalf;
|
|
} else if (strcasecmp(packing, "LineByLine") == 0) {
|
|
*HDMI3DPacking = bmdVideo3DPackingLinebyLine;
|
|
} else if (strcasecmp(packing, "TopAndBottom") == 0) {
|
|
*HDMI3DPacking = bmdVideo3DPackingTopAndBottom;
|
|
} else if (strcasecmp(packing, "FramePacking") == 0) {
|
|
*HDMI3DPacking = bmdVideo3DPackingFramePacking;
|
|
} else if (strcasecmp(packing, "LeftOnly") == 0) {
|
|
*HDMI3DPacking = bmdVideo3DPackingRightOnly;
|
|
} else if (strcasecmp(packing, "RightOnly") == 0) {
|
|
*HDMI3DPacking = bmdVideo3DPackingLeftOnly;
|
|
} else {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Unknown HDMI 3D packing %s.\n", packing);
|
|
return false;
|
|
}
|
|
} else if (strncasecmp(ptr, "audio_level=", strlen("audio_level=")) == 0) {
|
|
if (strcasecmp(ptr + strlen("audio_level="), "false") || strcasecmp(ptr + strlen("audio_level="), "mic") == 0) {
|
|
*audio_consumer_levels = 0;
|
|
} else {
|
|
*audio_consumer_levels = 1;
|
|
}
|
|
} else if (strncasecmp(ptr, "conversion=",
|
|
strlen("conversion=")) == 0) {
|
|
s->conversion_mode = (BMDVideoOutputConversionMode) bmd_read_fourcc(ptr + strlen("conversion="));
|
|
} else if (is_prefix_of(ptr, "Use1080pNotPsF") || is_prefix_of(ptr, "Use1080PsF")) {
|
|
if ((*use1080psf = parse_bmd_flag(strchr(ptr, '=') + 1)) == -1) {
|
|
return false;
|
|
}
|
|
if (strncasecmp(ptr, "Use1080pNotPsF", strlen("Use1080pNotPsF")) == 0) { // compat, inverse
|
|
*use1080psf = invert_bmd_flag(*use1080psf);
|
|
}
|
|
} else if (strcasecmp(ptr, "low-latency") == 0 || strcasecmp(ptr, "no-low-latency") == 0) {
|
|
s->low_latency = strcasecmp(ptr, "low-latency") == 0;
|
|
} else if (strcasecmp(ptr, "quad-square") == 0 || strcasecmp(ptr, "no-quad-square") == 0) {
|
|
s->quad_square_division_split = strcasecmp(ptr, "quad-square") == 0;
|
|
} else if (strncasecmp(ptr, "hdr", strlen("hdr")) == 0) {
|
|
s->requested_hdr_mode.EOTF = static_cast<int64_t>(HDR_EOTF::HDR); // default
|
|
if (strncasecmp(ptr, "hdr=", strlen("hdr=")) == 0) {
|
|
try {
|
|
s->requested_hdr_mode.Init(ptr + strlen("hdr="));
|
|
} catch (ug_no_error const &e) {
|
|
return false;
|
|
} catch (exception const &e) {
|
|
LOG(LOG_LEVEL_ERROR) << MOD_NAME << "HDR mode init: " << e.what() << "\n";
|
|
return false;
|
|
}
|
|
}
|
|
} else if (strstr(ptr, "keep-settings") == ptr) {
|
|
s->keep_device_defaults = true;
|
|
} else if (strstr(ptr, "drift_fix") == ptr) {
|
|
s->audio_drift_fixer.m_enabled = true;
|
|
}
|
|
else if (strncasecmp(ptr, "maxresample=", strlen("maxresample=")) == 0) {
|
|
uint32_t max_resample_delta = 0;
|
|
parse_uint32(ptr + strlen("maxresample="), "maxresample", &max_resample_delta, MAX_RESAMPLE_DELTA_DEFAULT);
|
|
s->audio_drift_fixer.set_max_hz(max_resample_delta);
|
|
LOG(LOG_LEVEL_INFO) << MOD_NAME << "Set Max Resample Delta to be " << max_resample_delta << "Hz\n";
|
|
}
|
|
else if (strncasecmp(ptr, "minresample=", strlen("minresample=")) == 0) {
|
|
uint32_t min_resample_delta = 0;
|
|
parse_uint32(ptr + strlen("minresample="), "minresample", &min_resample_delta, MIN_RESAMPLE_DELTA_DEFAULT);
|
|
s->audio_drift_fixer.set_min_hz(min_resample_delta);
|
|
LOG(LOG_LEVEL_INFO) << MOD_NAME << "Set Min Resample Delta to be " << min_resample_delta << "Hz\n";
|
|
}else if (strncasecmp(ptr, "targetbuffer=", strlen("targetbuffer=")) == 0) {
|
|
uint32_t target_buffer = 0;
|
|
parse_uint32(ptr + strlen("targetbuffer="), "targetbuffer",&target_buffer, TARGET_BUFFER_DEFAULT);
|
|
s->audio_drift_fixer.set_target_buffer(target_buffer);
|
|
LOG(LOG_LEVEL_INFO) << MOD_NAME << "Set Target Buffer to be " << target_buffer << " samples in buffer (per channel)\n";
|
|
} else {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Warning: unknown options in config string.\n");
|
|
return false;
|
|
}
|
|
ptr = strtok_r(nullptr, ":", &save_ptr);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void *display_decklink_init(struct module *parent, const char *fmt, unsigned int flags)
|
|
{
|
|
IDeckLinkIterator* deckLinkIterator;
|
|
HRESULT result;
|
|
vector<string> cardId;
|
|
int dnum = 0;
|
|
IDeckLinkConfiguration* deckLinkConfiguration = NULL;
|
|
// for Decklink Studio which has switchable XLR - analog 3 and 4 or AES/EBU 3,4 and 5,6
|
|
BMDAudioOutputAnalogAESSwitch audioConnection = (BMDAudioOutputAnalogAESSwitch) 0;
|
|
BMDVideo3DPackingFormat HDMI3DPacking = (BMDVideo3DPackingFormat) BMD_OPT_DEFAULT;
|
|
int audio_consumer_levels = -1;
|
|
int use1080psf = BMD_OPT_DEFAULT;
|
|
|
|
if (strcmp(fmt, "help") == 0 || strcmp(fmt, "fullhelp") == 0) {
|
|
show_help(strcmp(fmt, "fullhelp") == 0);
|
|
return INIT_NOERR;
|
|
}
|
|
|
|
if (!blackmagic_api_version_check()) {
|
|
return NULL;
|
|
}
|
|
|
|
struct state_decklink *s = new state_decklink();
|
|
s->magic = DECKLINK_MAGIC;
|
|
s->audio_drift_fixer.set_root(get_root_module(parent));
|
|
s->stereo = FALSE;
|
|
s->emit_timecode = false;
|
|
s->profile_req = BMD_OPT_DEFAULT;
|
|
s->link_req = 0;
|
|
s->devices_cnt = 1;
|
|
s->low_latency = true;
|
|
s->audio_drift_fixer.set_summary(&(s->audio_summary));
|
|
|
|
if (!settings_init(s, fmt, &cardId, &HDMI3DPacking, &audio_consumer_levels, &use1080psf)) {
|
|
delete s;
|
|
return NULL;
|
|
}
|
|
|
|
if (cardId.empty()) {
|
|
cardId.emplace_back("0");
|
|
}
|
|
s->devices_cnt = cardId.size();
|
|
|
|
if (s->stereo && s->devices_cnt > 1) {
|
|
LOG(LOG_LEVEL_ERROR) << MOD_NAME "Unsupported configuration - in stereo "
|
|
"mode, exactly one device index must be given.\n";
|
|
delete s;
|
|
return NULL;
|
|
}
|
|
|
|
if (s->low_latency) {
|
|
LOG(LOG_LEVEL_NOTICE) << MOD_NAME "Using low-latency mode. "
|
|
"In case of problems, you can try '-d decklink:no-low-latency'.\n";
|
|
}
|
|
|
|
// Initialize the DeckLink API
|
|
deckLinkIterator = create_decklink_iterator(true);
|
|
if (!deckLinkIterator)
|
|
{
|
|
delete s;
|
|
return NULL;
|
|
}
|
|
|
|
s->state.resize(s->devices_cnt);
|
|
|
|
// Connect to the first DeckLink instance
|
|
IDeckLink *deckLink;
|
|
while (deckLinkIterator->Next(&deckLink) == S_OK)
|
|
{
|
|
bool found = false;
|
|
for(int i = 0; i < s->devices_cnt; ++i) {
|
|
string deviceName = bmd_get_device_name(deckLink);
|
|
|
|
if (!deviceName.empty() && deviceName == cardId[i]) {
|
|
found = true;
|
|
}
|
|
|
|
if (isdigit(cardId[i].c_str()[0]) && dnum == atoi(cardId[i].c_str())){
|
|
found = true;
|
|
}
|
|
|
|
if (found) {
|
|
s->state.at(i).deckLink = deckLink;
|
|
}
|
|
}
|
|
if(!found && deckLink != NULL)
|
|
deckLink->Release();
|
|
dnum++;
|
|
}
|
|
deckLinkIterator->Release();
|
|
for(int i = 0; i < s->devices_cnt; ++i) {
|
|
if (s->state.at(i).deckLink == nullptr) {
|
|
LOG(LOG_LEVEL_ERROR) << "No DeckLink PCI card " << cardId[i] <<" found\n";
|
|
goto error;
|
|
}
|
|
// Print the model name of the DeckLink card
|
|
string deviceName = bmd_get_device_name(s->state.at(i).deckLink);
|
|
if (!deviceName.empty()) {
|
|
LOG(LOG_LEVEL_INFO) << MOD_NAME "Using device " << deviceName << "\n";
|
|
}
|
|
}
|
|
|
|
if(flags & (DISPLAY_FLAG_AUDIO_EMBEDDED | DISPLAY_FLAG_AUDIO_AESEBU | DISPLAY_FLAG_AUDIO_ANALOG)) {
|
|
s->play_audio = true;
|
|
switch(flags & (DISPLAY_FLAG_AUDIO_EMBEDDED | DISPLAY_FLAG_AUDIO_AESEBU | DISPLAY_FLAG_AUDIO_ANALOG)) {
|
|
case DISPLAY_FLAG_AUDIO_EMBEDDED:
|
|
audioConnection = (BMDAudioOutputAnalogAESSwitch) 0;
|
|
break;
|
|
case DISPLAY_FLAG_AUDIO_AESEBU:
|
|
audioConnection = bmdAudioOutputSwitchAESEBU;
|
|
break;
|
|
case DISPLAY_FLAG_AUDIO_ANALOG:
|
|
audioConnection = bmdAudioOutputSwitchAnalog;
|
|
break;
|
|
default:
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Unsupporetd audio connection.\n");
|
|
abort();
|
|
}
|
|
} else {
|
|
s->play_audio = false;
|
|
}
|
|
|
|
if(s->emit_timecode) {
|
|
s->timecode = new DeckLinkTimecode;
|
|
} else {
|
|
s->timecode = NULL;
|
|
}
|
|
|
|
for(int i = 0; i < s->devices_cnt; ++i) {
|
|
if (s->profile_req != BMD_OPT_DEFAULT && s->profile_req != BMD_OPT_KEEP) {
|
|
decklink_set_duplex(s->state.at(i).deckLink, s->profile_req);
|
|
}
|
|
|
|
// Get IDeckLinkAttributes object
|
|
IDeckLinkProfileAttributes *deckLinkAttributes = NULL;
|
|
result = s->state.at(i).deckLink->QueryInterface(IID_IDeckLinkProfileAttributes, reinterpret_cast<void**>(&deckLinkAttributes));
|
|
if (result != S_OK) {
|
|
log_msg(LOG_LEVEL_WARNING, "Could not query device attributes.\n");
|
|
}
|
|
s->state.at(i).deckLinkAttributes = deckLinkAttributes;
|
|
|
|
// Obtain the audio/video output interface (IDeckLinkOutput)
|
|
if ((result = s->state.at(i).deckLink->QueryInterface(IID_IDeckLinkOutput, reinterpret_cast<void**>(&s->state.at(i).deckLinkOutput))) != S_OK) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Could not obtain the IDeckLinkOutput interface: %08x\n", (int) result);
|
|
goto error;
|
|
}
|
|
|
|
// Query the DeckLink for its configuration interface
|
|
result = s->state.at(i).deckLink->QueryInterface(IID_IDeckLinkConfiguration, reinterpret_cast<void**>(&deckLinkConfiguration));
|
|
s->state.at(i).deckLinkConfiguration = deckLinkConfiguration;
|
|
if (result != S_OK)
|
|
{
|
|
log_msg(LOG_LEVEL_ERROR, "Could not obtain the IDeckLinkConfiguration interface: %08x\n", (int) result);
|
|
goto error;
|
|
}
|
|
|
|
BMD_CONFIG_SET(Int, bmdDeckLinkConfigVideoOutputConversionMode, s->conversion_mode, true);
|
|
|
|
if (!s->keep_device_defaults && use1080psf != BMD_OPT_KEEP) {
|
|
if (use1080psf == BMD_OPT_DEFAULT) {
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME << "Setting output signal as progressive, see option \"Use1080PsF\" to use PsF or keep default.\n";
|
|
}
|
|
BMD_BOOL val = use1080psf == BMD_OPT_DEFAULT || use1080psf == BMD_OPT_FALSE ? BMD_FALSE : BMD_TRUE;
|
|
result = deckLinkConfiguration->SetFlag(bmdDeckLinkConfigOutput1080pAsPsF, val);
|
|
if (result != S_OK) {
|
|
LOG(LOG_LEVEL_ERROR) << MOD_NAME << "Unable to set 1080p P/PsF mode.\n";
|
|
}
|
|
}
|
|
|
|
BMD_CONFIG_SET(Flag, bmdDeckLinkConfigLowLatencyVideoOutput, s->low_latency, true);
|
|
|
|
if (!s->keep_device_defaults && s->low_latency) {
|
|
BMD_CONFIG_SET(Flag, bmdDeckLinkConfigFieldFlickerRemoval, false, false);
|
|
}
|
|
|
|
BMD_CONFIG_SET(Int, bmdDeckLinkConfigHDMI3DPackingFormat, HDMI3DPacking, true);
|
|
BMD_CONFIG_SET(Int, bmdDeckLinkConfigVideoOutputIdleOperation, bmdIdleVideoOutputLastFrame, false);
|
|
|
|
if (s->sdi_dual_channel_level != BMD_OPT_DEFAULT) {
|
|
if (deckLinkAttributes) {
|
|
BMD_BOOL supports_level_a;
|
|
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsSMPTELevelAOutput, &supports_level_a) != S_OK) {
|
|
log_msg(LOG_LEVEL_WARNING, MOD_NAME "Could figure out if device supports Level A 3G-SDI.\n");
|
|
} else {
|
|
if (s->sdi_dual_channel_level == 'A' && supports_level_a == BMD_FALSE) {
|
|
log_msg(LOG_LEVEL_WARNING, MOD_NAME "Device does not support Level A 3G-SDI!\n");
|
|
}
|
|
}
|
|
}
|
|
HRESULT res = deckLinkConfiguration->SetFlag(bmdDeckLinkConfigSMPTELevelAOutput, s->sdi_dual_channel_level == 'A');
|
|
if(res != S_OK) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Unable set output 3G-SDI level.\n");
|
|
}
|
|
}
|
|
|
|
if (s->requested_hdr_mode.EOTF != static_cast<int64_t>(HDR_EOTF::NONE)) {
|
|
BMD_BOOL hdr_supp = BMD_FALSE;
|
|
if (s->state.at(i).deckLinkAttributes == nullptr || s->state.at(i).deckLinkAttributes->GetFlag(BMDDeckLinkSupportsHDRMetadata, &hdr_supp) != S_OK) {
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME << "HDR requested, but unable to validate HDR support. Will try to pass it anyway which may result in blank image if not supported - remove the option if so.\n";
|
|
} else {
|
|
if (hdr_supp != BMD_TRUE) {
|
|
LOG(LOG_LEVEL_ERROR) << MOD_NAME << "HDR requested, but card doesn't support that.\n";
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
BMD_BOOL rec2020_supp = BMD_FALSE;
|
|
if (s->state.at(i).deckLinkAttributes == nullptr || s->state.at(i).deckLinkAttributes->GetFlag(BMDDeckLinkSupportsColorspaceMetadata, &rec2020_supp) != S_OK) {
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME << "Cannot check Rec. 2020 color space metadata support.\n";
|
|
} else {
|
|
if (rec2020_supp != BMD_TRUE) {
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME << "Rec. 2020 color space metadata not supported.\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (s->play_audio && i == 0) {
|
|
/* Actually no action is required to set audio connection because Blackmagic card plays audio through all its outputs (AES/SDI/analog) ....
|
|
*/
|
|
LOG(LOG_LEVEL_INFO) << MOD_NAME "Audio output set to: " << bmd_get_audio_connection_name(audioConnection) << "\n";
|
|
/*
|
|
* .... one exception is a card that has switchable cables between AES/EBU and analog. (But this applies only for channels 3 and above.)
|
|
*/
|
|
if (audioConnection != 0) { // we will set switchable AESEBU or analog
|
|
result = deckLinkConfiguration->SetInt(bmdDeckLinkConfigAudioOutputAESAnalogSwitch,
|
|
audioConnection);
|
|
if(result == S_OK) { // has switchable channels
|
|
log_msg(LOG_LEVEL_INFO, MOD_NAME "Card with switchable audio channels detected. Switched to correct format.\n");
|
|
} else if(result == E_NOTIMPL) {
|
|
// normal case - without switchable channels
|
|
} else {
|
|
log_msg(LOG_LEVEL_WARNING, MOD_NAME "Unable to switch audio output for channels 3 or above although \n"
|
|
"card shall support it. Check if it is ok. Continuing anyway.\n");
|
|
}
|
|
}
|
|
|
|
if (audio_consumer_levels != -1) {
|
|
result = deckLinkConfiguration->SetFlag(bmdDeckLinkConfigAnalogAudioConsumerLevels,
|
|
audio_consumer_levels == 1 ? true : false);
|
|
if(result != S_OK) {
|
|
log_msg(LOG_LEVEL_WARNING, MOD_NAME "Unable set output audio consumer levels.\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
s->state.at(i).delegate = new PlaybackDelegate();
|
|
// Provide this class as a delegate to the audio and video output interfaces
|
|
if (!s->low_latency) {
|
|
s->state.at(i).deckLinkOutput->SetScheduledFrameCompletionCallback(s->state.at(i).delegate);
|
|
}
|
|
//s->state.at(i).deckLinkOutput->DisableAudioOutput();
|
|
}
|
|
|
|
s->frames = 0;
|
|
s->initialized_audio = s->initialized_video = false;
|
|
|
|
return (void *)s;
|
|
error:
|
|
display_decklink_done(s);
|
|
return NULL;
|
|
}
|
|
|
|
static void display_decklink_run(void *state)
|
|
{
|
|
UNUSED(state);
|
|
}
|
|
|
|
#define RELEASE_IF_NOT_NULL(x) if (x != nullptr) x->Release();
|
|
static void display_decklink_done(void *state)
|
|
{
|
|
debug_msg("display_decklink_done\n"); /* TOREMOVE */
|
|
struct state_decklink *s = (struct state_decklink *)state;
|
|
|
|
assert (s != NULL);
|
|
|
|
for (int i = 0; i < s->devices_cnt; ++i)
|
|
{
|
|
if (s->initialized_video) {
|
|
if (!s->low_latency) {
|
|
CALL_AND_CHECK(s->state.at(i).deckLinkOutput->StopScheduledPlayback (0, nullptr, 0), "StopScheduledPlayback");
|
|
}
|
|
|
|
CALL_AND_CHECK(s->state.at(i).deckLinkOutput->DisableVideoOutput(), "DisableVideoOutput");
|
|
}
|
|
|
|
if (s->initialized_audio) {
|
|
if (i == 0) {
|
|
CALL_AND_CHECK(s->state.at(i).deckLinkOutput->DisableAudioOutput(), "DisableAudiioOutput");
|
|
}
|
|
}
|
|
|
|
RELEASE_IF_NOT_NULL(s->state.at(i).deckLinkAttributes);
|
|
RELEASE_IF_NOT_NULL(s->state.at(i).deckLinkConfiguration);
|
|
RELEASE_IF_NOT_NULL(s->state.at(i).deckLinkOutput);
|
|
RELEASE_IF_NOT_NULL(s->state.at(i).deckLink);
|
|
|
|
delete s->state.at(i).delegate;
|
|
}
|
|
|
|
while (!s->buffer_pool.frame_queue.empty()) {
|
|
auto tmp = s->buffer_pool.frame_queue.front();
|
|
s->buffer_pool.frame_queue.pop();
|
|
delete tmp;
|
|
}
|
|
|
|
delete s->timecode;
|
|
|
|
delete s;
|
|
|
|
decklink_uninitialize();
|
|
}
|
|
|
|
/**
|
|
* This function returns true if any display mode and any output supports the
|
|
* codec. The codec, however, may not be supported with actual video mode.
|
|
*
|
|
* @todo For UltraStudio Pro DoesSupportVideoMode returns E_FAIL on not supported
|
|
* pixel formats instead of setting supported to false.
|
|
*/
|
|
static bool decklink_display_supports_codec(IDeckLinkOutput *deckLinkOutput, BMDPixelFormat pf) {
|
|
IDeckLinkDisplayModeIterator *displayModeIterator;
|
|
IDeckLinkDisplayMode* deckLinkDisplayMode;
|
|
|
|
if (FAILED(deckLinkOutput->GetDisplayModeIterator(&displayModeIterator))) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Fatal: cannot create display mode iterator.\n");
|
|
return false;
|
|
}
|
|
|
|
while (displayModeIterator->Next(&deckLinkDisplayMode) == S_OK) {
|
|
BMD_BOOL supported;
|
|
HRESULT res = deckLinkOutput->DoesSupportVideoMode(bmdVideoConnectionUnspecified, deckLinkDisplayMode->GetDisplayMode(), pf, bmdNoVideoOutputConversion, bmdSupportedVideoModeDefault, nullptr, &supported);
|
|
deckLinkDisplayMode->Release();
|
|
if (res != S_OK) {
|
|
CALL_AND_CHECK(res, "DoesSupportVideoMode");
|
|
continue;
|
|
}
|
|
if (supported) {
|
|
displayModeIterator->Release();
|
|
return true;
|
|
}
|
|
}
|
|
displayModeIterator->Release();
|
|
|
|
return false;
|
|
}
|
|
|
|
static int display_decklink_get_property(void *state, int property, void *val, size_t *len)
|
|
{
|
|
struct state_decklink *s = (struct state_decklink *)state;
|
|
vector<codec_t> codecs(uv_to_bmd_codec_map.size());
|
|
int rgb_shift[] = {16, 8, 0};
|
|
interlacing_t supported_il_modes[] = {PROGRESSIVE, INTERLACED_MERGED, SEGMENTED_FRAME};
|
|
int count = 0;
|
|
for (auto & c : uv_to_bmd_codec_map) {
|
|
if (decklink_display_supports_codec(s->state[0].deckLinkOutput, c.second)) {
|
|
codecs[count++] = c.first;
|
|
}
|
|
}
|
|
|
|
switch (property) {
|
|
case DISPLAY_PROPERTY_CODECS:
|
|
if(sizeof(codec_t) * count <= *len) {
|
|
memcpy(val, codecs.data(), sizeof(codec_t) * count);
|
|
*len = sizeof(codec_t) * count;
|
|
} else {
|
|
return FALSE;
|
|
}
|
|
break;
|
|
case DISPLAY_PROPERTY_RGB_SHIFT:
|
|
if(sizeof(rgb_shift) > *len) {
|
|
return FALSE;
|
|
}
|
|
memcpy(val, rgb_shift, sizeof(rgb_shift));
|
|
*len = sizeof(rgb_shift);
|
|
break;
|
|
case DISPLAY_PROPERTY_BUF_PITCH:
|
|
*(int *) val = PITCH_DEFAULT;
|
|
*len = sizeof(int);
|
|
break;
|
|
case DISPLAY_PROPERTY_VIDEO_MODE:
|
|
if(s->devices_cnt == 1 && !s->stereo)
|
|
*(int *) val = DISPLAY_PROPERTY_VIDEO_MERGED;
|
|
else
|
|
*(int *) val = DISPLAY_PROPERTY_VIDEO_SEPARATE_TILES;
|
|
break;
|
|
case DISPLAY_PROPERTY_SUPPORTED_IL_MODES:
|
|
if(sizeof(supported_il_modes) <= *len) {
|
|
memcpy(val, supported_il_modes, sizeof(supported_il_modes));
|
|
} else {
|
|
return FALSE;
|
|
}
|
|
*len = sizeof(supported_il_modes);
|
|
break;
|
|
case DISPLAY_PROPERTY_AUDIO_FORMAT:
|
|
{
|
|
assert(*len == sizeof(struct audio_desc));
|
|
struct audio_desc *desc = (struct audio_desc *) val;
|
|
desc->sample_rate = 48000;
|
|
if (desc->ch_count <= 2) {
|
|
desc->ch_count = 2;
|
|
} else if (desc->ch_count > 2 && desc->ch_count <= 8) {
|
|
desc->ch_count = 8;
|
|
} else {
|
|
desc->ch_count = 16;
|
|
}
|
|
desc->codec = AC_PCM;
|
|
desc->bps = desc->bps < 3 ? 2 : 4;
|
|
}
|
|
break;
|
|
default:
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/*
|
|
* AUDIO
|
|
*/
|
|
static void display_decklink_put_audio_frame(void *state, const struct audio_frame *frame)
|
|
{
|
|
struct state_decklink *s = (struct state_decklink *)state;
|
|
unsigned int sample_frame_count = frame->data_len / (frame->bps * frame->ch_count);
|
|
|
|
s->audio_summary.record_audio_time_diff();
|
|
|
|
assert(s->play_audio);
|
|
|
|
uint32_t sampleFramesWritten;
|
|
|
|
auto t0 = chrono::high_resolution_clock::now();
|
|
|
|
uint32_t buffered = 0;
|
|
s->state[0].deckLinkOutput->GetBufferedAudioSampleFrameCount(&buffered);
|
|
s->audio_summary.calculate_missing(buffered, sample_frame_count);
|
|
if (buffered == 0) {
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME << "audio buffer underflow!\n";
|
|
s->audio_summary.increment_buffer_underflow();
|
|
}
|
|
|
|
if (!s->audio_drift_fixer.update(buffered)) {
|
|
log_msg(LOG_LEVEL_WARNING, MOD_NAME "update drift early exit.\n");
|
|
return;
|
|
}
|
|
|
|
if (s->low_latency) {
|
|
HRESULT res = s->state[0].deckLinkOutput->WriteAudioSamplesSync(frame->data, sample_frame_count,
|
|
&sampleFramesWritten);
|
|
if (FAILED(res)) {
|
|
log_msg(LOG_LEVEL_WARNING, MOD_NAME "WriteAudioSamplesSync failed.\n");
|
|
return;
|
|
}
|
|
} else {
|
|
s->state[0].deckLinkOutput->ScheduleAudioSamples(frame->data, sample_frame_count, 0,
|
|
0, &sampleFramesWritten);
|
|
}
|
|
if (sampleFramesWritten != sample_frame_count) {
|
|
LOG(LOG_LEVEL_WARNING) << MOD_NAME << "audio buffer overflow! " << sample_frame_count
|
|
<< " samples written, " << sampleFramesWritten << " written, "
|
|
<< sample_frame_count - sampleFramesWritten<<" diff, "<<buffered<< " buffer size.\n";
|
|
s->audio_summary.increment_buffer_overflow();
|
|
}
|
|
LOG(LOG_LEVEL_DEBUG) << MOD_NAME "putf audio - lasted " << setprecision(2) << chrono::duration_cast<chrono::duration<double>>(chrono::high_resolution_clock::now() - t0).count() * 1000.0 << " ms.\n";
|
|
s->audio_summary.increment_audio_frames_played();
|
|
s->audio_summary.set_buffer_average(s->audio_drift_fixer.get_buffer_avg());
|
|
s->audio_summary.report();
|
|
s->audio_summary.mark_audio_time_end();
|
|
}
|
|
|
|
static int display_decklink_reconfigure_audio(void *state, int quant_samples, int channels,
|
|
int sample_rate) {
|
|
struct state_decklink *s = (struct state_decklink *)state;
|
|
BMDAudioSampleType sample_type;
|
|
|
|
unique_lock<mutex> lk(s->reconfiguration_lock);
|
|
|
|
assert(s->play_audio);
|
|
|
|
if (s->initialized_audio) {
|
|
CALL_AND_CHECK(s->state[0].deckLinkOutput->DisableAudioOutput(),
|
|
"DisableAudioOutput");
|
|
s->initialized_audio = false;
|
|
}
|
|
|
|
if (channels != 2 && channels != 8 &&
|
|
channels != 16) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "requested channel count isn't supported: "
|
|
"%d\n", channels);
|
|
return FALSE;
|
|
}
|
|
|
|
if((quant_samples != 16 && quant_samples != 32) ||
|
|
sample_rate != 48000) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "audio format isn't supported: "
|
|
"samples: %d, sample rate: %d\n",
|
|
quant_samples, sample_rate);
|
|
return FALSE;
|
|
}
|
|
switch(quant_samples) {
|
|
case 16:
|
|
sample_type = bmdAudioSampleType16bitInteger;
|
|
break;
|
|
case 32:
|
|
sample_type = bmdAudioSampleType32bitInteger;
|
|
break;
|
|
default:
|
|
return FALSE;
|
|
}
|
|
|
|
EXIT_IF_FAILED(s->state[0].deckLinkOutput->EnableAudioOutput(bmdAudioSampleRate48kHz,
|
|
sample_type,
|
|
channels,
|
|
bmdAudioOutputStreamContinuous),
|
|
"EnableAudioOutput");
|
|
|
|
if (!s->low_latency) {
|
|
// This will most certainly fail because it is started with in video
|
|
// reconfigure. However, this doesn't seem to bother, anyway.
|
|
CALL_AND_CHECK(s->state[0].deckLinkOutput->StartScheduledPlayback(0, s->frameRateScale, s->frameRateDuration), "StartScheduledPlayback (audio)");
|
|
}
|
|
|
|
s->aud_desc = { quant_samples / 8, sample_rate, channels, AC_PCM };
|
|
|
|
s->initialized_audio = true;
|
|
|
|
return TRUE;
|
|
|
|
error:
|
|
s->initialized_audio = false;
|
|
return FALSE;
|
|
}
|
|
|
|
#ifndef WIN32
|
|
static bool operator==(const REFIID & first, const REFIID & second){
|
|
return (first.byte0 == second.byte0) &&
|
|
(first.byte1 == second.byte1) &&
|
|
(first.byte2 == second.byte2) &&
|
|
(first.byte3 == second.byte3) &&
|
|
(first.byte4 == second.byte4) &&
|
|
(first.byte5 == second.byte5) &&
|
|
(first.byte6 == second.byte6) &&
|
|
(first.byte7 == second.byte7) &&
|
|
(first.byte8 == second.byte8) &&
|
|
(first.byte9 == second.byte9) &&
|
|
(first.byte10 == second.byte10) &&
|
|
(first.byte11 == second.byte11) &&
|
|
(first.byte12 == second.byte12) &&
|
|
(first.byte13 == second.byte13) &&
|
|
(first.byte14 == second.byte14) &&
|
|
(first.byte15 == second.byte15);
|
|
}
|
|
#endif
|
|
|
|
HRESULT DeckLinkFrame::QueryInterface(REFIID iid, LPVOID *ppv)
|
|
{
|
|
#ifdef _WIN32
|
|
IID iunknown = IID_IUnknown;
|
|
#else
|
|
CFUUIDBytes iunknown = CFUUIDGetUUIDBytes(IUnknownUUID);
|
|
#endif
|
|
HRESULT result = S_OK;
|
|
|
|
if (ppv == nullptr) {
|
|
return E_INVALIDARG;
|
|
}
|
|
|
|
// Initialise the return result
|
|
*ppv = nullptr;
|
|
|
|
LOG(LOG_LEVEL_DEBUG) << MOD_NAME << "DeckLinkFrame QueryInterface " << iid << "\n";
|
|
if (iid == iunknown) {
|
|
*ppv = this;
|
|
AddRef();
|
|
} else if (iid == IID_IDeckLinkVideoFrame) {
|
|
*ppv = static_cast<IDeckLinkVideoFrame*>(this);
|
|
AddRef();
|
|
} else if (iid == IID_IDeckLinkVideoFrameMetadataExtensions) {
|
|
if (m_metadata.EOTF == static_cast<int64_t>(HDR_EOTF::NONE)) {
|
|
result = E_NOINTERFACE;
|
|
} else {
|
|
*ppv = static_cast<IDeckLinkVideoFrameMetadataExtensions*>(this);
|
|
AddRef();
|
|
}
|
|
} else {
|
|
result = E_NOINTERFACE;
|
|
}
|
|
|
|
return result;
|
|
|
|
return E_NOINTERFACE;
|
|
}
|
|
|
|
ULONG DeckLinkFrame::AddRef()
|
|
{
|
|
return ++ref;
|
|
}
|
|
|
|
ULONG DeckLinkFrame::Release()
|
|
{
|
|
if (--ref == 0) {
|
|
lock_guard<mutex> lg(buffer_pool.lock);
|
|
buffer_pool.frame_queue.push(this);
|
|
}
|
|
return ref;
|
|
}
|
|
|
|
DeckLinkFrame::DeckLinkFrame(long w, long h, long rb, BMDPixelFormat pf, buffer_pool_t & bp, HDRMetadata const & hdr_metadata)
|
|
: width(w), height(h), rawBytes(rb), pixelFormat(pf), data(new char[rb * h]), timecode(NULL), ref(1l),
|
|
buffer_pool(bp)
|
|
{
|
|
clear_video_buffer(reinterpret_cast<unsigned char *>(data.get()), rawBytes, rawBytes, height,
|
|
pf == bmdFormat8BitYUV ? UYVY : (pf == bmdFormat10BitYUV ? v210 : RGBA));
|
|
m_metadata = hdr_metadata;
|
|
}
|
|
|
|
DeckLinkFrame *DeckLinkFrame::Create(long width, long height, long rawBytes, BMDPixelFormat pixelFormat, buffer_pool_t & buffer_pool, const HDRMetadata & hdr_metadata)
|
|
{
|
|
return new DeckLinkFrame(width, height, rawBytes, pixelFormat, buffer_pool, hdr_metadata);
|
|
}
|
|
|
|
DeckLinkFrame::~DeckLinkFrame()
|
|
{
|
|
}
|
|
|
|
long DeckLinkFrame::GetWidth ()
|
|
{
|
|
return width;
|
|
}
|
|
|
|
long DeckLinkFrame::GetHeight ()
|
|
{
|
|
return height;
|
|
}
|
|
|
|
long DeckLinkFrame::GetRowBytes ()
|
|
{
|
|
return rawBytes;
|
|
}
|
|
|
|
BMDPixelFormat DeckLinkFrame::GetPixelFormat ()
|
|
{
|
|
return pixelFormat;
|
|
}
|
|
|
|
BMDFrameFlags DeckLinkFrame::GetFlags ()
|
|
{
|
|
return m_metadata.EOTF == static_cast<int64_t>(HDR_EOTF::NONE) ? bmdFrameFlagDefault : bmdFrameContainsHDRMetadata;
|
|
}
|
|
|
|
HRESULT DeckLinkFrame::GetBytes (/* out */ void **buffer)
|
|
{
|
|
*buffer = static_cast<void *>(data.get());
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT DeckLinkFrame::GetTimecode (/* in */ BMDTimecodeFormat, /* out */ IDeckLinkTimecode **timecode)
|
|
{
|
|
*timecode = dynamic_cast<IDeckLinkTimecode *>(this->timecode);
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT DeckLinkFrame::GetAncillaryData (/* out */ IDeckLinkVideoFrameAncillary **)
|
|
{
|
|
return S_FALSE;
|
|
}
|
|
|
|
/* IDeckLinkMutableVideoFrame */
|
|
HRESULT DeckLinkFrame::SetFlags (/* in */ BMDFrameFlags)
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
|
|
HRESULT DeckLinkFrame::SetTimecode (/* in */ BMDTimecodeFormat, /* in */ IDeckLinkTimecode *timecode)
|
|
{
|
|
if(this->timecode)
|
|
this->timecode->Release();
|
|
this->timecode = timecode;
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT DeckLinkFrame::SetTimecodeFromComponents (/* in */ BMDTimecodeFormat, /* in */ uint8_t, /* in */ uint8_t, /* in */ uint8_t, /* in */ uint8_t, /* in */ BMDTimecodeFlags)
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
|
|
HRESULT DeckLinkFrame::SetAncillaryData (/* in */ IDeckLinkVideoFrameAncillary *)
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
|
|
HRESULT DeckLinkFrame::SetTimecodeUserBits (/* in */ BMDTimecodeFormat, /* in */ BMDTimecodeUserBits)
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
|
|
void HDRMetadata::Init(const string &fmt) {
|
|
auto opts = unique_ptr<char []>(new char [fmt.size() + 1]);
|
|
strcpy(opts.get(), fmt.c_str());
|
|
char *save_ptr = nullptr;
|
|
char *mode_c = strtok_r(opts.get(), ",", &save_ptr);
|
|
assert(mode_c != nullptr);
|
|
string mode = mode_c;
|
|
std::for_each(std::begin(mode), std::end(mode), [](char& c) {
|
|
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
|
});
|
|
if (mode == "SDR"s) {
|
|
EOTF = static_cast<int64_t>(HDR_EOTF::SDR);
|
|
} else if (mode == "HDR"s) {
|
|
EOTF = static_cast<int64_t>(HDR_EOTF::HDR);
|
|
} else if (mode == "PQ"s) {
|
|
EOTF = static_cast<int64_t>(HDR_EOTF::PQ);
|
|
} else if (mode == "HLG"s) {
|
|
EOTF = static_cast<int64_t>(HDR_EOTF::HLG);
|
|
} else if (mode == "HELP"s) {
|
|
cout << MOD_NAME << "HDR syntax:\n";
|
|
cout << "\tHDR[=<eotf>|int[,{<k>=<v>}*]\n";
|
|
cout << "\t\t<eotf> may be one of SDR, HDR, PQ, HLG or int 0-7\n";
|
|
cout << "\t\tFurther options may be specification of HDR values, accepted keys are (values are floats):\n";
|
|
cout << "\t\t\t- maxDisplayMasteringLuminance\n";
|
|
cout << "\t\t\t- minDisplayMasteringLuminance\n";
|
|
cout << "\t\t\t- maxCLL\n";
|
|
cout << "\t\t\t- maxFALL\n";
|
|
throw ug_no_error{};
|
|
} else {
|
|
EOTF = stoi(mode);
|
|
if (EOTF < 0 || EOTF > 7) {
|
|
throw out_of_range("Value outside [0..7]");
|
|
}
|
|
}
|
|
|
|
char *other_opt = nullptr;
|
|
while ((other_opt = strtok_r(nullptr, ",", &save_ptr)) != nullptr) {
|
|
if (strstr(other_opt, "maxDisplayMasteringLuminance=") != nullptr) {
|
|
maxDisplayMasteringLuminance = stod(other_opt + strlen("maxDisplayMasteringLuminance="));
|
|
} else if (strstr(other_opt, "minDisplayMasteringLuminance=") != nullptr) {
|
|
minDisplayMasteringLuminance = stod(other_opt + strlen("minDisplayMasteringLuminance="));
|
|
} else if (strstr(other_opt, "maxCLL=") != nullptr) {
|
|
maxCLL = stod(other_opt + strlen("maxCLL="));
|
|
} else if (strstr(other_opt, "maxFALL=") != nullptr) {
|
|
maxFALL = stod(other_opt + strlen("maxFALL="));
|
|
} else {
|
|
throw invalid_argument("Unrecognized HDR attribute "s + other_opt);
|
|
}
|
|
}
|
|
}
|
|
|
|
static inline void debug_print_metadata_id(const char *fn_name, BMDDeckLinkFrameMetadataID metadataID) {
|
|
if (log_level < LOG_LEVEL_DEBUG2) {
|
|
return;
|
|
}
|
|
array<char, sizeof metadataID + 1> fourcc{};
|
|
copy(reinterpret_cast<char *>(&metadataID), reinterpret_cast<char *>(&metadataID) + sizeof metadataID, fourcc.data());
|
|
LOG(LOG_LEVEL_DEBUG2) << MOD_NAME << "DecklLinkFrame " << fn_name << ": " << fourcc.data() << "\n";
|
|
}
|
|
|
|
// IDeckLinkVideoFrameMetadataExtensions interface
|
|
HRESULT DeckLinkFrame::GetInt(BMDDeckLinkFrameMetadataID metadataID, int64_t* value)
|
|
{
|
|
debug_print_metadata_id(static_cast<const char *>(__func__), metadataID);
|
|
switch (metadataID)
|
|
{
|
|
case bmdDeckLinkFrameMetadataHDRElectroOpticalTransferFunc:
|
|
*value = m_metadata.EOTF;
|
|
return S_OK;
|
|
case bmdDeckLinkFrameMetadataColorspace:
|
|
// Colorspace is fixed for this sample
|
|
*value = bmdColorspaceRec2020;
|
|
return S_OK;
|
|
default:
|
|
value = nullptr;
|
|
return E_INVALIDARG;
|
|
}
|
|
}
|
|
|
|
HRESULT DeckLinkFrame::GetFloat(BMDDeckLinkFrameMetadataID metadataID, double* value)
|
|
{
|
|
debug_print_metadata_id(static_cast<const char *>(__func__), metadataID);
|
|
switch (metadataID)
|
|
{
|
|
case bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedX:
|
|
*value = m_metadata.referencePrimaries.RedX;
|
|
return S_OK;
|
|
case bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedY:
|
|
*value = m_metadata.referencePrimaries.RedY;
|
|
return S_OK;
|
|
case bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenX:
|
|
*value = m_metadata.referencePrimaries.GreenX;
|
|
return S_OK;
|
|
case bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenY:
|
|
*value = m_metadata.referencePrimaries.GreenY;
|
|
return S_OK;
|
|
case bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueX:
|
|
*value = m_metadata.referencePrimaries.BlueX;
|
|
return S_OK;
|
|
case bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueY:
|
|
*value = m_metadata.referencePrimaries.BlueY;
|
|
return S_OK;
|
|
case bmdDeckLinkFrameMetadataHDRWhitePointX:
|
|
*value = m_metadata.referencePrimaries.WhiteX;
|
|
return S_OK;
|
|
case bmdDeckLinkFrameMetadataHDRWhitePointY:
|
|
*value = m_metadata.referencePrimaries.WhiteY;
|
|
return S_OK;
|
|
case bmdDeckLinkFrameMetadataHDRMaxDisplayMasteringLuminance:
|
|
*value = m_metadata.maxDisplayMasteringLuminance;
|
|
return S_OK;
|
|
case bmdDeckLinkFrameMetadataHDRMinDisplayMasteringLuminance:
|
|
*value = m_metadata.minDisplayMasteringLuminance;
|
|
return S_OK;
|
|
case bmdDeckLinkFrameMetadataHDRMaximumContentLightLevel:
|
|
*value = m_metadata.maxCLL;
|
|
return S_OK;
|
|
case bmdDeckLinkFrameMetadataHDRMaximumFrameAverageLightLevel:
|
|
*value = m_metadata.maxFALL;
|
|
return S_OK;
|
|
default:
|
|
value = nullptr;
|
|
return E_INVALIDARG;
|
|
}
|
|
}
|
|
|
|
HRESULT DeckLinkFrame::GetFlag(BMDDeckLinkFrameMetadataID metadataID, BMD_BOOL* value)
|
|
{
|
|
debug_print_metadata_id(static_cast<const char *>(__func__), metadataID);
|
|
// Not expecting GetFlag
|
|
*value = BMD_TRUE;
|
|
return E_INVALIDARG;
|
|
}
|
|
|
|
HRESULT DeckLinkFrame::GetString(BMDDeckLinkFrameMetadataID metadataID, BMD_STR* value)
|
|
{
|
|
debug_print_metadata_id(static_cast<const char *>(__func__), metadataID);
|
|
// Not expecting GetString
|
|
*value = nullptr;
|
|
return E_INVALIDARG;
|
|
}
|
|
|
|
HRESULT DeckLinkFrame::GetBytes(BMDDeckLinkFrameMetadataID metadataID, void* /* buffer */, uint32_t* bufferSize)
|
|
{
|
|
debug_print_metadata_id(static_cast<const char *>(__func__), metadataID);
|
|
*bufferSize = 0;
|
|
return E_INVALIDARG;
|
|
}
|
|
|
|
// 3D frame
|
|
DeckLink3DFrame::DeckLink3DFrame(long w, long h, long rb, BMDPixelFormat pf, buffer_pool_t & buffer_pool, HDRMetadata const & hdr_metadata)
|
|
: DeckLinkFrame(w, h, rb, pf, buffer_pool, hdr_metadata), rightEye(DeckLinkFrame::Create(w, h, rb, pf, buffer_pool, hdr_metadata))
|
|
{
|
|
}
|
|
|
|
DeckLink3DFrame *DeckLink3DFrame::Create(long width, long height, long rawBytes, BMDPixelFormat pixelFormat, buffer_pool_t & buffer_pool, HDRMetadata const & hdr_metadata)
|
|
{
|
|
DeckLink3DFrame *frame = new DeckLink3DFrame(width, height, rawBytes, pixelFormat, buffer_pool, hdr_metadata);
|
|
return frame;
|
|
}
|
|
|
|
DeckLink3DFrame::~DeckLink3DFrame()
|
|
{
|
|
}
|
|
|
|
ULONG DeckLink3DFrame::AddRef()
|
|
{
|
|
return DeckLinkFrame::AddRef();
|
|
}
|
|
|
|
ULONG DeckLink3DFrame::Release()
|
|
{
|
|
return DeckLinkFrame::Release();
|
|
}
|
|
|
|
HRESULT DeckLink3DFrame::QueryInterface(REFIID id, void **data)
|
|
{
|
|
LOG(LOG_LEVEL_DEBUG) << MOD_NAME << "DecklLink3DFrame QueryInterface " << id << "\n";
|
|
if(id == IID_IDeckLinkVideoFrame3DExtensions)
|
|
{
|
|
this->AddRef();
|
|
*data = dynamic_cast<IDeckLinkVideoFrame3DExtensions *>(this);
|
|
return S_OK;
|
|
}
|
|
return DeckLinkFrame::QueryInterface(id, data);
|
|
}
|
|
|
|
BMDVideo3DPackingFormat DeckLink3DFrame::Get3DPackingFormat()
|
|
{
|
|
return bmdVideo3DPackingLeftOnly;
|
|
}
|
|
|
|
HRESULT DeckLink3DFrame::GetFrameForRightEye(IDeckLinkVideoFrame ** frame)
|
|
{
|
|
*frame = rightEye.get();
|
|
rightEye->AddRef();
|
|
return S_OK;
|
|
}
|
|
|
|
/* function from DeckLink SDK sample DeviceList */
|
|
|
|
static void print_output_modes (IDeckLink* deckLink)
|
|
{
|
|
IDeckLinkOutput* deckLinkOutput = NULL;
|
|
IDeckLinkDisplayModeIterator* displayModeIterator = NULL;
|
|
IDeckLinkDisplayMode* displayMode = NULL;
|
|
HRESULT result;
|
|
int displayModeNumber = 0;
|
|
|
|
// Query the DeckLink for its configuration interface
|
|
result = deckLink->QueryInterface(IID_IDeckLinkOutput, (void**)&deckLinkOutput);
|
|
if (result != S_OK)
|
|
{
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Could not obtain the IDeckLinkOutput interface - result = %08x\n", (int) result);
|
|
if (result == E_NOINTERFACE) {
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Device doesn't support video playback.\n");
|
|
}
|
|
goto bail;
|
|
}
|
|
|
|
// Obtain an IDeckLinkDisplayModeIterator to enumerate the display modes supported on output
|
|
result = deckLinkOutput->GetDisplayModeIterator(&displayModeIterator);
|
|
if (result != S_OK)
|
|
{
|
|
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Could not obtain the video output display mode iterator - result = %08x\n", (int) result);
|
|
goto bail;
|
|
}
|
|
|
|
// List all supported output display modes
|
|
printf("\tdisplay modes:\n");
|
|
while (displayModeIterator->Next(&displayMode) == S_OK)
|
|
{
|
|
BMD_STR displayModeString = NULL;
|
|
|
|
result = displayMode->GetName(&displayModeString);
|
|
|
|
if (result == S_OK)
|
|
{
|
|
char *displayModeCString = get_cstr_from_bmd_api_str(displayModeString);
|
|
int modeWidth;
|
|
int modeHeight;
|
|
BMDDisplayModeFlags flags;
|
|
BMDTimeValue frameRateDuration;
|
|
BMDTimeScale frameRateScale;
|
|
|
|
// Obtain the display mode's properties
|
|
flags = displayMode->GetFlags();
|
|
modeWidth = displayMode->GetWidth();
|
|
modeHeight = displayMode->GetHeight();
|
|
displayMode->GetFrameRate(&frameRateDuration, &frameRateScale);
|
|
printf("\t\t%2d) %-20s %d x %d \t %2.2f FPS%s\n",displayModeNumber, displayModeCString,
|
|
modeWidth, modeHeight, (float) ((double)frameRateScale / (double)frameRateDuration),
|
|
(flags & bmdDisplayModeSupports3D ? "\t (supports 3D)" : ""));
|
|
release_bmd_api_str(displayModeString);
|
|
free(displayModeCString);
|
|
}
|
|
|
|
// Release the IDeckLinkDisplayMode object to prevent a leak
|
|
displayMode->Release();
|
|
|
|
displayModeNumber++;
|
|
}
|
|
|
|
bail:
|
|
// Ensure that the interfaces we obtained are released to prevent a memory leak
|
|
if (displayModeIterator != NULL)
|
|
displayModeIterator->Release();
|
|
|
|
if (deckLinkOutput != NULL)
|
|
deckLinkOutput->Release();
|
|
}
|
|
|
|
static const struct video_display_info display_decklink_info = {
|
|
display_decklink_probe,
|
|
display_decklink_init,
|
|
display_decklink_run,
|
|
display_decklink_done,
|
|
display_decklink_getf,
|
|
display_decklink_putf,
|
|
display_decklink_reconfigure_video,
|
|
display_decklink_get_property,
|
|
display_decklink_put_audio_frame,
|
|
display_decklink_reconfigure_audio,
|
|
DISPLAY_DOESNT_NEED_MAINLOOP,
|
|
MOD_NAME,
|
|
};
|
|
|
|
REGISTER_MODULE(decklink, &display_decklink_info, LIBRARY_CLASS_VIDEO_DISPLAY, VIDEO_DISPLAY_ABI_VERSION);
|
|
|
|
/* vim: set expandtab sw=8: */
|