Skip to content

Commit 5f862d5

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

File tree

8 files changed

+109
-47
lines changed

8 files changed

+109
-47
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: 77 additions & 30 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,77 @@ void VideoStreamPlaybackNDI::_update(double p_delta) {
111124

112125
void VideoStreamPlaybackNDI::_bind_methods() {}
113126

114-
void VideoStreamPlaybackNDI::render_first_frame() {
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+
PackedByteArray data;
131+
data.resize(p_video_frame.line_stride_in_bytes * p_video_frame.yres);
132+
memcpy(data.ptrw(), p_video_frame.p_data, data.size());
133+
134+
RID texture_rd_rid = texture->get_texture_rd_rid();
135+
136+
if (new_texture_size == texture_size && texture_rd_rid.is_valid()) {
137+
rd->texture_update(texture_rd_rid, 0, data);
138+
} else {
139+
if (texture_rd_rid.is_valid()) {
140+
rd->free_rid(texture_rd_rid);
141+
}
142+
143+
Ref<RDTextureFormat> tf;
144+
tf.instantiate();
145+
tf->set_format(RenderingDevice::DATA_FORMAT_B8G8R8A8_UNORM);
146+
tf->set_width(new_texture_size.x);
147+
tf->set_height(new_texture_size.y);
148+
tf->set_usage_bits(RenderingDevice::TEXTURE_USAGE_SAMPLING_BIT | RenderingDevice::TEXTURE_USAGE_CAN_UPDATE_BIT);
149+
150+
// tf->set_depth(1);
151+
// tf->set_array_layers(1);
152+
// tf->set_mipmaps(1);
153+
// tf->set_samples(RenderingDevice::TEXTURE_SAMPLES_1);
154+
// tf->set_texture_type(RenderingDevice::TEXTURE_TYPE_2D);
155+
156+
Ref<RDTextureView> tv;
157+
tv.instantiate();
158+
159+
Array texture_data = Array();
160+
texture_data.append(data);
161+
162+
texture_rd_rid = rd->texture_create(tf, tv, texture_data);
163+
tf.unref();
164+
tv.unref();
165+
166+
texture->set_texture_rd_rid(texture_rd_rid);
167+
168+
texture_size = new_texture_size;
169+
}
170+
}
171+
172+
void VideoStreamPlaybackNDI::wait_for_non_empty_frame() {
115173
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);
174+
if (render_video()) {
120175
return;
121176
}
122-
ndi->framesync_free_video(sync, &video_frame);
123177
OS::get_singleton()->delay_msec(10);
124178
}
125179

126180
// 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.");
181+
ERR_FAIL_MSG("NDI: Source not found at playback start. It will play at fallback resolution of 1920x1080 once discovered. See docs for NDIOutput.");
129182
}
130183

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);
137-
}
138-
}
184+
bool VideoStreamPlaybackNDI::render_video() {
185+
bool updated = false;
139186

187+
NDIlib_video_frame_v2_t video_frame;
140188
ndi->framesync_capture_video(sync, &video_frame, NDIlib_frame_format_type_progressive);
141189

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);
144-
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-
}
151-
152-
img = Image::create_from_data(video_frame.xres, video_frame.yres, false, Image::Format::FORMAT_RGBA8, video_buffer);
190+
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)) {
191+
update_texture(video_frame);
192+
updated = true;
153193
}
154194

155195
ndi->framesync_free_video(sync, &video_frame);
196+
197+
return updated;
156198
}
157199

158200
void VideoStreamPlaybackNDI::render_audio(double p_delta) {
@@ -173,9 +215,14 @@ void VideoStreamPlaybackNDI::render_audio(double p_delta) {
173215
audio_buffer_interleaved.set(i, audio_buffer_planar[stride_index + stride_offset]);
174216
}
175217

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

222+
if (skipped_samples > 0) {
223+
print_verbose("NDI: Skipped " + String::num_int64(skipped_samples) + " audio samples!");
224+
}
225+
179226
// Skip the older samples by playing the last ones in the array
180227
mix_audio(processed_samples, audio_buffer_interleaved, skipped_samples * _get_channels());
181228
}

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)