Commit 752df69b authored by François Cartegnie's avatar François Cartegnie 🤞

sout: add SDI stream output

Decklink vout backport, so
this is the currently the only support
parent 4be7fdc4
......@@ -37,6 +37,10 @@ Video output:
* Remove RealRTSP plugin
* Remove Real demuxer plugin
Stream output:
* New SDI output with improved audio and ancillary support.
Candidate for deprecation of decklink vout/aout modules.
macOS:
* Remove Growl notification support
......
......@@ -1896,7 +1896,7 @@ if test "${enable_decklink}" != "no"
then
if test "${with_decklink_sdk}" != "no" -a -n "${with_decklink_sdk}"
then
VLC_ADD_CPPFLAGS([decklink decklinkoutput],[-I${with_decklink_sdk}/include])
VLC_ADD_CPPFLAGS([decklink decklinkoutput stream_out_sdi],[-I${with_decklink_sdk}/include])
fi
VLC_SAVE_FLAGS
CPPFLAGS="${CPPFLAGS} ${CPPFLAGS_decklink}"
......
......@@ -340,6 +340,7 @@ $Id$
* scte18: SCTE-18/Emergency Alert Messaging for Cable decoder
* scte27: SCTE-27/Digicipher subtitles decoder
* sd_journal: logger output to SystemD journal
* sdiout: SDI stream output
* sdl_image: SDL-based image decoder
* sdp: SDP fake access
* secret: store secrets via Gnome libsecret
......
......@@ -50,6 +50,24 @@ sout_LTLIBRARIES = \
libstream_out_setid_plugin.la \
libstream_out_transcode_plugin.la
if HAVE_DECKLINK
libstream_out_sdi_plugin_la_CXXFLAGS = $(AM_CXXFLAGS) $(CPPFLAGS_decklinkoutput)
libstream_out_sdi_plugin_la_LIBADD = $(LIBS_decklink) $(LIBDL) -lpthread
libstream_out_sdi_plugin_la_SOURCES = stream_out/sdi/sdiout.cpp \
stream_out/sdi/sdiout.hpp \
stream_out/sdi/Ancillary.cpp \
stream_out/sdi/Ancillary.hpp \
stream_out/sdi/DBMSDIOutput.cpp \
stream_out/sdi/DBMSDIOutput.hpp \
stream_out/sdi/SDIOutput.cpp \
stream_out/sdi/SDIOutput.hpp \
stream_out/sdi/SDIStream.cpp \
stream_out/sdi/SDIStream.hpp \
stream_out/sdi/V210.cpp \
stream_out/sdi/V210.hpp
sout_LTLIBRARIES += libstream_out_sdi_plugin.la
endif
# RTP plugin
sout_LTLIBRARIES += libstream_out_rtp_plugin.la
libstream_out_rtp_plugin_la_SOURCES = \
......
/*****************************************************************************
* Ancillary.cpp: SDI Ancillary
*****************************************************************************
* Copyright © 2014-2016 VideoLAN and VideoLAN Authors
* 2018 VideoLabs
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "Ancillary.hpp"
using namespace sdi;
AFD::AFD(uint8_t afdcode, uint8_t ar)
{
this->afdcode = afdcode;
this->ar = ar;
}
AFD::~AFD()
{
}
static inline void put_le32(uint8_t **p, uint32_t d)
{
SetDWLE(*p, d);
(*p) += 4;
}
void AFD::FillBuffer(uint8_t *p_buf, size_t i_buf)
{
const size_t len = 6 /* vanc header */ + 8 /* AFD data */ + 1 /* csum */;
const size_t s = ((len + 5) / 6) * 6; // align for v210
if(s * 6 >= i_buf / 16)
return;
uint16_t afd[s];
afd[0] = 0x000;
afd[1] = 0x3ff;
afd[2] = 0x3ff;
afd[3] = 0x41; // DID
afd[4] = 0x05; // SDID
afd[5] = 8; // Data Count
int bar_data_flags = 0;
int bar_data_val1 = 0;
int bar_data_val2 = 0;
afd[ 6] = ((afdcode & 0x0F) << 3) | ((ar & 0x01) << 2); /* SMPTE 2016-1 */
afd[ 7] = 0; // reserved
afd[ 8] = 0; // reserved
afd[ 9] = bar_data_flags << 4;
afd[10] = bar_data_val1 << 8;
afd[11] = bar_data_val1 & 0xff;
afd[12] = bar_data_val2 << 8;
afd[13] = bar_data_val2 & 0xff;
/* parity bit */
for (size_t i = 3; i < len - 1; i++)
afd[i] |= vlc_parity((unsigned)afd[i]) ? 0x100 : 0x200;
/* vanc checksum */
uint16_t vanc_sum = 0;
for (size_t i = 3; i < len - 1; i++) {
vanc_sum += afd[i];
vanc_sum &= 0x1ff;
}
afd[len - 1] = vanc_sum | ((~vanc_sum & 0x100) << 1);
/* pad */
for (size_t i = len; i < s; i++)
afd[i] = 0x040;
/* convert to v210 and write into VANC */
for (size_t w = 0; w < s / 6 ; w++) {
put_le32(&p_buf, afd[w*6+0] << 10);
put_le32(&p_buf, afd[w*6+1] | (afd[w*6+2] << 20));
put_le32(&p_buf, afd[w*6+3] << 10);
put_le32(&p_buf, afd[w*6+4] | (afd[w*6+5] << 20));
}
}
/*****************************************************************************
* Ancillary.hpp: SDI Ancillary
*****************************************************************************
* Copyright © 2014-2016 VideoLAN and VideoLAN Authors
* 2018 VideoLabs
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifndef ANCILLARY_HPP
#define ANCILLARY_HPP
#include <vlc_common.h>
namespace sdi
{
class Ancillary
{
public:
virtual void FillBuffer(uint8_t *, size_t) = 0;
};
class AFD : public Ancillary
{
public:
AFD(uint8_t afdcode, uint8_t ar);
virtual ~AFD();
virtual void FillBuffer(uint8_t *, size_t);
private:
uint8_t afdcode;
uint8_t ar;
};
}
#endif // ANCILLARY_HPP
/*****************************************************************************
* DBMSDIOutput.cpp: Decklink SDI Output
*****************************************************************************
* Copyright © 2014-2016 VideoLAN and VideoLAN Authors
* 2018 VideoLabs
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "DBMSDIOutput.hpp"
#include "SDIStream.hpp"
#include "Ancillary.hpp"
#include "V210.hpp"
#include <DeckLinkAPIDispatch.cpp>
#include "sdiout.hpp"
#include <vlc_es.h>
#include <vlc_picture.h>
#include <vlc_interrupt.h>
#include <vlc_image.h>
#include <arpa/inet.h>
using namespace sdi_sout;
DBMSDIOutput::DBMSDIOutput(sout_stream_t *p_stream) :
SDIOutput(p_stream)
{
p_card = NULL;
p_output = NULL;
offset = 0;
lasttimestamp = 0;
b_running = false;
}
DBMSDIOutput::~DBMSDIOutput()
{
if(p_output)
{
BMDTimeValue out;
p_output->StopScheduledPlayback(lasttimestamp, &out, timescale);
p_output->DisableVideoOutput();
p_output->DisableAudioOutput();
p_output->Release();
}
if(p_card)
p_card->Release();
}
AbstractStream *DBMSDIOutput::Add(const es_format_t *fmt)
{
AbstractStream *s = SDIOutput::Add(fmt);
if(s)
{
msg_Dbg(p_stream, "accepted %s %4.4s",
s->getID().toString().c_str(), (const char *) &fmt->i_codec);
if( videoStream && (audioStream || audio.i_channels == 0) )
Start();
}
else
{
msg_Err(p_stream, "rejected es id %d %4.4s",
fmt->i_id, (const char *) &fmt->i_codec);
}
return s;
}
IDeckLinkDisplayMode * DBMSDIOutput::MatchDisplayMode(const video_format_t *fmt, BMDDisplayMode forcedmode)
{
HRESULT result;
IDeckLinkDisplayMode *p_selected = NULL;
IDeckLinkDisplayModeIterator *p_iterator = NULL;
for(int i=0; i<4 && p_selected==NULL; i++)
{
int i_width = (i % 2 == 0) ? fmt->i_width : fmt->i_visible_width;
int i_height = (i % 2 == 0) ? fmt->i_height : fmt->i_visible_height;
int i_div = (i > 2) ? 4 : 0;
result = p_output->GetDisplayModeIterator(&p_iterator);
if(result == S_OK)
{
IDeckLinkDisplayMode *p_mode = NULL;
while(p_iterator->Next(&p_mode) == S_OK)
{
BMDDisplayMode mode_id = p_mode->GetDisplayMode();
BMDTimeValue frameduration;
BMDTimeScale timescale;
const char *psz_mode_name;
if(p_mode->GetFrameRate(&frameduration, &timescale) == S_OK &&
p_mode->GetName(&psz_mode_name) == S_OK)
{
BMDDisplayMode modenl = htonl(mode_id);
if(i==0)
{
BMDFieldDominance field = htonl(p_mode->GetFieldDominance());
msg_Dbg(p_stream, "Found mode '%4.4s': %s (%ldx%ld, %4.4s, %.3f fps, scale %ld dur %ld)",
(const char*)&modenl, psz_mode_name,
p_mode->GetWidth(), p_mode->GetHeight(),
(const char *)&field,
double(timescale) / frameduration,
timescale, frameduration);
}
}
else
{
p_mode->Release();
continue;
}
if(forcedmode != bmdDisplayModeNotSupported && unlikely(!p_selected))
{
BMDDisplayMode modenl = htonl(forcedmode);
msg_Dbg(p_stream, "Forced mode '%4.4s'", (char *)&modenl);
if(forcedmode == mode_id)
p_selected = p_mode;
else
p_mode->Release();
continue;
}
if(p_selected == NULL && forcedmode == bmdDisplayModeNotSupported)
{
if(i_width >> i_div == p_mode->GetWidth() >> i_div &&
i_height >> i_div == p_mode->GetHeight() >> i_div)
{
unsigned int num_deck, den_deck;
unsigned int num_stream, den_stream;
vlc_ureduce(&num_deck, &den_deck, timescale, frameduration, 0);
vlc_ureduce(&num_stream, &den_stream,
fmt->i_frame_rate, fmt->i_frame_rate_base, 0);
if (num_deck == num_stream && den_deck == den_stream)
{
msg_Info(p_stream, "Matches incoming stream");
p_selected = p_mode;
continue;
}
}
}
p_mode->Release();
}
p_iterator->Release();
}
}
return p_selected;
}
const char * DBMSDIOutput::ErrorToString(long i_code)
{
static struct
{
long i_return_code;
const char * const psz_string;
} const errors_to_string[] = {
{ E_UNEXPECTED, "Unexpected error" },
{ E_NOTIMPL, "Not implemented" },
{ E_OUTOFMEMORY, "Out of memory" },
{ E_INVALIDARG, "Invalid argument" },
{ E_NOINTERFACE, "No interface" },
{ E_POINTER, "Invalid pointer" },
{ E_HANDLE, "Invalid handle" },
{ E_ABORT, "Aborted" },
{ E_FAIL, "Failed" },
{ E_ACCESSDENIED,"Access denied" }
};
for(size_t i=0; i<ARRAY_SIZE(errors_to_string); i++)
{
if(errors_to_string[i].i_return_code == i_code)
return errors_to_string[i].psz_string;
}
return NULL;
}
#define CHECK(message) do { \
if (result != S_OK) \
{ \
const char *psz_err = ErrorToString(result); \
if(psz_err)\
msg_Err(p_stream, message ": %s", psz_err); \
else \
msg_Err(p_stream, message ": 0x%X", result); \
goto error; \
} \
} while(0)
int DBMSDIOutput::Open()
{
HRESULT result;
IDeckLinkIterator *decklink_iterator = NULL;
int i_card_index = var_InheritInteger(p_stream, CFG_PREFIX "card-index");
if (i_card_index < 0)
{
msg_Err(p_stream, "Invalid card index %d", i_card_index);
goto error;
}
decklink_iterator = CreateDeckLinkIteratorInstance();
if (!decklink_iterator)
{
msg_Err(p_stream, "DeckLink drivers not found.");
goto error;
}
for(int i = 0; i <= i_card_index; ++i)
{
if (p_card)
{
p_card->Release();
p_card = NULL;
}
result = decklink_iterator->Next(&p_card);
CHECK("Card not found");
}
const char *psz_model_name;
result = p_card->GetModelName(&psz_model_name);
CHECK("Unknown model name");
msg_Dbg(p_stream, "Opened DeckLink PCI card %s", psz_model_name);
result = p_card->QueryInterface(IID_IDeckLinkOutput, (void**)&p_output);
CHECK("No outputs");
decklink_iterator->Release();
return VLC_SUCCESS;
error:
if (p_output)
{
p_output->Release();
p_output = NULL;
}
if (p_card)
{
p_card->Release();
p_output = NULL;
}
if (decklink_iterator)
decklink_iterator->Release();
return VLC_EGENERIC;
}
int DBMSDIOutput::ConfigureAudio(const audio_format_t *)
{
HRESULT result;
if(FAKE_DRIVER)
return VLC_SUCCESS;
if(!p_output)
return VLC_EGENERIC;
if(!video.configuredfmt.i_codec && b_running)
return VLC_EGENERIC;
if (audio.i_channels > 0)
{
audio.configuredfmt.i_codec =
audio.configuredfmt.audio.i_format = VLC_CODEC_S16N;
audio.configuredfmt.audio.i_channels = 2;
audio.configuredfmt.audio.i_physical_channels = AOUT_CHANS_STEREO;
audio.configuredfmt.audio.i_rate = 48000;
audio.configuredfmt.audio.i_bitspersample = 16;
audio.configuredfmt.audio.i_blockalign = 2 * 16 / 8;
audio.configuredfmt.audio.i_frame_length = FRAME_SIZE;
result = p_output->EnableAudioOutput(
bmdAudioSampleRate48kHz,
bmdAudioSampleType16bitInteger,
2,
bmdAudioOutputStreamTimestamped);
CHECK("Could not start audio output");
}
return VLC_SUCCESS;
error:
return VLC_EGENERIC;
}
static BMDVideoConnection getVConn(const char *psz)
{
BMDVideoConnection conn = bmdVideoConnectionSDI;
if(!psz)
return conn;
if (!strcmp(psz, "sdi"))
conn = bmdVideoConnectionSDI;
else if (!strcmp(psz, "hdmi"))
conn = bmdVideoConnectionHDMI;
else if (!strcmp(psz, "opticalsdi"))
conn = bmdVideoConnectionOpticalSDI;
else if (!strcmp(psz, "component"))
conn = bmdVideoConnectionComponent;
else if (!strcmp(psz, "composite"))
conn = bmdVideoConnectionComposite;
else if (!strcmp(psz, "svideo"))
conn = bmdVideoConnectionSVideo;
return conn;
}
int DBMSDIOutput::ConfigureVideo(const video_format_t *vfmt)
{
HRESULT result;
BMDDisplayMode wanted_mode_id = bmdDisplayModeNotSupported;
IDeckLinkConfiguration *p_config = NULL;
IDeckLinkAttributes *p_attributes = NULL;
IDeckLinkDisplayMode *p_display_mode = NULL;
char *psz_string = NULL;
video_format_t *fmt = &video.configuredfmt.video;
if(FAKE_DRIVER)
{
video_format_Copy(fmt, vfmt);
fmt->i_chroma = !video.tenbits ? VLC_CODEC_UYVY : VLC_CODEC_I422_10L;
fmt->i_frame_rate = (unsigned) frameduration;
fmt->i_frame_rate_base = (unsigned) timescale;
video.configuredfmt.i_codec = fmt->i_chroma;
return VLC_SUCCESS;
}
if(!p_output)
return VLC_EGENERIC;
if(!video.configuredfmt.i_codec && b_running)
return VLC_EGENERIC;
/* Now configure card */
if(!p_output)
return VLC_EGENERIC;
result = p_card->QueryInterface(IID_IDeckLinkConfiguration, (void**)&p_config);
CHECK("Could not get config interface");
psz_string = var_InheritString(p_stream, CFG_PREFIX "mode");
if(psz_string)
{
size_t len = strlen(psz_string);
if (len > 4)
{
free(psz_string);
msg_Err(p_stream, "Invalid mode %s", psz_string);
goto error;
}
memset(&wanted_mode_id, ' ', 4);
strncpy((char*)&wanted_mode_id, psz_string, 4);
wanted_mode_id = ntohl(wanted_mode_id);
free(psz_string);
}
/* Read attributes */
result = p_card->QueryInterface(IID_IDeckLinkAttributes, (void**)&p_attributes);
CHECK("Could not get IDeckLinkAttributes");
int64_t vconn;
result = p_attributes->GetInt(BMDDeckLinkVideoOutputConnections, &vconn); /* reads mask */
CHECK("Could not get BMDDeckLinkVideoOutputConnections");
psz_string = var_InheritString(p_stream, CFG_PREFIX "video-connection");
vconn = getVConn(psz_string);
free(psz_string);
if (vconn == 0)
{
msg_Err(p_stream, "Invalid video connection specified");
goto error;
}
result = p_config->SetInt(bmdDeckLinkConfigVideoOutputConnection, vconn);
CHECK("Could not set video output connection");
p_display_mode = MatchDisplayMode(vfmt, wanted_mode_id);
if(p_display_mode == NULL)
{
msg_Err(p_stream, "Could not negociate a compatible display mode");
goto error;
}
else
{
BMDDisplayMode mode_id = p_display_mode->GetDisplayMode();
BMDDisplayMode modenl = htonl(mode_id);
msg_Dbg(p_stream, "Selected mode '%4.4s'", (char *) &modenl);
BMDVideoOutputFlags flags = bmdVideoOutputVANC;
if (mode_id == bmdModeNTSC ||
mode_id == bmdModeNTSC2398 ||
mode_id == bmdModePAL)
{
flags = bmdVideoOutputVITC;
}
BMDDisplayModeSupport support;
IDeckLinkDisplayMode *resultMode;
result = p_output->DoesSupportVideoMode(mode_id,
video.tenbits ? bmdFormat10BitYUV : bmdFormat8BitYUV,
flags, &support, &resultMode);
CHECK("Does not support video mode");
if (support == bmdDisplayModeNotSupported)
{
msg_Err(p_stream, "Video mode not supported");
goto error;
}
if (p_display_mode->GetWidth() <= 0 || p_display_mode->GetWidth() & 1)
{
msg_Err(p_stream, "Unknown video mode specified.");
goto error;
}
result = p_display_mode->GetFrameRate(&frameduration,
&timescale);
CHECK("Could not read frame rate");
result = p_output->EnableVideoOutput(mode_id, flags);
CHECK("Could not enable video output");
video_format_Copy(fmt, vfmt);
fmt->i_width = fmt->i_visible_width = p_display_mode->GetWidth();
fmt->i_height = fmt->i_visible_height = p_display_mode->GetHeight();
fmt->i_x_offset = 0;
fmt->i_y_offset = 0;
fmt->i_sar_num = 0;
fmt->i_sar_den = 0;
fmt->i_chroma = !video.tenbits ? VLC_CODEC_UYVY : VLC_CODEC_I422_10L; /* we will convert to v210 */
fmt->i_frame_rate = (unsigned) frameduration;
fmt->i_frame_rate_base = (unsigned) timescale;
video.configuredfmt.i_codec = fmt->i_chroma;
char *psz_file = var_InheritString(p_stream, CFG_PREFIX "nosignal-image");
if(psz_file)
{
video.pic_nosignal = CreateNoSignalPicture(psz_file, fmt);
if (!video.pic_nosignal)
msg_Err(p_stream, "Could not create no signal picture");
free(psz_file);
}
}
p_display_mode->Release();
p_attributes->Release();
p_config->Release();
return VLC_SUCCESS;
error:
if (p_display_mode)
p_display_mode->Release();
if(p_attributes)
p_attributes->Release();
if (p_config)
p_config->Release();
return VLC_EGENERIC;
}
int DBMSDIOutput::Start()
{
HRESULT result;
if(FAKE_DRIVER && !b_running)
{
b_running = true;
return VLC_SUCCESS;
}
if(b_running)
return VLC_EGENERIC;
result = p_output->StartScheduledPlayback(
SEC_FROM_VLC_TICK(vlc_tick_now() * timescale), timescale, 1.0);
CHECK("Could not start playback");
b_running = true;
return VLC_SUCCESS;
error:
return VLC_EGENERIC;
}
int DBMSDIOutput::Process()