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]
