Skip to content

Commit c3341e6

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

File tree

8 files changed

+138
-48
lines changed

8 files changed

+138
-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: 106 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@ 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>
18+
19+
#include <cstdlib>
1520

1621
using namespace godot;
1722

1823
VideoStreamPlaybackNDI::VideoStreamPlaybackNDI() {
24+
rd = RenderingServer::get_singleton()->get_rendering_device();
1925
texture.instantiate();
2026
}
2127

@@ -26,6 +32,12 @@ VideoStreamPlaybackNDI::VideoStreamPlaybackNDI(NDIlib_recv_create_v3_t p_recv_de
2632

2733
VideoStreamPlaybackNDI::~VideoStreamPlaybackNDI() {
2834
_stop();
35+
36+
RID texture_rd_rid = texture->get_texture_rd_rid();
37+
if (texture_rd_rid.is_valid()) {
38+
rd->free_rid(texture_rd_rid);
39+
}
40+
2941
texture.unref();
3042
}
3143

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

114+
// NDI framesync delivers frames instantly but first ones are 0x0
115+
// Since the VideoStreamPlayer used to only adjust its size for the first frame, this workaround was needed before 4.5
116+
// PR that fixed it: https://github.com/godotengine/godot/pull/103912
117+
102118
if (p_delta == 0 && (int)Engine::get_singleton()->get_version_info().get("hex", 0) < 0x040402) {
103119
// 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();
120+
wait_for_non_empty_frame();
106121
} else {
107122
render_audio(p_delta);
108123
render_video();
@@ -111,48 +126,103 @@ void VideoStreamPlaybackNDI::_update(double p_delta) {
111126

112127
void VideoStreamPlaybackNDI::_bind_methods() {}
113128

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;
129+
void VideoStreamPlaybackNDI::update_texture(NDIlib_video_frame_v2_t p_video_frame) {
130+
Vector2i new_texture_size = Vector2i(p_video_frame.xres, p_video_frame.yres);
131+
132+
// Copy the data into a PackedByteArray.
133+
PackedByteArray data;
134+
data.resize(p_video_frame.line_stride_in_bytes * p_video_frame.yres);
135+
memcpy(data.ptrw(), p_video_frame.p_data, data.size());
136+
137+
RID texture_rd_rid = texture->get_texture_rd_rid();
138+
139+
// If texture of correct size already exists, try to update it.
140+
if (new_texture_size == texture_size && texture_rd_rid.is_valid()) {
141+
if (rd->texture_update(texture_rd_rid, 0, data) != OK) {
142+
ERR_PRINT_ONCE_ED("NDI: Failed to update the video texture (only printed once)");
121143
}
122-
ndi->framesync_free_video(sync, &video_frame);
123-
OS::get_singleton()->delay_msec(10);
144+
return;
145+
}
146+
147+
// No existing texture of correct size. Create it.
148+
149+
if (texture_rd_rid.is_valid()) {
150+
// Free texture of old size
151+
rd->free_rid(texture_rd_rid);
124152
}
125153

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.");
154+
Ref<RDTextureFormat> tf;
155+
tf.instantiate();
156+
tf->set_format(RenderingDevice::DATA_FORMAT_B8G8R8A8_UNORM);
157+
tf->set_width(new_texture_size.x);
158+
tf->set_height(new_texture_size.y);
159+
tf->set_usage_bits(RenderingDevice::TEXTURE_USAGE_SAMPLING_BIT | RenderingDevice::TEXTURE_USAGE_CAN_UPDATE_BIT);
160+
161+
// // These don't seem needed, probably defaults.
162+
// tf->set_depth(1);
163+
// tf->set_array_layers(1);
164+
// tf->set_mipmaps(1);
165+
// tf->set_samples(RenderingDevice::TEXTURE_SAMPLES_1);
166+
// tf->set_texture_type(RenderingDevice::TEXTURE_TYPE_2D);
167+
168+
Ref<RDTextureView> tv;
169+
tv.instantiate();
170+
171+
// RD::texture_create expects the data in a 2D array of layers, even if there is just one.
172+
Array texture_layers = Array();
173+
texture_layers.append(data);
174+
175+
texture_rd_rid = rd->texture_create(tf, tv, texture_layers);
176+
177+
// Clean up
178+
texture_layers.clear();
179+
tv.unref();
180+
tf.unref();
181+
182+
// Point Texture2DRD to the new texture.
183+
texture->set_texture_rd_rid(texture_rd_rid);
184+
texture_size = new_texture_size;
129185
}
130186

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);
187+
void VideoStreamPlaybackNDI::wait_for_non_empty_frame() {
188+
for (size_t i = 0; i < 1000; i++) {
189+
if (render_video()) {
190+
return;
137191
}
192+
OS::get_singleton()->delay_msec(10);
138193
}
139194

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

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);
198+
// Hijack NDIlib_video_frame_v2_t struct to create placeholder frame so that update_texture can do the rest.
199+
// This struct is populated just enough to work for update_texture. Don't let any NDI functions see this.
144200

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-
}
201+
NDIlib_video_frame_v2_t placeholder_frame;
202+
placeholder_frame.xres = 1920;
203+
placeholder_frame.yres = 1080;
204+
placeholder_frame.line_stride_in_bytes = placeholder_frame.xres * 4;
205+
placeholder_frame.p_data = (uint8_t *)calloc(placeholder_frame.line_stride_in_bytes * placeholder_frame.yres, 1);
206+
207+
update_texture(placeholder_frame); // We can reuse this. YIPPIE
208+
209+
std::free(placeholder_frame.p_data); // The Godot namspace overrides free, so gotta be explicit here.
210+
}
211+
212+
bool VideoStreamPlaybackNDI::render_video() {
213+
bool updated = false;
214+
215+
NDIlib_video_frame_v2_t video_frame;
216+
ndi->framesync_capture_video(sync, &video_frame, NDIlib_frame_format_type_progressive);
151217

152-
img = Image::create_from_data(video_frame.xres, video_frame.yres, false, Image::Format::FORMAT_RGBA8, video_buffer);
218+
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)) {
219+
update_texture(video_frame);
220+
updated = true;
153221
}
154222

155223
ndi->framesync_free_video(sync, &video_frame);
224+
225+
return updated;
156226
}
157227

158228
void VideoStreamPlaybackNDI::render_audio(double p_delta) {
@@ -173,9 +243,14 @@ void VideoStreamPlaybackNDI::render_audio(double p_delta) {
173243
audio_buffer_interleaved.set(i, audio_buffer_planar[stride_index + stride_offset]);
174244
}
175245

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

250+
if (skipped_samples > 0) {
251+
print_verbose("NDI: Skipped " + String::num_int64(skipped_samples) + " audio samples!");
252+
}
253+
179254
// Skip the older samples by playing the last ones in the array
180255
mix_audio(processed_samples, audio_buffer_interleaved, skipped_samples * _get_channels());
181256
}

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)