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
@@ -0,0 +1 @@
*.log
55 changes: 48 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,15 @@ dvledtx uses a JSON config file with three sections:
| **interfaces** | `name` | PCI BDF address of the NIC (e.g. `0000:06:00.0`) |
| | `sip` | Source IP address |
| | `dip` | Destination multicast IP address |
| **video** | `width` | Frame width in pixels |
| | `height` | Frame height in pixels |
| **video** | `width` | Source frame width in pixels |
| | `height` | Source frame height in pixels |
| | `tx_url` | Path to the source video file |
| **tx_video** | `scale_width` | (Optional) Output width after scaling |
| | `scale_height` | (Optional) Output height after scaling |
| | `fps` | Frames per second (25, 30, 50, 60) |
| | `fmt` | Pixel format (see [Supported Formats](#supported-formats)) |
| | `tx_url` | Path to the source video file |
| **tx_sessions[]** | `udp_port` | UDP port for the session |
| | `payload_type` | RTP payload type (typically 96) |
| | `payload_type` | (Optional) RTP payload type — defaults to `96` if not present |
| | `crop` | Region to transmit: `x`, `y`, `w`, `h` in pixels |

Example (`config/tx_fullhd_single_session.json`):
Expand All @@ -146,12 +148,14 @@ Example (`config/tx_fullhd_single_session.json`):
{ "name": "0000:06:00.0", "sip": "192.168.50.29", "dip": "239.168.85.20" }
],
"video": {
"width": 1920, "height": 1080, "fps": 30,
"fmt": "yuv422p10le",
"width": 1920, "height": 1080,
"tx_url": "bbb_sunflower_1080p_30fps_normal.mp4"
},
"tx_video": {
"fps": 30, "fmt": "yuv422p10le"
},
"tx_sessions": [
{ "udp_port": 20000, "payload_type": 96, "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } }
{ "udp_port": 20000, "crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 } }
]
}
```
Expand Down Expand Up @@ -236,6 +240,43 @@ When `log_file` is set, log output is written to that file in addition to the co

> **Note:** Maximum supported resolution is 3840x2160. Width must be even for YUV format alignment.

### Video Scaling

dvledtx supports upscaling and downscaling via optional `scale_width` and `scale_height` fields in the video configuration block. When set, the decoded source video is scaled to the specified dimensions before crop regions are applied.

| Feature | Description |
|---------|-------------|
| **Upscale** | Scale smaller source (e.g. 1080p) to larger output (e.g. 4K) |
| **Downscale** | Scale larger source (e.g. 4K) to smaller output (e.g. 1080p) |
| **Single session** | Full scaled frame transmitted as one stream |
| **Multi-session** | Crop regions applied to scaled frame for tiled LED walls |
| **Max output** | 3840x2160 (4K UHD) |

Example — upscale 1080p source to 4K output:
```json
{
"video": {
"width": 1920, "height": 1080,
"tx_url": "source_1080p.mp4"
},
"tx_video": {
"scale_width": 3840, "scale_height": 2160,
"fps": 30, "fmt": "yuv422p10le"
},
"tx_sessions": [
{ "udp_port": 20000, "crop": { "x": 0, "y": 0, "w": 3840, "h": 2160 } }
]
}
```

Log output when scaling is active:
```
[INFO ] Video: 1920x1080 -> scale 3840x2160 30fps yuv422p10le tx_url=source_1080p.mp4
[INFO ] Session 0: udp_port=20000 pt=96 crop=[0,0 3840x2160]
```

> **Note:** When `scale_width`/`scale_height` are set, crop bounds are validated against the scaled dimensions, not the source dimensions. Both fields must be specified together, and the scaled dimensions must satisfy the pixel format's chroma-alignment constraints (e.g. `yuv420` requires even width and height; `yuv422*` requires even width).

## Performance Considerations

### Optimization Features
Expand Down
13 changes: 7 additions & 6 deletions config/tx_2k_multi_session.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,33 @@
"log_file": "dvledtx.log",
"interfaces": [
{
"name": "0000:03:00.1",
"name": "0000:06:00.0",
"sip": "192.168.50.29",
"dip": "239.168.85.20"
}
],
"video": {
"width": 2560,
"height": 1440,
"tx_url": "bbb_sunflower_1080p_30fps_normal.mp4"
},
"tx_video": {
"scale_width": 2560,
Comment thread
sunilnom marked this conversation as resolved.
"scale_height": 1440,
"fps": 30,
"fmt": "yuv422p10le",
"tx_url": "ball_2k_yuv420p_30fps_5min.mp4"
"fmt": "yuv422p10le"
},
"tx_sessions": [
{
"udp_port": 20000,
"payload_type": 96,
"crop": { "x": 0, "y": 0, "w": 854, "h": 1440 }
},
{
"udp_port": 20002,
"payload_type": 96,
"crop": { "x": 854, "y": 0, "w": 854, "h": 1440 }
},
{
"udp_port": 20004,
"payload_type": 96,
"crop": { "x": 1708, "y": 0, "w": 852, "h": 1440 }
}
]
Expand Down
9 changes: 6 additions & 3 deletions config/tx_2k_single_session.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@
"video": {
"width": 2560,
"height": 1440,
"fps": 30,
"fmt": "yuv422p10le",
"tx_url": "ball_2k_yuv420p_30fps_5min.mp4"
},
"tx_video": {
"scale_width": 2560,
"scale_height": 1440,
"fps": 30,
"fmt": "yuv422p10le"
},
"tx_sessions": [
{
"udp_port": 20000,
"payload_type": 96,
"crop": { "x": 0, "y": 0, "w": 2560, "h": 1440 }
}
]
Expand Down
17 changes: 9 additions & 8 deletions config/tx_4k_multi_session.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,26 @@
"video": {
"width": 3840,
"height": 2160,
"tx_url": "ball_4k_gbrp10le_60fps_5min.mp4"
},
"tx_video": {
"scale_width": 1920,
"scale_height": 1080,
"fps": 30,
"fmt": "yuv422p10le",
"tx_url": "ball_4k_yuv420p_30fps_5min.mp4"
"fmt": "yuv422p10le"
},
"tx_sessions": [
{
"udp_port": 20000,
"payload_type": 96,
"crop": { "x": 0, "y": 0, "w": 1280, "h": 2160 }
"crop": { "x": 0, "y": 0, "w": 640, "h": 1080 }
},
{
"udp_port": 20002,
"payload_type": 96,
"crop": { "x": 1280, "y": 0, "w": 1280, "h": 2160 }
"crop": { "x": 640, "y": 0, "w": 640, "h": 1080 }
},
{
"udp_port": 20004,
"payload_type": 96,
"crop": { "x": 2560, "y": 0, "w": 1280, "h": 2160 }
"crop": { "x": 1280, "y": 0, "w": 640, "h": 1080 }
}
]
}
11 changes: 7 additions & 4 deletions config/tx_4k_single_session.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@
"video": {
"width": 3840,
"height": 2160,
"tx_url": "ball_4k_gbrp10le_60fps_5min.mp4"
},
"tx_video": {
"scale_width": 900,
"scale_height": 600,
"fps": 30,
"fmt": "yuv422p10le",
"tx_url": "ball_4k_yuv420p_30fps_5min.mp4"
"fmt": "yuv422p10le"
},
"tx_sessions": [
{
"udp_port": 20000,
"payload_type": 96,
"crop": { "x": 0, "y": 0, "w": 3840, "h": 2160 }
"crop": { "x": 0, "y": 0, "w": 900, "h": 600 }
}
]
}
11 changes: 6 additions & 5 deletions config/tx_fullhd_multi_session.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,25 @@
"video": {
"width": 1920,
"height": 1080,
"fps": 30,
"fmt": "yuv422p10le",
"tx_url": "bbb_sunflower_1080p_30fps_normal.mp4"
},
"tx_video": {
"scale_width": 1920,
"scale_height": 1080,
"fps": 30,
"fmt": "yuv422p10le"
},
"tx_sessions": [
{
"udp_port": 20000,
"payload_type": 96,
"crop": { "x": 0, "y": 0, "w": 640, "h": 1080 }
},
{
"udp_port": 20002,
"payload_type": 96,
"crop": { "x": 640, "y": 0, "w": 640, "h": 1080 }
},
{
"udp_port": 20004,
"payload_type": 96,
"crop": { "x": 1280, "y": 0, "w": 640, "h": 1080 }
}
]
Expand Down
15 changes: 9 additions & 6 deletions config/tx_fullhd_single_session.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,26 @@
"log_file": "dvledtx.log",
"interfaces": [
{
"name": "0000:03:00.1",
"name": "0000:06:00.0",
"sip": "192.168.50.29",
"dip": "239.168.85.20"
}
],
"video": {
"width": 3840,
"height": 2160,
"fps": 60,
"fmt": "gbrp10le",
"tx_url": "ball_4k_gbrp10le_30fps_5min.mp4"
"tx_url": "bbb_sunflower_1080p_30fps_normal.mp4"
},
"tx_video": {
"scale_width": 3840,
"scale_height": 2160,
"fps": 30,
"fmt": "yuv422p10le"
},
"tx_sessions": [
{
"udp_port": 20000,
"payload_type": 96,
"crop": { "x": 0, "y": 0, "w": 3840, "h": 2160 }
"crop": { "x": 0, "y": 0, "w": 1920, "h": 1080 }
}
]
}
2 changes: 2 additions & 0 deletions include/app_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ struct dvledtx_context {
/* Video parameters */
uint32_t width;
uint32_t height;
uint32_t scale_width; /* output width after scaling (0 = no scaling) */
uint32_t scale_height; /* output height after scaling (0 = no scaling) */
int fps; /* frames per second: 25, 30, 50, 60 */
enum AVPixelFormat fmt; /* e.g. AV_PIX_FMT_YUV422P10LE */

Expand Down
2 changes: 2 additions & 0 deletions include/util/config_reader.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ struct dvledtx_config {
/* video block */
uint32_t width;
uint32_t height;
uint32_t scale_width; /* 0 = no scaling (use source width) */
uint32_t scale_height; /* 0 = no scaling (use source height) */
int fps;
char fmt[32]; /* e.g. "yuv422p10le" */
char tx_url[256];
Expand Down
20 changes: 13 additions & 7 deletions src/core/session_manager.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ static void* st20p_tx_thread_shared(void* arg) {
struct shared_decode_ctx* dec = ctx->shared_dec;
int crop_x = ctx->crop_x_offset;
int crop_y = ctx->crop_y_offset;
int crop_w = ctx->crop_width > 0 ? ctx->crop_width : (int)ctx->app->width;
int crop_h = ctx->crop_height > 0 ? ctx->crop_height : (int)ctx->app->height;
int eff_w = (int)(ctx->app->scale_width > 0 ? ctx->app->scale_width : ctx->app->width);
int eff_h = (int)(ctx->app->scale_height > 0 ? ctx->app->scale_height : ctx->app->height);
int crop_w = ctx->crop_width > 0 ? ctx->crop_width : eff_w;
int crop_h = ctx->crop_height > 0 ? ctx->crop_height : eff_h;

LOG_INFO("ST20P TX(%d): shared thread started (crop x=%d y=%d w=%d h=%d)",
ctx->idx, crop_x, crop_y, crop_w, crop_h);
Expand Down Expand Up @@ -169,8 +171,10 @@ static void* st20p_tx_thread(void* arg) {
* Path B yuv_frame is already crop-sized (strip) → no offset needed. */
int crop_x = (ctx->use_ffmpeg == true) ? ctx->crop_x_offset : 0;
int crop_y = (ctx->use_ffmpeg == true) ? ctx->crop_y_offset : 0;
int crop_w = ctx->crop_width > 0 ? ctx->crop_width : (int)ctx->app->width;
int crop_h = ctx->crop_height > 0 ? ctx->crop_height : (int)ctx->app->height;
int eff_w = (int)(ctx->app->scale_width > 0 ? ctx->app->scale_width : ctx->app->width);
int eff_h = (int)(ctx->app->scale_height > 0 ? ctx->app->scale_height : ctx->app->height);
int crop_w = ctx->crop_width > 0 ? ctx->crop_width : eff_w;
int crop_h = ctx->crop_height > 0 ? ctx->crop_height : eff_h;

while (ctx->app->exit == false && session_manager_should_exit() == false) {
const AVFrame* frame = tx_fetch_next_frame(ctx);
Expand Down Expand Up @@ -217,13 +221,15 @@ int create_st20p_tx_session(session_manager_t* manager, struct dvledtx_context*
/* Fallback: divide the frame width evenly across all sessions */
if (ctx->crop_width == 0 || ctx->crop_height == 0) {
int total = app->st20p_sessions > 0 ? app->st20p_sessions : 1;
int strip_w = (int)app->width / total;
int eff_w = (int)(app->scale_width > 0 ? app->scale_width : app->width);
int eff_h = (int)(app->scale_height > 0 ? app->scale_height : app->height);
int strip_w = eff_w / total;
ctx->crop_x_offset = session_idx * strip_w;
ctx->crop_y_offset = 0;
ctx->crop_width = (session_idx == total - 1)
? ((int)app->width - ctx->crop_x_offset)
? (eff_w - ctx->crop_x_offset)
: strip_w;
ctx->crop_height = (int)app->height;
ctx->crop_height = eff_h;
}
LOG_INFO("ST20P TX session %d: crop rect x=%d y=%d w=%d h=%d",
session_idx, ctx->crop_x_offset, ctx->crop_y_offset,
Expand Down
8 changes: 6 additions & 2 deletions src/ffmpeg/ffmpeg_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,11 @@ static void close_ffmpeg_decoder(
* ========================================================================= */
int open_shared_ffmpeg(struct shared_decode_ctx* dec, const char* filename) {
const struct dvledtx_context* app = dec->app;
int target_w = (int)(app->scale_width > 0 ? app->scale_width : app->width);
int target_h = (int)(app->scale_height > 0 ? app->scale_height : app->height);
return open_ffmpeg_decoder(
filename, "Shared decode",
app->fmt, (int)app->width, (int)app->height,
app->fmt, target_w, target_h,
&dec->fmt_ctx, &dec->codec_ctx, &dec->sws_ctx,
&dec->av_frame, &dec->yuv_frame, &dec->av_packet,
&dec->video_stream_idx);
Expand All @@ -341,9 +343,11 @@ void close_shared_ffmpeg(struct shared_decode_ctx* dec) {
static int open_ffmpeg_source(struct st20p_tx_ctx* ctx, const char* filename) {
char log_prefix[64];
snprintf(log_prefix, sizeof(log_prefix), "ST20P TX(%d)", ctx->idx);
int target_w = (int)(ctx->app->scale_width > 0 ? ctx->app->scale_width : ctx->app->width);
int target_h = (int)(ctx->app->scale_height > 0 ? ctx->app->scale_height : ctx->app->height);
int ret = open_ffmpeg_decoder(
filename, log_prefix,
ctx->app->fmt, (int)ctx->app->width, (int)ctx->app->height,
ctx->app->fmt, target_w, target_h,
&ctx->fmt_ctx, &ctx->codec_ctx, &ctx->sws_ctx,
&ctx->av_frame, &ctx->yuv_frame, &ctx->av_packet,
&ctx->video_stream_idx);
Expand Down
Loading
Loading