Skip to content

Commit 0b1e698

Browse files
committed
fix(VideoStreamNDI): Use Texture2DRD to support BGRA
1 parent 4c4b5f4 commit 0b1e698

File tree

8 files changed

+136
-48
lines changed

8 files changed

+136
-48
lines changed

build_profile.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"OS",
4545
"ProjectSettings",
4646
"RDTextureFormat",
47+
"RDTextureView",
4748
"RenderingDevice",
4849
"RenderingServer",
4950
"Resource",
@@ -54,6 +55,7 @@
5455
"Time",
5556
"Timer",
5657
"Texture",
58+
"Texture2DRD",
5759
"Viewport",
5860
"ViewportTexture",
5961
"VideoStream",

project/addons/godot-ndi/demo/3DOutput.tscn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ font_size = 24
1414
shadow_size = 5
1515
shadow_color = Color(0, 0, 0, 0.235294)
1616

17-
[node name="3DOutput" type="SubViewportContainer"]
17+
[node name="SubViewportContainer" type="SubViewportContainer"]
1818
anchors_preset = 15
1919
anchor_right = 1.0
2020
anchor_bottom = 1.0

project/addons/godot-ndi/demo/AnimationOutput.tscn

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ loop_mode = 1
88
tracks/0/type = "value"
99
tracks/0/imported = false
1010
tracks/0/enabled = true
11-
tracks/0/path = NodePath("Icon:position")
11+
tracks/0/path = NodePath("SubViewport/Icon:position")
1212
tracks/0/interp = 1
1313
tracks/0/loop_wrap = true
1414
tracks/0/keys = {
@@ -25,7 +25,7 @@ loop_mode = 1
2525
tracks/0/type = "value"
2626
tracks/0/imported = false
2727
tracks/0/enabled = true
28-
tracks/0/path = NodePath("Icon:position")
28+
tracks/0/path = NodePath("SubViewport/Icon:position")
2929
tracks/0/interp = 1
3030
tracks/0/loop_wrap = true
3131
tracks/0/keys = {
@@ -41,17 +41,29 @@ _data = {
4141
&"new_animation": SubResource("Animation_0fbet")
4242
}
4343

44-
[node name="SubViewport" type="Node2D"]
44+
[node name="SubViewportContainer" type="SubViewportContainer"]
45+
anchors_preset = 15
46+
anchor_right = 1.0
47+
anchor_bottom = 1.0
48+
grow_horizontal = 2
49+
grow_vertical = 2
50+
stretch = true
4551

46-
[node name="NDIOutput" type="NDIOutput" parent="."]
52+
[node name="SubViewport" type="SubViewport" parent="."]
53+
transparent_bg = true
54+
size = Vector2i(1152, 648)
55+
render_target_update_mode = 4
56+
57+
[node name="NDIOutput" type="NDIOutput" parent="SubViewport"]
4758
name = "Godot (Animation)"
4859

49-
[node name="Icon" type="Sprite2D" parent="."]
60+
[node name="Icon" type="Sprite2D" parent="SubViewport"]
5061
position = Vector2(1034, 527)
5162
scale = Vector2(1.463, 1.463)
5263
texture = ExtResource("1_ekn6m")
5364

54-
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
65+
[node name="AnimationPlayer" type="AnimationPlayer" parent="SubViewport"]
66+
root_node = NodePath("../..")
5567
libraries = {
5668
&"": SubResource("AnimationLibrary_ee4bf")
5769
}

project/default_bus_layout.tres

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,5 @@
44
resource_name = "Capture"
55

66
[resource]
7-
bus/0/mute = true
87
bus/0/effect/0/effect = SubResource("AudioEffectCapture_j3pel")
98
bus/0/effect/0/enabled = true

project/project.godot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ config_version=5
1212

1313
config/name="godot-ndi"
1414
run/main_scene="uid://ddr3hiaft61tl"
15-
config/features=PackedStringArray("4.4", "Forward Plus")
15+
config/features=PackedStringArray("4.5", "Forward Plus")
1616
run/max_fps=60
1717

1818
[editor_plugins]

src/ndi.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ Error load_runtime() {
106106
break;
107107
}
108108

109-
ERR_FAIL_NULL_V_MSG(ndi, ERR_FILE_CANT_OPEN, "NDI: Failed to load NDI Runtime. Make sure its installed on your system. Paths tried: \n" + String("\n").join(runtime_paths));
109+
ERR_FAIL_NULL_V_MSG(ndi, ERR_FILE_CANT_OPEN, "NDI: Failed to load NDI 6.2 Runtime. Make sure its installed on your system. Paths tried: \n" + String("\n").join(runtime_paths));
110110
ERR_FAIL_COND_V_MSG(!ndi->initialize(), ERR_UNAVAILABLE, "NDI: NDI isn't supported on your device");
111111

112112
print_verbose(ndi->version());

src/video_stream_playback_ndi.cpp

Lines changed: 104 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
1212
#include <godot_cpp/classes/engine.hpp>
1313
#include <godot_cpp/classes/os.hpp>
1414
#include <godot_cpp/classes/project_settings.hpp>
15+
#include <godot_cpp/classes/rd_texture_format.hpp>
16+
#include <godot_cpp/classes/rd_texture_view.hpp>
17+
#include <godot_cpp/classes/rendering_server.hpp>
1518

1619
using namespace godot;
1720

1821
VideoStreamPlaybackNDI::VideoStreamPlaybackNDI() {
22+
rd = RenderingServer::get_singleton()->get_rendering_device();
1923
texture.instantiate();
2024
}
2125

@@ -26,6 +30,12 @@ VideoStreamPlaybackNDI::VideoStreamPlaybackNDI(NDIlib_recv_create_v3_t p_recv_de
2630

2731
VideoStreamPlaybackNDI::~VideoStreamPlaybackNDI() {
2832
_stop();
33+
34+
RID texture_rd_rid = texture->get_texture_rd_rid();
35+
if (texture_rd_rid.is_valid()) {
36+
rd->free_rid(texture_rd_rid);
37+
}
38+
2939
texture.unref();
3040
}
3141

@@ -99,10 +109,13 @@ Ref<Texture2D> VideoStreamPlaybackNDI::_get_texture() const {
99109
void VideoStreamPlaybackNDI::_update(double p_delta) {
100110
ERR_FAIL_COND_MSG(recv == nullptr || sync == nullptr, "VideoStreamPlaybackNDI wasn't setup properly");
101111

112+
// NDI framesync delivers frames instantly but first ones are 0x0
113+
// Since the VideoStreamPlayer used to only adjust its size for the first frame, this workaround was needed before 4.5
114+
// PR that fixed it: https://github.com/godotengine/godot/pull/103912
115+
102116
if (p_delta == 0 && (int)Engine::get_singleton()->get_version_info().get("hex", 0) < 0x040402) {
103117
// First frame is ticked with delta of 0
104-
// Workaround for https://github.com/godotengine/godot/pull/103912, required for Godot versions before 4.4.2
105-
render_first_frame();
118+
wait_for_non_empty_frame();
106119
} else {
107120
render_audio(p_delta);
108121
render_video();
@@ -111,48 +124,103 @@ void VideoStreamPlaybackNDI::_update(double p_delta) {
111124

112125
void VideoStreamPlaybackNDI::_bind_methods() {}
113126

114-
void VideoStreamPlaybackNDI::render_first_frame() {
115-
for (size_t i = 0; i < 1000; i++) {
116-
ndi->framesync_capture_video(sync, &video_frame, NDIlib_frame_format_type_progressive);
117-
if (video_frame.xres != 0 && video_frame.yres != 0) {
118-
texture->set_image(Image::create_empty(video_frame.xres, video_frame.yres, false, Image::FORMAT_RGBA8));
119-
ndi->framesync_free_video(sync, &video_frame);
120-
return;
127+
void VideoStreamPlaybackNDI::update_texture(NDIlib_video_frame_v2_t p_video_frame) {
128+
Vector2i new_texture_size = Vector2i(p_video_frame.xres, p_video_frame.yres);
129+
130+
// Copy the data into a PackedByteArray.
131+
PackedByteArray data;
132+
data.resize(p_video_frame.line_stride_in_bytes * p_video_frame.yres);
133+
memcpy(data.ptrw(), p_video_frame.p_data, data.size());
134+
135+
RID texture_rd_rid = texture->get_texture_rd_rid();
136+
137+
// If texture of correct size already exists, try to update it.
138+
if (new_texture_size == texture_size && texture_rd_rid.is_valid()) {
139+
if (rd->texture_update(texture_rd_rid, 0, data) != OK) {
140+
ERR_PRINT_ONCE_ED("NDI: Failed to update the video texture (only printed once)");
121141
}
122-
ndi->framesync_free_video(sync, &video_frame);
123-
OS::get_singleton()->delay_msec(10);
142+
return;
124143
}
125144

126-
// Fallback resolution
127-
texture->set_image(Image::create_empty(1920, 1080, false, Image::FORMAT_RGBA8));
128-
ERR_FAIL_MSG("NDI: Source not found at playback start. It will play at fallback resolution of 1920x1080 once discovered. See docs.");
145+
// No existing texture of correct size. Create it.
146+
147+
if (texture_rd_rid.is_valid()) {
148+
// Free texture of old size
149+
rd->free_rid(texture_rd_rid);
150+
}
151+
152+
Ref<RDTextureFormat> tf;
153+
tf.instantiate();
154+
tf->set_format(RenderingDevice::DATA_FORMAT_B8G8R8A8_UNORM);
155+
tf->set_width(new_texture_size.x);
156+
tf->set_height(new_texture_size.y);
157+
tf->set_usage_bits(RenderingDevice::TEXTURE_USAGE_SAMPLING_BIT | RenderingDevice::TEXTURE_USAGE_CAN_UPDATE_BIT);
158+
159+
// // These don't seem needed, probably defaults.
160+
// tf->set_depth(1);
161+
// tf->set_array_layers(1);
162+
// tf->set_mipmaps(1);
163+
// tf->set_samples(RenderingDevice::TEXTURE_SAMPLES_1);
164+
// tf->set_texture_type(RenderingDevice::TEXTURE_TYPE_2D);
165+
166+
Ref<RDTextureView> tv;
167+
tv.instantiate();
168+
169+
// RD::texture_create expects the data in a 2D array of layers, even if there is just one.
170+
Array texture_layers = Array();
171+
texture_layers.append(data);
172+
173+
texture_rd_rid = rd->texture_create(tf, tv, texture_layers);
174+
175+
// Clean up
176+
texture_layers.clear();
177+
tv.unref();
178+
tf.unref();
179+
180+
// Point Texture2DRD to the new texture.
181+
texture->set_texture_rd_rid(texture_rd_rid);
182+
texture_size = new_texture_size;
129183
}
130184

131-
void VideoStreamPlaybackNDI::render_video() {
132-
if (img.is_valid()) {
133-
if (texture->get_width() == img->get_width() && texture->get_height() == img->get_height()) {
134-
texture->update(img);
135-
} else {
136-
texture->set_image(img);
185+
void VideoStreamPlaybackNDI::wait_for_non_empty_frame() {
186+
for (size_t i = 0; i < 1000; i++) {
187+
if (render_video()) {
188+
return;
137189
}
190+
OS::get_singleton()->delay_msec(10);
138191
}
139192

140-
ndi->framesync_capture_video(sync, &video_frame, NDIlib_frame_format_type_progressive);
193+
// Failed to find source after 10s, create placeholder at most common resolution.
194+
ERR_FAIL_MSG("NDI: Source not found at playback start. It will play at fallback resolution of 1920x1080 once discovered. See docs for NDIOutput.");
141195

142-
if (video_frame.p_data != nullptr && (video_frame.FourCC == NDIlib_FourCC_type_BGRA || video_frame.FourCC == NDIlib_FourCC_type_BGRX)) {
143-
video_buffer.resize(video_frame.xres * video_frame.yres * 4);
196+
// Hijack NDIlib_video_frame_v2_t struct to create placeholder frame so that update_texture can do the rest.
197+
// This struct is populated just enough to work for update_texture. Don't let any NDI functions see this.
144198

145-
for (size_t i = 0; i < video_frame.xres * video_frame.yres; i++) {
146-
video_buffer.set(i * 4 + 0, video_frame.p_data[i * 4 + 2]);
147-
video_buffer.set(i * 4 + 1, video_frame.p_data[i * 4 + 1]);
148-
video_buffer.set(i * 4 + 2, video_frame.p_data[i * 4 + 0]);
149-
video_buffer.set(i * 4 + 3, video_frame.p_data[i * 4 + 3]);
150-
}
199+
NDIlib_video_frame_v2_t placeholder_frame;
200+
placeholder_frame.xres = 1920;
201+
placeholder_frame.yres = 1080;
202+
placeholder_frame.line_stride_in_bytes = placeholder_frame.xres * 4;
203+
placeholder_frame.p_data = (uint8_t *)calloc(placeholder_frame.line_stride_in_bytes * placeholder_frame.yres, 1);
204+
205+
update_texture(placeholder_frame); // We can reuse this. YIPPIE
206+
207+
std::free(placeholder_frame.p_data); // The Godot namspace overrides free, so gotta be explicit here.
208+
}
151209

152-
img = Image::create_from_data(video_frame.xres, video_frame.yres, false, Image::Format::FORMAT_RGBA8, video_buffer);
210+
bool VideoStreamPlaybackNDI::render_video() {
211+
bool updated = false;
212+
213+
NDIlib_video_frame_v2_t video_frame;
214+
ndi->framesync_capture_video(sync, &video_frame, NDIlib_frame_format_type_progressive);
215+
216+
if (video_frame.p_data != nullptr && video_frame.xres != 0 && video_frame.yres != 0 && (video_frame.FourCC == NDIlib_FourCC_type_BGRA || video_frame.FourCC == NDIlib_FourCC_type_BGRX)) {
217+
update_texture(video_frame);
218+
updated = true;
153219
}
154220

155221
ndi->framesync_free_video(sync, &video_frame);
222+
223+
return updated;
156224
}
157225

158226
void VideoStreamPlaybackNDI::render_audio(double p_delta) {
@@ -173,9 +241,14 @@ void VideoStreamPlaybackNDI::render_audio(double p_delta) {
173241
audio_buffer_interleaved.set(i, audio_buffer_planar[stride_index + stride_offset]);
174242
}
175243

176-
int processed_samples = Math::min(audio_frame.no_samples, 8192); // FIXME: dont hardcode this
244+
// There is a maximum to how many samples can be submitted at once, because otherwise the buffer will overflow
245+
int processed_samples = Math::min(audio_frame.no_samples, 4096); // FIXME: dont hardcode this (https://github.com/unvermuthet/godot-ndi/issues/4)
177246
int skipped_samples = audio_frame.no_samples - processed_samples;
178247

248+
if (skipped_samples > 0) {
249+
print_verbose("NDI: Skipped " + String::num_int64(skipped_samples) + " audio samples!");
250+
}
251+
179252
// Skip the older samples by playing the last ones in the array
180253
mix_audio(processed_samples, audio_buffer_interleaved, skipped_samples * _get_channels());
181254
}

src/video_stream_playback_ndi.hpp

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
1111

1212
#include "ndi.hpp"
1313

14-
#include <godot_cpp/classes/image_texture.hpp>
14+
#include <godot_cpp/classes/rendering_device.hpp>
15+
#include <godot_cpp/classes/texture2drd.hpp>
1516
#include <godot_cpp/classes/video_stream_playback.hpp>
1617

1718
using namespace godot;
@@ -48,12 +49,13 @@ class VideoStreamPlaybackNDI : public VideoStreamPlayback {
4849
NDIlib_recv_instance_t recv = nullptr;
4950
NDIlib_framesync_instance_t sync = nullptr;
5051

51-
Ref<ImageTexture> texture;
52-
Ref<Image> img;
53-
NDIlib_video_frame_v2_t video_frame;
54-
PackedByteArray video_buffer;
55-
void render_first_frame();
56-
void render_video();
52+
RenderingDevice *rd;
53+
Ref<Texture2DRD> texture;
54+
Vector2i texture_size = Vector2i(0, 0);
55+
void update_texture(NDIlib_video_frame_v2_t p_video_frame);
56+
57+
void wait_for_non_empty_frame();
58+
bool render_video();
5759

5860
NDIlib_audio_frame_v3_t audio_frame;
5961
PackedFloat32Array audio_buffer_planar;

0 commit comments

Comments
 (0)