From: Anton Khirnov <anton@khirnov.net> To: ffmpeg-devel@ffmpeg.org Subject: [FFmpeg-devel] [PATCH 17/24] fftools/ffmpeg: move sub2video handling to ffmpeg_filter Date: Sun, 28 May 2023 11:14:09 +0200 Message-ID: <20230528091416.17927-17-anton@khirnov.net> (raw) In-Reply-To: <20230528091416.17927-1-anton@khirnov.net> Make all relevant state per-filtergraph input, rather than per-input stream. Refactor the code to make it work and avoid leaking memory when a single subtitle stream is sent to multiple filters. --- fftools/ffmpeg.c | 133 ++-------------------------- fftools/ffmpeg.h | 7 +- fftools/ffmpeg_dec.c | 9 +- fftools/ffmpeg_demux.c | 1 - fftools/ffmpeg_filter.c | 189 +++++++++++++++++++++++++++++++++++----- 5 files changed, 177 insertions(+), 162 deletions(-) diff --git a/fftools/ffmpeg.c b/fftools/ffmpeg.c index e9e60407d2..36b4becaf2 100644 --- a/fftools/ffmpeg.c +++ b/fftools/ffmpeg.c @@ -147,139 +147,20 @@ static int restore_tty; This is a temporary solution until libavfilter gets real subtitles support. */ -static int sub2video_get_blank_frame(InputStream *ist) -{ - int ret; - AVFrame *frame = ist->sub2video.frame; - - av_frame_unref(frame); - frame->width = ist->sub2video.w; - frame->height = ist->sub2video.h; - frame->format = AV_PIX_FMT_RGB32; - if ((ret = av_frame_get_buffer(frame, 0)) < 0) - return ret; - memset(frame->data[0], 0, frame->height * frame->linesize[0]); - return 0; -} - -static void sub2video_copy_rect(uint8_t *dst, int dst_linesize, int w, int h, - AVSubtitleRect *r) -{ - uint32_t *pal, *dst2; - uint8_t *src, *src2; - int x, y; - - if (r->type != SUBTITLE_BITMAP) { - av_log(NULL, AV_LOG_WARNING, "sub2video: non-bitmap subtitle\n"); - return; - } - if (r->x < 0 || r->x + r->w > w || r->y < 0 || r->y + r->h > h) { - av_log(NULL, AV_LOG_WARNING, "sub2video: rectangle (%d %d %d %d) overflowing %d %d\n", - r->x, r->y, r->w, r->h, w, h - ); - return; - } - - dst += r->y * dst_linesize + r->x * 4; - src = r->data[0]; - pal = (uint32_t *)r->data[1]; - for (y = 0; y < r->h; y++) { - dst2 = (uint32_t *)dst; - src2 = src; - for (x = 0; x < r->w; x++) - *(dst2++) = pal[*(src2++)]; - dst += dst_linesize; - src += r->linesize[0]; - } -} - -static void sub2video_push_ref(InputStream *ist, int64_t pts) -{ - AVFrame *frame = ist->sub2video.frame; - int i; - int ret; - - av_assert1(frame->data[0]); - ist->sub2video.last_pts = frame->pts = pts; - for (i = 0; i < ist->nb_filters; i++) { - ret = av_buffersrc_add_frame_flags(ist->filters[i]->filter, frame, - AV_BUFFERSRC_FLAG_KEEP_REF | - AV_BUFFERSRC_FLAG_PUSH); - if (ret != AVERROR_EOF && ret < 0) - av_log(NULL, AV_LOG_WARNING, "Error while add the frame to buffer source(%s).\n", - av_err2str(ret)); - } -} - -void sub2video_update(InputStream *ist, int64_t heartbeat_pts, - const AVSubtitle *sub) -{ - AVFrame *frame = ist->sub2video.frame; - int8_t *dst; - int dst_linesize; - int num_rects, i; - int64_t pts, end_pts; - - if (!frame) - return; - if (sub) { - pts = av_rescale_q(sub->pts + sub->start_display_time * 1000LL, - AV_TIME_BASE_Q, ist->st->time_base); - end_pts = av_rescale_q(sub->pts + sub->end_display_time * 1000LL, - AV_TIME_BASE_Q, ist->st->time_base); - num_rects = sub->num_rects; - } else { - /* If we are initializing the system, utilize current heartbeat - PTS as the start time, and show until the following subpicture - is received. Otherwise, utilize the previous subpicture's end time - as the fall-back value. */ - pts = ist->sub2video.initialize ? - heartbeat_pts : ist->sub2video.end_pts; - end_pts = INT64_MAX; - num_rects = 0; - } - if (sub2video_get_blank_frame(ist) < 0) { - av_log(NULL, AV_LOG_ERROR, - "Impossible to get a blank canvas.\n"); - return; - } - dst = frame->data [0]; - dst_linesize = frame->linesize[0]; - for (i = 0; i < num_rects; i++) - sub2video_copy_rect(dst, dst_linesize, frame->width, frame->height, sub->rects[i]); - sub2video_push_ref(ist, pts); - ist->sub2video.end_pts = end_pts; - ist->sub2video.initialize = 0; -} - static void sub2video_heartbeat(InputFile *infile, int64_t pts, AVRational tb) { - int i, j, nb_reqs; - int64_t pts2; - /* When a frame is read from a file, examine all sub2video streams in the same file and send the sub2video frame again. Otherwise, decoded video frames could be accumulating in the filter graph while a filter (possibly overlay) is desperately waiting for a subtitle frame. */ - for (i = 0; i < infile->nb_streams; i++) { - InputStream *ist2 = infile->streams[i]; - if (!ist2->sub2video.frame) - continue; - /* subtitles seem to be usually muxed ahead of other streams; - if not, subtracting a larger time here is necessary */ - pts2 = av_rescale_q(pts, tb, ist2->st->time_base) - 1; - /* do not send the heartbeat frame if the subtitle is already ahead */ - if (pts2 <= ist2->sub2video.last_pts) + for (int i = 0; i < infile->nb_streams; i++) { + InputStream *ist = infile->streams[i]; + + if (ist->dec_ctx->codec_type != AVMEDIA_TYPE_SUBTITLE) continue; - if (pts2 >= ist2->sub2video.end_pts || ist2->sub2video.initialize) - /* if we have hit the end of the current displayed subpicture, - or if we need to initialize the system, update the - overlayed subpicture and its start/end times */ - sub2video_update(ist2, pts2 + 1, NULL); - for (j = 0, nb_reqs = 0; j < ist2->nb_filters; j++) - nb_reqs += av_buffersrc_get_nb_failed_requests(ist2->filters[j]->filter); - if (nb_reqs) - sub2video_push_ref(ist2, pts2); + + for (int j = 0; j < ist->nb_filters; j++) + ifilter_sub2video_heartbeat(ist->filters[j], pts, tb); } } diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h index d4aff5cc7c..75695d3fb5 100644 --- a/fftools/ffmpeg.h +++ b/fftools/ffmpeg.h @@ -372,11 +372,7 @@ typedef struct InputStream { } prev_sub; struct sub2video { - int64_t last_pts; - int64_t end_pts; - AVFrame *frame; int w, h; - unsigned int initialize; ///< marks if sub2video_update should force an initialization } sub2video; /* decoded data from this stream goes into all those filters @@ -741,13 +737,12 @@ int init_simple_filtergraph(InputStream *ist, OutputStream *ost, char *graph_desc); int init_complex_filtergraph(FilterGraph *fg); -void sub2video_update(InputStream *ist, int64_t heartbeat_pts, - const AVSubtitle *sub); int copy_av_subtitle(AVSubtitle *dst, const AVSubtitle *src); int ifilter_send_frame(InputFilter *ifilter, AVFrame *frame, int keep_reference); int ifilter_send_eof(InputFilter *ifilter, int64_t pts, AVRational tb); int ifilter_sub2video(InputFilter *ifilter, const AVSubtitle *sub); +void ifilter_sub2video_heartbeat(InputFilter *ifilter, int64_t pts, AVRational tb); /** * Set up fallback filtering parameters from a decoder context. They will only diff --git a/fftools/ffmpeg_dec.c b/fftools/ffmpeg_dec.c index a7acaf67c2..30959c64b7 100644 --- a/fftools/ffmpeg_dec.c +++ b/fftools/ffmpeg_dec.c @@ -321,13 +321,8 @@ static int video_frame_process(InputStream *ist, AVFrame *frame) static void sub2video_flush(InputStream *ist) { - int i; - int ret; - - if (ist->sub2video.end_pts < INT64_MAX) - sub2video_update(ist, INT64_MAX, NULL); - for (i = 0; i < ist->nb_filters; i++) { - ret = av_buffersrc_add_frame(ist->filters[i]->filter, NULL); + for (int i = 0; i < ist->nb_filters; i++) { + int ret = ifilter_sub2video(ist->filters[i], NULL); if (ret != AVERROR_EOF && ret < 0) av_log(NULL, AV_LOG_WARNING, "Flush the frame error.\n"); } diff --git a/fftools/ffmpeg_demux.c b/fftools/ffmpeg_demux.c index 817ccbbedc..5c15b8bad3 100644 --- a/fftools/ffmpeg_demux.c +++ b/fftools/ffmpeg_demux.c @@ -817,7 +817,6 @@ static void ist_free(InputStream **pist) av_dict_free(&ist->decoder_opts); avsubtitle_free(&ist->prev_sub.subtitle); - av_frame_free(&ist->sub2video.frame); av_freep(&ist->filters); av_freep(&ist->outputs); av_freep(&ist->hwaccel_device); diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c index 670e697f69..ce2a914745 100644 --- a/fftools/ffmpeg_filter.c +++ b/fftools/ffmpeg_filter.c @@ -107,6 +107,14 @@ typedef struct InputFilterPriv { struct { ///< queue of AVSubtitle* before filter init AVFifo *queue; + + AVFrame *frame; + + int64_t last_pts; + int64_t end_pts; + + ///< marks if sub2video_update should force an initialization + unsigned int initialize; } sub2video; } InputFilterPriv; @@ -115,6 +123,111 @@ static InputFilterPriv *ifp_from_ifilter(InputFilter *ifilter) return (InputFilterPriv*)ifilter; } +static int sub2video_get_blank_frame(InputFilterPriv *ifp) +{ + AVFrame *frame = ifp->sub2video.frame; + int ret; + + av_frame_unref(frame); + + frame->width = ifp->width; + frame->height = ifp->height; + frame->format = ifp->format; + + ret = av_frame_get_buffer(frame, 0); + if (ret < 0) + return ret; + + memset(frame->data[0], 0, frame->height * frame->linesize[0]); + + return 0; +} + +static void sub2video_copy_rect(uint8_t *dst, int dst_linesize, int w, int h, + AVSubtitleRect *r) +{ + uint32_t *pal, *dst2; + uint8_t *src, *src2; + int x, y; + + if (r->type != SUBTITLE_BITMAP) { + av_log(NULL, AV_LOG_WARNING, "sub2video: non-bitmap subtitle\n"); + return; + } + if (r->x < 0 || r->x + r->w > w || r->y < 0 || r->y + r->h > h) { + av_log(NULL, AV_LOG_WARNING, "sub2video: rectangle (%d %d %d %d) overflowing %d %d\n", + r->x, r->y, r->w, r->h, w, h + ); + return; + } + + dst += r->y * dst_linesize + r->x * 4; + src = r->data[0]; + pal = (uint32_t *)r->data[1]; + for (y = 0; y < r->h; y++) { + dst2 = (uint32_t *)dst; + src2 = src; + for (x = 0; x < r->w; x++) + *(dst2++) = pal[*(src2++)]; + dst += dst_linesize; + src += r->linesize[0]; + } +} + +static void sub2video_push_ref(InputFilterPriv *ifp, int64_t pts) +{ + AVFrame *frame = ifp->sub2video.frame; + int ret; + + av_assert1(frame->data[0]); + ifp->sub2video.last_pts = frame->pts = pts; + ret = av_buffersrc_add_frame_flags(ifp->ifilter.filter, frame, + AV_BUFFERSRC_FLAG_KEEP_REF | + AV_BUFFERSRC_FLAG_PUSH); + if (ret != AVERROR_EOF && ret < 0) + av_log(NULL, AV_LOG_WARNING, "Error while add the frame to buffer source(%s).\n", + av_err2str(ret)); +} + +static void sub2video_update(InputFilterPriv *ifp, int64_t heartbeat_pts, + const AVSubtitle *sub) +{ + AVFrame *frame = ifp->sub2video.frame; + int8_t *dst; + int dst_linesize; + int num_rects, i; + int64_t pts, end_pts; + + if (sub) { + pts = av_rescale_q(sub->pts + sub->start_display_time * 1000LL, + AV_TIME_BASE_Q, ifp->time_base); + end_pts = av_rescale_q(sub->pts + sub->end_display_time * 1000LL, + AV_TIME_BASE_Q, ifp->time_base); + num_rects = sub->num_rects; + } else { + /* If we are initializing the system, utilize current heartbeat + PTS as the start time, and show until the following subpicture + is received. Otherwise, utilize the previous subpicture's end time + as the fall-back value. */ + pts = ifp->sub2video.initialize ? + heartbeat_pts : ifp->sub2video.end_pts; + end_pts = INT64_MAX; + num_rects = 0; + } + if (sub2video_get_blank_frame(ifp) < 0) { + av_log(NULL, AV_LOG_ERROR, + "Impossible to get a blank canvas.\n"); + return; + } + dst = frame->data [0]; + dst_linesize = frame->linesize[0]; + for (i = 0; i < num_rects; i++) + sub2video_copy_rect(dst, dst_linesize, frame->width, frame->height, sub->rects[i]); + sub2video_push_ref(ifp, pts); + ifp->sub2video.end_pts = end_pts; + ifp->sub2video.initialize = 0; +} + // FIXME: YUV420P etc. are actually supported with full color range, // yet the latter information isn't available here. static const enum AVPixelFormat *get_compliance_normal_pix_fmts(const AVCodec *codec, const enum AVPixelFormat default_formats[]) @@ -465,6 +578,12 @@ static int ifilter_bind_ist(InputFilter *ifilter, InputStream *ist) ifp->ist = ist; ifp->type_src = ist->st->codecpar->codec_type; + if (ifp->type_src == AVMEDIA_TYPE_SUBTITLE) { + ifp->sub2video.frame = av_frame_alloc(); + if (!ifp->sub2video.frame) + return AVERROR(ENOMEM); + } + return 0; } @@ -610,6 +729,7 @@ void fg_free(FilterGraph **pfg) avsubtitle_free(&sub); av_fifo_freep2(&ifp->sub2video.queue); } + av_frame_free(&ifp->sub2video.frame); av_channel_layout_uninit(&ifp->fallback.ch_layout); @@ -1108,20 +1228,15 @@ void check_filter_outputs(void) } } -static int sub2video_prepare(InputStream *ist, InputFilter *ifilter) +static void sub2video_prepare(InputFilterPriv *ifp) { - ist->sub2video.frame = av_frame_alloc(); - if (!ist->sub2video.frame) - return AVERROR(ENOMEM); - ist->sub2video.last_pts = INT64_MIN; - ist->sub2video.end_pts = INT64_MIN; + ifp->sub2video.last_pts = INT64_MIN; + ifp->sub2video.end_pts = INT64_MIN; /* sub2video structure has been (re-)initialized. Mark it as such so that the system will be initialized with the first received heartbeat. */ - ist->sub2video.initialize = 1; - - return 0; + ifp->sub2video.initialize = 1; } static int configure_input_video_filter(FilterGraph *fg, InputFilter *ifilter, @@ -1156,11 +1271,8 @@ static int configure_input_video_filter(FilterGraph *fg, InputFilter *ifilter, if (!fr.num) fr = ist->framerate_guessed; - if (ist->dec_ctx->codec_type == AVMEDIA_TYPE_SUBTITLE) { - ret = sub2video_prepare(ist, ifilter); - if (ret < 0) - goto fail; - } + if (ifp->type_src == AVMEDIA_TYPE_SUBTITLE) + sub2video_prepare(ifp); ifp->time_base = ist->framerate.num ? av_inv_q(ist->framerate) : ist->st->time_base; @@ -1466,12 +1578,13 @@ int configure_filtergraph(FilterGraph *fg) /* process queued up subtitle packets */ for (i = 0; i < fg->nb_inputs; i++) { - InputFilterPriv *ifp = ifp_from_ifilter(fg->inputs[i]); - InputStream *ist = ifp->ist; - if (ifp->sub2video.queue && ist->sub2video.frame) { + InputFilter *ifilter = fg->inputs[i]; + InputFilterPriv *ifp = ifp_from_ifilter(ifilter); + + if (ifp->type_src == AVMEDIA_TYPE_SUBTITLE && ifp->sub2video.queue) { AVSubtitle tmp; while (av_fifo_read(ifp->sub2video.queue, &tmp, 1) >= 0) { - sub2video_update(ist, INT64_MIN, &tmp); + sub2video_update(ifp, INT64_MIN, &tmp); avsubtitle_free(&tmp); } } @@ -1620,15 +1733,47 @@ int reap_filters(int flush) return 0; } +void ifilter_sub2video_heartbeat(InputFilter *ifilter, int64_t pts, AVRational tb) +{ + InputFilterPriv *ifp = ifp_from_ifilter(ifilter); + int64_t pts2; + + if (!ifilter->graph->graph) + return; + + /* subtitles seem to be usually muxed ahead of other streams; + if not, subtracting a larger time here is necessary */ + pts2 = av_rescale_q(pts, tb, ifp->time_base) - 1; + + /* do not send the heartbeat frame if the subtitle is already ahead */ + if (pts2 <= ifp->sub2video.last_pts) + return; + + if (pts2 >= ifp->sub2video.end_pts || ifp->sub2video.initialize) + /* if we have hit the end of the current displayed subpicture, + or if we need to initialize the system, update the + overlayed subpicture and its start/end times */ + sub2video_update(ifp, pts2 + 1, NULL); + + if (av_buffersrc_get_nb_failed_requests(ifilter->filter)) + sub2video_push_ref(ifp, pts2); +} + int ifilter_sub2video(InputFilter *ifilter, const AVSubtitle *subtitle) { InputFilterPriv *ifp = ifp_from_ifilter(ifilter); - InputStream *ist = ifp->ist; int ret; - if (ist->sub2video.frame) { - sub2video_update(ist, INT64_MIN, subtitle); - } else { + if (ifilter->graph->graph) { + if (!subtitle) { + if (ifp->sub2video.end_pts < INT64_MAX) + sub2video_update(ifp, INT64_MAX, NULL); + + return av_buffersrc_add_frame(ifilter->filter, NULL); + } + + sub2video_update(ifp, INT64_MIN, subtitle); + } else if (subtitle) { AVSubtitle sub; if (!ifp->sub2video.queue) -- 2.40.1 _______________________________________________ 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:[~2023-05-28 9:16 UTC|newest] Thread overview: 27+ messages / expand[flat|nested] mbox.gz Atom feed top 2023-05-28 9:13 [FFmpeg-devel] [PATCH 01/24] fftools/ffmpeg_mux_init: merge ost_add_from_filter() to ost_add() Anton Khirnov 2023-05-28 9:13 ` [FFmpeg-devel] [PATCH 02/24] fftools/ffmpeg: add logging for creating output streams Anton Khirnov 2023-05-28 9:13 ` [FFmpeg-devel] [PATCH 03/24] fftools/ffmpeg_filter: use a dedicated variable for marking simple filtergraphs Anton Khirnov 2023-05-28 9:13 ` [FFmpeg-devel] [PATCH 04/24] fftools/ffmpeg_filter: always pass graph description to fg_create() Anton Khirnov 2023-05-28 9:13 ` [FFmpeg-devel] [PATCH 05/24] fftools/ffmpeg_filter: store just the link label in OutputFilter Anton Khirnov 2023-05-28 9:13 ` [FFmpeg-devel] [PATCH 06/24] fftools/ffmpeg_filter: decouple allocating InputFilter and binding it to InputStream Anton Khirnov 2023-05-28 9:13 ` [FFmpeg-devel] [PATCH 07/24] fftools/ffmpeg_filter: move some functions higher up Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 08/24] fftools/ffmpeg_filter: create Input/OutputFilters together with FilterGraph Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 09/24] fftools/ffmpeg_filter: factor out binding an output stream to OutputFilter Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 10/24] fftools/ffmpeg_mux_init: move OutputFilter setup code to ffmpeg_filter Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 11/24] fftools/ffmpeg_filter: try to configure filtergraphs earlier Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 12/24] fftools/ffmpeg: constify AVSubtitle parameters as appropriate Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 13/24] fftools/ffmpeg_dec: move sub2video submission to ffmpeg_filter Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 14/24] fftools/ffmpeg_filter: move sub2video subtitle queue to InputFilterPriv Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 15/24] fftools/ffmpeg: tweak sub2video_heartbeat() arguments Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 16/24] fftools/ffmpeg: rework setting sub2video parameters Anton Khirnov 2023-05-28 19:43 ` Michael Niedermayer 2023-05-29 12:49 ` [FFmpeg-devel] [PATCH] " Anton Khirnov 2023-05-31 17:02 ` Michael Niedermayer 2023-05-28 9:14 ` Anton Khirnov [this message] 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 18/24] fftools/ffmpeg_enc: stop configuring filters from encoder flush Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 19/24] fftools/ffmpeg_filter: drop unreachable code Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 20/24] fftools/ffmpeg_filter: make ifilter_has_all_input_formats() static Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 21/24] fftools/ffmpeg_filter: make InputStream.filter private Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 22/24] fftools/ffmpeg_filter: constify the argument of filtergraph_is_simple() Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 23/24] fftools/ffmpeg_mux: flush bsfs immediately on exceeding recoding time Anton Khirnov 2023-05-28 9:14 ` [FFmpeg-devel] [PATCH 24/24] fftools/ffmpeg_filter: do not flush encoders on parameter change Anton Khirnov
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=20230528091416.17927-17-anton@khirnov.net \ --to=anton@khirnov.net \ --cc=ffmpeg-devel@ffmpeg.org \ /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