Repository: godotengine/godot (language: cpp)

Issue:
SceneTreeTimer breaks when playing a video in loop
### Tested versions

v4.3 stable

### System information

Windows 10, Ryzen 3 3300x, 16gb RAM 2666mhz, GTX 1660 6gb.

### Issue description

If the video is looping, for whatever reason, the timer breaks and never returns the timeout signal. 

```
@onready var _video: VideoStreamPlayer = $video

var timer : SceneTreeTimer

func _ready() -> void:
	print('Starting')
	await _execute_and_stop()
	print('\n\nFinished!\n\n')
	
	
func _execute_and_stop() -> void:
	_video.play()
	timer = get_tree().create_timer(10, true)  			# Error.
	#timer = get_tree().create_timer(10, true, true)  	# ok.
	await timer.timeout
	_video.stop()
```

Playing a video with 6.9166667 sec (166 frames at 24fps), using a timer with 10 sec, that's the log I get when printing the 'time_left' in the SceneTreeTimer.

```
3.30371609090929
3.29765548484868
3.29159487878808
3.28553427272747
3.27947366666686
3.27341306060626
3.26735245454565
3.26129184848505
3.25523124242444
3.24917063636383
3.24311003030323
3.23704942424262
3.23098881818202
3.22492821212141
3.2188676060608
3.2128070000002
3.20674639393959
3.20068578787899
3.19462518181838
3.18856457575778
3.18250396969717
3.17644336363656
3.17038275757596
3.16432215151535
3.15826154545475
3.15220093939414
3.14614033333353
3.14007972727293
3.13401912121232
3.12795851515172
3.12189790909111
3.1158373030305
3.1097766969699
3.10371609090929
3.09765548484869
3.09159487878808
3.08553427272748
3.07947366666687
3.07341306060626
3.06735245454566
3.06129184848505
3.05523124242445
14745887137786335000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
14745887137786335000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
14745887137786335000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
14745887137786335000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
14745887137786335000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
```

In the MRP, there are 2 videos. For reasons I don't understand, the timer works ok when updating in the physics frame, but not in the process frame. Also, the first video1 works ok, but the video2 breaks the timer...

### Steps to reproduce

As shown in the script above.

### Minimal reproduction project (MRP)

[demo_timer.zip](https://github.com/user-attachments/files/17438564/demo_timer.zip)



Discussion / hints:
I can't reproduce the problem using the MRP. Also tried various combinations.
> I can't reproduce the problem using the MRP. Also tried various combinations.

that's odd. The timer breaks exactly when the video2 loops, at ~3.05 sec remaining. How can this be system-specific?
> that's odd. The timer breaks exactly when the video2 loops, at ~3.05 sec remaining. How can this be system-specific?

I could reproduce it using MRP
Seems that MinGW is doing some strange stuff with the code (again), i tested the mrp and in a build made with MSVC and the code works as expected, but compiled with MinGW has a strange behavior:

<br>

Godot v4.4.dev.custom_build [4631a617e], MSVC with production=yes:

https://github.com/user-attachments/assets/ad1d046d-be91-4078-84a1-07a4107d57a8

<br>

Godot v4.4.dev.custom_build [4631a617e], MinGW with production=yes lto=none

https://github.com/user-attachments/assets/789e4dfd-bf9e-49af-b817-97eab73a0f16

<br>

Godot v4.4.dev3.official [f4af8201b]

https://github.com/user-attachments/assets/accae297-a0ef-4026-857e-3858420e077a

<br>

Godot v4.4.dev3.official [f4af8201b] with `loop = false`

https://github.com/user-attachments/assets/be75b3d2-e977-43c0-9a5d-04d57c60c3f5


Maybe my compiled version with MinGW doesn't reproduce the bug in the same way like the official builds because the `lto=none` or maybe because the MinGW version (14.1.0 in my case) but using the official builds i reproduce the bug in the same way the issue shows. Also commenting the code that starts the video seems to make the timer work as expected in my self compiled build with MinGW.
I'm not able to see any of the videos you've sent. I suppose you encoded the videos with H.265 (HEVC)? I tried that once and had the same issue.


> I suppose you encoded the videos with H.265 (HEVC)?

Yes, strange that i can see normally in my pc and phone. 

But anyways, here what happens in my compiled build with MinGW:

```
Starting
10
9.96600733333333
9.94934066666667
9.94019966666667
9.94007104166223
9.93882166666667
9.93333333333333
9.92777777777778
9.92777777777778
9.91096377777778
9.89463577777778
9.89463577777778
9.87717377777778
9.87717377777778
9.85964777777778
9.84334977777777
9.84334977777777
9.82669577777777
0


Finished!
```
I took a look on the code related to `SceneTreeTimer` today and found something

https://github.com/godotengine/godot/blob/08f9cba0fbf27f171dea55de6f8274928b9f0d84/scene/main/scene_tree.cpp#L568-L602

<br>

Here `p_time` is used to process the timers and tweens, and for some reason between `_process(false)` and `_flush_ugc()` `p_time` can receive a strange number (which make `SceneTreeTimer.time_left` become that big number) or become a NaN (which makes `SceneTreeTimer.time_left` imediatly go to zero). Here a video that shows the bug hapenning, notice that `process_time` always keep normal ([Youtube link](https://youtu.be/jqhpkVzcjTM) for anyone that can't see the video here):


https://github.com/user-attachments/assets/8ac41c01-2fae-4a48-8ae3-75c56c64461c


<br>


Now if instead pass `p_time` for `process_timers` and `process_tweens` we pass `process_time` the bug simply dissapear, even `p_time` keep the normal value ([Youtube link](https://youtu.be/3N9J5XORovM)):



https://github.com/user-attachments/assets/2c090269-4c27-4c4b-9212-e8de1ce0d4b8


<br>

Another test i did was create a double variable in `SceneTree.h` and create a const double inside `SceneTree::process` to check if they also would break and that's what happens ([Youtube link](https://youtu.be/zbHftAO0IXs)):


https://github.com/user-attachments/assets/701e7df4-c6c7-416c-8e44-3781087f2c74


<br>

All the tests was made using Godot 08f9cba0f and compiling using MinGW with `production=yes` `lto=none` `debug_symbols=yes` (as said in my first commentary MSVC don't have this issue)

Use `process_time` as parameter to call `process_timers` and `process_tweens` works but seems like hiding another bug so i wanted some insight before do anything else. @timothyqiu any idea why this is happening?


Fix patch:
diff --git a/modules/theora/video_stream_theora.cpp b/modules/theora/video_stream_theora.cpp
index 8c7d639a6b99..02a1c7f8687a 100644
--- a/modules/theora/video_stream_theora.cpp
+++ b/modules/theora/video_stream_theora.cpp
@@ -39,31 +39,9 @@
 int VideoStreamPlaybackTheora::buffer_data() {
 	char *buffer = ogg_sync_buffer(&oy, 4096);
 
-#ifdef THEORA_USE_THREAD_STREAMING
-
-	int read;
-
-	do {
-		thread_sem->post();
-		read = MIN(ring_buffer.data_left(), 4096);
-		if (read) {
-			ring_buffer.read((uint8_t *)buffer, read);
-			ogg_sync_wrote(&oy, read);
-		} else {
-			OS::get_singleton()->delay_usec(100);
-		}
-
-	} while (read == 0);
-
-	return read;
-
-#else
-
 	uint64_t bytes = file->get_buffer((uint8_t *)buffer, 4096);
 	ogg_sync_wrote(&oy, bytes);
 	return (bytes);
-
-#endif
 }
 
 int VideoStreamPlaybackTheora::queue_page(ogg_page *page) {
@@ -82,34 +60,24 @@ int VideoStreamPlaybackTheora::queue_page(ogg_page *page) {
 	return 0;
 }
 
-void VideoStreamPlaybackTheora::video_write() {
-	th_ycbcr_buffer yuv;
-	th_decode_ycbcr_out(td, yuv);
-
-	int pitch = 4;
-	frame_data.resize(size.x * size.y * pitch);
-	{
-		uint8_t *w = frame_data.ptrw();
-		char *dst = (char *)w;
-
-		if (px_fmt == TH_PF_444) {
-			yuv444_2_rgb8888((uint8_t *)dst, (uint8_t *)yuv[0].data, (uint8_t *)yuv[1].data, (uint8_t *)yuv[2].data, size.x, size.y, yuv[0].stride, yuv[1].stride, size.x << 2);
-
-		} else if (px_fmt == TH_PF_422) {
-			yuv422_2_rgb8888((uint8_t *)dst, (uint8_t *)yuv[0].data, (uint8_t *)yuv[1].data, (uint8_t *)yuv[2].data, size.x, size.y, yuv[0].stride, yuv[1].stride, size.x << 2);
-
-		} else if (px_fmt == TH_PF_420) {
-			yuv420_2_rgb8888((uint8_t *)dst, (uint8_t *)yuv[0].data, (uint8_t *)yuv[1].data, (uint8_t *)yuv[2].data, size.x, size.y, yuv[0].stride, yuv[1].stride, size.x << 2);
-		}
-
-		format = Image::FORMAT_RGBA8;
+void VideoStreamPlaybackTheora::video_write(th_ycbcr_buffer yuv) {
+	uint8_t *w = frame_data.ptrw();
+	char *dst = (char *)w;
+	uint32_t y_offset = region.position.y * yuv[0].stride + region.position.x;
+	uint32_t uv_offset = region.position.y * yuv[1].stride + region.position.x;
+
+	if (px_fmt == TH_PF_444) {
+		yuv444_2_rgb8888((uint8_t *)dst, (uint8_t *)yuv[0].data + y_offset, (uint8_t *)yuv[1].data + uv_offset, (uint8_t *)yuv[2].data + uv_offset, region.size.x, region.size.y, yuv[0].stride, yuv[1].stride, region.size.x << 2);
+	} else if (px_fmt == TH_PF_422) {
+		yuv422_2_rgb8888((uint8_t *)dst, (uint8_t *)yuv[0].data + y_offset, (uint8_t *)yuv[1].data + uv_offset, (uint8_t *)yuv[2].data + uv_offset, region.size.x, region.size.y, yuv[0].stride, yuv[1].stride, region.size.x << 2);
+	} else if (px_fmt == TH_PF_420) {
+		yuv420_2_rgb8888((uint8_t *)dst, (uint8_t *)yuv[0].data + y_offset, (uint8_t *)yuv[1].data + uv_offset, (uint8_t *)yuv[2].data + uv_offset, region.size.x, region.size.y, yuv[0].stride, yuv[1].stride, region.size.x << 2);
 	}
 
-	Ref<Image> img = memnew(Image(size.x, size.y, false, Image::FORMAT_RGBA8, frame_data)); //zero copy image creation
+	Ref<Image> img;
+	img.instantiate(region.size.x, region.size.y, false, Image::FORMAT_RGBA8, frame_data); //zero copy image creation
 
 	texture->update(img); //zero copy send to rendering server
-
-	frames_pending = 1;
 }
 
 void VideoStreamPlaybackTheora::clear() {
@@ -136,20 +104,15 @@ void VideoStreamPlaybackTheora::clear() {
 	}
 	ogg_sync_clear(&oy);
 
-#ifdef THEORA_USE_THREAD_STREAMING
-	thread_exit = true;
-	thread_sem->post(); //just in case
-	thread.wait_to_finish();
-	ring_buffer.clear();
-#endif
-
 	theora_p = 0;
 	vorbis_p = 0;
-	videobuf_ready = 0;
-	frames_pending = 0;
-	videobuf_time = 0;
+	next_frame_time = 0;
+	current_frame_time = 0;
 	theora_eos = false;
 	vorbis_eos = false;
+	video_ready = false;
+	video_done = false;
+	audio_done = false;
 
 	file.unref();
 	playing = false;
@@ -164,17 +127,6 @@ void VideoStreamPlaybackTheora::set_file(const String &p_file) {
 	file = FileAccess::open(p_file, FileAccess::READ);
 	ERR_FAIL_COND_MSG(file.is_null(), "Cannot open file '" + p_file + "'.");
 
-#ifdef THEORA_USE_THREAD_STREAMING
-	thread_exit = false;
-	thread_eof = false;
-	//pre-fill buffer
-	int to_read = ring_buffer.space_left();
-	uint64_t read = file->get_buffer(read_buffer.ptr(), to_read);
-	ring_buffer.write(read_buffer.ptr(), read);
-
-	thread.start(_streaming_thread, this);
-#endif
-
 	ogg_sync_init(&oy);
 
 	/* init supporting Vorbis structures needed in header parsing */
@@ -327,16 +279,18 @@ void VideoStreamPlaybackTheora::set_file(const String &p_file) {
 		th_decode_ctl(td, TH_DECCTL_SET_PPLEVEL, &pp_level, sizeof(pp_level));
 		pp_inc = 0;
 
-		int w;
-		int h;
-		w = ((ti.pic_x + ti.frame_width + 1) & ~1) - (ti.pic_x & ~1);
-		h = ((ti.pic_y + ti.frame_height + 1) & ~1) - (ti.pic_y & ~1);
-		size.x = w;
-		size.y = h;
+		size.x = ti.frame_width;
+		size.y = ti.frame_height;
+		region.position.x = ti.pic_x;
+		region.position.y = ti.pic_y;
+		region.size.x = ti.pic_width;
+		region.size.y = ti.pic_height;
 
-		Ref<Image> img = Image::create_empty(w, h, false, Image::FORMAT_RGBA8);
+		Ref<Image> img = Image::create_empty(region.size.x, region.size.y, false, Image::FORMAT_RGBA8);
 		texture->set_image(img);
+		frame_data.resize(region.size.x * region.size.y * 4);
 
+		frame_duration = (double)ti.fps_denominator / ti.fps_numerator;
 	} else {
 		/* tear down the partial theora setup */
 		th_info_clear(&ti);
@@ -358,7 +312,8 @@ void VideoStreamPlaybackTheora::set_file(const String &p_file) {
 	playing = false;
 	buffering = true;
 	time = 0;
-	audio_frames_wrote = 0;
+	video_done = !theora_p;
+	audio_done = !vorbis_p;
 }
 
 double VideoStreamPlaybackTheora::get_time() const {
@@ -378,68 +333,41 @@ void VideoStreamPlaybackTheora::update(double p_delta) {
 	}
 
 	if (!playing || paused) {
-		//printf("not playing\n");
 		return;
 	}
 
-#ifdef THEORA_USE_THREAD_STREAMING
-	thread_sem->post();
-#endif
-
 	time += p_delta;
 
-	if (videobuf_time > get_time()) {
-		return; //no new frames need to be produced
-	}
-
-	bool frame_done = false;
-	bool audio_done = !vorbis_p;
-
-	while (!frame_done || (!audio_done && !vorbis_eos)) {
-		//a frame needs to be produced
+	double comp_time = get_time();
+	bool audio_ready = false;
 
+	// Read data until we fill the audio buffer and get a new video frame.
+	while ((!audio_ready && !audio_done) || (!video_ready && !video_done)) {
 		ogg_packet op;
-		bool no_theora = false;
-		bool buffer_full = false;
 
-		while (vorbis_p && !audio_done && !buffer_full) {
-			int ret;
+		while (!audio_ready && !audio_done) {
 			float **pcm;
-
-			/* if there's pending, decoded audio, grab it */
-			ret = vorbis_synthesis_pcmout(&vd, &pcm);
+			int ret = vorbis_synthesis_pcmout(&vd, &pcm);
 			if (ret > 0) {
 				const int AUXBUF_LEN = 4096;
 				int to_read = ret;
 				float aux_buffer[AUXBUF_LEN];
-
 				while (to_read) {
 					int m = MIN(AUXBUF_LEN / vi.channels, to_read);
-
 					int count = 0;
-
 					for (int j = 0; j < m; j++) {
 						for (int i = 0; i < vi.channels; i++) {
 							aux_buffer[count++] = pcm[i][j];
 						}
 					}
-
-					if (mix_callback) {
-						int mixed = mix_callback(mix_udata, aux_buffer, m);
-						to_read -= mixed;
-						if (mixed != m) { //could mix no more
-							buffer_full = true;
-							break;
-						}
-					} else {
-						to_read -= m; //just pretend we sent the audio
+					int mixed = mix_callback(mix_udata, aux_buffer, m);
+					to_read -= mixed;
+					if (mixed != m) { //could mix no more
+						audio_ready = true;
+						break;
 					}
 				}
-
 				vorbis_synthesis_read(&vd, ret - to_read);
-
-				audio_frames_wrote += ret - to_read;
-
 			} else {
 				/* no pending audio; is there a pending packet to decode? */
 				if (ogg_stream_packetout(&vo, &op) > 0) {
@@ -447,19 +375,13 @@ void VideoStreamPlaybackTheora::update(double p_delta) {
 						vorbis_synthesis_blockin(&vd, &vb);
 					}
 				} else { /* we need more data; break out to suck in another page */
+					audio_done = vorbis_eos;
 					break;
 				}
 			}
-
-			audio_done = videobuf_time < (audio_frames_wrote / float(vi.rate));
-
-			if (buffer_full) {
-				break;
-			}
 		}
 
-		while (theora_p && !frame_done) {
-			/* theora is one in, one out... */
+		while (!video_ready && !video_done) {
 			if (ogg_stream_packetout(&to, &op) > 0) {
 				/*HACK: This should be set after a seek or a gap, but we might not have
 				a granulepos for the first packet (we only have them for the last
@@ -472,62 +394,37 @@ void VideoStreamPlaybackTheora::update(double p_delta) {
 							sizeof(op.granulepos));
 				}
 				ogg_int64_t videobuf_granulepos;
-				if (th_decode_packetin(td, &op, &videobuf_granulepos) == 0) {
-					videobuf_time = th_granule_time(td, videobuf_granulepos);
-
-					//printf("frame time %f, play time %f, ready %i\n", (float)videobuf_time, get_time(), videobuf_ready);
-
-					/* is it already too old to be useful? This is only actually
-					 useful cosmetically after a SIGSTOP. Note that we have to
-					 decode the frame even if we don't show it (for now) due to
-					 keyframing. Soon enough libtheora will be able to deal
-					 with non-keyframe seeks.  */
-
-					if (videobuf_time >= get_time()) {
-						frame_done = true;
+				int ret = th_decode_packetin(td, &op, &videobuf_granulepos);
+				if (ret == 0 || ret == TH_DUPFRAME) {
+					next_frame_time = th_granule_time(td, videobuf_granulepos);
+					if (next_frame_time > comp_time) {
+						dup_frame = (ret == TH_DUPFRAME);
+						video_ready = true;
 					} else {
 						/*If we are too slow, reduce the pp level.*/
 						pp_inc = pp_level > 0 ? -1 : 0;
 					}
 				}
-
-			} else {
-				no_theora = true;
+			} else { /* we need more data; break out to suck in another page */
+				video_done = theora_eos;
 				break;
 			}
 		}
 
-#ifdef THEORA_USE_THREAD_STREAMING
-		if (file.is_valid() && thread_eof && no_theora && theora_eos && ring_buffer.data_left() == 0) {
-#else
-		if (file.is_valid() && /*!videobuf_ready && */ no_theora && theora_eos) {
-#endif
-			//printf("video done, stopping\n");
-			stop();
-			return;
-		}
-
-		if (!frame_done || !audio_done) {
-			//what's the point of waiting for audio to grab a page?
-
-			buffer_data();
-			while (ogg_sync_pageout(&oy, &og) > 0) {
-				queue_page(&og);
+		if (!video_ready || !audio_ready) {
+			int ret = buffer_data();
+			if (ret > 0) {
+				while (ogg_sync_pageout(&oy, &og) > 0) {
+					queue_page(&og);
+				}
+			} else {
+				vorbis_eos = true;
+				theora_eos = true;
+				break;
 			}
 		}
 
-		/* If playback has begun, top audio buffer off immediately. */
-		//if(stateflag) audio_write_nonblocking();
-
-		/* are we at or past time for this video frame? */
-		if (videobuf_ready && videobuf_time <= get_time()) {
-			//video_write();
-			//videobuf_ready=0;
-		} else {
-			//printf("frame at %f not ready (time %f), ready %i\n", (float)videobuf_time, get_time(), videobuf_ready);
-		}
-
-		double tdiff = videobuf_time - get_time();
+		double tdiff = next_frame_time - comp_time;
 		/*If we have lots of extra time, increase the post-processing level.*/
 		if (tdiff > ti.fps_denominator * 0.25 / ti.fps_numerator) {
 			pp_inc = pp_level < pp_level_max ? 1 : 0;
@@ -536,7 +433,22 @@ void VideoStreamPlaybackTheora::update(double p_delta) {
 		}
 	}
 
-	video_write();
+	if (!video_ready && video_done && audio_done) {
+		stop();
+		return;
+	}
+
+	// Wait for the last frame to end before rendering the next one.
+	if (video_ready && comp_time >= current_frame_time) {
+		if (!dup_frame) {
+			th_ycbcr_buffer yuv;
+			th_decode_ycbcr_out(td, yuv);
+			video_write(yuv);
+		}
+		dup_frame = false;
+		video_ready = false;
+		current_frame_time = next_frame_time;
+	}
 }
 
 void VideoStreamPlaybackTheora::play() {
@@ -596,44 +508,11 @@ int VideoStreamPlaybackTheora::get_mix_rate() const {
 	return vi.rate;
 }
 
-#ifdef THEORA_USE_THREAD_STREAMING
-
-void VideoStreamPlaybackTheora::_streaming_thread(void *ud) {
-	VideoStreamPlaybackTheora *vs = static_cast<VideoStreamPlaybackTheora *>(ud);
-
-	while (!vs->thread_exit) {
-		//just fill back the buffer
-		if (!vs->thread_eof) {
-			int to_read = vs->ring_buffer.space_left();
-			if (to_read > 0) {
-				uint64_t read = vs->file->get_buffer(vs->read_buffer.ptr(), to_read);
-				vs->ring_buffer.write(vs->read_buffer.ptr(), read);
-				vs->thread_eof = vs->file->eof_reached();
-			}
-		}
-
-		vs->thread_sem->wait();
-	}
-}
-
-#endif
-
 VideoStreamPlaybackTheora::VideoStreamPlaybackTheora() {
 	texture.instantiate();
-
-#ifdef THEORA_USE_THREAD_STREAMING
-	int rb_power = nearest_shift(RB_SIZE_KB * 1024);
-	ring_buffer.resize(rb_power);
-	read_buffer.resize(RB_SIZE_KB * 1024);
-	thread_sem = Semaphore::create();
-
-#endif
 }
 
 VideoStreamPlaybackTheora::~VideoStreamPlaybackTheora() {
-#ifdef THEORA_USE_THREAD_STREAMING
-	memdelete(thread_sem);
-#endif
 	clear();
 }
 
diff --git a/modules/theora/video_stream_theora.h b/modules/theora/video_stream_theora.h
index 5e6630f1e2f6..45b6746d40b1 100644
--- a/modules/theora/video_stream_theora.h
+++ b/modules/theora/video_stream_theora.h
@@ -41,27 +41,20 @@
 
 class ImageTexture;
 
-//#define THEORA_USE_THREAD_STREAMING
-
 class VideoStreamPlaybackTheora : public VideoStreamPlayback {
 	GDCLASS(VideoStreamPlaybackTheora, VideoStreamPlayback);
 
-	enum {
-		MAX_FRAMES = 4,
-	};
-
-	//Image frames[MAX_FRAMES];
 	Image::Format format = Image::Format::FORMAT_L8;
 	Vector<uint8_t> frame_data;
 	int frames_pending = 0;
 	Ref<FileAccess> file;
 	String file_name;
-	int audio_frames_wrote = 0;
 	Point2i size;
+	Rect2i region;
 
 	int buffer_data();
 	int queue_page(ogg_page *page);
-	void video_write();
+	void video_write(th_ycbcr_buffer yuv);
 	double get_time() const;
 
 	bool theora_eos = false;
@@ -79,43 +72,30 @@ class VideoStreamPlaybackTheora : public VideoStreamPlayback {
 	vorbis_block vb;
 	vorbis_comment vc;
 	th_pixel_fmt px_fmt;
-	double videobuf_time = 0;
-	int pp_inc = 0;
+	double frame_duration;
 
 	int theora_p = 0;
 	int vorbis_p = 0;
 	int pp_level_max = 0;
 	int pp_level = 0;
-	int videobuf_ready = 0;
+	int pp_inc = 0;
 
 	bool playing = false;
 	bool buffering = false;
+	bool paused = false;
+
+	bool dup_frame = false;
+	bool video_ready = false;
+	bool video_done = false;
+	bool audio_done = false;
 
-	double last_update_time = 0;
 	double time = 0;
+	double next_frame_time = 0;
+	double current_frame_time = 0;
 	double delay_compensation = 0;
 
 	Ref<ImageTexture> texture;
 
-	bool paused = false;
-
-#ifdef THEORA_USE_THREAD_STREAMING
-
-	enum {
-		RB_SIZE_KB = 1024
-	};
-
-	RingBuffer<uint8_t> ring_buffer;
-	Vector<uint8_t> read_buffer;
-	bool thread_eof = false;
-	Semaphore *thread_sem = nullptr;
-	Thread thread;
-	SafeFlag thread_exit;
-
-	static void _streaming_thread(void *ud);
-
-#endif
-
 	int audio_track = 0;
 
 protected:
diff --git a/scene/gui/video_stream_player.cpp b/scene/gui/video_stream_player.cpp
index 1a4bace9a50f..fa965179a4d4 100644
--- a/scene/gui/video_stream_player.cpp
+++ b/scene/gui/video_stream_player.cpp
@@ -158,6 +158,7 @@ void VideoStreamPlayer::_notification(int p_notification) {
 			playback->update(delta); // playback->is_playing() returns false in the last video frame
 
 			if (!playback->is_playing()) {
+				resampler.flush();
 				if (loop) {
 					play();
 					return;
diff --git a/servers/audio/audio_rb_resampler.h b/servers/audio/audio_rb_resampler.h
index 18ed1bb738e0..cecae07d8beb 100644
--- a/servers/audio/audio_rb_resampler.h
+++ b/servers/audio/audio_rb_resampler.h
@@ -86,7 +86,7 @@ struct AudioRBResampler {
 		} else if (w < r) {
 			space = r - w - 1;
 		} else {
-			space = (rb_len - r) + w - 1;
+			space = (rb_len - w) + (r - 1);
 		}
 
 		return space;


Test patch: