diff mbox series

[FFmpeg-devel] avformat: add a concat protocol that takes a line break delimited list of resources

Message ID 20210623125648.1092-1-jamrial@gmail.com
State Superseded
Headers show
Series [FFmpeg-devel] avformat: add a concat protocol that takes a line break delimited list of resources
Related show

Checks

Context Check Description
andriy/x86_make success Make finished
andriy/x86_make_fate success Make fate finished
andriy/PPC64_make success Make finished
andriy/PPC64_make_fate success Make fate finished

Commit Message

James Almer June 23, 2021, 12:56 p.m. UTC
Suggested-by: ffmpeg@fb.com
Signed-off-by: James Almer <jamrial@gmail.com>
---
 doc/protocols.texi      |  30 +++++++++++
 libavformat/Makefile    |   1 +
 libavformat/concat.c    | 111 ++++++++++++++++++++++++++++++++++++++++
 libavformat/protocols.c |   1 +
 4 files changed, 143 insertions(+)

Comments

Nicolas George June 24, 2021, 2:59 p.m. UTC | #1
James Almer (12021-06-23):
> Suggested-by: ffmpeg@fb.com
> Signed-off-by: James Almer <jamrial@gmail.com>
> ---
>  doc/protocols.texi      |  30 +++++++++++
>  libavformat/Makefile    |   1 +
>  libavformat/concat.c    | 111 ++++++++++++++++++++++++++++++++++++++++
>  libavformat/protocols.c |   1 +
>  4 files changed, 143 insertions(+)
> 
> diff --git a/doc/protocols.texi b/doc/protocols.texi
> index ccdfb6e439..2b8ce1b7d5 100644
> --- a/doc/protocols.texi
> +++ b/doc/protocols.texi
> @@ -215,6 +215,36 @@ ffplay concat:split1.mpeg\|split2.mpeg\|split3.mpeg
>  Note that you may need to escape the character "|" which is special for
>  many shells.
>  
> +@section concatf
> +
> +Physical concatenation protocol using a line break delimited list of
> +resources.
> +
> +Read and seek from many resources in sequence as if they were
> +a unique resource.
> +
> +A URL accepted by this protocol has the syntax:
> +@example
> +concatf:@var{URL}
> +@end example
> +
> +where @var{URL} is the url containing a line break delimited list of
> +resources to be concatenated, each one possibly specifying a distinct
> +protocol.
> +
> +For example to read a sequence of files @file{split1.mpeg},
> +@file{split2.mpeg}, @file{split3.mpeg} listed in separate lines within
> +a file @file{split.txt} with @command{ffplay} use the command:
> +@example
> +ffplay concatf:split.txt
> +@end example
> +Where @file{split.txt} contains the lines:
> +@example
> +split1.mpeg
> +split2.mpeg
> +split3.mpeg
> +@end example
> +
>  @section crypto
>  
>  AES-encrypted stream reading protocol.
> diff --git a/libavformat/Makefile b/libavformat/Makefile
> index c9ef564523..caca95802a 100644
> --- a/libavformat/Makefile
> +++ b/libavformat/Makefile
> @@ -616,6 +616,7 @@ OBJS-$(CONFIG_APPLEHTTP_PROTOCOL)        += hlsproto.o
>  OBJS-$(CONFIG_BLURAY_PROTOCOL)           += bluray.o
>  OBJS-$(CONFIG_CACHE_PROTOCOL)            += cache.o
>  OBJS-$(CONFIG_CONCAT_PROTOCOL)           += concat.o
> +OBJS-$(CONFIG_CONCATF_PROTOCOL)          += concat.o
>  OBJS-$(CONFIG_CRYPTO_PROTOCOL)           += crypto.o
>  OBJS-$(CONFIG_DATA_PROTOCOL)             += data_uri.o
>  OBJS-$(CONFIG_FFRTMPCRYPT_PROTOCOL)      += rtmpcrypt.o rtmpdigest.o rtmpdh.o
> diff --git a/libavformat/concat.c b/libavformat/concat.c
> index 278afd997d..d224b51db0 100644
> --- a/libavformat/concat.c
> +++ b/libavformat/concat.c
> @@ -22,9 +22,12 @@
>   */
>  
>  #include "libavutil/avstring.h"
> +#include "libavutil/bprint.h"
>  #include "libavutil/mem.h"
>  
>  #include "avformat.h"
> +#include "avio_internal.h"
> +#include "internal.h"
>  #include "url.h"
>  
>  #define AV_CAT_SEPARATOR "|"
> @@ -56,6 +59,7 @@ static av_cold int concat_close(URLContext *h)
>      return err < 0 ? -1 : 0;
>  }
>  
> +#if CONFIG_CONCAT_PROTOCOL
>  static av_cold int concat_open(URLContext *h, const char *uri, int flags)
>  {
>      char *node_uri = NULL;
> @@ -124,6 +128,7 @@ static av_cold int concat_open(URLContext *h, const char *uri, int flags)
>      data->total_size = total_size;
>      return err;
>  }
> +#endif
>  
>  static int concat_read(URLContext *h, unsigned char *buf, int size)
>  {
> @@ -188,6 +193,7 @@ static int64_t concat_seek(URLContext *h, int64_t pos, int whence)
>      return result;
>  }
>  
> +#if CONFIG_CONCAT_PROTOCOL
>  const URLProtocol ff_concat_protocol = {
>      .name           = "concat",
>      .url_open       = concat_open,
> @@ -197,3 +203,108 @@ const URLProtocol ff_concat_protocol = {
>      .priv_data_size = sizeof(struct concat_data),
>      .default_whitelist = "concat,file,subfile",
>  };
> +#endif
> +
> +#if CONFIG_CONCATF_PROTOCOL
> +static av_cold int concatf_open(URLContext *h, const char *uri, int flags)
> +{
> +    AVBPrint bp;
> +    struct concat_data  *data = h->priv_data;
> +    struct concat_nodes *nodes = NULL;
> +    AVIOContext *in = NULL;
> +    URLContext *uc;
> +    char *node_uri = NULL;
> +    int64_t size, total_size = 0;
> +    unsigned int nodes_size = 0;
> +    size_t i = 0;
> +    int err = 0;
> +
> +    if (!av_strstart(uri, "concatf:", &uri)) {
> +        av_log(h, AV_LOG_ERROR, "URL %s lacks prefix\n", uri);
> +        return AVERROR(EINVAL);
> +    }
> +
> +    /* handle input */
> +    if (!*uri)
> +        return AVERROR(ENOENT);
> +
> +    err = ffio_open_whitelist(&in, uri, AVIO_FLAG_READ, &h->interrupt_callback,
> +                              NULL, h->protocol_whitelist, h->protocol_blacklist);
> +    if (err < 0)
> +        return err;
> +
> +    av_bprint_init(&bp, 0, AV_BPRINT_SIZE_UNLIMITED);
> +
> +    for (i = 0; !avio_feof(in); i++) {
> +        uint8_t *cursor;
> +        size_t len = i;
> +

> +        if ((err = ff_read_line_to_bprint_overwrite(in, &bp)) <= 0) {

Are we ok that file with a \n in their name will not be supported?

> +            if (err == 0 && i == 0)
> +                err = AVERROR_INVALIDDATA;
> +            else if (err == AVERROR_EOF)
> +                err = 0;
> +            break;
> +        }
> +
> +        cursor = bp.str;

> +        node_uri = av_get_token((const char **)&cursor, "\t\r\n");

Does this protocol work with file names that contain an apostrophe or an
actual backslash?

> +        if (!node_uri) {
> +            err = AVERROR(ENOMEM);
> +            break;
> +        }
> +
> +        if (++len == SIZE_MAX / sizeof(*nodes)) {
> +            av_freep(&node_uri);
> +            err = AVERROR(ENAMETOOLONG);
> +            break;
> +        }
> +
> +        /* creating URLContext */
> +        err = ffurl_open_whitelist(&uc, node_uri, flags,
> +                                   &h->interrupt_callback, NULL, h->protocol_whitelist, h->protocol_blacklist, h);
> +        av_freep(&node_uri);
> +        if (err < 0)
> +            break;
> +
> +        /* creating size */
> +        if ((size = ffurl_size(uc)) < 0) {
> +            ffurl_close(uc);
> +            err = AVERROR(ENOSYS);
> +            break;
> +        }
> +
> +        nodes = av_fast_realloc(data->nodes, &nodes_size, sizeof(*nodes) * len);
> +        if (!nodes) {
> +            ffurl_close(uc);
> +            err = AVERROR(ENOMEM);
> +            break;
> +        }
> +        data->nodes = nodes;
> +
> +        /* assembling */
> +        data->nodes[i].uc   = uc;
> +        data->nodes[i].size = size;
> +        total_size += size;
> +    }
> +    avio_closep(&in);
> +    av_bprint_finalize(&bp, NULL);
> +    data->length = i;
> +
> +    if (err < 0)
> +        concat_close(h);
> +
> +    data->total_size = total_size;
> +    return err;
> +}
> +
> +const URLProtocol ff_concatf_protocol = {
> +    .name           = "concatf",
> +    .url_open       = concatf_open,
> +    .url_read       = concat_read,
> +    .url_seek       = concat_seek,
> +    .url_close      = concat_close,
> +    .priv_data_size = sizeof(struct concat_data),
> +    .default_whitelist = "concatf,concat,file,subfile",
> +};
> +#endif
> diff --git a/libavformat/protocols.c b/libavformat/protocols.c
> index 4b6b1c8e98..7f08f151b6 100644
> --- a/libavformat/protocols.c
> +++ b/libavformat/protocols.c
> @@ -27,6 +27,7 @@ extern const URLProtocol ff_async_protocol;
>  extern const URLProtocol ff_bluray_protocol;
>  extern const URLProtocol ff_cache_protocol;
>  extern const URLProtocol ff_concat_protocol;
> +extern const URLProtocol ff_concatf_protocol;
>  extern const URLProtocol ff_crypto_protocol;
>  extern const URLProtocol ff_data_protocol;
>  extern const URLProtocol ff_ffrtmpcrypt_protocol;

Regards,
James Almer June 24, 2021, 3:08 p.m. UTC | #2
On 6/24/2021 11:59 AM, Nicolas George wrote:
> James Almer (12021-06-23):
>> Suggested-by: ffmpeg@fb.com
>> Signed-off-by: James Almer <jamrial@gmail.com>
>> ---
>>   doc/protocols.texi      |  30 +++++++++++
>>   libavformat/Makefile    |   1 +
>>   libavformat/concat.c    | 111 ++++++++++++++++++++++++++++++++++++++++
>>   libavformat/protocols.c |   1 +
>>   4 files changed, 143 insertions(+)
>>
>> diff --git a/doc/protocols.texi b/doc/protocols.texi
>> index ccdfb6e439..2b8ce1b7d5 100644
>> --- a/doc/protocols.texi
>> +++ b/doc/protocols.texi
>> @@ -215,6 +215,36 @@ ffplay concat:split1.mpeg\|split2.mpeg\|split3.mpeg
>>   Note that you may need to escape the character "|" which is special for
>>   many shells.
>>   
>> +@section concatf
>> +
>> +Physical concatenation protocol using a line break delimited list of
>> +resources.
>> +
>> +Read and seek from many resources in sequence as if they were
>> +a unique resource.
>> +
>> +A URL accepted by this protocol has the syntax:
>> +@example
>> +concatf:@var{URL}
>> +@end example
>> +
>> +where @var{URL} is the url containing a line break delimited list of
>> +resources to be concatenated, each one possibly specifying a distinct
>> +protocol.
>> +
>> +For example to read a sequence of files @file{split1.mpeg},
>> +@file{split2.mpeg}, @file{split3.mpeg} listed in separate lines within
>> +a file @file{split.txt} with @command{ffplay} use the command:
>> +@example
>> +ffplay concatf:split.txt
>> +@end example
>> +Where @file{split.txt} contains the lines:
>> +@example
>> +split1.mpeg
>> +split2.mpeg
>> +split3.mpeg
>> +@end example
>> +
>>   @section crypto
>>   
>>   AES-encrypted stream reading protocol.
>> diff --git a/libavformat/Makefile b/libavformat/Makefile
>> index c9ef564523..caca95802a 100644
>> --- a/libavformat/Makefile
>> +++ b/libavformat/Makefile
>> @@ -616,6 +616,7 @@ OBJS-$(CONFIG_APPLEHTTP_PROTOCOL)        += hlsproto.o
>>   OBJS-$(CONFIG_BLURAY_PROTOCOL)           += bluray.o
>>   OBJS-$(CONFIG_CACHE_PROTOCOL)            += cache.o
>>   OBJS-$(CONFIG_CONCAT_PROTOCOL)           += concat.o
>> +OBJS-$(CONFIG_CONCATF_PROTOCOL)          += concat.o
>>   OBJS-$(CONFIG_CRYPTO_PROTOCOL)           += crypto.o
>>   OBJS-$(CONFIG_DATA_PROTOCOL)             += data_uri.o
>>   OBJS-$(CONFIG_FFRTMPCRYPT_PROTOCOL)      += rtmpcrypt.o rtmpdigest.o rtmpdh.o
>> diff --git a/libavformat/concat.c b/libavformat/concat.c
>> index 278afd997d..d224b51db0 100644
>> --- a/libavformat/concat.c
>> +++ b/libavformat/concat.c
>> @@ -22,9 +22,12 @@
>>    */
>>   
>>   #include "libavutil/avstring.h"
>> +#include "libavutil/bprint.h"
>>   #include "libavutil/mem.h"
>>   
>>   #include "avformat.h"
>> +#include "avio_internal.h"
>> +#include "internal.h"
>>   #include "url.h"
>>   
>>   #define AV_CAT_SEPARATOR "|"
>> @@ -56,6 +59,7 @@ static av_cold int concat_close(URLContext *h)
>>       return err < 0 ? -1 : 0;
>>   }
>>   
>> +#if CONFIG_CONCAT_PROTOCOL
>>   static av_cold int concat_open(URLContext *h, const char *uri, int flags)
>>   {
>>       char *node_uri = NULL;
>> @@ -124,6 +128,7 @@ static av_cold int concat_open(URLContext *h, const char *uri, int flags)
>>       data->total_size = total_size;
>>       return err;
>>   }
>> +#endif
>>   
>>   static int concat_read(URLContext *h, unsigned char *buf, int size)
>>   {
>> @@ -188,6 +193,7 @@ static int64_t concat_seek(URLContext *h, int64_t pos, int whence)
>>       return result;
>>   }
>>   
>> +#if CONFIG_CONCAT_PROTOCOL
>>   const URLProtocol ff_concat_protocol = {
>>       .name           = "concat",
>>       .url_open       = concat_open,
>> @@ -197,3 +203,108 @@ const URLProtocol ff_concat_protocol = {
>>       .priv_data_size = sizeof(struct concat_data),
>>       .default_whitelist = "concat,file,subfile",
>>   };
>> +#endif
>> +
>> +#if CONFIG_CONCATF_PROTOCOL
>> +static av_cold int concatf_open(URLContext *h, const char *uri, int flags)
>> +{
>> +    AVBPrint bp;
>> +    struct concat_data  *data = h->priv_data;
>> +    struct concat_nodes *nodes = NULL;
>> +    AVIOContext *in = NULL;
>> +    URLContext *uc;
>> +    char *node_uri = NULL;
>> +    int64_t size, total_size = 0;
>> +    unsigned int nodes_size = 0;
>> +    size_t i = 0;
>> +    int err = 0;
>> +
>> +    if (!av_strstart(uri, "concatf:", &uri)) {
>> +        av_log(h, AV_LOG_ERROR, "URL %s lacks prefix\n", uri);
>> +        return AVERROR(EINVAL);
>> +    }
>> +
>> +    /* handle input */
>> +    if (!*uri)
>> +        return AVERROR(ENOENT);
>> +
>> +    err = ffio_open_whitelist(&in, uri, AVIO_FLAG_READ, &h->interrupt_callback,
>> +                              NULL, h->protocol_whitelist, h->protocol_blacklist);
>> +    if (err < 0)
>> +        return err;
>> +
>> +    av_bprint_init(&bp, 0, AV_BPRINT_SIZE_UNLIMITED);
>> +
>> +    for (i = 0; !avio_feof(in); i++) {
>> +        uint8_t *cursor;
>> +        size_t len = i;
>> +
> 
>> +        if ((err = ff_read_line_to_bprint_overwrite(in, &bp)) <= 0) {
> 
> Are we ok that file with a \n in their name will not be supported?

Yes, since it's used to delimit files in the list.

> 
>> +            if (err == 0 && i == 0)
>> +                err = AVERROR_INVALIDDATA;
>> +            else if (err == AVERROR_EOF)
>> +                err = 0;
>> +            break;
>> +        }
>> +
>> +        cursor = bp.str;
> 
>> +        node_uri = av_get_token((const char **)&cursor, "\t\r\n");
> 
> Does this protocol work with file names that contain an apostrophe or an
> actual backslash?

At least on Windows a backslash is not allowed on file names (Alongside 
many other such characters), but apostrophes are, and they effectively 
need to be escaped. Thanks for catching it.

I'll add a mention in the doxy about the need to escape certain characters.

> 
>> +        if (!node_uri) {
>> +            err = AVERROR(ENOMEM);
>> +            break;
>> +        }
>> +
>> +        if (++len == SIZE_MAX / sizeof(*nodes)) {
>> +            av_freep(&node_uri);
>> +            err = AVERROR(ENAMETOOLONG);
>> +            break;
>> +        }
>> +
>> +        /* creating URLContext */
>> +        err = ffurl_open_whitelist(&uc, node_uri, flags,
>> +                                   &h->interrupt_callback, NULL, h->protocol_whitelist, h->protocol_blacklist, h);
>> +        av_freep(&node_uri);
>> +        if (err < 0)
>> +            break;
>> +
>> +        /* creating size */
>> +        if ((size = ffurl_size(uc)) < 0) {
>> +            ffurl_close(uc);
>> +            err = AVERROR(ENOSYS);
>> +            break;
>> +        }
>> +
>> +        nodes = av_fast_realloc(data->nodes, &nodes_size, sizeof(*nodes) * len);
>> +        if (!nodes) {
>> +            ffurl_close(uc);
>> +            err = AVERROR(ENOMEM);
>> +            break;
>> +        }
>> +        data->nodes = nodes;
>> +
>> +        /* assembling */
>> +        data->nodes[i].uc   = uc;
>> +        data->nodes[i].size = size;
>> +        total_size += size;
>> +    }
>> +    avio_closep(&in);
>> +    av_bprint_finalize(&bp, NULL);
>> +    data->length = i;
>> +
>> +    if (err < 0)
>> +        concat_close(h);
>> +
>> +    data->total_size = total_size;
>> +    return err;
>> +}
>> +
>> +const URLProtocol ff_concatf_protocol = {
>> +    .name           = "concatf",
>> +    .url_open       = concatf_open,
>> +    .url_read       = concat_read,
>> +    .url_seek       = concat_seek,
>> +    .url_close      = concat_close,
>> +    .priv_data_size = sizeof(struct concat_data),
>> +    .default_whitelist = "concatf,concat,file,subfile",
>> +};
>> +#endif
>> diff --git a/libavformat/protocols.c b/libavformat/protocols.c
>> index 4b6b1c8e98..7f08f151b6 100644
>> --- a/libavformat/protocols.c
>> +++ b/libavformat/protocols.c
>> @@ -27,6 +27,7 @@ extern const URLProtocol ff_async_protocol;
>>   extern const URLProtocol ff_bluray_protocol;
>>   extern const URLProtocol ff_cache_protocol;
>>   extern const URLProtocol ff_concat_protocol;
>> +extern const URLProtocol ff_concatf_protocol;
>>   extern const URLProtocol ff_crypto_protocol;
>>   extern const URLProtocol ff_data_protocol;
>>   extern const URLProtocol ff_ffrtmpcrypt_protocol;
> 
> Regards,
>
Nicolas George June 24, 2021, 3:44 p.m. UTC | #3
James Almer (12021-06-24):
> > Are we ok that file with a \n in their name will not be supported?
> Yes, since it's used to delimit files in the list.

I know, it is the reason I asked. So, I say it negatively:

I am not ok that this prevents valid filenames from being used, even if
they are very exotic.

> At least on Windows a backslash is not allowed on file names (Alongside many
> other such characters), but apostrophes are, and they effectively need to be
> escaped. Thanks for catching it.
> 
> I'll add a mention in the doxy about the need to escape certain characters.

On Unix, backslashes are allowed. So are newlines. They need to be
supported.

Regards,
James Almer June 24, 2021, 4:11 p.m. UTC | #4
On 6/24/2021 12:44 PM, Nicolas George wrote:
> James Almer (12021-06-24):
>>> Are we ok that file with a \n in their name will not be supported?
>> Yes, since it's used to delimit files in the list.
> 
> I know, it is the reason I asked. So, I say it negatively:
> 
> I am not ok that this prevents valid filenames from being used, even if
> they are very exotic.

Can the \n in filenames be escaped, either with backslash or quotes, 
like with other special characters? If so, then they should be supported 
that way already.

> 
>> At least on Windows a backslash is not allowed on file names (Alongside many
>> other such characters), but apostrophes are, and they effectively need to be
>> escaped. Thanks for catching it.
>>
>> I'll add a mention in the doxy about the need to escape certain characters.
> 
> On Unix, backslashes are allowed. So are newlines. They need to be
> supported.

The former is supported, of course. And i assume so should the latter.

> 
> Regards,
> 
> 
> _______________________________________________
> 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".
>
Nicolas George June 24, 2021, 4:38 p.m. UTC | #5
James Almer (12021-06-24):
> Can the \n in filenames be escaped, either with backslash or quotes, like
> with other special characters? If so, then they should be supported that way
> already.

Please test. Reading your code tells me it does not work as is, but it
could. You are making a common mistake there: you are parsing the same
thing twice differently, once with ff_read_line_to_bprint_overwrite()
and once with av_get_token().

Regards,
James Almer June 24, 2021, 9:07 p.m. UTC | #6
On 6/24/2021 1:38 PM, Nicolas George wrote:
> James Almer (12021-06-24):
>> Can the \n in filenames be escaped, either with backslash or quotes, like
>> with other special characters? If so, then they should be supported that way
>> already.
> 
> Please test. Reading your code tells me it does not work as is, but it
> could. You are making a common mistake there: you are parsing the same
> thing twice differently, once with ff_read_line_to_bprint_overwrite()
> and once with av_get_token().

Can you tell me how to create a file with a \n in its name? I can test 
on Archlinux.

Thanks.

> 
> Regards,
> 
> 
> _______________________________________________
> 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".
>
Nicolas George June 24, 2021, 9:34 p.m. UTC | #7
James Almer (12021-06-24):
> Can you tell me how to create a file with a \n in its name? I can test on
> Archlinux.

touch 'foo
bar'

With some shells, you can write:

touch $'foo\nbar'

Regards,
diff mbox series

Patch

diff --git a/doc/protocols.texi b/doc/protocols.texi
index ccdfb6e439..2b8ce1b7d5 100644
--- a/doc/protocols.texi
+++ b/doc/protocols.texi
@@ -215,6 +215,36 @@  ffplay concat:split1.mpeg\|split2.mpeg\|split3.mpeg
 Note that you may need to escape the character "|" which is special for
 many shells.
 
+@section concatf
+
+Physical concatenation protocol using a line break delimited list of
+resources.
+
+Read and seek from many resources in sequence as if they were
+a unique resource.
+
+A URL accepted by this protocol has the syntax:
+@example
+concatf:@var{URL}
+@end example
+
+where @var{URL} is the url containing a line break delimited list of
+resources to be concatenated, each one possibly specifying a distinct
+protocol.
+
+For example to read a sequence of files @file{split1.mpeg},
+@file{split2.mpeg}, @file{split3.mpeg} listed in separate lines within
+a file @file{split.txt} with @command{ffplay} use the command:
+@example
+ffplay concatf:split.txt
+@end example
+Where @file{split.txt} contains the lines:
+@example
+split1.mpeg
+split2.mpeg
+split3.mpeg
+@end example
+
 @section crypto
 
 AES-encrypted stream reading protocol.
diff --git a/libavformat/Makefile b/libavformat/Makefile
index c9ef564523..caca95802a 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -616,6 +616,7 @@  OBJS-$(CONFIG_APPLEHTTP_PROTOCOL)        += hlsproto.o
 OBJS-$(CONFIG_BLURAY_PROTOCOL)           += bluray.o
 OBJS-$(CONFIG_CACHE_PROTOCOL)            += cache.o
 OBJS-$(CONFIG_CONCAT_PROTOCOL)           += concat.o
+OBJS-$(CONFIG_CONCATF_PROTOCOL)          += concat.o
 OBJS-$(CONFIG_CRYPTO_PROTOCOL)           += crypto.o
 OBJS-$(CONFIG_DATA_PROTOCOL)             += data_uri.o
 OBJS-$(CONFIG_FFRTMPCRYPT_PROTOCOL)      += rtmpcrypt.o rtmpdigest.o rtmpdh.o
diff --git a/libavformat/concat.c b/libavformat/concat.c
index 278afd997d..d224b51db0 100644
--- a/libavformat/concat.c
+++ b/libavformat/concat.c
@@ -22,9 +22,12 @@ 
  */
 
 #include "libavutil/avstring.h"
+#include "libavutil/bprint.h"
 #include "libavutil/mem.h"
 
 #include "avformat.h"
+#include "avio_internal.h"
+#include "internal.h"
 #include "url.h"
 
 #define AV_CAT_SEPARATOR "|"
@@ -56,6 +59,7 @@  static av_cold int concat_close(URLContext *h)
     return err < 0 ? -1 : 0;
 }
 
+#if CONFIG_CONCAT_PROTOCOL
 static av_cold int concat_open(URLContext *h, const char *uri, int flags)
 {
     char *node_uri = NULL;
@@ -124,6 +128,7 @@  static av_cold int concat_open(URLContext *h, const char *uri, int flags)
     data->total_size = total_size;
     return err;
 }
+#endif
 
 static int concat_read(URLContext *h, unsigned char *buf, int size)
 {
@@ -188,6 +193,7 @@  static int64_t concat_seek(URLContext *h, int64_t pos, int whence)
     return result;
 }
 
+#if CONFIG_CONCAT_PROTOCOL
 const URLProtocol ff_concat_protocol = {
     .name           = "concat",
     .url_open       = concat_open,
@@ -197,3 +203,108 @@  const URLProtocol ff_concat_protocol = {
     .priv_data_size = sizeof(struct concat_data),
     .default_whitelist = "concat,file,subfile",
 };
+#endif
+
+#if CONFIG_CONCATF_PROTOCOL
+static av_cold int concatf_open(URLContext *h, const char *uri, int flags)
+{
+    AVBPrint bp;
+    struct concat_data  *data = h->priv_data;
+    struct concat_nodes *nodes = NULL;
+    AVIOContext *in = NULL;
+    URLContext *uc;
+    char *node_uri = NULL;
+    int64_t size, total_size = 0;
+    unsigned int nodes_size = 0;
+    size_t i = 0;
+    int err = 0;
+
+    if (!av_strstart(uri, "concatf:", &uri)) {
+        av_log(h, AV_LOG_ERROR, "URL %s lacks prefix\n", uri);
+        return AVERROR(EINVAL);
+    }
+
+    /* handle input */
+    if (!*uri)
+        return AVERROR(ENOENT);
+
+    err = ffio_open_whitelist(&in, uri, AVIO_FLAG_READ, &h->interrupt_callback,
+                              NULL, h->protocol_whitelist, h->protocol_blacklist);
+    if (err < 0)
+        return err;
+
+    av_bprint_init(&bp, 0, AV_BPRINT_SIZE_UNLIMITED);
+
+    for (i = 0; !avio_feof(in); i++) {
+        uint8_t *cursor;
+        size_t len = i;
+
+        if ((err = ff_read_line_to_bprint_overwrite(in, &bp)) <= 0) {
+            if (err == 0 && i == 0)
+                err = AVERROR_INVALIDDATA;
+            else if (err == AVERROR_EOF)
+                err = 0;
+            break;
+        }
+
+        cursor = bp.str;
+        node_uri = av_get_token((const char **)&cursor, "\t\r\n");
+        if (!node_uri) {
+            err = AVERROR(ENOMEM);
+            break;
+        }
+
+        if (++len == SIZE_MAX / sizeof(*nodes)) {
+            av_freep(&node_uri);
+            err = AVERROR(ENAMETOOLONG);
+            break;
+        }
+
+        /* creating URLContext */
+        err = ffurl_open_whitelist(&uc, node_uri, flags,
+                                   &h->interrupt_callback, NULL, h->protocol_whitelist, h->protocol_blacklist, h);
+        av_freep(&node_uri);
+        if (err < 0)
+            break;
+
+        /* creating size */
+        if ((size = ffurl_size(uc)) < 0) {
+            ffurl_close(uc);
+            err = AVERROR(ENOSYS);
+            break;
+        }
+
+        nodes = av_fast_realloc(data->nodes, &nodes_size, sizeof(*nodes) * len);
+        if (!nodes) {
+            ffurl_close(uc);
+            err = AVERROR(ENOMEM);
+            break;
+        }
+        data->nodes = nodes;
+
+        /* assembling */
+        data->nodes[i].uc   = uc;
+        data->nodes[i].size = size;
+        total_size += size;
+    }
+    avio_closep(&in);
+    av_bprint_finalize(&bp, NULL);
+    data->length = i;
+
+    if (err < 0)
+        concat_close(h);
+
+    data->total_size = total_size;
+    return err;
+}
+
+const URLProtocol ff_concatf_protocol = {
+    .name           = "concatf",
+    .url_open       = concatf_open,
+    .url_read       = concat_read,
+    .url_seek       = concat_seek,
+    .url_close      = concat_close,
+    .priv_data_size = sizeof(struct concat_data),
+    .default_whitelist = "concatf,concat,file,subfile",
+};
+#endif
diff --git a/libavformat/protocols.c b/libavformat/protocols.c
index 4b6b1c8e98..7f08f151b6 100644
--- a/libavformat/protocols.c
+++ b/libavformat/protocols.c
@@ -27,6 +27,7 @@  extern const URLProtocol ff_async_protocol;
 extern const URLProtocol ff_bluray_protocol;
 extern const URLProtocol ff_cache_protocol;
 extern const URLProtocol ff_concat_protocol;
+extern const URLProtocol ff_concatf_protocol;
 extern const URLProtocol ff_crypto_protocol;
 extern const URLProtocol ff_data_protocol;
 extern const URLProtocol ff_ffrtmpcrypt_protocol;