diff --git a/libavcodec/avcodec.c b/libavcodec/avcodec.c
index 6365ab87a6..a6ddc1c4e9 100644
--- a/libavcodec/avcodec.c
+++ b/libavcodec/avcodec.c
@@ -460,6 +460,7 @@ av_cold int avcodec_close(AVCodecContext *avctx)
         av_frame_free(&avci->recon_frame);
 
         ff_refstruct_unref(&avci->pool);
+        ff_refstruct_pool_uninit(&avci->progress_frame_pool);
 
         ff_hwaccel_uninit(avctx);
 
diff --git a/libavcodec/codec_internal.h b/libavcodec/codec_internal.h
index 130a7dc3cd..c03b64ceba 100644
--- a/libavcodec/codec_internal.h
+++ b/libavcodec/codec_internal.h
@@ -62,6 +62,10 @@
  * Codec initializes slice-based threading with a main function
  */
 #define FF_CODEC_CAP_SLICE_THREAD_HAS_MF    (1 << 5)
+/**
+ * The decoder might make use of the ProgressFrame API.
+ */
+#define FF_CODEC_CAP_USES_PROGRESSFRAMES    (1 << 11)
 /*
  * The codec supports frame threading and has inter-frame dependencies, so it
  * uses ff_thread_report/await_progress().
diff --git a/libavcodec/decode.c b/libavcodec/decode.c
index 7abfe7f0ce..3f98eb0df4 100644
--- a/libavcodec/decode.c
+++ b/libavcodec/decode.c
@@ -46,6 +46,8 @@
 #include "hwconfig.h"
 #include "internal.h"
 #include "packet_internal.h"
+#include "progressframe.h"
+#include "progressframe_internal.h"
 #include "refstruct.h"
 #include "thread.h"
 
@@ -1691,6 +1693,111 @@ int ff_reget_buffer(AVCodecContext *avctx, AVFrame *frame, int flags)
     return ret;
 }
 
+static void check_progress_consistency(const ProgressFrame *f)
+{
+    av_assert1(!!f->f == !!f->progress);
+    av_assert1(!f->progress || f->progress->f == f->f);
+}
+
+static int thread_progress_get(AVCodecContext *avctx, ProgressFrame *f)
+{
+    FFRefStructPool *pool = avctx->internal->progress_frame_pool;
+
+    av_assert1(!f->f && !f->progress);
+
+    f->progress = ff_refstruct_pool_get(pool);
+    if (!f->progress)
+        return AVERROR(ENOMEM);
+
+    if (avctx->active_thread_type & FF_THREAD_FRAME) {
+        f->progress->owner = avctx->internal->thread_ctx;
+        atomic_init(&f->progress->progress, -1);
+    } else
+        f->progress->owner = NULL;
+    f->f = f->progress->f;
+    return 0;
+}
+
+int ff_thread_progress_get_buffer(AVCodecContext *avctx, ProgressFrame *f, int flags)
+{
+    int ret;
+
+    ret = thread_progress_get(avctx, f);
+    if (ret < 0)
+        return ret;
+
+    ret = ff_thread_get_buffer(avctx, f->progress->f, flags);
+    if (ret < 0) {
+        f->f = NULL;
+        ff_refstruct_unref_ext(avctx, &f->progress);
+        return ret;
+    }
+    return 0;
+}
+
+void ff_thread_progress_ref(ProgressFrame *dst, const ProgressFrame *src)
+{
+    av_assert1(src->progress && src->f && src->f == src->progress->f);
+    av_assert1(!dst->f && !dst->progress);
+    dst->f = src->f;
+    dst->progress = ff_refstruct_ref(src->progress);
+}
+
+void ff_thread_progress_unref(AVCodecContext *avctx, ProgressFrame *f)
+{
+    check_progress_consistency(f);
+    f->f = NULL;
+    ff_refstruct_unref_ext(avctx, &f->progress);
+}
+
+void ff_thread_progress_replace(AVCodecContext *avctx,
+                                ProgressFrame *dst, const ProgressFrame *src)
+{
+    if (dst == src)
+        return;
+    ff_thread_progress_unref(avctx, dst);
+    check_progress_consistency(src);
+    if (src->f)
+        ff_thread_progress_ref(dst, src);
+}
+
+#if !HAVE_THREADS
+void ff_thread_progress_report(ProgressFrame *f, int n)
+{
+}
+
+void ff_thread_progress_await(const ProgressFrame *f, int n)
+{
+}
+#endif /* !HAVE_THREADS */
+
+static int progress_frame_pool_init_cb(FFRefStructOpaque unused, void *obj)
+{
+    ProgressInternal *progress = obj;
+
+    progress->f = av_frame_alloc();
+    if (!progress->f)
+        return AVERROR(ENOMEM);
+    return 0;
+}
+
+static void progress_frame_pool_reset_cb(FFRefStructOpaque dynamic_opaque,
+                                         FFRefStructOpaque unused,
+                                         void *obj)
+{
+    AVCodecContext *avctx = dynamic_opaque.nc;
+    ProgressInternal *progress = obj;
+
+    ff_thread_release_buffer(avctx, progress->f);
+}
+
+static void progress_frame_pool_free_entry_cb(FFRefStructOpaque opaque, void *obj)
+{
+    ProgressInternal *progress = obj;
+
+    av_frame_free(&progress->f);
+}
+
 int ff_decode_preinit(AVCodecContext *avctx)
 {
     AVCodecInternal *avci = avctx->internal;
@@ -1759,6 +1866,17 @@ int ff_decode_preinit(AVCodecContext *avctx)
     if (!avci->in_pkt || !avci->last_pkt_props)
         return AVERROR(ENOMEM);
 
+    if (ffcodec(avctx->codec)->caps_internal & FF_CODEC_CAP_USES_PROGRESSFRAMES) {
+        avci->progress_frame_pool =
+            ff_refstruct_pool_alloc_ext_c(sizeof(ProgressInternal),
+                                          FF_REFSTRUCT_POOL_FLAG_DYNAMIC_OPAQUE,
+                                          (FFRefStructOpaque){ NULL },
+                                          progress_frame_pool_init_cb,
+                                          (FFRefStructUnrefCB){ .unref_ext = progress_frame_pool_reset_cb },
+                                          progress_frame_pool_free_entry_cb, NULL);
+        if (!avci->progress_frame_pool)
+            return AVERROR(ENOMEM);
+    }
     ret = decode_bsfs_init(avctx);
     if (ret < 0)
         return ret;
diff --git a/libavcodec/internal.h b/libavcodec/internal.h
index eb9e0d707c..04d5bbb6fc 100644
--- a/libavcodec/internal.h
+++ b/libavcodec/internal.h
@@ -64,6 +64,8 @@ typedef struct AVCodecInternal {
 
     struct FramePool *pool;
 
+    struct FFRefStructPool *progress_frame_pool;
+
     void *thread_ctx;
 
     /**
diff --git a/libavcodec/progressframe.h b/libavcodec/progressframe.h
new file mode 100644
index 0000000000..bc1d8462fa
--- /dev/null
+++ b/libavcodec/progressframe.h
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2022 Andreas Rheinhardt <andreas.rheinhardt@outlook.com>
+ *
+ * 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
+ */
+
+#ifndef AVCODEC_PROGRESSFRAME_H
+#define AVCODEC_PROGRESSFRAME_H
+
+/**
+ * ProgressFrame is an API to easily share frames without an underlying
+ * av_frame_ref(). Its main usecase is in frame-threading scenarios,
+ * yet it could also be used for purely single-threaded decoders that
+ * want to keep multiple references to the same frame.
+ *
+ * The underlying principle behind the API is that all that is needed
+ * to share a frame is a reference count and a contract between all parties.
+ * The ProgressFrame provides the reference count and the frame is unreferenced
+ * via ff_thread_release_buffer() when the reference count reaches zero.
+ *
+ * In order to make this API also usable for frame-threaded decoders it also
+ * provides a way of exchanging simple information about the state of
+ * decoding the frame via ff_thread_progress_report() and
+ * ff_thread_progress_await().
+ *
+ * The typical contract for frame-threaded decoders is as follows:
+ * Thread A initializes a ProgressFrame via ff_thread_progress_get_buffer()
+ * (which already allocates the AVFrame's data buffers), calls
+ * ff_thread_finish_setup() and starts decoding the frame. Later threads
+ * receive a reference to this frame, which means they get a pointer
+ * to the AVFrame and the internal reference count gets incremented.
+ * Later threads whose frames use A's frame as reference as well as
+ * the thread that will eventually output A's frame will wait for
+ * progress on said frame reported by A. As soon as A has reported
+ * that it has finished decoding its frame, it must no longer modify it
+ * (neither its data nor its properties).
+ *
+ * Because creating a reference with this API does not involve reads
+ * from the actual AVFrame, the decoding thread may modify the properties
+ * (i.e. non-data fields) until it has indicated to be done with this
+ * frame. This is important for e.g. propagating decode_error_flags;
+ * it also allows to add side-data late.
+ */
+
+struct AVCodecContext;
+
+typedef struct ProgressFrame {
+    struct AVFrame *f;
+    struct ProgressInternal *progress;
+} ProgressFrame;
+
+/**
+ * Notify later decoding threads when part of their reference picture is ready.
+ * Call this when some part of the picture is finished decoding.
+ * Later calls with lower values of progress have no effect.
+ *
+ * @param f The picture being decoded.
+ * @param progress Value, in arbitrary units, of how much of the picture has decoded.
+ *
+ * @warning Calling this on a blank ProgressFrame causes undefined behaviour
+ */
+void ff_thread_progress_report(ProgressFrame *f, int progress);
+
+/**
+ * Wait for earlier decoding threads to finish reference pictures.
+ * Call this before accessing some part of a picture, with a given
+ * value for progress, and it will return after the responsible decoding
+ * thread calls ff_thread_progress_report() with the same or
+ * higher value for progress.
+ *
+ * @param f The picture being referenced.
+ * @param progress Value, in arbitrary units, to wait for.
+ *
+ * @warning Calling this on a blank ProgressFrame causes undefined behaviour
+ */
+void ff_thread_progress_await(const ProgressFrame *f, int progress);
+
+/**
+ * This function sets up the ProgressFrame, i.e. gets ProgressFrame.f
+ * and also calls ff_thread_get_buffer() on the frame.
+ *
+ * @note: This must only be called by codecs with the
+ *        FF_CODEC_CAP_USES_PROGRESSFRAMES internal cap.
+ */
+int ff_thread_progress_get_buffer(struct AVCodecContext *avctx,
+                                  ProgressFrame *f, int flags);
+
+/**
+ * Give up a reference to the underlying frame contained in a ProgressFrame
+ * and reset the ProgressFrame, setting all pointers to NULL.
+ *
+ * @note: This implies that when using this API the check for whether
+ *        a frame exists is by checking ProgressFrame.f and not
+ *        ProgressFrame.f->data[0] or ProgressFrame.f->buf[0].
+ */
+void ff_thread_progress_unref(struct AVCodecContext *avctx, ProgressFrame *f);
+
+/**
+ * Set dst.f to src.f and make dst a co-owner of src.f.
+ * dst can then be used to wait on progress of the underlying frame.
+ *
+ * @note: There is no underlying av_frame_ref() here. dst.f and src.f
+ *        really point to the same AVFrame. Typically this means that
+ *        the decoding thread is allowed to set all the properties of
+ *        the AVFrame until it has indicated to have finished decoding.
+ *        Afterwards later threads may read all of these fields.
+ *        Access to the frame's data is governed by
+ *        ff_thread_progress_report/await().
+ */
+void ff_thread_progress_ref(ProgressFrame *dst, const ProgressFrame *src);
+
+/**
+ * Do nothing if dst and src already refer to the same AVFrame;
+ * otherwise unreference dst and if src is not blank, put a reference
+ * to src's AVFrame in its place (in case src is not blank).
+ */
+void ff_thread_progress_replace(struct AVCodecContext *avctx,
+                                ProgressFrame *dst, const ProgressFrame *src);
+
+#endif /* AVCODEC_PROGRESSFRAME_H */
diff --git a/libavcodec/progressframe_internal.h b/libavcodec/progressframe_internal.h
new file mode 100644
index 0000000000..1101207106
--- /dev/null
+++ b/libavcodec/progressframe_internal.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022 Andreas Rheinhardt <andreas.rheinhardt@outlook.com>
+ *
+ * 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
+ */
+
+#ifndef AVCODEC_PROGRESSFRAME_INTERNAL_H
+#define AVCODEC_PROGRESSFRAME_INTERNAL_H
+
+#include <stdatomic.h>
+
+typedef struct ProgressInternal {
+    atomic_int progress;
+    struct PerThreadContext *owner; ///< != NULL iff frame-threading is in use
+    struct AVFrame *f;
+} ProgressInternal;
+
+#endif /* AVCODEC_PROGRESSFRAME_INTERNAL_H */
diff --git a/libavcodec/pthread_frame.c b/libavcodec/pthread_frame.c
index 282f3fad58..9e827f0606 100644
--- a/libavcodec/pthread_frame.c
+++ b/libavcodec/pthread_frame.c
@@ -34,6 +34,8 @@
 #include "hwaccel_internal.h"
 #include "hwconfig.h"
 #include "internal.h"
+#include "progressframe.h"
+#include "progressframe_internal.h"
 #include "pthread_internal.h"
 #include "refstruct.h"
 #include "thread.h"
@@ -795,6 +797,7 @@ static av_cold int init_thread(PerThreadContext *p, int *threads_to_free,
     if (!copy->internal)
         return AVERROR(ENOMEM);
     copy->internal->thread_ctx = p;
+    copy->internal->progress_frame_pool = avctx->internal->progress_frame_pool;
 
     copy->delay = avctx->delay;
 
@@ -1026,3 +1029,43 @@ void ff_thread_release_ext_buffer(AVCodecContext *avctx, ThreadFrame *f)
     f->owner[0] = f->owner[1] = NULL;
     ff_thread_release_buffer(avctx, f->f);
 }
+
+void ff_thread_progress_report(ProgressFrame *f, int n)
+{
+    ProgressInternal *pro = f->progress;
+    PerThreadContext *p = pro->owner;
+
+    if (!p ||
+        atomic_load_explicit(&pro->progress, memory_order_relaxed) >= n)
+        return;
+
+    if (atomic_load_explicit(&p->debug_threads, memory_order_relaxed))
+        av_log(p->avctx, AV_LOG_DEBUG,
+               "%p finished %d\n", (void*)pro, n);
+
+    pthread_mutex_lock(&p->progress_mutex);
+
+    atomic_store_explicit(&pro->progress, n, memory_order_release);
+
+    pthread_cond_broadcast(&p->progress_cond);
+    pthread_mutex_unlock(&p->progress_mutex);
+}
+
+void ff_thread_progress_await(const ProgressFrame *f, int n)
+{
+    ProgressInternal *pro = f->progress;
+    PerThreadContext *p = pro->owner;
+
+    if (!p ||
+        atomic_load_explicit(&pro->progress, memory_order_acquire) >= n)
+        return;
+
+    if (atomic_load_explicit(&p->debug_threads, memory_order_relaxed))
+        av_log(p->avctx, AV_LOG_DEBUG,
+               "thread awaiting %d from %p\n", n, (void*)pro);
+
+    pthread_mutex_lock(&p->progress_mutex);
+    while (atomic_load_explicit(&pro->progress, memory_order_relaxed) < n)
+        pthread_cond_wait(&p->progress_cond, &p->progress_mutex);
+    pthread_mutex_unlock(&p->progress_mutex);
+}
diff --git a/libavcodec/tests/avcodec.c b/libavcodec/tests/avcodec.c
index 08ca507bf0..f6e394c78d 100644
--- a/libavcodec/tests/avcodec.c
+++ b/libavcodec/tests/avcodec.c
@@ -145,7 +145,8 @@ int main(void){
                                         FF_CODEC_CAP_SETS_PKT_DTS |
                                         FF_CODEC_CAP_SKIP_FRAME_FILL_PARAM |
                                         FF_CODEC_CAP_EXPORTS_CROPPING |
-                                        FF_CODEC_CAP_SETS_FRAME_PROPS) ||
+                                        FF_CODEC_CAP_SETS_FRAME_PROPS |
+                                        FF_CODEC_CAP_USES_PROGRESSFRAMES) ||
                 codec->capabilities  & (AV_CODEC_CAP_AVOID_PROBING |
                                         AV_CODEC_CAP_CHANNEL_CONF  |
                                         AV_CODEC_CAP_DRAW_HORIZ_BAND))
