From: Steven Liu <lingjiujianke-at-gmail.com@ffmpeg.org> To: FFmpeg development discussions and patches <ffmpeg-devel@ffmpeg.org> Cc: Sergio Garcia Murillo <sergio.garcia.murillo@gmail.com>, Jack Lau <jacklau1222@qq.com> Subject: Re: [FFmpeg-devel] [PATCH v2 5/6] avformat/whip: implement NACK and RTX suppport Date: Wed, 2 Jul 2025 21:46:33 +0800 Message-ID: <CADxeRwkKy0fdv3goBXrHA__enPv=cintenuR8OXJ5+ObQxEAEw@mail.gmail.com> (raw) In-Reply-To: <20250702115955.33922-6-jacklau1222@qq.com> Jack Lau <jacklau1222gm-at-gmail.com@ffmpeg.org> 于2025年7月2日周三 20:09写道: > > RTP retransmission described in RFC4588 (RTX) is an effective packet > loss recovery technique for real-time applications with relaxed delay bounds. > > This patch provides a minimal implementation for RTX and RTCP NACK (RFC3940) > and its associated SDP signaling and negotiation. > > Co-authored-by: Sergio Garcia Murillo <sergio.garcia.murillo@gmail.com> > Signed-off-by: Jack Lau <jacklau1222@qq.com> > --- > libavformat/whip.c | 198 ++++++++++++++++++++++++++++++++++++++++++++- > 1 file changed, 195 insertions(+), 3 deletions(-) > > diff --git a/libavformat/whip.c b/libavformat/whip.c > index e287a3062f..fa6e3855d3 100644 > --- a/libavformat/whip.c > +++ b/libavformat/whip.c > @@ -114,6 +114,7 @@ > /* Referring to Chrome's definition of RTP payload types. */ > #define WHIP_RTP_PAYLOAD_TYPE_H264 106 > #define WHIP_RTP_PAYLOAD_TYPE_OPUS 111 > +#define WHIP_RTP_PAYLOAD_TYPE_RTX 105 > > /** > * The STUN message header, which is 20 bytes long, comprises the > @@ -150,6 +151,11 @@ > #define WHIP_SDP_SESSION_ID "4489045141692799359" > #define WHIP_SDP_CREATOR_IP "127.0.0.1" > > +/** > + * Retransmission / NACK support > +*/ > +#define HISTORY_SIZE_DEFAULT 512 > + > /* Calculate the elapsed time from starttime to endtime in milliseconds. */ > #define ELAPSED(starttime, endtime) ((int)(endtime - starttime) / 1000) > > @@ -194,9 +200,19 @@ enum WHIPState { > }; > > typedef enum WHIPFlags { > - WHIP_FLAG_IGNORE_IPV6 = (1 << 0) // Ignore ipv6 candidate > + WHIP_FLAG_IGNORE_IPV6 = (1 << 0), // Ignore ipv6 candidate > + WHIP_FLAG_DISABLE_RTX = (1 << 1) // Enable NACK and RTX > } WHIPFlags; > > +typedef struct RtpHistoryItem { > + /* original RTP seq */ > + uint16_t seq; /* original RTP seq */ > + /* length in bytes */ > + int size; /* length in bytes */ > + /* malloc-ed copy */ move the comments. > + uint8_t* buf; /* malloc-ed copy */ > +} RtpHistoryItem; for example: 171 typedef struct mkv_cuepoint { 172 uint64_t pts; 173 int stream_idx; 174 int64_t cluster_pos; ///< offset of the cluster containing the block relative to the segment 175 int64_t relative_pos; ///< relative offset from the position of the cluster containing the block 176 int64_t duration; ///< duration of the block according to time base 177 } mkv_cuepoint; > + > typedef struct WHIPContext { > AVClass *av_class; > > @@ -285,6 +301,7 @@ typedef struct WHIPContext { > /* The SRTP send context, to encrypt outgoing packets. */ > SRTPContext srtp_audio_send; > SRTPContext srtp_video_send; > + SRTPContext srtp_video_rtx_send; > SRTPContext srtp_rtcp_send; > /* The SRTP receive context, to decrypt incoming packets. */ > SRTPContext srtp_recv; > @@ -309,6 +326,14 @@ typedef struct WHIPContext { > /* The certificate and private key used for DTLS handshake. */ > char* cert_file; > char* key_file; > + > + /* RTX and NACK */ > + uint8_t rtx_payload_type; > + uint32_t video_rtx_ssrc; > + uint16_t rtx_seq; > + int history_size; > + RtpHistoryItem *history; /* ring buffer */ > + int hist_head; > } WHIPContext; > > /** > @@ -606,6 +631,16 @@ static int generate_sdp_offer(AVFormatContext *s) > whip->audio_payload_type = WHIP_RTP_PAYLOAD_TYPE_OPUS; > whip->video_payload_type = WHIP_RTP_PAYLOAD_TYPE_H264; > > + /* RTX and NACK init */ > + whip->rtx_payload_type = WHIP_RTP_PAYLOAD_TYPE_RTX; > + whip->video_rtx_ssrc = av_lfg_get(&whip->rnd); > + whip->rtx_seq = 0; > + whip->hist_head = 0; > + whip->history_size = FFMAX(64, whip->history_size); > + whip->history = av_calloc(whip->history_size, sizeof(*whip->history)); > + if (!whip->history) > + return AVERROR(ENOMEM); > + > av_bprintf(&bp, "" > "v=0\r\n" > "o=FFmpeg %s 2 IN IP4 %s\r\n" > @@ -656,7 +691,7 @@ static int generate_sdp_offer(AVFormatContext *s) > } > > av_bprintf(&bp, "" > - "m=video 9 UDP/TLS/RTP/SAVPF %u\r\n" > + "m=video 9 UDP/TLS/RTP/SAVPF %u %u\r\n" > "c=IN IP4 0.0.0.0\r\n" > "a=ice-ufrag:%s\r\n" > "a=ice-pwd:%s\r\n" > @@ -669,9 +704,16 @@ static int generate_sdp_offer(AVFormatContext *s) > "a=rtcp-rsize\r\n" > "a=rtpmap:%u %s/90000\r\n" > "a=fmtp:%u level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=%02x%02x%02x\r\n" > + "a=rtcp-fb:%u nack\r\n" > + "a=rtpmap:%u rtx/90000\r\n" > + "a=fmtp:%u apt=%u\r\n" > + "a=ssrc-group:FID %u %u\r\n" > + "a=ssrc:%u cname:FFmpeg\r\n" > + "a=ssrc:%u msid:FFmpeg video\r\n" > "a=ssrc:%u cname:FFmpeg\r\n" > "a=ssrc:%u msid:FFmpeg video\r\n", > whip->video_payload_type, > + whip->rtx_payload_type, > whip->ice_ufrag_local, > whip->ice_pwd_local, > whip->dtls_fingerprint, > @@ -681,8 +723,16 @@ static int generate_sdp_offer(AVFormatContext *s) > profile, > whip->constraint_set_flags, > level, > + whip->video_payload_type, > + whip->rtx_payload_type, > + whip->rtx_payload_type, > + whip->video_payload_type, > + whip->video_ssrc, > + whip->video_rtx_ssrc, > whip->video_ssrc, > - whip->video_ssrc); > + whip->video_ssrc, > + whip->video_rtx_ssrc, > + whip->video_rtx_ssrc); > } > > if (!av_bprint_is_complete(&bp)) { > @@ -1398,6 +1448,12 @@ static int setup_srtp(AVFormatContext *s) > goto end; > } > > + ret = ff_srtp_set_crypto(&whip->srtp_video_rtx_send, suite, buf); > + if (ret < 0) { > + av_log(whip, AV_LOG_ERROR, "WHIP: Failed to set crypto for video rtx send\n"); Since av_log(whip, has been set, the "WHIP:" prefix might be unnecessary. Same comment to bellow. > + goto end; > + } > + > ret = ff_srtp_set_crypto(&whip->srtp_rtcp_send, suite, buf); > if (ret < 0) { > av_log(whip, AV_LOG_ERROR, "Failed to set crypto for rtcp send\n"); > @@ -1427,6 +1483,37 @@ end: > return ret; > } > > + > +/** > + * RTX history helpers > + */ > + static void rtp_history_store(WHIPContext *whip, const uint8_t *buf, int size) return value. > +{ > + int pos = whip->hist_head % whip->history_size; > + RtpHistoryItem *it = &whip->history[pos]; > + /* free older entry */ > + av_free(it->buf); > + it->buf = av_malloc(size); > + if (!it->buf) > + return; return AVERROR(ENOMEM) > + > + memcpy(it->buf, buf, size); > + it->size = size; > + it->seq = AV_RB16(buf + 2); > + > + whip->hist_head = ++pos; > +} > + > +static const RtpHistoryItem *rtp_history_find(const WHIPContext *whip, uint16_t seq) > +{ > + for (int i = 0; i < whip->history_size; i++) { > + const RtpHistoryItem *it = &whip->history[i]; > + if (it->buf && it->seq == seq) > + return it; > + } > + return NULL; > +} > + > /** > * Callback triggered by the RTP muxer when it creates and sends out an RTP packet. > * > @@ -1463,6 +1550,10 @@ static int on_rtp_write_packet(void *opaque, const uint8_t *buf, int buf_size) > return 0; > } > > + /* Store only ORIGINAL video packets (non-RTX, non-RTCP) */ > + if (!is_rtcp && is_video) > + rtp_history_store(whip, buf, buf_size); ret = rtp_history_store(whip, buf, buf_size); if (ret < 0) process failed; > + > ret = ffurl_write(whip->udp, whip->buf, cipher_size); > if (ret < 0) { > av_log(whip, AV_LOG_ERROR, "Failed to write packet=%dB, ret=%d\n", cipher_size, ret); > @@ -1471,6 +1562,45 @@ static int on_rtp_write_packet(void *opaque, const uint8_t *buf, int buf_size) > > return ret; > } > +/** > + * See https://datatracker.ietf.org/doc/html/rfc4588 > + * Build and send a single RTX packet > + */ > +static int send_rtx_packet(AVFormatContext *s, const uint8_t *orig_pkt_buf, int orig_size) > +{ > + WHIPContext *whip = s->priv_data; > + int new_size, cipher_size; > + if (whip->flags & WHIP_FLAG_DISABLE_RTX) > + return 0; > + > + /* allocate new buffer: header + 2 + payload */ > + if (orig_size + 2 > sizeof(whip->buf)) > + return 0; > + > + memcpy(whip->buf, orig_pkt_buf, orig_size); > + > + uint8_t *hdr = whip->buf; > + uint16_t orig_seq = AV_RB16(hdr + 2); > + > + /* rewrite header */ > + hdr[1] = (hdr[1] & 0x80) | whip->rtx_payload_type; /* keep M bit */ > + AV_WB16(hdr + 2, whip->rtx_seq++); > + AV_WB32(hdr + 8, whip->video_rtx_ssrc); > + > + /* shift payload 2 bytes */ > + memmove(hdr + 12 + 2, hdr + 12, orig_size - 12); > + AV_WB16(hdr + 12, orig_seq); > + > + new_size = orig_size + 2; > + > + /* Encrypt by SRTP and send out. */ > + cipher_size = ff_srtp_encrypt(&whip->srtp_video_rtx_send, whip->buf, new_size, whip->buf, sizeof(whip->buf)); > + if (cipher_size <= 0 || cipher_size < new_size) { > + av_log(whip, AV_LOG_WARNING, "WHIP: Failed to encrypt packet=%dB, cipher=%dB\n", new_size, cipher_size); > + return 0; > + } > + return ffurl_write(whip->udp, whip->buf, cipher_size); > +} > > /** > * Creates dedicated RTP muxers for each stream in the AVFormatContext to build RTP > @@ -1793,6 +1923,66 @@ static int whip_write_packet(AVFormatContext *s, AVPacket *pkt) > goto end; > } > } > + /** > + * Handle RTCP NACK > + * Refer to RFC 4585, Section 6.2.1 > + * The Generic NACK message is identified by PT=RTPFB and FMT=1. > + * TODO: disable retransmisstion when "-tune zerolatency" > + */ > + if (media_is_rtcp(whip->buf, ret)) { > + int ptr = 0; > + uint8_t pt = whip->buf[ptr + 1]; > + uint8_t fmt = (whip->buf[ptr] & 0x1f); > + if (ptr + 4 <= ret && pt == 205 && fmt == 1) { > + /** > + * Refer to RFC 3550, Section 6.4.1. > + * The length of this RTCP packet in 32-bit words minus one, > + * including the header and any padding. > + */ > + int rtcp_len = (AV_RB16(&whip->buf[ptr + 2]) + 1) * 4; > + /* SRTCP index(4 bytes) + HMAC (SRTP_AES128_CM_SHA1_80 10bytes) */ > + int srtcp_len = rtcp_len + 4 + 10; > + if (srtcp_len == ret && rtcp_len >= 12) { > + int i = 0; > + uint8_t *buf = av_malloc(srtcp_len); if (!buf) process AVEROR(ENOMEM) > + memcpy(buf, whip->buf, srtcp_len); > + int ret = ff_srtp_decrypt(&whip->srtp_recv, buf, &srtcp_len); > + if (ret < 0) if error here, should free buf, otherwise will memleak. > + av_log(whip, AV_LOG_ERROR, "WHIP: SRTCP decrypt failed: %d\n", ret); > + while (12 + i < rtcp_len && !ret) { > + /** > + * See https://datatracker.ietf.org/doc/html/rfc4585#section-6.1 > + * Handle multi NACKs in bundled packet. > + */ > + uint16_t pid = AV_RB16(&buf[ptr + 12 + i]); > + uint16_t blp = AV_RB16(&buf[ptr + 14 + i]); > + > + /* retransmit pid + any bit set in blp */ > + for (int bit = -1; bit < 16; bit++) { > + uint16_t seq = (bit < 0) ? pid : pid + bit + 1; > + if (bit >= 0 && !(blp & (1 << bit))) > + continue; > + > + const RtpHistoryItem *it = rtp_history_find(whip, seq); > + if (it) { > + av_log(whip, AV_LOG_VERBOSE, > + "WHIP: NACK, packet found: size: %d, seq=%d, rtx size=%d, lateset stored packet seq:%d\n", > + it->size, seq, ret, whip->history[whip->hist_head-1].seq); > + ret = send_rtx_packet(s, it->buf, it->size); > + if (ret <= 0 && !(whip->flags & WHIP_FLAG_DISABLE_RTX)) > + av_log(whip, AV_LOG_ERROR, "WHIP: Failed to send RTX packet\n"); > + } else { > + av_log(whip, AV_LOG_VERBOSE, > + "WHIP: NACK, packet not found, seq=%d, latest stored packet seq: %d, latest rtx seq: %d\n", > + seq, whip->history[whip->hist_head-1].seq, whip->rtx_seq); > + } > + } > + i = i + 4; > + } > + av_free(buf); > + } > + } > + } > } else if (ret != AVERROR(EAGAIN)) { > av_log(whip, AV_LOG_ERROR, "Failed to read from UDP socket\n"); > goto end; > @@ -1898,6 +2088,8 @@ static const AVOption options[] = { > { "key_file", "Optional private key file path for DTLS", OFFSET(key_file), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, ENC }, > { "whip_flags", "Set flags affecting WHIP connection behavior", OFFSET(flags), AV_OPT_TYPE_FLAGS, { .i64 = 0 }, 0, UINT_MAX, ENC, .unit = "flags" }, > { "ignore_ipv6", "Ignore any IPv6 ICE candidate", 0, AV_OPT_TYPE_CONST, { .i64 = WHIP_FLAG_IGNORE_IPV6 }, 0, UINT_MAX, ENC, .unit = "flags" }, > + { "disable_rtx", "Disable RFC 4588 RTX", 0, AV_OPT_TYPE_CONST, { .i64 = WHIP_FLAG_DISABLE_RTX }, 0, UINT_MAX, ENC, .unit = "flags" }, > + { "rtx_history_size", "Packet history size", OFFSET(history_size), AV_OPT_TYPE_INT, { .i64 = HISTORY_SIZE_DEFAULT }, 64, 2048, ENC }, > { NULL }, > }; > > -- > 2.49.0 > > _______________________________________________ > ffmpeg-devel mailing list > ffmpeg-devel@ffmpeg.org > https://ffmpeg.org/mailman/listinfo/ffmpeg-devel > > To unsubscribe, visit link above, or email > ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".
next prev parent reply other threads:[~2025-07-02 13:46 UTC|newest] Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top 2025-07-02 11:59 [FFmpeg-devel] [PATCH v2 0/6] avformat/whip: Add NACK and RTX support (depends on ignore_ipv6 patchset) Jack Lau 2025-07-02 11:59 ` [FFmpeg-devel] [PATCH v2 1/6] avformat/whip: add whip_flags ignore_ipv6 to skip IPv6 ICE candidates Jack Lau 2025-07-02 11:59 ` [FFmpeg-devel] [PATCH v2 2/6] avformat/whip: fix typos Jack Lau 2025-07-02 11:59 ` [FFmpeg-devel] [PATCH v2 3/6] avformat/whip: fix H264 profile_iop bit map for SDP Jack Lau 2025-07-02 11:59 ` [FFmpeg-devel] [PATCH v2 4/6] WHIP: X509 cert serial number should be positive Jack Lau 2025-07-02 11:59 ` [FFmpeg-devel] [PATCH v2 5/6] avformat/whip: implement NACK and RTX suppport Jack Lau 2025-07-02 13:46 ` Steven Liu [this message] 2025-07-02 14:10 ` Jack Lau 2025-07-02 11:59 ` [FFmpeg-devel] [PATCH v2 6/6] avformat/whip: reindent whip options Jack Lau
Reply instructions: You may reply publicly to this message via plain-text email using any one of the following methods: * Save the following mbox file, import it into your mail client, and reply-to-all from there: mbox Avoid top-posting and favor interleaved quoting: https://en.wikipedia.org/wiki/Posting_style#Interleaved_style * Reply using the --to, --cc, and --in-reply-to switches of git-send-email(1): git send-email \ --in-reply-to='CADxeRwkKy0fdv3goBXrHA__enPv=cintenuR8OXJ5+ObQxEAEw@mail.gmail.com' \ --to=lingjiujianke-at-gmail.com@ffmpeg.org \ --cc=ffmpeg-devel@ffmpeg.org \ --cc=jacklau1222@qq.com \ --cc=sergio.garcia.murillo@gmail.com \ /path/to/YOUR_REPLY https://kernel.org/pub/software/scm/git/docs/git-send-email.html * If your mail client supports setting the In-Reply-To header via mailto: links, try the mailto: link
Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel This inbox may be cloned and mirrored by anyone: git clone --mirror https://master.gitmailbox.com/ffmpegdev/0 ffmpegdev/git/0.git # If you have public-inbox 1.1+ installed, you may # initialize and index your mirror using the following commands: public-inbox-init -V2 ffmpegdev ffmpegdev/ https://master.gitmailbox.com/ffmpegdev \ ffmpegdev@gitmailbox.com public-inbox-index ffmpegdev Example config snippet for mirrors. AGPL code for this site: git clone https://public-inbox.org/public-inbox.git