Along with !4450, this fixes WMV videos in microkiri (https://bugs.winehq.org/show_bug.cgi?id=9127#c102) and Wagamama High Spec Trial Edition (https://wagahigh.com/download_trial.php#normal ; ダウンロード means download).
-- v8: wmvcore/tests: Add tests for compressed output. winegstreamer: Implement compressed output support in WMSyncReader. winegstreamer: Leave pts/duration unchanged if they're not set. winegstreamer: Introduce mutex for wm_reader read_thread_shutdown and wg_parser. winegstreamer: Move file size to struct wm_reader. winegstreamer: Fill in a few more pieces of WMV format handling. winegstreamer: Add codec_data to WMVs.
From: Alfred Agrell floating@muncher.se
--- dlls/winegstreamer/quartz_parser.c | 14 +++++++++++--- dlls/winegstreamer/unixlib.h | 2 ++ dlls/winegstreamer/wg_format.c | 30 ++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-)
diff --git a/dlls/winegstreamer/quartz_parser.c b/dlls/winegstreamer/quartz_parser.c index e402e1aab52..56c3b20bbec 100644 --- a/dlls/winegstreamer/quartz_parser.c +++ b/dlls/winegstreamer/quartz_parser.c @@ -621,11 +621,11 @@ static bool amt_from_wg_format_video_cinepak(AM_MEDIA_TYPE *mt, const struct wg_
static bool amt_from_wg_format_video_wmv(AM_MEDIA_TYPE *mt, const struct wg_format *format) { - VIDEOINFO *video_format; + VIDEOINFOHEADER *video_format; uint32_t frame_time; const GUID *subtype;
- if (!(video_format = CoTaskMemAlloc(sizeof(*video_format)))) + if (!(video_format = CoTaskMemAlloc(sizeof(*video_format) + format->u.video_wmv.codec_data_len))) return false;
switch (format->u.video_wmv.format) @@ -656,7 +656,7 @@ static bool amt_from_wg_format_video_wmv(AM_MEDIA_TYPE *mt, const struct wg_form mt->bTemporalCompression = TRUE; mt->lSampleSize = 0; mt->formattype = FORMAT_VideoInfo; - mt->cbFormat = sizeof(VIDEOINFOHEADER); + mt->cbFormat = sizeof(*video_format) + format->u.video_wmv.codec_data_len; mt->pbFormat = (BYTE *)video_format;
memset(video_format, 0, sizeof(*video_format)); @@ -669,6 +669,7 @@ static bool amt_from_wg_format_video_wmv(AM_MEDIA_TYPE *mt, const struct wg_form video_format->bmiHeader.biHeight = format->u.video_wmv.height; video_format->bmiHeader.biPlanes = 1; video_format->bmiHeader.biCompression = mt->subtype.Data1; + memcpy(video_format+1, format->u.video_wmv.codec_data, format->u.video_wmv.codec_data_len);
return true; } @@ -997,6 +998,13 @@ static bool amt_to_wg_format_video_wmv(const AM_MEDIA_TYPE *mt, struct wg_format else format->u.video_wmv.format = WG_WMV_VIDEO_FORMAT_UNKNOWN;
+ format->u.video_wmv.codec_data_len = mt->cbFormat - sizeof(VIDEOINFOHEADER); + if (format->u.video_wmv.codec_data_len > sizeof(format->u.video_wmv.codec_data)) + { + ERR("Too big codec_data value (%u).\n", format->u.video_wmv.codec_data_len); + format->u.video_wmv.codec_data_len = 0; + } + memcpy(format->u.video_wmv.codec_data, video_format+1, format->u.video_wmv.codec_data_len); return true; }
diff --git a/dlls/winegstreamer/unixlib.h b/dlls/winegstreamer/unixlib.h index 86bd380c351..aec59c314c2 100644 --- a/dlls/winegstreamer/unixlib.h +++ b/dlls/winegstreamer/unixlib.h @@ -157,6 +157,8 @@ struct wg_format wg_wmv_video_format format; int32_t width, height; uint32_t fps_n, fps_d; + uint32_t codec_data_len; + unsigned char codec_data[64]; } video_wmv; struct { diff --git a/dlls/winegstreamer/wg_format.c b/dlls/winegstreamer/wg_format.c index 274a6dec261..a66718fe928 100644 --- a/dlls/winegstreamer/wg_format.c +++ b/dlls/winegstreamer/wg_format.c @@ -300,6 +300,9 @@ static void wg_format_from_caps_video_wmv(struct wg_format *format, const GstCap gchar format_buffer[5] = {'W','M','V','0',0}; enum wg_wmv_video_format wmv_format; const gchar *wmv_format_str = NULL; + const GValue *codec_data_value; + GstBuffer *codec_data; + GstMapInfo map;
if (!gst_structure_get_int(structure, "width", &width)) { @@ -344,6 +347,19 @@ static void wg_format_from_caps_video_wmv(struct wg_format *format, const GstCap format->u.video_wmv.format = wmv_format; format->u.video_wmv.fps_n = fps_n; format->u.video_wmv.fps_d = fps_d; + + if ((codec_data_value = gst_structure_get_value(structure, "codec_data")) && (codec_data = gst_value_get_buffer(codec_data_value))) + { + gst_buffer_map(codec_data, &map, GST_MAP_READ); + if (map.size <= sizeof(format->u.video_wmv.codec_data)) + { + format->u.video_wmv.codec_data_len = map.size; + memcpy(format->u.video_wmv.codec_data, map.data, map.size); + } + else + GST_WARNING("Too big codec_data value (%u) in %" GST_PTR_FORMAT ".", (UINT)map.size, caps); + gst_buffer_unmap(codec_data, &map); + } }
static void wg_format_from_caps_video_mpeg1(struct wg_format *format, const GstCaps *caps) @@ -733,6 +749,7 @@ static GstCaps *wg_format_to_caps_video_wmv(const struct wg_format *format) { unsigned int wmv_version; const char *wmv_format; + GstBuffer *buffer; GstCaps *caps;
if (!(caps = gst_caps_new_empty_simple("video/x-wmv"))) @@ -780,6 +797,19 @@ static GstCaps *wg_format_to_caps_video_wmv(const struct wg_format *format) if (format->u.video_wmv.fps_d || format->u.video_wmv.fps_n) gst_caps_set_simple(caps, "framerate", GST_TYPE_FRACTION, format->u.video_wmv.fps_n, format->u.video_wmv.fps_d, NULL);
+ if (format->u.video_wmv.codec_data_len) + { + if (!(buffer = gst_buffer_new_and_alloc(format->u.video_wmv.codec_data_len))) + { + gst_caps_unref(caps); + return NULL; + } + + gst_buffer_fill(buffer, 0, format->u.video_wmv.codec_data, format->u.video_wmv.codec_data_len); + gst_caps_set_simple(caps, "codec_data", GST_TYPE_BUFFER, buffer, NULL); + gst_buffer_unref(buffer); + } + return caps; }
From: Alfred Agrell floating@muncher.se
--- dlls/winegstreamer/quartz_parser.c | 9 +++++++-- dlls/winegstreamer/wg_format.c | 7 ++++++- 2 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/dlls/winegstreamer/quartz_parser.c b/dlls/winegstreamer/quartz_parser.c index 56c3b20bbec..3a47091504d 100644 --- a/dlls/winegstreamer/quartz_parser.c +++ b/dlls/winegstreamer/quartz_parser.c @@ -400,6 +400,10 @@ unsigned int wg_format_get_max_size(const struct wg_format *format) return wg_format_get_max_size_video_raw(WG_VIDEO_FORMAT_YV12, format->u.video_mpeg1.width, format->u.video_mpeg1.height);
+ case WG_MAJOR_TYPE_VIDEO_WMV: + return wg_format_get_max_size_video_raw(WG_VIDEO_FORMAT_YV12, + format->u.video_wmv.width, format->u.video_wmv.height); + case WG_MAJOR_TYPE_AUDIO: { unsigned int rate = format->u.audio.rate, channels = format->u.audio.channels; @@ -454,7 +458,6 @@ unsigned int wg_format_get_max_size(const struct wg_format *format)
case WG_MAJOR_TYPE_AUDIO_MPEG4: case WG_MAJOR_TYPE_VIDEO_H264: - case WG_MAJOR_TYPE_VIDEO_WMV: case WG_MAJOR_TYPE_VIDEO_INDEO: FIXME("Format %u not implemented!\n", format->major_type); return 0; @@ -664,11 +667,13 @@ static bool amt_from_wg_format_video_wmv(AM_MEDIA_TYPE *mt, const struct wg_form video_format->rcTarget = video_format->rcSource; if ((frame_time = MulDiv(10000000, format->u.video_wmv.fps_d, format->u.video_wmv.fps_n)) != -1) video_format->AvgTimePerFrame = frame_time; - video_format->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + video_format->bmiHeader.biSize = sizeof(BITMAPINFOHEADER) + format->u.video_wmv.codec_data_len; video_format->bmiHeader.biWidth = format->u.video_wmv.width; video_format->bmiHeader.biHeight = format->u.video_wmv.height; video_format->bmiHeader.biPlanes = 1; video_format->bmiHeader.biCompression = mt->subtype.Data1; + video_format->bmiHeader.biBitCount = 24; + video_format->dwBitRate = 0; memcpy(video_format+1, format->u.video_wmv.codec_data, format->u.video_wmv.codec_data_len);
return true; diff --git a/dlls/winegstreamer/wg_format.c b/dlls/winegstreamer/wg_format.c index a66718fe928..9404a0290eb 100644 --- a/dlls/winegstreamer/wg_format.c +++ b/dlls/winegstreamer/wg_format.c @@ -893,7 +893,6 @@ bool wg_format_compare(const struct wg_format *a, const struct wg_format *b) case WG_MAJOR_TYPE_AUDIO_MPEG4: case WG_MAJOR_TYPE_AUDIO_WMA: case WG_MAJOR_TYPE_VIDEO_H264: - case WG_MAJOR_TYPE_VIDEO_WMV: case WG_MAJOR_TYPE_VIDEO_INDEO: case WG_MAJOR_TYPE_VIDEO_MPEG1: GST_FIXME("Format %u not implemented!", a->major_type); @@ -917,6 +916,12 @@ bool wg_format_compare(const struct wg_format *a, const struct wg_format *b) /* Do not compare FPS. */ return a->u.video_cinepak.width == b->u.video_cinepak.width && a->u.video_cinepak.height == b->u.video_cinepak.height; + + case WG_MAJOR_TYPE_VIDEO_WMV: + /* Do not compare FPS. */ + return a->u.video_wmv.format == b->u.video_wmv.format + && a->u.video_wmv.width == b->u.video_wmv.width + && a->u.video_wmv.height == b->u.video_wmv.height; }
assert(0);
From: Alfred Agrell floating@muncher.se
--- dlls/winegstreamer/wm_reader.c | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-)
diff --git a/dlls/winegstreamer/wm_reader.c b/dlls/winegstreamer/wm_reader.c index 882b6df1bbb..09f69bc6208 100644 --- a/dlls/winegstreamer/wm_reader.c +++ b/dlls/winegstreamer/wm_reader.c @@ -55,6 +55,7 @@ struct wm_reader
CRITICAL_SECTION cs; QWORD start_time; + QWORD file_size;
IStream *source_stream; HANDLE file; @@ -592,27 +593,12 @@ static DWORD CALLBACK read_thread(void *arg) IStream *stream = reader->source_stream; HANDLE file = reader->file; size_t buffer_size = 4096; - uint64_t file_size; + uint64_t file_size = reader->file_size; void *data;
if (!(data = malloc(buffer_size))) return 0;
- if (file) - { - LARGE_INTEGER size; - - GetFileSizeEx(file, &size); - file_size = size.QuadPart; - } - else - { - STATSTG stat; - - IStream_Stat(stream, &stat, STATFLAG_NONAME); - file_size = stat.cbSize.QuadPart; - } - TRACE("Starting read thread for reader %p.\n", reader);
while (!reader->read_thread_shutdown) @@ -1453,7 +1439,7 @@ static const IWMReaderTimecodeVtbl timecode_vtbl = timecode_GetTimecodeRangeBounds, };
-static HRESULT init_stream(struct wm_reader *reader, QWORD file_size) +static HRESULT init_stream(struct wm_reader *reader) { wg_parser_t wg_parser; HRESULT hr; @@ -1470,7 +1456,7 @@ static HRESULT init_stream(struct wm_reader *reader, QWORD file_size) goto out_destroy_parser; }
- if (FAILED(hr = wg_parser_connect(reader->wg_parser, file_size))) + if (FAILED(hr = wg_parser_connect(reader->wg_parser, reader->file_size))) { ERR("Failed to connect parser, hr %#lx.\n", hr); goto out_shutdown_thread; @@ -2134,8 +2120,9 @@ static HRESULT WINAPI reader_Open(IWMSyncReader2 *iface, const WCHAR *filename) }
reader->file = file; + reader->file_size = size.QuadPart;
- if (FAILED(hr = init_stream(reader, size.QuadPart))) + if (FAILED(hr = init_stream(reader))) reader->file = NULL;
LeaveCriticalSection(&reader->cs); @@ -2166,7 +2153,9 @@ static HRESULT WINAPI reader_OpenStream(IWMSyncReader2 *iface, IStream *stream) }
IStream_AddRef(reader->source_stream = stream); - if (FAILED(hr = init_stream(reader, stat.cbSize.QuadPart))) + reader->file_size = stat.cbSize.QuadPart; + + if (FAILED(hr = init_stream(reader))) { IStream_Release(stream); reader->source_stream = NULL;
From: Alfred Agrell floating@muncher.se
Latter is meaningless in this commit, but necessary in the next. --- dlls/winegstreamer/wm_reader.c | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-)
diff --git a/dlls/winegstreamer/wm_reader.c b/dlls/winegstreamer/wm_reader.c index 09f69bc6208..595baeea060 100644 --- a/dlls/winegstreamer/wm_reader.c +++ b/dlls/winegstreamer/wm_reader.c @@ -54,6 +54,7 @@ struct wm_reader LONG refcount;
CRITICAL_SECTION cs; + CRITICAL_SECTION init_cs; /* Protects wg_parser and read_thread_shutdown */ QWORD start_time; QWORD file_size;
@@ -601,15 +602,25 @@ static DWORD CALLBACK read_thread(void *arg)
TRACE("Starting read thread for reader %p.\n", reader);
- while (!reader->read_thread_shutdown) + while (true) { + wg_parser_t parser; LARGE_INTEGER large_offset; uint64_t offset; ULONG ret_size; uint32_t size; HRESULT hr;
- if (!wg_parser_get_next_read_offset(reader->wg_parser, &offset, &size)) + EnterCriticalSection(&reader->init_cs); + if (reader->read_thread_shutdown) + { + LeaveCriticalSection(&reader->init_cs); + break; + } + parser = reader->wg_parser; + LeaveCriticalSection(&reader->init_cs); + + if (!wg_parser_get_next_read_offset(parser, &offset, &size)) continue;
if (offset >= file_size) @@ -619,7 +630,7 @@ static DWORD CALLBACK read_thread(void *arg)
if (!size) { - wg_parser_push_data(reader->wg_parser, data, 0); + wg_parser_push_data(parser, data, 0); continue; }
@@ -638,7 +649,7 @@ static DWORD CALLBACK read_thread(void *arg) || !ReadFile(file, data, size, &ret_size, NULL)) { ERR("Failed to read %u bytes at offset %I64u, error %lu.\n", size, offset, GetLastError()); - wg_parser_push_data(reader->wg_parser, NULL, 0); + wg_parser_push_data(parser, NULL, 0); continue; } } @@ -649,14 +660,14 @@ static DWORD CALLBACK read_thread(void *arg) if (FAILED(hr)) { ERR("Failed to read %u bytes at offset %I64u, hr %#lx.\n", size, offset, hr); - wg_parser_push_data(reader->wg_parser, NULL, 0); + wg_parser_push_data(parser, NULL, 0); continue; } }
if (ret_size != size) ERR("Unexpected short read: requested %u bytes, got %lu.\n", size, ret_size); - wg_parser_push_data(reader->wg_parser, data, ret_size); + wg_parser_push_data(parser, data, ret_size); }
free(data); @@ -1450,6 +1461,7 @@ static HRESULT init_stream(struct wm_reader *reader)
reader->wg_parser = wg_parser; reader->read_thread_shutdown = false; + if (!(reader->read_thread = CreateThread(NULL, 0, read_thread, reader, 0, NULL))) { hr = E_OUTOFMEMORY; @@ -1517,14 +1529,18 @@ out_disconnect_parser: wg_parser_disconnect(reader->wg_parser);
out_shutdown_thread: + EnterCriticalSection(&reader->init_cs); reader->read_thread_shutdown = true; + LeaveCriticalSection(&reader->init_cs); WaitForSingleObject(reader->read_thread, INFINITE); CloseHandle(reader->read_thread); reader->read_thread = NULL;
out_destroy_parser: + EnterCriticalSection(&reader->init_cs); wg_parser_destroy(reader->wg_parser); reader->wg_parser = 0; + LeaveCriticalSection(&reader->init_cs);
return hr; } @@ -1728,6 +1744,8 @@ static ULONG WINAPI unknown_inner_Release(IUnknown *iface)
reader->cs.DebugInfo->Spare[0] = 0; DeleteCriticalSection(&reader->cs); + reader->init_cs.DebugInfo->Spare[0] = 0; + DeleteCriticalSection(&reader->init_cs);
free(reader); } @@ -1781,7 +1799,9 @@ static HRESULT WINAPI reader_Close(IWMSyncReader2 *iface)
wg_parser_disconnect(reader->wg_parser);
+ EnterCriticalSection(&reader->init_cs); reader->read_thread_shutdown = true; + LeaveCriticalSection(&reader->init_cs); WaitForSingleObject(reader->read_thread, INFINITE); CloseHandle(reader->read_thread); reader->read_thread = NULL; @@ -2557,6 +2577,8 @@ HRESULT WINAPI winegstreamer_create_wm_sync_reader(IUnknown *outer, void **out)
InitializeCriticalSection(&object->cs); object->cs.DebugInfo->Spare[0] = (DWORD_PTR)(__FILE__ ": reader.cs"); + InitializeCriticalSection(&object->init_cs); + object->init_cs.DebugInfo->Spare[0] = (DWORD_PTR)(__FILE__ ": reader.init_cs");
TRACE("Created reader %p.\n", object); *out = outer ? (void *)&object->IUnknown_inner : (void *)&object->IWMSyncReader2_iface;
From: Alfred Agrell floating@muncher.se
--- dlls/winegstreamer/wm_reader.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/dlls/winegstreamer/wm_reader.c b/dlls/winegstreamer/wm_reader.c index 595baeea060..09d07ddfdb0 100644 --- a/dlls/winegstreamer/wm_reader.c +++ b/dlls/winegstreamer/wm_reader.c @@ -1664,13 +1664,15 @@ static HRESULT wm_reader_read_stream_sample(struct wm_reader *reader, struct wg_
wg_parser_stream_release_buffer(stream->wg_stream);
- if (!buffer->has_pts) + if (buffer->has_pts) + *pts = buffer->pts; + else FIXME("Missing PTS.\n"); - if (!buffer->has_duration) + if (buffer->has_duration) + *duration = buffer->duration; + else FIXME("Missing duration.\n");
- *pts = buffer->pts; - *duration = buffer->duration; *flags = 0; if (buffer->discontinuity) *flags |= WM_SF_DISCONTINUITY;
From: Alfred Agrell floating@muncher.se
--- dlls/winegstreamer/wm_reader.c | 79 ++++++++++++++++++++++++++++------ dlls/wmvcore/tests/wmvcore.c | 29 +++++++++++-- 2 files changed, 91 insertions(+), 17 deletions(-)
diff --git a/dlls/winegstreamer/wm_reader.c b/dlls/winegstreamer/wm_reader.c index 09d07ddfdb0..b649ddb06be 100644 --- a/dlls/winegstreamer/wm_reader.c +++ b/dlls/winegstreamer/wm_reader.c @@ -30,10 +30,6 @@ struct wm_stream WMT_STREAM_SELECTION selection; WORD index; bool eos; - /* Note that we only pretend to read compressed samples, and instead output - * uncompressed samples regardless of whether we are configured to read - * compressed samples. Rather, the behaviour of the reader objects differs - * in nontrivial ways depending on this field. */ bool read_compressed;
IWMReaderAllocatorEx *output_allocator; @@ -620,6 +616,12 @@ static DWORD CALLBACK read_thread(void *arg) parser = reader->wg_parser; LeaveCriticalSection(&reader->init_cs);
+ if (!parser) + { + Sleep(10); + continue; + } + if (!wg_parser_get_next_read_offset(parser, &offset, &size)) continue;
@@ -1450,6 +1452,8 @@ static const IWMReaderTimecodeVtbl timecode_vtbl = timecode_GetTimecodeRangeBounds, };
+static void setup_stream_formats(struct wm_reader *reader); + static HRESULT init_stream(struct wm_reader *reader) { wg_parser_t wg_parser; @@ -1475,13 +1479,40 @@ static HRESULT init_stream(struct wm_reader *reader) }
reader->stream_count = wg_parser_get_stream_count(reader->wg_parser); - if (!(reader->streams = calloc(reader->stream_count, sizeof(*reader->streams)))) { hr = E_OUTOFMEMORY; goto out_disconnect_parser; } + for (i = 0; i < reader->stream_count; ++i) + reader->streams[i].selection = WMT_ON; + + setup_stream_formats(reader); + return S_OK; + +out_disconnect_parser: + wg_parser_disconnect(reader->wg_parser);
+out_shutdown_thread: + EnterCriticalSection(&reader->init_cs); + reader->read_thread_shutdown = true; + LeaveCriticalSection(&reader->init_cs); + WaitForSingleObject(reader->read_thread, INFINITE); + CloseHandle(reader->read_thread); + reader->read_thread = NULL; + +out_destroy_parser: + EnterCriticalSection(&reader->init_cs); + wg_parser_destroy(reader->wg_parser); + reader->wg_parser = 0; + LeaveCriticalSection(&reader->init_cs); + + return hr; +} + +static void setup_stream_formats(struct wm_reader *reader) +{ + WORD i; for (i = 0; i < reader->stream_count; ++i) { struct wm_stream *stream = &reader->streams[i]; @@ -1489,7 +1520,6 @@ static HRESULT init_stream(struct wm_reader *reader) stream->wg_stream = wg_parser_get_stream(reader->wg_parser, i); stream->reader = reader; stream->index = i; - stream->selection = WMT_ON; wg_parser_stream_get_preferred_format(stream->wg_stream, &stream->format); if (stream->format.major_type == WG_MAJOR_TYPE_AUDIO) { @@ -1515,26 +1545,46 @@ static HRESULT init_stream(struct wm_reader *reader) if (stream->format.u.video.height > 0) stream->format.u.video.height = -stream->format.u.video.height; } - wg_parser_stream_enable(stream->wg_stream, &stream->format); + if (stream->selection == WMT_ON) + wg_parser_stream_enable(stream->wg_stream, &stream->format); }
/* We probably discarded events because streams weren't enabled yet. * Now that they're all enabled seek back to the start again. */ wg_parser_stream_seek(reader->streams[0].wg_stream, 1.0, 0, 0, AM_SEEKING_AbsolutePositioning, AM_SEEKING_NoPositioning); +}
- return S_OK; +static HRESULT reinit_stream(struct wm_reader *reader, bool read_compressed) +{ + wg_parser_t wg_parser; + HRESULT hr;
-out_disconnect_parser: wg_parser_disconnect(reader->wg_parser); + EnterCriticalSection(&reader->init_cs); + wg_parser_destroy(reader->wg_parser); + reader->wg_parser = 0; + LeaveCriticalSection(&reader->init_cs); + + if (!(wg_parser = wg_parser_create(WG_PARSER_DECODEBIN, read_compressed))) + return E_OUTOFMEMORY;
-out_shutdown_thread: EnterCriticalSection(&reader->init_cs); - reader->read_thread_shutdown = true; + reader->wg_parser = wg_parser; + reader->read_thread_shutdown = false; LeaveCriticalSection(&reader->init_cs); - WaitForSingleObject(reader->read_thread, INFINITE); - CloseHandle(reader->read_thread); - reader->read_thread = NULL; + + if (FAILED(hr = wg_parser_connect(reader->wg_parser, reader->file_size))) + { + ERR("Failed to connect parser, hr %#lx.\n", hr); + goto out_destroy_parser; + } + + assert(reader->stream_count == wg_parser_get_stream_count(reader->wg_parser)); + + setup_stream_formats(reader); + + return S_OK;
out_destroy_parser: EnterCriticalSection(&reader->init_cs); @@ -2351,6 +2401,7 @@ static HRESULT WINAPI reader_SetReadStreamSamples(IWMSyncReader2 *iface, WORD st }
stream->read_compressed = compressed; + reinit_stream(reader, compressed);
LeaveCriticalSection(&reader->cs); return S_OK; diff --git a/dlls/wmvcore/tests/wmvcore.c b/dlls/wmvcore/tests/wmvcore.c index 1741299e654..35688bd04e4 100644 --- a/dlls/wmvcore/tests/wmvcore.c +++ b/dlls/wmvcore/tests/wmvcore.c @@ -1848,6 +1848,7 @@ struct callback QWORD next_pts[2]; QWORD expect_time; HANDLE expect_ontime, got_ontime; + bool todo_rewind; };
static struct callback *impl_from_IWMReaderCallback(IWMReaderCallback *iface) @@ -2052,8 +2053,16 @@ static HRESULT WINAPI callback_OnSample(IWMReaderCallback *iface, DWORD output, trace("%lu: %04lx: IWMReaderCallback::OnSample(output %lu, time %I64u, duration %I64u, flags %#lx)\n", GetTickCount(), GetCurrentThreadId(), output, time, duration, flags);
+ if (callback->last_pts[output] > time && callback->todo_rewind) + { + callback->todo_rewind = false; + todo_wine ok(0, "changing compression state in Wine rewinds the stream\n"); + callback->last_pts[0] = 0; + callback->last_pts[1] = 0; + } + /* uncompressed samples are slightly out of order because of decoding delay */ - ok(callback->last_pts[output] <= time, "got time %I64d\n", time); + ok(callback->last_pts[output] <= time, "expected %I64d <= %I64d\n", callback->last_pts[output], time); callback->last_pts[output] = time; callback->next_pts[output] = time + duration;
@@ -2134,7 +2143,15 @@ static HRESULT WINAPI callback_advanced_OnStreamSample(IWMReaderCallbackAdvanced trace("%lu: %04lx: IWMReaderCallbackAdvanced::OnStreamSample(stream %u, pts %I64u, duration %I64u, flags %#lx)\n", GetTickCount(), GetCurrentThreadId(), stream_number, pts, duration, flags);
- ok(callback->last_pts[output] <= pts, "got pts %I64d\n", pts); + if (callback->last_pts[output] > pts && callback->todo_rewind) + { + callback->todo_rewind = false; + todo_wine ok(0, "changing compression state in Wine rewinds the stream\n"); + callback->last_pts[0] = 0; + callback->last_pts[1] = 0; + } + + ok(callback->last_pts[output] <= pts, "expected %I64d <= %I64d\n", callback->last_pts[output], pts); callback->last_pts[output] = pts; callback->next_pts[output] = pts + duration;
@@ -2146,7 +2163,7 @@ static HRESULT WINAPI callback_advanced_OnStreamSample(IWMReaderCallbackAdvanced else { ok(callback->callback_tid == GetCurrentThreadId(), "got wrong thread\n"); - ok(callback->last_pts[1 - output] <= pts, "got pts %I64d\n", pts); + ok(callback->last_pts[1 - output] <= pts, "expected %I64d <= %I64d\n", callback->last_pts[1 - output], pts); }
if (!callback->output_tid[output]) @@ -2709,6 +2726,8 @@ static void run_async_reader(IWMReader *reader, IWMReaderAdvanced2 *advanced, st todo_wine ok(hr == E_UNEXPECTED, "Got hr %#lx.\n", hr);
+ if (winetest_platform_is_wine) + callback->todo_rewind = true; callback->expect_time = 13460000; hr = IWMReaderAdvanced2_DeliverTime(advanced, 13460000); ok(hr == S_OK, "Got hr %#lx.\n", hr); @@ -2723,6 +2742,7 @@ static void run_async_reader(IWMReader *reader, IWMReaderAdvanced2 *advanced, st ok(callback->next_pts[1] == 13270000, "Got pts %I64d.\n", callback->next_pts[1]); ok(callback->sample_count > 0, "Got no samples.\n"); callback->sample_count = 0; + ok(!callback->todo_rewind, "Didn't rewind as expected\n");
callback->read_compressed = true; hr = IWMReaderAdvanced2_SetReceiveStreamSamples(advanced, 1, TRUE); @@ -2731,6 +2751,8 @@ static void run_async_reader(IWMReader *reader, IWMReaderAdvanced2 *advanced, st ok(hr == S_OK, "Got hr %#lx.\n", hr); }
+ if (winetest_platform_is_wine && callback->read_compressed) + callback->todo_rewind = true; callback->expect_time = test_wmv_duration * 2; hr = IWMReaderAdvanced2_DeliverTime(advanced, test_wmv_duration * 2); ok(hr == S_OK, "Got hr %#lx.\n", hr); @@ -2754,6 +2776,7 @@ static void run_async_reader(IWMReader *reader, IWMReaderAdvanced2 *advanced, st callback->next_pts[0] = 0; callback->last_pts[1] = 0; callback->next_pts[1] = 0; + ok(!callback->todo_rewind, "Didn't rewind as expected\n");
hr = IWMReader_Stop(reader); ok(hr == S_OK, "Got hr %#lx.\n", hr);
From: Alfred Agrell floating@muncher.se
--- dlls/wmvcore/tests/wmvcore.c | 185 +++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+)
diff --git a/dlls/wmvcore/tests/wmvcore.c b/dlls/wmvcore/tests/wmvcore.c index 35688bd04e4..97c343704ac 100644 --- a/dlls/wmvcore/tests/wmvcore.c +++ b/dlls/wmvcore/tests/wmvcore.c @@ -1819,6 +1819,190 @@ static void test_sync_reader_file(void) ok(ret, "Failed to delete %s, error %lu.\n", debugstr_w(filename), GetLastError()); }
+static void test_stream_output_type(IWMProfile *profile, WORD stream_num, const WM_MEDIA_TYPE *expected) +{ + IWMStreamConfig *stream_config; + IWMMediaProps *media_props; + char mt_buffer[2000]; + WM_MEDIA_TYPE *mt; + DWORD ret_size; + HRESULT hr; + + hr = IWMProfile_GetStreamByNumber(profile, stream_num, &stream_config); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + + hr = IWMStreamConfig_QueryInterface(stream_config, &IID_IWMMediaProps, (void**)&media_props); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + + mt = (WM_MEDIA_TYPE *)mt_buffer; + memset(mt_buffer, 0xcc, sizeof(mt_buffer)); + ret_size = sizeof(mt_buffer); + hr = IWMMediaProps_GetMediaType(media_props, mt, &ret_size); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + ok(compare_media_types(mt, expected), "Media types didn't match.\n"); + + IWMStreamConfig_Release(stream_config); + IWMMediaProps_Release(media_props); +} + +static const VIDEOINFOHEADER vih_wmv1 = { + .rcSource = { 0, 0, 64, 48 }, + .rcTarget = { 0, 0, 64, 48 }, + .dwBitRate = 0x0002e418, + .dwBitErrorRate = 0, + .AvgTimePerFrame = 0, + .bmiHeader.biSize = sizeof(BITMAPINFOHEADER), + .bmiHeader.biWidth = 64, + .bmiHeader.biHeight = 48, + .bmiHeader.biPlanes = 1, + .bmiHeader.biBitCount = 0x18, + .bmiHeader.biCompression = MAKEFOURCC('W','M','V','1'), + .bmiHeader.biSizeImage = 0, + .bmiHeader.biXPelsPerMeter = 0, + .bmiHeader.biYPelsPerMeter = 0, +}; +static const WM_MEDIA_TYPE mt_wmv1 = { + /* MEDIATYPE_Video, MEDIASUBTYPE_WMV1, FORMAT_VideoInfo */ + .majortype = {0x73646976, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}}, + .subtype = {0x31564d57, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}}, + .bTemporalCompression = TRUE, + .formattype = {0x05589f80, 0xc356, 0x11ce, {0xbf, 0x01, 0x00, 0xaa, 0x00, 0x55, 0x59, 0x5a}}, + .cbFormat = sizeof(VIDEOINFOHEADER), + .pbFormat = (BYTE *)&vih_wmv1, +}; + +static const MSAUDIO1WAVEFORMAT wfx_msaudio1 = { + .wfx.wFormatTag = WAVE_FORMAT_MSAUDIO1, + .wfx.nChannels = 1, + .wfx.nSamplesPerSec = 44100, + .wfx.nAvgBytesPerSec = 16000, + .wfx.nBlockAlign = 0x02e7, + .wfx.wBitsPerSample = 0x0010, + .wfx.cbSize = MSAUDIO1_WFX_EXTRA_BYTES, + .wSamplesPerBlock = 0, + .wEncodeOptions = 1, +}; +static const WM_MEDIA_TYPE mt_msaudio1 = { + /* MEDIATYPE_Audio, MEDIASUBTYPE_MSAUDIO1, FORMAT_WaveFormatEx */ + .majortype = {0x73647561, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}}, + .subtype = {0x00000160, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}}, + .bFixedSizeSamples = TRUE, + .lSampleSize = 0x02e7, + .formattype = {0x05589f81, 0xc356, 0x11ce, {0xbf, 0x01, 0x00, 0xaa, 0x00, 0x55, 0x59, 0x5a}}, + .cbFormat = sizeof(MSAUDIO1WAVEFORMAT), + .pbFormat = (BYTE *)&wfx_msaudio1, +}; + +static void test_sync_reader_compressed_output(void) +{ + static const DWORD audio_sample_times[] = { + 0, 460000, 920000, 1390000, 1850000, 2320000, 2780000, 3250000, 3710000, 4180000, 4640000, 5100000, + 5570000, 6030000, 6500000, 6960000, 7430000, 7890000, 8350000, 8820000, 9280000, 9750000, 10210000, + 10680000, 11140000, 11610000, 12070000, 12530000, 13000000, 13460000, 13930000, 14390000, 14860000, + 15320000, 15790000, 16250000, 16710000, 17180000, 17640000, 18110000, 18570000, 19040000, 19500000, 19960000, + 99999999 + }; + static const DWORD video_sample_sizes[] = { + 1117, 8, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 1117, 8, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 1117, 8, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 1117, 8, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 1117, 8 + }; + + const WCHAR *filename = load_resource(L"test.wmv"); + DWORD flags, bytes_count; + QWORD sample_time, sample_duration; + IWMSyncReader *reader; + IWMProfile *profile; + INSSBuffer *sample; + WORD stream_num; + HRESULT hr; + BYTE *data; + + DWORD audio_idx = 0; + DWORD video_idx = 0; + + VIDEOINFOHEADER vih = vih_wmv1; + WM_MEDIA_TYPE mt = mt_wmv1; + mt.pbFormat = (BYTE *)&vih; + + hr = WMCreateSyncReader(NULL, 0, &reader); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + IWMSyncReader_QueryInterface(reader, &IID_IWMProfile, (void **)&profile); + + hr = IWMSyncReader_Open(reader, filename); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + + if (winetest_platform_is_wine) + { + todo_wine ok(0, "dwBitRate is not implemented\n"); + vih.dwBitRate = 0; + } + test_stream_output_type(profile, 1, &mt); + test_stream_output_type(profile, 2, &mt_msaudio1); + + hr = IWMSyncReader_SetReadStreamSamples(reader, 1, TRUE); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + hr = IWMSyncReader_SetReadStreamSamples(reader, 2, TRUE); + ok(hr == S_OK, "Got hr %#lx.\n", hr); + + test_stream_output_type(profile, 1, &mt); + test_stream_output_type(profile, 2, &mt_msaudio1); + + while (video_idx < 50 || audio_idx < 44) + { + DWORD next_video_time = 460000 + video_idx * 400000; + DWORD next_audio_time = audio_sample_times[audio_idx]; + + sample_duration = 1234; + hr = IWMSyncReader_GetNextSample(reader, 0, &sample, &sample_time, &sample_duration, &flags, NULL, &stream_num); + ok(hr == S_OK, "%lu/%lu: Got hr %#lx.\n", video_idx, audio_idx, hr); + /* we don't care about the buffer, but GetLength is unimplemented in Wine */ + hr = INSSBuffer_GetBufferAndLength(sample, &data, &bytes_count); + ok(hr == S_OK, "%lu/%lu: Got hr %#lx.\n", video_idx, audio_idx, hr); + + if (next_video_time <= next_audio_time) + { + ok(stream_num == 1, "%lu: Got %lu\n", video_idx, (DWORD)stream_num); + ok(sample_time == next_video_time, "%lu: Expected %lu, got %lu\n", video_idx, next_video_time, (DWORD)sample_time); + todo_wine ok(sample_duration == 10000, "%lu: Got %lu\n", video_idx, (DWORD)sample_duration); + + if (video_idx == 0) + ok(flags == (WM_SF_CLEANPOINT|WM_SF_DISCONTINUITY), "%lu: Got %lu\n", audio_idx, flags); + else if (video_sample_sizes[video_idx] == 1117) + ok(flags == WM_SF_CLEANPOINT, "%lu: Got %lu\n", audio_idx, flags); + else + ok(flags == 0, "%lu: Got %lu\n", audio_idx, flags); + ok(bytes_count == video_sample_sizes[video_idx], + "%lu: Expected %lu, got %lu\n", video_idx, video_sample_sizes[video_idx], bytes_count); + video_idx++; + } + else + { + ok(stream_num == 2, "%lu: Got %lu\n", audio_idx, (DWORD)stream_num); + ok(sample_time == next_audio_time, "%lu: Expected %lu, got %lu\n", audio_idx, next_audio_time, (DWORD)sample_time); + todo_wine ok(sample_duration == 460000, "%lu: Got %lu\n", audio_idx, (DWORD)sample_duration); + + if (audio_idx == 0) + todo_wine ok(flags == (WM_SF_CLEANPOINT|WM_SF_DISCONTINUITY), "%lu: Got %lu\n", audio_idx, flags); + else + todo_wine ok(flags == WM_SF_CLEANPOINT, "%lu: Got %lu\n", audio_idx, flags); + ok(bytes_count == 743, "%lu: Got %lu\n", audio_idx, bytes_count); + + audio_idx++; + } + + INSSBuffer_Release(sample); + } + + hr = IWMSyncReader_GetNextSample(reader, 0, &sample, &sample_time, &sample_duration, &flags, NULL, &stream_num); + ok(hr == NS_E_NO_MORE_SAMPLES, "Got hr %#lx.\n", hr); + + IWMSyncReader_Release(reader); + IWMProfile_Release(profile); +} + struct callback { IWMReaderCallback IWMReaderCallback_iface; @@ -3979,6 +4163,7 @@ START_TEST(wmvcore) test_sync_reader_streaming(); test_sync_reader_types(); test_sync_reader_file(); + test_sync_reader_compressed_output(); test_async_reader_settings(); test_async_reader_streaming(); test_async_reader_types();
On Fri Dec 1 14:19:24 2023 +0000, Alfred Agrell wrote:
Recreating the thread throws approximately 99999 test failures about being called from wrong thread. Should I throw some todo_wine or something at them instead? Some of those tests feel kinda overzealous.
I think the input_tid tests could be relaxed, or even removed entirely. Rémi may correct me on this but I don't think that an application actually cared about the read thread.
Would you prefer if I just tell the tests to explicitly rewind the stream when changing compression mode? [As mentioned in wm_reader.c](https://gitlab.winehq.org/wine/wine/-/blob/master/dlls/winegstreamer/wm_read...), no plausible program reconfigures streams dynamically anyways.
That's probably best.
Alternatively we could do something like
if (pts < last_pts) ++callback->rewind_count;
todo_wine ok(!callback->rewind_count);
which I think achieves the same goal while being simpler.
On Fri Dec 1 14:19:25 2023 +0000, Alfred Agrell wrote:
The motivation is the todo_wine ok(sample_duration == 460000) in the test. If has_duration is false, duration is uninitialized, and occasionally equal to 460000. Making the write conditional allows me to set duration to 1234 in the caller, which is guaranteed not equal to 10000 or 460000. Unrelated, good point. I'll split it to a separate commit.
Huh, why is GStreamer not giving us duration?
Zebediah Figura (@zfigura) commented about dlls/wmvcore/tests/wmvcore.c:
- DWORD flags, bytes_count;
- QWORD sample_time, sample_duration;
- IWMSyncReader *reader;
- IWMProfile *profile;
- INSSBuffer *sample;
- WORD stream_num;
- HRESULT hr;
- BYTE *data;
- DWORD audio_idx = 0;
- DWORD video_idx = 0;
- VIDEOINFOHEADER vih = vih_wmv1;
- WM_MEDIA_TYPE mt = mt_wmv1;
- mt.pbFormat = (BYTE *)&vih;
Style nitpick, but let's please put all the variable declarations together and separated from all the code.
On Fri Dec 1 19:33:44 2023 +0000, Zebediah Figura wrote:
I think the input_tid tests could be relaxed, or even removed entirely. Rémi may correct me on this but I don't think that an application actually cared about the read thread.
I won't be able to confirm either way, only thing I remember is PTSD from deadlocks in the quartz / asf reader / wm reader / winegstreamer stack.
Then on the subject of compressed output, I'm not maintaining the WMReader side so I don't want to object to any solution but my opinion is the same as for the MF media source: IMO restarting the wg_parser to make it start / stop decoding isn't the right way. Instead we should wrap lower level unix components, and combine optional WMV / WMA decoders (which I believe should now be more or less working) on the PE side like native does.
Zebediah Figura (@zfigura) commented about dlls/wmvcore/tests/wmvcore.c:
- VIDEOINFOHEADER vih = vih_wmv1;
- WM_MEDIA_TYPE mt = mt_wmv1;
- mt.pbFormat = (BYTE *)&vih;
- hr = WMCreateSyncReader(NULL, 0, &reader);
- ok(hr == S_OK, "Got hr %#lx.\n", hr);
- IWMSyncReader_QueryInterface(reader, &IID_IWMProfile, (void **)&profile);
- hr = IWMSyncReader_Open(reader, filename);
- ok(hr == S_OK, "Got hr %#lx.\n", hr);
- if (winetest_platform_is_wine)
- {
todo_wine ok(0, "dwBitRate is not implemented\n");
vih.dwBitRate = 0;
- }
This test doesn't make any sense. If it's that the type we return is correct except for the bit rate, then let's just put a todo_wine on that statement. Or, if we want to be a bit stronger, we can do something like
``` /* We don't return the correct bit rate, but we still want to make sure * that regressions in other fields aren't hidden by a todo_wine. */ todo_wine ok(mt->dwBitRate == expect->dwBitRate); mt->dwBitRate = expect->dwBitRate; ```
Zebediah Figura (@zfigura) commented about dlls/wmvcore/tests/wmvcore.c:
- ok(hr == S_OK, "Got hr %#lx.\n", hr);
- test_stream_output_type(profile, 1, &mt);
- test_stream_output_type(profile, 2, &mt_msaudio1);
- while (video_idx < 50 || audio_idx < 44)
- {
DWORD next_video_time = 460000 + video_idx * 400000;
DWORD next_audio_time = audio_sample_times[audio_idx];
sample_duration = 1234;
hr = IWMSyncReader_GetNextSample(reader, 0, &sample, &sample_time, &sample_duration, &flags, NULL, &stream_num);
ok(hr == S_OK, "%lu/%lu: Got hr %#lx.\n", video_idx, audio_idx, hr);
/* we don't care about the buffer, but GetLength is unimplemented in Wine */
hr = INSSBuffer_GetBufferAndLength(sample, &data, &bytes_count);
ok(hr == S_OK, "%lu/%lu: Got hr %#lx.\n", video_idx, audio_idx, hr);
We have a winetest_push_context() that takes care of the prefix. See its use in e.g. test_async_reader_types().
Zebediah Figura (@zfigura) commented about dlls/wmvcore/tests/wmvcore.c:
- hr = IWMStreamConfig_QueryInterface(stream_config, &IID_IWMMediaProps, (void**)&media_props);
- ok(hr == S_OK, "Got hr %#lx.\n", hr);
- mt = (WM_MEDIA_TYPE *)mt_buffer;
- memset(mt_buffer, 0xcc, sizeof(mt_buffer));
- ret_size = sizeof(mt_buffer);
- hr = IWMMediaProps_GetMediaType(media_props, mt, &ret_size);
- ok(hr == S_OK, "Got hr %#lx.\n", hr);
- ok(compare_media_types(mt, expected), "Media types didn't match.\n");
- IWMStreamConfig_Release(stream_config);
- IWMMediaProps_Release(media_props);
+}
+static const VIDEOINFOHEADER vih_wmv1 = {
Style nitpick, opening braces always go on their own line, to match vertically with the closing brace.
On Fri Dec 1 19:49:09 2023 +0000, Rémi Bernon wrote:
I won't be able to confirm either way, only thing I remember is PTSD from deadlocks in the quartz / asf reader / wm reader / winegstreamer stack. Then on the subject of compressed output, I'm not maintaining the WMReader side so I don't want to object to any solution but my opinion is the same as for the MF media source: IMO restarting the wg_parser to make it start / stop decoding isn't the right way. Instead we should wrap lower level unix components, and combine optional WMV / WMA decoders (which I believe should now be more or less working) on the PE side like native does.
Considering the `stream.input_tid = 0; /* FIXME: currently required as Wine calls IStream_Stat synchronously in OpenStream */` in tests/wmvcore.c, I'm inclined to agree that those tests are unnecessary.
That proposal would indeed solve a bunch of todos, but it may also have a performance impact; I don't know how much we copy things around when crossing the PE/Unix boundary.
Switched to making it recreate the thread. I kept the mutex, though; I think those unprotected read_thread_shutdown accesses in current master are harmless in practice, but they make me nervous. (I could've changed the mutex to a few InterlockedOr(), but that'd need a bunch of ugly casts, and it's less clear than a mutex.)
On Fri Dec 1 19:45:27 2023 +0000, Zebediah Figura wrote:
Huh, why is GStreamer not giving us duration?
That's a question for the GStreamer asfdemux devs, not me. My guess is they didn't think anyone would need it (or WMV bitrate), so they left it unimplemented, but that's just a guess.
Feel free to use `GST_DEBUG_NO_COLOR=1 GST_DEBUG=5 gst-launch-1.0 filesrc location=dlls/wmvcore/tests/test.wmv ! asfdemux ! video/x-wmv ! fakesink 2>&1 | grep gst_pad_chain_data_unchecked` to verify that it's not an issue on Wine's side.
PTS is present on everything, that part of the change is just for consistency. (Though leaving the out params unchanged is rather unhelpful. I'll make it zero them instead.)
On Fri Dec 1 19:51:25 2023 +0000, Zebediah Figura wrote:
This test doesn't make any sense. If it's that the type we return is correct except for the bit rate, then let's just put a todo_wine on that statement. Or, if we want to be a bit stronger, we can do something like
/* We don't return the correct bit rate, but we still want to make sure * that regressions in other fields aren't hidden by a todo_wine. */ todo_wine ok(mt->dwBitRate == expect->dwBitRate); mt->dwBitRate = expect->dwBitRate;
Problem is that statement is a memcmp of the entire pbFormat, and it's inside compare_media_types(), inside test_stream_output_type().
But yes, those variables aren't beautiful either. Sure, let's do it.
On Fri Dec 1 19:52:48 2023 +0000, Zebediah Figura wrote:
We have a winetest_push_context() that takes care of the prefix. See its use in e.g. test_async_reader_types().
Yep, clear improvement. (Removing them also showed that I had a few audio_idx prints that should've been video_idx.)
On Fri Dec 1 19:54:27 2023 +0000, Zebediah Figura wrote:
Style nitpick, opening braces always go on their own line, to match vertically with the closing brace.
Can do
On Fri Dec 1 19:47:12 2023 +0000, Zebediah Figura wrote:
Style nitpick, but let's please put all the variable declarations together and separated from all the code.
Deleted some of them instead. Sure, can merge the others.