/* 
 * Copyright (C) 2010-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.h>
#include <pjmedia/converter.h>
#include <pjmedia-codec.h>
#include <pjlib-util.h>
#include <pjlib.h>
#include <stdio.h>
#include <stdlib.h>

#ifndef _MSC_VER
#include <signal.h>
#endif

/**
 * \page page_pjmedia_samples_aviplay_c Samples: Playing AVI File to
 * Video and Sound Devices
 *
 * This is a very simple example to use the @ref PJMEDIA_FILE_PLAY,
 * @ref PJMED_SND_PORT, and @ref PJMEDIA_VIDEO_PORT. In this example, we
 * open the file, video, and sound devices, then connect the file to both
 * video and sound devices to play the contents of the file.
 *
 *
 * This file is pjsip-apps/src/samples/aviplay.c
 *
 * \includelineno aviplay.c
 */


/*
 * aviplay.c
 *
 * PURPOSE:
 *  Play a AVI file to video and sound devices.
 *
 * USAGE:
 *  aviplay FILE.AVI
 */


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

#include "util.h"

/* For logging purpose. */
#define THIS_FILE   "aviplay.c"

static const char *desc = 
" FILE                                                              \n"
"                                                                   \n"
"  aviplay.c                                                        \n"
"                                                                   \n"
" PURPOSE                                                           \n"
"                                                                   \n"
"  Demonstrate how to play a AVI file.                              \n"
"                                                                   \n"
" USAGE                                                             \n"
"                                                                   \n"
"  aviplay FILE.AVI                                                 \n";

struct codec_fmt {
    pj_uint32_t         pjmedia_id;
    const char         *codec_id;
    /* Do we need to convert the decoded frame? */
    pj_bool_t           need_conversion;
    /* If conversion is needed, dst_fmt indicates the destination format */
    pjmedia_format_id   dst_fmt;
} codec_fmts[] = {{PJMEDIA_FORMAT_MJPEG, "mjpeg",
                   PJ_TRUE , PJMEDIA_FORMAT_I420},
                  {PJMEDIA_FORMAT_H263 , "h263" ,
                   PJ_FALSE, 0},
                  {PJMEDIA_FORMAT_MPEG4, "mp4v"}, 
                  {PJMEDIA_FORMAT_H264 , "h264"}
                 };

typedef struct avi_port_t
{
    pjmedia_vid_port   *vid_port;
    pjmedia_snd_port   *snd_port;
    pj_bool_t           is_running;
    pj_bool_t           is_quitting;
} avi_port_t;

typedef struct codec_port_data_t
{
    pjmedia_vid_codec   *codec;
    pjmedia_port        *src_port;
    pj_uint8_t          *enc_buf;
    pj_size_t            enc_buf_size;
    
    pjmedia_converter   *conv;
} codec_port_data_t;

static pj_status_t avi_event_cb(pjmedia_event *event,
                                void *user_data)
{
    avi_port_t *ap = (avi_port_t *)user_data;
    
    switch (event->type) {
    case PJMEDIA_EVENT_WND_CLOSED:
        ap->is_quitting = PJ_TRUE;
        break;
    case PJMEDIA_EVENT_MOUSE_BTN_DOWN:
        if (ap->is_running) {
            pjmedia_vid_port_stop(ap->vid_port);
            if (ap->snd_port)
                pjmedia_aud_stream_stop(
                    pjmedia_snd_port_get_snd_stream(ap->snd_port));
        } else {
            pjmedia_vid_port_start(ap->vid_port);
            if (ap->snd_port)
                pjmedia_aud_stream_start(
                    pjmedia_snd_port_get_snd_stream(ap->snd_port));
        }
        ap->is_running = !ap->is_running;
        break;
    default:
        return PJ_SUCCESS;
    }
    
    /* We handled the event on our own, so return non-PJ_SUCCESS here */
    return -1;
}

static pj_status_t codec_get_frame(pjmedia_port *port,
                                   pjmedia_frame *frame)
{
    codec_port_data_t *port_data = (codec_port_data_t*)port->port_data.pdata;
    pjmedia_vid_codec *codec = port_data->codec;
    pjmedia_frame enc_frame;
    pj_status_t status;
    
    enc_frame.buf = port_data->enc_buf;
    enc_frame.size = port_data->enc_buf_size;
    
    if (port_data->conv) {
        pj_size_t frame_size = frame->size;
        
        status = pjmedia_port_get_frame(port_data->src_port, frame);
        if (status != PJ_SUCCESS) goto on_error;
        
        status = pjmedia_vid_codec_decode(codec, 1, frame,
                                          (unsigned)frame->size, &enc_frame);
        if (status != PJ_SUCCESS) goto on_error;
        
        frame->size = frame_size;
        status = pjmedia_converter_convert(port_data->conv, &enc_frame, frame);
        if (status != PJ_SUCCESS) goto on_error;
        
        return PJ_SUCCESS;
    }
    
    status = pjmedia_port_get_frame(port_data->src_port, &enc_frame);
    if (status != PJ_SUCCESS) goto on_error;
    
    status = pjmedia_vid_codec_decode(codec, 1, &enc_frame,
                                      (unsigned)frame->size, frame);
    if (status != PJ_SUCCESS) goto on_error;
    
    return PJ_SUCCESS;
    
on_error:
    pj_perror(3, THIS_FILE, status, "codec_get_frame() error");
    return status;
}

static int aviplay(pj_pool_t *pool, const char *fname)
{
    pjmedia_vid_port *renderer=NULL;
    pjmedia_vid_port_param param;
    const pjmedia_video_format_info *vfi;
    pjmedia_video_format_detail *vfd;
    pjmedia_snd_port *snd_port = NULL;
    pj_status_t status;
    int rc = 0;
    pjmedia_avi_streams *avi_streams;
    pjmedia_avi_stream *vid_stream, *aud_stream;
    pjmedia_port *vid_port = NULL, *aud_port = NULL;
    pjmedia_vid_codec *codec=NULL;
    codec_port_data_t codec_port_data;
    avi_port_t avi_port;
    
    pj_bzero(&avi_port, sizeof(avi_port));
    
    CHECK( pjmedia_avi_player_create_streams(pool, fname, 0, &avi_streams) );
    
    vid_stream = pjmedia_avi_streams_get_stream_by_media(avi_streams,
                                                         0,
                                                         PJMEDIA_TYPE_VIDEO);
    vid_port = pjmedia_avi_stream_get_port(vid_stream);
    
    if (vid_port) {
        pjmedia_vid_port_param_default(&param);
        
        CHECK(   pjmedia_vid_dev_default_param(pool,
                                               PJMEDIA_VID_DEFAULT_RENDER_DEV,
                                               &param.vidparam) );
        
        /* Create renderer, set it to active  */
        param.active = PJ_TRUE;
        param.vidparam.dir = PJMEDIA_DIR_RENDER;
        param.vidparam.flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW_FLAGS;
        param.vidparam.window_flags = PJMEDIA_VID_DEV_WND_BORDER | PJMEDIA_VID_DEV_WND_RESIZABLE;
        vfd = pjmedia_format_get_video_format_detail(&vid_port->info.fmt,
                                                     PJ_TRUE);
        pjmedia_format_init_video(&param.vidparam.fmt, 
                                  vid_port->info.fmt.id,
                                  vfd->size.w, vfd->size.h,
                                  vfd->fps.num, vfd->fps.denum);
        
        vfi = pjmedia_get_video_format_info(
                  pjmedia_video_format_mgr_instance(),
                  vid_port->info.fmt.id);
        /* Check whether the frame is encoded */
        if (!vfi || vfi->bpp == 0) {
            /* Yes, prepare codec */
            pj_str_t codec_id_st;
            unsigned info_cnt = 1, i, k;
            const pjmedia_vid_codec_info *codec_info;
            pj_str_t port_name = {"codec", 5};
            pj_uint8_t *enc_buf = NULL;
            pj_size_t enc_buf_size = 0;
            pjmedia_vid_dev_info rdr_info;
            pjmedia_port codec_port;
            pjmedia_vid_codec_param codec_param;
            struct codec_fmt *codecp = NULL;

            /* Lookup codec */
            for (i = 0; i < PJ_ARRAY_SIZE(codec_fmts); i++) {
                if (vid_port->info.fmt.id == codec_fmts[i].pjmedia_id) {
                    codecp = &codec_fmts[i];
                    break;
                }
            }
            if (!codecp) {
                rc = 242; goto on_return;
            }
            pj_cstr(&codec_id_st, codecp->codec_id);
            CHECK(   pjmedia_vid_codec_mgr_find_codecs_by_id(NULL,
                                                             &codec_id_st, 
                                                             &info_cnt, 
                                                             &codec_info,
                                                             NULL) );

            CHECK( pjmedia_vid_codec_mgr_get_default_param(NULL, codec_info,
                                                           &codec_param) );
            
            pjmedia_format_copy(&codec_param.enc_fmt, &param.vidparam.fmt);

            pjmedia_vid_dev_get_info(param.vidparam.rend_id, &rdr_info);
            for (i=0; i<codec_info->dec_fmt_id_cnt; ++i) {
                for (k=0; k<rdr_info.fmt_cnt; ++k) {
                    if ((int)codec_info->dec_fmt_id[i]==(int)rdr_info.fmt[k].id)
                    {
                        param.vidparam.fmt.id = codec_info->dec_fmt_id[i];
                        i = codec_info->dec_fmt_id_cnt;
                        break;
                    }
                }
            }
            
            /* Open codec */
            CHECK( pjmedia_vid_codec_mgr_alloc_codec(NULL, codec_info,
                                                     &codec));
            
            CHECK( pjmedia_vid_codec_init(codec, pool) );
            
            pjmedia_format_copy(&codec_param.dec_fmt, &param.vidparam.fmt);
            codec_param.dir = PJMEDIA_DIR_DECODING;
            codec_param.packing = PJMEDIA_VID_PACKING_WHOLE;
            CHECK( pjmedia_vid_codec_open(codec, &codec_param) );
            
            /* Alloc encoding buffer */
            enc_buf_size =  codec_param.dec_fmt.det.vid.size.w *
            codec_param.dec_fmt.det.vid.size.h * 4;
            enc_buf = pj_pool_alloc(pool, enc_buf_size +
                                    128 /*< padding, required for vid codecs
                                            such as ffmpeg. Must be >=
                                            AV_INPUT_BUFFER_PADDING_SIZE.
                                            And must not be included in
                                            the enc_buf_size calculation
                                            above. */);
            
            /* Init codec port */
            pj_bzero(&codec_port, sizeof(codec_port));
            CHECK( pjmedia_port_info_init2(&codec_port.info, &port_name,
                                             0x1234,
                                             PJMEDIA_DIR_ENCODING, 
                                             &codec_param.dec_fmt) );

            pj_bzero(&codec_port_data, sizeof(codec_port_data));
            codec_port_data.codec = codec;
            codec_port_data.src_port = vid_port;
            codec_port_data.enc_buf = enc_buf;
            codec_port_data.enc_buf_size = enc_buf_size;
            
            codec_port.get_frame = &codec_get_frame;
            codec_port.port_data.pdata = &codec_port_data;
            
            /* Check whether we need to convert the decoded frame */
            if (codecp->need_conversion) {
                pjmedia_conversion_param conv_param;
                
                pjmedia_format_copy(&conv_param.src, &param.vidparam.fmt);
                pjmedia_format_copy(&conv_param.dst, &param.vidparam.fmt);
                conv_param.dst.id = codecp->dst_fmt;
                param.vidparam.fmt.id = conv_param.dst.id;
                
                CHECK( pjmedia_converter_create(NULL, pool, &conv_param,
                                                &codec_port_data.conv));
            }
            
            CHECK( pjmedia_vid_port_create(pool, &param, &renderer) );
            
            CHECK( pjmedia_vid_port_connect(renderer, &codec_port,
                                            PJ_FALSE) );
        } else {
            CHECK( pjmedia_vid_port_create(pool, &param, &renderer) );
            
            /* Connect avi port to renderer */
            CHECK( pjmedia_vid_port_connect(renderer, vid_port,
                                            PJ_FALSE) );
        }
        
    }
    
    aud_stream = pjmedia_avi_streams_get_stream_by_media(avi_streams,
                                                         0,
                                                         PJMEDIA_TYPE_AUDIO);
    aud_port = pjmedia_avi_stream_get_port(aud_stream);
    
    if (aud_port) {
        /* Create sound player port. */
        CHECK( pjmedia_snd_port_create_player(
                 pool,                              /* pool                 */
                 -1,                                /* use default dev.     */
                 PJMEDIA_PIA_SRATE(&aud_port->info),/* clock rate.          */
                 PJMEDIA_PIA_CCNT(&aud_port->info), /* # of channels.       */
                 PJMEDIA_PIA_SPF(&aud_port->info),  /* samples per frame.   */
                 PJMEDIA_PIA_BITS(&aud_port->info), /* bits per sample.     */
                 0,                                 /* options              */
                 &snd_port                          /* returned port        */
                 ));
        
        /* Connect file port to the sound player.
         * Stream playing will commence immediately.
         */
        CHECK( pjmedia_snd_port_connect(snd_port, aud_port) );
    }
    
    if (vid_port) {
        pjmedia_vid_dev_cb cb;
        
        pj_bzero(&cb, sizeof(cb));
        avi_port.snd_port = snd_port;
        avi_port.vid_port = renderer;
        avi_port.is_running = PJ_TRUE;
        pjmedia_vid_port_set_cb(renderer, &cb, &avi_port);

        /* subscribe events */
        pjmedia_event_subscribe(NULL, &avi_event_cb, &avi_port,
                                renderer);

        if (snd_port) {
            /* Synchronize video rendering and audio playback */
            pjmedia_vid_port_set_clock_src(
                renderer,
                pjmedia_snd_port_get_clock_src(
                    snd_port, PJMEDIA_DIR_PLAYBACK));
        }
                                              
        
        /* Start video streaming.. */
        CHECK( pjmedia_vid_port_start(renderer) );
    }
    
    while (!avi_port.is_quitting) {
        pj_thread_sleep(100);
    }

on_error:
on_return:
    if (snd_port) {
        pjmedia_snd_port_disconnect(snd_port);
        /* Without this sleep, Windows/DirectSound will repeteadly
         * play the last frame during destroy.
         */
        pj_thread_sleep(100);
        pjmedia_snd_port_destroy(snd_port);
    }
    if (renderer) {
        pjmedia_event_unsubscribe(NULL, &avi_event_cb, &avi_port,
                                  renderer);
        pjmedia_vid_port_destroy(renderer);
    }
    if (aud_port)
        pjmedia_port_destroy(aud_port);
    if (vid_port)
        pjmedia_port_destroy(vid_port);
    if (codec) {
        pjmedia_vid_codec_close(codec);
        pjmedia_vid_codec_mgr_dealloc_codec(NULL, codec);
    }
    
    return rc;
}

#ifndef _MSC_VER
static void sig_handler(int sig)
{
    switch (sig)
    {
    case SIGINT:
    case SIGTERM:
        break;
    }
    puts("exit..");
    exit(1);
}
#endif

static int main_func(int argc, char *argv[])
{
    pj_caching_pool cp;
    pj_pool_t *pool;
    int rc = 0;
    pj_status_t status = PJ_SUCCESS;
    
    if (argc != 2) {
        puts("Error: filename required");
        puts(desc);
        return 110;
    }

#ifndef _MSC_VER
    signal(SIGINT, sig_handler);
    signal(SIGTERM, sig_handler);
#endif

    /* Must init PJLIB first: */
    status = pj_init();
    PJ_ASSERT_RETURN(status == PJ_SUCCESS, 120);

    /* Must create a pool factory before we can allocate any memory. */
    pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0);

    /* Create memory pool for our file player */
    pool = pj_pool_create( &cp.factory,     /* pool factory         */
                           "AVI",           /* pool name.           */
                           4000,            /* init size            */
                           4000,            /* increment size       */
                           NULL             /* callback on error    */
                           );

    pjmedia_video_format_mgr_create(pool, 64, 0, NULL);
    pjmedia_converter_mgr_create(pool, NULL);
    pjmedia_event_mgr_create(pool, 0, NULL);
    pjmedia_vid_codec_mgr_create(pool, NULL);
    
    status = pjmedia_vid_dev_subsys_init(&cp.factory);
    if (status != PJ_SUCCESS) {
        app_perror(THIS_FILE, "Video init failed", status);
        rc = 130;
        goto on_return;
    }
    
    status = pjmedia_aud_subsys_init(&cp.factory);
    if (status != PJ_SUCCESS) {
        rc = 140;
        goto on_return;
    }
    
#if defined(PJMEDIA_HAS_OPENH264_CODEC) && PJMEDIA_HAS_OPENH264_CODEC != 0
    status = pjmedia_codec_openh264_vid_init(NULL, &cp.factory);
    if (status != PJ_SUCCESS) {
        rc = 150;
        goto on_return;
    }
#endif

#if PJMEDIA_HAS_FFMPEG_VID_CODEC
    status = pjmedia_codec_ffmpeg_vid_init(NULL, &cp.factory);
    if (status != PJ_SUCCESS) {
        rc = 160;
        goto on_return;    
    }
#endif

    rc = aviplay(pool, argv[1]);
    
    /* 
     * File should be playing and looping now 
     */

    /* Without this sleep, Windows/DirectSound will repeteadly
     * play the last frame during destroy.
     */
    pj_thread_sleep(100);

on_return:    
#if PJMEDIA_HAS_FFMPEG_VID_CODEC
    pjmedia_codec_ffmpeg_vid_deinit();
#endif
#if defined(PJMEDIA_HAS_OPENH264_CODEC) && PJMEDIA_HAS_OPENH264_CODEC != 0
    pjmedia_codec_openh264_vid_deinit();
#endif
    pjmedia_aud_subsys_shutdown();
    pjmedia_vid_dev_subsys_shutdown();
    
    pjmedia_video_format_mgr_destroy(pjmedia_video_format_mgr_instance());
    pjmedia_converter_mgr_destroy(pjmedia_converter_mgr_instance());
    pjmedia_event_mgr_destroy(pjmedia_event_mgr_instance());
    pjmedia_vid_codec_mgr_destroy(pjmedia_vid_codec_mgr_instance());    
    
    /* Release application pool */
    pj_pool_release( pool );

    /* Destroy pool factory */
    pj_caching_pool_destroy( &cp );

    /* Shutdown PJLIB */
    pj_shutdown();

    /* Done. */
    return rc;
}

int main(int argc, char *argv[])
{
    return pj_run_app(&main_func, argc, argv, 0);
}

#else

int main(int argc, char *argv[])
{
    PJ_UNUSED_ARG(argc);
    PJ_UNUSED_ARG(argv);
    puts("Error: this sample requires video capability (PJMEDIA_HAS_VIDEO == 1)");
    return -1;
}

#endif /* PJMEDIA_HAS_VIDEO */
