@@ -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
1621using namespace godot ;
1722
1823VideoStreamPlaybackNDI::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
2733VideoStreamPlaybackNDI::~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 {
99111void 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
112127void 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
158228void 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 }
0 commit comments