/* json.c

   Copyright (c) 2003-2026 HandBrake Team
   This file is part of the HandBrake source code
   Homepage: <http://handbrake.fr/>.
   It may be used under the terms of the GNU General Public License v2.
   For full terms see the file COPYING file or visit http://www.gnu.org/licenses/gpl-2.0.html
 */

#include <jansson.h>
#include "handbrake/handbrake.h"
#include "handbrake/hb_json.h"
#include "libavutil/base64.h"

/**
 * Convert an hb_state_t to a jansson dict
 * @param state - Pointer to hb_state_t to convert
 */
hb_dict_t* hb_state_to_dict( hb_state_t * state)
{
    const char * state_s;
    hb_dict_t *dict = NULL;
    json_error_t error;

    switch (state->state)
    {
    case HB_STATE_IDLE:
        state_s = "IDLE";
        break;
    case HB_STATE_SCANNING:
        state_s = "SCANNING";
        break;
    case HB_STATE_SCANDONE:
        state_s = "SCANDONE";
        break;
    case HB_STATE_WORKING:
        state_s = "WORKING";
        break;
    case HB_STATE_PAUSED:
        state_s = "PAUSED";
        break;
    case HB_STATE_SEARCHING:
        state_s = "SEARCHING";
        break;
    case HB_STATE_WORKDONE:
        state_s = "WORKDONE";
        break;
    case HB_STATE_MUXING:
        state_s = "MUXING";
        break;
    default:
        state_s = "UNKNOWN";
        break;
    }

    switch (state->state)
    {
    case HB_STATE_IDLE:
        dict = json_pack_ex(&error, 0, "{s:o}",
                    "State", hb_value_string(state_s));
        break;
    case HB_STATE_SCANNING:
    case HB_STATE_SCANDONE:
        dict = json_pack_ex(&error, 0,
            "{s:o, s{s:o, s:o, s:o, s:o, s:o, s:o}}",
            "State", hb_value_string(state_s),
            "Scanning",
                "SequenceID",   hb_value_int(state->sequence_id),
                "Progress",     hb_value_double(state->param.scanning.progress),
                "Preview",      hb_value_int(state->param.scanning.preview_cur),
                "PreviewCount", hb_value_int(state->param.scanning.preview_count),
                "Title",        hb_value_int(state->param.scanning.title_cur),
                "TitleCount",   hb_value_int(state->param.scanning.title_count));
        break;
    case HB_STATE_WORKING:
    case HB_STATE_PAUSED:
    case HB_STATE_SEARCHING:
        dict = json_pack_ex(&error, 0,
            "{s:o, s{s:o, s:o, s:o, s:o, s:o, s:o,"
                   " s:o, s:o, s:o, s:o, s:o, s:o}}",
            "State", hb_value_string(state_s),
            "Working",
                "Progress",     hb_value_double(state->param.working.progress),
                "PassID",       hb_value_int(state->param.working.pass_id),
                "Pass",         hb_value_int(state->param.working.pass),
                "PassCount",    hb_value_int(state->param.working.pass_count),
                "Rate",         hb_value_double(state->param.working.rate_cur),
                "RateAvg",      hb_value_double(state->param.working.rate_avg),
                "ETASeconds",   hb_value_int(state->param.working.eta_seconds),
                "Hours",        hb_value_int(state->param.working.hours),
                "Minutes",      hb_value_int(state->param.working.minutes),
                "Paused",       hb_value_int(state->param.working.paused),
                "Seconds",      hb_value_int(state->param.working.seconds),
                "SequenceID",   hb_value_int(state->sequence_id));
        break;
    case HB_STATE_WORKDONE:
        dict = json_pack_ex(&error, 0,
            "{s:o, s{s:o, s:o}}",
            "State", hb_value_string(state_s),
            "WorkDone",
                "SequenceID",   hb_value_int(state->sequence_id),
                "Error",        hb_value_int(state->param.working.error));
        break;
    case HB_STATE_MUXING:
        dict = json_pack_ex(&error, 0,
            "{s:o, s{s:o}}",
            "State", hb_value_string(state_s),
            "Muxing",
                "Progress", hb_value_double(state->param.muxing.progress));
        break;
    default:
        dict = json_pack_ex(&error, 0, "{s:o}",
                    "State", hb_value_string(state_s));
        hb_error("hb_state_to_dict: unrecognized state %d", state->state);
        break;
    }
    if (dict == NULL)
    {
        hb_error("hb_state_to_dict, json pack failure: %s", error.text);
    }
    return dict;
}

hb_dict_t * hb_version_dict()
{
    hb_dict_t * dict;
    json_error_t error;

    dict = json_pack_ex(&error, 0,
        "{s:o, s:o, s:o, s{s:o, s:o, s:o}, s:o, s:o, s:o, s:o, s:o}",
        "Name",          hb_value_string(HB_PROJECT_NAME),
        "Official",      hb_value_bool(HB_PROJECT_REPO_OFFICIAL),
        "Type",          hb_value_string(HB_PROJECT_REPO_TYPE),
        "Version",
            "Major",     hb_value_int(HB_PROJECT_VERSION_MAJOR),
            "Minor",     hb_value_int(HB_PROJECT_VERSION_MINOR),
            "Point",     hb_value_int(HB_PROJECT_VERSION_POINT),
        "VersionString", hb_value_string(HB_PROJECT_VERSION),
        "RepoHash",      hb_value_string(HB_PROJECT_REPO_HASH),
        "RepoDate",      hb_value_string(HB_PROJECT_REPO_DATE),
        "System",        hb_value_string(HB_PROJECT_HOST_SYSTEMF),
        "Arch",          hb_value_string(HB_PROJECT_HOST_ARCH));
    if (dict == NULL)
    {
        hb_error("hb_version_dict, json pack failure: %s", error.text);
        return NULL;
    }

    return dict;
}

/**
 * Get the current state of an hb instance as a json string
 * @param h - Pointer to an hb_handle_t hb instance
 */
char* hb_get_state_json( hb_handle_t * h )
{
    hb_state_t state;

    hb_get_state(h, &state);
    hb_dict_t *dict = hb_state_to_dict(&state);

    char *json_state = hb_value_get_json(dict);
    hb_value_free(&dict);

    return json_state;
}

hb_dict_t * hb_audio_attributes_to_dict(uint32_t attributes)
{
    json_error_t error;
    hb_dict_t * dict;

    dict = json_pack_ex(&error, 0,
        "{s:o, s:o, s:o, s:o, s:o, s:o}",
        "Normal",           hb_value_bool(attributes & HB_AUDIO_ATTR_NORMAL),
        "VisuallyImpaired", hb_value_bool(attributes &
                                          HB_AUDIO_ATTR_VISUALLY_IMPAIRED),
        "Commentary",       hb_value_bool(attributes &
                                          HB_AUDIO_ATTR_COMMENTARY),
        "AltCommentary",    hb_value_bool(attributes &
                                          HB_AUDIO_ATTR_ALT_COMMENTARY),
        "Secondary",        hb_value_bool(attributes & HB_AUDIO_ATTR_SECONDARY),
        "Default",          hb_value_bool(attributes & HB_AUDIO_ATTR_DEFAULT));
    if (dict == NULL)
    {
        hb_error("hb_audio_attributes_to_dict, json pack failure: %s", error.text);
    }
    return dict;
}

hb_dict_t * hb_subtitle_attributes_to_dict(uint32_t attributes)
{
    json_error_t error;
    hb_dict_t * dict;

    dict = json_pack_ex(&error, 0,
        "{s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o}",
        "Normal",        hb_value_bool(attributes & HB_SUBTITLE_ATTR_NORMAL),
        "Large",         hb_value_bool(attributes & HB_SUBTITLE_ATTR_LARGE),
        "Children",      hb_value_bool(attributes & HB_SUBTITLE_ATTR_CHILDREN),
        "ClosedCaption", hb_value_bool(attributes & HB_SUBTITLE_ATTR_CC),
        "Forced",        hb_value_bool(attributes & HB_SUBTITLE_ATTR_FORCED),
        "Commentary",    hb_value_bool(attributes &
                                       HB_SUBTITLE_ATTR_COMMENTARY),
        "4By3",          hb_value_bool(attributes & HB_SUBTITLE_ATTR_4_3),
        "Wide",          hb_value_bool(attributes & HB_SUBTITLE_ATTR_WIDE),
        "Letterbox",     hb_value_bool(attributes & HB_SUBTITLE_ATTR_LETTERBOX),
        "PanScan",       hb_value_bool(attributes & HB_SUBTITLE_ATTR_PANSCAN),
        "Default",       hb_value_bool(attributes & HB_SUBTITLE_ATTR_DEFAULT));
    if (dict == NULL)
    {
        hb_error("hb_subtitle_attributes_to_dict, json pack failure: %s", error.text);
    }
    return dict;
}

static hb_dict_t* hb_title_to_dict_internal( hb_title_t *title )
{
    hb_dict_t *dict;
    json_error_t error;
    int ii;

    if (title == NULL)
        return NULL;

    int h_shift, v_shift;
    int chroma_available = hb_get_chroma_sub_sample(title->pix_fmt, &h_shift, &v_shift);
    char *chroma_subsampling = NULL;

    if (chroma_available == 0)
    {
        int h_value = 4 >> h_shift;
        int v_value = v_shift ? 0 : h_value;
        chroma_subsampling = hb_strdup_printf("4:%d:%d", h_value, v_value);
    }

    dict = json_pack_ex(&error, 0,
    "{"
        // Type, Path, Name, Index, KeepDuplicateTitles, Playlist, AngleCount
        "s:o, s:o, s:o, s:o, s:o, s:o, s:o,"
        // Duration {Ticks, Hours, Minutes, Seconds}
        "s:{s:o, s:o, s:o, s:o},"
        // Geometry {Width, Height, PAR {Num, Den},
        "s:{s:o, s:o, s:{s:o, s:o}},"
        // Crop[Top, Bottom, Left, Right]}
        "s:[oooo],"
        // LooseCrop[Top, Bottom, Left, Right]}
        "s:[oooo],"
        // Color {Format, Range, Primary, Transfer, Matrix, ChromaLocation, ChromaSubsampling, BitDepth}
        "s:{s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o},"
        // FrameRate {Num, Den}
        "s:{s:o, s:o},"
        // InterlaceDetected, VideoCodec
        "s:o, s:o,"
        // Metadata
        "s:o"
    "}",
    "Type",                 hb_value_int(title->type),
    "Path",                 hb_value_string(title->path),
    "Name",                 hb_value_string(title->name),
    "Index",                hb_value_int(title->index),
    "KeepDuplicateTitles",  hb_value_bool(title->keep_duplicate_titles),
    "Playlist",             hb_value_int(title->playlist),
    "AngleCount",           hb_value_int(title->angle_count),
    "Duration",
        "Ticks",            hb_value_int(title->duration),
        "Hours",            hb_value_int(title->hours),
        "Minutes",          hb_value_int(title->minutes),
        "Seconds",          hb_value_int(title->seconds),
    "Geometry",
        "Width",            hb_value_int(title->geometry.width),
        "Height",           hb_value_int(title->geometry.height),
        "PAR",
            "Num",          hb_value_int(title->geometry.par.num),
            "Den",          hb_value_int(title->geometry.par.den),
    "Crop",                 hb_value_int(title->crop[0]),
                            hb_value_int(title->crop[1]),
                            hb_value_int(title->crop[2]),
                            hb_value_int(title->crop[3]),
    "LooseCrop",            hb_value_int(title->loose_crop[0]),
                            hb_value_int(title->loose_crop[1]),
                            hb_value_int(title->loose_crop[2]),
                            hb_value_int(title->loose_crop[3]),
    "Color",
        "Format",           hb_value_int(title->pix_fmt),
        "Range",            hb_value_int(title->color_range),
        "Primary",          hb_value_int(title->color_prim),
        "Transfer",         hb_value_int(title->color_transfer),
        "Matrix",           hb_value_int(title->color_matrix),
        "ChromaLocation",   hb_value_int(title->chroma_location),
        "ChromaSubsampling", hb_value_string(chroma_subsampling ? chroma_subsampling : "unknown"),
        "BitDepth",          hb_value_int(hb_get_bit_depth(title->pix_fmt)),
    "FrameRate",
        "Num",              hb_value_int(title->vrate.num),
        "Den",              hb_value_int(title->vrate.den),
    "InterlaceDetected",    hb_value_bool(title->detected_interlacing),
    "VideoCodec",           hb_value_string(title->video_codec_name),
    "Metadata",             hb_value_dup(title->metadata->dict)
    );
    if (dict == NULL)
    {
        hb_error("hb_title_to_dict_internal, json pack failure: %s", error.text);
        free(chroma_subsampling);
        return NULL;
    }

    free(chroma_subsampling);

    // Mastering Display Color Volume metadata
    hb_dict_t *mastering_dict;
    if (title->mastering.has_primaries || title->mastering.has_luminance)
    {
        mastering_dict = json_pack_ex(&error, 0,
        "{"
        // DisplayPrimaries[3][2]
        "s:[[[ii],[ii]],[[ii],[ii]],[[ii],[ii]]],"
        // WhitePoint[2],
        "s:[[i,i],[i,i]],"
        // MinLuminance, MaxLuminance, HasPrimaries, HasLuminance
        "s:[i,i],s:[i,i],s:b,s:b"
        "}",
            "DisplayPrimaries", title->mastering.display_primaries[0][0].num,
                                title->mastering.display_primaries[0][0].den,
                                title->mastering.display_primaries[0][1].num,
                                title->mastering.display_primaries[0][1].den,
                                title->mastering.display_primaries[1][0].num,
                                title->mastering.display_primaries[1][0].den,
                                title->mastering.display_primaries[1][1].num,
                                title->mastering.display_primaries[1][1].den,
                                title->mastering.display_primaries[2][0].num,
                                title->mastering.display_primaries[2][0].den,
                                title->mastering.display_primaries[2][1].num,
                                title->mastering.display_primaries[2][1].den,
            "WhitePoint", title->mastering.white_point[0].num,
                          title->mastering.white_point[0].den,
                          title->mastering.white_point[1].num,
                          title->mastering.white_point[1].den,
            "MinLuminance", title->mastering.min_luminance.num,
                            title->mastering.min_luminance.den,
            "MaxLuminance", title->mastering.max_luminance.num,
                            title->mastering.max_luminance.den,
            "HasPrimaries", title->mastering.has_primaries,
            "HasLuminance", title->mastering.has_luminance
        );
        hb_dict_set(dict, "MasteringDisplayColorVolume", mastering_dict);
    }

    // Content Light Level metadata
    hb_dict_t *coll_dict;
    if (title->coll.max_cll && title->coll.max_fall)
    {
        coll_dict = json_pack_ex(&error, 0, "{s:i, s:i}",
            "MaxCLL",  title->coll.max_cll,
            "MaxFALL", title->coll.max_fall);
        hb_dict_set(dict, "ContentLightLevel", coll_dict);
    }

    // Dolby Vision Configuration Record
    hb_dict_t *dovi_dict;
    if (title->dovi.dv_profile)
    {
        dovi_dict = json_pack_ex(&error, 0, "{s:i, s:i, s:i, s:i, s:i, s:i, s:i, s:i}",
            "DVVersionMajor",          title->dovi.dv_version_major,
            "DVVersionMinor",          title->dovi.dv_version_minor,
            "DVProfile",               title->dovi.dv_profile,
            "DVLevel",                 title->dovi.dv_level,
            "RPUPresentFlag",          title->dovi.rpu_present_flag,
            "ELPresentFlag",           title->dovi.el_present_flag,
            "BLPresentFlag",           title->dovi.bl_present_flag,
            "BLSignalCompatibilityId", title->dovi.dv_bl_signal_compatibility_id);
        hb_dict_set(dict, "DolbyVisionConfigurationRecord", dovi_dict);
    }

    // HDR10+ Flag
    if (title->hdr_10_plus)
    {
        hb_dict_set(dict, "HDR10+", hb_value_int(title->hdr_10_plus));
    }

    if (title->container_name != NULL)
    {
        hb_dict_set(dict, "Container", hb_value_string(title->container_name));
    }

    // process chapter list
    hb_dict_t * chapter_list = hb_value_array_init();
    for (ii = 0; ii < hb_list_count(title->list_chapter); ii++)
    {
        hb_dict_t *chapter_dict;
        char *name = "";
        hb_chapter_t *chapter = hb_list_item(title->list_chapter, ii);
        if (chapter->title != NULL)
            name = chapter->title;

        chapter_dict = json_pack_ex(&error, 0,
            "{s:o, s:{s:o, s:o, s:o, s:o}}",
            "Name",         hb_value_string(name),
            "Duration",
                "Ticks",    hb_value_int(chapter->duration),
                "Hours",    hb_value_int(chapter->hours),
                "Minutes",  hb_value_int(chapter->minutes),
                "Seconds",  hb_value_int(chapter->seconds)
        );
        if (chapter_dict == NULL)
        {
            hb_error("hb_title_to_dict_internal, chapter, json pack failure: %s", error.text);
            return NULL;
        }
        hb_value_array_append(chapter_list, chapter_dict);
    }
    hb_dict_set(dict, "ChapterList", chapter_list);

    // process audio list
    hb_dict_t * audio_list = hb_value_array_init();
    for (ii = 0; ii < hb_list_count(title->list_audio); ii++)
    {
        const char * codec_name;
        char         channel_layout_name[256];
        int          channel_count, lfe_count;
        hb_dict_t  * audio_dict, * attributes;
        hb_audio_t * audio = hb_list_item(title->list_audio, ii);

        codec_name = hb_audio_decoder_get_name(audio->config.in.codec,
                                               audio->config.in.codec_param);
        hb_layout_get_name(audio->config.in.ch_layout,
                           channel_layout_name, sizeof(channel_layout_name));
        channel_count = hb_layout_get_discrete_channel_count(
                                     audio->config.in.ch_layout);
        lfe_count     = hb_layout_get_low_freq_channel_count(
                                     audio->config.in.ch_layout);

        attributes = hb_audio_attributes_to_dict(audio->config.lang.attributes);
        audio_dict = json_pack_ex(&error, 0,
        "{s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o}",
            "TrackNumber",       hb_value_int(ii + 1),
            "Description",       hb_value_string(audio->config.lang.description),
            "Language",          hb_value_string(audio->config.lang.simple),
            "LanguageCode",      hb_value_string(audio->config.lang.iso639_2),
            "Attributes",        attributes,
            "Codec",             hb_value_int(audio->config.in.codec),
            "CodecParam",        hb_value_int(audio->config.in.codec_param),
            "CodecName",         hb_value_string(codec_name),
            "SampleRate",        hb_value_int(audio->config.in.samplerate),
            "BitRate",           hb_value_int(audio->config.in.bitrate),
            "ChannelLayout",     hb_value_string(channel_layout_name),
            "ChannelCount",      hb_value_int(channel_count),
            "LFECount",          hb_value_int(lfe_count));
        if (audio_dict == NULL)
        {
            hb_error("hb_title_to_dict_internal, audio, json pack failure: %s", error.text);
            return NULL;
        }
        if (audio->config.in.name != NULL)
        {
            hb_dict_set_string(audio_dict, "Name", audio->config.in.name);
        }
        hb_value_array_append(audio_list, audio_dict);
    }
    hb_dict_set(dict, "AudioList", audio_list);

    // process subtitle list
    hb_value_array_t * subtitle_list = hb_value_array_init();
    for (ii = 0; ii < hb_list_count(title->list_subtitle); ii++)
    {
        const char    * format;
        hb_dict_t     * subtitle_dict, * attributes;
        hb_subtitle_t * subtitle = hb_list_item(title->list_subtitle, ii);

        format = subtitle->format == PICTURESUB ? "bitmap" : "text";
        attributes = hb_subtitle_attributes_to_dict(subtitle->attributes);
        subtitle_dict = json_pack_ex(&error, 0,
            "{s:o, s:o, s:o, s:o, s:o, s:o, s:o}",
            "TrackNumber",  hb_value_int(ii + 1),
            "Format",       hb_value_string(format),
            "Source",       hb_value_int(subtitle->source),
            "SourceName",   hb_value_string(hb_subsource_name(subtitle->source)),
            "Attributes",   attributes,
            "Language",     hb_value_string(subtitle->lang),
            "LanguageCode", hb_value_string(subtitle->iso639_2));
        if (subtitle_dict == NULL)
        {
            hb_error("hb_title_to_dict_internal, subtitle, json pack failure: %s", error.text);
            return NULL;
        }
        if (subtitle->name != NULL)
        {
            hb_dict_set_string(subtitle_dict, "Name", subtitle->name);
        }
        hb_value_array_append(subtitle_list, subtitle_dict);
    }
    hb_dict_set(dict, "SubtitleList", subtitle_list);

    // process cover arts
    if (title->metadata && title->metadata->list_coverart)
    {
        hb_value_array_t *art_array = hb_value_array_init();
        for (ii = 0; ii < hb_list_count(title->metadata->list_coverart); ii++)
        {
            hb_coverart_t *art = hb_list_item(title->metadata->list_coverart, ii);
            hb_dict_t *coverart_dict = json_pack_ex(&error, 0,
                                                    "{s:o, s:o, s:o}",
                                                    "ID", hb_value_int(ii),
                                                    "Name",  hb_value_string(art->name),
                                                    "Type",  hb_value_int(art->type));
            if (coverart_dict)
            {
                hb_value_array_append(art_array, coverart_dict);
            }
        }
        hb_dict_set(dict, "CoverArts", art_array);
    }

    return dict;
}

/**
 * Convert an hb_title_t to a jansson dict
 * @param title - Pointer to the hb_title_t to convert
 */
hb_dict_t* hb_title_to_dict( hb_handle_t *h, int title_index )
{
    hb_title_t *title = hb_find_title_by_index(h, title_index);
    return hb_title_to_dict_internal(title);
}

/**
 * Convert an hb_title_set_t to a jansson dict
 * @param title - Pointer to the hb_title_set_t to convert
 */
hb_dict_t* hb_title_set_to_dict( const hb_title_set_t * title_set )
{
    hb_dict_t *dict;
    json_error_t error;
    int ii;

    dict = json_pack_ex(&error, 0,
        "{s:o, s:[]}",
        "MainFeature", hb_value_int(title_set->feature),
        "TitleList");
    // process title list
    hb_dict_t *title_list = hb_dict_get(dict, "TitleList");
    for (ii = 0; ii < hb_list_count(title_set->list_title); ii++)
    {
        hb_title_t *title = hb_list_item(title_set->list_title, ii);
        hb_dict_t *title_dict = hb_title_to_dict_internal(title);
        hb_value_array_append(title_list, title_dict);
    }

    return dict;
}

/**
 * Convert an hb_title_t to a json string
 * @param title - Pointer to hb_title_t to convert
 */
char* hb_title_to_json( hb_handle_t *h, int title_index )
{
    hb_dict_t *dict = hb_title_to_dict(h, title_index);
    if (dict == NULL)
        return NULL;

    char *json_title = hb_value_get_json(dict);
    hb_value_free(&dict);

    return json_title;
}

/**
 * Get the current title set of an hb instance as a json string
 * @param h - Pointer to hb_handle_t hb instance
 */
char* hb_get_title_set_json( hb_handle_t * h )
{
    hb_dict_t *dict = hb_title_set_to_dict(hb_get_title_set(h));

    char *json_title_set = hb_value_get_json(dict);
    hb_value_free(&dict);

    return json_title_set;
}

/**
 * Convert an hb_job_t to an hb_dict_t
 * @param job - Pointer to the hb_job_t to convert
 */
hb_dict_t* hb_job_to_dict( const hb_job_t * job )
{
    hb_dict_t * dict;
    json_error_t error;
    int subtitle_search_burn;
    int ii;

    if (job == NULL || job->title == NULL)
        return NULL;

    // Assumes that the UI has reduced geometry settings to only the
    // necessary PAR value

    subtitle_search_burn = job->select_subtitle_config.dest == RENDERSUB;
    dict = json_pack_ex(&error, 0,
    "{"
    // SequenceID
    "s:o,"
    // Destination {Mux, InlineParameterSets, AlignAVStart,
    //              ChapterMarkers, ChapterList}
    "s:{s:o, s:o, s:o, s:o, s:[]},"
    // Source {Path, Title, Angle, HWDecode, KeepDuplicateTitles}
    "s:{s:o, s:o, s:o, s:o, s:o},"
    // PAR {Num, Den}
    "s:{s:o, s:o},"
    // Video {Encoder, HardwareDecode, AdapterIndex, AsyncDepth}
    "s:{s:o, s:o, s:o, s:o},"
    // Audio {CopyMask, FallbackEncoder, AudioList []}
    "s:{s:[], s:o, s:[]},"
    // Subtitles {Search {Enable, Forced, Default, Burn}, SubtitleList []}
    "s:{s:{s:o, s:o, s:o, s:o}, s:[]},"
    // Metadata
    "s:o,"
    // Filters {FilterList []}
    "s:{s:[]}"
    "}",
        "SequenceID",           hb_value_int(job->sequence_id),
        "Destination",
            "Mux",              hb_value_int(job->mux),
            "InlineParameterSets", hb_value_bool(job->inline_parameter_sets),
            "AlignAVStart",     hb_value_bool(job->align_av_start),
            "ChapterMarkers",   hb_value_bool(job->chapter_markers),
            "ChapterList",
        "Source",
            "Path",             hb_value_string(job->title->path),
            "Title",            hb_value_int(job->title->index),
            "Angle",            hb_value_int(job->angle),
            "HWDecode",         hb_value_int(job->hw_decode),
            "KeepDuplicateTitles", hb_value_bool(job->keep_duplicate_titles),
        "PAR",
            "Num",              hb_value_int(job->par.num),
            "Den",              hb_value_int(job->par.den),
        "Video",
            "Encoder",          hb_value_int(job->vcodec),
            "HardwareDecode",   hb_value_int(job->hw_decode),
            "AdapterIndex",     hb_value_int(job->hw_device_index),
            "AsyncDepth",       hb_value_int(job->hw_device_async_depth),
        "Audio",
            "CopyMask",
            "FallbackEncoder",  hb_value_int(job->acodec_fallback),
            "AudioList",
        "Subtitle",
            "Search",
                "Enable",       hb_value_bool(job->indepth_scan),
                "Forced",       hb_value_bool(job->select_subtitle_config.force),
                "Default",      hb_value_bool(job->select_subtitle_config.default_track),
                "Burn",         hb_value_bool(subtitle_search_burn),
            "SubtitleList",
        "Metadata",             hb_value_dup(job->metadata->dict),
        "Filters",
            "FilterList"
    );
    if (dict == NULL)
    {
        hb_error("hb_job_to_dict, json pack failure: %s", error.text);
        return NULL;
    }
    hb_dict_t *dest_dict = hb_dict_get(dict, "Destination");
    if (job->file != NULL)
    {
        hb_dict_set(dest_dict, "File", hb_value_string(job->file));
    }
    if (job->mux)
    {
        hb_dict_t *options_dict;
        options_dict = json_pack_ex(&error, 0, "{s:o, s:o}",
            "Optimize",         hb_value_bool(job->optimize),
            "IpodAtom",         hb_value_bool(job->ipod_atom));
        hb_dict_set(dest_dict, "Options", options_dict);
    }
    hb_dict_t *source_dict = hb_dict_get(dict, "Source");
    hb_dict_t *range_dict;
    if (job->start_at_preview > 0)
    {
        range_dict = json_pack_ex(&error, 0, "{s:o, s:o, s:o, s:o}",
            "Type",       hb_value_string("preview"),
            "Start",      hb_value_int(job->start_at_preview),
            "End",        hb_value_int(job->pts_to_stop),
            "SeekPoints", hb_value_int(job->seek_points));
    }
    else if (job->pts_to_start != 0 || job->pts_to_stop != 0)
    {
        range_dict = hb_dict_init();
        hb_dict_set(range_dict, "Type", hb_value_string("time"));
        if (job->pts_to_start > 0)
        {
            hb_dict_set(range_dict, "Start", hb_value_int(job->pts_to_start));
        }
        if (job->pts_to_stop > 0)
        {
            hb_dict_set(range_dict, "End",
                        hb_value_int(job->pts_to_start + job->pts_to_stop));
        }
    }
    else if (job->frame_to_start != 0 || job->frame_to_stop != 0)
    {
        range_dict = hb_dict_init();
        hb_dict_set(range_dict, "Type", hb_value_string("frame"));
        if (job->frame_to_start > 0)
        {
            hb_dict_set(range_dict, "Start",
                        hb_value_int(job->frame_to_start));
        }
        if (job->frame_to_stop > 0)
        {
            hb_dict_set(range_dict, "End",
                        hb_value_int(job->frame_to_start + job->frame_to_stop - 1));
        }
    }
    else
    {
        range_dict = json_pack_ex(&error, 0, "{s:o, s:o, s:o}",
            "Type",  hb_value_string("chapter"),
            "Start", hb_value_int(job->chapter_start),
            "End",   hb_value_int(job->chapter_end));
    }
    hb_dict_set(source_dict, "Range", range_dict);

    hb_dict_t *video_dict = hb_dict_get(dict, "Video");
    hb_dict_set(video_dict, "ColorInputFormat",
                hb_value_int(job->input_pix_fmt));
    hb_dict_set(video_dict, "ColorOutputFormat",
                hb_value_int(job->output_pix_fmt));
    hb_dict_set(video_dict, "ColorRange",
                hb_value_int(job->color_range));
    hb_dict_set(video_dict, "ColorPrimaries",
                hb_value_int(job->color_prim));
    hb_dict_set(video_dict, "ColorTransfer",
                hb_value_int(job->color_transfer));
    hb_dict_set(video_dict, "ColorMatrix",
                hb_value_int(job->color_matrix));
    hb_dict_set(video_dict, "ChromaLocation",
                hb_value_int(job->chroma_location));
    if (job->color_prim_override != HB_COLR_PRI_UNSET)
    {
        hb_dict_set(video_dict, "ColorPrimariesOverride",
                    hb_value_int(job->color_prim_override));
    }
    if (job->color_transfer_override != HB_COLR_TRA_UNSET)
    {
        hb_dict_set(video_dict, "ColorTransferOverride",
                    hb_value_int(job->color_transfer_override));
    }
    if (job->color_matrix_override != HB_COLR_MAT_UNSET)
    {
        hb_dict_set(video_dict, "ColorMatrixOverride",
                    hb_value_int(job->color_matrix_override));
    }

    // Mastering Display Color Volume metadata
    hb_dict_t *mastering_dict;
    if (job->mastering.has_primaries || job->mastering.has_luminance)
    {
        mastering_dict = json_pack_ex(&error, 0,
        "{"
        // DisplayPrimaries[3][2]
        "s:[[[ii],[ii]],[[ii],[ii]],[[ii],[ii]]],"
        // WhitePoint[2],
        "s:[[i,i],[i,i]],"
        // MinLuminance, MaxLuminance, HasPrimaries, HasLuminance
        "s:[i,i],s:[i,i],s:b,s:b"
        "}",
            "DisplayPrimaries", job->mastering.display_primaries[0][0].num,
                                job->mastering.display_primaries[0][0].den,
                                job->mastering.display_primaries[0][1].num,
                                job->mastering.display_primaries[0][1].den,
                                job->mastering.display_primaries[1][0].num,
                                job->mastering.display_primaries[1][0].den,
                                job->mastering.display_primaries[1][1].num,
                                job->mastering.display_primaries[1][1].den,
                                job->mastering.display_primaries[2][0].num,
                                job->mastering.display_primaries[2][0].den,
                                job->mastering.display_primaries[2][1].num,
                                job->mastering.display_primaries[2][1].den,
            "WhitePoint", job->mastering.white_point[0].num,
                          job->mastering.white_point[0].den,
                          job->mastering.white_point[1].num,
                          job->mastering.white_point[1].den,
            "MinLuminance", job->mastering.min_luminance.num,
                            job->mastering.min_luminance.den,
            "MaxLuminance", job->mastering.max_luminance.num,
                            job->mastering.max_luminance.den,
            "HasPrimaries", job->mastering.has_primaries,
            "HasLuminance", job->mastering.has_luminance
        );
        hb_dict_set(video_dict, "MasteringDisplayColorVolume", mastering_dict);
    }

    // Content Light Level metadata
    hb_dict_t *coll_dict;
    if (job->coll.max_cll && job->coll.max_fall)
    {
        coll_dict = json_pack_ex(&error, 0, "{s:i, s:i}",
            "MaxCLL",  job->coll.max_cll,
            "MaxFALL", job->coll.max_fall);
        hb_dict_set(video_dict, "ContentLightLevel", coll_dict);
    }

    // Dolby Vision Configuration Record
    hb_dict_t *dovi_dict;
    if (job->dovi.dv_profile)
    {
        dovi_dict = json_pack_ex(&error, 0, "{s:i, s:i, s:i, s:i, s:i, s:i, s:i, s:i}",
            "DVVersionMajor",          job->dovi.dv_version_major,
            "DVVersionMinor",          job->dovi.dv_version_minor,
            "DVProfile",               job->dovi.dv_profile,
            "DVLevel",                 job->dovi.dv_level,
            "RPUPresentFlag",          job->dovi.rpu_present_flag,
            "ELPresentFlag",           job->dovi.el_present_flag,
            "BLPresentFlag",           job->dovi.bl_present_flag,
            "BLSignalCompatibilityId", job->dovi.dv_bl_signal_compatibility_id);
        hb_dict_set(video_dict, "DolbyVisionConfigurationRecord", dovi_dict);
    }

    if (job->vquality > HB_INVALID_VIDEO_QUALITY)
    {
        hb_dict_set(video_dict, "Quality", hb_value_double(job->vquality));
    }
    else
    {
        hb_dict_set(video_dict, "Bitrate", hb_value_int(job->vbitrate));
        hb_dict_set(video_dict, "MultiPass", hb_value_bool(job->multipass));
        hb_dict_set(video_dict, "Turbo",
                            hb_value_bool(job->fastanalysispass));
    }
    hb_dict_set(video_dict, "PasshtruHDRDynamicMetadata",
                        hb_value_int(job->passthru_dynamic_hdr_metadata));

    if (job->encoder_preset != NULL)
    {
        hb_dict_set(video_dict, "Preset",
                            hb_value_string(job->encoder_preset));
    }
    if (job->encoder_tune != NULL)
    {
        hb_dict_set(video_dict, "Tune", hb_value_string(job->encoder_tune));
    }
    if (job->encoder_profile != NULL)
    {
        hb_dict_set(video_dict, "Profile",
                    hb_value_string(job->encoder_profile));
    }
    if (job->encoder_level != NULL)
    {
        hb_dict_set(video_dict, "Level", hb_value_string(job->encoder_level));
    }
    if (job->encoder_options != NULL)
    {
        hb_dict_set(video_dict, "Options",
                    hb_value_string(job->encoder_options));
    }

    // process chapter list
    hb_dict_t *chapter_list = hb_dict_get(dest_dict, "ChapterList");
    for (ii = 0; ii < hb_list_count(job->list_chapter); ii++)
    {
        hb_dict_t *chapter_dict;
        char *name = "";
        hb_chapter_t *chapter = hb_list_item(job->list_chapter, ii);
        if (chapter->title != NULL)
            name = chapter->title;

        chapter_dict = json_pack_ex(&error, 0,
            "{s:o, s:{s:o, s:o, s:o, s:o}}",
            "Name",         hb_value_string(name),
            "Duration",
                "Ticks",    hb_value_int(chapter->duration),
                "Hours",    hb_value_int(chapter->hours),
                "Minutes",  hb_value_int(chapter->minutes),
                "Seconds",  hb_value_int(chapter->seconds)
        );
        hb_value_array_append(chapter_list, chapter_dict);
    }

    // process filter list
    hb_dict_t *filters_dict = hb_dict_get(dict, "Filters");
    hb_value_array_t *filter_list = hb_dict_get(filters_dict, "FilterList");
    for (ii = 0; ii < hb_list_count(job->list_filter); ii++)
    {
        hb_dict_t *filter_dict;
        hb_filter_object_t *filter = hb_list_item(job->list_filter, ii);

        filter_dict = json_pack_ex(&error, 0, "{s:o}",
                                "ID", hb_value_int(filter->id));
        if (filter->settings != NULL)
        {
            hb_dict_set(filter_dict, "Settings",
                        hb_value_dup(filter->settings));
        }

        hb_value_array_append(filter_list, filter_dict);
    }

    hb_dict_t *audios_dict = hb_dict_get(dict, "Audio");
    // Construct audio CopyMask
    hb_value_array_t *copy_mask = hb_dict_get(audios_dict, "CopyMask");
    int acodec;
    for (acodec = 1; acodec != HB_ACODEC_PASS_FLAG; acodec <<= 1)
    {
        if (acodec & job->acodec_copy_mask)
        {
            const char *name;
            name = hb_audio_encoder_get_name(acodec | HB_ACODEC_PASS_FLAG);
            if (name != NULL)
            {
                hb_value_t *val = hb_value_string(name);
                hb_value_array_append(copy_mask, val);
            }
        }
    }
    // process audio list
    hb_dict_t *audio_list = hb_dict_get(audios_dict, "AudioList");
    for (ii = 0; ii < hb_list_count(job->list_audio); ii++)
    {
        hb_dict_t *audio_dict;
        hb_audio_t *audio = hb_list_item(job->list_audio, ii);

        audio_dict = json_pack_ex(&error, 0,
            "{s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o}",
            "Track",                hb_value_int(audio->config.index),
            "Encoder",              hb_value_int(audio->config.out.codec),
            "Gain",                 hb_value_double(audio->config.out.gain),
            "DRC",                  hb_value_double(audio->config.out.dynamic_range_compression),
            "Mixdown",              hb_value_int(audio->config.out.mixdown),
            "NormalizeMixLevel",    hb_value_bool(audio->config.out.normalize_mix_level),
            "DitherMethod",         hb_value_int(audio->config.out.dither_method),
            "Samplerate",           hb_value_int(audio->config.out.samplerate),
            "Bitrate",              hb_value_int(audio->config.out.bitrate),
            "Quality",              hb_value_double(audio->config.out.quality),
            "CompressionLevel",     hb_value_double(audio->config.out.compression_level));
        if (audio->config.out.name != NULL)
        {
            hb_dict_set_string(audio_dict, "Name", audio->config.out.name);
        }

        hb_value_array_append(audio_list, audio_dict);
    }

    // process subtitle list
    hb_dict_t *subtitles_dict = hb_dict_get(dict, "Subtitle");
    if (job->select_subtitle_config.external_filename != NULL)
    {
        hb_dict_t *search = hb_dict_get(subtitles_dict, "Search");
        hb_dict_set_string(search, "ExternalFilename",
                           job->select_subtitle_config.external_filename);
    }
    hb_dict_t *subtitle_list = hb_dict_get(subtitles_dict, "SubtitleList");
    for (ii = 0; ii < hb_list_count(job->list_subtitle); ii++)
    {
        hb_dict_t *subtitle_dict;
        hb_subtitle_t *subtitle = hb_list_item(job->list_subtitle, ii);

        if (subtitle->source == IMPORTSRT ||
            subtitle->source == IMPORTSSA)
        {
            subtitle_dict = json_pack_ex(&error, 0,
                "{s:o, s:o, s:o, s:{s:o, s:o, s:o}}",
                "Default",  hb_value_bool(subtitle->config.default_track),
                "Burn",     hb_value_bool(subtitle->config.dest == RENDERSUB),
                "Offset",   hb_value_int(subtitle->config.offset),
                "Import",
                    "Format",   hb_value_string(subtitle->source == IMPORTSRT ?
                                                "SRT" : "SSA"),
                    "Filename", hb_value_string(subtitle->config.src_filename),
                    "Language", hb_value_string(subtitle->iso639_2));
            if (subtitle->source == IMPORTSRT)
            {
                hb_dict_t *import_dict = hb_dict_get(subtitle_dict, "Import");
                hb_dict_set(import_dict, "Codeset",
                            hb_value_string(subtitle->config.src_codeset));
            }
        }
        else
        {
            subtitle_dict = json_pack_ex(&error, 0,
            "{s:o, s:o, s:o, s:o, s:o}",
                "Track",    hb_value_int(subtitle->track),
                "Default",  hb_value_bool(subtitle->config.default_track),
                "Forced",   hb_value_bool(subtitle->config.force),
                "Burn",     hb_value_bool(subtitle->config.dest == RENDERSUB),
                "Offset",   hb_value_int(subtitle->config.offset));
        }
        if (subtitle->config.name != NULL)
        {
            hb_dict_set_string(subtitle_dict, "Name", subtitle->config.name);
        }
        if (subtitle->config.external_filename != NULL)
        {
            hb_dict_set_string(subtitle_dict, "ExternalFilename",
                               subtitle->config.external_filename);
        }
        hb_value_array_append(subtitle_list, subtitle_dict);
    }

    // process cover arts
    if (job->metadata && job->metadata->list_coverart)
    {
        hb_value_array_t *art_array = hb_value_array_init();
        for (ii = 0; ii < hb_list_count(job->metadata->list_coverart); ii++)
        {
            hb_coverart_t *art = hb_list_item(job->metadata->list_coverart, ii);
            hb_dict_t *art_dict = json_pack_ex(&error, 0,
                                               "{s:o, s:o, s:o}",
                                               "ID", hb_value_int(ii),
                                               "Name",  hb_value_string(art->name),
                                               "Type",  hb_value_int(art->type));
            if (art_dict)
            {
                hb_value_array_append(art_array, art_dict);
            }
        }
        hb_dict_set(dict, "CoverArts", art_array);
    }

    return dict;
}

/**
 * Convert an hb_job_t to a json string
 * @param job - Pointer to the hb_job_t to convert
 */
char* hb_job_to_json( const hb_job_t * job )
{
    hb_dict_t *dict = hb_job_to_dict(job);

    if (dict == NULL)
        return NULL;

    char *json_job = hb_value_get_json(dict);
    hb_value_free(&dict);

    return json_job;
}

// These functions exist only to perform type checking when using
// json_unpack_ex().
typedef const char * const_str_t;

static double*      unpack_f(double *f)     { return f; }
static int*         unpack_i(int *i)        { return i; }
static unsigned*    unpack_u(unsigned *u)   { return u; }
static json_int_t*  unpack_I(json_int_t *i) { return i; }
static int *        unpack_b(int *b)        { return b; }
static const_str_t* unpack_s(const_str_t *s){ return s; }
static json_t**     unpack_o(json_t** o)    { return o; }

void hb_json_job_scan( hb_handle_t * h, const char * json_job )
{
    hb_dict_t * dict;
    int result;
    json_error_t error;

    dict = hb_value_json(json_job);

    int title_index, hw_decode, keep_duplicate_titles;
    const char *path = NULL;

    result = json_unpack_ex(dict, &error, 0, "{s:{s:s, s:i, s?i, s?b}}",
                            "Source",
                                "Path",     unpack_s(&path),
                                "Title",    unpack_i(&title_index),
                                "HWDecode", unpack_i(&hw_decode),
                                "KeepDuplicateTitles", unpack_b(&keep_duplicate_titles)
                           );
    if (result < 0)
    {
        hb_error("json unpack failure, failed to find title: %s", error.text);
        hb_value_free(&dict);
        return;
    }

    // If the job wants to use Hardware decode, it must also be
    // enabled during scan.  So enable it here.
    hb_list_t *file_paths = hb_list_init();
    hb_list_add(file_paths, (char *)path);
    hb_scan(h, file_paths, title_index, -1, 0, 0, 0, 0, 0, NULL, hw_decode, keep_duplicate_titles);
    hb_list_close(&file_paths);

    // Wait for scan to complete
    hb_state_t state;
    hb_get_state2(h, &state);
    while (state.state == HB_STATE_SCANNING)
    {
        hb_snooze(50);
        hb_get_state2(h, &state);
    }
    hb_value_free(&dict);
}

static int validate_audio_codec_mux(int codec, int mux, int track)
{
    const hb_encoder_t *enc = NULL;
    while ((enc = hb_audio_encoder_get_next(enc)) != NULL)
    {
        if ((enc->codec == codec) && (enc->muxers & mux) == 0)
        {
            if (codec != HB_ACODEC_NONE)
            {
                hb_error("track %d: incompatible encoder '%s' for muxer '%s'",
                         track + 1, enc->short_name,
                         hb_container_get_short_name(mux));
            }
            return -1;
        }
    }
    return 0;
}

/**
 * Convert a json string representation of a job to an hb_job_t
 * @param h        - Pointer to the hb_handle_t hb instance which contains the
 *                   title that the job refers to.
 * @param json_job - Pointer to json string representation of a job
 */
hb_job_t* hb_dict_to_job( hb_handle_t * h, hb_dict_t *dict )
{
    hb_job_t * job;
    int result;
    json_error_t error;
    int titleindex;

    if (dict == NULL)
        return NULL;

    result = json_unpack_ex(dict, &error, 0, "{s:{s:i}}",
                            "Source", "Title", unpack_i(&titleindex));
    if (result < 0)
    {
        hb_error("hb_dict_to_job: failed to find title: %s", error.text);
        return NULL;
    }

    job = hb_job_init_by_index(h, titleindex);
    if (job == NULL)
    {
        hb_error("hb_dict_to_job: Title %d doesn't exist", titleindex);
        return NULL;
    }

    hb_value_array_t * chapter_list = NULL;
    hb_value_array_t * audio_list = NULL;
    hb_value_array_t * subtitle_list = NULL;
    hb_value_array_t * filter_list = NULL;
    hb_value_t       * mux = NULL, * vcodec = NULL;
    hb_dict_t        * mastering_dict = NULL;
    hb_dict_t        * coll_dict = NULL;
    hb_dict_t        * dovi_dict = NULL;
    hb_value_t       * acodec_copy_mask = NULL, * acodec_fallback = NULL;
    const char       * destfile = NULL;
    const char       * range_type = NULL;
    const char       * video_preset = NULL, * video_tune = NULL;
    const char       * video_profile = NULL, * video_level = NULL;
    const char       * video_options = NULL;
    int                passthru_dynamic_hdr_metadata = -1;
    int                subtitle_search_burn = 0;
    const char       * subtitle_search_external_filename = NULL;
    json_int_t         range_start = -1, range_end = -1, range_seek_points = -1;
    int                vbitrate = -1;
    double             vquality = HB_INVALID_VIDEO_QUALITY;
    hb_dict_t        * meta_dict = NULL;
    hb_value_array_t * art_array = NULL;

    result = json_unpack_ex(dict, &error, 0,
    "{"
    // SequenceID
    "s:i,"
    // Destination {File, Mux, InlineParameterSets, AlignAVStart,
    //              ChapterMarkers, ChapterList,
    //              Options {Optimize, IpodAtom}}
    "s:{s?s, s:o, s?b, s?b, s:b, s?o s?{s?b, s?b}},"
    // Source {Angle, KeepDuplicateTitles, Range {Type, Start, End, SeekPoints}}
    "s:{s?i, s?b, s?{s:s, s?I, s?I, s?I}},"
    // PAR {Num, Den}
    "s?{s:i, s:i},"
    // Video {Codec, Quality, Bitrate, Preset, Tune, Profile, Level, Options
    //       MultiPass, Turbo, PasshtruHDRDynamicMetadata
    //       ColorInputFormat, ColorOutputFormat, ColorRange,
    //       ColorPrimaries, ColorTransfer, ColorMatrix, ChromaLocation,
    //       MasteringDisplayColorVolume,
    //       ContentLightLevel,
    //       DolbyVisionConfigurationRecord
    //       ColorPrimariesOverride, ColorTransferOverride, ColorMatrixOverride,
    //       HardwareDecode, AdapterIndex, AsyncDepth
    "s:{s:o, s?F, s?i, s?s, s?s, s?s, s?s, s?s,"
    "   s?b, s?b, s?i,"
    "   s?i, s?i, s?i,"
    "   s?i, s?i, s?i, s?i,"
    "   s?o,"
    "   s?o,"
    "   s?o,"
    "   s?i, s?i, s?i,"
    "   s?i, s?i, s?i},"
    // Audio {CopyMask, FallbackEncoder, AudioList}
    "s?{s?o, s?o, s?o},"
    // Subtitle {Search {Enable, Forced, Default, Burn, ExternalFilename}, SubtitleList}
    "s?{s?{s:b, s?b, s?b, s?b, s?s}, s?o},"
    // Metadata
    "s?o,"
    // Cover arts
    "s?o,"
    // Filters {FilterList}
    "s?{s?o}"
    "}",
        "SequenceID",               unpack_i(&job->sequence_id),
        "Destination",
            "File",                 unpack_s(&destfile),
            "Mux",                  unpack_o(&mux),
            "InlineParameterSets",  unpack_b(&job->inline_parameter_sets),
            "AlignAVStart",         unpack_b(&job->align_av_start),
            "ChapterMarkers",       unpack_b(&job->chapter_markers),
            "ChapterList",          unpack_o(&chapter_list),
            "Options",
                "Optimize",         unpack_b(&job->optimize),
                "IpodAtom",         unpack_b(&job->ipod_atom),
        "Source",
            "Angle",                unpack_i(&job->angle),
            "KeepDuplicateTitles",  unpack_b(&job->keep_duplicate_titles),
            "Range",
                "Type",             unpack_s(&range_type),
                "Start",            unpack_I(&range_start),
                "End",              unpack_I(&range_end),
                "SeekPoints",       unpack_I(&range_seek_points),
        "PAR",
            "Num",                  unpack_i(&job->par.num),
            "Den",                  unpack_i(&job->par.den),
        "Video",
            "Encoder",              unpack_o(&vcodec),
            "Quality",              unpack_f(&vquality),
            "Bitrate",              unpack_i(&vbitrate),
            "Preset",               unpack_s(&video_preset),
            "Tune",                 unpack_s(&video_tune),
            "Profile",              unpack_s(&video_profile),
            "Level",                unpack_s(&video_level),
            "Options",              unpack_s(&video_options),
            "MultiPass",            unpack_b(&job->multipass),
            "Turbo",                unpack_b(&job->fastanalysispass),
            "PasshtruHDRDynamicMetadata", unpack_i(&passthru_dynamic_hdr_metadata),
            "ColorInputFormat",     unpack_i(&job->input_pix_fmt),
            "ColorOutputFormat",    unpack_i(&job->output_pix_fmt),
            "ColorRange",           unpack_i(&job->color_range),
            "ColorPrimaries",       unpack_i(&job->color_prim),
            "ColorTransfer",        unpack_i(&job->color_transfer),
            "ColorMatrix",          unpack_i(&job->color_matrix),
            "ChromaLocation",       unpack_i(&job->chroma_location),
            "MasteringDisplayColorVolume", unpack_o(&mastering_dict),
            "ContentLightLevel",    unpack_o(&coll_dict),
            "DolbyVisionConfigurationRecord", unpack_o(&dovi_dict),
            "ColorPrimariesOverride", unpack_i(&job->color_prim_override),
            "ColorTransferOverride",  unpack_i(&job->color_transfer_override),
            "ColorMatrixOverride",    unpack_i(&job->color_matrix_override),
            "HardwareDecode",         unpack_i(&job->hw_decode),
            "AdapterIndex",           unpack_i(&job->hw_device_index),
            "AsyncDepth",             unpack_i(&job->hw_device_async_depth),
        "Audio",
            "CopyMask",             unpack_o(&acodec_copy_mask),
            "FallbackEncoder",      unpack_o(&acodec_fallback),
            "AudioList",            unpack_o(&audio_list),
        "Subtitle",
            "Search",
                "Enable",           unpack_b(&job->indepth_scan),
                "Forced",           unpack_b(&job->select_subtitle_config.force),
                "Default",          unpack_b(&job->select_subtitle_config.default_track),
                "Burn",             unpack_b(&subtitle_search_burn),
                "ExternalFilename", unpack_s(&subtitle_search_external_filename),
            "SubtitleList",         unpack_o(&subtitle_list),
        "Metadata",                 unpack_o(&meta_dict),
        "CoverArts",                unpack_o(&art_array),
        "Filters",
            "FilterList",           unpack_o(&filter_list)
    );
    if (result < 0)
    {
        hb_error("hb_dict_to_job: failed to parse dict: %s", error.text);
        goto fail;
    }
    if (meta_dict != NULL)
    {
        hb_value_free(&job->metadata->dict);
        job->metadata->dict = hb_value_dup(meta_dict);
    }
    if (art_array != NULL)
    {
        if (hb_value_type(art_array) == HB_VALUE_TYPE_ARRAY)
        {
            int count = hb_value_array_len(art_array);
            for (int ii = hb_list_count(job->metadata->list_coverart) - 1; ii >= 0; ii--)
            {
                int found = 0;
                for (int jj = count; jj >= 0; jj--)
                {
                    hb_dict_t *art_dict = hb_value_array_get(art_array, jj);
                    if (art_dict)
                    {
                        int index = hb_dict_get_int(art_dict, "ID");
                        if (index == ii)
                        {
                            found = 1;
                            break;
                        }
                    }
                }

                if (found == 0)
                {
                    hb_metadata_rem_coverart(job->metadata, ii);
                }
            }
        }
    }
    // Lookup mux id
    if (hb_value_type(mux) == HB_VALUE_TYPE_STRING)
    {
        const char *s = hb_value_get_string(mux);
        job->mux = hb_container_get_from_name(s);
        if (job->mux == 0)
            job->mux = hb_container_get_from_extension(s);
    }
    else
    {
        job->mux = hb_value_get_int(mux);
    }

    // Lookup video codec
    if (hb_value_type(vcodec) == HB_VALUE_TYPE_STRING)
    {
        const char *s = hb_value_get_string(vcodec);
        job->vcodec = hb_video_encoder_get_from_name(s);
    }
    else
    {
        job->vcodec = hb_value_get_int(vcodec);
    }

    if (range_type != NULL)
    {
        if (!strcasecmp(range_type, "preview"))
        {
            if (range_start >= 0)
                job->start_at_preview = range_start;
            if (range_end >= 0)
                job->pts_to_stop = range_end;
            if (range_seek_points >= 0)
                job->seek_points = range_seek_points;
        }
        else if (!strcasecmp(range_type, "chapter"))
        {
            if (range_start >= 0)
                job->chapter_start = range_start;
            if (range_end >= 0)
                job->chapter_end = range_end;
        }
        else if (!strcasecmp(range_type, "time"))
        {
            if (range_start >= 0)
                job->pts_to_start = range_start;
            if (range_end >= 0)
                job->pts_to_stop = range_end - job->pts_to_start;
        }
        else if (!strcasecmp(range_type, "frame"))
        {
            if (range_start > 0)
                job->frame_to_start = range_start;
            if (range_end > 0)
                job->frame_to_stop = range_end - job->frame_to_start + 1;
        }
    }

    if (passthru_dynamic_hdr_metadata > -1)
    {
        job->passthru_dynamic_hdr_metadata = passthru_dynamic_hdr_metadata;
    }

    if (destfile != NULL && destfile[0] != 0)
    {
        hb_job_set_file(job, destfile);
    }

    hb_job_set_encoder_preset(job, video_preset);
    hb_job_set_encoder_tune(job, video_tune);
    hb_job_set_encoder_profile(job, video_profile);
    hb_job_set_encoder_level(job, video_level);
    hb_job_set_encoder_options(job, video_options);

    // If both vbitrate and vquality were specified, vbitrate is used;
    // we need to ensure the unused rate control mode is always set to an
    // invalid value, as if both values are valid, behavior is undefined
    // (some encoders first check for a valid vquality, whereas others
    //  check for a valid vbitrate instead)
    if (vbitrate > 0)
    {
        job->vbitrate = vbitrate;
        job->vquality = HB_INVALID_VIDEO_QUALITY;
    }
    else if (vquality > HB_INVALID_VIDEO_QUALITY)
    {
        job->vbitrate = -1;
        job->vquality = vquality;
    }
    // If neither were specified, defaults are used (set in job_setup())

    if (mastering_dict != NULL)
    {
        result = json_unpack_ex(mastering_dict, &error, 0,
        "{"
        // DisplayPrimaries[3][2]
        "s:[[[ii],[ii]],[[ii],[ii]],[[ii],[ii]]],"
        // WhitePoint[2],
        "s:[[i,i],[i,i]],"
        // MinLuminance, MaxLuminance, HasPrimaries, HasLuminance
        "s:[i,i],s:[i,i],s:b,s:b"
        "}",
            "DisplayPrimaries", unpack_i(&job->mastering.display_primaries[0][0].num),
                                unpack_i(&job->mastering.display_primaries[0][0].den),
                                unpack_i(&job->mastering.display_primaries[0][1].num),
                                unpack_i(&job->mastering.display_primaries[0][1].den),
                                unpack_i(&job->mastering.display_primaries[1][0].num),
                                unpack_i(&job->mastering.display_primaries[1][0].den),
                                unpack_i(&job->mastering.display_primaries[1][1].num),
                                unpack_i(&job->mastering.display_primaries[1][1].den),
                                unpack_i(&job->mastering.display_primaries[2][0].num),
                                unpack_i(&job->mastering.display_primaries[2][0].den),
                                unpack_i(&job->mastering.display_primaries[2][1].num),
                                unpack_i(&job->mastering.display_primaries[2][1].den),
            "WhitePoint", unpack_i(&job->mastering.white_point[0].num),
                          unpack_i(&job->mastering.white_point[0].den),
                          unpack_i(&job->mastering.white_point[1].num),
                          unpack_i(&job->mastering.white_point[1].den),
            "MinLuminance", unpack_i(&job->mastering.min_luminance.num),
                            unpack_i(&job->mastering.min_luminance.den),
            "MaxLuminance", unpack_i(&job->mastering.max_luminance.num),
                            unpack_i(&job->mastering.max_luminance.den),
            "HasPrimaries", unpack_b(&job->mastering.has_primaries),
            "HasLuminance", unpack_b(&job->mastering.has_luminance)
        );
        if (result < 0)
        {
            hb_error("hb_dict_to_job: failed to parse mastering_dict: %s", error.text);
            goto fail;
        }
    }

    if (coll_dict != NULL)
    {
        result = json_unpack_ex(coll_dict, &error, 0,
        // {MaxCLL, MaxFALL}
        "{s:i, s:i}",
            "MaxCLL",   unpack_u(&job->coll.max_cll),
            "MaxFALL",  unpack_u(&job->coll.max_fall)
        );
        if (result < 0)
        {
            hb_error("hb_dict_to_job: failed to parse coll_dict: %s", error.text);
            goto fail;
        }
    }

    if (dovi_dict != NULL)
    {
        result = json_unpack_ex(dovi_dict, &error, 0,
        "{s:i, s:i, s:i, s:i, s:i, s:i, s:i, s:i}",
            "DVVersionMajor",          unpack_u(&job->dovi.dv_version_major),
            "DVVersionMinor",          unpack_u(&job->dovi.dv_version_minor),
            "DVProfile",               unpack_u(&job->dovi.dv_profile),
            "DVLevel",                 unpack_u(&job->dovi.dv_level),
            "RPUPresentFlag",          unpack_u(&job->dovi.rpu_present_flag),
            "ELPresentFlag",           unpack_u(&job->dovi.el_present_flag),
            "BLPresentFlag",           unpack_u(&job->dovi.bl_present_flag),
            "BLSignalCompatibilityId", unpack_u(&job->dovi.dv_bl_signal_compatibility_id)
        );
        if (result < 0)
        {
            hb_error("hb_dict_to_job: failed to parse dovi_dict: %s", error.text);
            goto fail;
        }
    }

    // process chapter list
    if (chapter_list != NULL &&
        hb_value_type(chapter_list) == HB_VALUE_TYPE_ARRAY)
    {
        int ii, count;
        hb_dict_t *chapter_dict;
        count = hb_value_array_len(chapter_list);
        for (ii = 0; ii < count; ii++)
        {
            chapter_dict = hb_value_array_get(chapter_list, ii);
            const char *name = NULL;
            result = json_unpack_ex(chapter_dict, &error, 0,
                                    "{s:s}", "Name", unpack_s(&name));
            if (result < 0)
            {
                hb_error("hb_dict_to_job: failed to find chapter name: %s",
                         error.text);
                goto fail;
            }
            if (name != NULL && name[0] != 0)
            {
                hb_chapter_t *chapter;
                chapter = hb_list_item(job->list_chapter, ii);
                if (chapter != NULL)
                {
                    hb_chapter_set_title(chapter, name);
                }
            }
        }
    }

    // process filter list
    if (filter_list != NULL &&
        hb_value_type(filter_list) == HB_VALUE_TYPE_ARRAY)
    {
        int ii, count;
        hb_dict_t *filter_dict;
        count = hb_value_array_len(filter_list);
        for (ii = 0; ii < count; ii++)
        {
            filter_dict = hb_value_array_get(filter_list, ii);
            int filter_id = -1;
            hb_value_t *filter_settings = NULL;
            result = json_unpack_ex(filter_dict, &error, 0, "{s:i, s?o}",
                                    "ID",       unpack_i(&filter_id),
                                    "Settings", unpack_o(&filter_settings));
            if (result < 0)
            {
                hb_error("hb_dict_to_job: failed to find filter settings: %s",
                         error.text);
                goto fail;
            }
            if (filter_id >= HB_FILTER_FIRST && filter_id <= HB_FILTER_LAST)
            {
                hb_filter_object_t *filter;
                filter = hb_filter_init(filter_id);
                hb_add_filter_dict(job, filter, filter_settings);
            }
        }
    }

    // process audio list
    if (acodec_fallback != NULL)
    {
        if (hb_value_type(acodec_fallback) == HB_VALUE_TYPE_STRING)
        {
            const char *s = hb_value_get_string(acodec_fallback);
            job->acodec_fallback = hb_audio_encoder_get_from_name(s);
        }
        else
        {
            job->acodec_fallback = hb_value_get_int(acodec_fallback);
        }
    }
    if (acodec_copy_mask != NULL)
    {
        if (hb_value_type(acodec_copy_mask) == HB_VALUE_TYPE_ARRAY)
        {
            int count, ii;
            count = hb_value_array_len(acodec_copy_mask);
            for (ii = 0; ii < count; ii++)
            {
                hb_value_t *value = hb_value_array_get(acodec_copy_mask, ii);
                if (hb_value_type(value) == HB_VALUE_TYPE_STRING)
                {
                    const char *s = hb_value_get_string(value);
                    job->acodec_copy_mask |= hb_audio_encoder_get_from_name(s);
                }
                else
                {
                    job->acodec_copy_mask |= hb_value_get_int(value);
                }
            }
        }
        else if (hb_value_type(acodec_copy_mask) == HB_VALUE_TYPE_STRING)
        {
            // Split the string at ','
            char *s = strdup(hb_value_get_string(acodec_copy_mask));
            char *cur = s;
            while (cur != NULL && cur[0] != 0)
            {
                char *next = strchr(cur, ',');
                if (next != NULL)
                {
                    *next = 0;
                    next++;
                }
                job->acodec_copy_mask |= hb_audio_encoder_get_from_name(cur);
                cur = next;
            }
            free(s);
        }
        else
        {
            job->acodec_copy_mask = hb_value_get_int(acodec_copy_mask);
        }
    }
    if (audio_list != NULL && hb_value_type(audio_list) == HB_VALUE_TYPE_ARRAY)
    {
        int ii, count;
        hb_dict_t *audio_dict;
        count = hb_value_array_len(audio_list);
        for (ii = 0; ii < count; ii++)
        {
            audio_dict = hb_value_array_get(audio_list, ii);
            hb_audio_config_t audio;
            hb_value_t *acodec = NULL, *samplerate = NULL, *mixdown = NULL;
            hb_value_t *dither = NULL;
            const char *name = NULL;

            hb_audio_config_init(&audio);
            result = json_unpack_ex(audio_dict, &error, 0,
                "{s:i, s?s, s?o, s?F, s?F, s?o, s?b, s?o, s?o, s?i, s?F, s?F}",
                "Track",                unpack_i(&audio.index),
                "Name",                 unpack_s(&name),
                "Encoder",              unpack_o(&acodec),
                "Gain",                 unpack_f(&audio.out.gain),
                "DRC",                  unpack_f(&audio.out.dynamic_range_compression),
                "Mixdown",              unpack_o(&mixdown),
                "NormalizeMixLevel",    unpack_b(&audio.out.normalize_mix_level),
                "DitherMethod",         unpack_o(&dither),
                "Samplerate",           unpack_o(&samplerate),
                "Bitrate",              unpack_i(&audio.out.bitrate),
                "Quality",              unpack_f(&audio.out.quality),
                "CompressionLevel",     unpack_f(&audio.out.compression_level));
            if (result < 0)
            {
                hb_error("hb_dict_to_job: failed to find audio settings: %s",
                         error.text);
                goto fail;
            }
            if (acodec != NULL)
            {
                if (hb_value_type(acodec) == HB_VALUE_TYPE_STRING)
                {
                    const char *s = hb_value_get_string(acodec);
                    audio.out.codec = hb_audio_encoder_get_from_name(s);
                }
                else
                {
                    audio.out.codec = hb_value_get_int(acodec);
                }
            }
            if (mixdown != NULL)
            {
                if (hb_value_type(mixdown) == HB_VALUE_TYPE_STRING)
                {
                    const char *s = hb_value_get_string(mixdown);
                    audio.out.mixdown = hb_mixdown_get_from_name(s);
                }
                else
                {
                    audio.out.mixdown = hb_value_get_int(mixdown);
                }
            }
            if (samplerate != NULL)
            {
                if (hb_value_type(samplerate) == HB_VALUE_TYPE_STRING)
                {
                    const char *s = hb_value_get_string(samplerate);
                    audio.out.samplerate = hb_audio_samplerate_get_from_name(s);
                    if (audio.out.samplerate < 0)
                        audio.out.samplerate = 0;
                }
                else
                {
                    audio.out.samplerate = hb_value_get_int(samplerate);
                }
            }
            if (dither != NULL)
            {
                if (hb_value_type(dither) == HB_VALUE_TYPE_STRING)
                {
                    const char *s = hb_value_get_string(dither);
                    audio.out.dither_method = hb_audio_dither_get_from_name(s);
                }
                else
                {
                    audio.out.dither_method = hb_value_get_int(dither);
                }
            }
            if (name != NULL)
            {
                audio.out.name = name;
            }
            if (audio.index >= 0)
            {
                audio.out.track = ii;
                hb_audio_add(job, &audio);
            }
            hb_audio_config_close(&audio);
        }
    }

    // Audio sanity checks
    int ii;
    for (ii = 0; ii < hb_list_count(job->list_audio); )
    {
        hb_audio_config_t *acfg;
        acfg = hb_list_audio_config_item(job->list_audio, ii);
        if (validate_audio_codec_mux(acfg->out.codec, job->mux, ii))
        {
            // drop the track
            hb_audio_t * audio = hb_list_item(job->list_audio, ii);
            hb_list_rem(job->list_audio, audio);
            hb_audio_close(&audio);
            continue;
        }
        ii++;
    }

    job->select_subtitle_config.dest = subtitle_search_burn ?
                                            RENDERSUB : PASSTHRUSUB;
    hb_update_str(&job->select_subtitle_config.external_filename,
                  subtitle_search_external_filename);

    // process subtitle list
    if (subtitle_list != NULL &&
        hb_value_type(subtitle_list) == HB_VALUE_TYPE_ARRAY)
    {
        int ii, count;
        hb_dict_t *subtitle_dict;
        count = hb_value_array_len(subtitle_list);
        for (ii = 0; ii < count; ii++)
        {
            subtitle_dict = hb_value_array_get(subtitle_list, ii);
            hb_subtitle_config_t sub_config = {0};
            int track = -1;
            int burn = 0;
            const char *importfile = NULL;
            json_int_t offset = 0;
            const char *name = NULL;
            const char *external_filename = NULL;

            result = json_unpack_ex(subtitle_dict, &error, 0,
                                    "{s?i, s?s, s?s, s?{s:s}, s?{s:s}}",
                                    "Track", unpack_i(&track),
                                    "Name",  unpack_s(&name),
                                    "ExternalFilename", unpack_s(&external_filename),
                                    // Support legacy "SRT" import
                                    "SRT",
                                        "Filename", unpack_s(&importfile),
                                    "Import",
                                        "Filename", unpack_s(&importfile));
            if (result < 0)
            {
                hb_error("json unpack failure: %s", error.text);
                hb_job_close(&job);
                return NULL;
            }

            // Embedded subtitle track
            if (track >= 0 && importfile == NULL)
            {
                hb_subtitle_t *subtitle;
                subtitle = hb_list_item(job->title->list_subtitle, track);
                if (subtitle != NULL)
                {
                    sub_config = subtitle->config;
                    result = json_unpack_ex(subtitle_dict, &error, 0,
                        "{s?b, s?b, s?b, s?I}",
                        "Default",  unpack_b(&sub_config.default_track),
                        "Forced",   unpack_b(&sub_config.force),
                        "Burn",     unpack_b(&burn),
                        "Offset",   unpack_I(&offset));
                    if (result < 0)
                    {
                        hb_error("json unpack failure: %s", error.text);
                        hb_job_close(&job);
                        return NULL;
                    }
                    sub_config.name = name;
                    sub_config.offset = offset;
                    sub_config.dest = burn ? RENDERSUB : PASSTHRUSUB;
                    sub_config.external_filename = (char*)external_filename;
                    hb_subtitle_add(job, &sub_config, track);
                }
            }
            else if (importfile != NULL)
            {
                sub_config.src_filename = strdup(importfile);

                const char * lang = "und";
                const char * srtcodeset = "UTF-8";
                const char * format = "SRT";
                int          source = IMPORTSRT;
                result = json_unpack_ex(subtitle_dict, &error, 0,
                    "{s?b, s?b, s?I, "         // Common
                    "s?{s?s, s?s, s?s},"       // Legacy SRT settings
                    "s?{s?s, s?s, s?s, s?s}}", // Import settings
                    "Default",  unpack_b(&sub_config.default_track),
                    "Burn",     unpack_b(&burn),
                    "Offset",   unpack_I(&offset),
                    "SRT",
                        "Filename", unpack_s(&importfile),
                        "Language", unpack_s(&lang),
                        "Codeset",  unpack_s(&srtcodeset),
                    "Import",
                        "Format",   unpack_s(&format),
                        "Filename", unpack_s(&importfile),
                        "Language", unpack_s(&lang),
                        "Codeset",  unpack_s(&srtcodeset));
                if (result < 0)
                {
                    hb_error("json unpack failure: %s", error.text);
                    hb_job_close(&job);
                    return NULL;
                }
                sub_config.name = name;
                sub_config.offset = offset;
                sub_config.dest = burn ? RENDERSUB : PASSTHRUSUB;
                strncpy(sub_config.src_codeset, srtcodeset, 39);
                sub_config.src_codeset[39] = 0;
                if (!strcasecmp(format, "SSA"))
                {
                    source = IMPORTSSA;
                }
                sub_config.external_filename = (char*)external_filename;
                hb_import_subtitle_add(job, &sub_config, lang, source);
            }
        }
    }

    return job;

fail:
    hb_job_close(&job);
    return NULL;
}

hb_job_t* hb_json_to_job( hb_handle_t * h, const char * json_job )
{
    hb_dict_t *dict = hb_value_json(json_job);
    hb_job_t *job = hb_dict_to_job(h, dict);
    hb_value_free(&dict);
    return job;
}

/**
 * Initialize an hb_job_t and return a json string representation of the job
 * @param h             - Pointer to hb_handle_t instance that contains the
 *                        specified title_index
 * @param title_index   - Index of hb_title_t to use for job initialization.
 *                        Index comes from title->index or "Index" key
 *                        in json representation of a title.
 */
char* hb_job_init_json(hb_handle_t *h, int title_index)
{
    hb_job_t *job = hb_job_init_by_index(h, title_index);
    char *json_job = hb_job_to_json(job);
    hb_job_close(&job);
    return json_job;
}

char* hb_preset_job_init_json(hb_handle_t *h, int title_index,
                              const char *json_preset)
{
    hb_dict_t * preset   = hb_value_json(json_preset);
    hb_dict_t * job      = hb_preset_job_init(h, title_index, preset);
    char      * json_job = hb_value_get_json(job);

    hb_value_free(&preset);
    hb_value_free(&job);

    return json_job;
}

/**
 * Add a json string job to the hb queue
 * @param h         - Pointer to hb_handle_t instance that job is added to
 * @param json_job  - json string representation of job to add
 */
int hb_add_json( hb_handle_t * h, const char * json_job )
{
    hb_job_t job;

    job.json = json_job;
    return hb_add(h, &job);
}


/**
 * Calculates destination width and height for anamorphic content
 *
 * Returns geometry as json string {Width, Height, PAR {Num, Den}}
 * @param json_param - contains source and destination geometry params.
 *                     This encapsulates the values that are in
 *                     hb_geometry_t and hb_geometry_settings_t
 */
char* hb_set_anamorphic_size_json(const char * json_param)
{
    int json_result;
    json_error_t error;
    hb_dict_t * dict;
    hb_geometry_t geo_result;
    hb_geometry_t src;
    hb_geometry_settings_t ui_geo;

    // Clear dest geometry since some fields are optional.
    memset(&ui_geo, 0, sizeof(ui_geo));

    dict = hb_value_json(json_param);
    json_result = json_unpack_ex(dict, &error, 0,
    "{"
    // SourceGeometry
    //  {Width, Height, PAR {Num, Den}}
    "s:{s:i, s:i, s:{s:i, s:i}},"
    // DestSettings
    "s:{"
    //   Geometry {Width, Height, PAR {Num, Den}},
    "s:{s:i, s:i, s:{s:i, s:i}},"
    //   AnamorphicMode, Flags, Keep, ItuPAR, Modulus, MaxWidth, MaxHeight,
    //   DisplayWidth, DisplayHeight
    "s:i, s?i, s?i, s?b, s?i, s?i, s?i, s?i, s?i"
    //   Crop [Top, Bottom, Left, Right]
    "s?[iiii]"
    //   Pad [Top, Bottom, Left, Right]
    "s?[iiii]"
    "  }"
    "}",
    "SourceGeometry",
        "Width",                unpack_i(&src.width),
        "Height",               unpack_i(&src.height),
        "PAR",
            "Num",              unpack_i(&src.par.num),
            "Den",              unpack_i(&src.par.den),
    "DestSettings",
        "Geometry",
            "Width",            unpack_i(&ui_geo.geometry.width),
            "Height",           unpack_i(&ui_geo.geometry.height),
            "PAR",
                "Num",          unpack_i(&ui_geo.geometry.par.num),
                "Den",          unpack_i(&ui_geo.geometry.par.den),
        "AnamorphicMode",       unpack_i(&ui_geo.mode),
        "Flags",                unpack_i(&ui_geo.flags),
        "Keep",                 unpack_i(&ui_geo.keep),
        "ItuPAR",               unpack_b(&ui_geo.itu_par),
        "Modulus",              unpack_i(&ui_geo.modulus),
        "MaxWidth",             unpack_i(&ui_geo.maxWidth),
        "MaxHeight",            unpack_i(&ui_geo.maxHeight),
        "DisplayWidth",         unpack_i(&ui_geo.displayWidth),
        "DisplayHeight",        unpack_i(&ui_geo.displayHeight),
        "Crop",                 unpack_i(&ui_geo.crop[0]),
                                unpack_i(&ui_geo.crop[1]),
                                unpack_i(&ui_geo.crop[2]),
                                unpack_i(&ui_geo.crop[3]),
        "Pad",                  unpack_i(&ui_geo.pad[0]),
                                unpack_i(&ui_geo.pad[1]),
                                unpack_i(&ui_geo.pad[2]),
                                unpack_i(&ui_geo.pad[3])
    );
    hb_value_free(&dict);

    if (json_result < 0)
    {
        hb_error("json unpack failure: %s", error.text);
        return NULL;
    }

    hb_set_anamorphic_size2(&src, &ui_geo, &geo_result);

    dict = json_pack_ex(&error, 0,
        "{s:o, s:o, s:{s:o, s:o}}",
            "Width",        hb_value_int(geo_result.width),
            "Height",       hb_value_int(geo_result.height),
            "PAR",
                "Num",      hb_value_int(geo_result.par.num),
                "Den",      hb_value_int(geo_result.par.den));
    if (dict == NULL)
    {
        hb_error("hb_set_anamorphic_size_json: pack failure: %s", error.text);
        return NULL;
    }
    char *result = hb_value_get_json(dict);
    hb_value_free(&dict);

    return result;
}

hb_image_t * hb_get_preview3_json(hb_handle_t * h, int picture, const char *json_job)
{
    hb_image_t * image;
    hb_dict_t  * job_dict;

    job_dict = hb_value_json(json_job);
    image = hb_get_preview3(h, picture, job_dict);
    hb_value_free(&job_dict);

    return image;
}

char* hb_get_preview_params_json(int title_idx, int preview_idx,
                            int deinterlace, hb_geometry_settings_t *settings)
{
    json_error_t error;
    hb_dict_t * dict;

    dict = json_pack_ex(&error, 0,
        "{"
        "s:o, s:o, s:o,"
        "s:{"
        "   s:{s:o, s:o, s:{s:o, s:o}},"
        "   s:o, s:o, s:o, s:o, s:o, s:o"
        "   s:[oooo]"
        "  }"
        "}",
        "Title",                hb_value_int(title_idx),
        "Preview",              hb_value_int(preview_idx),
        "Deinterlace",          hb_value_bool(deinterlace),
        "DestSettings",
            "Geometry",
                "Width",        hb_value_int(settings->geometry.width),
                "Height",       hb_value_int(settings->geometry.height),
                "PAR",
                    "Num",      hb_value_int(settings->geometry.par.num),
                    "Den",      hb_value_int(settings->geometry.par.den),
            "AnamorphicMode",   hb_value_int(settings->mode),
            "Keep",             hb_value_int(settings->keep),
            "ItuPAR",           hb_value_bool(settings->itu_par),
            "Modulus",          hb_value_int(settings->modulus),
            "MaxWidth",         hb_value_int(settings->maxWidth),
            "MaxHeight",        hb_value_int(settings->maxHeight),
            "Crop",             hb_value_int(settings->crop[0]),
                                hb_value_int(settings->crop[1]),
                                hb_value_int(settings->crop[2]),
                                hb_value_int(settings->crop[3])
    );
    if (dict == NULL)
    {
        hb_error("hb_get_preview_params_json: pack failure: %s", error.text);
        return NULL;
    }

    char *result = hb_value_get_json(dict);
    hb_value_free(&dict);

    return result;
}

hb_image_t* hb_json_to_image(char *json_image)
{
    int json_result;
    json_error_t error;
    hb_dict_t * dict;
    int pix_fmt, width, height;
    dict = hb_value_json(json_image);
    json_result = json_unpack_ex(dict, &error, 0,
        "{"
        // Format, Width, Height
        "s:i, s:i, s:i,"
        "}",
        "Format",                   unpack_i(&pix_fmt),
        "Width",                    unpack_i(&width),
        "Height",                   unpack_b(&height)
    );
    if (json_result < 0)
    {
        hb_error("image: json unpack failure: %s", error.text);
        hb_value_free(&dict);
        return NULL;
    }

    hb_image_t *image = hb_image_init(pix_fmt, width, height);
    if (image == NULL)
    {
        hb_value_free(&dict);
        return NULL;
    }

    hb_value_array_t * planes = NULL;
    json_result = json_unpack_ex(dict, &error, 0,
                                 "{s:o}", "Planes", unpack_o(&planes));
    if (json_result < 0)
    {
        hb_error("image::planes: json unpack failure: %s", error.text);
        hb_value_free(&dict);
        return image;
    }
    if (hb_value_type(planes) == HB_VALUE_TYPE_ARRAY)
    {
        int ii, count;
        hb_dict_t *plane_dict;
        count = hb_value_array_len(planes);
        for (ii = 0; ii < count; ii++)
        {
            plane_dict = hb_value_array_get(planes, ii);
            const char *data = NULL;
            int size;
            json_result = json_unpack_ex(plane_dict, &error, 0,
                                         "{s:i, s:s}",
                                         "Size", unpack_i(&size),
                                         "Data", unpack_s(&data));
            if (json_result < 0)
            {
                hb_error("image::plane::data: json unpack failure: %s", error.text);
                hb_value_free(&dict);
                return image;
            }
            if (image->plane[ii].size > 0 && data != NULL)
            {
                av_base64_decode(image->plane[ii].data, data,
                                 image->plane[ii].size);
            }
        }
    }
    hb_value_free(&dict);

    return image;
}
