[PATCH v3 0/4] MR10864: winegstreamer: Rebase PTS values so the stream starts at zero.
mpegpsdemux uses raw PTS values from the stream, which often start at a non-zero timestamp. To avoid an initial delay, we rebase the PTS by subtracting the first raw value so that the buffer timeline begins at zero for the MPEG-I splitter in DirectShow. Without this adjustment, the first sample would not be presented until its original (non-zero) timestamp in MPEG-1 system stream. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59565 -- v3: winegstreamer: Rebase PTS values so the stream starts at zero. winegstreamer: Change wg_parser_create argument from bool to bit flags. quartz/tests: Add a timestamp test for the first sample. winegstreamer: Make sure every stream has a buffer or an initial gap on initialization. https://gitlab.winehq.org/wine/wine/-/merge_requests/10864
From: Akihiro Sagawa <sagawa.aki@gmail.com> --- dlls/winegstreamer/wg_parser.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/dlls/winegstreamer/wg_parser.c b/dlls/winegstreamer/wg_parser.c index da414af6134..f68946b243a 100644 --- a/dlls/winegstreamer/wg_parser.c +++ b/dlls/winegstreamer/wg_parser.c @@ -114,7 +114,7 @@ struct wg_parser_stream GstBuffer *buffer; GstMapInfo map_info; - bool flushing, eos, enabled, has_tags, has_buffer, no_more_pads, get_buffer_called; + bool flushing, eos, enabled, has_tags, has_buffer, has_initial_gap, no_more_pads, get_buffer_called; uint64_t duration; gchar *tags[WG_PARSER_TAG_COUNT]; @@ -699,6 +699,15 @@ static gboolean sink_event_cb(GstPad *pad, GstObject *parent, GstEvent *event) break; } + case GST_EVENT_GAP: + if (stream->has_buffer || stream->has_initial_gap) + break; + pthread_mutex_lock(&parser->mutex); + stream->has_initial_gap = true; + pthread_mutex_unlock(&parser->mutex); + pthread_cond_signal(&parser->init_cond); + break; + case GST_EVENT_TAG: pthread_mutex_lock(&parser->mutex); stream->has_tags = true; @@ -1681,8 +1690,8 @@ static NTSTATUS wg_parser_connect(void *args) struct wg_parser_stream *stream = parser->streams[i]; gint64 duration; - /* If we received a buffer, waiting for tags or caps does not make sense anymore. */ - while ((!stream->current_caps || !stream->has_tags) && !parser->error && !stream->has_buffer) + /* Make sure the stream has a buffer or an initial gap. */ + while (!parser->error && !stream->has_buffer && !stream->has_initial_gap) pthread_cond_wait(&parser->init_cond, &parser->mutex); /* GStreamer doesn't actually provide any guarantees about when duration -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10864
From: Akihiro Sagawa <sagawa.aki@gmail.com> --- dlls/quartz/tests/mpegsplit.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dlls/quartz/tests/mpegsplit.c b/dlls/quartz/tests/mpegsplit.c index bebb7d39fcc..32d33b1fbef 100644 --- a/dlls/quartz/tests/mpegsplit.c +++ b/dlls/quartz/tests/mpegsplit.c @@ -1084,6 +1084,7 @@ struct testfilter HANDLE eos_event; unsigned int sample_count, eos_count, new_segment_count, byte_count; REFERENCE_TIME segment_start, segment_end_min, segment_end_max, seek_start, seek_end; + REFERENCE_TIME sample_start; LONGLONG read_position; }; @@ -1212,6 +1213,9 @@ static HRESULT WINAPI testsink_Receive(struct strmbase_sink *iface, IMediaSample if (winetest_debug > 1) trace("%04lx: Got sample with timestamps %I64d-%I64d.\n", GetCurrentThreadId(), start, end); + if (filter->sample_count == 0 && filter->byte_count == 0) + filter->sample_start = start; + ok(filter->new_segment_count, "Expected NewSegment() before Receive().\n"); IPin_QueryInterface(iface->pin.peer, &IID_IMediaSeeking, (void **)&seeking); @@ -2102,6 +2106,11 @@ static void test_video_file(void) ok(testsink_video.byte_count == 1214, "Video sink got %u bytes.\n", testsink_video.byte_count); ok(testsink_audio.byte_count == 8777, "Audio sink got %u bytes.\n", testsink_audio.byte_count); + /* Native uses 55 for the first sample timestamp, but there's no need to copy the exact value. */ + todo_wine ok(min(testsink_video.sample_start, testsink_audio.sample_start) < 100, + "The first sample timestamp is off by more 100, video %I64d and audio %I64d.\n", + testsink_video.sample_start, testsink_audio.sample_start); + IAMStreamSelect_Release(sel); IPin_Release(source_video); IPin_Release(source_audio); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10864
From: Akihiro Sagawa <sagawa.aki@gmail.com> --- dlls/winegstreamer/gst_private.h | 2 +- dlls/winegstreamer/main.c | 6 +++--- dlls/winegstreamer/media_source.c | 2 +- dlls/winegstreamer/quartz_parser.c | 14 +++++++------- dlls/winegstreamer/unixlib.h | 8 +++++++- dlls/winegstreamer/wg_parser.c | 2 +- dlls/winegstreamer/wm_reader.c | 4 ++-- 7 files changed, 22 insertions(+), 16 deletions(-) diff --git a/dlls/winegstreamer/gst_private.h b/dlls/winegstreamer/gst_private.h index 7a5c2f3f4b0..71ef17985ce 100644 --- a/dlls/winegstreamer/gst_private.h +++ b/dlls/winegstreamer/gst_private.h @@ -70,7 +70,7 @@ HRESULT wg_sample_queue_create(struct wg_sample_queue **out); void wg_sample_queue_destroy(struct wg_sample_queue *queue); void wg_sample_queue_flush(struct wg_sample_queue *queue, bool all); -wg_parser_t wg_parser_create(bool output_compressed); +wg_parser_t wg_parser_create(UINT32 flags); /* see wg_parser_create_flag */ void wg_parser_destroy(wg_parser_t parser); HRESULT wg_parser_connect(wg_parser_t parser, uint64_t file_size, const WCHAR *uri); diff --git a/dlls/winegstreamer/main.c b/dlls/winegstreamer/main.c index 6a8bd4b6413..5cafcc1491f 100644 --- a/dlls/winegstreamer/main.c +++ b/dlls/winegstreamer/main.c @@ -160,16 +160,16 @@ static HRESULT wg_media_type_to_mf(const struct wg_media_type *wg_media_type, IM return E_NOTIMPL; } -wg_parser_t wg_parser_create(bool output_compressed) +wg_parser_t wg_parser_create(UINT32 flags) { struct wg_parser_create_params params = { - .output_compressed = output_compressed, + .flags = flags, .err_on = ERR_ON(quartz), .warn_on = WARN_ON(quartz), }; - TRACE("output_compressed %d.\n", output_compressed); + TRACE("flags %#x.\n", flags); if (WINE_UNIX_CALL(unix_wg_parser_create, ¶ms)) return 0; diff --git a/dlls/winegstreamer/media_source.c b/dlls/winegstreamer/media_source.c index 0735e74bdf3..4e33d92a6bc 100644 --- a/dlls/winegstreamer/media_source.c +++ b/dlls/winegstreamer/media_source.c @@ -1670,7 +1670,7 @@ static HRESULT media_source_create(struct object_context *context, IMFMediaSourc if (FAILED(hr = MFAllocateWorkQueue(&object->async_commands_queue))) goto fail; - if (!(parser = wg_parser_create(FALSE))) + if (!(parser = wg_parser_create(WG_PARSER_CREATE_FLAG_NONE))) { hr = E_OUTOFMEMORY; goto fail; diff --git a/dlls/winegstreamer/quartz_parser.c b/dlls/winegstreamer/quartz_parser.c index 38971f2fcb9..9a26c863aa9 100644 --- a/dlls/winegstreamer/quartz_parser.c +++ b/dlls/winegstreamer/quartz_parser.c @@ -1878,16 +1878,16 @@ static HRESULT decodebin_parser_source_get_media_type(struct parser_source *pin, return VFW_S_NO_MORE_ITEMS; } -static HRESULT parser_create(BOOL output_compressed, struct parser **parser) +static HRESULT parser_create(UINT32 flags, struct parser **parser) { struct parser *object; if (!(object = calloc(1, sizeof(*object)))) return E_OUTOFMEMORY; - object->output_compressed = output_compressed; + object->output_compressed = flags & WG_PARSER_CREATE_FLAG_OUTPUT_COMPRESSED; - if (!(object->wg_parser = wg_parser_create(output_compressed))) + if (!(object->wg_parser = wg_parser_create(flags))) { free(object); return E_OUTOFMEMORY; @@ -1907,7 +1907,7 @@ HRESULT decodebin_parser_create(IUnknown *outer, IUnknown **out) struct parser *object; HRESULT hr; - if (FAILED(hr = parser_create(FALSE, &object))) + if (FAILED(hr = parser_create(WG_PARSER_CREATE_FLAG_NONE, &object))) return hr; strmbase_filter_init(&object->filter, outer, &CLSID_decodebin_parser, &filter_ops); @@ -2481,7 +2481,7 @@ HRESULT wave_parser_create(IUnknown *outer, IUnknown **out) struct parser *object; HRESULT hr; - if (FAILED(hr = parser_create(TRUE, &object))) + if (FAILED(hr = parser_create(WG_PARSER_CREATE_FLAG_OUTPUT_COMPRESSED, &object))) return hr; strmbase_filter_init(&object->filter, outer, &CLSID_WAVEParser, &filter_ops); @@ -2590,7 +2590,7 @@ HRESULT avi_splitter_create(IUnknown *outer, IUnknown **out) struct parser *object; HRESULT hr; - if (FAILED(hr = parser_create(TRUE, &object))) + if (FAILED(hr = parser_create(WG_PARSER_CREATE_FLAG_OUTPUT_COMPRESSED, &object))) return hr; strmbase_filter_init(&object->filter, outer, &CLSID_AviSplitter, &filter_ops); @@ -2752,7 +2752,7 @@ HRESULT mpeg_splitter_create(IUnknown *outer, IUnknown **out) struct parser *object; HRESULT hr; - if (FAILED(hr = parser_create(TRUE, &object))) + if (FAILED(hr = parser_create(WG_PARSER_CREATE_FLAG_OUTPUT_COMPRESSED, &object))) return hr; strmbase_filter_init(&object->filter, outer, &CLSID_MPEG1Splitter, &mpeg_splitter_ops); diff --git a/dlls/winegstreamer/unixlib.h b/dlls/winegstreamer/unixlib.h index 24cef388bb5..c4b6043684b 100644 --- a/dlls/winegstreamer/unixlib.h +++ b/dlls/winegstreamer/unixlib.h @@ -218,10 +218,16 @@ struct wg_init_gstreamer_params UINT8 err_on; }; +enum wg_parser_create_flag +{ + WG_PARSER_CREATE_FLAG_NONE = 0, + WG_PARSER_CREATE_FLAG_OUTPUT_COMPRESSED = 1, +}; + struct wg_parser_create_params { wg_parser_t parser; - UINT8 output_compressed; + UINT32 flags; UINT8 err_on; UINT8 warn_on; }; diff --git a/dlls/winegstreamer/wg_parser.c b/dlls/winegstreamer/wg_parser.c index f68946b243a..d63a329a9e1 100644 --- a/dlls/winegstreamer/wg_parser.c +++ b/dlls/winegstreamer/wg_parser.c @@ -1889,7 +1889,7 @@ static NTSTATUS wg_parser_create(void *args) pthread_cond_init(&parser->init_cond, NULL); pthread_cond_init(&parser->read_cond, NULL); pthread_cond_init(&parser->read_done_cond, NULL); - parser->output_compressed = params->output_compressed; + parser->output_compressed = params->flags & WG_PARSER_CREATE_FLAG_OUTPUT_COMPRESSED; parser->err_on = params->err_on; parser->warn_on = params->warn_on; GST_DEBUG("Created winegstreamer parser %p.", parser); diff --git a/dlls/winegstreamer/wm_reader.c b/dlls/winegstreamer/wm_reader.c index 6a800e4448c..4c072aa4265 100644 --- a/dlls/winegstreamer/wm_reader.c +++ b/dlls/winegstreamer/wm_reader.c @@ -1494,7 +1494,7 @@ static HRESULT init_stream(struct wm_reader *reader) HRESULT hr; WORD i; - if (!(wg_parser = wg_parser_create(FALSE))) + if (!(wg_parser = wg_parser_create(WG_PARSER_CREATE_FLAG_NONE))) return E_OUTOFMEMORY; reader->wg_parser = wg_parser; @@ -1621,7 +1621,7 @@ static HRESULT reinit_stream(struct wm_reader *reader, bool read_compressed) wg_parser_destroy(reader->wg_parser); reader->wg_parser = 0; - if (!(wg_parser = wg_parser_create(read_compressed))) + if (!(wg_parser = wg_parser_create(read_compressed ? WG_PARSER_CREATE_FLAG_OUTPUT_COMPRESSED : WG_PARSER_CREATE_FLAG_NONE))) return E_OUTOFMEMORY; reader->wg_parser = wg_parser; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10864
From: Akihiro Sagawa <sagawa.aki@gmail.com> mpegpsdemux uses raw PTS values from the stream, which often start at a non-zero timestamp. To avoid an initial delay, we rebase the PTS by subtracting the first raw value so that the buffer timeline begins at zero for the MPEG-I Splitter in DirectShow. Without this adjustment, the first sample would not be presented until its original (non-zero) timestamp in MPEG-1 system stream. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59565 --- dlls/quartz/tests/mpegsplit.c | 2 +- dlls/winegstreamer/quartz_parser.c | 3 ++- dlls/winegstreamer/unixlib.h | 1 + dlls/winegstreamer/wg_parser.c | 27 +++++++++++++++++++++++++-- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/dlls/quartz/tests/mpegsplit.c b/dlls/quartz/tests/mpegsplit.c index 32d33b1fbef..06e32828e86 100644 --- a/dlls/quartz/tests/mpegsplit.c +++ b/dlls/quartz/tests/mpegsplit.c @@ -2107,7 +2107,7 @@ static void test_video_file(void) ok(testsink_audio.byte_count == 8777, "Audio sink got %u bytes.\n", testsink_audio.byte_count); /* Native uses 55 for the first sample timestamp, but there's no need to copy the exact value. */ - todo_wine ok(min(testsink_video.sample_start, testsink_audio.sample_start) < 100, + ok(min(testsink_video.sample_start, testsink_audio.sample_start) < 100, "The first sample timestamp is off by more 100, video %I64d and audio %I64d.\n", testsink_video.sample_start, testsink_audio.sample_start); diff --git a/dlls/winegstreamer/quartz_parser.c b/dlls/winegstreamer/quartz_parser.c index 9a26c863aa9..a1910202767 100644 --- a/dlls/winegstreamer/quartz_parser.c +++ b/dlls/winegstreamer/quartz_parser.c @@ -2752,7 +2752,8 @@ HRESULT mpeg_splitter_create(IUnknown *outer, IUnknown **out) struct parser *object; HRESULT hr; - if (FAILED(hr = parser_create(WG_PARSER_CREATE_FLAG_OUTPUT_COMPRESSED, &object))) + if (FAILED(hr = parser_create(WG_PARSER_CREATE_FLAG_OUTPUT_COMPRESSED | + WG_PARSER_CREATE_FLAG_PTS_REBASED, &object))) return hr; strmbase_filter_init(&object->filter, outer, &CLSID_MPEG1Splitter, &mpeg_splitter_ops); diff --git a/dlls/winegstreamer/unixlib.h b/dlls/winegstreamer/unixlib.h index c4b6043684b..d274d6f7457 100644 --- a/dlls/winegstreamer/unixlib.h +++ b/dlls/winegstreamer/unixlib.h @@ -222,6 +222,7 @@ enum wg_parser_create_flag { WG_PARSER_CREATE_FLAG_NONE = 0, WG_PARSER_CREATE_FLAG_OUTPUT_COMPRESSED = 1, + WG_PARSER_CREATE_FLAG_PTS_REBASED = 2, }; struct wg_parser_create_params diff --git a/dlls/winegstreamer/wg_parser.c b/dlls/winegstreamer/wg_parser.c index d63a329a9e1..c4b96d95d43 100644 --- a/dlls/winegstreamer/wg_parser.c +++ b/dlls/winegstreamer/wg_parser.c @@ -68,6 +68,7 @@ struct wg_parser GstPad *my_src; guint64 file_size, start_offset, next_offset, stop_offset; + GstClockTime base_pts; guint64 next_pull_offset; gchar *uri; @@ -76,7 +77,7 @@ struct wg_parser pthread_mutex_t mutex; pthread_cond_t init_cond; - bool output_compressed; + bool output_compressed, pts_rebased; bool no_more_pads, has_duration, error; bool err_on, warn_on; @@ -366,8 +367,17 @@ static NTSTATUS wg_parser_stream_get_buffer(void *args) * that this will need modification to wg_parser_stream_notify_qos() as * well. */ + /* Because mpegpsdemux reports a non-zero PTS for the earliest buffer among + * all streams, we rebase the PTS by subtracting base_pts so that our + * stream starts at zero for the MPEG-I Splitter in quartz. */ + if ((wg_buffer->has_pts = GST_BUFFER_PTS_IS_VALID(buffer))) - wg_buffer->pts = GST_BUFFER_PTS(buffer) / 100; + { + if (parser->pts_rebased && GST_CLOCK_TIME_IS_VALID(parser->base_pts)) + wg_buffer->pts = (GST_BUFFER_PTS(buffer) - parser->base_pts) / 100; + else + wg_buffer->pts = GST_BUFFER_PTS(buffer) / 100; + } if ((wg_buffer->has_duration = GST_BUFFER_DURATION_IS_VALID(buffer))) wg_buffer->duration = GST_BUFFER_DURATION(buffer) / 100; wg_buffer->discontinuity = GST_BUFFER_FLAG_IS_SET(buffer, GST_BUFFER_FLAG_DISCONT); @@ -734,6 +744,17 @@ static GstFlowReturn sink_chain_cb(GstPad *pad, GstObject *parent, GstBuffer *bu if (!stream->has_buffer) { stream->has_buffer = true; + + /* Keep the earliest PTS for adjusting. */ + if (!stream->has_initial_gap && + GST_BUFFER_PTS_IS_VALID(buffer) && + (GST_BUFFER_PTS(buffer) < parser->base_pts)) + { + parser->base_pts = GST_BUFFER_PTS(buffer); + GST_LOG("Updated base PTS to %" GST_TIME_FORMAT ".", + GST_TIME_ARGS(parser->base_pts)); + } + pthread_cond_signal(&parser->init_cond); } @@ -1890,8 +1911,10 @@ static NTSTATUS wg_parser_create(void *args) pthread_cond_init(&parser->read_cond, NULL); pthread_cond_init(&parser->read_done_cond, NULL); parser->output_compressed = params->flags & WG_PARSER_CREATE_FLAG_OUTPUT_COMPRESSED; + parser->pts_rebased = params->flags & WG_PARSER_CREATE_FLAG_PTS_REBASED; parser->err_on = params->err_on; parser->warn_on = params->warn_on; + parser->base_pts = GST_CLOCK_TIME_NONE; GST_DEBUG("Created winegstreamer parser %p.", parser); params->parser = (wg_parser_t)(ULONG_PTR)parser; return S_OK; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10864
Updated. In this version, - Make sure every stream has a buffer (or an initial gap) on initialization by altering the existing condition. - Introduce WG_PARSER_CREATE_FLAG_NONE instead of 0. To be honest, I don't have Atelier Ryza 3. So, I could not reproduce @otakuxtom's issue. According to [its web page](https://atelier.games/secretdx/us/), Atelier Ryza 3 seems to use the VP9 codec for its movies. As far as I can tell, the first patch affects all media types including MP4 and WebM. I manually tested both MP4 and WebM videos with Media Foundation and the both passed. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10864#note_149013
On Mon Aug 17 07:35:54 2026 +0000, Akihiro Sagawa wrote:
Updated. In this version, - Make sure every stream has a buffer (or an initial gap) on initialization by altering the existing condition. - Introduce `WG_PARSER_CREATE_FLAG_NONE` instead of `0`. To be honest, I don't have Atelier Ryza 3, so I couldn't reproduce @otakuxtom's issue. According to [its web page](https://atelier.games/secretdx/us/), Atelier Ryza 3 seems to use the VP9 codec for its movies. As far as I can tell, the first patch affects all media types including MP4 and WebM. I manually tested both MP4 and WebM videos with Media Foundation and the both passed. After test this version, It actually return to normal. so I think its fixed.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10864#note_149040
Ok this patch reduces that nasty long stutter that happens when videos loop in games that had shown that issue. But the problem is even thou it reduced it to just a tiny jitter the fact that there's still a jitter is a problem. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10864#note_149528
Sorry for the delay. I'm seeing some test failures here (repeated several times per run): ``` mpegvideo.c:960: Test succeeded inside todo block: Got stop time 0, expected 0. mpegvideo.c:960: Test marked todo: Got stop time 33333, expected 22222. mpegvideo.c:960: Test marked todo: Got stop time 120000, expected 0. mpegvideo.c:960: Test marked todo: Got stop time 120000, expected 0. ``` -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10864#note_149705
participants (5)
-
Akihiro Sagawa -
Akihiro Sagawa (@sgwaki) -
Chunhao Hung (@otakuxtom) -
Elizabeth Figura (@zfigura) -
Re-a (@Liverel)