diff mbox series

[FFmpeg-devel,2/2] ffprobe: add -o option

Message ID 20210418213058.24475-2-stefasab@gmail.com
State New
Headers show
Series [FFmpeg-devel,1/2] lavf/avio: add avio_vprintf() | expand

Checks

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

Commit Message

Stefano Sabatini April 18, 2021, 9:30 p.m. UTC
This enables printing to a resource specified with -o OUTPUT.

Address issue: http://trac.ffmpeg.org/ticket/8024
---
 doc/ffprobe.texi  |   7 ++
 fftools/ffprobe.c | 174 ++++++++++++++++++++++++++++++----------------
 2 files changed, 120 insertions(+), 61 deletions(-)

Comments

Michael Niedermayer April 19, 2021, 9:26 a.m. UTC | #1
On Sun, Apr 18, 2021 at 11:30:58PM +0200, Stefano Sabatini wrote:
> This enables printing to a resource specified with -o OUTPUT.
> 
> Address issue: http://trac.ffmpeg.org/ticket/8024
> ---
>  doc/ffprobe.texi  |   7 ++
>  fftools/ffprobe.c | 174 ++++++++++++++++++++++++++++++----------------
>  2 files changed, 120 insertions(+), 61 deletions(-)

This seems breaking some fate tests like fate-gaplessenc-itunes-to-ipod-aac

I see 0 bytes in the output:
  73 3D 4B 44  00 73 69 64  65 5F 64 61  74 61 7C 0A  0A
  
thx
  
[...]
Stefano Sabatini April 21, 2021, 9:57 p.m. UTC | #2
On date Monday 2021-04-19 11:26:49 +0200, Michael Niedermayer wrote:
> On Sun, Apr 18, 2021 at 11:30:58PM +0200, Stefano Sabatini wrote:
> > This enables printing to a resource specified with -o OUTPUT.
> > 
> > Address issue: http://trac.ffmpeg.org/ticket/8024
> > ---
> >  doc/ffprobe.texi  |   7 ++
> >  fftools/ffprobe.c | 174 ++++++++++++++++++++++++++++++----------------
> >  2 files changed, 120 insertions(+), 61 deletions(-)
> 
> This seems breaking some fate tests like fate-gaplessenc-itunes-to-ipod-aac
> 
> I see 0 bytes in the output:
>   73 3D 4B 44  00 73 69 64  65 5F 64 61  74 61 7C 0A  0A
>   
> thx

That's right, I'm still forgetting to run make fate :-)
(avio_put_str() was adding the 0), should be fixed now.

Thanks.
Gyan Doshi April 22, 2021, 6:01 a.m. UTC | #3
On 2021-04-22 03:27, Stefano Sabatini wrote:
> On date Monday 2021-04-19 11:26:49 +0200, Michael Niedermayer wrote:
>> On Sun, Apr 18, 2021 at 11:30:58PM +0200, Stefano Sabatini wrote:
>>> This enables printing to a resource specified with -o OUTPUT.
>>>
>>> Address issue: http://trac.ffmpeg.org/ticket/8024
>>> ---
>>>   doc/ffprobe.texi  |   7 ++
>>>   fftools/ffprobe.c | 174 ++++++++++++++++++++++++++++++----------------
>>>   2 files changed, 120 insertions(+), 61 deletions(-)
>> This seems breaking some fate tests like fate-gaplessenc-itunes-to-ipod-aac
>>
>> I see 0 bytes in the output:
>>    73 3D 4B 44  00 73 69 64  65 5F 64 61  74 61 7C 0A  0A
>>    
>> thx
> That's right, I'm still forgetting to run make fate :-)
> (avio_put_str() was adding the 0), should be fixed now.

If you post your patches inline, patchwork should pick them up and run 
fate. You can check the logs like here:

https://patchwork.ffmpeg.org/project/ffmpeg/patch/20210421174055.65029-1-jamrial@gmail.com/

Regards,
Gyan
Stefano Sabatini April 3, 2022, 2:06 p.m. UTC | #4
On date Wednesday 2021-04-21 23:57:04 +0200, Stefano Sabatini wrote:
> On date Monday 2021-04-19 11:26:49 +0200, Michael Niedermayer wrote:
> > On Sun, Apr 18, 2021 at 11:30:58PM +0200, Stefano Sabatini wrote:
> > > This enables printing to a resource specified with -o OUTPUT.
> > > 
> > > Address issue: http://trac.ffmpeg.org/ticket/8024
> > > ---
> > >  doc/ffprobe.texi  |   7 ++
> > >  fftools/ffprobe.c | 174 ++++++++++++++++++++++++++++++----------------
> > >  2 files changed, 120 insertions(+), 61 deletions(-)
> > 
> > This seems breaking some fate tests like fate-gaplessenc-itunes-to-ipod-aac
> > 
> > I see 0 bytes in the output:
> >   73 3D 4B 44  00 73 69 64  65 5F 64 61  74 61 7C 0A  0A
> >   
> > thx
> 
> That's right, I'm still forgetting to run make fate :-)
> (avio_put_str() was adding the 0), should be fixed now.
> 
> Thanks.

Updated again, now it's locally passing fate with FATE samples (don't
remember if other issues were spotted the past time).

Best regards,
Stefano
Marton Balint June 9, 2022, 7:09 p.m. UTC | #5
On Sun, 3 Apr 2022, Stefano Sabatini wrote:

> On date Wednesday 2021-04-21 23:57:04 +0200, Stefano Sabatini wrote:
>> On date Monday 2021-04-19 11:26:49 +0200, Michael Niedermayer wrote:
>>> On Sun, Apr 18, 2021 at 11:30:58PM +0200, Stefano Sabatini wrote:
>>>> This enables printing to a resource specified with -o OUTPUT.
>>>>
>>>> Address issue: http://trac.ffmpeg.org/ticket/8024
>>>> ---
>>>>  doc/ffprobe.texi  |   7 ++
>>>>  fftools/ffprobe.c | 174 ++++++++++++++++++++++++++++++----------------
>>>>  2 files changed, 120 insertions(+), 61 deletions(-)
>>>
>>> This seems breaking some fate tests like fate-gaplessenc-itunes-to-ipod-aac
>>>
>>> I see 0 bytes in the output:
>>>   73 3D 4B 44  00 73 69 64  65 5F 64 61  74 61 7C 0A  0A
>>>
>>> thx
>>
>> That's right, I'm still forgetting to run make fate :-)
>> (avio_put_str() was adding the 0), should be fixed now.
>>
>> Thanks.
>
> Updated again, now it's locally passing fate with FATE samples (don't
> remember if other issues were spotted the past time).

This looks good to me in general, so I intend to apply. One thing that we 
might do is to keep writing to the standard output if no output file is 
specified instead of using the special "pipe:" URL. This way ffprobe 
keeps working even if the pipe protocol is not compiled into 
ffmpeg/libavformat.

Regards,
Marton
Stefano Sabatini June 12, 2022, 3:33 p.m. UTC | #6
On date Thursday 2022-06-09 21:09:02 +0200, Marton Balint wrote:
> On Sun, 3 Apr 2022, Stefano Sabatini wrote:
[...]
> > Updated again, now it's locally passing fate with FATE samples (don't
> > remember if other issues were spotted the past time).
> 
> This looks good to me in general, so I intend to apply. One thing that we
> might do is to keep writing to the standard output if no output file is
> specified instead of using the special "pipe:" URL. This way ffprobe keeps
> working even if the pipe protocol is not compiled into ffmpeg/libavformat.

Sounds good, patch updated accordingly.
Marton Balint June 13, 2022, 8:47 p.m. UTC | #7
On Sun, 12 Jun 2022, Stefano Sabatini wrote:

> On date Thursday 2022-06-09 21:09:02 +0200, Marton Balint wrote:
>> On Sun, 3 Apr 2022, Stefano Sabatini wrote:
> [...]
>>> Updated again, now it's locally passing fate with FATE samples (don't
>>> remember if other issues were spotted the past time).
>>
>> This looks good to me in general, so I intend to apply. One thing that we
>> might do is to keep writing to the standard output if no output file is
>> specified instead of using the special "pipe:" URL. This way ffprobe keeps
>> working even if the pipe protocol is not compiled into ffmpeg/libavformat.
>
> Sounds good, patch updated accordingly.

Thanks, applied the series.

Regards,
Marton
diff mbox series

Patch

diff --git a/doc/ffprobe.texi b/doc/ffprobe.texi
index d7fab4ff40..f57b46a8fd 100644
--- a/doc/ffprobe.texi
+++ b/doc/ffprobe.texi
@@ -28,6 +28,9 @@  If a url is specified in input, ffprobe will try to open and
 probe the url content. If the url cannot be opened or recognized as
 a multimedia file, a positive exit code is returned.
 
+If no output is specified as output with @option{o} ffprobe will write
+to stdout.
+
 ffprobe may be employed both as a standalone application or in
 combination with a textual filter, which may perform more
 sophisticated processing, e.g. statistical processing or plotting.
@@ -342,6 +345,10 @@  on the specific build.
 @item -i @var{input_url}
 Read @var{input_url}.
 
+@item -o @var{output_url}
+Write output to @var{output_url}. If not specified, the output is sent
+to stdout.
+
 @end table
 @c man end
 
diff --git a/fftools/ffprobe.c b/fftools/ffprobe.c
index 38462e1ff3..cdec261f29 100644
--- a/fftools/ffprobe.c
+++ b/fftools/ffprobe.c
@@ -258,6 +258,7 @@  static const OptionDef *options;
 static const char *input_filename;
 static const char *print_input_filename;
 static AVInputFormat *iformat = NULL;
+static const char *output_filename = NULL;
 
 static struct AVHashContext *hash;
 
@@ -453,6 +454,7 @@  typedef struct Writer {
 struct WriterContext {
     const AVClass *class;           ///< class of the writer
     const Writer *writer;           ///< the Writer of which this is an instance
+    AVIOContext *avio;              /// the I/O context used to write
     char *name;                     ///< name of this writer instance
     void *priv;                     ///< private data for use by the filter
 
@@ -530,6 +532,10 @@  static void writer_close(WriterContext **wctx)
         av_opt_free((*wctx)->priv);
     av_freep(&((*wctx)->priv));
     av_opt_free(*wctx);
+    if ((*wctx)->avio) {
+        avio_flush((*wctx)->avio);
+        avio_close((*wctx)->avio);
+    }
     av_freep(wctx);
 }
 
@@ -543,7 +549,7 @@  static void bprint_bytes(AVBPrint *bp, const uint8_t *ubuf, size_t ubuf_size)
 
 
 static int writer_open(WriterContext **wctx, const Writer *writer, const char *args,
-                       const struct section *sections, int nb_sections)
+                       const struct section *sections, int nb_sections, const char *url)
 {
     int i, ret = 0;
 
@@ -614,6 +620,9 @@  static int writer_open(WriterContext **wctx, const Writer *writer, const char *a
         }
     }
 
+    if ((ret = avio_open(&(*wctx)->avio, url, AVIO_FLAG_WRITE)) < 0)
+        goto fail;
+
     for (i = 0; i < SECTION_MAX_NB_LEVELS; i++)
         av_bprint_init(&(*wctx)->section_pbuf[i], 1, AV_BPRINT_SIZE_UNLIMITED);
 
@@ -879,6 +888,25 @@  static void writer_print_integers(WriterContext *wctx, const char *name,
     av_bprint_finalize(&bp, NULL);
 }
 
+static inline void writer_w8(WriterContext *wctx, int b)
+{
+    avio_w8(wctx->avio, b);
+}
+
+static inline void writer_put_str(WriterContext *wctx, const char *str)
+{
+    avio_put_str(wctx->avio, str);
+}
+
+static inline void writer_printf(WriterContext *wctx, const char *fmt, ...)
+{
+    va_list ap;
+
+    va_start(ap, fmt);
+    avio_vprintf(wctx->avio, fmt, ap);
+    va_end(ap);
+}
+
 #define MAX_REGISTERED_WRITERS_NB 64
 
 static const Writer *registered_writers[MAX_REGISTERED_WRITERS_NB + 1];
@@ -973,7 +1001,7 @@  static void default_print_section_header(WriterContext *wctx)
         return;
 
     if (!(section->flags & (SECTION_FLAG_IS_WRAPPER|SECTION_FLAG_IS_ARRAY)))
-        printf("[%s]\n", upcase_string(buf, sizeof(buf), section->name));
+        writer_printf(wctx, "[%s]\n", upcase_string(buf, sizeof(buf), section->name));
 }
 
 static void default_print_section_footer(WriterContext *wctx)
@@ -986,7 +1014,7 @@  static void default_print_section_footer(WriterContext *wctx)
         return;
 
     if (!(section->flags & (SECTION_FLAG_IS_WRAPPER|SECTION_FLAG_IS_ARRAY)))
-        printf("[/%s]\n", upcase_string(buf, sizeof(buf), section->name));
+        writer_printf(wctx, "[/%s]\n", upcase_string(buf, sizeof(buf), section->name));
 }
 
 static void default_print_str(WriterContext *wctx, const char *key, const char *value)
@@ -994,8 +1022,8 @@  static void default_print_str(WriterContext *wctx, const char *key, const char *
     DefaultContext *def = wctx->priv;
 
     if (!def->nokey)
-        printf("%s%s=", wctx->section_pbuf[wctx->level].str, key);
-    printf("%s\n", value);
+        writer_printf(wctx, "%s%s=", wctx->section_pbuf[wctx->level].str, key);
+    writer_printf(wctx, "%s\n", value);
 }
 
 static void default_print_int(WriterContext *wctx, const char *key, long long int value)
@@ -1003,8 +1031,8 @@  static void default_print_int(WriterContext *wctx, const char *key, long long in
     DefaultContext *def = wctx->priv;
 
     if (!def->nokey)
-        printf("%s%s=", wctx->section_pbuf[wctx->level].str, key);
-    printf("%lld\n", value);
+        writer_printf(wctx, "%s%s=", wctx->section_pbuf[wctx->level].str, key);
+    writer_printf(wctx, "%lld\n", value);
 }
 
 static const Writer default_writer = {
@@ -1143,11 +1171,11 @@  static void compact_print_section_header(WriterContext *wctx)
         if (parent_section && compact->has_nested_elems[wctx->level-1] &&
             (section->flags & SECTION_FLAG_IS_ARRAY)) {
             compact->terminate_line[wctx->level-1] = 0;
-            printf("\n");
+            writer_w8(wctx, '\n');
         }
         if (compact->print_section &&
             !(section->flags & (SECTION_FLAG_IS_WRAPPER|SECTION_FLAG_IS_ARRAY)))
-            printf("%s%c", section->name, compact->item_sep);
+            writer_printf(wctx, "%s%c", section->name, compact->item_sep);
     }
 }
 
@@ -1158,7 +1186,7 @@  static void compact_print_section_footer(WriterContext *wctx)
     if (!compact->nested_section[wctx->level] &&
         compact->terminate_line[wctx->level] &&
         !(wctx->section[wctx->level]->flags & (SECTION_FLAG_IS_WRAPPER|SECTION_FLAG_IS_ARRAY)))
-        printf("\n");
+        writer_w8(wctx, '\n');
 }
 
 static void compact_print_str(WriterContext *wctx, const char *key, const char *value)
@@ -1166,11 +1194,11 @@  static void compact_print_str(WriterContext *wctx, const char *key, const char *
     CompactContext *compact = wctx->priv;
     AVBPrint buf;
 
-    if (wctx->nb_item[wctx->level]) printf("%c", compact->item_sep);
+    if (wctx->nb_item[wctx->level]) writer_w8(wctx, compact->item_sep);
     if (!compact->nokey)
-        printf("%s%s=", wctx->section_pbuf[wctx->level].str, key);
+        writer_printf(wctx, "%s%s=", wctx->section_pbuf[wctx->level].str, key);
     av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
-    printf("%s", compact->escape_str(&buf, value, compact->item_sep, wctx));
+    writer_put_str(wctx, compact->escape_str(&buf, value, compact->item_sep, wctx));
     av_bprint_finalize(&buf, NULL);
 }
 
@@ -1178,10 +1206,10 @@  static void compact_print_int(WriterContext *wctx, const char *key, long long in
 {
     CompactContext *compact = wctx->priv;
 
-    if (wctx->nb_item[wctx->level]) printf("%c", compact->item_sep);
+    if (wctx->nb_item[wctx->level]) writer_w8(wctx, compact->item_sep);
     if (!compact->nokey)
-        printf("%s%s=", wctx->section_pbuf[wctx->level].str, key);
-    printf("%lld", value);
+        writer_printf(wctx, "%s%s=", wctx->section_pbuf[wctx->level].str, key);
+    writer_printf(wctx, "%lld", value);
 }
 
 static const Writer compact_writer = {
@@ -1324,7 +1352,7 @@  static void flat_print_section_header(WriterContext *wctx)
 
 static void flat_print_int(WriterContext *wctx, const char *key, long long int value)
 {
-    printf("%s%s=%lld\n", wctx->section_pbuf[wctx->level].str, key, value);
+    writer_printf(wctx, "%s%s=%lld\n", wctx->section_pbuf[wctx->level].str, key, value);
 }
 
 static void flat_print_str(WriterContext *wctx, const char *key, const char *value)
@@ -1332,11 +1360,11 @@  static void flat_print_str(WriterContext *wctx, const char *key, const char *val
     FlatContext *flat = wctx->priv;
     AVBPrint buf;
 
-    printf("%s", wctx->section_pbuf[wctx->level].str);
+    writer_put_str(wctx, wctx->section_pbuf[wctx->level].str);
     av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
-    printf("%s=", flat_escape_key_str(&buf, key, flat->sep));
+    writer_printf(wctx, "%s=", flat_escape_key_str(&buf, key, flat->sep));
     av_bprint_clear(&buf);
-    printf("\"%s\"\n", flat_escape_value_str(&buf, value));
+    writer_printf(wctx, "\"%s\"\n", flat_escape_value_str(&buf, value));
     av_bprint_finalize(&buf, NULL);
 }
 
@@ -1406,12 +1434,12 @@  static void ini_print_section_header(WriterContext *wctx)
 
     av_bprint_clear(buf);
     if (!parent_section) {
-        printf("# ffprobe output\n\n");
+        writer_put_str(wctx, "# ffprobe output\n\n");
         return;
     }
 
     if (wctx->nb_item[wctx->level-1])
-        printf("\n");
+        writer_w8(wctx, '\n');
 
     av_bprintf(buf, "%s", wctx->section_pbuf[wctx->level-1].str);
     if (ini->hierarchical ||
@@ -1426,7 +1454,7 @@  static void ini_print_section_header(WriterContext *wctx)
     }
 
     if (!(section->flags & (SECTION_FLAG_IS_ARRAY|SECTION_FLAG_IS_WRAPPER)))
-        printf("[%s]\n", buf->str);
+        writer_printf(wctx, "[%s]\n", buf->str);
 }
 
 static void ini_print_str(WriterContext *wctx, const char *key, const char *value)
@@ -1434,15 +1462,15 @@  static void ini_print_str(WriterContext *wctx, const char *key, const char *valu
     AVBPrint buf;
 
     av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
-    printf("%s=", ini_escape_str(&buf, key));
+    writer_printf(wctx, "%s=", ini_escape_str(&buf, key));
     av_bprint_clear(&buf);
-    printf("%s\n", ini_escape_str(&buf, value));
+    writer_printf(wctx, "%s\n", ini_escape_str(&buf, value));
     av_bprint_finalize(&buf, NULL);
 }
 
 static void ini_print_int(WriterContext *wctx, const char *key, long long int value)
 {
-    printf("%s=%lld\n", key, value);
+    writer_printf(wctx, "%s=%lld\n", key, value);
 }
 
 static const Writer ini_writer = {
@@ -1505,7 +1533,7 @@  static const char *json_escape_str(AVBPrint *dst, const char *src, void *log_ctx
     return dst->str;
 }
 
-#define JSON_INDENT() printf("%*c", json->indent_level * 4, ' ')
+#define JSON_INDENT() writer_printf(wctx, "%*c", json->indent_level * 4, ' ')
 
 static void json_print_section_header(WriterContext *wctx)
 {
@@ -1516,10 +1544,10 @@  static void json_print_section_header(WriterContext *wctx)
         wctx->section[wctx->level-1] : NULL;
 
     if (wctx->level && wctx->nb_item[wctx->level-1])
-        printf(",\n");
+        writer_put_str(wctx, ",\n");
 
     if (section->flags & SECTION_FLAG_IS_WRAPPER) {
-        printf("{\n");
+        writer_put_str(wctx, "{\n");
         json->indent_level++;
     } else {
         av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
@@ -1528,17 +1556,17 @@  static void json_print_section_header(WriterContext *wctx)
 
         json->indent_level++;
         if (section->flags & SECTION_FLAG_IS_ARRAY) {
-            printf("\"%s\": [\n", buf.str);
+            writer_printf(wctx, "\"%s\": [\n", buf.str);
         } else if (parent_section && !(parent_section->flags & SECTION_FLAG_IS_ARRAY)) {
-            printf("\"%s\": {%s", buf.str, json->item_start_end);
+            writer_printf(wctx, "\"%s\": {%s", buf.str, json->item_start_end);
         } else {
-            printf("{%s", json->item_start_end);
+            writer_printf(wctx, "{%s", json->item_start_end);
 
             /* this is required so the parser can distinguish between packets and frames */
             if (parent_section && parent_section->id == SECTION_ID_PACKETS_AND_FRAMES) {
                 if (!json->compact)
                     JSON_INDENT();
-                printf("\"type\": \"%s\"", section->name);
+                writer_printf(wctx, "\"type\": \"%s\"", section->name);
             }
         }
         av_bprint_finalize(&buf, NULL);
@@ -1552,18 +1580,18 @@  static void json_print_section_footer(WriterContext *wctx)
 
     if (wctx->level == 0) {
         json->indent_level--;
-        printf("\n}\n");
+        writer_put_str(wctx, "\n}\n");
     } else if (section->flags & SECTION_FLAG_IS_ARRAY) {
-        printf("\n");
+        writer_w8(wctx, '\n');
         json->indent_level--;
         JSON_INDENT();
-        printf("]");
+        writer_w8(wctx, ']');
     } else {
-        printf("%s", json->item_start_end);
+        writer_put_str(wctx, json->item_start_end);
         json->indent_level--;
         if (!json->compact)
             JSON_INDENT();
-        printf("}");
+        writer_w8(wctx, '}');
     }
 }
 
@@ -1573,9 +1601,9 @@  static inline void json_print_item_str(WriterContext *wctx,
     AVBPrint buf;
 
     av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
-    printf("\"%s\":", json_escape_str(&buf, key,   wctx));
+    writer_printf(wctx, "\"%s\":", json_escape_str(&buf, key,   wctx));
     av_bprint_clear(&buf);
-    printf(" \"%s\"", json_escape_str(&buf, value, wctx));
+    writer_printf(wctx, " \"%s\"", json_escape_str(&buf, value, wctx));
     av_bprint_finalize(&buf, NULL);
 }
 
@@ -1586,7 +1614,7 @@  static void json_print_str(WriterContext *wctx, const char *key, const char *val
         wctx->section[wctx->level-1] : NULL;
 
     if (wctx->nb_item[wctx->level] || (parent_section && parent_section->id == SECTION_ID_PACKETS_AND_FRAMES))
-        printf("%s", json->item_sep);
+        writer_put_str(wctx, json->item_sep);
     if (!json->compact)
         JSON_INDENT();
     json_print_item_str(wctx, key, value);
@@ -1600,12 +1628,12 @@  static void json_print_int(WriterContext *wctx, const char *key, long long int v
     AVBPrint buf;
 
     if (wctx->nb_item[wctx->level] || (parent_section && parent_section->id == SECTION_ID_PACKETS_AND_FRAMES))
-        printf("%s", json->item_sep);
+        writer_put_str(wctx, json->item_sep);
     if (!json->compact)
         JSON_INDENT();
 
     av_bprint_init(&buf, 1, AV_BPRINT_SIZE_UNLIMITED);
-    printf("\"%s\": %lld", json_escape_str(&buf, key, wctx), value);
+    writer_printf(wctx, "\"%s\": %lld", json_escape_str(&buf, key, wctx), value);
     av_bprint_finalize(&buf, NULL);
 }
 
@@ -1672,7 +1700,7 @@  static av_cold int xml_init(WriterContext *wctx)
     return 0;
 }
 
-#define XML_INDENT() printf("%*c", xml->indent_level * 4, ' ')
+#define XML_INDENT() writer_printf(wctx, "%*c", xml->indent_level * 4, ' ')
 
 static void xml_print_section_header(WriterContext *wctx)
 {
@@ -1686,8 +1714,8 @@  static void xml_print_section_header(WriterContext *wctx)
             "xmlns:ffprobe='http://www.ffmpeg.org/schema/ffprobe' "
             "xsi:schemaLocation='http://www.ffmpeg.org/schema/ffprobe ffprobe.xsd'";
 
-        printf("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
-        printf("<%sffprobe%s>\n",
+        writer_put_str(wctx, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+        writer_printf(wctx, "<%sffprobe%s>\n",
                xml->fully_qualified ? "ffprobe:" : "",
                xml->fully_qualified ? qual : "");
         return;
@@ -1695,20 +1723,20 @@  static void xml_print_section_header(WriterContext *wctx)
 
     if (xml->within_tag) {
         xml->within_tag = 0;
-        printf(">\n");
+        writer_put_str(wctx, ">\n");
     }
     if (section->flags & SECTION_FLAG_HAS_VARIABLE_FIELDS) {
         xml->indent_level++;
     } else {
         if (parent_section && (parent_section->flags & SECTION_FLAG_IS_WRAPPER) &&
             wctx->level && wctx->nb_item[wctx->level-1])
-            printf("\n");
+            writer_w8(wctx, '\n');
         xml->indent_level++;
 
         if (section->flags & SECTION_FLAG_IS_ARRAY) {
-            XML_INDENT(); printf("<%s>\n", section->name);
+            XML_INDENT(); writer_printf(wctx, "<%s>\n", section->name);
         } else {
-            XML_INDENT(); printf("<%s ", section->name);
+            XML_INDENT(); writer_printf(wctx, "<%s ", section->name);
             xml->within_tag = 1;
         }
     }
@@ -1720,15 +1748,15 @@  static void xml_print_section_footer(WriterContext *wctx)
     const struct section *section = wctx->section[wctx->level];
 
     if (wctx->level == 0) {
-        printf("</%sffprobe>\n", xml->fully_qualified ? "ffprobe:" : "");
+        writer_printf(wctx, "</%sffprobe>\n", xml->fully_qualified ? "ffprobe:" : "");
     } else if (xml->within_tag) {
         xml->within_tag = 0;
-        printf("/>\n");
+        writer_put_str(wctx, "/>\n");
         xml->indent_level--;
     } else if (section->flags & SECTION_FLAG_HAS_VARIABLE_FIELDS) {
         xml->indent_level--;
     } else {
-        XML_INDENT(); printf("</%s>\n", section->name);
+        XML_INDENT(); writer_printf(wctx, "</%s>\n", section->name);
         xml->indent_level--;
     }
 }
@@ -1745,20 +1773,20 @@  static void xml_print_str(WriterContext *wctx, const char *key, const char *valu
         XML_INDENT();
         av_bprint_escape(&buf, key, NULL,
                          AV_ESCAPE_MODE_XML, AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
-        printf("<%s key=\"%s\"",
-               section->element_name, buf.str);
+        writer_printf(wctx, "<%s key=\"%s\"",
+                      section->element_name, buf.str);
         av_bprint_clear(&buf);
 
         av_bprint_escape(&buf, value, NULL,
                          AV_ESCAPE_MODE_XML, AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
-        printf(" value=\"%s\"/>\n", buf.str);
+        writer_printf(wctx, " value=\"%s\"/>\n", buf.str);
     } else {
         if (wctx->nb_item[wctx->level])
-            printf(" ");
+            writer_w8(wctx, ' ');
 
         av_bprint_escape(&buf, value, NULL,
                          AV_ESCAPE_MODE_XML, AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
-        printf("%s=\"%s\"", key, buf.str);
+        writer_printf(wctx, "%s=\"%s\"", key, buf.str);
     }
 
     av_bprint_finalize(&buf, NULL);
@@ -1767,8 +1795,8 @@  static void xml_print_str(WriterContext *wctx, const char *key, const char *valu
 static void xml_print_int(WriterContext *wctx, const char *key, long long int value)
 {
     if (wctx->nb_item[wctx->level])
-        printf(" ");
-    printf("%s=\"%lld\"", key, value);
+        writer_w8(wctx, ' ');
+    writer_printf(wctx, "%s=\"%lld\"", key, value);
 }
 
 static Writer xml_writer = {
@@ -3380,6 +3408,25 @@  static int opt_input_file_i(void *optctx, const char *opt, const char *arg)
     return 0;
 }
 
+static void opt_output_file(void *optctx, const char *arg)
+{
+    if (output_filename) {
+        av_log(NULL, AV_LOG_ERROR,
+                "Argument '%s' provided as output filename, but '%s' was already specified.\n",
+                arg, output_filename);
+        exit_program(1);
+    }
+    if (!strcmp(arg, "-"))
+        arg = "pipe:";
+    output_filename = arg;
+}
+
+static int opt_output_file_o(void *optctx, const char *opt, const char *arg)
+{
+    opt_output_file(optctx, arg);
+    return 0;
+}
+
 static int opt_print_filename(void *optctx, const char *opt, const char *arg)
 {
     print_input_filename = arg;
@@ -3644,6 +3691,7 @@  static const OptionDef real_options[] = {
     { "read_intervals", HAS_ARG, {.func_arg = opt_read_intervals}, "set read intervals", "read_intervals" },
     { "default", HAS_ARG | OPT_AUDIO | OPT_VIDEO | OPT_EXPERT, {.func_arg = opt_default}, "generic catch all option", "" },
     { "i", HAS_ARG, {.func_arg = opt_input_file_i}, "read specified file", "input_file"},
+    { "o", HAS_ARG, {.func_arg = opt_output_file_o}, "write to specified output", "output_file"},
     { "print_filename", HAS_ARG, {.func_arg = opt_print_filename}, "override the printed input filename", "print_file"},
     { "find_stream_info", OPT_BOOL | OPT_INPUT | OPT_EXPERT, { &find_stream_info },
         "read and decode the streams to fill missing information with heuristics" },
@@ -3771,8 +3819,12 @@  int main(int argc, char **argv)
         goto end;
     }
 
+    if (!output_filename) {
+        output_filename = "pipe:";
+    }
+
     if ((ret = writer_open(&wctx, w, w_args,
-                           sections, FF_ARRAY_ELEMS(sections))) >= 0) {
+                           sections, FF_ARRAY_ELEMS(sections), output_filename)) >= 0) {
         if (w == &xml_writer)
             wctx->string_validation_utf8_flags |= AV_UTF8_FLAG_EXCLUDE_XML_INVALID_CONTROL_CODES;