/* 
 * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
 * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
 *
 * 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/vid_codec_util.h>
#include <pjmedia/errno.h>
#include <pjmedia/stream_common.h>
#include <pjlib-util/base64.h>
#include <pj/ctype.h>
#include <pj/math.h>


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


#define THIS_FILE   "vid_codec_util.c"

/* If this is set to non-zero, H.264 custom negotiation will require
 * "profile-level-id" and "packetization-mode" to be exact match to
 * get a successful negotiation. Note that flexible answer (updating
 * SDP answer to match remote offer) is always active regardless the
 * value of this macro.
 */
#define H264_STRICT_SDP_NEGO        0

/* Default frame rate, if not specified */
#define DEFAULT_H264_FPS_NUM        10
#define DEFAULT_H264_FPS_DENUM      1

/* Default aspect ratio, if not specified */
#define DEFAULT_H264_RATIO_NUM      4
#define DEFAULT_H264_RATIO_DENUM    3


/* ITU resolution definition */
struct mpi_resolution_t
{
    pj_str_t            name;    
    pjmedia_rect_size   size;
}
mpi_resolutions [] =
{
    {{"CIF",3},     {352,288}},
    {{"QCIF",4},    {176,144}},
    {{"SQCIF",5},   {88,72}},
    {{"CIF4",4},    {704,576}},
    {{"CIF16",5},   {1408,1142}},
};


#define CALC_H264_MB_NUM(size) (((size.w+15)/16)*((size.h+15)/16))
#define CALC_H264_MBPS(size,fps) CALC_H264_MB_NUM(size)*fps.num/fps.denum


/* Parse fmtp value for custom resolution, e.g: "CUSTOM=800,600,2" */
static pj_status_t parse_custom_res_fmtp(const pj_str_t *fmtp_val,
                                         pjmedia_rect_size *size,
                                         unsigned *mpi)
{
    const char *p, *p_end;
    pj_str_t token;
    unsigned long val[3] = {0};
    unsigned i = 0;

    p = token.ptr = fmtp_val->ptr;
    p_end = p + fmtp_val->slen;

    while (p<=p_end && i<PJ_ARRAY_SIZE(val)) {
        if (*p==',' || p==p_end) {
            token.slen = (char*)p - token.ptr;
            val[i++] = pj_strtoul(&token);
            token.ptr = (char*)p+1;
        }
        ++p;
    }

    if (!val[0] || !val[1])
        return PJ_ETOOSMALL;

    if (val[2]<1 || val[2]>32)
        return PJ_EINVAL;

    size->w = val[0];
    size->h = val[1];
    *mpi = val[2];
    return PJ_SUCCESS;
}


/* H263 fmtp parser */
PJ_DEF(pj_status_t) pjmedia_vid_codec_parse_h263_fmtp(
                                    const pjmedia_codec_fmtp *fmtp,
                                    pjmedia_vid_codec_h263_fmtp *h263_fmtp)
{
    const pj_str_t CUSTOM = {"CUSTOM", 6};
    unsigned i;

    pj_bzero(h263_fmtp, sizeof(*h263_fmtp));

    for (i=0; i<fmtp->cnt; ++i) {
        unsigned j;
        pj_bool_t parsed = PJ_FALSE;

        if (h263_fmtp->mpi_cnt >= PJ_ARRAY_SIZE(h263_fmtp->mpi)) {
            pj_assert(!"Too small MPI array in H263 fmtp");
            continue;
        }

        /* Standard size MPIs */
        for (j=0; j<PJ_ARRAY_SIZE(mpi_resolutions) && !parsed; ++j) {
            if (pj_stricmp(&fmtp->param[i].name, &mpi_resolutions[j].name)==0)
            {
                unsigned mpi;

                mpi = pj_strtoul(&fmtp->param[i].val);
                if (mpi<1 || mpi>32)
                    return PJMEDIA_SDP_EINFMTP;

                h263_fmtp->mpi[h263_fmtp->mpi_cnt].size = 
                                                    mpi_resolutions[j].size;
                h263_fmtp->mpi[h263_fmtp->mpi_cnt].val = mpi;
                ++h263_fmtp->mpi_cnt;
                parsed = PJ_TRUE;
            }
        }
        if (parsed)
            continue;

        /* Custom size MPIs */
        if (pj_stricmp(&fmtp->param[i].name, &CUSTOM)==0) {
            pjmedia_rect_size size;
            unsigned mpi;
            pj_status_t status;

            status = parse_custom_res_fmtp(&fmtp->param[i].val, &size, &mpi);
            if (status != PJ_SUCCESS)
                return PJMEDIA_SDP_EINFMTP;

            h263_fmtp->mpi[h263_fmtp->mpi_cnt].size = size;
            h263_fmtp->mpi[h263_fmtp->mpi_cnt].val = mpi;
            ++h263_fmtp->mpi_cnt;
        }
    }

    return PJ_SUCCESS;
}


static unsigned fps_to_mpi(const pjmedia_ratio *fps) 
{
    unsigned mpi;

    /* Original formula = (fps->denum * 30000) / (fps->num * 1001) */
    mpi = (fps->denum*30000 + fps->num*1001/2) / (fps->num*1001);
    
    /* Normalize, should be in the range of 1-32 */
    if (mpi > 32) mpi = 32;
    if (mpi < 1) mpi = 1;

    return mpi;
}

PJ_DEF(pj_status_t) pjmedia_vid_codec_h263_apply_fmtp(
                                pjmedia_vid_codec_param *param)
{
    if (param->dir & PJMEDIA_DIR_ENCODING) {
        pjmedia_vid_codec_h263_fmtp fmtp_loc, fmtp_rem;
        pjmedia_rect_size size = {0};
        unsigned mpi = 0;
        pjmedia_video_format_detail *vfd;
        pj_status_t status;

        vfd = pjmedia_format_get_video_format_detail(&param->enc_fmt,
                                                     PJ_TRUE);

        /* Get local param */
        // Local param should be fetched from "param->enc_fmt" instead of
        // "param->dec_fmtp".
        //status = pjmedia_vid_codec_parse_h263_fmtp(&param->dec_fmtp,
        //                                         &fmtp_loc);
        //if (status != PJ_SUCCESS)
        //    return status;
        fmtp_loc.mpi_cnt = 1;
        fmtp_loc.mpi[0].size = vfd->size;
        fmtp_loc.mpi[0].val  = fps_to_mpi(&vfd->fps);

        /* Get remote param */
        status = pjmedia_vid_codec_parse_h263_fmtp(&param->enc_fmtp,
                                                   &fmtp_rem);
        if (status != PJ_SUCCESS)
            return status;

        /* Negotiate size & MPI setting */
        if (fmtp_rem.mpi_cnt == 0) {
            /* Remote doesn't specify MPI setting, send QCIF=1 */
            size.w = 176;
            size.h = 144;
            mpi    = 1;
        //} else if (fmtp_loc.mpi_cnt == 0) {
        //    /* Local MPI setting not set, just use remote preference. */
        //    size = fmtp_rem.mpi[0].size;
        //    mpi  = fmtp_rem.mpi[0].val;
        } else {
            /* Both have preferences, let's try to match them */
            unsigned i, j;
            pj_bool_t matched = PJ_FALSE;
            pj_uint32_t min_diff = 0xFFFFFFFF;
            pj_uint32_t loc_sq, rem_sq, diff;

            /* Find the exact size match or the closest size, then choose
             * the highest MPI among the match/closest pair.
             */
            for (i = 0; i < fmtp_rem.mpi_cnt && !matched; ++i) {
                rem_sq = fmtp_rem.mpi[i].size.w * fmtp_rem.mpi[i].size.h;
                for (j = 0; j < fmtp_loc.mpi_cnt; ++j) {
                    /* See if we got exact match */
                    if (fmtp_rem.mpi[i].size.w == fmtp_loc.mpi[j].size.w &&
                        fmtp_rem.mpi[i].size.h == fmtp_loc.mpi[j].size.h)
                    {
                        size = fmtp_rem.mpi[i].size;
                        mpi  = PJ_MAX(fmtp_rem.mpi[i].val,
                                      fmtp_loc.mpi[j].val);
                        matched = PJ_TRUE;
                        break;
                    }

                    /* Otherwise keep looking for the closest match */
                    loc_sq = fmtp_loc.mpi[j].size.w * fmtp_loc.mpi[j].size.h;
                    diff = loc_sq>rem_sq? (loc_sq-rem_sq):(rem_sq-loc_sq);
                    if (diff < min_diff) {
                        size = rem_sq<loc_sq? fmtp_rem.mpi[i].size :
                                              fmtp_loc.mpi[j].size;
                        mpi  = PJ_MAX(fmtp_rem.mpi[i].val,
                                      fmtp_loc.mpi[j].val);
                    }
                }
            }
        }

        /* Apply the negotiation result */
        vfd->size = size;
        vfd->fps.num = 30000;
        vfd->fps.denum = 1001 * mpi;
    }

    if (param->dir & PJMEDIA_DIR_DECODING) {
        /* Here we just want to find the highest resolution and the lowest MPI
         * we support and set it as the decoder param.
         */
        pjmedia_vid_codec_h263_fmtp fmtp;
        pjmedia_video_format_detail *vfd;
        pj_status_t status;
        
        status = pjmedia_vid_codec_parse_h263_fmtp(&param->dec_fmtp,
                                                   &fmtp);
        if (status != PJ_SUCCESS)
            return status;

        vfd = pjmedia_format_get_video_format_detail(&param->dec_fmt,
                                                     PJ_TRUE);

        if (fmtp.mpi_cnt == 0) {
            /* No resolution specified, lets just assume 4CIF=1! */
            vfd->size.w = 704;
            vfd->size.h = 576;
            vfd->fps.num = 30000;
            vfd->fps.denum = 1001;
        } else {
            unsigned i, max_size = 0, max_size_idx = 0, min_mpi = 32;
            
            /* Get the largest size and the lowest MPI */
            for (i = 0; i < fmtp.mpi_cnt; ++i) {
                if (fmtp.mpi[i].size.w * fmtp.mpi[i].size.h > max_size) {
                    max_size = fmtp.mpi[i].size.w * fmtp.mpi[i].size.h;
                    max_size_idx = i;
                }
                if (fmtp.mpi[i].val < min_mpi)
                    min_mpi = fmtp.mpi[i].val;
            }

            vfd->size = fmtp.mpi[max_size_idx].size;
            vfd->fps.num = 30000;
            vfd->fps.denum = 1001 * min_mpi;
        }
    }

    return PJ_SUCCESS;
}


/* Declaration of H.264 level info */
typedef struct h264_level_info_t
{
    unsigned id;            /* Level id.                        */
    unsigned max_mbps;      /* Max macroblocks per second.      */
    unsigned max_mb;        /* Max macroblocks.                 */
    unsigned max_br;        /* Max bitrate (kbps).              */
} h264_level_info_t;


/* Init H264 parameters based on profile-level-id */
static pj_status_t init_h264_profile(const pj_str_t *profile,
                                     pjmedia_vid_codec_h264_fmtp *fmtp)
{
    // Taken from https://www.itu.int/rec/T-REC-H.264
    // H.264 (08/21) Table A-1 – Level limits
    const h264_level_info_t level_info[] =
    {
        { 10,     1485,     99,     64 },
        { 9,      1485,     99,    128 }, /*< level 1b */
        { 11,     3000,    396,    192 },
        { 12,     6000,    396,    384 },
        { 13,    11880,    396,    768 },
        { 20,    11880,    396,   2000 },
        { 21,    19800,    792,   4000 },
        { 22,    20250,   1620,   4000 },
        { 30,    40500,   1620,  10000 },
        { 31,   108000,   3600,  14000 },
        { 32,   216000,   5120,  20000 },
        { 40,   245760,   8192,  20000 },
        { 41,   245760,   8192,  50000 },
        { 42,   522240,   8704,  50000 },
        { 50,   589824,  22080, 135000 },
        { 51,   983040,  36864, 240000 },
        { 52,  2073600,  36864, 240000 },
        { 60,  4177920, 139264, 240000 },
        { 61,  8355840, 139264, 480000 },
        { 62, 16711680, 139264, 800000 },
    };
    unsigned i, tmp;
    pj_str_t endst;
    const h264_level_info_t *li = NULL;

    if (profile->slen != 6)
        return PJMEDIA_SDP_EINFMTP;

    tmp = pj_strtoul2(profile, &endst, 16);
    if (endst.slen)
        return PJMEDIA_SDP_EINFMTP;

    fmtp->profile_idc = (pj_uint8_t)((tmp >> 16) & 0xFF);
    fmtp->profile_iop = (pj_uint8_t)((tmp >> 8) & 0xFF);
    fmtp->level = (pj_uint8_t)(tmp & 0xFF);

    for (i = 0; i < PJ_ARRAY_SIZE(level_info); ++i) {
        if (level_info[i].id == fmtp->level) {
            li = &level_info[i];
            break;
        }
    }
    if (li == NULL)
        return PJMEDIA_SDP_EINFMTP;

    /* Init profile level spec */
    if (fmtp->max_br == 0)
        fmtp->max_br = li->max_br;
    if (fmtp->max_mbps == 0)
        fmtp->max_mbps = li->max_mbps;
    if (fmtp->max_fs == 0)
        fmtp->max_fs = li->max_mb;

    return PJ_SUCCESS;
}


/* H264 fmtp parser */
PJ_DEF(pj_status_t) pjmedia_vid_codec_h264_parse_fmtp(
                                    const pjmedia_codec_fmtp *fmtp,
                                    pjmedia_vid_codec_h264_fmtp *h264_fmtp)
{
    const pj_str_t PROFILE_LEVEL_ID     = {"profile-level-id", 16};
    const pj_str_t MAX_MBPS             = {"max-mbps", 8};
    const pj_str_t MAX_FS               = {"max-fs", 6};
    const pj_str_t MAX_CPB              = {"max-cpb", 7};
    const pj_str_t MAX_DPB              = {"max-dpb", 7};
    const pj_str_t MAX_BR               = {"max-br", 6};
    const pj_str_t PACKETIZATION_MODE   = {"packetization-mode", 18};
    const pj_str_t SPROP_PARAMETER_SETS = {"sprop-parameter-sets", 20};
    unsigned i;
    pj_status_t status;

    pj_bzero(h264_fmtp, sizeof(*h264_fmtp));

    for (i=0; i<fmtp->cnt; ++i) {
        unsigned tmp;
        if (pj_stricmp(&fmtp->param[i].name, &PROFILE_LEVEL_ID)==0) {
            /* Init H264 parameters based on level, if not set yet */
            status = init_h264_profile(&fmtp->param[i].val, h264_fmtp);
            if (status != PJ_SUCCESS)
                return status;
        } else if (pj_stricmp(&fmtp->param[i].name, &PACKETIZATION_MODE)==0) {
            tmp = pj_strtoul(&fmtp->param[i].val);
            if (tmp <= 2) 
                h264_fmtp->packetization_mode = (pj_uint8_t)tmp;
            else
                return PJMEDIA_SDP_EINFMTP;
        } else if (pj_stricmp(&fmtp->param[i].name, &MAX_MBPS)==0) {
            tmp = pj_strtoul(&fmtp->param[i].val);
            h264_fmtp->max_mbps = PJ_MAX(tmp, h264_fmtp->max_mbps);
        } else if (pj_stricmp(&fmtp->param[i].name, &MAX_FS)==0) {
            tmp = pj_strtoul(&fmtp->param[i].val);
            h264_fmtp->max_fs = PJ_MAX(tmp, h264_fmtp->max_fs);
        } else if (pj_stricmp(&fmtp->param[i].name, &MAX_CPB)==0) {
            tmp = pj_strtoul(&fmtp->param[i].val);
            h264_fmtp->max_cpb = PJ_MAX(tmp, h264_fmtp->max_cpb);
        } else if (pj_stricmp(&fmtp->param[i].name, &MAX_DPB)==0) {
            tmp = pj_strtoul(&fmtp->param[i].val);
            h264_fmtp->max_dpb = PJ_MAX(tmp, h264_fmtp->max_dpb);
        } else if (pj_stricmp(&fmtp->param[i].name, &MAX_BR)==0) {
            tmp = pj_strtoul(&fmtp->param[i].val);
            h264_fmtp->max_br = PJ_MAX(tmp, h264_fmtp->max_br);
        } else if (pj_stricmp(&fmtp->param[i].name, &SPROP_PARAMETER_SETS)==0)
        {
            pj_str_t sps_st;

            sps_st = fmtp->param[i].val;
            while (sps_st.slen) {
                pj_str_t tmp_st;
                int tmp_len;
                const pj_uint8_t start_code[3] = {0, 0, 1};
                char *p;
                pj_uint8_t *nal;

                /* Find field separator ',' */
                tmp_st = sps_st;
                p = pj_strchr(&sps_st, ',');
                if (p) {
                    tmp_st.slen = p - sps_st.ptr;
                    sps_st.ptr  = p+1;
                    sps_st.slen -= (tmp_st.slen+1);
                } else {
                    sps_st.slen = 0;
                }

                /* Decode field and build NAL unit for this param */
                nal = &h264_fmtp->sprop_param_sets[
                                          h264_fmtp->sprop_param_sets_len];
                tmp_len = PJ_ARRAY_SIZE(h264_fmtp->sprop_param_sets) -
                          (int)h264_fmtp->sprop_param_sets_len -
                          PJ_ARRAY_SIZE(start_code);
                status = pj_base64_decode(&tmp_st,
                                          nal + PJ_ARRAY_SIZE(start_code),
                                          &tmp_len);
                if (status != PJ_SUCCESS)
                    return PJMEDIA_SDP_EINFMTP;

                tmp_len += PJ_ARRAY_SIZE(start_code);
                pj_memcpy(nal, start_code, PJ_ARRAY_SIZE(start_code));
                h264_fmtp->sprop_param_sets_len += tmp_len;
            }
        }
    }

    /* When profile-level-id is not specified, use default value "42000A" */
    if (h264_fmtp->profile_idc == 0) {
        const pj_str_t DEF_PROFILE = {"42000A", 6};

        status = init_h264_profile(&DEF_PROFILE, h264_fmtp);
        if (status != PJ_SUCCESS)
            return status;
    }

    return PJ_SUCCESS;
}

PJ_DEF(pj_status_t) pjmedia_vid_codec_h264_match_sdp(pj_pool_t *pool,
                                                     pjmedia_sdp_media *offer,
                                                     unsigned o_fmt_idx,
                                                     pjmedia_sdp_media *answer,
                                                     unsigned a_fmt_idx,
                                                     unsigned option)
{
    const pj_str_t PROFILE_LEVEL_ID     = {"profile-level-id", 16};
    const pj_str_t PACKETIZATION_MODE   = {"packetization-mode", 18};
    pjmedia_codec_fmtp o_fmtp_raw, a_fmtp_raw;
    pjmedia_vid_codec_h264_fmtp o_fmtp, a_fmtp;
    pj_status_t status;

    PJ_UNUSED_ARG(pool);

    /* Parse offer */
    status = pjmedia_stream_info_parse_fmtp(
                                    NULL, offer, 
                                    pj_strtoul(&offer->desc.fmt[o_fmt_idx]),
                                    &o_fmtp_raw);
    if (status != PJ_SUCCESS)
        return status;

    status = pjmedia_vid_codec_h264_parse_fmtp(&o_fmtp_raw, &o_fmtp);
    if (status != PJ_SUCCESS)
        return status;

    /* Parse answer */
    status = pjmedia_stream_info_parse_fmtp(
                                    NULL, answer, 
                                    pj_strtoul(&answer->desc.fmt[a_fmt_idx]),
                                    &a_fmtp_raw);
    if (status != PJ_SUCCESS)
        return status;

    status = pjmedia_vid_codec_h264_parse_fmtp(&a_fmtp_raw, &a_fmtp);
    if (status != PJ_SUCCESS)
        return status;

    if (option & PJMEDIA_SDP_NEG_FMT_MATCH_ALLOW_MODIFY_ANSWER) {
        unsigned i;

        /* Flexible negotiation, if the answer has higher capability than
         * the offer, adjust the answer capability to be match to the offer.
         */
        if (a_fmtp.profile_idc >= o_fmtp.profile_idc)
            a_fmtp.profile_idc = o_fmtp.profile_idc;
        if (a_fmtp.profile_iop != o_fmtp.profile_iop)
            a_fmtp.profile_iop = o_fmtp.profile_iop;
        if (a_fmtp.level >= o_fmtp.level)
            a_fmtp.level = o_fmtp.level;
        if (a_fmtp.packetization_mode >= o_fmtp.packetization_mode)
            a_fmtp.packetization_mode = o_fmtp.packetization_mode;

        /* Match them now */
#if H264_STRICT_SDP_NEGO
        if (a_fmtp.profile_idc != o_fmtp.profile_idc ||
            a_fmtp.profile_iop != o_fmtp.profile_iop ||
            a_fmtp.level != o_fmtp.level ||
            a_fmtp.packetization_mode != o_fmtp.packetization_mode)
        {
            return PJMEDIA_SDP_EFORMATNOTEQUAL;
        }
#else
        if (a_fmtp.profile_idc != o_fmtp.profile_idc)
        {
            return PJMEDIA_SDP_EFORMATNOTEQUAL;
        }
#endif

        /* Update the answer */
        for (i = 0; i < a_fmtp_raw.cnt; ++i) {
            if (pj_stricmp(&a_fmtp_raw.param[i].name, &PROFILE_LEVEL_ID) == 0)
            {
                char *p = a_fmtp_raw.param[i].val.ptr;
                pj_val_to_hex_digit(a_fmtp.profile_idc, p);
                p += 2;
                pj_val_to_hex_digit(a_fmtp.profile_iop, p);
                p += 2;
                pj_val_to_hex_digit(a_fmtp.level, p);
            }
            else if (pj_stricmp(&a_fmtp_raw.param[i].name, &PACKETIZATION_MODE) == 0)
            {
                char *p = a_fmtp_raw.param[i].val.ptr;
                *p = '0' + a_fmtp.packetization_mode;
            }
        }
    } else {
#if H264_STRICT_SDP_NEGO
        /* Strict negotiation */
        if (a_fmtp.profile_idc != o_fmtp.profile_idc ||
            a_fmtp.profile_iop != o_fmtp.profile_iop ||
            a_fmtp.level != o_fmtp.level ||
            a_fmtp.packetization_mode != o_fmtp.packetization_mode)
        {
            return PJMEDIA_SDP_EFORMATNOTEQUAL;
        }
#else
        /* Permissive negotiation */
        if (a_fmtp.profile_idc != o_fmtp.profile_idc)
        {
            return PJMEDIA_SDP_EFORMATNOTEQUAL;
        }
#endif
    }

    return PJ_SUCCESS;
}


/* Find greatest common divisor (GCD) */
static unsigned gcd (unsigned a, unsigned b) {
    unsigned c;
    while (b) {
        c = a % b;
        a = b;
        b = c;
    }
    return a;
}

/* Find highest resolution possible for the specified H264 fmtp and
 * aspect ratio.
 */
static pj_status_t find_highest_res(pjmedia_vid_codec_h264_fmtp *fmtp,
                                    const pjmedia_ratio *fps,
                                    const pjmedia_ratio *ratio,
                                    pjmedia_rect_size *size,
                                    pj_bool_t is_decoding)
{
    pjmedia_ratio def_ratio = { DEFAULT_H264_RATIO_NUM,
                                DEFAULT_H264_RATIO_DENUM };
    pjmedia_ratio def_fps   = { DEFAULT_H264_FPS_NUM,
                                DEFAULT_H264_FPS_DENUM };
    pjmedia_ratio asp_ratio, the_fps;
    unsigned max_fs, g, scale;

    pj_assert(size);

    /* Get the ratio, or just use default if not provided. */
    if (ratio && ratio->num && ratio->denum) {
        asp_ratio = *ratio;
    } else {
        asp_ratio = def_ratio;
    }

    /* Normalize the aspect ratio */
    g = gcd(asp_ratio.num, asp_ratio.denum);
    asp_ratio.num /= g;
    asp_ratio.denum /= g;

    /* Get the frame rate, or just use default if not provided. */
    if (fps && fps->num && fps->denum) {
        the_fps = *fps;
    } else {
        the_fps = def_fps;
    }

    /* Calculate maximum size (in macroblocks) */
    max_fs = fmtp->max_mbps * the_fps.denum / the_fps.num;
    max_fs = PJ_MIN(max_fs, fmtp->max_fs);

    /* Check if the specified ratio is using big numbers
     * (not normalizable), override it!
     */
    if ((int)max_fs < asp_ratio.num * asp_ratio.denum) {
        if ((int)max_fs >= def_ratio.num * def_ratio.denum)
            asp_ratio = def_ratio;
        else
            asp_ratio.num = asp_ratio.denum = 1;
    }

    /* Calculate the scale factor for size */
    scale = pj_isqrt(max_fs / asp_ratio.denum / asp_ratio.num);

    /* Calculate the size, note that the frame size is in macroblock units */
    size->w = asp_ratio.num   * scale * 16;
    size->h = asp_ratio.denum * scale * 16;

    /* #1769: for decoding, size is usually used for allocating buffer,
     * so we need to make sure that frame size is not less than max_fs.
     */
    if (is_decoding && ((size->w * size->h) >> 8) < max_fs) {
        /* Size is less than max_fs, recalculate using ratio 1:1 and
         * round up the scale.
         */
        scale = pj_isqrt(max_fs) + 1;
        size->w = size->h = scale * 16;
    }

    return PJ_SUCCESS;
}


PJ_DEF(pj_status_t) pjmedia_vid_codec_h264_apply_fmtp(
                                pjmedia_vid_codec_param *param)
{
    if (param->dir & PJMEDIA_DIR_ENCODING) {
        pjmedia_vid_codec_h264_fmtp fmtp;
        pjmedia_video_format_detail *vfd;
        pj_status_t status;

        /* Get remote param */
        status = pjmedia_vid_codec_h264_parse_fmtp(&param->enc_fmtp,
                                                   &fmtp);
        if (status != PJ_SUCCESS)
            return status;

        /* Adjust fps, size, and bitrate to conform to H.264 level
         * specified by remote SDP fmtp.
         */
        vfd = pjmedia_format_get_video_format_detail(&param->enc_fmt,
                                                     PJ_TRUE);

        if (vfd->fps.num == 0 || vfd->fps.denum == 0) {
            vfd->fps.num   = DEFAULT_H264_FPS_NUM;
            vfd->fps.denum = DEFAULT_H264_FPS_DENUM;
        }

        if (vfd->size.w && vfd->size.h) {
            unsigned mb, mbps;
            
            /* Scale down the resolution if it exceeds profile spec */
            mb = CALC_H264_MB_NUM(vfd->size);
            mbps = CALC_H264_MBPS(vfd->size, vfd->fps);
            if (mb > fmtp.max_fs || mbps > fmtp.max_mbps) {
                pjmedia_ratio r;
                r.num = vfd->size.w;
                r.denum = vfd->size.h;
                find_highest_res(&fmtp, &vfd->fps, &r, &vfd->size, PJ_FALSE);
            }
        } else {
            /* When not specified, just use the highest res possible*/
            pjmedia_ratio r;
            r.num = vfd->size.w;
            r.denum = vfd->size.h;
            find_highest_res(&fmtp, &vfd->fps, &r, &vfd->size, PJ_FALSE);
        }

        /* Encoding bitrate must not be higher than H264 level spec */
        if (vfd->avg_bps > fmtp.max_br * 1000)
            vfd->avg_bps = fmtp.max_br * 1000;
        if (vfd->max_bps > fmtp.max_br * 1000)
            vfd->max_bps = fmtp.max_br * 1000;
    }

    if (param->dir & PJMEDIA_DIR_DECODING) {
        /* Here we just want to find the highest resolution possible from the
         * fmtp and set it as the decoder param.
         */
        pjmedia_vid_codec_h264_fmtp fmtp;
        pjmedia_video_format_detail *vfd;
        pjmedia_ratio r;
        pjmedia_rect_size highest_size;
        pj_status_t status;
        
        status = pjmedia_vid_codec_h264_parse_fmtp(&param->dec_fmtp,
                                                   &fmtp);
        if (status != PJ_SUCCESS)
            return status;

        vfd = pjmedia_format_get_video_format_detail(&param->dec_fmt,
                                                     PJ_TRUE);

        if (vfd->fps.num == 0 || vfd->fps.denum == 0) {
            vfd->fps.num   = DEFAULT_H264_FPS_NUM;
            vfd->fps.denum = DEFAULT_H264_FPS_DENUM;
        }

        /* Normalize decoding resolution, i.e: it must not be lower than
         * the H264 profile level setting used, as this may be used by
         * app to allocate buffer.
         */
        r.num = vfd->size.w;
        r.denum = vfd->size.h;
        find_highest_res(&fmtp, &vfd->fps, &r, &highest_size, PJ_TRUE);
        if (vfd->size.w * vfd->size.h < highest_size.w * highest_size.h)
            vfd->size = highest_size;

        /* Normalize decoding bitrate based on H264 level spec */
        if (vfd->avg_bps < fmtp.max_br * 1000)
            vfd->avg_bps = fmtp.max_br * 1000;
        if (vfd->max_bps < fmtp.max_br * 1000)
            vfd->max_bps = fmtp.max_br * 1000;
    }

    return PJ_SUCCESS;
}


/* VPX fmtp parser */
PJ_DEF(pj_status_t) pjmedia_vid_codec_vpx_parse_fmtp(
                                    const pjmedia_codec_fmtp *fmtp,
                                    pjmedia_vid_codec_vpx_fmtp *vpx_fmtp)
{
    const pj_str_t PROFILE_ID   = {"profile-id", 10};
    const pj_str_t MAX_FR       = {"max-fr", 6};
    const pj_str_t MAX_FS       = {"max-fs", 6};

    unsigned i;

    pj_bzero(vpx_fmtp, sizeof(*vpx_fmtp));

    for (i = 0; i < fmtp->cnt; ++i) {
        unsigned tmp;

        if (pj_stricmp(&fmtp->param[i].name, &MAX_FS) == 0) {
            tmp = pj_strtoul(&fmtp->param[i].val);
            vpx_fmtp->max_fs = PJ_MAX(tmp, vpx_fmtp->max_fs);
        } else if (pj_stricmp(&fmtp->param[i].name, &MAX_FR) == 0) {
            tmp = pj_strtoul(&fmtp->param[i].val);
            vpx_fmtp->max_fr = PJ_MAX(tmp, vpx_fmtp->max_fr);
        } else if (pj_stricmp(&fmtp->param[i].name, &PROFILE_ID) == 0) {
            tmp = pj_strtoul(&fmtp->param[i].val);
            vpx_fmtp->profile_id = (pj_uint8_t)
                                   PJ_MAX(tmp, vpx_fmtp->profile_id);
        }
    }

    return PJ_SUCCESS;
}


PJ_DEF(pj_status_t) pjmedia_vid_codec_vpx_apply_fmtp(
                                pjmedia_vid_codec_param *param)
{
    if (param->dir & PJMEDIA_DIR_ENCODING) {
        pjmedia_vid_codec_vpx_fmtp fmtp;
        pjmedia_video_format_detail *vfd;
        pj_status_t status;

        /* Get remote param */
        status = pjmedia_vid_codec_vpx_parse_fmtp(&param->enc_fmtp,
                                                  &fmtp);
        if (status != PJ_SUCCESS)
            return status;

        /* Adjust fps and size to conform to the parameter
         * specified by remote SDP fmtp.
         */
        vfd = pjmedia_format_get_video_format_detail(&param->enc_fmt,
                                                     PJ_TRUE);

        if (fmtp.max_fr > 0) {
            if ((float)vfd->fps.num/vfd->fps.denum > (float)fmtp.max_fr) {
                vfd->fps.num   = fmtp.max_fr;
                vfd->fps.denum = 1;
            }
        }
        
        if (fmtp.max_fs > 0) {
            unsigned max_res = ((int)pj_isqrt(fmtp.max_fs * 8)) * 16;
            
            if (vfd->size.w > max_res || vfd->size.h > max_res) {
                /* Here we maintain the aspect ratio. Or should we scale down
                 * to some predetermined resolution instead (for example,
                 * if the requested resolution is 640x480 and max_res is
                 * 600, should we scale down to 480x360)?
                 */
                unsigned larger = (vfd->size.w > vfd->size.h)?
                                  vfd->size.w: vfd->size.h;
                float scale = (float)max_res/larger;

                vfd->size.w = (int)(scale * vfd->size.w);
                vfd->size.h = (int)(scale * vfd->size.h);
            }
        }
    }

    if (param->dir & PJMEDIA_DIR_DECODING) {
        /* Here we just want to find the highest fps and resolution possible
         * from the fmtp and set it as the decoder param.
         */
        pjmedia_vid_codec_vpx_fmtp fmtp;
        pjmedia_video_format_detail *vfd;
        pj_status_t status;

        /* Get remote param */
        status = pjmedia_vid_codec_vpx_parse_fmtp(&param->dec_fmtp,
                                                  &fmtp);
        if (status != PJ_SUCCESS)
            return status;

        vfd = pjmedia_format_get_video_format_detail(&param->dec_fmt,
                                                     PJ_TRUE);

        if (fmtp.max_fr > 0) {
            vfd->fps.num   = fmtp.max_fr;
            vfd->fps.denum = 1;
        }
        
        if (fmtp.max_fs > 0) {
            unsigned max_res = ((int)pj_isqrt(fmtp.max_fs * 8)) * 16;
            
            vfd->size.w = max_res;
            vfd->size.h = max_res;
        }
    }

    return PJ_SUCCESS;
}


#endif /* PJMEDIA_HAS_VIDEO */
