PR #23753 opened by Michel Lespinasse (walken)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23753
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23753.patch

RTMP reconnect feature, from 
https://github.com/veovera/enhanced-rtmp/blob/main/docs/enhanced/enhanced-rtmp-v2.md



>From 77e1760a984a53b7b338f6876031d08b8d8ba222 Mon Sep 17 00:00:00 2001
From: Michel Lespinasse <[email protected]>
Date: Wed, 1 Jul 2026 13:00:05 +0200
Subject: [PATCH 1/7] avformat/rtmpproto: add internal rtmp_connect() function

rtmp_connect() handles everything previously done by rtmp_open(),
except for the cleanup done in the failed case.

This is in preparation for the RTMP reconnect feature, which will require
opening a new connection with the cleanup done differently in the failed case.
---
 libavformat/rtmpproto.c | 68 ++++++++++++++++++-----------------------
 1 file changed, 30 insertions(+), 38 deletions(-)

diff --git a/libavformat/rtmpproto.c b/libavformat/rtmpproto.c
index 785455e06d..5d92c92e06 100644
--- a/libavformat/rtmpproto.c
+++ b/libavformat/rtmpproto.c
@@ -2685,9 +2685,8 @@ static int inject_fake_duration_metadata(RTMPContext *rt)
  *             and 'playpath' is a file name (the rest of the path,
  *             may be prefixed with "mp4:")
  */
-static int rtmp_open(URLContext *s, const char *uri, int flags, AVDictionary 
**opts)
+static int rtmp_connect(URLContext *s, RTMPContext *rt, const char *uri, int 
flags, AVDictionary **opts)
 {
-    RTMPContext *rt = s->priv_data;
     char proto[8], hostname[256], path[1024], auth[100], *fname;
     char *old_app, *qmark, *n, fname_buffer[1024];
     uint8_t buf[2048];
@@ -2762,19 +2761,19 @@ reconnect:
                                     &s->interrupt_callback, opts,
                                     s->protocol_whitelist, 
s->protocol_blacklist, s)) < 0) {
         av_log(s , AV_LOG_ERROR, "Cannot open connection %s\n", buf);
-        goto fail;
+        return ret;
     }
 
     if (rt->swfverify) {
         if ((ret = rtmp_calc_swfhash(s)) < 0)
-            goto fail;
+            return ret;
     }
 
     rt->state = STATE_START;
     if (!rt->listen && (ret = rtmp_handshake(s, rt)) < 0)
-        goto fail;
+        return ret;
     if (rt->listen && (ret = rtmp_server_handshake(s, rt)) < 0)
-        goto fail;
+        return ret;
 
     rt->out_chunk_size = 128;
     rt->in_chunk_size  = 128; // Probably overwritten later
@@ -2784,10 +2783,8 @@ reconnect:
     old_app = rt->app;
 
     rt->app = av_malloc(APP_MAX_LENGTH);
-    if (!rt->app) {
-        ret = AVERROR(ENOMEM);
-        goto fail;
-    }
+    if (!rt->app)
+        return AVERROR(ENOMEM);
 
     //extract "app" part from path
     qmark = strchr(path, '?');
@@ -2834,10 +2831,8 @@ reconnect:
 
     if (old_app) {
         // The name of application has been defined by the user, override it.
-        if (strlen(old_app) >= APP_MAX_LENGTH) {
-            ret = AVERROR(EINVAL);
-            goto fail;
-        }
+        if (strlen(old_app) >= APP_MAX_LENGTH)
+            return AVERROR(EINVAL);
         av_free(rt->app);
         rt->app = old_app;
     }
@@ -2847,10 +2842,8 @@ reconnect:
         if (fname)
             max_len = strlen(fname) + 5; // add prefix "mp4:"
         rt->playpath = av_malloc(max_len);
-        if (!rt->playpath) {
-            ret = AVERROR(ENOMEM);
-            goto fail;
-        }
+        if (!rt->playpath)
+            return AVERROR(ENOMEM);
 
         if (fname) {
             int len = strlen(fname);
@@ -2871,20 +2864,16 @@ reconnect:
 
     if (!rt->tcurl) {
         rt->tcurl = av_malloc(TCURL_MAX_LENGTH);
-        if (!rt->tcurl) {
-            ret = AVERROR(ENOMEM);
-            goto fail;
-        }
+        if (!rt->tcurl)
+            return AVERROR(ENOMEM);
         ff_url_join(rt->tcurl, TCURL_MAX_LENGTH, proto, NULL, hostname,
                     port, "/%s", rt->app);
     }
 
     if (!rt->flashver) {
         rt->flashver = av_malloc(FLASHVER_MAX_LENGTH);
-        if (!rt->flashver) {
-            ret = AVERROR(ENOMEM);
-            goto fail;
-        }
+        if (!rt->flashver)
+            return AVERROR(ENOMEM);
         if (rt->is_input) {
             snprintf(rt->flashver, FLASHVER_MAX_LENGTH, "%s %d,%d,%d,%d",
                     RTMP_CLIENT_PLATFORM, RTMP_CLIENT_VER1, RTMP_CLIENT_VER2,
@@ -2896,10 +2885,8 @@ reconnect:
     }
     if (   strlen(rt->flashver) > FLASHVER_MAX_LENGTH
         || strlen(rt->tcurl   ) >    TCURL_MAX_LENGTH
-    ) {
-        ret = AVERROR(EINVAL);
-        goto fail;
-    }
+    )
+        return AVERROR(EINVAL);
 
     rt->receive_report_size = 1048576;
     rt->bytes_read = 0;
@@ -2914,17 +2901,17 @@ reconnect:
            proto, path, rt->app, rt->playpath);
     if (!rt->listen) {
         if ((ret = gen_connect(s, rt)) < 0)
-            goto fail;
+            return ret;
     } else {
         if ((ret = read_connect(s, s->priv_data)) < 0)
-            goto fail;
+            return ret;
     }
 
     do {
         ret = get_packet(s, 1);
     } while (ret == AVERROR(EAGAIN));
     if (ret < 0)
-        goto fail;
+        return ret;
 
     if (rt->do_reconnect) {
         int i;
@@ -2942,7 +2929,7 @@ reconnect:
         // generate FLV header for demuxer
         rt->flv_size = 13;
         if ((ret = av_reallocp(&rt->flv_data, rt->flv_size)) < 0)
-            goto fail;
+            return ret;
         rt->flv_off  = 0;
         memcpy(rt->flv_data, "FLV\1\0\0\0\0\011\0\0\0\0", rt->flv_size);
 
@@ -2953,7 +2940,7 @@ reconnect:
         // audio or video packet arrives.
         while (!rt->has_audio && !rt->has_video && !rt->received_metadata) {
             if ((ret = get_packet(s, 0)) < 0)
-               goto fail;
+                return ret;
         }
 
         // Either after we have read the metadata or (if there is none) the
@@ -2971,7 +2958,7 @@ reconnect:
         // to inform the FLV decoder about the duration.
         if (!rt->received_metadata && rt->duration > 0) {
             if ((ret = inject_fake_duration_metadata(rt)) < 0)
-                goto fail;
+                return ret;
         }
     } else {
         rt->flv_size = 0;
@@ -2983,9 +2970,14 @@ reconnect:
     s->max_packet_size = rt->stream->max_packet_size;
     s->is_streamed     = 1;
     return 0;
+}
 
-fail:
-    rtmp_close(s);
+static int rtmp_open(URLContext *s, const char *uri, int flags, AVDictionary 
**opts) {
+    RTMPContext *rt = s->priv_data;
+    int ret = rtmp_connect(s, rt, uri, flags, opts);
+
+    if (ret < 0)
+        rtmp_close(s);
     return ret;
 }
 
-- 
2.52.0


>From 266ef7d0016cf4fa3957645c0b01f25f9ecbfec7 Mon Sep 17 00:00:00 2001
From: Michel Lespinasse <[email protected]>
Date: Wed, 1 Jul 2026 13:00:05 +0200
Subject: [PATCH 2/7] avformat/rtmpproto: save the rtmp_connect() parameters

save the rtmp_connect uri, flags and options,
which might be needed for a future RTMP reconnect.
---
 libavformat/rtmpproto.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/libavformat/rtmpproto.c b/libavformat/rtmpproto.c
index 5d92c92e06..b35f20678d 100644
--- a/libavformat/rtmpproto.c
+++ b/libavformat/rtmpproto.c
@@ -134,6 +134,9 @@ typedef struct RTMPContext {
     char          auth_params[500];
     int           do_reconnect;
     int           auth_tried;
+    char          connect_uri[TCURL_MAX_LENGTH];
+    int           connect_flags;
+    AVDictionary* connect_opts;
 } RTMPContext;
 
 #define PLAYER_KEY_OPEN_PART_LEN 30   ///< length of partial key used for 
first client digest signing
@@ -2609,6 +2612,7 @@ static int rtmp_close(URLContext *h)
     free_tracked_methods(rt);
     av_freep(&rt->flv_data);
     ffurl_closep(&rt->stream);
+    av_dict_free(&rt->connect_opts);
     return ret;
 }
 
@@ -2693,6 +2697,11 @@ static int rtmp_connect(URLContext *s, RTMPContext *rt, 
const char *uri, int fla
     int port;
     int ret;
 
+    // Copy original params
+    av_strlcpy(rt->connect_uri, uri, TCURL_MAX_LENGTH);
+    rt->connect_flags = flags;
+    av_dict_copy(&rt->connect_opts, *opts, 0);
+
     if (rt->listen_timeout > 0)
         rt->listen = 1;
 
-- 
2.52.0


>From 85eacd7d85fddf00c4f45b1e4b2265b114d9bc81 Mon Sep 17 00:00:00 2001
From: Michel Lespinasse <[email protected]>
Date: Wed, 1 Jul 2026 13:00:05 +0200
Subject: [PATCH 3/7] avformat/rtmpproto: add rtmp_save() and rtmp_resend()

rtmp_save() saves the rt->out_pkt payload into the passed RTMPSaved structure,
and rtmp_resend() sends it again onto the current RTMP connection.

This will be used to save header packets and resend them after an
RTMP reconnect, thus providing the server with enough context to resume
decoding the audio/video streams.
---
 libavformat/rtmpproto.c | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/libavformat/rtmpproto.c b/libavformat/rtmpproto.c
index b35f20678d..c4922a23c2 100644
--- a/libavformat/rtmpproto.c
+++ b/libavformat/rtmpproto.c
@@ -75,6 +75,11 @@ typedef struct TrackedMethod {
     int id;
 } TrackedMethod;
 
+typedef struct RTMPSaved {
+    char* data;
+    int   size;
+} RTMPSaved;
+
 /** protocol handler context */
 typedef struct RTMPContext {
     const AVClass *class;
@@ -2990,6 +2995,32 @@ static int rtmp_open(URLContext *s, const char *uri, int 
flags, AVDictionary **o
     return ret;
 }
 
+/* Save rt->out_pkt payload into *save */
+static int rtmp_save(RTMPContext *rt, RTMPSaved *save)
+{
+    int size = rt->out_pkt.size, ret;
+    if ((ret = av_reallocp(&save->data, size)) < 0)
+        return ret;
+    memcpy(save->data, rt->out_pkt.data, size);
+    save->size = size;
+    return 0;
+}
+
+/* Resend a previously saved packet payload */
+static int rtmp_resend(RTMPContext *rt, RTMPSaved *save, int channel_id, 
RTMPPacketType type, int timestamp)
+{
+    RTMPPacket pkt = { 0 };
+    int size = save->size, ret;
+
+    if (!size)
+        return 0;
+    if ((ret = ff_rtmp_packet_create(&pkt, channel_id, type, timestamp, size)) 
< 0)
+        return ret;
+    pkt.extra = rt->stream_id;
+    memcpy(pkt.data, save->data, size);
+    return rtmp_send_packet(rt, &pkt, 0);
+}
+
 static int rtmp_read(URLContext *s, uint8_t *buf, int size)
 {
     RTMPContext *rt = s->priv_data;
-- 
2.52.0


>From 61e4694374641e41828f3e865649c978c0bdcfd5 Mon Sep 17 00:00:00 2001
From: Michel Lespinasse <[email protected]>
Date: Wed, 1 Jul 2026 13:00:05 +0200
Subject: [PATCH 4/7] avformat/rtmpproto: implement rtmp_reconnect()

Implement rtmp_reconnect(). It closes the current RTMP connection,
and opens a new one to rt->reconnect_uri, using the previous connection's
flags and options.

In rtmp_reconnect(), we don't want to reset the tracking of FLV packets
passed to the RTMP transmuxer. To that end, the code that did so is moved
from rtmp_connect() to rtmp_open(), so that it will only run during the
initial open and not during RTMP reconnect.

rtmp_connect() is also modified to build the underlying TCP connection URL
based on the uri argument instead of s->filename. The two are identical
when called from rtmp_open(), but not when called from rtmp_reconnect().

After reconnecting to the desired RTMP uri, resend any onMetaData packet
that might have been previously sent to the original RTMP server.
---
 libavformat/rtmpproto.c | 44 ++++++++++++++++++++++++++++++++++-------
 1 file changed, 37 insertions(+), 7 deletions(-)

diff --git a/libavformat/rtmpproto.c b/libavformat/rtmpproto.c
index c4922a23c2..705b3d035c 100644
--- a/libavformat/rtmpproto.c
+++ b/libavformat/rtmpproto.c
@@ -142,6 +142,9 @@ typedef struct RTMPContext {
     char          connect_uri[TCURL_MAX_LENGTH];
     int           connect_flags;
     AVDictionary* connect_opts;
+    int           reconnect_request;
+    char          reconnect_uri[TCURL_MAX_LENGTH];
+    RTMPSaved     saved_metadata;             ///< saved onMetaData payload
 } RTMPContext;
 
 #define PLAYER_KEY_OPEN_PART_LEN 30   ///< length of partial key used for 
first client digest signing
@@ -2618,6 +2621,7 @@ static int rtmp_close(URLContext *h)
     av_freep(&rt->flv_data);
     ffurl_closep(&rt->stream);
     av_dict_free(&rt->connect_opts);
+    av_freep(&rt->saved_metadata.data);
     return ret;
 }
 
@@ -2714,7 +2718,7 @@ static int rtmp_connect(URLContext *s, RTMPContext *rt, 
const char *uri, int fla
 
     av_url_split(proto, sizeof(proto), auth, sizeof(auth),
                  hostname, sizeof(hostname), &port,
-                 path, sizeof(path), s->filename);
+                 path, sizeof(path), uri);
 
     n = strchr(path, ' ');
     if (n) {
@@ -2974,11 +2978,6 @@ reconnect:
             if ((ret = inject_fake_duration_metadata(rt)) < 0)
                 return ret;
         }
-    } else {
-        rt->flv_size = 0;
-        rt->flv_data = NULL;
-        rt->flv_off  = 0;
-        rt->skip_bytes = 13;
     }
 
     s->max_packet_size = rt->stream->max_packet_size;
@@ -2990,8 +2989,14 @@ static int rtmp_open(URLContext *s, const char *uri, int 
flags, AVDictionary **o
     RTMPContext *rt = s->priv_data;
     int ret = rtmp_connect(s, rt, uri, flags, opts);
 
-    if (ret < 0)
+    if (ret < 0) {
         rtmp_close(s);
+    } else if (!rt->is_input) {
+        rt->flv_size = 0;
+        rt->flv_data = NULL;
+        rt->flv_off  = 0;
+        rt->skip_bytes = 13;
+    }
     return ret;
 }
 
@@ -3021,6 +3026,28 @@ static int rtmp_resend(RTMPContext *rt, RTMPSaved *save, 
int channel_id, RTMPPac
     return rtmp_send_packet(rt, &pkt, 0);
 }
 
+static int rtmp_reconnect(URLContext *s, RTMPContext *rt) {
+    int i, ret;
+
+    av_log(s, AV_LOG_INFO, "RTMP reconnect '%s'\n", rt->reconnect_uri);
+
+    // Close current RTMP connection
+    ffurl_closep(&rt->stream);
+    rt->do_reconnect = 0;
+    rt->nb_invokes   = 0;
+    for (i = 0; i < 2; i++)
+        memset(rt->prev_pkt[i], 0, sizeof(**rt->prev_pkt) * 
rt->nb_prev_pkt[i]);
+    free_tracked_methods(rt);
+
+    rt->reconnect_request = 0;
+
+    if ((ret = rtmp_connect(s, rt, rt->reconnect_uri, rt->connect_flags, 
&rt->connect_opts)) < 0)
+        return ret;
+
+    return rtmp_resend(rt, &rt->saved_metadata, RTMP_AUDIO_CHANNEL,
+                       RTMP_PT_NOTIFY, 0);
+}
+
 static int rtmp_read(URLContext *s, uint8_t *buf, int size)
 {
     RTMPContext *rt = s->priv_data;
@@ -3181,6 +3208,9 @@ static int rtmp_write(URLContext *s, const uint8_t *buf, 
int size)
                         rt->out_pkt.size += 16;
                         ptr = rt->out_pkt.data;
                         ff_amf_write_string(&ptr, "@setDataFrame");
+                        if (!strcmp(commandbuffer, "onMetaData") &&
+                            (ret = rtmp_save(rt, &rt->saved_metadata)) < 0)
+                            return ret;
                     }
                 }
             }
-- 
2.52.0


>From 8ca45ffbb046c93b2485e07c7080a4699dab9e71 Mon Sep 17 00:00:00 2001
From: Michel Lespinasse <[email protected]>
Date: Wed, 1 Jul 2026 13:00:05 +0200
Subject: [PATCH 5/7] avformat/rtmpproto: add rtmp_parse_av()

before sending any RTMP audio or video packets, pass them to rtmp_parse_av().

rtmp_parse_av() first parses the packet's AudioTagHeader or VideoTagHeader
in order to determine the packet type, frame type (for video frames only)
and track index.

An RTMPAVTrack structure is then allocated to maintain the state of the
corresponding audio or video track. Any relevant per-track headers
(video metadata, audio multichannel config, audio or video sequence headers)
are saved there.

Each track also has a current forwarding state, set to either
RTMP_DROP_PACKET or RTMP_FORWARD_PACKET. During RTMP reconnect, each track
is set to RTMP_DROP_PACKET. When an appropriate decoder restart point is
identified for the track (that is, a video keyframe, or any audio coded frame),
the state changes to RTMP_FORWARD_PACKET and any previously saved per-track
headers are resent.

Because rtmp_parse_av() is aware of each track's decoder restart points,
it is also responsible for calling rtmp_reconnect() when a reconnect is
pending (as signalled by rt->reconnect_request). A reconnect is triggered
when hitting a video track 0 restart point, or, if there are no video tracks,
any audio track restart point. This convention is chosen in order to help
ensure that every track will be able to restart as soon as possible after
the RTMP reconnection. Still, it is not perfect - in the case of multiple
video streams with unaligned keyframes, the video tracks 1 and above
might have a delay (until their next keyframe) before getting restarted,
after the RTMP reconnection triggered by video track 0's keyframe.

If rtmp_parse_av() can't properly parse the packet's AudioTagHeader
or VideoTagHeader, it returns RTMP_PARSE_ERROR, which is currently
equal to RTMP_DROP_PACKET and causes the packet to be dropped.
This is intended to help identify any such parsing errors, and make them
behave similarly before and after an RTMP reconnect, rather than try
forwarding such packets for transparency before an RTMP reconnect,
which would hide problems until the next RTMP reconnect.
---
 libavformat/rtmpproto.c | 164 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 162 insertions(+), 2 deletions(-)

diff --git a/libavformat/rtmpproto.c b/libavformat/rtmpproto.c
index 705b3d035c..f3928d2900 100644
--- a/libavformat/rtmpproto.c
+++ b/libavformat/rtmpproto.c
@@ -80,6 +80,23 @@ typedef struct RTMPSaved {
     int   size;
 } RTMPSaved;
 
+typedef enum {
+    RTMP_DROP_PACKET    = 0,
+    RTMP_FORWARD_PACKET = 1,
+    RTMP_PARSE_ERROR = RTMP_DROP_PACKET,
+} RTMPParseResult;
+
+enum {
+    SAVE_SLOT_0 = 0,
+    SAVE_SLOT_1,
+    NB_SAVE_SLOT,
+};
+
+typedef struct RTMPAVTrack {
+    RTMPParseResult forwarding;
+    RTMPSaved       saved[NB_SAVE_SLOT];
+} RTMPAVTrack;
+
 /** protocol handler context */
 typedef struct RTMPContext {
     const AVClass *class;
@@ -145,6 +162,8 @@ typedef struct RTMPContext {
     int           reconnect_request;
     char          reconnect_uri[TCURL_MAX_LENGTH];
     RTMPSaved     saved_metadata;             ///< saved onMetaData payload
+    RTMPAVTrack*  av_tracks[2];               ///< audio and video tracks
+    int           nb_av_tracks[2];            ///< nb audio and video tracks
 } RTMPContext;
 
 #define PLAYER_KEY_OPEN_PART_LEN 30   ///< length of partial key used for 
first client digest signing
@@ -2600,7 +2619,7 @@ static int get_packet(URLContext *s, int for_header)
 static int rtmp_close(URLContext *h)
 {
     RTMPContext *rt = h->priv_data;
-    int ret = 0, i, j;
+    int ret = 0, i, j, k;
 
     if (!rt->is_input) {
         rt->flv_data = NULL;
@@ -2622,6 +2641,12 @@ static int rtmp_close(URLContext *h)
     ffurl_closep(&rt->stream);
     av_dict_free(&rt->connect_opts);
     av_freep(&rt->saved_metadata.data);
+    for (i = 0; i < 2; i++) {
+        for (j = 0; j < rt->nb_av_tracks[i]; j++)
+            for (k = 0; k < NB_SAVE_SLOT; k++)
+                av_freep(&rt->av_tracks[i][j].saved[k].data);
+        av_freep(&rt->av_tracks[i]);
+    }
     return ret;
 }
 
@@ -3027,7 +3052,7 @@ static int rtmp_resend(RTMPContext *rt, RTMPSaved *save, 
int channel_id, RTMPPac
 }
 
 static int rtmp_reconnect(URLContext *s, RTMPContext *rt) {
-    int i, ret;
+    int i, j, ret;
 
     av_log(s, AV_LOG_INFO, "RTMP reconnect '%s'\n", rt->reconnect_uri);
 
@@ -3044,10 +3069,136 @@ static int rtmp_reconnect(URLContext *s, RTMPContext 
*rt) {
     if ((ret = rtmp_connect(s, rt, rt->reconnect_uri, rt->connect_flags, 
&rt->connect_opts)) < 0)
         return ret;
 
+    for (i = 0; i < 2; i++)
+        for (j = 0; j < rt->nb_av_tracks[i]; j++)
+            rt->av_tracks[i][j].forwarding = RTMP_DROP_PACKET;
+
     return rtmp_resend(rt, &rt->saved_metadata, RTMP_AUDIO_CHANNEL,
                        RTMP_PT_NOTIFY, 0);
 }
 
+static int rtmp_parse_av(URLContext *s, RTMPContext *rt) {
+    uint8_t *p = rt->out_pkt.data;
+    int size = rt->out_pkt.size;
+    int video = (rt->out_pkt.type == RTMP_PT_VIDEO);
+    int frame_type = FLV_FRAME_KEY, pkt_type = PacketTypeCodedFrames, 
track_idx = 0;
+    int i, ret;
+    RTMPAVTrack *track;
+
+    /*
+     * Parse AudioTagHeader or VideoTagHeader to determine packet type, 
(video) frame type and track idx.
+     */
+    if (size < 1)
+        return RTMP_PARSE_ERROR;
+    if (video) {
+        frame_type = *p & FLV_VIDEO_FRAMETYPE_MASK;
+        if (frame_type < FLV_FRAME_KEY || frame_type > 
FLV_FRAME_VIDEO_INFO_CMD)
+            return RTMP_PARSE_ERROR;
+    }
+    if (video ? (*p & 0x80) : ((*p & FLV_AUDIO_CODECID_MASK) == 
FLV_CODECID_EX_HEADER)) {      // ExAudioTagHeader or ExVideoTagHeader
+        pkt_type = *p & 0xf;
+        p++; size--;
+        while (pkt_type == PacketTypeModEx) {
+            int ex_size;
+            if (size < 3)
+                return RTMP_PARSE_ERROR;
+            if (*p != 0xff) {
+                ex_size = *p;
+                p++; size--;
+            } else {
+                ex_size = p[1] << 8 | p[2];
+                p += 3; size -= 3;
+            }
+            if (size < ex_size + 2)
+                return RTMP_PARSE_ERROR;
+            pkt_type = p[ex_size+1] & 0xf;
+            p += ex_size+2; size -= ex_size+2;
+        }
+        if (video && frame_type == FLV_FRAME_VIDEO_INFO_CMD &&
+            pkt_type != PacketTypeMetadata)
+            return RTMP_FORWARD_PACKET;
+        if (pkt_type == (video ? PacketTypeMultitrack : 
AudioPacketTypeMultitrack)) {
+            uint8_t multitrack_type;
+            if (size < 6)
+                return RTMP_PARSE_ERROR;
+            multitrack_type = *p & 0xf0;
+            pkt_type = *p & 0xf;
+            track_idx = p[5];
+            if (multitrack_type != MultitrackTypeOneTrack &&
+                (size < 9 || size != (p[6]<<16 | p[7]<<8 | p[8]) + 9)) {
+                /*
+                 * Multiple track payloads mashed into a single packet.
+                 * We do not support this with RTMP reconnect.
+                 */
+                return RTMP_PARSE_ERROR;
+            }
+        }
+    } else if (!video && (*p & FLV_AUDIO_CODECID_MASK) == FLV_CODECID_AAC) {   
// legacy audio AAC format, no ExAudioTagHeader
+        if (size < 2)
+            return RTMP_PARSE_ERROR;
+        pkt_type = p[1];
+        if (pkt_type < AudioPacketTypeSequenceStart || pkt_type > 
AudioPacketTypeCodedFrames)
+            return RTMP_PARSE_ERROR;
+    } else if (video && (*p & FLV_VIDEO_CODECID_MASK) == FLV_CODECID_H264) {   
// legacy video AVC format, no ExVideoTagHeader
+        if (size < 5)
+            return RTMP_PARSE_ERROR;
+        pkt_type = p[1];
+        if (pkt_type < PacketTypeSequenceStart || pkt_type > 
PacketTypeSequenceEnd)
+            return RTMP_PARSE_ERROR;
+    }
+
+    if (track_idx >= rt->nb_av_tracks[video]) {
+        int nb_alloc = 2*track_idx+1;
+        RTMPAVTrack* ptr = av_realloc_array(rt->av_tracks[video], nb_alloc,
+                                            sizeof(*rt->av_tracks[video]));
+        if (!ptr)
+            return AVERROR(ENOMEM);
+        memset(ptr + rt->nb_av_tracks[video], 0,
+               (nb_alloc - rt->nb_av_tracks[video]) * 
sizeof(*rt->av_tracks[video]));
+        rt->av_tracks[video] = ptr;
+        rt->nb_av_tracks[video] = nb_alloc;
+    }
+    track = rt->av_tracks[video] + track_idx;
+
+    /*
+     * Save video track metadata and sequence header,
+     * or audio track multichannel config and sequence header.
+     * note PacketTypeMetadata      == AudioPacketTypeMultichannelConfig
+     * and  PacketTypeSequenceStart == AudioPacketTypeSequenceStart
+     */
+    if (pkt_type == PacketTypeMetadata)
+        ret = rtmp_save(rt, track->saved + SAVE_SLOT_0);
+    else if (pkt_type == PacketTypeSequenceStart ||
+             (video && pkt_type == PacketTypeMPEG2TSSequenceStart))
+        ret = rtmp_save(rt, track->saved + SAVE_SLOT_1);
+    if (ret < 0)
+        return ret;
+
+    /*
+     * Identify per-track restart points.
+     * if rt->reconnect_request, reconnect on a restart point in video track 0,
+     * or in any audio track if there are no video tracks.
+     * When restarting a track, resend any saved header packets.
+     * note PacketTypeCodedFrames == AudioPacketTypeCodedFrames
+     */
+    if ((pkt_type == PacketTypeCodedFrames ||
+         (video && pkt_type == PacketTypeCodedFramesX)) &&
+        frame_type == FLV_FRAME_KEY) {
+        if (rt->reconnect_request &&
+            ((video && !track_idx) || !rt->nb_av_tracks[1]) &&
+            (ret = rtmp_reconnect(s, rt)) < 0)
+            return ret;
+        if (track->forwarding == RTMP_DROP_PACKET) {
+            track->forwarding = RTMP_FORWARD_PACKET;
+            for (i = 0; i < NB_SAVE_SLOT; i++)
+                if ((ret = rtmp_resend(rt, track->saved + i, 
rt->out_pkt.channel_id, rt->out_pkt.type, rt->out_pkt.timestamp)) < 0)
+                    return ret;
+        }
+    }
+
+    return track->forwarding;
+}
+
 static int rtmp_read(URLContext *s, uint8_t *buf, int size)
 {
     RTMPContext *rt = s->priv_data;
@@ -3215,8 +3366,17 @@ static int rtmp_write(URLContext *s, const uint8_t *buf, 
int size)
                 }
             }
 
+            if (pkttype == RTMP_PT_AUDIO || pkttype == RTMP_PT_VIDEO) {
+                if ((ret = rtmp_parse_av(s, rt)) < 0)
+                    return ret;
+                else if (ret == RTMP_DROP_PACKET)
+                    goto drop;
+            }
+
             if ((ret = rtmp_send_packet(rt, &rt->out_pkt, 0)) < 0)
                 return ret;
+
+        drop:
             rt->flv_size = 0;
             rt->flv_off = 0;
             rt->flv_header_bytes = 0;
-- 
2.52.0


>From c42d0af457c659d9e0f03534ccd6aea0fb3e52a2 Mon Sep 17 00:00:00 2001
From: Michel Lespinasse <[email protected]>
Date: Wed, 1 Jul 2026 13:00:05 +0200
Subject: [PATCH 6/7] avformat/rtmpproto: announce support for RTMP reconnect

When sending a connect command, set capsEx to 1 to tell the server we
support the RTMP reconnect feature.

When receiving a NetConnection.Connect.ReconnectRequest status message,
set rt->reconnect_request to indicate a pending reconnection,
and set the rt->reconnect_uri appropriately based on the status's
tcUrl if present.
---
 libavformat/rtmpproto.c | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/libavformat/rtmpproto.c b/libavformat/rtmpproto.c
index f3928d2900..d3c68bd735 100644
--- a/libavformat/rtmpproto.c
+++ b/libavformat/rtmpproto.c
@@ -429,6 +429,9 @@ static int gen_connect(URLContext *s, RTMPContext *rt)
     if (!rt->is_input) {
         ff_amf_write_field_name(&p, "type");
         ff_amf_write_string(&p, "nonprivate");
+        ff_amf_write_field_name(&p, "capsEx");
+        ff_amf_write_number(&p, 1.0);  // Announce support for RTMP reconnect
+
     }
     ff_amf_write_field_name(&p, "flashVer");
     ff_amf_write_string(&p, rt->flashver);
@@ -2236,7 +2239,7 @@ static int handle_invoke_status(URLContext *s, RTMPPacket 
*pkt)
     RTMPContext *rt = s->priv_data;
     const uint8_t *data_end = pkt->data + pkt->size;
     const uint8_t *ptr = pkt->data + RTMP_HEADER;
-    uint8_t tmpstr[256];
+    uint8_t tmpstr[TCURL_MAX_LENGTH];
     int i, t;
 
     for (i = 0; i < 2; i++) {
@@ -2265,6 +2268,16 @@ static int handle_invoke_status(URLContext *s, 
RTMPPacket *pkt)
     if (!t && !strcmp(tmpstr, "NetStream.Publish.Start")) rt->state = 
STATE_PUBLISHING;
     if (!t && !strcmp(tmpstr, "NetStream.Seek.Notify")) rt->state = 
STATE_PLAYING;
 
+    if (!t && !strcmp(tmpstr, "NetConnection.Connect.ReconnectRequest")) {
+        rt->reconnect_request = 1;
+        if (!ff_amf_get_field_value(ptr, data_end, "tcUrl", tmpstr, 
sizeof(tmpstr)))
+            ff_make_absolute_url(rt->reconnect_uri, sizeof(rt->reconnect_uri),
+                                 rt->connect_uri, tmpstr);
+        else
+            av_strlcpy(rt->reconnect_uri, rt->connect_uri, 
sizeof(rt->reconnect_uri));
+        av_log(s, AV_LOG_INFO, "RTMP reconnect request to '%s'\n", 
rt->reconnect_uri);
+    }
+
     return 0;
 }
 
-- 
2.52.0


>From bea7319a34fa1e59c2d39cc36cd90c199f4c7571 Mon Sep 17 00:00:00 2001
From: Michel Lespinasse <[email protected]>
Date: Thu, 9 Jul 2026 14:36:45 +0200
Subject: [PATCH 7/7] avformat/rtmpproto: test options to force client
 initiated reconnect

Add the rtmp_reconnect_time and rtmp_reconnect_uri command line parameters
to force a client initiated reconnect for testing purposes.

This is intended for testing only; normally RTMP reconnect should be
initiated by the server side.
---
 libavformat/rtmpproto.c | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/libavformat/rtmpproto.c b/libavformat/rtmpproto.c
index d3c68bd735..d7a00dd3d5 100644
--- a/libavformat/rtmpproto.c
+++ b/libavformat/rtmpproto.c
@@ -164,6 +164,9 @@ typedef struct RTMPContext {
     RTMPSaved     saved_metadata;             ///< saved onMetaData payload
     RTMPAVTrack*  av_tracks[2];               ///< audio and video tracks
     int           nb_av_tracks[2];            ///< nb audio and video tracks
+    uint32_t      test_reconnect_timestamp;
+    int           test_reconnect_interval;    ///< Forces a reconnected every 
Xs (in media time)
+    char*         test_reconnect_uri;
 } RTMPContext;
 
 #define PLAYER_KEY_OPEN_PART_LEN 30   ///< length of partial key used for 
first client digest signing
@@ -3379,6 +3382,20 @@ static int rtmp_write(URLContext *s, const uint8_t *buf, 
int size)
                 }
             }
 
+            if (!rt->test_reconnect_timestamp)
+                rt->test_reconnect_timestamp = rt->out_pkt.timestamp;
+            else if (!rt->is_input && rt->test_reconnect_interval > 0 &&
+                     rt->out_pkt.timestamp > rt->test_reconnect_timestamp + 
rt->test_reconnect_interval * 1000) {
+                rt->test_reconnect_timestamp = rt->out_pkt.timestamp;
+                rt->reconnect_request = 1;
+                if (rt->test_reconnect_uri)
+                    ff_make_absolute_url(rt->reconnect_uri, 
sizeof(rt->reconnect_uri),
+                                         rt->connect_uri, 
rt->test_reconnect_uri);
+                else
+                    av_strlcpy(rt->reconnect_uri, rt->connect_uri, 
sizeof(rt->reconnect_uri));
+                av_log(s, AV_LOG_INFO, "triggered internal interval 
reconnection '%s'\n", rt->reconnect_uri);
+            }
+
             if (pkttype == RTMP_PT_AUDIO || pkttype == RTMP_PT_VIDEO) {
                 if ((ret = rtmp_parse_av(s, rt)) < 0)
                     return ret;
@@ -3460,6 +3477,8 @@ static const AVOption rtmp_options[] = {
     {"listen",      "Listen for incoming rtmp connections", OFFSET(listen), 
AV_OPT_TYPE_INT, {.i64 = 0}, INT_MIN, INT_MAX, DEC, .unit = "rtmp_listen" },
     {"tcp_nodelay", "Use TCP_NODELAY to disable Nagle's algorithm", 
OFFSET(tcp_nodelay), AV_OPT_TYPE_INT, {.i64 = 0}, 0, 1, DEC|ENC},
     {"timeout", "Maximum timeout (in seconds) to wait for incoming 
connections. -1 is infinite. Implies -rtmp_listen 1",  OFFSET(listen_timeout), 
AV_OPT_TYPE_INT, {.i64 = -1}, INT_MIN, INT_MAX, DEC, .unit = "rtmp_listen" },
+    {"rtmp_reconnect_time", "Interval (in seconds) to force a client 
reconnection, it is based on media time. By default is 0 (no reconnection)", 
OFFSET(test_reconnect_interval), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, ENC },
+    {"rtmp_reconnect_uri", "URL to reconnect to after rtmp_reconnect_time.", 
OFFSET(test_reconnect_uri), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, ENC},
     { NULL },
 };
 
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to