mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-23 16:50:47 +00:00
691 lines
16 KiB
C
691 lines
16 KiB
C
/*
|
|
* simple-decoder.c - Simple Decoder Application
|
|
*
|
|
* Copyright (C) 2013-2014 Intel Corporation
|
|
* Author: Gwenole Beauchesne <gwenole.beauchesne@intel.com>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public License
|
|
* as published by the Free Software Foundation; either version 2.1
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free
|
|
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
/*
|
|
* This is a really simple decoder application that only accepts raw
|
|
* bitstreams. So, it may be needed to suggest what codec to use to
|
|
* the application.
|
|
*/
|
|
|
|
#include "gst/vaapi/sysdeps.h"
|
|
#include <stdarg.h>
|
|
#include <gst/vaapi/gstvaapidecoder.h>
|
|
#include <gst/vaapi/gstvaapidecoder_h264.h>
|
|
#include <gst/vaapi/gstvaapidecoder_jpeg.h>
|
|
#include <gst/vaapi/gstvaapidecoder_mpeg2.h>
|
|
#include <gst/vaapi/gstvaapidecoder_mpeg4.h>
|
|
#include <gst/vaapi/gstvaapidecoder_vc1.h>
|
|
#include <gst/vaapi/gstvaapiwindow.h>
|
|
#include "codec.h"
|
|
#include "output.h"
|
|
|
|
static gchar *g_codec_str;
|
|
static gboolean g_benchmark;
|
|
|
|
static GOptionEntry g_options[] = {
|
|
{"codec", 'c',
|
|
0,
|
|
G_OPTION_ARG_STRING, &g_codec_str,
|
|
"suggested codec", NULL},
|
|
{"benchmark", 0,
|
|
0,
|
|
G_OPTION_ARG_NONE, &g_benchmark,
|
|
"benchmark mode", NULL},
|
|
{NULL,}
|
|
};
|
|
|
|
typedef enum
|
|
{
|
|
APP_RUNNING,
|
|
APP_GOT_EOS,
|
|
APP_GOT_ERROR,
|
|
} AppEvent;
|
|
|
|
typedef enum
|
|
{
|
|
APP_ERROR_NONE,
|
|
APP_ERROR_DECODER,
|
|
APP_ERROR_RENDERER,
|
|
} AppError;
|
|
|
|
typedef struct
|
|
{
|
|
GstVaapiSurfaceProxy *proxy;
|
|
GstClockTime pts;
|
|
GstClockTime duration;
|
|
} RenderFrame;
|
|
|
|
typedef struct
|
|
{
|
|
GMutex mutex;
|
|
GMappedFile *file;
|
|
gchar *file_name;
|
|
guint file_offset;
|
|
guint file_size;
|
|
guchar *file_data;
|
|
GstVaapiDisplay *display;
|
|
GstVaapiDecoder *decoder;
|
|
GThread *decoder_thread;
|
|
gboolean decoder_thread_cancel;
|
|
GAsyncQueue *decoder_queue;
|
|
GstVaapiCodec codec;
|
|
guint fps_n;
|
|
guint fps_d;
|
|
guint32 frame_duration;
|
|
guint surface_width;
|
|
guint surface_height;
|
|
GstVaapiWindow *window;
|
|
guint window_width;
|
|
guint window_height;
|
|
GThread *render_thread;
|
|
gboolean render_thread_cancel;
|
|
GCond render_ready;
|
|
RenderFrame *last_frame;
|
|
GError *error;
|
|
AppEvent event;
|
|
GCond event_cond;
|
|
GTimer *timer;
|
|
guint32 num_frames;
|
|
} App;
|
|
|
|
static inline RenderFrame *
|
|
render_frame_new (void)
|
|
{
|
|
return g_slice_new (RenderFrame);
|
|
}
|
|
|
|
static void
|
|
render_frame_free (RenderFrame * rfp)
|
|
{
|
|
if (G_UNLIKELY (!rfp))
|
|
return;
|
|
gst_vaapi_surface_proxy_replace (&rfp->proxy, NULL);
|
|
g_slice_free (RenderFrame, rfp);
|
|
}
|
|
|
|
static inline void
|
|
render_frame_replace (RenderFrame ** rfp_ptr, RenderFrame * new_rfp)
|
|
{
|
|
if (*rfp_ptr)
|
|
render_frame_free (*rfp_ptr);
|
|
*rfp_ptr = new_rfp;
|
|
}
|
|
|
|
#define APP_ERROR app_error_quark()
|
|
static GQuark
|
|
app_error_quark (void)
|
|
{
|
|
static gsize g_quark;
|
|
|
|
if (g_once_init_enter (&g_quark)) {
|
|
gsize quark = (gsize) g_quark_from_static_string ("AppError");
|
|
g_once_init_leave (&g_quark, quark);
|
|
}
|
|
return g_quark;
|
|
}
|
|
|
|
static void
|
|
app_send_error (App * app, GError * error)
|
|
{
|
|
g_mutex_lock (&app->mutex);
|
|
app->error = error;
|
|
app->event = APP_GOT_ERROR;
|
|
g_cond_signal (&app->event_cond);
|
|
g_mutex_unlock (&app->mutex);
|
|
}
|
|
|
|
static void
|
|
app_send_eos (App * app)
|
|
{
|
|
g_mutex_lock (&app->mutex);
|
|
app->event = APP_GOT_EOS;
|
|
g_cond_signal (&app->event_cond);
|
|
g_mutex_unlock (&app->mutex);
|
|
}
|
|
|
|
static const gchar *
|
|
get_decoder_status_string (GstVaapiDecoderStatus status)
|
|
{
|
|
const gchar *str;
|
|
|
|
#define DEFINE_STATUS(status, status_string) \
|
|
case GST_VAAPI_DECODER_STATUS_##status: \
|
|
str = status_string; \
|
|
break
|
|
|
|
switch (status) {
|
|
DEFINE_STATUS (SUCCESS, "<success>");
|
|
DEFINE_STATUS (END_OF_STREAM, "<EOS>");
|
|
DEFINE_STATUS (ERROR_ALLOCATION_FAILED, "allocation failed");
|
|
DEFINE_STATUS (ERROR_INIT_FAILED, "initialization failed");
|
|
DEFINE_STATUS (ERROR_UNSUPPORTED_CODEC, "unsupported codec");
|
|
DEFINE_STATUS (ERROR_NO_DATA, "not enough data");
|
|
DEFINE_STATUS (ERROR_NO_SURFACE, "no surface vailable");
|
|
DEFINE_STATUS (ERROR_INVALID_SURFACE, "invalid surface");
|
|
DEFINE_STATUS (ERROR_BITSTREAM_PARSER, "bitstream parser error");
|
|
DEFINE_STATUS (ERROR_UNSUPPORTED_PROFILE, "unsupported profile");
|
|
DEFINE_STATUS (ERROR_UNSUPPORTED_CHROMA_FORMAT,
|
|
"unsupported chroma-format");
|
|
DEFINE_STATUS (ERROR_INVALID_PARAMETER, "invalid parameter");
|
|
default:
|
|
str = "<unknown>";
|
|
break;
|
|
}
|
|
#undef DEFINE_STATUS
|
|
|
|
return str;
|
|
}
|
|
|
|
static const gchar *
|
|
get_error_string (AppError error)
|
|
{
|
|
const gchar *str;
|
|
|
|
#define DEFINE_ERROR(error, error_string) \
|
|
case APP_ERROR_##error: \
|
|
str = error_string; \
|
|
break
|
|
|
|
switch (error) {
|
|
DEFINE_ERROR (NONE, "<none>");
|
|
DEFINE_ERROR (DECODER, "decoder");
|
|
DEFINE_ERROR (RENDERER, "renderer");
|
|
default:
|
|
str = "unknown";
|
|
break;
|
|
}
|
|
#undef DEFINE_ERROR
|
|
|
|
return str;
|
|
}
|
|
|
|
static gpointer
|
|
decoder_thread (gpointer data)
|
|
{
|
|
App *const app = data;
|
|
GError *error = NULL;
|
|
GstVaapiDecoderStatus status;
|
|
GstVaapiSurfaceProxy *proxy;
|
|
RenderFrame *rfp;
|
|
GstBuffer *buffer;
|
|
GstClockTime pts;
|
|
gboolean got_eos = FALSE;
|
|
guint ofs;
|
|
|
|
g_print ("Decoder thread started\n");
|
|
|
|
#define SEND_ERROR(...) \
|
|
do { \
|
|
error = g_error_new(APP_ERROR, APP_ERROR_DECODER, __VA_ARGS__); \
|
|
goto send_error; \
|
|
} while (0)
|
|
|
|
pts = g_get_monotonic_time ();
|
|
ofs = 0;
|
|
while (!g_atomic_int_get (&app->decoder_thread_cancel)) {
|
|
if (G_UNLIKELY (ofs == app->file_size))
|
|
buffer = NULL;
|
|
else {
|
|
const gsize size = MIN (4096, app->file_size - ofs);
|
|
buffer = gst_buffer_new_wrapped_full (GST_MEMORY_FLAG_READONLY,
|
|
app->file_data, app->file_size, ofs, size, NULL, NULL);
|
|
if (!buffer)
|
|
SEND_ERROR ("failed to allocate new buffer");
|
|
ofs += size;
|
|
}
|
|
if (!gst_vaapi_decoder_put_buffer (app->decoder, buffer))
|
|
SEND_ERROR ("failed to push buffer to decoder");
|
|
gst_buffer_replace (&buffer, NULL);
|
|
|
|
status = gst_vaapi_decoder_get_surface (app->decoder, &proxy);
|
|
switch (status) {
|
|
case GST_VAAPI_DECODER_STATUS_SUCCESS:
|
|
rfp = render_frame_new ();
|
|
if (!rfp)
|
|
SEND_ERROR ("failed to allocate render frame");
|
|
rfp->proxy = proxy;
|
|
rfp->pts = pts;
|
|
rfp->duration = app->frame_duration;
|
|
pts += app->frame_duration;
|
|
g_async_queue_push (app->decoder_queue, rfp);
|
|
break;
|
|
case GST_VAAPI_DECODER_STATUS_ERROR_NO_DATA:
|
|
/* nothing to do, just continue to the next iteration */
|
|
break;
|
|
case GST_VAAPI_DECODER_STATUS_END_OF_STREAM:
|
|
gst_vaapi_decoder_flush (app->decoder);
|
|
if (got_eos)
|
|
goto send_eos;
|
|
got_eos = TRUE;
|
|
break;
|
|
default:
|
|
SEND_ERROR ("%s", get_decoder_status_string (status));
|
|
break;
|
|
}
|
|
}
|
|
return NULL;
|
|
|
|
#undef SEND_ERROR
|
|
|
|
send_eos:
|
|
app_send_eos (app);
|
|
return NULL;
|
|
|
|
send_error:
|
|
app_send_error (app, error);
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
app_set_framerate (App * app, guint fps_n, guint fps_d)
|
|
{
|
|
if (!fps_n || !fps_d)
|
|
return;
|
|
|
|
g_mutex_lock (&app->mutex);
|
|
if (fps_n != app->fps_n || fps_d != app->fps_d) {
|
|
app->fps_n = fps_n;
|
|
app->fps_d = fps_d;
|
|
app->frame_duration =
|
|
gst_util_uint64_scale (GST_TIME_AS_USECONDS (GST_SECOND), fps_d, fps_n);
|
|
}
|
|
g_mutex_unlock (&app->mutex);
|
|
}
|
|
|
|
static void
|
|
handle_decoder_state_changes (GstVaapiDecoder * decoder,
|
|
const GstVideoCodecState * codec_state, gpointer user_data)
|
|
{
|
|
App *const app = user_data;
|
|
|
|
g_assert (app->decoder == decoder);
|
|
app_set_framerate (app, codec_state->info.fps_n, codec_state->info.fps_d);
|
|
}
|
|
|
|
static gboolean
|
|
start_decoder (App * app)
|
|
{
|
|
GstCaps *caps;
|
|
|
|
app->file = g_mapped_file_new (app->file_name, FALSE, NULL);
|
|
if (!app->file)
|
|
return FALSE;
|
|
|
|
app->file_size = g_mapped_file_get_length (app->file);
|
|
app->file_data = (guint8 *) g_mapped_file_get_contents (app->file);
|
|
if (!app->file_data)
|
|
return FALSE;
|
|
|
|
caps = caps_from_codec (app->codec);
|
|
switch (app->codec) {
|
|
case GST_VAAPI_CODEC_H264:
|
|
app->decoder = gst_vaapi_decoder_h264_new (app->display, caps);
|
|
break;
|
|
case GST_VAAPI_CODEC_JPEG:
|
|
app->decoder = gst_vaapi_decoder_jpeg_new (app->display, caps);
|
|
break;
|
|
case GST_VAAPI_CODEC_MPEG2:
|
|
app->decoder = gst_vaapi_decoder_mpeg2_new (app->display, caps);
|
|
break;
|
|
case GST_VAAPI_CODEC_MPEG4:
|
|
app->decoder = gst_vaapi_decoder_mpeg4_new (app->display, caps);
|
|
break;
|
|
case GST_VAAPI_CODEC_VC1:
|
|
app->decoder = gst_vaapi_decoder_vc1_new (app->display, caps);
|
|
break;
|
|
default:
|
|
app->decoder = NULL;
|
|
break;
|
|
}
|
|
if (!app->decoder)
|
|
return FALSE;
|
|
|
|
gst_vaapi_decoder_set_codec_state_changed_func (app->decoder,
|
|
handle_decoder_state_changes, app);
|
|
|
|
g_timer_start (app->timer);
|
|
|
|
app->decoder_thread = g_thread_try_new ("Decoder Thread", decoder_thread,
|
|
app, NULL);
|
|
if (!app->decoder_thread)
|
|
return FALSE;
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
stop_decoder (App * app)
|
|
{
|
|
g_timer_stop (app->timer);
|
|
|
|
g_atomic_int_set (&app->decoder_thread_cancel, TRUE);
|
|
g_thread_join (app->decoder_thread);
|
|
g_print ("Decoder thread stopped\n");
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
ensure_window_size (App * app, GstVaapiSurface * surface)
|
|
{
|
|
guint width, height;
|
|
|
|
if (gst_vaapi_window_get_fullscreen (app->window))
|
|
return;
|
|
|
|
gst_vaapi_surface_get_size (surface, &width, &height);
|
|
if (app->surface_width == width && app->surface_height == height)
|
|
return;
|
|
app->surface_width = width;
|
|
app->surface_height = height;
|
|
|
|
gst_vaapi_window_set_size (app->window, width, height);
|
|
gst_vaapi_window_get_size (app->window,
|
|
&app->window_width, &app->window_height);
|
|
}
|
|
|
|
static inline void
|
|
renderer_wait_until (App * app, GstClockTime pts)
|
|
{
|
|
g_mutex_lock (&app->mutex);
|
|
do {
|
|
} while (g_cond_wait_until (&app->render_ready, &app->mutex, pts));
|
|
g_mutex_unlock (&app->mutex);
|
|
}
|
|
|
|
static gboolean
|
|
renderer_process (App * app, RenderFrame * rfp)
|
|
{
|
|
GError *error = NULL;
|
|
GstVaapiSurface *surface;
|
|
const GstVaapiRectangle *crop_rect;
|
|
|
|
#define SEND_ERROR(...) \
|
|
do { \
|
|
error = g_error_new(APP_ERROR, APP_ERROR_RENDERER, __VA_ARGS__); \
|
|
goto send_error; \
|
|
} while (0)
|
|
|
|
surface = gst_vaapi_surface_proxy_get_surface (rfp->proxy);
|
|
if (!surface)
|
|
SEND_ERROR ("failed to get decoded surface from render frame");
|
|
|
|
ensure_window_size (app, surface);
|
|
|
|
crop_rect = gst_vaapi_surface_proxy_get_crop_rect (rfp->proxy);
|
|
|
|
if (!gst_vaapi_surface_sync (surface))
|
|
SEND_ERROR ("failed to sync decoded surface");
|
|
|
|
if (G_LIKELY (!g_benchmark))
|
|
renderer_wait_until (app, rfp->pts);
|
|
|
|
if (!gst_vaapi_window_put_surface (app->window, surface,
|
|
crop_rect, NULL, GST_VAAPI_PICTURE_STRUCTURE_FRAME))
|
|
SEND_ERROR ("failed to render surface %" GST_VAAPI_ID_FORMAT,
|
|
GST_VAAPI_ID_ARGS (gst_vaapi_surface_get_id (surface)));
|
|
|
|
app->num_frames++;
|
|
|
|
render_frame_replace (&app->last_frame, rfp);
|
|
return TRUE;
|
|
|
|
#undef SEND_ERROR
|
|
|
|
send_error:
|
|
app_send_error (app, error);
|
|
return FALSE;
|
|
}
|
|
|
|
static gpointer
|
|
renderer_thread (gpointer data)
|
|
{
|
|
App *const app = data;
|
|
RenderFrame *rfp;
|
|
|
|
g_print ("Render thread started\n");
|
|
|
|
while (!g_atomic_int_get (&app->render_thread_cancel)) {
|
|
rfp = g_async_queue_timeout_pop (app->decoder_queue, 1000000);
|
|
if (rfp && !renderer_process (app, rfp))
|
|
break;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static gboolean
|
|
flush_decoder_queue (App * app)
|
|
{
|
|
RenderFrame *rfp;
|
|
|
|
/* Flush pending surfaces */
|
|
do {
|
|
rfp = g_async_queue_try_pop (app->decoder_queue);
|
|
if (!rfp)
|
|
return TRUE;
|
|
} while (renderer_process (app, rfp));
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
start_renderer (App * app)
|
|
{
|
|
app->render_thread = g_thread_try_new ("Renderer Thread", renderer_thread,
|
|
app, NULL);
|
|
if (!app->render_thread)
|
|
return FALSE;
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
stop_renderer (App * app)
|
|
{
|
|
g_atomic_int_set (&app->render_thread_cancel, TRUE);
|
|
g_thread_join (app->render_thread);
|
|
|
|
g_print ("Render thread stopped\n");
|
|
|
|
flush_decoder_queue (app);
|
|
render_frame_replace (&app->last_frame, NULL);
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
app_free (App * app)
|
|
{
|
|
if (!app)
|
|
return;
|
|
|
|
if (app->file) {
|
|
g_mapped_file_unref (app->file);
|
|
app->file = NULL;
|
|
}
|
|
g_free (app->file_name);
|
|
|
|
gst_vaapi_decoder_replace (&app->decoder, NULL);
|
|
gst_vaapi_window_replace (&app->window, NULL);
|
|
gst_vaapi_display_replace (&app->display, NULL);
|
|
|
|
if (app->decoder_queue) {
|
|
g_async_queue_unref (app->decoder_queue);
|
|
app->decoder_queue = NULL;
|
|
}
|
|
|
|
if (app->timer) {
|
|
g_timer_destroy (app->timer);
|
|
app->timer = NULL;
|
|
}
|
|
|
|
g_cond_clear (&app->render_ready);
|
|
g_cond_clear (&app->event_cond);
|
|
g_mutex_clear (&app->mutex);
|
|
g_slice_free (App, app);
|
|
}
|
|
|
|
static App *
|
|
app_new (void)
|
|
{
|
|
App *app;
|
|
|
|
app = g_slice_new0 (App);
|
|
if (!app)
|
|
return NULL;
|
|
|
|
g_mutex_init (&app->mutex);
|
|
g_cond_init (&app->event_cond);
|
|
g_cond_init (&app->render_ready);
|
|
|
|
app_set_framerate (app, 60, 1);
|
|
app->window_width = 640;
|
|
app->window_height = 480;
|
|
|
|
app->decoder_queue = g_async_queue_new_full (
|
|
(GDestroyNotify) render_frame_free);
|
|
if (!app->decoder_queue)
|
|
goto error;
|
|
|
|
app->timer = g_timer_new ();
|
|
if (!app->timer)
|
|
goto error;
|
|
return app;
|
|
|
|
error:
|
|
app_free (app);
|
|
return NULL;
|
|
}
|
|
|
|
static gboolean
|
|
app_check_events (App * app)
|
|
{
|
|
GError *error = NULL;
|
|
gboolean stop = FALSE;
|
|
|
|
do {
|
|
g_mutex_lock (&app->mutex);
|
|
while (app->event == APP_RUNNING)
|
|
g_cond_wait (&app->event_cond, &app->mutex);
|
|
|
|
switch (app->event) {
|
|
case APP_GOT_ERROR:
|
|
error = app->error;
|
|
app->error = NULL;
|
|
/* fall-through */
|
|
case APP_GOT_EOS:
|
|
stop = TRUE;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
g_mutex_unlock (&app->mutex);
|
|
} while (!stop);
|
|
|
|
if (!error)
|
|
return TRUE;
|
|
|
|
g_message ("%s error: %s", get_error_string (error->code), error->message);
|
|
g_error_free (error);
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
app_run (App * app, int argc, char *argv[])
|
|
{
|
|
if (argc < 2) {
|
|
g_message ("no bitstream file specified");
|
|
return FALSE;
|
|
}
|
|
app->file_name = g_strdup (argv[1]);
|
|
|
|
if (!g_file_test (app->file_name, G_FILE_TEST_IS_REGULAR)) {
|
|
g_message ("failed to find file '%s'", app->file_name);
|
|
return FALSE;
|
|
}
|
|
|
|
app->codec = identify_codec (app->file_name);
|
|
if (!app->codec) {
|
|
app->codec = identify_codec_from_string (g_codec_str);
|
|
if (!app->codec) {
|
|
g_message ("failed to identify codec for '%s'", app->file_name);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
g_print ("Simple decoder (%s bitstream)\n", string_from_codec (app->codec));
|
|
|
|
app->display = video_output_create_display (NULL);
|
|
if (!app->display) {
|
|
g_message ("failed to create VA display");
|
|
return FALSE;
|
|
}
|
|
|
|
app->window = video_output_create_window (app->display,
|
|
app->window_width, app->window_height);
|
|
if (!app->window) {
|
|
g_message ("failed to create window");
|
|
return FALSE;
|
|
}
|
|
|
|
gst_vaapi_window_show (app->window);
|
|
|
|
if (!start_decoder (app)) {
|
|
g_message ("failed to start decoder thread");
|
|
return FALSE;
|
|
}
|
|
|
|
if (!start_renderer (app)) {
|
|
g_message ("failed to start renderer thread");
|
|
return FALSE;
|
|
}
|
|
|
|
app_check_events (app);
|
|
|
|
stop_renderer (app);
|
|
stop_decoder (app);
|
|
|
|
g_print ("Decoded %u frames", app->num_frames);
|
|
if (g_benchmark) {
|
|
const gdouble elapsed = g_timer_elapsed (app->timer, NULL);
|
|
g_print (" in %.2f sec (%.1f fps)\n",
|
|
elapsed, (gdouble) app->num_frames / elapsed);
|
|
}
|
|
g_print ("\n");
|
|
return TRUE;
|
|
}
|
|
|
|
int
|
|
main (int argc, char *argv[])
|
|
{
|
|
App *app;
|
|
gint ret;
|
|
|
|
if (!video_output_init (&argc, argv, g_options))
|
|
g_error ("failed to initialize video output subsystem");
|
|
|
|
app = app_new ();
|
|
if (!app)
|
|
g_error ("failed to create application context");
|
|
|
|
ret = !app_run (app, argc, argv);
|
|
|
|
app_free (app);
|
|
g_free (g_codec_str);
|
|
video_output_exit ();
|
|
return ret;
|
|
}
|