@@ -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
1619using namespace godot ;
1720
1821VideoStreamPlaybackNDI::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
2731VideoStreamPlaybackNDI::~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 {
99109void 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
112125void 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
158226void 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 }
0 commit comments