/*
 * demultiplex_x11.c -- extract full-screen images from an X11 connection.
 * (C) 2006-2010 Francesco Romani <fromani at gmail dot com>
 *
 * This file is part of transcode, a video stream processing tool.
 *
 * transcode is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * transcode 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "transcode.h"
#include "libtc/optstr.h"

#include "libtc/tcmodule-plugin.h"
#include "libtc/tctimer.h"
#include "libtc/optstr.h"

#include "x11source.h"

/*%*
 *%* DESCRIPTION 
 *%*   This module captures video frames from X window system using libX11.
 *%*
 *%* BUILD-DEPENDS
 *%*   libcx11-6 >= 1.0.0
 *%*
 *%* DEPENDS
 *%*   libcx11-6 >= 1.0.0
 *%*
 *%* PROCESSING
 *%*   import/demuxer
 *%*
 *%* MEDIA
 *%*   video
 *%*
 *%* #INPUT
 *%*
 *%* OUTPUT
 *%*   YUV420P, YUV422P, RGB24*
 *%*
 *%* OPTION
 *%*   skew_limit (integer)
 *%*     maximum frame A/V skew (ms) before correction attempt
 *%*/


/*
 * TODO (approx. priority order)
 * - Improve framerate emulation.
 *   It isn't easy without encoder support, and we will not have
 *   any smarter encoder at least until 1.2.0.
 * - Grab cursor.
 * - Make faster where feasible.
 */

#define DEBUG 1

#define LEGACY 1

#ifdef LEGACY
# define MOD_NAME    "import_x11.so"
#else
# define MOD_NAME    "demultiplex_x11.so"
#endif

#define MOD_VERSION "v0.1.0 (2007-07-21)"
#define MOD_CAP     "fetch full-screen frames from an X11 connection"

#define MOD_FEATURES \
    TC_MODULE_FEATURE_DEMULTIPLEX|TC_MODULE_FEATURE_VIDEO
#define MOD_FLAGS \
    TC_MODULE_FLAG_RECONFIGURABLE

/*************************************************************************/

static const char tc_x11_help[] = ""
    "Overview:\n"
    "    This module acts as a bridge from transcode an a X11 server.\n"
    "    It grabs screenshots at fixed rate from X11 connection, allowing\n"
    "    to record screencast and so on.\n"
    "Options:\n"
    "    skew_limit=N  tune maximum frame skew (ms) before correction\n"
    "    help          produce module overview and options explanations\n";

#define SKEW_LIM_DEFAULT    0
#define SKEW_LIM_MIN        0
#define SKEW_LIM_MAX        5

static const int frame_delay_divs[] = {
/*  div     skew_lim         */
    1,      /* 0 (disabled)  */ 
    2,      /* 1 (weakest)   */
    3,
    5,
    10,
    20      /* 5 (strongest) */
};


typedef struct tcx11privatedata_ TCX11PrivateData;
struct tcx11privatedata_ {
    TCX11Source src;
    TCTimer timer;

    uint64_t frame_delay; 
    /* how much (ms) we must sleep to properly emulate frame rate? */

    uint32_t expired;   /* counter for execessively delayed frames */

    uint64_t reftime;  /* reference time (ms) for skew computation */

    int64_t skew;       /* take in account excess of retard (ms)   */
    int64_t skew_limit; /* how much (ms) skew we can tolerate?     */
};


/*************************************************************************/
/* helpers */

static void tdebug(const TCX11PrivateData *priv, const char *str)
{
#ifdef DEBUG
    uint64_t now = tc_gettime();
    tc_log_info(MOD_NAME, "%-18s %lu", str, (unsigned long)(now - priv->reftime));
#endif
    return;
}

/*************************************************************************/

static int tc_x11_init(TCModuleInstance *self, uint32_t features)
{
    TCX11PrivateData *priv = NULL;

    TC_MODULE_SELF_CHECK(self, "init");
    TC_MODULE_INIT_CHECK(self, MOD_FEATURES, features);

    if (verbose) {
        tc_log_info(MOD_NAME, "%s %s", MOD_VERSION, MOD_CAP);
    }
    priv = tc_malloc(sizeof(TCX11PrivateData));
    if (priv == NULL) {
        return TC_ERROR;
    }

    self->userdata = priv;    
    return TC_OK;
}

static int tc_x11_fini(TCModuleInstance *self)
{
    TC_MODULE_SELF_CHECK(self, "fini");

    tc_free(self->userdata);
    self->userdata = NULL;

    return TC_OK;
}

static int tc_x11_configure(TCModuleInstance *self,
                            const char *options, vob_t *vob)
{
    TCX11PrivateData *priv = NULL;
    int ret = 0, skew_lim = SKEW_LIM_DEFAULT;

    TC_MODULE_SELF_CHECK(self, "configure");

    priv = self->userdata;

    if (options != NULL) {
        optstr_get(options, "skew_limit", "%i", &skew_lim);
        if (skew_lim < SKEW_LIM_MIN || skew_lim > SKEW_LIM_MAX) {
            tc_log_warn(MOD_NAME, "skew limit value out of range,"
                                  " reset to defaults [%i]",
                        SKEW_LIM_DEFAULT);
        }
    }

    priv->skew = 0;
    priv->reftime = 0;
    priv->expired = 0;
    priv->frame_delay = (uint64_t)(1000000.0 / vob->fps); /* microseconds */
    priv->skew_limit = priv->frame_delay / frame_delay_divs[skew_lim];

    if (verbose >= TC_DEBUG) {
        tc_log_info(MOD_NAME, "frame delay: %lu ms",
                              (unsigned long)priv->frame_delay);
        tc_log_info(MOD_NAME, "skew limit:  %li ms",
                              (long)priv->skew_limit);
    }


    ret = tc_timer_init_soft(&priv->timer, 0);
    if (ret != 0) {
        tc_log_error(MOD_NAME, "configure: can't initialize timer");
        return TC_ERROR;
    }

    /* nothing to do here, yet */
    ret = tc_x11source_is_display_name(vob->video_in_file);
    if (ret == TC_FALSE) {
        tc_log_error(MOD_NAME, "configure: given source doesn't look like"
                               " a DISPLAY specifier");
        return TC_ERROR;
    }

    ret = tc_x11source_open(&priv->src, vob->video_in_file,
                            TC_X11_MODE_BEST, vob->im_v_codec);
    if (ret != 0) {
        tc_log_error(MOD_NAME, "configure: failed to open X11 connection"
                               " to '%s'", vob->video_in_file);
        return TC_ERROR;
    }

    return TC_OK;
}

static int tc_x11_inspect(TCModuleInstance *self,
                          const char *param, const char **value)
{
    TC_MODULE_SELF_CHECK(self, "inspect");

    if (optstr_lookup(param, "help")) {
        *value = tc_x11_help;
    }

    return TC_OK;
}

static int tc_x11_stop(TCModuleInstance *self)
{
    TCX11PrivateData *priv = NULL;
    int ret = 0;

    TC_MODULE_SELF_CHECK(self, "stop");

    priv = self->userdata;

    ret = tc_x11source_close(&priv->src);
    if (ret != 0) {
        tc_log_error(MOD_NAME, "stop: failed to close X11 connection");
        return TC_ERROR;
    }

    ret = tc_timer_fini(&priv->timer);
    if (ret != 0) {
        tc_log_error(MOD_NAME, "stop: failed to stop timer");
        return TC_ERROR;
    }

    if (verbose >= TC_DEBUG) {
        tc_log_info(MOD_NAME, "expired frames count: %lu",
                              (unsigned long)priv->expired);
    }
    return TC_OK;
}

static int tc_x11_demultiplex(TCModuleInstance *self,
                              vframe_list_t *vframe, aframe_list_t *aframe)
{
    TCX11PrivateData *priv = NULL;
    uint64_t now = 0;
    int ret = 0;

    TC_MODULE_SELF_CHECK(self, "demultiplex");
    priv = self->userdata;

    priv->reftime = tc_gettime();

    tdebug(priv, "begin demultiplex");

    if (aframe != NULL) {
        aframe->audio_len = 0; /* no audio from here */
    }

    if (vframe != NULL) {
        tdebug(priv, "  begin acquire");

        ret = tc_x11source_acquire(&priv->src, vframe->video_buf,
                                   vframe->video_size);

        tdebug(priv, "  end acquire");

        if (ret > 0) {
            int64_t naptime = 0;
            uint64_t now = 0;
            
            vframe->attributes |= TC_FRAME_IS_KEYFRAME;
            vframe->video_len = ret;
       
            now = tc_gettime();
            naptime = (priv->frame_delay - (now - priv->reftime));

            if (priv->skew >= priv->skew_limit) {
                tc_log_info(MOD_NAME, "  skew correction (naptime was %lu)", 
                                      (unsigned long)naptime);
                int64_t t = naptime;
                naptime -= priv->skew;
                priv->skew = TC_MAX(0, priv->skew - t);
            }

            if (naptime <= 0) {
                /* don't sleep at all if delay is already excessive */
                tc_log_info(MOD_NAME, "%-18s", "  NO SLEEP!");
                priv->expired++;
            } else {
                tc_log_info(MOD_NAME, "%-18s %lu", "  sleep time",
                                      (unsigned long)(naptime));
                tc_timer_sleep(&priv->timer, (uint64_t)naptime);
            }
        }
    }

    now = tc_gettime();
    now -= priv->reftime;
    priv->skew += now - priv->frame_delay;

    tdebug(priv, "end multiplex");

    tc_log_info(MOD_NAME, "%-18s %li", "detected skew", (long)(priv->skew));
    return (ret > 0) ?ret :-1;
}


/*************************************************************************/

static const TCCodecID tc_x11_codecs_in[] = { TC_CODEC_ERROR };

/* a multiplexor is at the end of pipeline */
static const TCCodecID tc_x11_codecs_out[] = { 
    TC_CODEC_RGB, TC_CODEC_YUV420P, TC_CODEC_YUV422P, TC_CODEC_ERROR 
};

static const TCFormatID tc_x11_formats_in[] = { TC_FORMAT_X11, TC_FORMAT_ERROR };
static const TCFormatID tc_x11_formats_out[] = { TC_FORMAT_ERROR };

static const TCModuleInfo tc_x11_info = {
    .features    = MOD_FEATURES,
    .flags       = MOD_FLAGS,
    .name        = MOD_NAME,
    .version     = MOD_VERSION,
    .description = MOD_CAP,
    .codecs_in   = tc_x11_codecs_in,
    .codecs_out  = tc_x11_codecs_out,
    .formats_in  = tc_x11_formats_in,
    .formats_out = tc_x11_formats_out
};

static const TCModuleClass tc_x11_class = {
    TC_MODULE_CLASS_HEAD(tc_x11),

    .init         = tc_x11_init,
    .fini         = tc_x11_fini,
    .configure    = tc_x11_configure,
    .stop         = tc_x11_stop,
    .inspect      = tc_x11_inspect,

    .demultiplex  = tc_x11_demultiplex,
};

TC_MODULE_ENTRY_POINT(tc_x11)

/*************************************************************************/
/*************************************************************************/

/* Old-fashioned module interface. */

static TCModuleInstance mod_video;

static int verbose_flag;
static int capability_flag = TC_CAP_YUV|TC_CAP_RGB|TC_CAP_YUV422|TC_CAP_VID;

#define MOD_PRE x11
#define MOD_CODEC "(video) X11"

#include "import_def.h"

/*************************************************************************/

#define RETURN_IF_FAILED(ret) do { \
    if ((ret) != TC_OK) { \
        return ret; \
    } \
} while (0)

#define COMMON_CHECK(param) do { \
    if ((param)->flag != TC_VIDEO) { \
        return TC_ERROR; \
    } \
} while (0)


MOD_open
{
    int ret;

    COMMON_CHECK(param);

    ret = tc_x11_init(&mod_video, TC_MODULE_FEATURE_DEMULTIPLEX);
    RETURN_IF_FAILED(ret);

    ret = tc_x11_configure(&mod_video, "", vob);
    RETURN_IF_FAILED(ret);

    return TC_OK;
}

MOD_decode
{
    vframe_list_t vframe;
    int ret = 0;

    COMMON_CHECK(param);

    vframe.attributes = 0;
    vframe.video_buf = param->buffer;
    vframe.video_size = param->size;

    ret = tc_x11_demultiplex(&mod_video, &vframe, NULL);

    if (ret <= 0) {
        /* well, frames from X11 never "ends", really :) */
        return TC_ERROR;
    }

    param->size = ret;
    param->attributes = vframe.attributes;
    return TC_OK;
}

MOD_close
{
    int ret;

    COMMON_CHECK(param);
    
    ret = tc_x11_stop(&mod_video);
    RETURN_IF_FAILED(ret);

    ret = tc_x11_fini(&mod_video);
    RETURN_IF_FAILED(ret);

    return TC_OK;
}

/*************************************************************************/
/*
 * Local variables:
 *   c-file-style: "stroustrup"
 *   c-file-offsets: ((case-label . *) (statement-case-intro . *))
 *   indent-tabs-mode: nil
 * End:
 *
 * vim: expandtab shiftwidth=4:
 */
