Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@
/src
/mapfile
/tools/python/__pycache__/
.vscode/
9 changes: 9 additions & 0 deletions doc/indevs.texi
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,9 @@ Set the audio sample rate in Hz to capture. This option is only used when the in
@item buffer_packing
Specifies the buffer packing format and whether to enable the FPGA's color space converter on the board. If not set, the default buffer packing is YUV422 10-bit when the Line Padding property can be enabled, or YUV422 8-bit otherwise.

@item dual_stream
Force the 3G-B dual stream interface when it cannot be auto-detected. A 3G Level B-DS stream received on the RX0 physical connector is split into two independent streams: one RX0 stream receives the A link and one RX1 stream receives the B link. When enabled, FFmpeg expects the two streams to be provided on consecutive channels (RX0 for the A link, RX1 for the B link). This option is only relevant for SDI 3G Level B-DS signals. An error is displayed if the selected board and channel do not support the 3G-B dual stream interface. Accepted values are @option{1} (or @option{true}) to enable dual stream mode, and @option{0} (or @option{false}) to disable it. Default is @option{false}.

@end table

@subsection Examples
Expand Down Expand Up @@ -1566,6 +1569,12 @@ Capture a video clip from the options using the ltc companion card timestamp sou
ffmpeg -channel_index 0 -board_index 0 -timestamp_source ltc_companion_card -f videomaster -i dummy -c:a copy -c:v copy output.avi
@end example

@item
Capture a 3G Level B-DS dual stream signal received on RX0/RX1 of board 0
@example
ffmpeg -f videomaster -board_index 0 -channel_index 0 -dual_stream 1 -i dummy -c:a copy -c:v copy output.avi
@end example

@end itemize

@section vfwcap
Expand Down
174 changes: 142 additions & 32 deletions libavdevice/videomaster_common.c
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,16 @@ static int get_nb_channels_from_audio_infoframe_and_aes_status(
*/
static VHD_CORE_BOARDPROPERTY get_passive_loopback_property(int channel_index);

/**
* @brief Get the RX SDI board property from the given index
*
* @param index Index of the RX SDI
* @return uint32_t The RX SDI board property

*/
static uint32_t
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The forward declaration marks get_rx_sdi_board_property_clock_divisor_from_index() as static, but the definition later is non-static. This causes a linkage mismatch and can fail to compile with conflicting storage-class diagnostics. Make the definition static (or remove static from the prototype) so they match.

Suggested change
static uint32_t
uint32_t

Copilot uses AI. Check for mistakes.
get_rx_sdi_board_property_clock_divisor_from_index(uint32_t index);

/**
* @brief Get the rx stream type Videomaster enumeration from the channel index
*
Expand Down Expand Up @@ -533,7 +543,8 @@ int add_device_info_into_list(VideoMasterContext *videomaster_context,
&videomaster_context->video_height,
&videomaster_context->video_frame_rate_num,
&videomaster_context->video_frame_rate_den,
&videomaster_context->video_interlaced),
&videomaster_context->video_interlaced,
videomaster_context->dual_stream),
"", error_msg);

snprintf(error_msg, sizeof(error_msg),
Expand Down Expand Up @@ -887,7 +898,8 @@ int get_audio_stream_properties_from_audio_infoframe(
&videomaster_context->video_height,
&videomaster_context->video_frame_rate_num,
&videomaster_context->video_frame_rate_den,
&videomaster_context->video_interlaced),
&videomaster_context->video_interlaced,
videomaster_context->dual_stream),
"Video stream properties retrieved successfully",
"Could not retrieve video stream properties");

Expand Down Expand Up @@ -1163,6 +1175,39 @@ int get_nb_channels_from_audio_infoframe_and_aes_status(
return return_code;
}

uint32_t get_rx_sdi_board_property_clock_divisor_from_index(uint32_t index)
{
switch (index)
{
case 0:
return VHD_SDI_BP_RX0_CLOCK_DIV;
case 1:
return VHD_SDI_BP_RX1_CLOCK_DIV;
case 2:
return VHD_SDI_BP_RX2_CLOCK_DIV;
case 3:
return VHD_SDI_BP_RX3_CLOCK_DIV;
case 4:
return VHD_SDI_BP_RX4_CLOCK_DIV;
case 5:
return VHD_SDI_BP_RX5_CLOCK_DIV;
case 6:
return VHD_SDI_BP_RX6_CLOCK_DIV;
case 7:
return VHD_SDI_BP_RX7_CLOCK_DIV;
case 8:
return VHD_SDI_BP_RX8_CLOCK_DIV;
case 9:
return VHD_SDI_BP_RX9_CLOCK_DIV;
case 10:
return VHD_SDI_BP_RX10_CLOCK_DIV;
case 11:
return VHD_SDI_BP_RX11_CLOCK_DIV;
default:
return VHD_SDI_BP_RX0_CLOCK_DIV;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_rx_sdi_board_property_clock_divisor_from_index() returns RX0’s property for any unknown index. That can silently use the wrong clock divisor when channel_index is out of the supported range (and it’s inconsistent with get_rx_stream_type_from_index(), which returns an invalid sentinel). Consider returning an invalid/"NB" value and having the caller detect/report EINVAL instead of falling back to RX0.

Suggested change
return VHD_SDI_BP_RX0_CLOCK_DIV;
/* Invalid index: return sentinel value instead of falling back to RX0. */
return 0;

Copilot uses AI. Check for mistakes.
}
}

VHD_STREAMTYPE get_rx_stream_type_from_index(uint32_t index)
{
switch (index)
Expand Down Expand Up @@ -1455,7 +1500,7 @@ int init_audio_info(VideoMasterContext *videomaster_context,
VHD_AUDIOGROUP *audio_group = NULL;
VHD_AUDIOCHANNEL *audio_channel = NULL;
VHD_AUDIOFORMAT buffer_format = videomaster_context->audio_sample_size ==
AV_VIDEOMASTER_SAMPLE_SIZE_16
AV_VIDEOMASTER_SAMPLE_SIZE_16
? VHD_AF_16
: VHD_AF_24;
uint32_t channel_count = 0;
Expand Down Expand Up @@ -2091,7 +2136,8 @@ int ff_videomaster_get_video_stream_properties(
AVFormatContext *avctx, HANDLE board_handle, HANDLE stream_handle,
uint32_t channel_index, enum AVVideoMasterChannelType *channel_type,
union VideoMasterVideoInfo *video_info, uint32_t *width, uint32_t *height,
uint32_t *frame_rate_num, uint32_t *frame_rate_den, bool *interlaced)
uint32_t *frame_rate_num, uint32_t *frame_rate_den, bool *interlaced,
bool dual_stream)
{
uint32_t frame_rate = 0;
uint32_t total_width = 0;
Expand Down Expand Up @@ -2199,39 +2245,80 @@ int ff_videomaster_get_video_stream_properties(
}
else
{
handle_vhd_status(
avctx,
VHD_GetChannelProperty(board_handle, VHD_RX_CHANNEL, channel_index,
VHD_SDI_CP_VIDEO_STANDARD,
(uint32_t *)&video_info->sdi.video_standard),
"", "");
if (dual_stream)
{
video_info->sdi.interface = VHD_INTERFACE_3G_B_DS_425_1;

// must start the stream with correct interface to get auto
// detection
HANDLE stream_handle = NULL;
VHD_OpenStreamHandle(board_handle,
get_rx_stream_type_from_index(channel_index),
VHD_SDI_STPROC_JOINED, NULL, &stream_handle,
NULL);
VHD_SetStreamProperty(stream_handle, VHD_SDI_SP_INTERFACE,
video_info->sdi.interface);

handle_vhd_status(avctx,
VHD_GetStreamProperty(
stream_handle, VHD_SDI_SP_VIDEO_STANDARD,
(uint32_t *)&video_info->sdi.video_standard),
"", "");
int status = VHD_StartStream(stream_handle);

handle_vhd_status(avctx,
VHD_GetStreamProperty(
stream_handle, VHD_SDI_SP_VIDEO_STANDARD,
(uint32_t *)&video_info->sdi.video_standard),
"", "");

handle_vhd_status(
avctx,
VHD_GetBoardProperty(
board_handle,
get_rx_sdi_board_property_clock_divisor_from_index(
channel_index),
(uint32_t *)&video_info->sdi.clock_divisor),
"", "");
if (status == VHDERR_NOERROR)
VHD_StopStream(stream_handle);
VHD_CloseStreamHandle(stream_handle);
Comment on lines +2254 to +2285
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the dual_stream SDI path, a new local variable named stream_handle shadows the function parameter, and VHD_OpenStreamHandle/VHD_SetStreamProperty/VHD_StartStream return values are not checked before using/closing the handle. If opening the handle fails, subsequent calls will operate on NULL and may crash. Use a differently named local handle (or reuse local_stream_handle), wrap VHD_* calls with handle_vhd_status (and honor its return), and only stop/close when the handle was successfully opened.

Suggested change
HANDLE stream_handle = NULL;
VHD_OpenStreamHandle(board_handle,
get_rx_stream_type_from_index(channel_index),
VHD_SDI_STPROC_JOINED, NULL, &stream_handle,
NULL);
VHD_SetStreamProperty(stream_handle, VHD_SDI_SP_INTERFACE,
video_info->sdi.interface);
handle_vhd_status(avctx,
VHD_GetStreamProperty(
stream_handle, VHD_SDI_SP_VIDEO_STANDARD,
(uint32_t *)&video_info->sdi.video_standard),
"", "");
int status = VHD_StartStream(stream_handle);
handle_vhd_status(avctx,
VHD_GetStreamProperty(
stream_handle, VHD_SDI_SP_VIDEO_STANDARD,
(uint32_t *)&video_info->sdi.video_standard),
"", "");
handle_vhd_status(
avctx,
VHD_GetBoardProperty(
board_handle,
get_rx_sdi_board_property_clock_divisor_from_index(
channel_index),
(uint32_t *)&video_info->sdi.clock_divisor),
"", "");
if (status == VHDERR_NOERROR)
VHD_StopStream(stream_handle);
VHD_CloseStreamHandle(stream_handle);
HANDLE local_stream_handle = NULL;
int open_status = handle_vhd_status(
avctx,
VHD_OpenStreamHandle(
board_handle,
get_rx_stream_type_from_index(channel_index),
VHD_SDI_STPROC_JOINED, NULL, &local_stream_handle, NULL),
"", "");
if (open_status == VHDERR_NOERROR && local_stream_handle)
{
handle_vhd_status(
avctx,
VHD_SetStreamProperty(local_stream_handle,
VHD_SDI_SP_INTERFACE,
video_info->sdi.interface),
"", "");
handle_vhd_status(
avctx,
VHD_GetStreamProperty(
local_stream_handle, VHD_SDI_SP_VIDEO_STANDARD,
(uint32_t *)&video_info->sdi.video_standard),
"", "");
int start_status = handle_vhd_status(
avctx, VHD_StartStream(local_stream_handle), "", "");
handle_vhd_status(
avctx,
VHD_GetStreamProperty(
local_stream_handle, VHD_SDI_SP_VIDEO_STANDARD,
(uint32_t *)&video_info->sdi.video_standard),
"", "");
handle_vhd_status(
avctx,
VHD_GetBoardProperty(
board_handle,
get_rx_sdi_board_property_clock_divisor_from_index(
channel_index),
(uint32_t *)&video_info->sdi.clock_divisor),
"", "");
if (start_status == VHDERR_NOERROR)
VHD_StopStream(local_stream_handle);
VHD_CloseStreamHandle(local_stream_handle);
}

Copilot uses AI. Check for mistakes.
}
else
{
handle_vhd_status(
avctx,
VHD_GetChannelProperty(board_handle, VHD_RX_CHANNEL,
channel_index, VHD_SDI_CP_INTERFACE,
(uint32_t *)&video_info->sdi.interface),
"", "");
handle_vhd_status(avctx,
VHD_GetChannelProperty(
board_handle, VHD_RX_CHANNEL, channel_index,
VHD_SDI_CP_VIDEO_STANDARD,
(uint32_t *)&video_info->sdi.video_standard),
"", "");

handle_vhd_status(avctx,
VHD_GetChannelProperty(
board_handle, VHD_RX_CHANNEL, channel_index,
VHD_SDI_CP_CLOCK_DIVISOR,
(uint32_t *)&video_info->sdi.clock_divisor),
"", "");

handle_vhd_status(
avctx,
VHD_GetChannelProperty(board_handle, VHD_RX_CHANNEL,
channel_index, VHD_SDI_CP_GENLOCK_OFFSET,
&video_info->sdi.genlock_offset),
"", "");
}
handle_vhd_status(avctx,
VHD_GetVideoCharacteristics(
video_info->sdi.video_standard, width, height,
(BOOL32 *)interlaced, &frame_rate),
"", "");

handle_vhd_status(
avctx,
VHD_GetChannelProperty(board_handle, VHD_RX_CHANNEL, channel_index,
VHD_SDI_CP_CLOCK_DIVISOR,
(uint32_t *)&video_info->sdi.clock_divisor),
"", "");

handle_vhd_status(
avctx,
VHD_GetChannelProperty(board_handle, VHD_RX_CHANNEL, channel_index,
VHD_SDI_CP_INTERFACE,
(uint32_t *)&video_info->sdi.interface),
"", "");

handle_vhd_status(
avctx,
VHD_GetChannelProperty(board_handle, VHD_RX_CHANNEL, channel_index,
VHD_SDI_CP_GENLOCK_OFFSET,
&video_info->sdi.genlock_offset),
"", "");

*frame_rate_num = frame_rate * 1000;
switch (video_info->sdi.clock_divisor)
{
Expand All @@ -2255,6 +2342,29 @@ int ff_videomaster_get_video_stream_properties(
return 0;
}

bool ff_videomaster_is_3g_b_ds_interface_supported(
VideoMasterContext *videomaster_context)
{
bool interface_supported = false;
enum AVVideoMasterChannelType channel_type =
ff_videomaster_get_channel_type_from_index(
videomaster_context->avctx, videomaster_context->board_handle,
videomaster_context->channel_index);
if (videomaster_context->board_handle &&
channel_type == AV_VIDEOMASTER_CHANNEL_SDI)
VHD_GetBoardCapSDIInterface(videomaster_context->board_handle,
get_rx_stream_type_from_index(
videomaster_context->channel_index),
VHD_INTERFACE_3G_B_DS_425_1,
(BOOL32 *)&interface_supported);
else
{
av_log(videomaster_context->avctx, AV_LOG_ERROR,
"Board handle is missing or channel type is not SDI\n");
}
return interface_supported;
}

bool ff_videomaster_is_channel_locked(VideoMasterContext *videomaster_context)
{
bool channel_locked = false;
Expand Down Expand Up @@ -2428,7 +2538,7 @@ const char *ff_videomaster_sample_size_to_string(
int ff_videomaster_start_stream(VideoMasterContext *videomaster_context)
{
int av_error = 0;
int has_field_merge_capability = 0;
int has_field_merge_capability = 0;
const VideoMasterBufferPackingInfo *info = NULL;
av_log(videomaster_context->avctx, AV_LOG_TRACE,
"ff_videomaster_start_stream: IN\n");
Expand Down
24 changes: 20 additions & 4 deletions libavdevice/videomaster_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,9 @@ typedef struct VideoMasterContext
enum AVVideoMasterChannelType
channel_type; ///< type of the channel (HDMI or SDI)
enum AVVideoMasterTimeStampType
timestamp_source; ///< source of the timestamp
timestamp_source; ///< source of the timestamp
bool dual_stream; ///< true if the stream must be configured with 3GB-DS
///< interface

uint32_t api_version; ///< API version
uint32_t number_of_boards; ///< number of boards detected
Expand Down Expand Up @@ -318,6 +320,8 @@ typedef struct VideoMasterData
int64_t sample_rate; ///< sample rate of the audio stream
int64_t sample_size; ///< bits per sample in the audio stream
int64_t buffer_packing; ///< buffer packing format
bool dual_stream; ///< true if the stream must be configured with 3GB-DS
///< interface
} VideoMasterData;

/**
Expand Down Expand Up @@ -546,14 +550,26 @@ int ff_videomaster_get_timestamp(VideoMasterContext *videomaster_context,
* @param frame_rate_den Pointer to store the denominator of the frame rate of
* the video stream.
* @param interlaced Pointer to store whether the video stream is interlaced.
* @param dual_stream Indicates whether the stream must be configured with 3G B
* DS interface instead of auto-detect one.
*
* @return 0 on success, or a negative AVERROR code on failure.
*/
int ff_videomaster_get_video_stream_properties(
AVFormatContext *avctx, HANDLE board_handle, HANDLE stream_handle,
uint32_t channel_index, enum AVVideoMasterChannelType *channel_type,
union VideoMasterVideoInfo *video_info, uint32_t *width, uint32_t *height,
uint32_t *frame_rate_num, uint32_t *frame_rate_den, bool *interlaced);
uint32_t *frame_rate_num, uint32_t *frame_rate_den, bool *interlaced,
bool dual_stream);

/**
* @brief Checks if 3G-B DS interface is supported on the VideoMaster device.
*
* @param videomaster_context The VideoMaster context to use.
* @return true if 3G-B DS interface is supported, false otherwise.
*/
bool ff_videomaster_is_3g_b_ds_interface_supported(
VideoMasterContext *videomaster_context);

/**
* @brief Checks if the channel is locked on the VideoMaster device.
Expand Down Expand Up @@ -611,8 +627,8 @@ bool ff_videomaster_is_ltc_on_board_timestamp_supported(
* @brief Opens a handle to the VideoMaster board.
*
* This function initializes and opens a handle to the VideoMaster board
* specified by the board index board_index in the provided VideoMaster context.
* The handle is used for subsequent operations on the board.
* specified by the board index board_index in the provided VideoMaster
* context. The handle is used for subsequent operations on the board.
*
* @param videomaster_context The VideoMaster context to use.
* @return 0 on success, or negative AVERROR code on failure:
Expand Down
Loading
Loading