4330021617
Use unbuffered async mode by default, and when in unbuffered mode, decouple audio/video so that audio plays as soon as it's received. This is a workaround for decklink device drivers having unreliable video/audio timestamps (audio/video sync drifting over time). From testing, it seems that the handling of video and audio is completely separate in the driver; along with the timestamp calculations. For example, when the thread of the decklink audio callback is stalled, it would cause the timestamps of the audio alone to go out of sync, which indicates timestamps are calculated more or less on the spot independent of what video is doing (which is how we replicated the issue fixed by b63e4b055e68a). Because decklink drivers treats the audio and video as essentially decoupled, we must also treat it as decoupled. This is what was causing video/audio to drift out of sync over time.
290 lines
9.4 KiB
C++
290 lines
9.4 KiB
C++
#include "decklink.hpp"
|
|
#include "decklink-device.hpp"
|
|
#include "decklink-device-discovery.hpp"
|
|
|
|
#include <obs-module.h>
|
|
|
|
OBS_DECLARE_MODULE()
|
|
OBS_MODULE_USE_DEFAULT_LOCALE("decklink", "en-US")
|
|
|
|
#define DEVICE_HASH "device_hash"
|
|
#define DEVICE_NAME "device_name"
|
|
#define MODE_ID "mode_id"
|
|
#define MODE_NAME "mode_name"
|
|
#define CHANNEL_FORMAT "channel_format"
|
|
#define PIXEL_FORMAT "pixel_format"
|
|
#define COLOR_SPACE "color_space"
|
|
#define COLOR_RANGE "color_range"
|
|
#define BUFFERING "buffering"
|
|
|
|
#define TEXT_DEVICE obs_module_text("Device")
|
|
#define TEXT_MODE obs_module_text("Mode")
|
|
#define TEXT_PIXEL_FORMAT obs_module_text("PixelFormat")
|
|
#define TEXT_COLOR_SPACE obs_module_text("ColorSpace")
|
|
#define TEXT_COLOR_SPACE_DEFAULT obs_module_text("ColorSpace.Default")
|
|
#define TEXT_COLOR_RANGE obs_module_text("ColorRange")
|
|
#define TEXT_COLOR_RANGE_DEFAULT obs_module_text("ColorRange.Default")
|
|
#define TEXT_COLOR_RANGE_PARTIAL obs_module_text("ColorRange.Partial")
|
|
#define TEXT_COLOR_RANGE_FULL obs_module_text("ColorRange.Full")
|
|
#define TEXT_CHANNEL_FORMAT obs_module_text("ChannelFormat")
|
|
#define TEXT_CHANNEL_FORMAT_NONE obs_module_text("ChannelFormat.None")
|
|
#define TEXT_CHANNEL_FORMAT_2_0CH obs_module_text("ChannelFormat.2_0ch")
|
|
#define TEXT_CHANNEL_FORMAT_5_1CH obs_module_text("ChannelFormat.5_1ch")
|
|
#define TEXT_CHANNEL_FORMAT_5_1CH_BACK obs_module_text("ChannelFormat.5_1chBack")
|
|
#define TEXT_CHANNEL_FORMAT_7_1CH obs_module_text("ChannelFormat.7_1ch")
|
|
#define TEXT_BUFFERING obs_module_text("Buffering")
|
|
|
|
static DeckLinkDeviceDiscovery *deviceEnum = nullptr;
|
|
|
|
static void decklink_enable_buffering(DeckLink *decklink, bool enabled)
|
|
{
|
|
obs_source_t *source = decklink->GetSource();
|
|
obs_source_set_async_unbuffered(source, !enabled);
|
|
decklink->buffering = enabled;
|
|
}
|
|
|
|
static void *decklink_create(obs_data_t *settings, obs_source_t *source)
|
|
{
|
|
DeckLink *decklink = new DeckLink(source, deviceEnum);
|
|
|
|
obs_source_set_async_decoupled(source, true);
|
|
decklink_enable_buffering(decklink,
|
|
obs_data_get_bool(settings, BUFFERING));
|
|
|
|
obs_source_update(source, settings);
|
|
return decklink;
|
|
}
|
|
|
|
static void decklink_destroy(void *data)
|
|
{
|
|
DeckLink *decklink = (DeckLink *)data;
|
|
delete decklink;
|
|
}
|
|
|
|
static void decklink_update(void *data, obs_data_t *settings)
|
|
{
|
|
DeckLink *decklink = (DeckLink *)data;
|
|
const char *hash = obs_data_get_string(settings, DEVICE_HASH);
|
|
long long id = obs_data_get_int(settings, MODE_ID);
|
|
BMDPixelFormat pixelFormat = (BMDPixelFormat)obs_data_get_int(settings,
|
|
PIXEL_FORMAT);
|
|
video_colorspace colorSpace = (video_colorspace)obs_data_get_int(settings,
|
|
COLOR_SPACE);
|
|
video_range_type colorRange = (video_range_type)obs_data_get_int(settings,
|
|
COLOR_RANGE);
|
|
speaker_layout channelFormat = (speaker_layout)obs_data_get_int(settings,
|
|
CHANNEL_FORMAT);
|
|
|
|
decklink_enable_buffering(decklink,
|
|
obs_data_get_bool(settings, BUFFERING));
|
|
|
|
ComPtr<DeckLinkDevice> device;
|
|
device.Set(deviceEnum->FindByHash(hash));
|
|
|
|
decklink->SetPixelFormat(pixelFormat);
|
|
decklink->SetColorSpace(colorSpace);
|
|
decklink->SetColorRange(colorRange);
|
|
decklink->SetChannelFormat(channelFormat);
|
|
decklink->Activate(device, id);
|
|
}
|
|
|
|
static void decklink_get_defaults(obs_data_t *settings)
|
|
{
|
|
obs_data_set_default_bool(settings, BUFFERING, false);
|
|
obs_data_set_default_int(settings, PIXEL_FORMAT, bmdFormat8BitYUV);
|
|
obs_data_set_default_int(settings, COLOR_SPACE, VIDEO_CS_DEFAULT);
|
|
obs_data_set_default_int(settings, COLOR_RANGE, VIDEO_RANGE_DEFAULT);
|
|
obs_data_set_default_int(settings, CHANNEL_FORMAT, SPEAKERS_STEREO);
|
|
}
|
|
|
|
static const char *decklink_get_name(void*)
|
|
{
|
|
return obs_module_text("BlackmagicDevice");
|
|
}
|
|
|
|
static bool decklink_device_changed(obs_properties_t *props,
|
|
obs_property_t *list, obs_data_t *settings)
|
|
{
|
|
const char *name = obs_data_get_string(settings, DEVICE_NAME);
|
|
const char *hash = obs_data_get_string(settings, DEVICE_HASH);
|
|
const char *mode = obs_data_get_string(settings, MODE_NAME);
|
|
long long modeId = obs_data_get_int(settings, MODE_ID);
|
|
|
|
size_t itemCount = obs_property_list_item_count(list);
|
|
bool itemFound = false;
|
|
|
|
for (size_t i = 0; i < itemCount; i++) {
|
|
const char *curHash = obs_property_list_item_string(list, i);
|
|
if (strcmp(hash, curHash) == 0) {
|
|
itemFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!itemFound) {
|
|
obs_property_list_insert_string(list, 0, name, hash);
|
|
obs_property_list_item_disable(list, 0, true);
|
|
}
|
|
|
|
obs_property_t *modeList = obs_properties_get(props, MODE_ID);
|
|
obs_property_t *channelList = obs_properties_get(props, CHANNEL_FORMAT);
|
|
|
|
obs_property_list_clear(modeList);
|
|
|
|
obs_property_list_clear(channelList);
|
|
obs_property_list_add_int(channelList, TEXT_CHANNEL_FORMAT_NONE,
|
|
SPEAKERS_UNKNOWN);
|
|
obs_property_list_add_int(channelList, TEXT_CHANNEL_FORMAT_2_0CH,
|
|
SPEAKERS_STEREO);
|
|
|
|
ComPtr<DeckLinkDevice> device;
|
|
device.Set(deviceEnum->FindByHash(hash));
|
|
|
|
if (!device) {
|
|
obs_property_list_add_int(modeList, mode, modeId);
|
|
obs_property_list_item_disable(modeList, 0, true);
|
|
} else {
|
|
const std::vector<DeckLinkDeviceMode*> &modes =
|
|
device->GetModes();
|
|
|
|
for (DeckLinkDeviceMode *mode : modes) {
|
|
obs_property_list_add_int(modeList,
|
|
mode->GetName().c_str(),
|
|
mode->GetId());
|
|
}
|
|
|
|
if (device->GetMaxChannel() >= 8) {
|
|
obs_property_list_add_int(channelList, TEXT_CHANNEL_FORMAT_5_1CH,
|
|
SPEAKERS_5POINT1);
|
|
obs_property_list_add_int(channelList, TEXT_CHANNEL_FORMAT_5_1CH_BACK,
|
|
SPEAKERS_5POINT1_SURROUND);
|
|
obs_property_list_add_int(channelList, TEXT_CHANNEL_FORMAT_7_1CH,
|
|
SPEAKERS_7POINT1);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void fill_out_devices(obs_property_t *list)
|
|
{
|
|
deviceEnum->Lock();
|
|
|
|
const std::vector<DeckLinkDevice*> &devices = deviceEnum->GetDevices();
|
|
for (DeckLinkDevice *device : devices) {
|
|
obs_property_list_add_string(list,
|
|
device->GetDisplayName().c_str(),
|
|
device->GetHash().c_str());
|
|
}
|
|
|
|
deviceEnum->Unlock();
|
|
}
|
|
|
|
static bool color_format_changed(obs_properties_t *props,
|
|
obs_property_t *list, obs_data_t *settings);
|
|
|
|
static bool mode_id_changed(obs_properties_t *props,
|
|
obs_property_t *list, obs_data_t *settings)
|
|
{
|
|
long long id = obs_data_get_int(settings, MODE_ID);
|
|
|
|
list = obs_properties_get(props, PIXEL_FORMAT);
|
|
obs_property_set_visible(list, id != MODE_ID_AUTO);
|
|
|
|
return color_format_changed(props, nullptr, settings);
|
|
}
|
|
|
|
static bool color_format_changed(obs_properties_t *props,
|
|
obs_property_t *list, obs_data_t *settings)
|
|
{
|
|
long long id = obs_data_get_int(settings, MODE_ID);
|
|
BMDPixelFormat pixelFormat = (BMDPixelFormat)obs_data_get_int(settings,
|
|
PIXEL_FORMAT);
|
|
|
|
list = obs_properties_get(props, COLOR_SPACE);
|
|
obs_property_set_visible(list,
|
|
id != MODE_ID_AUTO && pixelFormat == bmdFormat8BitYUV);
|
|
|
|
list = obs_properties_get(props, COLOR_RANGE);
|
|
obs_property_set_visible(list,
|
|
id == MODE_ID_AUTO || pixelFormat == bmdFormat8BitYUV);
|
|
|
|
return true;
|
|
}
|
|
|
|
static obs_properties_t *decklink_get_properties(void *data)
|
|
{
|
|
obs_properties_t *props = obs_properties_create();
|
|
|
|
obs_property_t *list = obs_properties_add_list(props, DEVICE_HASH,
|
|
TEXT_DEVICE, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
|
|
obs_property_set_modified_callback(list, decklink_device_changed);
|
|
|
|
fill_out_devices(list);
|
|
|
|
list = obs_properties_add_list(props, MODE_ID, TEXT_MODE,
|
|
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
|
|
obs_property_set_modified_callback(list, mode_id_changed);
|
|
|
|
list = obs_properties_add_list(props, PIXEL_FORMAT,
|
|
TEXT_PIXEL_FORMAT, OBS_COMBO_TYPE_LIST,
|
|
OBS_COMBO_FORMAT_INT);
|
|
obs_property_set_modified_callback(list, color_format_changed);
|
|
|
|
obs_property_list_add_int(list, "8-bit YUV", bmdFormat8BitYUV);
|
|
obs_property_list_add_int(list, "8-bit BGRA", bmdFormat8BitBGRA);
|
|
|
|
list = obs_properties_add_list(props, COLOR_SPACE, TEXT_COLOR_SPACE,
|
|
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
|
|
obs_property_list_add_int(list, TEXT_COLOR_SPACE_DEFAULT, VIDEO_CS_DEFAULT);
|
|
obs_property_list_add_int(list, "BT.601", VIDEO_CS_601);
|
|
obs_property_list_add_int(list, "BT.709", VIDEO_CS_709);
|
|
|
|
list = obs_properties_add_list(props, COLOR_RANGE, TEXT_COLOR_RANGE,
|
|
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
|
|
obs_property_list_add_int(list, TEXT_COLOR_RANGE_DEFAULT, VIDEO_RANGE_DEFAULT);
|
|
obs_property_list_add_int(list, TEXT_COLOR_RANGE_PARTIAL, VIDEO_RANGE_PARTIAL);
|
|
obs_property_list_add_int(list, TEXT_COLOR_RANGE_FULL, VIDEO_RANGE_FULL);
|
|
|
|
list = obs_properties_add_list(props, CHANNEL_FORMAT,
|
|
TEXT_CHANNEL_FORMAT, OBS_COMBO_TYPE_LIST,
|
|
OBS_COMBO_FORMAT_INT);
|
|
obs_property_list_add_int(list, TEXT_CHANNEL_FORMAT_NONE,
|
|
SPEAKERS_UNKNOWN);
|
|
obs_property_list_add_int(list, TEXT_CHANNEL_FORMAT_2_0CH,
|
|
SPEAKERS_STEREO);
|
|
|
|
obs_properties_add_bool(props, BUFFERING, TEXT_BUFFERING);
|
|
|
|
UNUSED_PARAMETER(data);
|
|
return props;
|
|
}
|
|
|
|
bool obs_module_load(void)
|
|
{
|
|
deviceEnum = new DeckLinkDeviceDiscovery();
|
|
if (!deviceEnum->Init())
|
|
return true;
|
|
|
|
struct obs_source_info info = {};
|
|
info.id = "decklink-input";
|
|
info.type = OBS_SOURCE_TYPE_INPUT;
|
|
info.output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_AUDIO |
|
|
OBS_SOURCE_DO_NOT_DUPLICATE;
|
|
info.create = decklink_create;
|
|
info.destroy = decklink_destroy;
|
|
info.get_defaults = decklink_get_defaults;
|
|
info.get_name = decklink_get_name;
|
|
info.get_properties = decklink_get_properties;
|
|
info.update = decklink_update;
|
|
|
|
obs_register_source(&info);
|
|
|
|
return true;
|
|
}
|
|
|
|
void obs_module_unload(void)
|
|
{
|
|
delete deviceEnum;
|
|
}
|