/*
 * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
 *
 * This program 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.
 *
 * This program 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
#include <pjmedia-videodev/videodev_imp.h>
#include <pjmedia/event.h>
#include <pj/assert.h>
#include <pj/log.h>
#include <pj/os.h>

#if defined(PJMEDIA_HAS_VIDEO) && PJMEDIA_HAS_VIDEO != 0 && \
    defined(PJMEDIA_VIDEO_DEV_HAS_SDL) && PJMEDIA_VIDEO_DEV_HAS_SDL != 0

#include <SDL.h>
#include <SDL_syswm.h>
#if PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL
#   include "SDL_opengl.h"
#   define OPENGL_DEV_IDX 1
#endif /* PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL */

#if !(SDL_VERSION_ATLEAST(1,3,0))
#   error "SDL 1.3 or later is required"
#endif

#if defined(PJ_DARWINOS) && PJ_DARWINOS!=0
#   include "TargetConditionals.h"
#   include <Foundation/Foundation.h>
#endif

#define THIS_FILE               "sdl_dev.c"
#define DEFAULT_CLOCK_RATE      90000
#define DEFAULT_WIDTH           640
#define DEFAULT_HEIGHT          480
#define DEFAULT_FPS             25

typedef struct sdl_fmt_info
{
    pjmedia_format_id   fmt_id;
    Uint32              sdl_format;
    Uint32              Rmask;
    Uint32              Gmask;
    Uint32              Bmask;
    Uint32              Amask;
} sdl_fmt_info;

static sdl_fmt_info sdl_fmts[] =
{
#if PJ_IS_BIG_ENDIAN
    {PJMEDIA_FORMAT_RGBA,  (Uint32)SDL_PIXELFORMAT_RGBA8888,
     0xFF000000, 0xFF0000, 0xFF00, 0xFF} ,
    {PJMEDIA_FORMAT_RGB24, (Uint32)SDL_PIXELFORMAT_RGB24,
     0xFF0000, 0xFF00, 0xFF, 0} ,
    {PJMEDIA_FORMAT_BGRA,  (Uint32)SDL_PIXELFORMAT_BGRA8888,
     0xFF00, 0xFF0000, 0xFF000000, 0xFF} ,
#else /* PJ_IS_BIG_ENDIAN */
    {PJMEDIA_FORMAT_RGBA,  (Uint32)SDL_PIXELFORMAT_ABGR8888,
     0xFF, 0xFF00, 0xFF0000, 0xFF000000} ,
    {PJMEDIA_FORMAT_RGB24, (Uint32)SDL_PIXELFORMAT_BGR24,
     0xFF, 0xFF00, 0xFF0000, 0} ,
    {PJMEDIA_FORMAT_BGRA,  (Uint32)SDL_PIXELFORMAT_ARGB8888,
     0xFF0000, 0xFF00, 0xFF, 0xFF000000} ,
#endif /* PJ_IS_BIG_ENDIAN */

    {PJMEDIA_FORMAT_DIB , (Uint32)SDL_PIXELFORMAT_RGB24,
     0xFF0000, 0xFF00, 0xFF, 0} ,

    {PJMEDIA_FORMAT_YUY2, SDL_PIXELFORMAT_YUY2, 0, 0, 0, 0} ,
    {PJMEDIA_FORMAT_UYVY, SDL_PIXELFORMAT_UYVY, 0, 0, 0, 0} ,
    {PJMEDIA_FORMAT_YVYU, SDL_PIXELFORMAT_YVYU, 0, 0, 0, 0} ,
    {PJMEDIA_FORMAT_I420, SDL_PIXELFORMAT_IYUV, 0, 0, 0, 0} ,
    {PJMEDIA_FORMAT_YV12, SDL_PIXELFORMAT_YV12, 0, 0, 0, 0} ,
    {PJMEDIA_FORMAT_I420JPEG, SDL_PIXELFORMAT_IYUV, 0, 0, 0, 0} ,
    {PJMEDIA_FORMAT_I422JPEG, SDL_PIXELFORMAT_YV12, 0, 0, 0, 0}
};

/* sdl_ device info */
struct sdl_dev_info
{
    pjmedia_vid_dev_info         info;
};

/* Linked list of streams */
struct stream_list
{
    PJ_DECL_LIST_MEMBER(struct stream_list);
    struct sdl_stream   *stream;
};

#define INITIAL_MAX_JOBS 64
#define JOB_QUEUE_INC_FACTOR 2

typedef pj_status_t (*job_func_ptr)(void *data);

typedef struct job {
    job_func_ptr    func;
    void           *data;
    unsigned        flags;
    pj_status_t     retval;
} job;

#if defined(PJ_DARWINOS) && PJ_DARWINOS!=0
@interface JQDelegate: NSObject
{
    @public
    job *pjob;
}

- (void)run_job;
@end

@implementation JQDelegate
- (void)run_job
{
    pjob->retval = (*pjob->func)(pjob->data);
}
@end
#endif /* PJ_DARWINOS */

typedef struct job_queue {
    pj_pool_t      *pool;
    job           **jobs;
    pj_sem_t      **job_sem;
    pj_sem_t      **old_sem;
    pj_mutex_t     *mutex;
    pj_thread_t    *thread;
    pj_sem_t       *sem;

    unsigned        size;
    unsigned        head, tail;
    pj_bool_t       is_full;
    pj_bool_t       is_quitting;
} job_queue;

/* sdl_ factory */
struct sdl_factory
{
    pjmedia_vid_dev_factory      base;
    pj_pool_t                   *pool;
    pj_pool_factory             *pf;

    unsigned                     dev_count;
    struct sdl_dev_info         *dev_info;
    job_queue                   *jq;

    pj_thread_t                 *sdl_thread;        /**< SDL thread.        */
    pj_sem_t                    *sem;
    pj_mutex_t                  *mutex;
    struct stream_list           streams;
    pj_bool_t                    is_quitting;
    pj_thread_desc               thread_desc;
    pj_thread_t                 *ev_thread;
};

/* Video stream. */
struct sdl_stream
{
    pjmedia_vid_dev_stream       base;              /**< Base stream        */
    pjmedia_vid_dev_param        param;             /**< Settings           */
    pj_pool_t                   *pool;              /**< Memory pool.       */

    pjmedia_vid_dev_cb           vid_cb;            /**< Stream callback.   */
    void                        *user_data;         /**< Application data.  */

    struct sdl_factory          *sf;
    const pjmedia_frame         *frame;
    pj_bool_t                    is_running;
    pj_timestamp                 last_ts;
    struct stream_list           list_entry;

    SDL_Window                  *window;            /**< Display window.    */
    SDL_Renderer                *renderer;          /**< Display renderer.  */
    SDL_Texture                 *scr_tex;           /**< Screen texture.    */
    int                          pitch;             /**< Pitch value.       */
    SDL_Rect                     rect;              /**< Frame rectangle.   */
    SDL_Rect                     dstrect;           /**< Display rectangle. */
#if PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL
    SDL_GLContext               *gl_context;
    GLuint                       texture;
#endif /* PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL */

    pjmedia_video_apply_fmt_param vafp;
};

/* Prototypes */
static pj_status_t sdl_factory_init(pjmedia_vid_dev_factory *f);
static pj_status_t sdl_factory_destroy(pjmedia_vid_dev_factory *f);
static pj_status_t sdl_factory_refresh(pjmedia_vid_dev_factory *f);
static unsigned    sdl_factory_get_dev_count(pjmedia_vid_dev_factory *f);
static pj_status_t sdl_factory_get_dev_info(pjmedia_vid_dev_factory *f,
                                            unsigned index,
                                            pjmedia_vid_dev_info *info);
static pj_status_t sdl_factory_default_param(pj_pool_t *pool,
                                             pjmedia_vid_dev_factory *f,
                                             unsigned index,
                                             pjmedia_vid_dev_param *param);
static pj_status_t sdl_factory_create_stream(
                                        pjmedia_vid_dev_factory *f,
                                        pjmedia_vid_dev_param *param,
                                        const pjmedia_vid_dev_cb *cb,
                                        void *user_data,
                                        pjmedia_vid_dev_stream **p_vid_strm);

static pj_status_t sdl_stream_get_param(pjmedia_vid_dev_stream *strm,
                                        pjmedia_vid_dev_param *param);
static pj_status_t sdl_stream_get_cap(pjmedia_vid_dev_stream *strm,
                                      pjmedia_vid_dev_cap cap,
                                      void *value);
static pj_status_t sdl_stream_set_cap(pjmedia_vid_dev_stream *strm,
                                      pjmedia_vid_dev_cap cap,
                                      const void *value);
static pj_status_t sdl_stream_put_frame(pjmedia_vid_dev_stream *strm,
                                        const pjmedia_frame *frame);
static pj_status_t sdl_stream_start(pjmedia_vid_dev_stream *strm);
static pj_status_t sdl_stream_stop(pjmedia_vid_dev_stream *strm);
static pj_status_t sdl_stream_destroy(pjmedia_vid_dev_stream *strm);

static pj_status_t resize_disp(struct sdl_stream *strm,
                               pjmedia_rect_size *new_disp_size);
static pj_status_t sdl_destroy_all(void *data);

/* Job queue prototypes */
static pj_status_t job_queue_create(pj_pool_t *pool, job_queue **pjq);
static pj_status_t job_queue_post_job(job_queue *jq, job_func_ptr func,
                                      void *data, unsigned flags,
                                      pj_status_t *retval);
static pj_status_t job_queue_destroy(job_queue *jq);

/* Operations */
static pjmedia_vid_dev_factory_op factory_op =
{
    &sdl_factory_init,
    &sdl_factory_destroy,
    &sdl_factory_get_dev_count,
    &sdl_factory_get_dev_info,
    &sdl_factory_default_param,
    &sdl_factory_create_stream,
    &sdl_factory_refresh
};

static pjmedia_vid_dev_stream_op stream_op =
{
    &sdl_stream_get_param,
    &sdl_stream_get_cap,
    &sdl_stream_set_cap,
    &sdl_stream_start,
    NULL,
    &sdl_stream_put_frame,
    &sdl_stream_stop,
    &sdl_stream_destroy
};

/*
 * Util
 */
static void sdl_log_err(const char *op)
{
    PJ_LOG(1,(THIS_FILE, "%s error: %s", op, SDL_GetError()));
}

/****************************************************************************
 * Factory operations
 */
/*
 * Init sdl_ video driver.
 */
pjmedia_vid_dev_factory* pjmedia_sdl_factory(pj_pool_factory *pf)
{
    struct sdl_factory *f;
    pj_pool_t *pool;

    pool = pj_pool_create(pf, "sdl video", 1000, 1000, NULL);
    f = PJ_POOL_ZALLOC_T(pool, struct sdl_factory);
    f->pf = pf;
    f->pool = pool;
    f->base.op = &factory_op;

    return &f->base;
}

static pj_status_t sdl_init(void * data)
{
    PJ_UNUSED_ARG(data);

#if SDL_VERSION_ATLEAST(2,0,2)
    /* Since SDL 2.0.2, screensaver is disabled by default, to maintain
     * the existing behavior, let's enable screensaver.
     */
    SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
#endif

    if (SDL_Init(SDL_INIT_VIDEO)) {
        sdl_log_err("SDL_Init()");
        return PJMEDIA_EVID_INIT;
    }

    return PJ_SUCCESS;
}

static struct sdl_stream* find_stream(struct sdl_factory *sf,
                                      Uint32 windowID,
                                      pjmedia_event *pevent)
{
    struct stream_list *it, *itBegin;
    struct sdl_stream *strm = NULL;

    itBegin = &sf->streams;
    for (it = itBegin->next; it != itBegin; it = it->next) {
        if (SDL_GetWindowID(it->stream->window) == windowID)
        {
            strm = it->stream;
            break;
        }
    }
 
    if (strm)
        pjmedia_event_init(pevent, PJMEDIA_EVENT_NONE, &strm->last_ts,
                           strm);

    return strm;
}

static pj_status_t handle_event(void *data)
{
    struct sdl_factory *sf = (struct sdl_factory*)data;
    SDL_Event sevent;

    if (!pj_thread_is_registered())
        pj_thread_register("sdl_ev", sf->thread_desc, &sf->ev_thread);

    while (SDL_PollEvent(&sevent)) {
        struct sdl_stream *strm = NULL;
        pjmedia_event pevent;

        pj_mutex_lock(sf->mutex);
        pevent.type = PJMEDIA_EVENT_NONE;
        switch(sevent.type) {
        case SDL_MOUSEBUTTONDOWN:
            strm = find_stream(sf, sevent.button.windowID, &pevent);
            pevent.type = PJMEDIA_EVENT_MOUSE_BTN_DOWN;
            break;
        case SDL_WINDOWEVENT:
            strm = find_stream(sf, sevent.window.windowID, &pevent);
            switch (sevent.window.event) {
            case SDL_WINDOWEVENT_RESIZED:
                pevent.type = PJMEDIA_EVENT_WND_RESIZED;
                pevent.data.wnd_resized.new_size.w =
                    sevent.window.data1;
                pevent.data.wnd_resized.new_size.h =
                    sevent.window.data2;
                break;
            case SDL_WINDOWEVENT_CLOSE:
                pevent.type = PJMEDIA_EVENT_WND_CLOSING;
                break;
            }
            break;
        default:
            break;
        }

        if (strm && pevent.type != PJMEDIA_EVENT_NONE) {
            pj_status_t status;

            pjmedia_event_publish(NULL, strm, &pevent, 0);

            switch (pevent.type) {
            case PJMEDIA_EVENT_WND_RESIZED:
                status = resize_disp(strm, &pevent.data.wnd_resized.new_size);
                if (status != PJ_SUCCESS) {
                    PJ_PERROR(3, (THIS_FILE, status,
                                  "Failed resizing the display."));
                }
                break;
            case PJMEDIA_EVENT_WND_CLOSING:
                if (pevent.data.wnd_closing.cancel) {
                    /* Cancel the closing operation */
                    break;
                }

                /* Proceed to cleanup SDL. App must still call
                 * pjmedia_dev_stream_destroy() when getting WND_CLOSED
                 * event
                 */
                sdl_stream_stop(&strm->base);
                sdl_destroy_all(strm);
                pjmedia_event_init(&pevent, PJMEDIA_EVENT_WND_CLOSED,
                                   &strm->last_ts, strm);
                pjmedia_event_publish(NULL, strm, &pevent, 0);

                /*
                 * Note: don't access the stream after this point, it
                 * might have been destroyed
                 */
                break;
            default:
                /* Just to prevent gcc warning about unused enums */
                break;
            }
        }

        pj_mutex_unlock(sf->mutex);
    }

    return PJ_SUCCESS;
}

static int sdl_ev_thread(void *data)
{
    struct sdl_factory *sf = (struct sdl_factory*)data;

    while(1) {
        pj_status_t status;

        pj_mutex_lock(sf->mutex);
        if (pj_list_empty(&sf->streams)) {
            pj_mutex_unlock(sf->mutex);
            /* Wait until there is any stream. */
            pj_sem_wait(sf->sem);
        } else
            pj_mutex_unlock(sf->mutex);

        if (sf->is_quitting)
            break;

        job_queue_post_job(sf->jq, handle_event, sf, 0, &status);

        pj_thread_sleep(50);
    }

    return 0;
}

static pj_status_t sdl_quit(void *data)
{
    PJ_UNUSED_ARG(data);
    SDL_Quit();
    return PJ_SUCCESS;
}

/* API: init factory */
static pj_status_t sdl_factory_init(pjmedia_vid_dev_factory *f)
{
    struct sdl_factory *sf = (struct sdl_factory*)f;
    struct sdl_dev_info *ddi;
    unsigned i, j;
    pj_status_t status;
    SDL_version version;

    pj_list_init(&sf->streams);

    status = job_queue_create(sf->pool, &sf->jq);
    if (status != PJ_SUCCESS)
        return PJMEDIA_EVID_INIT;

    job_queue_post_job(sf->jq, sdl_init, NULL, 0, &status);
    if (status != PJ_SUCCESS)
        return status;

    status = pj_mutex_create_recursive(sf->pool, "sdl_factory",
                                       &sf->mutex);
    if (status != PJ_SUCCESS)
        return status;

    status = pj_sem_create(sf->pool, NULL, 0, 1, &sf->sem);
    if (status != PJ_SUCCESS)
        return status;

    /* Create event handler thread. */
    status = pj_thread_create(sf->pool, "sdl_thread", sdl_ev_thread,
                              sf, 0, 0, &sf->sdl_thread);
    if (status != PJ_SUCCESS)
        return status;

    sf->dev_count = 1;
#if PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL
    sf->dev_count++;
#endif /* PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL */
    sf->dev_info = (struct sdl_dev_info*)
                   pj_pool_calloc(sf->pool, sf->dev_count,
                                  sizeof(struct sdl_dev_info));

    ddi = &sf->dev_info[0];
    pj_bzero(ddi, sizeof(*ddi));
    strncpy(ddi->info.name, "SDL renderer", sizeof(ddi->info.name));
    ddi->info.name[sizeof(ddi->info.name)-1] = '\0';
    ddi->info.fmt_cnt = PJ_ARRAY_SIZE(sdl_fmts);

#if PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL
    ddi = &sf->dev_info[OPENGL_DEV_IDX];
    pj_bzero(ddi, sizeof(*ddi));
    strncpy(ddi->info.name, "SDL openGL renderer", sizeof(ddi->info.name));
    ddi->info.name[sizeof(ddi->info.name)-1] = '\0';
    ddi->info.fmt_cnt = 1;
#endif /* PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL */

    for (i = 0; i < sf->dev_count; i++) {
        ddi = &sf->dev_info[i];
        strncpy(ddi->info.driver, "SDL", sizeof(ddi->info.driver));
        ddi->info.driver[sizeof(ddi->info.driver)-1] = '\0';
        ddi->info.dir = PJMEDIA_DIR_RENDER;
        ddi->info.has_callback = PJ_FALSE;
        ddi->info.caps = PJMEDIA_VID_DEV_CAP_FORMAT |
                         PJMEDIA_VID_DEV_CAP_OUTPUT_RESIZE;
        ddi->info.caps |= PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW;
        ddi->info.caps |= PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW_FLAGS;

        for (j = 0; j < ddi->info.fmt_cnt; j++) {
            pjmedia_format *fmt = &ddi->info.fmt[j];
            pjmedia_format_init_video(fmt, sdl_fmts[j].fmt_id,
                                      DEFAULT_WIDTH, DEFAULT_HEIGHT,
                                      DEFAULT_FPS, 1);
        }
    }

    SDL_VERSION(&version);
    PJ_LOG(4, (THIS_FILE, "SDL %d.%d initialized",
                          version.major, version.minor));

    return PJ_SUCCESS;
}

/* API: destroy factory */
static pj_status_t sdl_factory_destroy(pjmedia_vid_dev_factory *f)
{
    struct sdl_factory *sf = (struct sdl_factory*)f;
    pj_pool_t *pool = sf->pool;
    pj_status_t status;

    pj_assert(pj_list_empty(&sf->streams));

    sf->is_quitting = PJ_TRUE;
    if (sf->sdl_thread) {
        pj_sem_post(sf->sem);
#if defined(PJ_DARWINOS) && PJ_DARWINOS!=0
        /* To prevent pj_thread_join() of getting stuck if we are in
         * the main thread and we haven't finished processing the job
         * posted by sdl_thread.
         */
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
#endif
        pj_thread_join(sf->sdl_thread);
        pj_thread_destroy(sf->sdl_thread);
    }

    if (sf->mutex) {
        pj_mutex_destroy(sf->mutex);
        sf->mutex = NULL;
    }

    if (sf->sem) {
        pj_sem_destroy(sf->sem);
        sf->sem = NULL;
    }

    job_queue_post_job(sf->jq, sdl_quit, NULL, 0, &status);
    job_queue_destroy(sf->jq);

    sf->pool = NULL;
    pj_pool_release(pool);

    return PJ_SUCCESS;
}

/* API: refresh the list of devices */
static pj_status_t sdl_factory_refresh(pjmedia_vid_dev_factory *f)
{
    PJ_UNUSED_ARG(f);
    return PJ_SUCCESS;
}

/* API: get number of devices */
static unsigned sdl_factory_get_dev_count(pjmedia_vid_dev_factory *f)
{
    struct sdl_factory *sf = (struct sdl_factory*)f;
    return sf->dev_count;
}

/* API: get device info */
static pj_status_t sdl_factory_get_dev_info(pjmedia_vid_dev_factory *f,
                                            unsigned index,
                                            pjmedia_vid_dev_info *info)
{
    struct sdl_factory *sf = (struct sdl_factory*)f;

    PJ_ASSERT_RETURN(index < sf->dev_count, PJMEDIA_EVID_INVDEV);

    pj_memcpy(info, &sf->dev_info[index].info, sizeof(*info));

    return PJ_SUCCESS;
}

/* API: create default device parameter */
static pj_status_t sdl_factory_default_param(pj_pool_t *pool,
                                             pjmedia_vid_dev_factory *f,
                                             unsigned index,
                                             pjmedia_vid_dev_param *param)
{
    struct sdl_factory *sf = (struct sdl_factory*)f;
    struct sdl_dev_info *di = &sf->dev_info[index];

    PJ_ASSERT_RETURN(index < sf->dev_count, PJMEDIA_EVID_INVDEV);
    
    PJ_UNUSED_ARG(pool);

    pj_bzero(param, sizeof(*param));
    param->dir = PJMEDIA_DIR_RENDER;
    param->rend_id = index;
    param->cap_id = PJMEDIA_VID_INVALID_DEV;

    /* Set the device capabilities here */
    param->flags = PJMEDIA_VID_DEV_CAP_FORMAT;
    param->fmt.type = PJMEDIA_TYPE_VIDEO;
    param->clock_rate = DEFAULT_CLOCK_RATE;
    pj_memcpy(&param->fmt, &di->info.fmt[0], sizeof(param->fmt));

    return PJ_SUCCESS;
}

static sdl_fmt_info* get_sdl_format_info(pjmedia_format_id id)
{
    unsigned i;

    for (i = 0; i < sizeof(sdl_fmts)/sizeof(sdl_fmts[0]); i++) {
        if (sdl_fmts[i].fmt_id == id)
            return &sdl_fmts[i];
    }

    return NULL;
}

static pj_status_t sdl_destroy(void *data)
{
    struct sdl_stream *strm = (struct sdl_stream *)data;
     
#if PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL
    if (strm->texture) {
        glDeleteTextures(1, &strm->texture);
        strm->texture = 0;
    }
    if (strm->gl_context) {
        SDL_GL_DeleteContext(strm->gl_context);
        strm->gl_context = NULL;
    }
#endif /* PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL */
    if (strm->scr_tex) {
        SDL_DestroyTexture(strm->scr_tex);
        strm->scr_tex = NULL;
    }
    if (strm->renderer) {
        SDL_DestroyRenderer(strm->renderer);
        strm->renderer = NULL;
    }    
    return PJ_SUCCESS;
}

static pj_status_t sdl_destroy_all(void *data)
{
    struct sdl_stream *strm = (struct sdl_stream *)data;  

    sdl_destroy(data);
#if !defined(TARGET_OS_IPHONE) || TARGET_OS_IPHONE == 0
    if (strm->window &&
        !(strm->param.flags & PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW))
    {
        SDL_DestroyWindow(strm->window);
    }
    strm->window = NULL;
#endif /* TARGET_OS_IPHONE */
    return PJ_SUCCESS;
}

static pj_status_t sdl_create_window(struct sdl_stream *strm, 
                                     pj_bool_t use_app_win,
                                     Uint32 sdl_format,
                                     pjmedia_vid_dev_hwnd *hwnd)
{
    if (!strm->window) {
        Uint32 flags = 0;
        
        if (strm->param.flags & PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW_FLAGS) {
            if (!(strm->param.window_flags & PJMEDIA_VID_DEV_WND_BORDER))
                flags |= SDL_WINDOW_BORDERLESS;
            if (strm->param.window_flags & PJMEDIA_VID_DEV_WND_RESIZABLE)
                flags |= SDL_WINDOW_RESIZABLE;
        } else {
            flags |= SDL_WINDOW_BORDERLESS;
        }

        if (!((strm->param.flags & PJMEDIA_VID_DEV_CAP_OUTPUT_HIDE) &&
            strm->param.window_hide))
        {
            flags |= SDL_WINDOW_SHOWN;
        } else {
            flags &= ~SDL_WINDOW_SHOWN;
            flags |= SDL_WINDOW_HIDDEN;
        }

        if ((strm->param.flags & PJMEDIA_VID_DEV_CAP_OUTPUT_FULLSCREEN) &&
            strm->param.window_fullscreen)
        {
            if (strm->param.window_fullscreen == PJMEDIA_VID_DEV_FULLSCREEN)
                flags |= SDL_WINDOW_FULLSCREEN;
            else
                flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
        }

#if PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL
        if (strm->param.rend_id == OPENGL_DEV_IDX)
            flags |= SDL_WINDOW_OPENGL;
#endif /* PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL */   
        if (use_app_win) {
            /* Use the window supplied by the application. */       
            strm->window = SDL_CreateWindowFrom(hwnd->info.window);
            if (!strm->window) {
                sdl_log_err("SDL_CreateWindowFrom()");
                return PJMEDIA_EVID_SYSERR;
            }
        } else {
            int x, y;

            x = y = SDL_WINDOWPOS_CENTERED;
            if (strm->param.flags & PJMEDIA_VID_DEV_CAP_OUTPUT_POSITION) {
                x = strm->param.window_pos.x;
                y = strm->param.window_pos.y;
            }

            /* Create the window where we will draw. */
            strm->window = SDL_CreateWindow("pjmedia-SDL video",
                                            x, y,
                                            strm->param.disp_size.w,
                                            strm->param.disp_size.h,
                                            flags);
            if (!strm->window) {
                sdl_log_err("SDL_CreateWindow()");
                return PJMEDIA_EVID_SYSERR;
            }
        }
    }

    /**
      * We must call SDL_CreateRenderer in order for draw calls to
      * affect this window.
      */
    strm->renderer = SDL_CreateRenderer(strm->window, -1, 0);
    if (!strm->renderer) {
        sdl_log_err("SDL_CreateRenderer()");
        return PJMEDIA_EVID_SYSERR;
    }

#if PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL
    if (strm->param.rend_id == OPENGL_DEV_IDX) {
        strm->gl_context = SDL_GL_CreateContext(strm->window);
        if (!strm->gl_context) {
            sdl_log_err("SDL_GL_CreateContext()");
            return PJMEDIA_EVID_SYSERR;
        }
        SDL_GL_MakeCurrent(strm->window, strm->gl_context);

        /* Init some OpenGL settings */
        glDisable(GL_DEPTH_TEST);
        glDisable(GL_CULL_FACE);
        glEnable(GL_TEXTURE_2D);
        
        /* Init the viewport */
        glViewport(0, 0, strm->param.disp_size.w, strm->param.disp_size.h);
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        
        glOrtho(0.0, (GLdouble)strm->param.disp_size.w,
                (GLdouble)strm->param.disp_size.h, 0.0, 0.0, 1.0);
        
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        
        /* Create a texture */
        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
        glGenTextures(1, &strm->texture);

        if (!strm->texture)
            return PJMEDIA_EVID_SYSERR;
    } else
#endif /* PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL */
    {    
        strm->scr_tex = SDL_CreateTexture(strm->renderer, sdl_format,
                                          SDL_TEXTUREACCESS_STREAMING,
                                          strm->rect.w, strm->rect.h);
        if (strm->scr_tex == NULL) {
            sdl_log_err("SDL_CreateTexture()");
            return PJMEDIA_EVID_SYSERR;
        }
    
        strm->pitch = strm->rect.w * SDL_BYTESPERPIXEL(sdl_format);
    }

    return PJ_SUCCESS;
}

static pj_status_t sdl_create_rend(struct sdl_stream * strm,
                                   pjmedia_format *fmt)
{
    sdl_fmt_info *sdl_info;
    const pjmedia_video_format_info *vfi;
    pjmedia_video_format_detail *vfd;

    sdl_info = get_sdl_format_info(fmt->id);
    vfi = pjmedia_get_video_format_info(pjmedia_video_format_mgr_instance(),
                                        fmt->id);
    if (!vfi || !sdl_info)
        return PJMEDIA_EVID_BADFORMAT;

    strm->vafp.size = fmt->det.vid.size;
    strm->vafp.buffer = NULL;
    if (vfi->apply_fmt(vfi, &strm->vafp) != PJ_SUCCESS)
        return PJMEDIA_EVID_BADFORMAT;

    vfd = pjmedia_format_get_video_format_detail(fmt, PJ_TRUE);
    strm->rect.x = strm->rect.y = 0;
    strm->rect.w = (Uint16)vfd->size.w;
    strm->rect.h = (Uint16)vfd->size.h;
    if (strm->param.disp_size.w == 0)
        strm->param.disp_size.w = strm->rect.w;
    if (strm->param.disp_size.h == 0)
        strm->param.disp_size.h = strm->rect.h;
    strm->dstrect.x = strm->dstrect.y = 0;
    strm->dstrect.w = (Uint16)strm->param.disp_size.w;
    strm->dstrect.h = (Uint16)strm->param.disp_size.h;

    sdl_destroy(strm);

#if PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL
    if (strm->param.rend_id == OPENGL_DEV_IDX) {
        SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1);
    }
#endif /* PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL */
    return sdl_create_window(strm, 
                         (strm->param.flags & PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW),
                         sdl_info->sdl_format,
                         &strm->param.window);
}

static pj_status_t sdl_create(void *data)
{
    struct sdl_stream *strm = (struct sdl_stream *)data;
    return sdl_create_rend(strm, &strm->param.fmt);
}

static pj_status_t resize_disp(struct sdl_stream *strm,
                               pjmedia_rect_size *new_disp_size)
{
    pj_memcpy(&strm->param.disp_size, new_disp_size,
              sizeof(strm->param.disp_size));
    
    if (strm->scr_tex) {
        strm->dstrect.x = strm->dstrect.y = 0;
        strm->dstrect.w = (Uint16)strm->param.disp_size.w;
        strm->dstrect.h = (Uint16)strm->param.disp_size.h;
        SDL_RenderSetViewport(strm->renderer, &strm->dstrect);
    }
#if PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL
    else if (strm->param.rend_id == OPENGL_DEV_IDX) {
        sdl_create_rend(strm, &strm->param.fmt);
    }
#endif /* PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL */

    return PJ_SUCCESS;
}

static pj_status_t change_format(struct sdl_stream *strm,
                                 pjmedia_format *new_fmt)
{
    pj_status_t status;

    /* Recreate SDL renderer */
    status = sdl_create_rend(strm, (new_fmt? new_fmt :
                                   &strm->param.fmt));
    if (status == PJ_SUCCESS && new_fmt)
        pjmedia_format_copy(&strm->param.fmt, new_fmt);

    return status;
}

static pj_status_t put_frame(void *data)
{
    struct sdl_stream *stream = (struct sdl_stream *)data;
    const pjmedia_frame *frame = stream->frame;

    if (stream->scr_tex) {
        SDL_UpdateTexture(stream->scr_tex, NULL, frame->buf, stream->pitch);
        SDL_RenderClear(stream->renderer);
        SDL_RenderCopy(stream->renderer, stream->scr_tex,
                       &stream->rect, &stream->dstrect);
        SDL_RenderPresent(stream->renderer);
    }
#if PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL
    else if (stream->param.rend_id == OPENGL_DEV_IDX && stream->texture) {
        glBindTexture(GL_TEXTURE_2D, stream->texture);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
                     stream->rect.w, stream->rect.h, 0,
                     GL_RGBA, GL_UNSIGNED_BYTE, frame->buf);
        glBegin(GL_TRIANGLE_STRIP);
        glTexCoord2f(0, 0); glVertex2i(0, 0);
        glTexCoord2f(1, 0); glVertex2i(stream->param.disp_size.w, 0);
        glTexCoord2f(0, 1); glVertex2i(0, stream->param.disp_size.h);
        glTexCoord2f(1, 1);
        glVertex2i(stream->param.disp_size.w, stream->param.disp_size.h);
        glEnd();
        SDL_GL_SwapWindow(stream->window);
    }
#endif /* PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL */

    return PJ_SUCCESS;
}

/* API: Put frame from stream */
static pj_status_t sdl_stream_put_frame(pjmedia_vid_dev_stream *strm,
                                        const pjmedia_frame *frame)
{
    struct sdl_stream *stream = (struct sdl_stream*)strm;
    pj_status_t status;

    stream->last_ts.u64 = frame->timestamp.u64;

    /* Video conference just trying to send heart beat for updating timestamp
     * or keep-alive, this port doesn't need any, just ignore.
     */
    if (frame->size==0 || frame->buf==NULL)
        return PJ_SUCCESS;

    if (frame->size < stream->vafp.framebytes)
        return PJ_ETOOSMALL;

    if (!stream->is_running)
        return PJ_EINVALIDOP;

    stream->frame = frame;
    job_queue_post_job(stream->sf->jq, put_frame, strm, 0, &status);
    
    return status;
}

/* API: create stream */
static pj_status_t sdl_factory_create_stream(
                                        pjmedia_vid_dev_factory *f,
                                        pjmedia_vid_dev_param *param,
                                        const pjmedia_vid_dev_cb *cb,
                                        void *user_data,
                                        pjmedia_vid_dev_stream **p_vid_strm)
{
    struct sdl_factory *sf = (struct sdl_factory*)f;
    pj_pool_t *pool;
    struct sdl_stream *strm;
    pj_status_t status;

    PJ_ASSERT_RETURN(param->dir == PJMEDIA_DIR_RENDER, PJ_EINVAL);

    /* Create and Initialize stream descriptor */
    pool = pj_pool_create(sf->pf, "sdl-dev", 1000, 1000, NULL);
    PJ_ASSERT_RETURN(pool != NULL, PJ_ENOMEM);

    strm = PJ_POOL_ZALLOC_T(pool, struct sdl_stream);
    pj_memcpy(&strm->param, param, sizeof(*param));
    strm->pool = pool;
    strm->sf = sf;
    pj_memcpy(&strm->vid_cb, cb, sizeof(*cb));
    pj_list_init(&strm->list_entry);
    strm->list_entry.stream = strm;
    strm->user_data = user_data;

    /* Create render stream here */
    job_queue_post_job(sf->jq, sdl_create, strm, 0, &status);
    if (status != PJ_SUCCESS) {
        goto on_error;
    }
    pj_mutex_lock(strm->sf->mutex);
    if (pj_list_empty(&strm->sf->streams))
        pj_sem_post(strm->sf->sem);
    pj_list_insert_after(&strm->sf->streams, &strm->list_entry);
    pj_mutex_unlock(strm->sf->mutex);

    /* Done */
    strm->base.op = &stream_op;
    *p_vid_strm = &strm->base;

    return PJ_SUCCESS;

on_error:
    sdl_stream_destroy(&strm->base);
    return status;
}

/* API: Get stream info. */
static pj_status_t sdl_stream_get_param(pjmedia_vid_dev_stream *s,
                                        pjmedia_vid_dev_param *pi)
{
    struct sdl_stream *strm = (struct sdl_stream*)s;

    PJ_ASSERT_RETURN(strm && pi, PJ_EINVAL);

    pj_memcpy(pi, &strm->param, sizeof(*pi));

    if (sdl_stream_get_cap(s, PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW,
                           &pi->window) == PJ_SUCCESS)
    {
        pi->flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW;
    }
    if (sdl_stream_get_cap(s, PJMEDIA_VID_DEV_CAP_OUTPUT_POSITION,
                           &pi->window_pos) == PJ_SUCCESS)
    {
        pi->flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_POSITION;
    }
    if (sdl_stream_get_cap(s, PJMEDIA_VID_DEV_CAP_OUTPUT_RESIZE,
                           &pi->disp_size) == PJ_SUCCESS)
    {
        pi->flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_RESIZE;
    }
    if (sdl_stream_get_cap(s, PJMEDIA_VID_DEV_CAP_OUTPUT_HIDE,
                           &pi->window_hide) == PJ_SUCCESS)
    {
        pi->flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_HIDE;
    }
    if (sdl_stream_get_cap(s, PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW_FLAGS,
                           &pi->window_flags) == PJ_SUCCESS)
    {
        pi->flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW_FLAGS;
    }
    if (sdl_stream_get_cap(s, PJMEDIA_VID_DEV_CAP_OUTPUT_FULLSCREEN,
                           &pi->window_fullscreen) == PJ_SUCCESS)
    {
        pi->flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_FULLSCREEN;
    }

    return PJ_SUCCESS;
}

struct strm_cap {
    struct sdl_stream   *strm;
    pjmedia_vid_dev_cap  cap;
    union {
        void            *pval;
        const void      *cpval;
    } pval;
};

static pj_status_t get_cap(void *data)
{
    struct strm_cap *scap = (struct strm_cap *)data;
    struct sdl_stream *strm = scap->strm;
    pjmedia_vid_dev_cap cap = scap->cap;
    void *pval = scap->pval.pval;

    if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW)
    {
        SDL_SysWMinfo info;
        SDL_VERSION(&info.version);

        if (SDL_GetWindowWMInfo(strm->window, &info)) {
            pjmedia_vid_dev_hwnd *wnd = (pjmedia_vid_dev_hwnd *)pval;
            if (0) { }
#if defined(SDL_VIDEO_DRIVER_WINDOWS)
            else if (info.subsystem == SDL_SYSWM_WINDOWS) {
                wnd->type = PJMEDIA_VID_DEV_HWND_TYPE_WINDOWS;
                wnd->info.win.hwnd = (void *)info.info.win.window;
            }
#endif
#if defined(SDL_VIDEO_DRIVER_X11)
            else if (info.subsystem == SDL_SYSWM_X11) {
                wnd->info.x11.window = (void *)info.info.x11.window;
                wnd->info.x11.display = (void *)info.info.x11.display;
            }
#endif
#if defined(SDL_VIDEO_DRIVER_COCOA)
            else if (info.subsystem == SDL_SYSWM_COCOA) {
                wnd->info.cocoa.window = (void *)info.info.cocoa.window;
            }
#endif
#if defined(SDL_VIDEO_DRIVER_UIKIT)
            else if (info.subsystem == SDL_SYSWM_UIKIT) {
                wnd->info.ios.window = (void *)info.info.uikit.window;
            }
#endif
            else {
                return PJMEDIA_EVID_INVCAP;
            }
            return PJ_SUCCESS;
        } else
            return PJMEDIA_EVID_INVCAP;
    } else if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_POSITION) {
        SDL_GetWindowPosition(strm->window, &((pjmedia_coord *)pval)->x,
                              &((pjmedia_coord *)pval)->y);
        return PJ_SUCCESS;
    } else if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_RESIZE) {
        SDL_GetWindowSize(strm->window, (int *)&((pjmedia_rect_size *)pval)->w,
                          (int *)&((pjmedia_rect_size *)pval)->h);
        return PJ_SUCCESS;
    } else if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_HIDE) {
        Uint32 flag = SDL_GetWindowFlags(strm->window);
        *((pj_bool_t *)pval) = (flag & SDL_WINDOW_HIDDEN)? PJ_TRUE: PJ_FALSE;
        return PJ_SUCCESS;
    } else if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW_FLAGS) {
        Uint32 flag = SDL_GetWindowFlags(strm->window);
        unsigned *wnd_flags = (unsigned *)pval;
        if (!(flag & SDL_WINDOW_BORDERLESS))
            *wnd_flags |= PJMEDIA_VID_DEV_WND_BORDER;
        if (flag & SDL_WINDOW_RESIZABLE)
            *wnd_flags |= PJMEDIA_VID_DEV_WND_RESIZABLE;
        return PJ_SUCCESS;
    } else if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_FULLSCREEN) {
        Uint32 flag = SDL_GetWindowFlags(strm->window);
        pjmedia_vid_dev_fullscreen_flag val = PJMEDIA_VID_DEV_WINDOWED;
        if ((flag & SDL_WINDOW_FULLSCREEN_DESKTOP) ==
                    SDL_WINDOW_FULLSCREEN_DESKTOP)
        {
             val = PJMEDIA_VID_DEV_FULLSCREEN_DESKTOP;
        } else if ((flag & SDL_WINDOW_FULLSCREEN) == SDL_WINDOW_FULLSCREEN) {
             val = PJMEDIA_VID_DEV_FULLSCREEN;
        }
        *((pjmedia_vid_dev_fullscreen_flag*)pval) = val;
        return PJ_SUCCESS;
    }

    return PJMEDIA_EVID_INVCAP;
}

/* API: get capability */
static pj_status_t sdl_stream_get_cap(pjmedia_vid_dev_stream *s,
                                      pjmedia_vid_dev_cap cap,
                                      void *pval)
{
    struct sdl_stream *strm = (struct sdl_stream*)s;
    struct strm_cap scap;
    pj_status_t status;

    PJ_ASSERT_RETURN(s && pval, PJ_EINVAL);

    scap.strm = strm;
    scap.cap = cap;
    scap.pval.pval = pval;

    job_queue_post_job(strm->sf->jq, get_cap, &scap, 0, &status);

    return status;
}

static pj_status_t set_cap(void *data)
{
    struct strm_cap *scap = (struct strm_cap *)data;
    struct sdl_stream *strm = scap->strm;
    pjmedia_vid_dev_cap cap = scap->cap;
    const void *pval = scap->pval.cpval;

    if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_POSITION) {
        /**
         * Setting window's position when the window is hidden also sets
         * the window's flag to shown (while the window is, actually,
         * still hidden). This causes problems later when setting/querying
         * the window's visibility.
         * See ticket #1429 (https://github.com/pjsip/pjproject/issues/1429)
         */
        Uint32 flag = SDL_GetWindowFlags(strm->window);
        if (flag & SDL_WINDOW_HIDDEN)
            SDL_ShowWindow(strm->window);
        SDL_SetWindowPosition(strm->window, ((pjmedia_coord *)pval)->x,
                              ((pjmedia_coord *)pval)->y);
        if (flag & SDL_WINDOW_HIDDEN)
            SDL_HideWindow(strm->window);
        return PJ_SUCCESS;
    } else if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_HIDE) {
        if (*(pj_bool_t *)pval)
            SDL_HideWindow(strm->window);
        else
            SDL_ShowWindow(strm->window);
        return PJ_SUCCESS;
    } else if (cap == PJMEDIA_VID_DEV_CAP_FORMAT) {
        pj_status_t status;

        status = change_format(strm, (pjmedia_format *)pval);
        if (status != PJ_SUCCESS) {
            pj_status_t status_;
            
            /**
             * Failed to change the output format. Try to revert
             * to its original format.
             */
            status_ = change_format(strm, &strm->param.fmt);
            if (status_ != PJ_SUCCESS) {
                /**
                 * This means that we failed to revert to our
                 * original state!
                 */
                status = PJMEDIA_EVID_ERR;
            }
        }
        
        return status;
    } else if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_RESIZE) {
        pjmedia_rect_size *new_size = (pjmedia_rect_size *)pval;
        Uint32 flag = SDL_GetWindowFlags(strm->window);
        pj_status_t status;

        /**
         * Exit full-screen if engaged, since resizing while in full-screen is
         * not supported.
         */
        if (flag & SDL_WINDOW_FULLSCREEN_DESKTOP)
            SDL_SetWindowFullscreen(strm->window, 0);

        SDL_SetWindowSize(strm->window, new_size->w, new_size->h);
        status = resize_disp(strm, new_size);

        /* Restore full-screen if it was engaged. */
        if (flag & SDL_WINDOW_FULLSCREEN_DESKTOP)
            SDL_SetWindowFullscreen(strm->window, SDL_WINDOW_FULLSCREEN_DESKTOP);

        return status;
    } else if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW) {
        pjmedia_vid_dev_hwnd *hwnd = (pjmedia_vid_dev_hwnd*)pval;
        pj_status_t status = PJ_SUCCESS;
        sdl_fmt_info *sdl_info = get_sdl_format_info(strm->param.fmt.id);
        /* Re-init SDL */
        status = sdl_destroy_all(strm);
        if (status != PJ_SUCCESS)
            return status;      

        status = sdl_create_window(strm, PJ_TRUE, sdl_info->sdl_format, hwnd);
        PJ_PERROR(4, (THIS_FILE, status,
                      "Re-initializing SDL with native window %d",
                      hwnd->info.window));
        return status;  
    } else if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_FULLSCREEN) {
        Uint32 flag;
        pjmedia_vid_dev_fullscreen_flag val =
                                    *(pjmedia_vid_dev_fullscreen_flag*)pval;

        flag = SDL_GetWindowFlags(strm->window);
        if (val == PJMEDIA_VID_DEV_FULLSCREEN_DESKTOP)
            flag |= SDL_WINDOW_FULLSCREEN_DESKTOP;
        else if (val == PJMEDIA_VID_DEV_FULLSCREEN)
            flag |= SDL_WINDOW_FULLSCREEN;
        else
            flag &= (~SDL_WINDOW_FULLSCREEN_DESKTOP);

        SDL_SetWindowFullscreen(strm->window, flag);

        /* Trying to restore the border after returning from fullscreen,
         * unfortunately not sure how to put back the resizable flag.
         */
        if ((flag & SDL_WINDOW_FULLSCREEN)==0 &&
            (flag & SDL_WINDOW_BORDERLESS)==0)
        {
            SDL_SetWindowBordered(strm->window, SDL_FALSE);
            SDL_SetWindowBordered(strm->window, SDL_TRUE);
        }

        return PJ_SUCCESS;
    }

    return PJMEDIA_EVID_INVCAP;
}

/* API: set capability */
static pj_status_t sdl_stream_set_cap(pjmedia_vid_dev_stream *s,
                                      pjmedia_vid_dev_cap cap,
                                      const void *pval)
{
    struct sdl_stream *strm = (struct sdl_stream*)s;
    struct strm_cap scap;
    pj_status_t status;

    PJ_ASSERT_RETURN(s && pval, PJ_EINVAL);

    scap.strm = strm;
    scap.cap = cap;
    scap.pval.cpval = pval;

    job_queue_post_job(strm->sf->jq, set_cap, &scap, 0, &status);

    return status;
}

/* API: Start stream. */
static pj_status_t sdl_stream_start(pjmedia_vid_dev_stream *strm)
{
    struct sdl_stream *stream = (struct sdl_stream*)strm;

    PJ_LOG(4, (THIS_FILE, "Starting sdl video stream"));

    stream->is_running = PJ_TRUE;

    return PJ_SUCCESS;
}


/* API: Stop stream. */
static pj_status_t sdl_stream_stop(pjmedia_vid_dev_stream *strm)
{
    struct sdl_stream *stream = (struct sdl_stream*)strm;

    PJ_LOG(4, (THIS_FILE, "Stopping sdl video stream"));

    stream->is_running = PJ_FALSE;

    return PJ_SUCCESS;
}


/* API: Destroy stream. */
static pj_status_t sdl_stream_destroy(pjmedia_vid_dev_stream *strm)
{
    struct sdl_stream *stream = (struct sdl_stream*)strm;
    pj_status_t status;

    PJ_ASSERT_RETURN(stream != NULL, PJ_EINVAL);

    sdl_stream_stop(strm);

    job_queue_post_job(stream->sf->jq, sdl_destroy_all, strm, 0, &status);
    if (status != PJ_SUCCESS)
        return status;

    pj_mutex_lock(stream->sf->mutex);
    if (!pj_list_empty(&stream->list_entry))
        pj_list_erase(&stream->list_entry);
    pj_mutex_unlock(stream->sf->mutex);

    pj_pool_release(stream->pool);

    return PJ_SUCCESS;
}

/****************************************************************************
 * Job queue implementation
 */
#if PJ_DARWINOS==0
static int job_thread(void * data)
{
    job_queue *jq = (job_queue *)data;

    while (1) {
        job *jb;

        /* Wait until there is a job. */
        pj_sem_wait(jq->sem);

        /* Make sure there is no pending jobs before we quit. */
        if (jq->is_quitting && jq->head == jq->tail && !jq->is_full)
            break;

        jb = jq->jobs[jq->head];
        jb->retval = (*jb->func)(jb->data);
        /* If job queue is full and we already finish all the pending
         * jobs, increase the size.
         */
        if (jq->is_full && ((jq->head + 1) % jq->size == jq->tail)) {
            unsigned i, head;
            pj_status_t status;

            if (jq->old_sem) {
                for (i = 0; i < jq->size / JOB_QUEUE_INC_FACTOR; i++) {
                    pj_sem_destroy(jq->old_sem[i]);
                }
            }
            jq->old_sem = jq->job_sem;

            /* Double the job queue size. */
            jq->size *= JOB_QUEUE_INC_FACTOR;
            pj_sem_destroy(jq->sem);
            status = pj_sem_create(jq->pool, "thread_sem", 0, jq->size + 1,
                                   &jq->sem);
            if (status != PJ_SUCCESS) {
                PJ_PERROR(3, (THIS_FILE, status,
                              "Failed growing SDL job queue size."));
                return 0;
            }
            jq->jobs = (job **)pj_pool_calloc(jq->pool, jq->size,
                                              sizeof(job *));
            jq->job_sem = (pj_sem_t **) pj_pool_calloc(jq->pool, jq->size,
                                                       sizeof(pj_sem_t *));
            for (i = 0; i < jq->size; i++) {
                status = pj_sem_create(jq->pool, "job_sem", 0, 1,
                                       &jq->job_sem[i]);
                if (status != PJ_SUCCESS) {
                    PJ_PERROR(3, (THIS_FILE, status,
                                  "Failed growing SDL job queue size."));
                    return 0;
                }
            }
            jq->is_full = PJ_FALSE;
            head = jq->head;
            jq->head = jq->tail = 0;
            pj_sem_post(jq->old_sem[head]);
        } else {
            pj_sem_post(jq->job_sem[jq->head]);
            jq->head = (jq->head + 1) % jq->size;
        }
    }

    return 0;
}
#endif

static pj_status_t job_queue_create(pj_pool_t *pool, job_queue **pjq)
{
    unsigned i;
    pj_status_t status;

    job_queue *jq = PJ_POOL_ZALLOC_T(pool, job_queue);
    jq->pool = pool;
    jq->size = INITIAL_MAX_JOBS;
    status = pj_sem_create(pool, "thread_sem", 0, jq->size + 1, &jq->sem);
    if (status != PJ_SUCCESS)
        goto on_error;
    jq->jobs = (job **)pj_pool_calloc(pool, jq->size, sizeof(job *));
    jq->job_sem = (pj_sem_t **) pj_pool_calloc(pool, jq->size,
                                               sizeof(pj_sem_t *));
    for (i = 0; i < jq->size; i++) {
        status = pj_sem_create(pool, "job_sem", 0, 1, &jq->job_sem[i]);
        if (status != PJ_SUCCESS)
            goto on_error;
    }

    status = pj_mutex_create_recursive(pool, "job_mutex", &jq->mutex);
    if (status != PJ_SUCCESS)
        goto on_error;

#if defined(PJ_DARWINOS) && PJ_DARWINOS!=0
    PJ_UNUSED_ARG(status);
#else
    status = pj_thread_create(pool, "job_th", job_thread, jq, 0, 0,
                              &jq->thread);
    if (status != PJ_SUCCESS)
        goto on_error;
#endif /* PJ_DARWINOS */

    *pjq = jq;
    return PJ_SUCCESS;

on_error:
    job_queue_destroy(jq);
    return status;
}

static pj_status_t job_queue_post_job(job_queue *jq, job_func_ptr func,
                                      void *data, unsigned flags,
                                      pj_status_t *retval)
{
    job jb;
    int tail;

    if (jq->is_quitting)
        return PJ_EBUSY;

    jb.func = func;
    jb.data = data;
    jb.flags = flags;

#if defined(PJ_DARWINOS) && PJ_DARWINOS!=0
    PJ_UNUSED_ARG(tail);
    NSAutoreleasePool *apool = [[NSAutoreleasePool alloc]init];
    JQDelegate *jqd = [[JQDelegate alloc]init];
    jqd->pjob = &jb;
    [jqd performSelectorOnMainThread:@selector(run_job)
         withObject:nil waitUntilDone:YES];
    [jqd release];
    [apool release];
#else /* PJ_DARWINOS */
    pj_mutex_lock(jq->mutex);
    jq->jobs[jq->tail] = &jb;
    tail = jq->tail;
    jq->tail = (jq->tail + 1) % jq->size;
    if (jq->tail == jq->head) {
        jq->is_full = PJ_TRUE;
        PJ_LOG(4, (THIS_FILE, "SDL job queue is full, increasing "
                              "the queue size."));
        pj_sem_post(jq->sem);
        /* Wait until our posted job is completed. */
        pj_sem_wait(jq->job_sem[tail]);
        pj_mutex_unlock(jq->mutex);
    } else {
        pj_mutex_unlock(jq->mutex);
        pj_sem_post(jq->sem);
        /* Wait until our posted job is completed. */
        pj_sem_wait(jq->job_sem[tail]);
    }
#endif /* PJ_DARWINOS */

    *retval = jb.retval;

    return PJ_SUCCESS;
}

static pj_status_t job_queue_destroy(job_queue *jq)
{
    unsigned i;

    jq->is_quitting = PJ_TRUE;

    if (jq->thread) {
        pj_sem_post(jq->sem);
        pj_thread_join(jq->thread);
        pj_thread_destroy(jq->thread);
    }

    if (jq->sem) {
        pj_sem_destroy(jq->sem);
        jq->sem = NULL;
    }
    for (i = 0; i < jq->size; i++) {
        if (jq->job_sem[i]) {
            pj_sem_destroy(jq->job_sem[i]);
            jq->job_sem[i] = NULL;
        }
    }
    if (jq->old_sem) {
        for (i = 0; i < jq->size / JOB_QUEUE_INC_FACTOR; i++) {
            if (jq->old_sem[i]) {
                pj_sem_destroy(jq->old_sem[i]);
                jq->old_sem[i] = NULL;
            }
        }
    }
    if (jq->mutex) {
        pj_mutex_destroy(jq->mutex);
        jq->mutex = NULL;
    }

    return PJ_SUCCESS;
}

#ifdef _MSC_VER
#   if defined(PJMEDIA_SDL_LIB)
#       pragma comment( lib, PJMEDIA_SDL_LIB)
#   elif SDL_VERSION_ATLEAST(2,0,0)
#       pragma comment( lib, "sdl2.lib")
#   elif SDL_VERSION_ATLEAST(1,3,0)
#       pragma comment( lib, "sdl.lib")
#   endif
#   if PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL
#       pragma comment(lib, "OpenGL32.lib")
#   endif /* PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL */
#endif /* _MSC_VER */


#endif  /* PJMEDIA_VIDEO_DEV_HAS_SDL */
