tests: image: fix string representation for GstVideoFormat.
[platform/upstream/gstreamer-vaapi.git] / tests / simple-decoder.c
1 /*
2  *  simple-decoder.c - Simple Decoder Application
3  *
4  *  Copyright (C) 2013 Intel Corporation
5  *
6  *  This library is free software; you can redistribute it and/or
7  *  modify it under the terms of the GNU Lesser General Public License
8  *  as published by the Free Software Foundation; either version 2.1
9  *  of the License, or (at your option) any later version.
10  *
11  *  This library is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  *  Lesser General Public License for more details.
15  *
16  *  You should have received a copy of the GNU Lesser General Public
17  *  License along with this library; if not, write to the Free
18  *  Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19  *  Boston, MA 02110-1301 USA
20  */
21
22 /*
23  * This is a really simple decoder application that only accepts raw
24  * bitstreams. So, it may be needed to suggest what codec to use to
25  * the application.
26  */
27
28 #include "gst/vaapi/sysdeps.h"
29 #include <stdarg.h>
30 #include <gst/vaapi/gstvaapidecoder.h>
31 #include <gst/vaapi/gstvaapidecoder_h264.h>
32 #include <gst/vaapi/gstvaapidecoder_jpeg.h>
33 #include <gst/vaapi/gstvaapidecoder_mpeg2.h>
34 #include <gst/vaapi/gstvaapidecoder_mpeg4.h>
35 #include <gst/vaapi/gstvaapidecoder_vc1.h>
36 #include <gst/vaapi/gstvaapiwindow.h>
37 #include <gst/vaapi/gstvaapipixmap.h>
38 #include "codec.h"
39 #include "output.h"
40
41 static gchar *g_codec_str;
42 static gboolean g_use_pixmap;
43 static gboolean g_benchmark;
44
45 static GOptionEntry g_options[] = {
46     { "codec", 'c',
47       0,
48       G_OPTION_ARG_STRING, &g_codec_str,
49       "suggested codec", NULL },
50     { "pixmap", 0,
51       0,
52       G_OPTION_ARG_NONE, &g_use_pixmap,
53       "use render-to-pixmap", NULL },
54     { "benchmark", 0,
55       0,
56       G_OPTION_ARG_NONE, &g_benchmark,
57       "benchmark mode", NULL },
58     { NULL, }
59 };
60
61 typedef enum {
62     APP_RUNNING,
63     APP_GOT_EOS,
64     APP_GOT_ERROR,
65 } AppEvent;
66
67 typedef enum {
68     APP_ERROR_NONE,
69     APP_ERROR_DECODER,
70     APP_ERROR_RENDERER,
71 } AppError;
72
73 typedef struct {
74     GstVaapiSurfaceProxy *proxy;
75     GstClockTime        pts;
76     GstClockTime        duration;
77 } RenderFrame;
78
79 typedef struct {
80     GMutex              mutex;
81     GMappedFile        *file;
82     gchar              *file_name;
83     guint               file_offset;
84     guint               file_size;
85     guchar             *file_data;
86     GstVaapiDisplay    *display;
87     GstVaapiDecoder    *decoder;
88     GThread            *decoder_thread;
89     volatile gboolean   decoder_thread_cancel;
90     GCond               decoder_ready;
91     GAsyncQueue        *decoder_queue;
92     GstVaapiCodec       codec;
93     guint               fps_n;
94     guint               fps_d;
95     guint32             frame_duration;
96     guint               surface_width;
97     guint               surface_height;
98     GstVaapiPixmap     *pixmaps[2];
99     guint               pixmap_id;
100     guint               pixmap_width;
101     guint               pixmap_height;
102     GstVaapiWindow     *window;
103     guint               window_width;
104     guint               window_height;
105     GThread            *render_thread;
106     volatile gboolean   render_thread_cancel;
107     GCond               render_ready;
108     RenderFrame        *last_frame;
109     GError             *error;
110     AppEvent            event;
111     GCond               event_cond;
112     GTimer             *timer;
113     guint32             num_frames;
114 } App;
115
116 static inline RenderFrame *
117 render_frame_new(void)
118 {
119     return g_slice_new(RenderFrame);
120 }
121
122 static void
123 render_frame_free(RenderFrame *rfp)
124 {
125     if (G_UNLIKELY(!rfp))
126         return;
127     gst_vaapi_surface_proxy_replace(&rfp->proxy, NULL);
128     g_slice_free(RenderFrame, rfp);
129 }
130
131 static inline void
132 render_frame_replace(RenderFrame **rfp_ptr, RenderFrame *new_rfp)
133 {
134     if (*rfp_ptr)
135         render_frame_free(*rfp_ptr);
136     *rfp_ptr = new_rfp;
137 }
138
139 #define APP_ERROR app_error_quark()
140 static GQuark
141 app_error_quark(void)
142 {
143     static gsize g_quark;
144
145     if (g_once_init_enter(&g_quark)) {
146         gsize quark = (gsize)g_quark_from_static_string("AppError");
147         g_once_init_leave(&g_quark, quark);
148     }
149     return g_quark;
150 }
151
152 static void
153 app_send_error(App *app, GError *error)
154 {
155     g_mutex_lock(&app->mutex);
156     app->error = error;
157     app->event = APP_GOT_ERROR;
158     g_cond_signal(&app->event_cond);
159     g_mutex_unlock(&app->mutex);
160 }
161
162 static void
163 app_send_eos(App *app)
164 {
165     g_mutex_lock(&app->mutex);
166     app->event = APP_GOT_EOS;
167     g_cond_signal(&app->event_cond);
168     g_mutex_unlock(&app->mutex);
169 }
170
171 static const gchar *
172 get_decoder_status_string(GstVaapiDecoderStatus status)
173 {
174     const gchar *str;
175
176 #define DEFINE_STATUS(status, status_string) \
177     case GST_VAAPI_DECODER_STATUS_##status:  \
178         str = status_string;                 \
179         break
180
181     switch (status) {
182         DEFINE_STATUS(SUCCESS,                  "<success>");
183         DEFINE_STATUS(END_OF_STREAM,            "<EOS>");
184         DEFINE_STATUS(ERROR_ALLOCATION_FAILED,  "allocation failed");
185         DEFINE_STATUS(ERROR_INIT_FAILED,        "initialization failed");
186         DEFINE_STATUS(ERROR_UNSUPPORTED_CODEC,  "unsupported codec");
187         DEFINE_STATUS(ERROR_NO_DATA,            "not enough data");
188         DEFINE_STATUS(ERROR_NO_SURFACE,         "no surface vailable");
189         DEFINE_STATUS(ERROR_INVALID_SURFACE,    "invalid surface");
190         DEFINE_STATUS(ERROR_BITSTREAM_PARSER,   "bitstream parser error");
191         DEFINE_STATUS(ERROR_UNSUPPORTED_PROFILE,
192                       "unsupported profile");
193         DEFINE_STATUS(ERROR_UNSUPPORTED_CHROMA_FORMAT,
194                       "unsupported chroma-format");
195         DEFINE_STATUS(ERROR_INVALID_PARAMETER,  "invalid parameter");
196     default:
197         str = "<unknown>";
198         break;
199     }
200 #undef DEFINE_STATUS
201
202     return str;
203 }
204
205 static const gchar *
206 get_error_string(AppError error)
207 {
208     const gchar *str;
209
210 #define DEFINE_ERROR(error, error_string)       \
211     case APP_ERROR_##error:                     \
212         str = error_string;                     \
213         break
214
215     switch (error) {
216         DEFINE_ERROR(NONE,      "<none>");
217         DEFINE_ERROR(DECODER,   "decoder");
218         DEFINE_ERROR(RENDERER,  "renderer");
219     default:
220         str = "unknown";
221         break;
222     }
223 #undef DEFINE_ERROR
224
225     return str;
226 }
227
228 static void
229 decoder_release(App *app)
230 {
231     g_mutex_lock(&app->mutex);
232     g_cond_signal(&app->decoder_ready);
233     g_mutex_unlock(&app->mutex);
234 }
235
236 static gpointer
237 decoder_thread(gpointer data)
238 {
239     App * const app = data;
240     GError *error = NULL;
241     GstVaapiDecoderStatus status;
242     GstVaapiSurfaceProxy *proxy;
243     RenderFrame *rfp;
244     GstBuffer *buffer;
245     GstClockTime pts;
246     gboolean got_surface, got_eos = FALSE;
247     gint64 end_time;
248     guint ofs;
249
250     g_print("Decoder thread started\n");
251
252 #define SEND_ERROR(...)                                                 \
253     do {                                                                \
254         error = g_error_new(APP_ERROR, APP_ERROR_DECODER, __VA_ARGS__); \
255         goto send_error;                                                \
256     } while (0)
257
258     pts = g_get_monotonic_time();
259     ofs = 0;
260     while (!app->decoder_thread_cancel) {
261         if (G_UNLIKELY(ofs == app->file_size))
262             buffer = NULL;
263         else {
264             const gsize size = MIN(4096, app->file_size - ofs);
265             buffer = gst_buffer_new_wrapped_full(GST_MEMORY_FLAG_READONLY,
266                 app->file_data, app->file_size, ofs, size, NULL, NULL);
267             if (!buffer)
268                 SEND_ERROR("failed to allocate new buffer");
269             ofs += size;
270         }
271         if (!gst_vaapi_decoder_put_buffer(app->decoder, buffer))
272             SEND_ERROR("failed to push buffer to decoder");
273         gst_buffer_replace(&buffer, NULL);
274
275     get_surface:
276         status = gst_vaapi_decoder_get_surface(app->decoder, &proxy);
277         switch (status) {
278         case GST_VAAPI_DECODER_STATUS_SUCCESS:
279             gst_vaapi_surface_proxy_set_destroy_notify(proxy,
280                 (GDestroyNotify)decoder_release, app);
281             rfp = render_frame_new();
282             if (!rfp)
283                 SEND_ERROR("failed to allocate render frame");
284             rfp->proxy = proxy;
285             rfp->pts = pts;
286             rfp->duration = app->frame_duration;
287             pts += app->frame_duration;
288             g_async_queue_push(app->decoder_queue, rfp);
289             break;
290         case GST_VAAPI_DECODER_STATUS_ERROR_NO_DATA:
291             /* nothing to do, just continue to the next iteration */
292             break;
293         case GST_VAAPI_DECODER_STATUS_END_OF_STREAM:
294             gst_vaapi_decoder_flush(app->decoder);
295             if (got_eos)
296                 goto send_eos;
297             got_eos = TRUE;
298             break;
299         case GST_VAAPI_DECODER_STATUS_ERROR_NO_SURFACE:
300             end_time = g_get_monotonic_time() + G_TIME_SPAN_SECOND;
301             g_mutex_lock(&app->mutex);
302             got_surface = g_cond_wait_until(&app->decoder_ready, &app->mutex,
303                 end_time);
304             g_mutex_unlock(&app->mutex);
305             if (got_surface)
306                 goto get_surface;
307             SEND_ERROR("failed to acquire a surface within one second");
308             break;
309         default:
310             SEND_ERROR("%s", get_decoder_status_string(status));
311             break;
312         }
313     }
314     return NULL;
315
316 #undef SEND_ERROR
317
318 send_eos:
319     app_send_eos(app);
320     return NULL;
321
322 send_error:
323     app_send_error(app, error);
324     return NULL;
325 }
326
327 static void
328 app_set_framerate(App *app, guint fps_n, guint fps_d)
329 {
330     if (!fps_n || !fps_d)
331         return;
332
333     g_mutex_lock(&app->mutex);
334     if (fps_n != app->fps_n || fps_d != app->fps_d) {
335         app->fps_n = fps_n;
336         app->fps_d = fps_d;
337         app->frame_duration = gst_util_uint64_scale(
338             GST_TIME_AS_USECONDS(GST_SECOND), fps_d, fps_n);
339     }
340     g_mutex_unlock(&app->mutex);
341 }
342
343 static void
344 handle_decoder_state_changes(GstVaapiDecoder *decoder,
345     const GstVideoCodecState *codec_state, gpointer user_data)
346 {
347     App * const app = user_data;
348
349     g_assert(app->decoder == decoder);
350     app_set_framerate(app, codec_state->info.fps_n, codec_state->info.fps_d);
351 }
352
353 static gboolean
354 start_decoder(App *app)
355 {
356     GstCaps *caps;
357
358     app->file = g_mapped_file_new(app->file_name, FALSE, NULL);
359     if (!app->file)
360         return FALSE;
361
362     app->file_size = g_mapped_file_get_length(app->file);
363     app->file_data = (guint8 *)g_mapped_file_get_contents(app->file);
364     if (!app->file_data)
365         return FALSE;
366
367     caps = caps_from_codec(app->codec);
368     switch (app->codec) {
369     case GST_VAAPI_CODEC_H264:
370         app->decoder = gst_vaapi_decoder_h264_new(app->display, caps);
371         break;
372 #if USE_JPEG_DECODER
373     case GST_VAAPI_CODEC_JPEG:
374         app->decoder = gst_vaapi_decoder_jpeg_new(app->display, caps);
375         break;
376 #endif
377     case GST_VAAPI_CODEC_MPEG2:
378         app->decoder = gst_vaapi_decoder_mpeg2_new(app->display, caps);
379         break;
380     case GST_VAAPI_CODEC_MPEG4:
381         app->decoder = gst_vaapi_decoder_mpeg4_new(app->display, caps);
382         break;
383     case GST_VAAPI_CODEC_VC1:
384         app->decoder = gst_vaapi_decoder_vc1_new(app->display, caps);
385         break;
386     default:
387         app->decoder = NULL;
388         break;
389     }
390     if (!app->decoder)
391         return FALSE;
392
393     gst_vaapi_decoder_set_codec_state_changed_func(app->decoder,
394         handle_decoder_state_changes, app);
395
396     g_timer_start(app->timer);
397
398     app->decoder_thread = g_thread_create(decoder_thread, app, TRUE, NULL);
399     if (!app->decoder_thread)
400         return FALSE;
401     return TRUE;
402 }
403
404 static gboolean
405 stop_decoder(App *app)
406 {
407     g_timer_stop(app->timer);
408
409     app->decoder_thread_cancel = TRUE;
410     g_thread_join(app->decoder_thread);
411     g_print("Decoder thread stopped\n");
412     return TRUE;
413 }
414
415 static void
416 ensure_window_size(App *app, GstVaapiSurface *surface)
417 {
418     guint width, height;
419
420     if (gst_vaapi_window_get_fullscreen(app->window))
421         return;
422
423     gst_vaapi_surface_get_size(surface, &width, &height);
424     if (app->surface_width == width && app->surface_height == height)
425         return;
426     app->surface_width = width;
427     app->surface_height = height;
428
429     gst_vaapi_window_set_size(app->window, width, height);
430     gst_vaapi_window_get_size(app->window,
431         &app->window_width, &app->window_height);
432 }
433
434 static gboolean
435 ensure_pixmaps(App *app, GstVaapiSurface *surface,
436     const GstVaapiRectangle *crop_rect)
437 {
438     GstVaapiPixmap *pixmaps[G_N_ELEMENTS(app->pixmaps)];
439     guint num_pixmaps, i, width, height;
440     gboolean success = FALSE;
441
442     if (crop_rect) {
443         width  = crop_rect->width;
444         height = crop_rect->height;
445     }
446     else
447         gst_vaapi_surface_get_size(surface, &width, &height);
448     if (app->pixmap_width == width && app->pixmap_height == height)
449         return TRUE;
450
451     for (i = 0, num_pixmaps = 0; i < G_N_ELEMENTS(pixmaps); i++) {
452         GstVaapiPixmap * const pixmap =
453             video_output_create_pixmap(app->display, GST_VIDEO_FORMAT_xRGB,
454                 width, height);
455         if (!pixmap)
456             goto end;
457         pixmaps[num_pixmaps++] = pixmap;
458     }
459
460     for (i = 0; i < num_pixmaps; i++)
461         gst_vaapi_pixmap_replace(&app->pixmaps[i], pixmaps[i]);
462     app->pixmap_width = width;
463     app->pixmap_height = height;
464     success = TRUE;
465
466 end:
467     for (i = 0; i < num_pixmaps; i++)
468         gst_vaapi_pixmap_replace(&pixmaps[i], NULL);
469     return success;
470 }
471
472 static inline void
473 renderer_wait_until(App *app, GstClockTime pts)
474 {
475     g_mutex_lock(&app->mutex);
476     do {
477     } while (g_cond_wait_until(&app->render_ready, &app->mutex, pts));
478     g_mutex_unlock(&app->mutex);
479 }
480
481 static gboolean
482 renderer_process(App *app, RenderFrame *rfp)
483 {
484     GError *error = NULL;
485     GstVaapiSurface *surface;
486     const GstVaapiRectangle *crop_rect;
487
488 #define SEND_ERROR(...)                                                 \
489     do {                                                                \
490         error = g_error_new(APP_ERROR, APP_ERROR_RENDERER, __VA_ARGS__); \
491         goto send_error;                                                \
492     } while (0)
493
494     surface = gst_vaapi_surface_proxy_get_surface(rfp->proxy);
495     if (!surface)
496         SEND_ERROR("failed to get decoded surface from render frame");
497
498     ensure_window_size(app, surface);
499
500     crop_rect = gst_vaapi_surface_proxy_get_crop_rect(rfp->proxy);
501     if (!ensure_pixmaps(app, surface, crop_rect))
502         SEND_ERROR("failed to create intermediate pixmaps");
503
504     if (!gst_vaapi_surface_sync(surface))
505         SEND_ERROR("failed to sync decoded surface");
506
507     if (G_LIKELY(!g_benchmark))
508         renderer_wait_until(app, rfp->pts);
509
510     if (G_UNLIKELY(g_use_pixmap)) {
511         GstVaapiPixmap * const pixmap = app->pixmaps[app->pixmap_id];
512
513         if (!gst_vaapi_pixmap_put_surface(pixmap, surface, crop_rect,
514                 GST_VAAPI_PICTURE_STRUCTURE_FRAME))
515             SEND_ERROR("failed to render to pixmap");
516
517         if (!gst_vaapi_window_put_pixmap(app->window, pixmap, NULL, NULL))
518             SEND_ERROR("failed to render surface %" GST_VAAPI_ID_FORMAT,
519                        GST_VAAPI_ID_ARGS(pixmap));
520
521         app->pixmap_id = (app->pixmap_id + 1) % G_N_ELEMENTS(app->pixmaps);
522     }
523     else if (!gst_vaapi_window_put_surface(app->window, surface,
524                  crop_rect, NULL, GST_VAAPI_PICTURE_STRUCTURE_FRAME))
525         SEND_ERROR("failed to render surface %" GST_VAAPI_ID_FORMAT,
526                    GST_VAAPI_ID_ARGS(gst_vaapi_surface_get_id(surface)));
527
528     app->num_frames++;
529
530     render_frame_replace(&app->last_frame, rfp);
531     return TRUE;
532
533 #undef SEND_ERROR
534
535 send_error:
536     app_send_error(app, error);
537     return FALSE;
538 }
539
540 static gpointer
541 renderer_thread(gpointer data)
542 {
543     App * const app = data;
544     RenderFrame *rfp;
545
546     g_print("Render thread started\n");
547
548     while (!app->render_thread_cancel) {
549         rfp = g_async_queue_timeout_pop(app->decoder_queue, 1000000);
550         if (rfp && !renderer_process(app, rfp))
551             break;
552     }
553     return NULL;
554 }
555
556 static gboolean
557 flush_decoder_queue(App *app)
558 {
559     RenderFrame *rfp;
560
561     /* Flush pending surfaces */
562     do {
563         rfp = g_async_queue_try_pop(app->decoder_queue);
564         if (!rfp)
565             return TRUE;
566     } while (renderer_process(app, rfp));
567     return FALSE;
568 }
569
570 static gboolean
571 start_renderer(App *app)
572 {
573     app->render_thread = g_thread_create(renderer_thread, app, TRUE, NULL);
574     if (!app->render_thread)
575         return FALSE;
576     return TRUE;
577 }
578
579 static gboolean
580 stop_renderer(App *app)
581 {
582     app->render_thread_cancel = TRUE;
583     g_thread_join(app->render_thread);
584
585     g_print("Render thread stopped\n");
586
587     flush_decoder_queue(app);
588     render_frame_replace(&app->last_frame, NULL);
589     return TRUE;
590 }
591
592 static void
593 app_free(App *app)
594 {
595     guint i;
596
597     if (!app)
598         return;
599
600     if (app->file) {
601         g_mapped_file_unref(app->file);
602         app->file = NULL;
603     }
604     g_free(app->file_name);
605
606     for (i = 0; i < G_N_ELEMENTS(app->pixmaps); i++)
607         gst_vaapi_pixmap_replace(&app->pixmaps[i], NULL);
608     gst_vaapi_decoder_replace(&app->decoder, NULL);
609     gst_vaapi_window_replace(&app->window, NULL);
610     gst_vaapi_display_replace(&app->display, NULL);
611
612     if (app->decoder_queue) {
613         g_async_queue_unref(app->decoder_queue);
614         app->decoder_queue = NULL;
615     }
616     g_cond_clear(&app->decoder_ready);
617
618     if (app->timer) {
619         g_timer_destroy(app->timer);
620         app->timer = NULL;
621     }
622
623     g_cond_clear(&app->render_ready);
624     g_cond_clear(&app->event_cond);
625     g_mutex_clear(&app->mutex);
626     g_slice_free(App, app);
627 }
628
629 static App *
630 app_new(void)
631 {
632     App *app;
633
634     app = g_slice_new0(App);
635     if (!app)
636         return NULL;
637
638     g_mutex_init(&app->mutex);
639     g_cond_init(&app->event_cond);
640     g_cond_init(&app->decoder_ready);
641     g_cond_init(&app->render_ready);
642
643     app_set_framerate(app, 60, 1);
644     app->window_width = 640;
645     app->window_height = 480;
646
647     app->decoder_queue = g_async_queue_new_full(
648         (GDestroyNotify)render_frame_free);
649     if (!app->decoder_queue)
650         goto error;
651
652     app->timer = g_timer_new();
653     if (!app->timer)
654         goto error;
655     return app;
656
657 error:
658     app_free(app);
659     return NULL;
660 }
661
662 static gboolean
663 app_check_events(App *app)
664 {
665     GError *error = NULL;
666     gboolean stop = FALSE;
667
668     do {
669         g_mutex_lock(&app->mutex);
670         while (app->event == APP_RUNNING)
671             g_cond_wait(&app->event_cond, &app->mutex);
672
673         switch (app->event) {
674         case APP_GOT_ERROR:
675             error = app->error;
676             app->error = NULL;
677             /* fall-through */
678         case APP_GOT_EOS:
679             stop = TRUE;
680             break;
681         default:
682             break;
683         }
684         g_mutex_unlock(&app->mutex);
685     } while (!stop);
686
687     if (!error)
688         return TRUE;
689
690     g_message("%s error: %s", get_error_string(error->code), error->message);
691     g_error_free(error);
692     return FALSE;
693 }
694
695 static gboolean
696 app_run(App *app, int argc, char *argv[])
697 {
698     if (argc < 2) {
699         g_message("no bitstream file specified");
700         return FALSE;
701     }
702     app->file_name = g_strdup(argv[1]);
703
704     if (!g_file_test(app->file_name, G_FILE_TEST_IS_REGULAR)) {
705         g_message("failed to find file '%s'", app->file_name);
706         return FALSE;
707     }
708
709     app->codec = identify_codec(app->file_name);
710     if (!app->codec) {
711         app->codec = identify_codec_from_string(g_codec_str);
712         if (!app->codec) {
713             g_message("failed to identify codec for '%s'", app->file_name);
714             return FALSE;
715         }
716     }
717
718     g_print("Simple decoder (%s bitstream)\n", string_from_codec(app->codec));
719
720     app->display = video_output_create_display(NULL);
721     if (!app->display) {
722         g_message("failed to create VA display");
723         return FALSE;
724     }
725
726     app->window = video_output_create_window(app->display,
727         app->window_width, app->window_height);
728     if (!app->window) {
729         g_message("failed to create window");
730         return FALSE;
731     }
732
733     gst_vaapi_window_show(app->window);
734
735     if (!start_decoder(app)) {
736         g_message("failed to start decoder thread");
737         return FALSE;
738     }
739
740     if (!start_renderer(app)) {
741         g_message("failed to start renderer thread");
742         return FALSE;
743     }
744
745     app_check_events(app);
746
747     stop_renderer(app);
748     stop_decoder(app);
749
750     g_print("Decoded %u frames", app->num_frames);
751     if (g_benchmark) {
752         const gdouble elapsed = g_timer_elapsed(app->timer, NULL);
753         g_print(" in %.2f sec (%.1f fps)\n",
754                 elapsed, (gdouble)app->num_frames / elapsed);
755     }
756     g_print("\n");
757     return TRUE;
758 }
759
760 int
761 main(int argc, char *argv[])
762 {
763     App *app;
764     gint ret;
765
766     if (!video_output_init(&argc, argv, g_options))
767         g_error("failed to initialize video output subsystem");
768
769     app = app_new();
770     if (!app)
771         g_error("failed to create application context");
772
773     ret = !app_run(app, argc, argv);
774
775     app_free(app);
776     g_free(g_codec_str);
777     video_output_exit();
778     return ret;
779 }