diff mbox series

[FFmpeg-devel,1/2] avdevice/dshow: implement capabilities API

Message ID 20210603223302.1047-3-dcnieho@gmail.com
State Withdrawn, archived
Headers show
Series [FFmpeg-devel,1/2] avdevice/dshow: implement capabilities API | expand

Checks

Context Check Description
andriy/x86_make success Make finished
andriy/x86_make_fate success Make fate finished
andriy/PPC64_make success Make finished
andriy/PPC64_make_fate success Make fate finished

Commit Message

Diederick C. Niehorster June 3, 2021, 10:32 p.m. UTC
This implements avdevice_capabilities_create and avdevice_capabilities_free for the dshow device.

Signed-off-by: Diederick Niehorster <dcnieho@gmail.com>
---
 libavdevice/dshow.c | 498 ++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 462 insertions(+), 36 deletions(-)
diff mbox series

Patch

diff --git a/libavdevice/dshow.c b/libavdevice/dshow.c
index 8d0a6fcc09..c65492119e 100644
--- a/libavdevice/dshow.c
+++ b/libavdevice/dshow.c
@@ -30,6 +30,48 @@ 
 #include "objidl.h"
 #include "shlwapi.h"
 
+enum DshowCapQueryType {
+    CAP_QUERY_NONE = 0,
+    CAP_QUERY_SAMPLE_FORMAT,
+    CAP_QUERY_SAMPLE_RATE,
+    CAP_QUERY_CHANNELS,
+    CAP_QUERY_CODEC,
+    CAP_QUERY_PIXEL_FORMAT,
+    CAP_QUERY_FRAME_SIZE,
+    CAP_QUERY_FPS
+};
+typedef struct DshowCapQueryTypeEntry {
+    const char*             name;
+    enum DshowCapQueryType  query_type;
+} DshowCapQueryTypeEntry;
+
+static const DshowCapQueryTypeEntry query_table[] = {
+    { "sample_format",  CAP_QUERY_SAMPLE_FORMAT },
+    { "sample_rate",    CAP_QUERY_SAMPLE_RATE },
+    { "channels",       CAP_QUERY_CHANNELS },
+    { "codec",          CAP_QUERY_CODEC },
+    { "pixel_format",   CAP_QUERY_PIXEL_FORMAT },
+    { "frame_size",     CAP_QUERY_FRAME_SIZE },
+    { "fps",            CAP_QUERY_FPS },
+};
+
+static enum DshowCapQueryType dshow_get_query_type(const char* option_name)
+{
+    for (int i = 0; i < FF_ARRAY_ELEMS(query_table); ++i) {
+        if (!strcmp(query_table[i].name, option_name))
+            return query_table[i].query_type;
+    }
+    return CAP_QUERY_NONE;
+}
+
+static const char* dshow_get_query_type_string(enum DshowCapQueryType query_type)
+{
+    for (int i = 0; i < FF_ARRAY_ELEMS(query_table); ++i) {
+        if (query_table[i].query_type == query_type)
+            return query_table[i].name;
+    }
+    return NULL;
+}
 
 static enum AVPixelFormat dshow_pixfmt(DWORD biCompression, WORD biBitCount)
 {
@@ -54,6 +96,26 @@  static enum AVPixelFormat dshow_pixfmt(DWORD biCompression, WORD biBitCount)
     return avpriv_find_pix_fmt(avpriv_get_raw_pix_fmt_tags(), biCompression); // all others
 }
 
+static enum AVCodecID waveform_codec_id(enum AVSampleFormat sample_fmt)
+{
+    switch (sample_fmt) {
+    case AV_SAMPLE_FMT_U8:  return AV_CODEC_ID_PCM_U8;
+    case AV_SAMPLE_FMT_S16: return AV_CODEC_ID_PCM_S16LE;
+    case AV_SAMPLE_FMT_S32: return AV_CODEC_ID_PCM_S32LE;
+    default:                return AV_CODEC_ID_NONE; /* Should never happen. */
+    }
+}
+
+static enum AVSampleFormat sample_fmt_bits_per_sample(int bits)
+{
+    switch (bits) {
+    case 8:  return AV_SAMPLE_FMT_U8;
+    case 16: return AV_SAMPLE_FMT_S16;
+    case 32: return AV_SAMPLE_FMT_S32;
+    default: return AV_SAMPLE_FMT_NONE; /* Should never happen. */
+    }
+}
+
 static int
 dshow_read_close(AVFormatContext *s)
 {
@@ -317,10 +379,13 @@  fail1:
  * try to set parameters specified through AVOptions and if successful
  * return 1 in *pformat_set.
  * If pformat_set is NULL, list all pin capabilities.
+ * When listing pin capabilities, if ranges is NULL, output to log,
+ * else store capabilities in ranges.
  */
 static void
 dshow_cycle_formats(AVFormatContext *avctx, enum dshowDeviceType devtype,
-                    IPin *pin, int *pformat_set)
+                    IPin *pin, int *pformat_set,
+                    AVOptionRanges *ranges, enum DshowCapQueryType query_type)
 {
     struct dshow_ctx *ctx = avctx->priv_data;
     IAMStreamConfig *config = NULL;
@@ -338,7 +403,11 @@  dshow_cycle_formats(AVFormatContext *avctx, enum dshowDeviceType devtype,
     if (!caps)
         goto end;
 
-    for (i = 0; i < n && !format_set; i++) {
+    for (i = 0; i < n && (!format_set || ranges); i++) {
+        AVOptionRange *range = NULL;
+        AVOptionRange **range_list = NULL;
+        int nb_range = 0;
+
         r = IAMStreamConfig_GetStreamCaps(config, i, &type, (void *) caps);
         if (r != S_OK)
             goto next;
@@ -365,7 +434,7 @@  dshow_cycle_formats(AVFormatContext *avctx, enum dshowDeviceType devtype,
             } else {
                 goto next;
             }
-            if (!pformat_set) {
+            if (!pformat_set && !ranges) {
                 enum AVPixelFormat pix_fmt = dshow_pixfmt(bih->biCompression, bih->biBitCount);
                 if (pix_fmt == AV_PIX_FMT_NONE) {
                     enum AVCodecID codec_id = av_codec_get_id(tags, bih->biCompression);
@@ -410,6 +479,81 @@  dshow_cycle_formats(AVFormatContext *avctx, enum dshowDeviceType devtype,
                 bih->biWidth  = ctx->requested_width;
                 bih->biHeight = ctx->requested_height;
             }
+
+            if (ranges) {
+                if (query_type == CAP_QUERY_FRAME_SIZE) {
+                    for (int j = 0; j < 3; j++) {
+                        range = av_mallocz(sizeof(AVOptionRange));
+                        if (!range)
+                            goto next;
+                        range->str = av_strdup(j == 0 ? "pixel_count" : (j == 1 ? "width" : (j == 2 ? "height" : "")));
+                        if (!range->str)
+                            goto next;
+                        switch (j)
+                        {
+                        case 0:
+                            range->value_min = vcaps->MinOutputSize.cx * vcaps->MinOutputSize.cy;
+                            range->value_max = vcaps->MaxOutputSize.cx * vcaps->MaxOutputSize.cy;
+                            break;
+                        case 1:
+                            range->value_min = vcaps->MinOutputSize.cx;
+                            range->value_max = vcaps->MaxOutputSize.cx;
+                            break;
+                        case 2:
+                            range->value_min = vcaps->MinOutputSize.cy;
+                            range->value_max = vcaps->MaxOutputSize.cy;
+                            break;
+                        }
+                        range->is_range = range->value_min != range->value_max;
+
+                        if (av_dynarray_add_nofree(&range_list,
+                                                   &nb_range, range) < 0)
+                            goto next;
+                        range = NULL;  // copied into array, make sure not freed below
+                    }
+                }
+                else {
+                    range = av_mallocz(sizeof(AVOptionRange));
+                    if (!range)
+                        goto next;
+                    range->str = av_strdup(dshow_get_query_type_string(query_type));
+                    if (!range->str)
+                        goto next;
+
+                    switch (query_type)
+                    {
+                    case CAP_QUERY_CODEC:
+                        if (dshow_pixfmt(bih->biCompression, bih->biBitCount) == AV_PIX_FMT_NONE) {
+                            const AVCodecTag* const tags[] = { avformat_get_riff_video_tags(), NULL };
+                            range->value_min = av_codec_get_id(tags, bih->biCompression);
+                        }
+                        else
+                            range->value_min = AV_CODEC_ID_RAWVIDEO;
+                        range->value_max = range->value_min;
+                        break;
+                    case CAP_QUERY_PIXEL_FORMAT:
+                        range->value_min = range->value_max = dshow_pixfmt(bih->biCompression, bih->biBitCount);
+                        range->value_min;
+                        break;
+                    case CAP_QUERY_FPS:
+                        range->value_min = 1e7 / vcaps->MaxFrameInterval;
+                        range->value_max = 1e7 / vcaps->MinFrameInterval;
+                        break;
+
+                    default:
+                        // an audio property is being queried, output 0
+                        range->value_min = range->value_max = 0;
+                        break;
+                    }
+
+                    range->is_range = range->value_min != range->value_max;
+
+                    if (av_dynarray_add_nofree(&range_list,
+                                               &nb_range, range) < 0)
+                        goto next;
+                    range = NULL;  // copied into array, make sure not freed below
+                }
+            }
         } else {
             AUDIO_STREAM_CONFIG_CAPS *acaps = caps;
             WAVEFORMATEX *fx;
@@ -421,7 +565,7 @@  dshow_cycle_formats(AVFormatContext *avctx, enum dshowDeviceType devtype,
             } else {
                 goto next;
             }
-            if (!pformat_set) {
+            if (!pformat_set && !ranges) {
                 av_log(avctx, AV_LOG_INFO, "  min ch=%lu bits=%lu rate=%6lu max ch=%lu bits=%lu rate=%6lu\n",
                        acaps->MinimumChannels, acaps->MinimumBitsPerSample, acaps->MinimumSampleFrequency,
                        acaps->MaximumChannels, acaps->MaximumBitsPerSample, acaps->MaximumSampleFrequency);
@@ -445,11 +589,90 @@  dshow_cycle_formats(AVFormatContext *avctx, enum dshowDeviceType devtype,
                     goto next;
                 fx->nChannels = ctx->channels;
             }
+
+            if (ranges) {
+                if (query_type == CAP_QUERY_FRAME_SIZE) {
+                    for (int j = 0; j < 3; j++) {
+                        range = av_mallocz(sizeof(AVOptionRange));
+                        if (!range)
+                            goto next;
+                        range->str = av_strdup(j == 0 ? "pixel_count" : (j == 1 ? "width" : (j == 2 ? "height" : "")));
+                        if (!range->str)
+                            goto next;
+                        // an video property is being queried, output 0
+                        range->value_min = range->value_max = 0;
+                        range->is_range = 0;
+
+                        if (av_dynarray_add_nofree(&range_list,
+                                                   &nb_range, range) < 0)
+                            goto next;
+                        range = NULL;  // copied into array, make sure not freed below
+                    }
+                }
+                else {
+                    range = av_mallocz(sizeof(AVOptionRange));
+                    if (!range)
+                        goto next;
+                    range->str = av_strdup(dshow_get_query_type_string(query_type));
+                    if (!range->str)
+                        goto next;
+
+                    switch (query_type)
+                    {
+                    case CAP_QUERY_SAMPLE_FORMAT:
+                        range->value_min = sample_fmt_bits_per_sample(acaps->MinimumBitsPerSample);
+                        range->value_max = sample_fmt_bits_per_sample(acaps->MaximumBitsPerSample);
+                        break;
+                    case CAP_QUERY_SAMPLE_RATE:
+                        range->value_min = acaps->MinimumSampleFrequency;
+                        range->value_max = acaps->MaximumSampleFrequency;
+                        break;
+                    case CAP_QUERY_CHANNELS:
+                        range->value_min = acaps->MinimumChannels;
+                        range->value_max = acaps->MaximumChannels;
+                        break;
+
+                    default:
+                        // an video property is being queried, output 0
+                        range->value_min = range->value_max = 0;
+                        break;
+                    }
+
+                    range->is_range = range->value_min != range->value_max;
+
+                    if (av_dynarray_add_nofree(&range_list,
+                                               &nb_range, range) < 0)
+                        goto next;
+                    range = NULL;  // copied into array, make sure not freed below
+                }
+            }
         }
         if (IAMStreamConfig_SetFormat(config, type) != S_OK)
             goto next;
+        else if (ranges) {
+            // format matched and could be set successfully. Add capabilities to ranges output
+            for (int j=0; j< nb_range; ++j) {
+                if (av_dynarray_add_nofree(&ranges->range,
+                                           &ranges->nb_ranges, range_list[j]) < 0)
+                    goto next;
+                range_list[j] = NULL;  // copied into array, make sure not freed below
+            }
+        }
         format_set = 1;
 next:
+        if (range) {
+            av_freep(&range->str);
+            av_freep(&range);
+        }
+        if (range_list) {
+            for (int j = 0; j < nb_range; ++j) {
+                if (range_list[j]) {
+                    av_freep(&range_list[j]->str);
+                    av_freep(&range_list[j]);
+                }
+            }
+            av_freep(&range_list);
+        }
         if (type->pbFormat)
             CoTaskMemFree(type->pbFormat);
         CoTaskMemFree(type);
@@ -558,10 +781,13 @@  end:
  * devtype, retrieve the first output pin and return the pointer to the
  * object found in *ppin.
  * If ppin is NULL, cycle through all pins listing audio/video capabilities.
+ * If ppin is not NULL and ranges is also not null, enumerate all formats
+ * supported by the selected pin.
  */
 static int
 dshow_cycle_pins(AVFormatContext *avctx, enum dshowDeviceType devtype,
-                 enum dshowSourceFilterType sourcetype, IBaseFilter *device_filter, IPin **ppin)
+                 enum dshowSourceFilterType sourcetype, IBaseFilter *device_filter,
+                 IPin **ppin, AVOptionRanges *ranges, enum DshowCapQueryType query_type)
 {
     struct dshow_ctx *ctx = avctx->priv_data;
     IEnumPins *pins = 0;
@@ -630,7 +856,7 @@  dshow_cycle_pins(AVFormatContext *avctx, enum dshowDeviceType devtype,
 
         if (!ppin) {
             av_log(avctx, AV_LOG_INFO, " Pin \"%s\" (alternative pin name \"%s\")\n", name_buf, pin_buf);
-            dshow_cycle_formats(avctx, devtype, pin, NULL);
+            dshow_cycle_formats(avctx, devtype, pin, NULL, NULL, CAP_QUERY_NONE);
             goto next;
         }
 
@@ -642,13 +868,13 @@  dshow_cycle_pins(AVFormatContext *avctx, enum dshowDeviceType devtype,
             }
         }
 
-        if (set_format) {
-            dshow_cycle_formats(avctx, devtype, pin, &format_set);
+        if (set_format || ranges) {
+            dshow_cycle_formats(avctx, devtype, pin, &format_set, ranges, query_type);
             if (!format_set) {
                 goto next;
             }
         }
-        if (devtype == AudioDevice && ctx->audio_buffer_size) {
+        if (devtype == AudioDevice && ctx->audio_buffer_size && !ranges) {
             if (dshow_set_audio_buffer_size(avctx, pin) < 0) {
                 av_log(avctx, AV_LOG_ERROR, "unable to set audio buffer size %d to pin, using pin anyway...", ctx->audio_buffer_size);
             }
@@ -673,8 +899,22 @@  next:
             IEnumMediaTypes_Release(types);
         if (p)
             IKsPropertySet_Release(p);
-        if (device_pin != pin)
+        if (device_pin != pin) {
             IPin_Release(pin);
+            // clear all ranges info again, wrong pin
+            if (ranges) {
+                for (int i = 0; i < ranges->nb_ranges; i++) {
+                    AVOptionRange* range = ranges->range[i];
+                    if (range) {
+                        av_freep(&range->str);
+                        av_freep(&ranges->range[i]);
+                    }
+                }
+                av_freep(&ranges->range);
+                ranges->nb_ranges = 0;
+            }
+            device_pin = NULL;
+        }
         av_free(name_buf);
         av_free(pin_buf);
         if (pin_id)
@@ -706,17 +946,19 @@  next:
  */
 static int
 dshow_list_device_options(AVFormatContext *avctx, ICreateDevEnum *devenum,
-                          enum dshowDeviceType devtype, enum dshowSourceFilterType sourcetype)
+                          enum dshowDeviceType devtype, enum dshowSourceFilterType sourcetype,
+                          AVOptionRanges *ranges, enum DshowCapQueryType query_type)
 {
     struct dshow_ctx *ctx = avctx->priv_data;
     IBaseFilter *device_filter = NULL;
+    IPin *device_pin = NULL;
     char *device_unique_name = NULL;
     int r;
 
     if ((r = dshow_cycle_devices(avctx, devenum, devtype, sourcetype, &device_filter, &device_unique_name)) < 0)
         return r;
     ctx->device_filter[devtype] = device_filter;
-    if ((r = dshow_cycle_pins(avctx, devtype, sourcetype, device_filter, NULL)) < 0)
+    if ((r = dshow_cycle_pins(avctx, devtype, sourcetype, device_filter, ranges ? &device_pin : NULL, ranges, query_type)) < 0)
         return r;
     av_freep(&device_unique_name);
     return 0;
@@ -800,7 +1042,7 @@  dshow_open_device(AVFormatContext *avctx, ICreateDevEnum *devenum,
         goto error;
     }
 
-    if ((r = dshow_cycle_pins(avctx, devtype, sourcetype, device_filter, &device_pin)) < 0) {
+    if ((r = dshow_cycle_pins(avctx, devtype, sourcetype, device_filter, &device_pin, NULL, CAP_QUERY_NONE)) < 0) {
         ret = r;
         goto error;
     }
@@ -912,26 +1154,6 @@  error:
     return ret;
 }
 
-static enum AVCodecID waveform_codec_id(enum AVSampleFormat sample_fmt)
-{
-    switch (sample_fmt) {
-    case AV_SAMPLE_FMT_U8:  return AV_CODEC_ID_PCM_U8;
-    case AV_SAMPLE_FMT_S16: return AV_CODEC_ID_PCM_S16LE;
-    case AV_SAMPLE_FMT_S32: return AV_CODEC_ID_PCM_S32LE;
-    default:                return AV_CODEC_ID_NONE; /* Should never happen. */
-    }
-}
-
-static enum AVSampleFormat sample_fmt_bits_per_sample(int bits)
-{
-    switch (bits) {
-    case 8:  return AV_SAMPLE_FMT_U8;
-    case 16: return AV_SAMPLE_FMT_S16;
-    case 32: return AV_SAMPLE_FMT_S32;
-    default: return AV_SAMPLE_FMT_NONE; /* Should never happen. */
-    }
-}
-
 static int
 dshow_add_device(AVFormatContext *avctx,
                  enum dshowDeviceType devtype)
@@ -1138,14 +1360,14 @@  static int dshow_read_header(AVFormatContext *avctx)
     }
     if (ctx->list_options) {
         if (ctx->device_name[VideoDevice])
-            if ((r = dshow_list_device_options(avctx, devenum, VideoDevice, VideoSourceDevice))) {
+            if ((r = dshow_list_device_options(avctx, devenum, VideoDevice, VideoSourceDevice, NULL, CAP_QUERY_NONE))) {
                 ret = r;
                 goto error;
             }
         if (ctx->device_name[AudioDevice]) {
-            if (dshow_list_device_options(avctx, devenum, AudioDevice, AudioSourceDevice)) {
+            if (dshow_list_device_options(avctx, devenum, AudioDevice, AudioSourceDevice, NULL, CAP_QUERY_NONE)) {
                 /* show audio options from combined video+audio sources as fallback */
-                if ((r = dshow_list_device_options(avctx, devenum, AudioDevice, VideoSourceDevice))) {
+                if ((r = dshow_list_device_options(avctx, devenum, AudioDevice, VideoSourceDevice, NULL, CAP_QUERY_NONE))) {
                     ret = r;
                     goto error;
                 }
@@ -1289,6 +1511,208 @@  static int dshow_read_packet(AVFormatContext *s, AVPacket *pkt)
     return ctx->eof ? AVERROR(EIO) : pkt->size;
 }
 
+// TODO: how to expose extra info? If no field found, check if it in a set of extra keys (like color_range)?
+static int dshow_query_ranges_func(AVOptionRanges** ranges_arg, void* obj, const char* key, int flags)
+{
+    AVDeviceCapabilitiesQuery *caps = obj;
+    const AVFormatContext *avctx = caps->device_context;
+    struct dshow_ctx *ctx = avctx->priv_data;
+
+    int backup_sample_size;
+    int backup_sample_rate;
+    int backup_channels;
+    enum AVCodecID backup_video_codec_id;
+    enum AVPixelFormat backup_pixel_format;
+    int backup_requested_width;
+    int backup_requested_height;
+    char* backup_framerate = NULL;
+
+    enum DshowCapQueryType query_type = CAP_QUERY_NONE;
+
+    AVOptionRanges *ranges = av_mallocz(sizeof(AVOptionRanges));
+    const AVOption *field = av_opt_find(obj, key, NULL, 0, flags);
+    int ret;
+
+    ICreateDevEnum *devenum = NULL;
+
+    *ranges_arg = NULL;
+
+    if (!ranges) {
+        ret = AVERROR(ENOMEM);
+        goto fail1;
+    }
+
+    if (!field) {
+        ret = AVERROR_OPTION_NOT_FOUND;
+        goto fail1;
+    }
+
+    // turn option name into cap query
+    query_type = dshow_get_query_type(field->name);
+
+    if (query_type == CAP_QUERY_NONE) {
+        av_log(avctx, AV_LOG_ERROR, "Querying the option %s is not supported for a dshow device\n",field->name);
+        ret = AVERROR(EINVAL);
+        goto fail1;
+    }
+
+    if (ctx->device_name[0]) {
+        av_log(avctx, AV_LOG_ERROR, "You cannot query device capabilities on an opened device\n");
+        ret = AVERROR(EIO);
+        goto fail1;
+    }
+
+    if (!parse_device_name(avctx)) {
+        av_log(avctx, AV_LOG_ERROR, "You must set a device name (AVFormatContext url) to specify which device to query capabilities from\n");
+        ret = AVERROR(EINVAL);
+        goto fail1;
+    }
+
+    // take backup of dshow parameters/options
+    // audio
+    backup_sample_size = ctx->sample_size;
+    backup_sample_rate = ctx->sample_rate;
+    backup_channels = ctx->channels;
+    // video
+    backup_video_codec_id = ctx->video_codec_id;
+    backup_pixel_format = ctx->pixel_format;
+    backup_requested_width = ctx->requested_width;
+    backup_requested_height= ctx->requested_height;
+    backup_framerate = ctx->framerate;
+
+
+    // set format constraints set in AVDeviceCapabilitiesQuery
+    // audio (NB: channel_layout not used)
+    ctx->sample_size = av_get_bytes_per_sample(caps->sample_format) << 3;
+    ctx->sample_rate = (caps->sample_rate == -1) ? 0 : caps->sample_rate;
+    ctx->channels = (caps->channels == -1) ? 0 : caps->channels;
+    // video (NB: window_width and window_height not used)
+    ctx->video_codec_id = caps->codec;
+    ctx->pixel_format = caps->pixel_format;
+    ctx->requested_width = caps->frame_width;
+    ctx->requested_height = caps->frame_height;
+    // checking whether requested framerate is set is done by !ctx->framerate
+    if (caps->fps.num > 0 && caps->fps.den > 0) {
+        ctx->requested_framerate = caps->fps;
+        ctx->framerate = av_strdup("dummy");    // just make sure its non-zero
+        if (!ctx->framerate) {
+            ret = AVERROR(ENOMEM);
+            goto fail2;
+        }
+    }
+    else
+        ctx->framerate = NULL;  // make sure its NULL (if it wasn't, we already have a backup of the pointer to restore later)
+
+    // now iterate matching format of pin that would be selected when device
+    // is opened with options currently in effect.
+    // for each matching format, output its parameter range, also if that same
+    // range already returned for another format. That way, user can reconstruct
+    // possible valid combinations by av_opt_query_ranges() for each of the
+    // format options and matching returned values by sequence number.
+    CoInitialize(0);
+
+    if (CoCreateInstance(&CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER,
+                         &IID_ICreateDevEnum, (void **) &devenum)) {
+        av_log(avctx, AV_LOG_ERROR, "Could not enumerate system devices.\n");
+        goto fail2;
+    }
+    ctx->video_codec_id = avctx->video_codec_id ? avctx->video_codec_id
+                                                : AV_CODEC_ID_RAWVIDEO;
+
+    ranges->nb_components = field->type == AV_OPT_TYPE_IMAGE_SIZE ? 3 : 1;
+    if (ctx->device_name[VideoDevice])
+        if ((ret = dshow_list_device_options(avctx, devenum, VideoDevice, VideoSourceDevice, ranges, query_type)) < 0)
+            goto fail2;
+    if (ctx->device_name[AudioDevice]) {
+        if (dshow_list_device_options(avctx, devenum, AudioDevice, AudioSourceDevice, ranges, query_type) < 0) {
+            /* show audio options from combined video+audio sources as fallback */
+            if ((ret = dshow_list_device_options(avctx, devenum, AudioDevice, VideoSourceDevice, ranges, query_type)) < 0)
+                goto fail2;
+        }
+    }
+    ret = ranges->nb_ranges ? ranges->nb_components : 0;
+
+    // when dealing with a multi-component item (regardless of whether
+    // AV_OPT_MULTI_COMPONENT_RANGE is set or not), we need to reorganize the
+    // output range array from [r1_c1 r1_c2 r1_c3 r2_c1 r2_c2 r2_c3 ...] to
+    // [r1_c1 r2_c1 ... r1_c2 r2_c2 ... r1_c3 r2_c3 ...] to be consistent with
+    // documentation of AVOptionRanges in libavutil/opt.h
+    // this is only the case for a AV_OPT_TYPE_IMAGE_SIZE option.
+    if (ranges->nb_ranges && ranges->nb_components>1) {
+        AVOptionRanges* old_ranges;
+        ranges->nb_ranges /= ranges->nb_components;
+        old_ranges = ranges;
+        ranges = av_mallocz(sizeof(AVOptionRanges));
+        if (!ranges) {
+            ranges = old_ranges;    // for cleanup
+            ret = AVERROR(ENOMEM);
+            goto fail2;
+        }
+        ranges->nb_components = old_ranges->nb_components;
+        ranges->nb_ranges = old_ranges->nb_ranges;
+        ranges->range = av_malloc(ranges->nb_components * ranges->nb_ranges * sizeof(AVOptionRange*));
+
+        for (int n = 0; n < ranges->nb_components * ranges->nb_ranges; n++) {
+            int i = n / ranges->nb_components;
+            int j = n % ranges->nb_components;
+            ranges->range[ranges->nb_ranges * j + i] = old_ranges->range[n];
+            old_ranges->range[n] = NULL; // don't keep double references
+        }
+
+        av_opt_freep_ranges(&old_ranges);
+    }
+
+    // success, set output
+    *ranges_arg = ranges;
+
+fail2:
+    // set dshow parameters/options back to original values
+    // audio
+    ctx->sample_size = backup_sample_size;
+    ctx->sample_rate = backup_sample_rate;
+    ctx->channels = backup_channels;
+    // video
+    ctx->video_codec_id = backup_video_codec_id;
+    ctx->pixel_format = backup_pixel_format;
+    ctx->requested_width = backup_requested_width;
+    ctx->requested_height = backup_requested_height;
+    if (ctx->framerate)
+        av_free(ctx->framerate);
+    ctx->framerate = backup_framerate;
+
+    if (devenum)
+        ICreateDevEnum_Release(devenum);
+
+    dshow_read_close(avctx); // clears filenames and removes device_filters or other state variables that may have been set
+
+fail1:
+    if (ret < 0)
+        av_opt_freep_ranges(&ranges);
+
+    return ret;
+}
+
+// fake class to point av_opt_query_ranges to our query_ranges function
+static const AVClass dshow_dev_caps_class = {
+    .class_name = "",
+    .item_name = av_default_item_name,
+    .option = av_device_capabilities,
+    .version = LIBAVDEVICE_VERSION_INT,
+    .query_ranges = dshow_query_ranges_func,
+};
+
+static int dshow_create_device_capabilities(struct AVFormatContext* s, struct AVDeviceCapabilitiesQuery* caps)
+{
+    caps->av_class = &dshow_dev_caps_class;
+    return 0;
+}
+
+static int dshow_free_device_capabilities(struct AVFormatContext* s, struct AVDeviceCapabilitiesQuery* caps)
+{
+    // nothing to clean up
+    return 0;
+}
+
 #define OFFSET(x) offsetof(struct dshow_ctx, x)
 #define DEC AV_OPT_FLAG_DECODING_PARAM
 static const AVOption options[] = {
@@ -1335,6 +1759,8 @@  const AVInputFormat ff_dshow_demuxer = {
     .read_header    = dshow_read_header,
     .read_packet    = dshow_read_packet,
     .read_close     = dshow_read_close,
+    .create_device_capabilities = dshow_create_device_capabilities,
+    .free_device_capabilities = dshow_free_device_capabilities,
     .flags          = AVFMT_NOFILE,
     .priv_class     = &dshow_class,
 };