#include <stdio.h>
#include <regex>
extern "C" {
    #include "libavutil/log.h"
}
#include <string.h>
#include "VN_Player.hh"

int av_log_level = AV_LOG_INFO;

const char* k_tsfn_create_buf    = "tsfn_create_buf";
const char* k_tsfn_new_frame     = "tsfn_new_frame";
const char* k_tsfn_state_changed = "tsfn_state_changed";
const char* k_tsfn_msg_log       = "tsfn_msg_log";
const char* k_tsfn_recording_status_changed = "tsfn_recording_status_changed";

const std::string k_debug_tag("VN_Player");
const std::string k_msg_log("msg_log");
const std::string k_new_frame("new_frame");
const std::string k_state_changed("state_changed");
const std::string k_recording_status_changed("recording_status_changed");
const std::string k_create_image_buffer("create_image_buffer");

#ifndef WIN32
int TryMutex::trylock()
{
    return pthread_mutex_trylock(mutex);
}
#endif

void vn_log(int level, const char* fmt, ...)
{
    if(level > av_log_level)
        return;

    va_list ap;
    va_start(ap, fmt);
    vfprintf(level < AV_LOG_INFO ? stderr : stdout, fmt, ap);
    va_end(ap);
}

static void av_log_callback(void* ptr, int level, const char* fmt, va_list vl)
{
    if(level > av_log_level)
        return;

    va_list vl2;
    char line[1024] = {0};
    static int print_prefix = 0;

    va_copy(vl2, vl);
    av_log_default_callback(ptr, level, fmt, vl);
    av_log_format_line(ptr, level, fmt, vl2, line, sizeof(line), &print_prefix);
    va_end(vl2);

    // skip garbage
    if(strncmp(line, "[h264 @ ", 8) && strncmp(line, "[hevc @ ", 8))
        switch(level) {
        case AV_LOG_PANIC:
            fprintf(stderr, "av_log_callback>: PANIC %s - %s\n", k_debug_tag.c_str(), line);
            break;
        case AV_LOG_FATAL:
            fprintf(stderr, "av_log_callback>: FATAL %s - %s\n", k_debug_tag.c_str(), line);
            break;
        case AV_LOG_ERROR:
            fprintf(stderr, "av_log_callback>: ERROR %s - %s\n", k_debug_tag.c_str(), line);
            break;
        case AV_LOG_WARNING:
            fprintf(stderr, "av_log_callback>: WARN %s - %s\n", k_debug_tag.c_str(), line);
            break;
        case AV_LOG_INFO:
            fprintf(stdout, "av_log_callback>: INFO %s - %s\n", k_debug_tag.c_str(), line);
            break;
        case AV_LOG_VERBOSE:
            fprintf(stdout, "av_log_callback>: VERBOSE %s - %s\n", k_debug_tag.c_str(), line);
            break;
        case AV_LOG_DEBUG:
            fprintf(stdout, "av_log_callback>: DEBUG %s - %s\n", k_debug_tag.c_str(), line);
            break;
        case AV_LOG_TRACE:
            fprintf(stdout, "av_log_callback>: TRACE %s - %s\n", k_debug_tag.c_str(), line);
            break;
        default:
            fprintf(stderr, "av_log_callback>: UNKNOWN %s - %s\n", k_debug_tag.c_str(), line);
            break;
        }
}

//Napi::FunctionReference VN_Player::constructor;

// Runs on the JS thread.
/*static*/ void VN_Player::FinalizeTSFN(napi_env env, void* data, void* context) {
    // This is where you would wait for the threads to quit. This
    // function will only be called when all the threads are done using
    // the tsfn so, presumably, they can be joined here.

    // VN_Player* player = static_cast<VN_Player*>(context);

    vn_log(AV_LOG_INFO, "FinalizeTSFN>: %s  - data: %p, context: %p\n",
           k_debug_tag.c_str(), data, context);
}

VN_Player::VN_Player(const Napi::CallbackInfo& info)
    : Napi::ObjectWrap<VN_Player>(info)
    , vsync_(false)
    , muted_(false)
    , inited_(false)
    , new_frame_ready_(false)
    , state_(0)
    , buf_num_(1)
    , buf_pos_(0)
{
    Napi::Env env = info.Env();
    Napi::HandleScope scope(env);
    size_t length = info.Length();

    if(length < 1 || !info[0].IsObject()) {
        Napi::Error::New(env, "Object expected as arg1").ThrowAsJavaScriptException();
        return;
    }
    Napi::Object obj = info[0].As<Napi::Object>();

    Napi::Value fnEmit = obj.Get("emit");
    if(fnEmit == env.Undefined() || !fnEmit.IsFunction()) {
        Napi::Error::New(env, "Emit function expected as value for object's key 'emit'").ThrowAsJavaScriptException();
        return;
    }

    Napi::Value objid = obj.Get("objid");
    if(objid == env.Undefined() || !objid.IsString()) {
        Napi::TypeError::New(env, "Camera ObjId of string type expected as value for object's key 'objid'").ThrowAsJavaScriptException();
        return;
    }
    objid_ = objid.As<Napi::String>().Utf8Value();
    short_objid_ = objid_.substr(0, 4);

    Napi::Value vsync = obj.Get("vsync");
    if(vsync == env.Undefined() || !vsync.IsBoolean()) {
        Napi::TypeError::New(env, "Vsync of boolean type expected as value for object's key 'vsync'").ThrowAsJavaScriptException();
        return;
    }
    vsync_ = vsync.As<Napi::Boolean>().Value();

    Napi::Value buf_num = obj.Get("frame_buffers_num");
    if(buf_num == env.Undefined() || !buf_num.IsNumber()) {
        Napi::TypeError::New(env, "Number of frame buffers expected as Napi::Number::Int32Value() for object's key 'frame_buffers_num'").ThrowAsJavaScriptException();
        return;
    }
    buf_num_ = buf_num.As<Napi::Number>().Int32Value();

    Napi::Value muted = obj.Get("muted");
    if (muted != env.Undefined()) {
        if (!muted.IsBoolean()) {
            Napi::TypeError::New(env, "muted of boolean type expected as value for object's key 'muted'").ThrowAsJavaScriptException();
            return;
        }
        muted_ = muted.As<Napi::Boolean>().Value();
    }

#if 0
    Napi::Value speed = obj.Get("speed");
    if(buf_num == env.Undefined() || !buf_num.IsNumber()) {
        Napi::TypeError::New(env, "Speed of float type as Napi::Number::FloatValue() for object's key 'speed'").ThrowAsJavaScriptException();
        return;
    }
#endif

    Napi::Value cache_size = obj.Get("cache_size");
    if(cache_size == env.Undefined() || !cache_size.IsNumber()) {
        Napi::TypeError::New(env, "Cache size in MB expected as Napi::Number::Uint32Value() for object's key 'cache_size'").ThrowAsJavaScriptException();
        return;
    }

    Napi::Value jbuf_len = obj.Get("jitter_buffer_len");
    if(jbuf_len == env.Undefined() || !jbuf_len.IsNumber()) {
        Napi::TypeError::New(env, "Jitter buffer length in ms expected as Napi::Number::Uint32Value() for object's key 'jitter_buffer_len'").ThrowAsJavaScriptException();
        return;
    }

    Napi::Value log_level = obj.Get("log_level");
    if(log_level != env.Undefined() && log_level.IsString() && log_level.As<Napi::String>().Utf8Value() == "debug")
        av_log_level = AV_LOG_DEBUG;

    memset(&last_captured_time_, 0, sizeof(struct timeval));
    for(int i=0; i<TRIPLE_BUF; i++)
        if(i >= buf_num_) {
            captured_time_[i] = nullptr;
            frame_lock_[i] = nullptr;
        } else {
            captured_time_[i] = new struct timeval;
            memset(captured_time_[i], 0, sizeof(struct timeval));
#ifdef WIN32
            frame_lock_[i] = new CRITICAL_SECTION;
            InitializeCriticalSection(frame_lock_[i]);
        }
    InitializeCriticalSection(&new_frame_ready_lock_);
    InitializeConditionVariable(&create_buf_lock_);
#else
            frame_lock_[i] = new TryMutex;
        }
#endif

    void* context = this;

    NAPI_THROW_IF_FAILED_VOID(
        env,
        napi_create_threadsafe_function(
            env,
            fnEmit,       // js callback
            nullptr,
            Napi::String::New(env, "Thread-safe create_image_buffer() function"),
            0,            // unlimited queue size
            1,            // initially only used from the main thread
            nullptr,      // data to make use of during finalization
            VN_Player::FinalizeTSFN, // gets called when the tsfn goes out of use
            context,      // data that can be set here and retrieved on any thread
            cb_js_create_buf,   // function to call into JS
            &tsfn_create_buf_
        )
    );

    NAPI_THROW_IF_FAILED_VOID(
        env,
        napi_create_threadsafe_function(
            env,
            fnEmit,       // js callback
            nullptr,
            Napi::String::New(env, "Thread-safe new_frame() function"),
            buf_num_,     // limit queue size to the number of frame buffers
            1,            // initially only used from the main thread
            nullptr,      // data to make use of during finalization
            VN_Player::FinalizeTSFN, // gets called when the tsfn goes out of use
            context,      // data that can be set here and retrieved on any thread
            cb_js_new_frame,   // function to call into JS
            &tsfn_new_frame_
        )
    );

    NAPI_THROW_IF_FAILED_VOID(
        env,
        napi_create_threadsafe_function(
            env,
            fnEmit,       // js callback
            nullptr,
            Napi::String::New(env, "Thread-safe state_changed() function"),
            0,            // unlimited queue size
            1,            // initially only used from the main thread
            nullptr,      // data to make use of during finalization
            VN_Player::FinalizeTSFN, // gets called when the tsfn goes out of use
            context,      // data that can be set here and retrieved on any thread
            cb_js_state_changed,   // function to call into JS
            &tsfn_state_changed_
        )
    );

    NAPI_THROW_IF_FAILED_VOID(
        env,
        napi_create_threadsafe_function(
            env,
            fnEmit,       // js callback
            nullptr,
            Napi::String::New(env, "Thread-safe recording_status_changed() function"),
            0,            // unlimited queue size
            1,            // initially only used from the main thread
            nullptr,      // data to make use of during finalization
            VN_Player::FinalizeTSFN, // gets called when the tsfn goes out of use
            context,      // data that can be set here and retrieved on any thread
            cb_js_recording_status_changed,   // function to call into JS
            &tsfn_recording_status_changed_
        )
    );

    NAPI_THROW_IF_FAILED_VOID(
        env,
        napi_create_threadsafe_function(
            env,
            fnEmit,       // js callback
            nullptr,
            Napi::String::New(env, "Thread-safe msg_log() function"),
            0,            // unlimited queue size
            1,            // initially only used from the main thread
            nullptr,      // data to make use of during finalization
            VN_Player::FinalizeTSFN, // gets called when the tsfn goes out of use
            context,      // data that can be set here and retrieved on any thread
            cb_js_msg_log,   // function to call into JS
            &tsfn_msg_log_
        )
    );

    napi_unref_threadsafe_function(env, tsfn_create_buf_);
    napi_unref_threadsafe_function(env, tsfn_new_frame_);
    napi_unref_threadsafe_function(env, tsfn_state_changed_);
    napi_unref_threadsafe_function(env, tsfn_recording_status_changed_);
    napi_unref_threadsafe_function(env, tsfn_msg_log_);

    // Now you can pass `tsfn` to any number of threads. Each one must
    // first call `napi_threadsafe_function_acquire()`. Then it may call
    // `napi_call_threadsafe_function()` any number of times. If on one
    // of those occasions the return value from
    // `napi_call_threadsafe_function()` is `napi_closing`, the thread
    // must make no more calls to any of the thread-safe function APIs.
    // If it never receives `napi_closing` from
    // `napi_call_threadsafe_function()` then, before exiting, the
    // thread must call `napi_release_threadsafe_function()`.

    memset(&conf_, 0, sizeof(conf_));
    conf_.cache_size = cache_size.As<Napi::Number>().Uint32Value(); // Mb
    conf_.decoder_type = DECODER_SW;
    conf_.pixel_format = PIX_YUV420P;
    conf_.rtp_transport = RTP_TRANSPORT_TCP;
    conf_.client_data = context;
    conf_.buffer_len = jbuf_len.As<Napi::Number>().Uint32Value();   // in ms
    //conf_.speed = speed.As<Napi::Number>().FloatValue();

    fprintf(stderr, "Initial Jitter>: %dms\n", conf_.buffer_len);

    struct vn_player_callbacks_t callbacks = {};
    callbacks.create_image_buffer = create_image_buffer;
    callbacks.lock_image_buffer   = lock_image_buffer;
    callbacks.unlock_image_buffer = unlock_image_buffer;
    callbacks.buffer_changed = buffer_changed;
    callbacks.new_frame      = new_frame;
    callbacks.new_stream     = new_stream;
    callbacks.state_changed  = state_changed;
    callbacks.stream_removed = stream_removed;
    callbacks.msg_log        = msg_log;
    callbacks.recording_status_changed = recording_status_changed;

    ctx_ = vn_player_create(&conf_);
    vn_player_set_callbacks(ctx_, &callbacks);

    av_log_set_callback(av_log_callback);
    av_log_set_level(av_log_level);

    vn_player_mute_audio(ctx_, muted_);

    vn_log(AV_LOG_INFO, "%s_%s - create\n", k_debug_tag.c_str(), get_short_objid().c_str());
}

VN_Player::~VN_Player() {
    vn_log(AV_LOG_INFO, "~%s_%s - destroy\n", k_debug_tag.c_str(), get_short_objid().c_str());
    if (ctx_) {
        vn_player_destroy(ctx_);
        ctx_ = NULL;
    }

    for(int i=0; i<TRIPLE_BUF; i++) {
        delete captured_time_[i];
        delete frame_lock_[i];
    }
}

Napi::Object VN_Player::Init(Napi::Env env, Napi::Object exports) {
    vn_log(AV_LOG_INFO, "Init>: %s\n", k_debug_tag.c_str());
    Napi::HandleScope scope(env);

    // This method is used to hook the accessor and method callbacks
    Napi::Function func = DefineClass(env, "VN_Player", {
        InstanceMethod("play", &VN_Player::Play),
        InstanceMethod("pause", &VN_Player::Pause),
        InstanceMethod("resume", &VN_Player::Resume),
        InstanceMethod("teardown", &VN_Player::Teardown),
        InstanceMethod("jump", &VN_Player::Jump),
        InstanceMethod("crash", &VN_Player::Crash),
        InstanceMethod("speed", &VN_Player::Speed),
        InstanceMethod("muteAudio", &VN_Player::MuteAudio),
        InstanceMethod("setJitterBufferLen", &VN_Player::SetJitterBufferLen),

        InstanceMethod("set_img_buffer", &VN_Player::SetImgBuffer),
        InstanceMethod("get_info_and_lock_frame_if_ready", &VN_Player::GetInfoAndLockFrameIfReady),
        InstanceMethod("frame_unlock", &VN_Player::FrameUnlock)
    });

    // Create a persistent reference to the class constructor. This will allow
    // a function called on a class prototype and a function
    // called on instance of a class to be distinguished from each other.
//    constructor = Napi::Persistent(func);

    // Call the SuppressDestruct() method on the static data prevent the calling
    // to this destructor to reset the reference when the environment is no longer
    // available.
//    constructor.SuppressDestruct();

    exports.Set("VN_Player", func);
    return exports;
}

// The data field here will contain a pointer to the single continuous buffer as a result of create_image_buffer callback.
// Such buffer will be created natively on JS side and will be returned to sdk as an external buffer for the new frames.
// This way we reach zero-copy mechanism while drawing received frames
struct buf_info_t {
    int width;
    int height;
    int buf_num;
    uint8_t* data;
};

struct state_info_t {
    int state;
    int code;
    std::string msg;
};

struct recording_info_t {
    int status;
    int progress;
    time_t timestamp;
    std::string filename;
    std::string msg;
};

// helper function that is used by all JS callbacks with a description in one place
inline bool check_cb_js_env(napi_env env, napi_value js_cb) {
    // `data` was passed to `napi_call_threadsafe_function()` by one
    // of the threads. The order in which the threads add data as
    // they call `napi_call_threadsafe_function()` is the order in
    // which they will be given to this callback.
    //
    // `context` was passed to `napi_create_threadsafe_function()` and
    // is being provided here.

    if (env == nullptr || js_cb == nullptr) {
        // The tsfn is in the process of getting cleaned up and there are
        // still items left on the queue. This function gets called with
        // each `void*` item, but with `env` and `js_cb` set to `NULL`,
        // because calls can no longer be made into JS, but the `void*`s
        // may still need to be freed.

        return false;
    }

    return true;
}

// Runs on the JS thread.
/*static*/ void VN_Player::cb_js_create_buf(napi_env e, napi_value js_cb, void* context, void* data) {
    VN_Player* player = static_cast<VN_Player*>(context);
    buf_info_t& buf_info = *static_cast<buf_info_t*>(data);

    bool ok(false);
    const std::string inst(k_debug_tag + "_" + player->get_short_objid());
    do {
        if (!check_cb_js_env(e, js_cb)) {
            vn_log(AV_LOG_ERROR, "cb_js_create_buf>: %s - skip buffer creation in JS because %s() in the process of getting cleaned up\n",
                   inst.c_str(), k_tsfn_create_buf);
            break;
        }

        try {
            Napi::Env env(e);
            Napi::Function(env, js_cb).Call({
                Napi::String::New(env, k_create_image_buffer),
                Napi::Number::New(env, buf_info.width),
                Napi::Number::New(env, buf_info.height),
                Napi::External<buf_info_t>::New(env, &buf_info)
            });

            ok = true;
        } catch (const Napi::Error &e) {
            vn_log(AV_LOG_ERROR, "cb_js_create_buf>: %s - caught JavaScript exception: %s\n",
                   inst.c_str(), e.what());
        } catch (...) {
            vn_log(AV_LOG_ERROR, "cb_js_create_buf>: %s - caught JavaScript UNKNOWN exception\n",
                   inst.c_str());
        }
    } while(0);

    if(!ok) {
        buf_info.buf_num = 1; // fallback to one buffer
        buf_info.data = 0;    // buffer will be created by sdk

        // need to avoid deadlock on fail
#ifdef WIN32
        vn_log(AV_LOG_INFO, "cb_js_create_buf>: %s - trying to lock frame_lock_...\n", inst.c_str());
        EnterCriticalSection(player->frame_lock_[0]);
        vn_log(AV_LOG_INFO, "cb_js_create_buf>: %s - signal about callback completion...\n", inst.c_str());
        WakeConditionVariable(&player->create_buf_lock_);
        vn_log(AV_LOG_INFO, "cb_js_create_buf>: %s - done. Unlocking frame_lock_\n", inst.c_str());
        LeaveCriticalSection(player->frame_lock_[0]);
#else
        vn_log(AV_LOG_INFO, "cb_js_create_buf>: %s - trying to lock create_buf_lock_...\n", inst.c_str());
        videonext::media::MutexGuard g(player->create_buf_lock_);
        vn_log(AV_LOG_INFO, "cb_js_create_buf>: %s - signal about callback completion...\n", inst.c_str());
        player->create_buf_lock_.signal();
        vn_log(AV_LOG_INFO, "cb_js_create_buf>: %s - done. Unlocking create_buf_lock_\n", inst.c_str());
#endif
    }
}

/*static*/ void VN_Player::cb_js_new_frame(napi_env e, napi_value js_cb, void* context, void* data) {
    VN_Player* player = static_cast<VN_Player*>(context);
    const std::string inst(k_debug_tag + "_" + player->get_short_objid());

    if (!check_cb_js_env(e, js_cb)) {
        vn_log(AV_LOG_ERROR, "cb_js_new_frame>: %s - skip passing new frame into JS because %s() in the process of getting cleaned up\n",
               inst.c_str(), k_tsfn_new_frame);
        return;
    }

    try {
        Napi::Env env(e);
        Napi::Function(env, js_cb).Call({
            Napi::String::New(env, k_new_frame),
            player->GetInfoAndLockFrameIfReady(env)
        });
    } catch (const Napi::Error &e) {
        vn_log(AV_LOG_ERROR, "cb_js_new_frame>: %s - caught JavaScript exception: %s\n", inst.c_str(), e.what());
    } catch (...) {
        vn_log(AV_LOG_ERROR, "cb_js_new_frame>: %s - caught JavaScript UNKNOWN exception\n", inst.c_str());
    }

    // need to explicitly call the frame_unlock() method after the end of the frame rendering
}

/*static*/ void VN_Player::cb_js_state_changed(napi_env e, napi_value js_cb, void* context, void* data) {
    VN_Player* player = static_cast<VN_Player*>(context);

    if (!check_cb_js_env(e, js_cb)) {
        vn_log(AV_LOG_ERROR, "cb_js_state_changed>: %s_%s - skip state change propagation into JS because %s() in the process of getting cleaned up\n",
               k_debug_tag.c_str(), player->get_short_objid().c_str(), k_tsfn_state_changed);
        return;
    }

    std::shared_ptr<state_info_t> p_state(static_cast<state_info_t*>(data));

    try {
        Napi::Env env(e);
        Napi::Function(env, js_cb).Call({
            Napi::String::New(env, k_state_changed),
            Napi::Number::New(env, p_state->state),
            Napi::Number::New(env, p_state->code),
            Napi::String::New(env, p_state->msg)
        });
    } catch (const Napi::Error &e) {
        vn_log(AV_LOG_ERROR, "cb_js_state_changed>: %s_%s - caught JavaScript exception: %s\n",
               k_debug_tag.c_str(), player->get_short_objid().c_str(), e.what());
    } catch (...) {
        vn_log(AV_LOG_ERROR, "cb_js_state_changed>: %s_%s - caught JavaScript UNKNOWN exception\n",
               k_debug_tag.c_str(), player->get_short_objid().c_str());
    }
}

/*static*/ void VN_Player::cb_js_recording_status_changed(napi_env e, napi_value js_cb, void* context, void* data) {
    VN_Player* player = static_cast<VN_Player*>(context);

    if (!check_cb_js_env(e, js_cb)) {
        vn_log(AV_LOG_ERROR, "cb_js_recording_status_changed>: %s_%s - skip state change propagation into JS because %s() in the process of getting cleaned up\n",
               k_debug_tag.c_str(), player->get_short_objid().c_str(), k_tsfn_recording_status_changed);
        return;
    }

    std::shared_ptr<recording_info_t> p_state(static_cast<recording_info_t*>(data));

    try {
        Napi::Env env(e);
        Napi::Function(env, js_cb).Call({
            Napi::String::New(env, k_recording_status_changed),
            Napi::Number::New(env, p_state->status),
            Napi::Number::New(env, p_state->timestamp),
            Napi::String::New(env, p_state->filename),
            Napi::String::New(env, p_state->msg)
        });
    } catch (const Napi::Error &e) {
        vn_log(AV_LOG_ERROR, "cb_js_recording_status_changed>: %s_%s - caught JavaScript exception: %s\n",
               k_debug_tag.c_str(), player->get_short_objid().c_str(), e.what());
    } catch (...) {
        vn_log(AV_LOG_ERROR, "cb_js_recording_status_changed>: %s_%s - caught JavaScript UNKNONW exception\n",
               k_debug_tag.c_str(), player->get_short_objid().c_str());
    }
}

/*static*/ void VN_Player::cb_js_msg_log(napi_env e, napi_value js_cb, void* context, void* data) {
    VN_Player* player = static_cast<VN_Player*>(context);

    if (!check_cb_js_env(e, js_cb)) {
        vn_log(AV_LOG_ERROR, "cb_js_msg_log>: %s_%s - skip logging message propagation into JS because %s() in the process of getting cleaned up\n",
               k_debug_tag.c_str(), player->get_short_objid().c_str(), k_tsfn_msg_log);
        return;
    }

    std::shared_ptr<std::string> p_msg(static_cast<std::string*>(data));

    try {
        Napi::Env env(e);
        Napi::Function(env, js_cb).Call({
            Napi::String::New(env, k_msg_log),
            Napi::String::New(env, *p_msg)
        });
    } catch (const Napi::Error &e) {
        vn_log(AV_LOG_ERROR, "cb_js_msg_log>: %s_%s - caught JavaScript exception: %s\n",
               k_debug_tag.c_str(), player->get_short_objid().c_str(), e.what());
    } catch (...) {
        vn_log(AV_LOG_ERROR, "cb_js_msg_log>: %s_%s - caught JavaScript UNKNOWN exception\n",
               k_debug_tag.c_str(), player->get_short_objid().c_str());
    }
}

void VN_Player::Play(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    size_t length = info.Length();

    if (length < 1 || !info[0].IsString()) {
        Napi::TypeError::New(env, "URL of type String expected as arg1").ThrowAsJavaScriptException();
        return;
    }

    if(length < 2 || !info[1].IsNumber()) {
        Napi::TypeError::New(env, "Speed of type Number expected as arg2").ThrowAsJavaScriptException();
        return;
    }

    vn_log(AV_LOG_INFO, "Play>: %s_%s: %s\n",
           k_debug_tag.c_str(), get_short_objid().c_str(), info[0].As<Napi::String>().Utf8Value().c_str());

    vn_player_open(ctx_,
                   info[0].As<Napi::String>().Utf8Value().c_str(),
                   info[1].As<Napi::Number>().FloatValue());
}

void VN_Player::Pause(const Napi::CallbackInfo& info) {

    vn_log(AV_LOG_INFO, "Pause>: %s_%s\n",
           k_debug_tag.c_str(), get_short_objid().c_str());

    vn_player_pause(ctx_);
}

void VN_Player::Resume(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    size_t length = info.Length();

    if (length < 1 || !info[0].IsNumber()) {
        Napi::TypeError::New(env, "Play direction expected as Napi::Number::Int32Value() (-1: backward, 1: forward)").ThrowAsJavaScriptException();
        return;
    }

    vn_log(AV_LOG_INFO, "Resume>: %s_%s - direction: %d\n",
           k_debug_tag.c_str(), get_short_objid().c_str(), info[0].As<Napi::Number>().Int32Value());
    vn_player_resume(ctx_, info[0].As<Napi::Number>().Int32Value());
}

void VN_Player::Teardown(const Napi::CallbackInfo& info) {

    vn_log(AV_LOG_INFO, "Teardown>: %s_%s\n",
           k_debug_tag.c_str(), get_short_objid().c_str());

    if (ctx_) {
        vn_player_destroy(ctx_);
        ctx_ = NULL;
    }
}

void VN_Player::Jump(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    size_t length = info.Length();

    if (length <= 0 || !info[0].IsNumber()) {
        Napi::TypeError::New(env, "Unix epoch timestamp expected as Napi::Number::Uint32Value()").ThrowAsJavaScriptException();
        return;
    }

    vn_log(AV_LOG_INFO, "Jump>: %s_%s - to: %d\n",
           k_debug_tag.c_str(), get_short_objid().c_str(), info[0].As<Napi::Number>().Int32Value());

    vn_player_move_to_ts(ctx_, info[0].As<Napi::Number>().Uint32Value());
}

void VN_Player::Speed(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    size_t length = info.Length();

    if(length < 1 || (length == 1 && !info[0].IsNumber())) {
        Napi::TypeError::New(env, "Speed of type Number expected as arg1").ThrowAsJavaScriptException();
        return;
    }

    vn_log(AV_LOG_INFO, "Speed>: %s_%s - speed: %f\n",
           k_debug_tag.c_str(), get_short_objid().c_str(), info[0].As<Napi::Number>().FloatValue());

    vn_player_set_playback_speed(ctx_, info[0].As<Napi::Number>().FloatValue());
}

void VN_Player::MuteAudio(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    size_t length = info.Length();

    if(length < 1 || (length == 1 && !info[0].IsBoolean())) {
        Napi::TypeError::New(env, "Mute of type Boolean expected as arg1").ThrowAsJavaScriptException();
        return;
    }

    vn_log(AV_LOG_INFO, "MuteAudio>: %s_%s - val: %d\n",
           k_debug_tag.c_str(), get_short_objid().c_str(), info[0].As<Napi::Boolean>().Value());

    muted_ = info[0].As<Napi::Boolean>().Value();

    vn_player_mute_audio(ctx_, muted_);
}

void VN_Player::SetJitterBufferLen(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();
    size_t length = info.Length();

    if(length < 1 || (length == 1 && !info[0].IsNumber())) {
        Napi::TypeError::New(env, "Jitter of type Number expected as arg1").ThrowAsJavaScriptException();
        return;
    }

    vn_log(AV_LOG_INFO, "Jitter>: %s_%s - jitter: %dms\n",
           k_debug_tag.c_str(), get_short_objid().c_str(), info[0].As<Napi::Number>().Int32Value());

    vn_player_set_jitter_buffer_len(ctx_, info[0].As<Napi::Number>().Int32Value());
}

void VN_Player::Crash(const Napi::CallbackInfo& info) {
    vn_log(AV_LOG_INFO, "Crash>: let's crash;)\n");
    char* c = NULL;
    *c = 0;
}

void VN_Player::SetImgBuffer(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();
    size_t length = info.Length();

    if(length < 1 || !info[0].IsArrayBuffer()) {
        Napi::TypeError::New(env, "ArrayBuffer expected as arg1").ThrowAsJavaScriptException();
        return;
    }

    if(length < 2 || !info[1].IsExternal()) {
        Napi::TypeError::New(env, "Napi::External<buf_info_t> expected as arg2").ThrowAsJavaScriptException();
        return;
    }

    buf_info_t& buf_info = *info[1].As<Napi::External<buf_info_t> >().Data();
    buf_info.data = static_cast<uint8_t*>(info[0].As<Napi::ArrayBuffer>().Data());
    const std::string inst(k_debug_tag + "_" + get_short_objid());

    vn_log(AV_LOG_INFO, "SetImgBuffer>: %s - created single continuous buffer (%p) of length %zu to cover %d %s. Frame dims: %dx%d, color space: yuv420p\n",
           inst.c_str(),
           buf_info.data,
           info[0].As<Napi::ArrayBuffer>().ByteLength(),
           buf_info.buf_num, buf_info.buf_num == 1 ? "buffer" : "separate buffers",
           buf_info.width, buf_info.height);

    inited_ = true;

#if 0 // TODO: remove
    for(int i=1; i<buf_info.buf_num; i++) {
        captured_time_[i] = new struct timeval;
        memset(captured_time_[i], 0, sizeof(struct timeval));
#ifdef WIN32
        frame_lock_[i] = new CRITICAL_SECTION;
        InitializeCriticalSection(frame_lock_[i]);
#else
        frame_lock_[i] = new TryMutex;
#endif
    }
#endif

#ifdef WIN32
    vn_log(AV_LOG_INFO, "SetImgBuffer>: %s - trying to lock frame_lock_...\n", inst.c_str());
    EnterCriticalSection(frame_lock_[0]);
    vn_log(AV_LOG_INFO, "SetImgBuffer>: %s - signal about callback completion...\n", inst.c_str());
    WakeConditionVariable(&create_buf_lock_);
    vn_log(AV_LOG_INFO, "SetImgBuffer>: %s - done. Unlocking frame_lock_\n", inst.c_str());
    LeaveCriticalSection(frame_lock_[0]);
#else
    vn_log(AV_LOG_INFO, "SetImgBuffer>: %s - trying to lock create_buf_lock_...\n", inst.c_str());
    videonext::media::MutexGuard g(create_buf_lock_);
    vn_log(AV_LOG_INFO, "SetImgBuffer>: %s - signal about callback completion...\n", inst.c_str());
    create_buf_lock_.signal();
    vn_log(AV_LOG_INFO, "SetImgBuffer>: %s - done. Unlocking create_buf_lock_\n", inst.c_str());
#endif
}

Napi::Value VN_Player::GetInfoAndLockFrameIfReady(Napi::Env env) {
    Napi::String objs_data;
    int64_t ts;
    int buf_num = 0, buf_pos = 0, n_attempts = 0;
    bool inited_and_ready;
    struct timeval last_tv;

#ifdef WIN32
    EnterCriticalSection(&new_frame_ready_lock_);
#else
    new_frame_ready_lock_.lock();
#endif

    inited_and_ready = inited_ && new_frame_ready_;
    if(inited_and_ready) {
        buf_num = n_attempts = buf_num_;
        buf_pos = buf_pos_;
        memcpy(&last_tv, &last_captured_time_, sizeof(struct timeval));
        objs_data = Napi::String::New(env, objs_data_);
        if(!objs_data_.empty())
            objs_data_.clear();
        new_frame_ready_ = false;
    }

#ifdef WIN32
    LeaveCriticalSection(&new_frame_ready_lock_);
#else
    new_frame_ready_lock_.unlock();
#endif

    Napi::Object obj = Napi::Object::New(env);
    obj.Set(Napi::String::New(env, "new"), Napi::Boolean::New(env, inited_and_ready));

    if(!inited_and_ready)
        return obj;

    bool locked(false);
    for(int i=n_attempts; i>0; --i) {
#ifdef WIN32
        if(TryEnterCriticalSection(frame_lock_[buf_pos]))
#else
        if(frame_lock_[buf_pos]->trylock() == 0)
#endif
        {
            struct timeval& tv = *captured_time_[buf_pos];
            if(tv.tv_sec > last_tv.tv_sec || (tv.tv_sec == last_tv.tv_sec && tv.tv_usec >= last_tv.tv_usec)) {
                locked = true;
                ts = (int64_t)tv.tv_sec * 1000000 + tv.tv_usec;
                tv.tv_sec = tv.tv_usec = 0;
                break;
            } else
#ifdef WIN32
                LeaveCriticalSection(frame_lock_[buf_pos]);
#else
                frame_lock_[buf_pos]->unlock();
#endif
        } else
            vn_log(AV_LOG_WARNING, "GetInfoAndLockFrameIfReady>: %s_%s - trylock[%d] FAILED, %d attempts left\n",
                   k_debug_tag.c_str(), get_short_objid().c_str(), buf_pos, n_attempts-1);

        if(--buf_pos < 0)
            buf_pos = buf_num - 1;
    }

    if(!locked) {
        // let's revert 'new_frame_ready_' flag
        // TODO: restore obj_data_ !!!
#ifdef WIN32
        EnterCriticalSection(&new_frame_ready_lock_);
        new_frame_ready_ = true;
        LeaveCriticalSection(&new_frame_ready_lock_);
#else
        new_frame_ready_lock_.lock();
        new_frame_ready_ = true;
        new_frame_ready_lock_.unlock();
#endif

        obj.Set(Napi::String::New(env, "new"), Napi::Boolean::New(env, !inited_and_ready));
        return obj;
    }

    obj.Set(Napi::String::New(env, "ts"), Napi::Number::New(env, ts));
    obj.Set(Napi::String::New(env, "meta"), objs_data);
    obj.Set(Napi::String::New(env, "buf_pos"), buf_pos);

    // need to explicitly call the frame_unlock(buf_pos) method after the end of the frame rendering

    return obj;
}

Napi::Value VN_Player::GetInfoAndLockFrameIfReady(const Napi::CallbackInfo& info) {
    return GetInfoAndLockFrameIfReady(info.Env());
}

void VN_Player::FrameUnlock(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();

    if(info.Length() < 1 || !info[0].IsNumber()) {
        Napi::TypeError::New(env, "FrameUnlock() expected buffer's position as Napi::Number::Int32Value() to unlock").ThrowAsJavaScriptException();
        return;
    }
    int buf_pos = info[0].As<Napi::Number>().Int32Value();

#ifdef WIN32
    LeaveCriticalSection(frame_lock_[buf_pos]);
#else
    frame_lock_[buf_pos]->unlock();
#endif
}

// helper function that doing some routine actions when calling a thread-safe function
bool call_tsfn(napi_threadsafe_function tsfn, void* context, void* data, const char* tsfn_name)
{
    /* JavaScript functions can normally only be called from a native addon's main thread.
     * When an addon has additional threads and JavaScript functions need to be invoked based on the processing
     * completed by those threads, those threads must communicate with the addon's main thread so that the main
     * thread can invoke the JavaScript function on their behalf.
     * The thread-safe function APIs provide an easy way to do this.
     * More details here:
     *
     * https://nodejs.org/dist/latest-v11.x/docs/api/n-api.html#n_api_asynchronous_thread_safe_function_calls
     */

    VN_Player* player = static_cast<VN_Player*>(context);

    napi_status status = napi_acquire_threadsafe_function(tsfn);
    if (status != napi_ok) {
        vn_log(AV_LOG_ERROR, "call_tsfn>: %s_%s - fail to acquire %s(), status=%d%s\n",
               k_debug_tag.c_str(), player->get_short_objid().c_str(), tsfn_name, status, status == napi_closing ? " (closing)" : "");
        return false;
    }

    // Initiate the call into JavaScript. The call into JavaScript will not
    // have happened when this function returns, but it will be queued
    status = napi_call_threadsafe_function(tsfn, data, napi_tsfn_nonblocking);
    switch(status)
    {
    case napi_ok:
    case napi_queue_full:
        break;
    default:
        vn_log(AV_LOG_ERROR, "call_tsfn>: %s_%s - fail to call %s(), status=%d%s\n",
               k_debug_tag.c_str(), player->get_short_objid().c_str(), tsfn_name, status, status == napi_closing ? " (closing)" : "");
        // Indicate that this thread will make no further use of the thread-safe function.
        (void)napi_release_threadsafe_function(tsfn, napi_tsfn_release);
        return false;
    }

    // Indicate that this thread will make no further use of the thread-safe function.
    (void)napi_release_threadsafe_function(tsfn, napi_tsfn_release);

    return true;
}

// sdk callbacks
/*static*/ void* VN_Player::create_image_buffer(int width, int height, float dar, VN_PLAYER_FRAME_BUFFERS_NUM& buf_num, void *client_data)
{
    VN_Player* player = static_cast<VN_Player*>(client_data);
    buf_info_t buf_info = { width, height, player->buf_num_, 0 };
    const std::string inst(k_debug_tag + "_" + player->get_short_objid());

    vn_log(AV_LOG_INFO, "create_image_buffer>: %s\n", inst.c_str());
    //vn_log(AV_LOG_DEBUG, "create_image_buffer>: %s - pre buf_num=%d\n", debug_tag, buf_num);

    do {
#ifdef WIN32
        EnterCriticalSection(player->frame_lock_[0]);

        if (!call_tsfn(player->tsfn_create_buf_, client_data, &buf_info, k_tsfn_create_buf)) {
            LeaveCriticalSection(player->frame_lock_[0]);
            break;
        }

        vn_log(AV_LOG_INFO, "create_image_buffer>: %s - waiting create_buf_lock...\n", inst.c_str());
        SleepConditionVariableCS(&player->create_buf_lock_, player->frame_lock_[0], INFINITE);
        vn_log(AV_LOG_INFO, "create_image_buffer>: %s - done waiting create_buf_lock\n", inst.c_str());

        LeaveCriticalSection(player->frame_lock_[0]);
#else
        videonext::media::MutexGuard g(player->create_buf_lock_);

        if (!call_tsfn(player->tsfn_create_buf_, client_data, &buf_info, k_tsfn_create_buf))
            break;

        vn_log(AV_LOG_INFO, "create_image_buffer>: %s - waiting create_buf_lock...\n", inst.c_str());
        player->create_buf_lock_.wait();
        vn_log(AV_LOG_INFO, "create_image_buffer>: %s - done waiting create_buf_lock\n", inst.c_str());
#endif
    } while(0);

    player->buf_num_ = /*buf_num =*/ buf_info.buf_num;
    vn_log(AV_LOG_DEBUG, "create_image_buffer>: %s - post buf_num=%u, buf=%p\n", inst.c_str(), player->buf_num_, buf_info.data);

    if(player->buf_num_ < 2)
        buf_num = SINGLE_BUF;
    else if(player->buf_num_ == 2)
        buf_num = DOUBLE_BUF;
    else
        buf_num = TRIPLE_BUF;

    return buf_info.data;
}

/*static*/ int VN_Player::lock_image_buffer(int buf_pos, void *client_data)
{
    VN_Player* player = static_cast<VN_Player*>(client_data);
    const std::string inst(k_debug_tag + "_" + player->get_short_objid());

    vn_log(AV_LOG_DEBUG, "lock_image_buffer(%d)>: %s - try to lock...\n", buf_pos, inst.c_str());
    int buf_num = player->buf_num_;
    for(int i=buf_num; i>0; --i) {
#ifdef WIN32
        if(!TryEnterCriticalSection(player->frame_lock_[buf_pos]))
#else
        if(player->frame_lock_[buf_pos]->trylock() != 0)
#endif
        {
            vn_log(AV_LOG_WARNING, "lock_image_buffer(%d)>: %s - trylock() FAILED\n", buf_pos, inst.c_str());
            if(++buf_pos >= buf_num)
                buf_pos = 0;
        } else
            return buf_pos;
    }

#ifdef WIN32
    EnterCriticalSection(player->frame_lock_[buf_num]);
#else
    player->frame_lock_[buf_pos]->lock();
#endif
    vn_log(AV_LOG_DEBUG, "lock_image_buffer(%d)>: %s - locked\n", buf_pos, inst.c_str());

    return buf_pos;
}

/*static*/ void VN_Player::unlock_image_buffer(int buf_pos, void *client_data)
{
    VN_Player* player = static_cast<VN_Player*>(client_data);
    const std::string inst(k_debug_tag + "_" + player->get_short_objid());

    vn_log(AV_LOG_DEBUG, "unlock_image_buffer(%d)>: try to unlock...\n", buf_pos, inst.c_str());
#ifdef WIN32
    LeaveCriticalSection(player->frame_lock_[buf_pos]);
#else
    player->frame_lock_[buf_pos]->unlock();
#endif
    vn_log(AV_LOG_DEBUG, "unlock_image_buffer(%d)>: %s - unlocked\n", buf_pos, inst.c_str());
}

/*static*/ void VN_Player::new_frame(const vn_player_frame_t* frame,
                                     const vn_player_cache_boundaries_t* bounds,
                                     int _buf_pos,
                                     void* client_data)
{
    VN_Player* player = static_cast<VN_Player*>(client_data);
    const std::string inst(k_debug_tag + "_" + player->get_short_objid());
    //vn_log(AV_LOG_DEBUG, "new_frame(%d)>: %s, type=%d, objs_data_size=%u\n", _buf_pos, inst.c_str(), frame->stream_info->type, frame->objects_data_size);

    bool vsync;
    int buf_pos = _buf_pos, buf_num, n_attempts;

    if (frame->stream_info->type == VN_PLAYER_STREAM_TYPE::VIDEO && frame->data[0])
    {
        vn_log(AV_LOG_DEBUG, "new_frame(%d)>: %s - new video frame %dx%d linesizes: %d, %d, %d\n",
               buf_pos, inst.c_str(), frame->width, frame->height,
               frame->data_size[0], frame->data_size[1], frame->data_size[2]);

#ifdef YUV_DEBUG
        static int cnt = 0;
        if(cnt < 10) {
            char tmp[1024];
            sprintf(tmp, "raw/%s_%d.yuv", player->objid_.c_str(), cnt++);
            FILE* f = fopen(tmp, "w");
            if(f) {
                fwrite(frame->data[0], frame->data_size[0]*frame->height,   1, f);
                fwrite(frame->data[1], frame->data_size[1]*frame->height/2, 1, f);
                fwrite(frame->data[2], frame->data_size[2]*frame->height/2, 1, f);
                fclose(f);
            }
        }
#endif

#ifdef WIN32
        EnterCriticalSection(&player->new_frame_ready_lock_);
#else
        player->new_frame_ready_lock_.lock();
#endif

        vsync = player->vsync_;
        buf_num = n_attempts = player->buf_num_;

        if(frame->objects_data_size > 2) {
            if(player->objs_data_.empty())
                player->objs_data_.assign(reinterpret_cast<const char*>(frame->objects_data),
                                          frame->objects_data_size);
            else if(*player->objs_data_.rbegin() == ']'
                    && frame->objects_data[0] == '['
                    && frame->objects_data[frame->objects_data_size-1] == ']') {
                vn_log(AV_LOG_DEBUG, "new_frame(%d)>: %s - GROWING OBJECTS_DATA!\n", buf_num, inst.c_str());
                *player->objs_data_.rbegin() = ',';
                player->objs_data_.append(reinterpret_cast<const char*>(frame->objects_data + 1),
                                          frame->objects_data_size - 1);
            }
        }

#ifdef WIN32
        LeaveCriticalSection(&player->new_frame_ready_lock_);
#else
        player->new_frame_ready_lock_.unlock();
#endif

#ifdef WIN32
        EnterCriticalSection(player->frame_lock_[buf_pos]);
        memcpy(player->captured_time_[buf_pos], frame->captured_time, sizeof(struct timeval));
        LeaveCriticalSection(player->frame_lock_[buf_pos]);
#else
#if 0
        if(player->frame_lock_[buf_pos]->trylock() != 0) {
            vn_log(AV_LOG_WARNING, "new_frame(%d)>: %s - trylock() FAILED\n", buf_num, inst.c_str());
            player->frame_lock_[buf_pos]->lock();
        }
#endif
        player->frame_lock_[buf_pos]->lock();
        memcpy(player->captured_time_[buf_pos], frame->captured_time, sizeof(struct timeval));
        player->frame_lock_[buf_pos]->unlock();
#endif

#ifdef WIN32
        EnterCriticalSection(&player->new_frame_ready_lock_);
        player->buf_pos_ = buf_pos;
        player->new_frame_ready_ = true;
        memcpy(&player->last_captured_time_, frame->captured_time, sizeof(struct timeval));
        LeaveCriticalSection(&player->new_frame_ready_lock_);
#else
        player->new_frame_ready_lock_.lock();
        player->buf_pos_ = buf_pos;
        player->new_frame_ready_ = true;
        memcpy(&player->last_captured_time_, frame->captured_time, sizeof(struct timeval));
        player->new_frame_ready_lock_.unlock();
#endif

        if(!vsync)
            // emit 'new_frame' ONLY if vsync disabled
            (void)call_tsfn(player->tsfn_new_frame_, client_data, nullptr, k_tsfn_new_frame);
    }
}

/*static*/ void VN_Player::buffer_changed(const vn_player_cache_boundaries_t* bounds, void* client_data)
{
    VN_Player* player = static_cast<VN_Player*>(client_data);

    vn_log(AV_LOG_INFO, "buffer_changed>: %s_%s\n", k_debug_tag.c_str(), player->get_short_objid().c_str());
}

std::string state_to_str(VN_PLAYER_STREAM_STATE state)
{
    std::string state_str;
    switch (state)
    {
        case IDLE:
            state_str="IDLE";
            break;
        case PLAY_FROM_SERVER:
            state_str="PLAY_FROM_SERVER";
            break;
        case PLAY_FROM_BUFFER:
            state_str="PLAY_FROM_BUFFER";
            break;
        case STOPPED:
            state_str="STOPPED";
            break;
        case OPENING:
            state_str="OPENING";
            break;
        case OPENED:
            state_str="OPENED";
            break;
        case BUFFERING:
            state_str="BUFFERING";
            break;
        default:
            state_str="UNKNOWN";
    }

    return state_str;
}

std::string recording_status_to_str(VN_PLAYER_RECORDING_STATUS state)
{
    std::string state_str;
    switch (state)
    {
        case RECORDING_IDLE:
            state_str="RECORDING_IDLE";
            break;
        case RECORDING_STARTED:
            state_str="RECORDING_STARTED";
            break;
        case RECORDING_ENDING:
            state_str="RECORDING_ENDING";
            break;
        case RECORDING_ENDED:
            state_str="RECORDING_ENDED";
            break;
        case RECORDING_ERROR:
            state_str="RECORDING_ERROR";
            break;
        case RECORDING_SKIPPED:
            state_str="RECORDING_SKIPPED";
            break;
        default:
            state_str="UNKNOWN";
    }

    return state_str;
}

/*static*/ void VN_Player::state_changed(VN_PLAYER_STREAM_STATE state, const vn_player_result_status_t* status, void* client_data)
{
    VN_Player* player = static_cast<VN_Player*>(client_data);

    vn_log(AV_LOG_INFO, "state_changed>: %s_%s - state: %d (%s), result status: %d (%s)\n",
           k_debug_tag.c_str(), player->get_short_objid().c_str(), state, state_to_str(state).c_str(),
           status->error_code, status->error_str ? status->error_str : "");

    // only to know the player's last state
    player->state_ = state; // TODO: remove if it is redundant

    state_info_t* p_state_info = new state_info_t;
    p_state_info->state = state;
    p_state_info->code  = status->error_code;
    p_state_info->msg   = status->error_str;
    if (!call_tsfn(player->tsfn_state_changed_, client_data, p_state_info, k_tsfn_state_changed))
        return;

    if (state == VN_PLAYER_STREAM_STATE::OPENED)
        vn_player_play(player->ctx_);
    else if (state == VN_PLAYER_STREAM_STATE::PLAY_FROM_SERVER)
        vn_player_start_recording(player->ctx_, NULL, NULL);
}

/*static*/ void VN_Player::recording_status_changed(const vn_player_recording_status_t* status, void* client_data)
{
    VN_Player* player = static_cast<VN_Player*>(client_data);

    vn_log(AV_LOG_INFO, "recording_status_changed>: %s_%s - state: %d (%s), progress: %d, timestamp: %ld, filename: %s, msg: %s\n",
           k_debug_tag.c_str(), player->get_short_objid().c_str(), status->status, recording_status_to_str(status->status).c_str(),
           status->progress, status->timestamp, status->filename ? status->filename : "", status->error_str ? status->error_str : "");

    recording_info_t* p_rec_info = new recording_info_t;
    p_rec_info->status    = status->status;
    p_rec_info->progress  = status->progress;
    p_rec_info->timestamp = status->timestamp;
    p_rec_info->filename  = status->filename ? status->filename : "";
    p_rec_info->msg       = status->error_str ? status->error_str : "";
    if (!call_tsfn(player->tsfn_recording_status_changed_, client_data, p_rec_info, k_tsfn_recording_status_changed))
        return;
}

/*static*/ void VN_Player::new_stream(const vn_player_stream_info_t* streamInfo, void* client_data)
{
    VN_Player* player = static_cast<VN_Player*>(client_data);

    vn_log(AV_LOG_INFO, "new_stream>: %s_%s\n", k_debug_tag.c_str(), player->get_short_objid().c_str());
}

/*static*/ void VN_Player::stream_removed(const vn_player_stream_info_t* streamInfo, void* client_data)
{
    VN_Player* player = static_cast<VN_Player*>(client_data);

    vn_log(AV_LOG_INFO, "stream_removed>: %s_%s\n", k_debug_tag.c_str(), player->get_short_objid().c_str());
}

/*static*/ void VN_Player::msg_log(const char* msg, void* client_data)
{
    VN_Player* player = static_cast<VN_Player*>(client_data);

    std::string* p_msg = new std::string(msg);
    *p_msg = std::regex_replace(*p_msg, std::regex("^ +| +$|( ) +"), "$1");
    *p_msg = std::regex_replace(*p_msg, std::regex("\n"), "");

    vn_log(AV_LOG_INFO, "msg_log>: %s_%s - %s\n", k_debug_tag.c_str(), player->get_short_objid().c_str(), p_msg->c_str());

    (void)call_tsfn(player->tsfn_msg_log_, client_data, p_msg, k_tsfn_msg_log);
}
