diff --git a/libavformat/Makefile b/libavformat/Makefile
index 6566e40cac..0661432e29 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -244,6 +244,7 @@ OBJS-$(CONFIG_HEVC_MUXER)                += rawenc.o
 OBJS-$(CONFIG_HLS_DEMUXER)               += hls.o hls_sample_encryption.o
 OBJS-$(CONFIG_HLS_MUXER)                 += hlsenc.o hlsplaylist.o avc.o
 OBJS-$(CONFIG_HNM_DEMUXER)               += hnm.o
+OBJS-$(CONFIG_HVQM4_DEMUXER)             += hvqm4dec.o
 OBJS-$(CONFIG_ICO_DEMUXER)               += icodec.o
 OBJS-$(CONFIG_ICO_MUXER)                 += icoenc.o
 OBJS-$(CONFIG_IDCIN_DEMUXER)             += idcin.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index d066a7745b..2b809d8adc 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -199,6 +199,7 @@ extern const AVOutputFormat ff_hevc_muxer;
 extern const AVInputFormat  ff_hls_demuxer;
 extern const AVOutputFormat ff_hls_muxer;
 extern const AVInputFormat  ff_hnm_demuxer;
+extern const AVInputFormat  ff_hvqm4_demuxer;
 extern const AVInputFormat  ff_ico_demuxer;
 extern const AVOutputFormat ff_ico_muxer;
 extern const AVInputFormat  ff_idcin_demuxer;
diff --git a/libavformat/hvqm4dec.c b/libavformat/hvqm4dec.c
new file mode 100644
index 0000000000..9d51947f4e
--- /dev/null
+++ b/libavformat/hvqm4dec.c
@@ -0,0 +1,265 @@
+/*
+ * HVQM4 demuxer
+ * Copyright (c) 2021-2022 Paul B Mahol
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "libavutil/intreadwrite.h"
+
+#include "avformat.h"
+#include "internal.h"
+
+typedef struct HVQM4Context {
+    uint32_t nb_blocks;
+    uint32_t current_block;
+    uint32_t current_block_size;
+    int64_t previous_block_size;
+    int64_t current_block_offset;
+    int64_t pos;
+    int64_t pts;
+} HVQM4Context;
+
+static int hvqm4_probe(const AVProbeData *p)
+{
+    if (memcmp(p->buf, "HVQM4 1.3", 9) &&
+        memcmp(p->buf, "HVQM4 1.5", 9))
+        return 0;
+
+    return AVPROBE_SCORE_MAX;
+}
+
+static int hvqm4_read_header(AVFormatContext *s)
+{
+    HVQM4Context *hvqm4 = s->priv_data;
+    AVIOContext *pb = s->pb;
+    AVStream *vst, *ast;
+    uint32_t header_size, usec_per_frame;
+    uint32_t video_frames, audio_frames;
+    int audio_format;
+
+    vst = avformat_new_stream(s, NULL);
+    if (!vst)
+        return AVERROR(ENOMEM);
+
+    avio_skip(pb, 16);
+
+    header_size = avio_rb32(pb);
+    avio_skip(pb, 4);
+    hvqm4->nb_blocks = avio_rb32(pb);
+    video_frames = avio_rb32(pb);
+    audio_frames = avio_rb32(pb);
+    usec_per_frame = avio_rb32(pb);
+    avio_skip(pb, 12);
+
+    vst->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+    vst->codecpar->codec_id   = AV_CODEC_ID_HVQM4;
+    vst->codecpar->width      = avio_rb16(pb);
+    vst->codecpar->height     = avio_rb16(pb);
+    vst->start_time           = 0;
+    vst->duration             =
+    vst->nb_frames            = video_frames;
+
+    avio_skip(pb, 2);
+    avio_skip(pb, 2);
+
+    avpriv_set_pts_info(vst, 64, usec_per_frame, 1000000);
+
+    ast = avformat_new_stream(s, NULL);
+    if (!ast)
+        return AVERROR(ENOMEM);
+
+    ast->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
+    ast->codecpar->channels   = avio_r8(pb);
+    ast->codecpar->bits_per_coded_sample = avio_r8(pb);
+    ast->nb_frames            = audio_frames;
+    ast->start_time           = 0;
+    audio_format              = avio_r8(pb);
+    switch (audio_format) {
+    case 0:
+        ast->codecpar->codec_id = AV_CODEC_ID_ADPCM_IMA_HVQM4;
+        break;
+    case 1:
+        ast->codecpar->codec_id = AV_CODEC_ID_PCM_S16LE;
+        break;
+    }
+    avio_skip(pb, 1);
+    ast->codecpar->sample_rate = avio_rb32(pb);
+
+    avpriv_set_pts_info(ast, 64, 1, ast->codecpar->sample_rate);
+
+    avio_skip(pb, header_size - avio_tell(pb));
+
+    return 0;
+}
+
+static int hvqm4_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    HVQM4Context *hvqm4 = s->priv_data;
+    int media_type, frame_type, ret;
+    AVStream *vst = s->streams[0];
+    AVIOContext *pb = s->pb;
+    int32_t size;
+    int64_t pos;
+
+    if (avio_feof(pb))
+        return AVERROR_EOF;
+
+    hvqm4->pos = pos = avio_tell(pb);
+
+    if (hvqm4->current_block_offset >= hvqm4->current_block_size) {
+        if (hvqm4->current_block_size)
+            hvqm4->current_block++;
+        hvqm4->previous_block_size = avio_rb32(pb);
+        hvqm4->current_block_size = avio_rb32(pb);
+        hvqm4->current_block_offset = 0;
+        avio_skip(pb, 8);
+        avio_skip(pb, 4);
+    }
+
+    media_type = avio_rb16(pb);
+    frame_type = avio_rb16(pb);
+    size = avio_rb32(pb);
+    ret = av_new_packet(pkt, size + 2);
+    if (ret < 0)
+        return ret;
+
+    AV_WB16(pkt->data, frame_type);
+    ret = avio_read(pb, pkt->data + 2, size);
+    if (ret < 0)
+        return ret;
+
+    pkt->pos = pos;
+    pkt->stream_index = media_type ? 0 : 1;
+    if (media_type == 1 && pkt->size >= 6)
+        pkt->pts = hvqm4->pts = av_rescale(hvqm4->current_block, vst->time_base.den, vst->time_base.num) + AV_RB32(pkt->data + 2);
+    if ((frame_type == 0x10 && media_type == 1) ||
+        media_type == 0)
+        pkt->flags |= AV_PKT_FLAG_KEY;
+    if (media_type == 1)
+        pkt->duration = 1;
+    else if (pkt->size >= 6)
+        pkt->duration = AV_RB32(pkt->data + 2);
+    hvqm4->current_block_offset += avio_tell(pb) - pos;
+
+    return ret;
+}
+
+static int hvqm4_read_seek(AVFormatContext *s, int stream_index,
+                           int64_t timestamp, int flags)
+{
+    HVQM4Context *hvqm4 = s->priv_data;
+    AVStream *vst = s->streams[0];
+    AVIOContext *pb = s->pb;
+    uint32_t new_current_block, current_block;
+    uint32_t new_current_block_size, current_block_size;
+    int64_t new_current_block_offset, current_block_offset;
+    int64_t previous_block_size;
+    int64_t pos, new_pts, pts;
+
+    if (stream_index != 0)
+        return -1;
+
+    timestamp = av_clip64(timestamp, 0, vst->nb_frames);
+
+    current_block = new_current_block = hvqm4->current_block;
+    current_block_size = new_current_block_size = hvqm4->current_block_size;
+    previous_block_size = hvqm4->previous_block_size;
+    current_block_offset = new_current_block_offset = hvqm4->current_block_offset;
+
+    pts = new_pts = hvqm4->pts;
+    if (pts < timestamp) {
+        while (pts < timestamp) {
+            int media_type;
+            uint32_t size;
+
+            if (avio_feof(pb))
+                return -1;
+
+            new_current_block = current_block;
+            new_current_block_size = current_block_size;
+            new_current_block_offset = current_block_offset;
+            new_pts = pts;
+
+            pos = avio_tell(pb);
+            if (current_block_offset >= current_block_size) {
+                if (current_block_size)
+                    current_block++;
+                previous_block_size = avio_rb32(pb);
+                current_block_size = avio_rb32(pb);
+                current_block_offset = 0;
+                avio_skip(pb, 8);
+                avio_skip(pb, 4);
+                pts = av_rescale(current_block, vst->time_base.den, vst->time_base.num);
+            }
+
+            media_type = avio_rb16(pb);
+            avio_skip(pb, 2);
+            if (pts >= timestamp && media_type == 1)
+                break;
+            size = avio_rb32(pb);
+            avio_skip(pb, size);
+            current_block_offset += avio_tell(pb) - pos;
+            pts += media_type == 1;
+        }
+    } else {
+        while (pts > timestamp) {
+            pos = avio_tell(pb);
+
+            if (pos > 0 && current_block > 0) {
+                current_block--;
+                if (avio_seek(pb, -previous_block_size - current_block_offset, SEEK_CUR) < 0)
+                    return -1;
+
+                pos = avio_tell(pb);
+                previous_block_size = avio_rb32(pb);
+                current_block_offset = 4;
+            }
+
+            pts = av_rescale(current_block, vst->time_base.den, vst->time_base.num);
+            new_current_block = current_block;
+            new_current_block_size = 0;
+            new_current_block_offset = 0;
+            new_pts = pts;
+
+            if (pts <= timestamp)
+                break;
+        }
+    }
+
+    if (avio_seek(pb, pos, SEEK_SET) < 0)
+        return -1;
+
+    hvqm4->current_block = new_current_block;
+    hvqm4->current_block_size = new_current_block_size;
+    hvqm4->current_block_offset = new_current_block_offset;
+    hvqm4->previous_block_size = previous_block_size;
+    hvqm4->pts = new_pts;
+
+    return 0;
+}
+
+const AVInputFormat ff_hvqm4_demuxer = {
+    .name           = "hvqm4",
+    .long_name      = NULL_IF_CONFIG_SMALL("HVQM4"),
+    .priv_data_size = sizeof(HVQM4Context),
+    .read_probe     = hvqm4_probe,
+    .read_header    = hvqm4_read_header,
+    .read_packet    = hvqm4_read_packet,
+    .read_seek      = hvqm4_read_seek,
+    .extensions     = "h4m",
+};
