/*
 * This file is part of vimix - video live mixer
 *
 * **Copyright** (C) 2019-2023 Bruno Herbelin <bruno.herbelin@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
**/

#include <gst/gst.h>

//  Desktop OpenGL function loader
#include <glad/glad.h>

#include "Timeline.h"
#include "defines.h"
#include "Log.h"
#include "Resource.h"
#include "Visitor/Visitor.h"
#include "Toolkit/SystemToolkit.h"
#include "Toolkit/BaseToolkit.h"
#include "Toolkit/GstToolkit.h"
#include "Metronome.h"
#include "Settings.h"

#include "MediaPlayer.h"

#ifndef NDEBUG
#define MEDIA_PLAYER_DEBUG
#endif

#if GST_VERSION_MAJOR > 0 && GST_VERSION_MINOR > 18
#define USE_GST_PLAYBIN
#endif

#ifdef USE_GST_OPENGL_SYNC_HANDLER
#include <gst/gl/gl.h>
#include "RenderingManager.h"
#endif

std::list<GstElement*> MediaPlayer::registered_;

MediaPlayer::MediaPlayer()
{
    // create unique id
    id_ = BaseToolkit::uniqueId();

    uri_ = "undefined";
    pipeline_ = nullptr;
    opened_ = false;
    enabled_ = true;
    desired_state_ = GST_STATE_PAUSED;
    audio_enabled_ = false;

    failed_ = false;
    pending_ = false;
    metro_sync_ = Metronome::SYNC_NONE;
    force_update_ = false;
    seeking_ = false;
    force_software_decoding_ = false;
    rate_ = 1.0;
    rate_change_ = RATE_CHANGE_NONE;
    decoder_name_ = "";
    video_filter_ = "";
    video_filter_available_ = true;
    position_ = GST_CLOCK_TIME_NONE;
    loop_ = LoopMode::LOOP_REWIND;
    loop_status_ = LoopStatus::LOOP_STATUS_DEFAULT;
    fading_mode_ = FadingMode::FADING_COLOR;

    // start index in frame_ stack
    write_index_ = 0;
    last_index_ = 0;

    // no PBO by default
    pbo_[0] = pbo_[1] = 0;
    pbo_size_ = 0;
    pbo_index_ = 0;
    pbo_next_index_ = 0;

#ifdef USE_GST_OPENGL_SYNC_HANDLER
    // try to use GLMemory for zero-copy GPU textures
    use_gl_memory_ = Settings::application.render.gst_glmemory_context;
#endif

    // OpenGL texture
    textureindex_ = 0;
}

MediaPlayer::~MediaPlayer()
{
    close();

#ifdef MEDIA_PLAYER_DEBUG
    g_printerr("MediaPlayer %s deleted\n", std::to_string(id_).c_str());
#endif
}

void MediaPlayer::accept(Visitor& v) {
    v.visit(*this);
}

guint MediaPlayer::texture() const
{
    if (textureindex_ == 0)
        return Resource::getTextureBlack();

    return textureindex_;
}

#define LIMIT_DISCOVERER

MediaInfo MediaPlayer::UriDiscoverer(const std::string &uri)
{
#ifdef MEDIA_PLAYER_DEBUG
    Log::Info("Checking uri '%s'", uri.c_str());
#endif

#ifdef LIMIT_DISCOVERER
    // Limiting the number of discoverer thread to TWO in parallel
    // Otherwise, a large number of discoverers are executed (when loading a file)
    // leading to a peak of memory and CPU usage : this causes slow down of FPS
    // and a hungry consumption of RAM.
    static std::mutex mtx_primary;
    static std::mutex mtx_secondary;
    bool use_primary = true;
    if ( !mtx_primary.try_lock() ) { // non-blocking
        use_primary = false;
        mtx_secondary.lock(); // blocking
    }
#endif

    // returned value
    MediaInfo video_stream_info;

    // if uri is valid
    if ( SystemToolkit::file_exists( gst_uri_get_location(uri.c_str()) ) ) {

        GError *err = NULL;
        GstDiscoverer *discoverer = gst_discoverer_new (DISCOVER_TIMOUT * GST_SECOND, &err);

        /* Instantiate the Discoverer */
        if (!discoverer) {
            Log::Warning("MediaPlayer Error creating discoverer instance: %s\n", err->message);
        }
        else {
            // disable GPU decoding plugins to avoid conflicts during discovery
            // GstToolkit::enable_gpu_decoding_plugins(false);
            //// TODO : implement this properly as this disables GPU decoding globally

            GstDiscovererInfo *info = NULL;
            info = gst_discoverer_discover_uri (discoverer, uri.c_str(), &err);
            GstDiscovererResult result = gst_discoverer_info_get_result (info);
            switch (result) {
            case GST_DISCOVERER_URI_INVALID:
                video_stream_info.log = "Invalid URI";
                break;
            case GST_DISCOVERER_ERROR:
                video_stream_info.log = std::string( "Warning: " ) + err->message;
                result = GST_DISCOVERER_OK; // try to read the file anyways, discoverer can report errors but still read the file
                break;
            case GST_DISCOVERER_TIMEOUT:
                video_stream_info.log = "Timeout loading";
                break;
            case GST_DISCOVERER_BUSY:
                video_stream_info.log = "Busy";
                break;
            case GST_DISCOVERER_MISSING_PLUGINS:
            {
                const GstStructure *s = gst_discoverer_info_get_misc (info);
                gchar *str = gst_structure_to_string (s);
                video_stream_info.log = std::string( "Warning: " ) + std::string(str);
                g_free (str);
                result = GST_DISCOVERER_OK; // try to read the file anyways, discoverer can report errors but still read the file
            }
                break;
            default:
                break;
            }

            if ( result == GST_DISCOVERER_OK ) {
                // get videos in information found
                GList *streams = gst_discoverer_info_get_video_streams(info);
                if ( g_list_length(streams) > 0) {
                    GList *tmp;
                    for (tmp = streams; tmp && !video_stream_info.valid; tmp = tmp->next ) {
                        GstDiscovererStreamInfo *tmpinf = (GstDiscovererStreamInfo *) tmp->data;
                        if ( GST_IS_DISCOVERER_VIDEO_INFO(tmpinf) )
                        {
                            // found a video / image stream : fill-in information
                            GstDiscovererVideoInfo* vinfo = GST_DISCOVERER_VIDEO_INFO(tmpinf);
                            video_stream_info.width = gst_discoverer_video_info_get_width(vinfo);
                            video_stream_info.height = gst_discoverer_video_info_get_height(vinfo);
                            guint parn = gst_discoverer_video_info_get_par_num(vinfo);
                            guint pard = gst_discoverer_video_info_get_par_denom(vinfo);
                            video_stream_info.par_width = (video_stream_info.width * parn) / pard;
                            video_stream_info.interlaced = gst_discoverer_video_info_is_interlaced(vinfo);
                            video_stream_info.bitrate = gst_discoverer_video_info_get_bitrate(vinfo);
                            video_stream_info.isimage = gst_discoverer_video_info_is_image(vinfo);
                            // if its a video, set duration, framerate, etc.
                            if ( !video_stream_info.isimage ) {
                                video_stream_info.end = gst_discoverer_info_get_duration (info) ;
                                video_stream_info.seekable = gst_discoverer_info_get_seekable (info);
                                video_stream_info.framerate_n = gst_discoverer_video_info_get_framerate_num(vinfo);
                                video_stream_info.framerate_d = gst_discoverer_video_info_get_framerate_denom(vinfo);
                                if (video_stream_info.framerate_n == 0 || video_stream_info.framerate_d == 0) {
                                    Log::Info("'%s': No framerate indicated in the file; using default 30fps", uri.c_str());
                                    video_stream_info.framerate_n = 30;
                                    video_stream_info.framerate_d = 1;
                                }
                                video_stream_info.dt = ( (GST_SECOND * static_cast<guint64>(video_stream_info.framerate_d)) / (static_cast<guint64>(video_stream_info.framerate_n)) );
                                // confirm (or infirm) that its not a single frame
                                if ( video_stream_info.end < video_stream_info.dt * 2)
                                    video_stream_info.isimage = true;
                            }
                            // try to fill-in the codec information
                            GstCaps *caps = gst_discoverer_stream_info_get_caps (tmpinf);
                            if (caps) {
                                gchar *codecstring = gst_pb_utils_get_codec_description(caps);
                                video_stream_info.codec_name = std::string( codecstring );
                                g_free(codecstring);
                                gst_caps_unref (caps);
                            }
                            const GstTagList *tags = gst_discoverer_stream_info_get_tags(tmpinf);
                            if ( tags ) {
                                gchar *container = NULL;
                                if ( gst_tag_list_get_string (tags, GST_TAG_CONTAINER_FORMAT, &container) )
                                    video_stream_info.codec_name += ", " + std::string(container);
                                if (container)
                                    g_free(container);
                            }
                            // exit loop
                            // inform that it succeeded
                            video_stream_info.valid = true;
                        }
                    }

                    if (!video_stream_info.valid)
                        video_stream_info.log = "Invalid video stream";
                }
                else
                    video_stream_info.log = "No video stream";

                gst_discoverer_stream_info_list_free(streams);

                // test audio
                GList *audios = gst_discoverer_info_get_audio_streams(info);
                video_stream_info.hasaudio = g_list_length(audios) > 0 && Settings::application.accept_audio;
                gst_discoverer_stream_info_list_free(audios);
            }

            if (info)
                gst_discoverer_info_unref (info);

            g_object_unref( discoverer );

            // restore GPU decoding plugins state
            // GstToolkit::enable_gpu_decoding_plugins(Settings::application.render.gpu_decoding);

        }

        g_clear_error (&err);

    }
    else {
        video_stream_info.log = "No such file";
    }

#ifdef LIMIT_DISCOVERER
    if (use_primary)
        mtx_primary.unlock();
    else
        mtx_secondary.unlock();
#endif
    // return the info
    return video_stream_info;
}

void MediaPlayer::open (const std::string & filename, const std::string &uri)
{
    // set path
    filename_ = BaseToolkit::transliterate( filename );

    // set uri to open
    if (uri.empty())
        uri_ = GstToolkit::filename_to_uri( filename );
    else
        uri_ = uri;

    if (uri_.empty())
        failed_ = true;

    // close before re-openning
    if (isOpen())
        close();

    // start URI discovering thread:
    discoverer_ = std::async( MediaPlayer::UriDiscoverer, uri_);
    // wait for discoverer to finish in the future (test in update)

//    // debug without thread
//    media_ = MediaPlayer::UriDiscoverer(uri_);
//    if (media_.valid) {
//        timeline_.setEnd( media_.end );
//        timeline_.setStep( media_.dt );
//        execute_open();
//    }

}


void MediaPlayer::reopen()
{
    // re-openning is meaningfull only if it was already open
    if (pipeline_ != nullptr && opened_) {
        // reload : terminate pipeline and re-create it
        close();
        execute_open();
    }
}


GstBusSyncReply MediaPlayer::signal_handler(GstBus *, GstMessage *msg, gpointer ptr)
{
    // only handle error messages
    if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ERROR && ptr != nullptr) {
        // register failure in source
        reinterpret_cast<MediaPlayer *>(ptr)->failed_ = true;

        // inform user
        GError *error;
        gst_message_parse_error(msg, &error, NULL);
        Log::Warning("MediaPlayer %s : %s",
                         std::to_string(reinterpret_cast<MediaPlayer*>(ptr)->id()).c_str(),
                         error->message);
        g_error_free(error);
    }
#ifdef USE_GST_OPENGL_SYNC_HANDLER
    // setup OpenGL contexts for GStreamer elements from global Rendering opengl 
    else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_NEED_CONTEXT) {
        const gchar* contextType;
        gst_message_parse_context_type(msg, &contextType);

        if (!g_strcmp0(contextType, GST_GL_DISPLAY_CONTEXT_TYPE) && Rendering::manager().global_display) {
            GstContext *displayContext = gst_context_new(GST_GL_DISPLAY_CONTEXT_TYPE, TRUE);
            gst_context_set_gl_display(displayContext, Rendering::manager().global_display);
            gst_element_set_context(GST_ELEMENT(msg->src), displayContext);
            gst_context_unref (displayContext);
        }
        if (!g_strcmp0(contextType, "gst.gl.app_context") && Rendering::manager().global_gl_context) {
            GstContext *appContext = gst_context_new("gst.gl.app_context", TRUE);
            GstStructure* structure = gst_context_writable_structure(appContext);
            gst_structure_set(structure, "context", GST_TYPE_GL_CONTEXT, Rendering::manager().global_gl_context, nullptr);
            gst_element_set_context(GST_ELEMENT(msg->src), appContext);
            gst_context_unref (appContext);
        }
    }
#endif

    // drop all messages to avoid filling up the stack
    gst_message_unref (msg);
    return GST_BUS_DROP;
}

#ifdef USE_GST_PLAYBIN

// GstPlayFlags flags from playbin. It is the policy of GStreamer to
// not publicly expose element-specific enums. That's why this
// GstPlayFlags enum has been copied here.
// https://gstreamer.freedesktop.org/documentation/playback/playsink.html?gi-language=c#GstPlayFlags
typedef enum {
    GST_PLAY_FLAG_VIDEO         = 0x00000001,
    GST_PLAY_FLAG_AUDIO         = 0x00000002,
    GST_PLAY_FLAG_TEXT          = 0x00000004,
    GST_PLAY_FLAG_VIS           = 0x00000008,
    GST_PLAY_FLAG_SOFT_VOLUME   = 0x00000010,
    GST_PLAY_FLAG_NATIVE_AUDIO  = 0x00000020,
    GST_PLAY_FLAG_NATIVE_VIDEO  = 0x00000040,
    GST_PLAY_FLAG_DOWNLOAD      = 0x00000080,
    GST_PLAY_FLAG_BUFFERING     = 0x00000100,
    GST_PLAY_FLAG_DEINTERLACE   = 0x00000200,
    GST_PLAY_FLAG_SW_COLOR      = 0x00000400,
    GST_PLAY_FLAG_FORCE_FILTER  = 0x00000800,
    GST_PLAY_FLAG_SW_DECODER    = 0x00001000
} GstPlayFlags;

//
// Setup a media player using gstreamer playbin
//
void MediaPlayer::execute_open()
{
    // create playbin
    pipeline_ = gst_element_factory_make ("playbin", std::to_string(id_).c_str());

    // set uri of file to open
    g_object_set ( G_OBJECT (pipeline_),  "uri", uri_.c_str(),  NULL);

    // connect to callback for elements monitoring
    g_signal_connect ( G_OBJECT (pipeline_), "element-setup", G_CALLBACK (callback_element_setup), this);

    // Get and modify playbin flags
    gint flags;
    // ENABLE ONLY VIDEO, NOT AUDIO AND TEXT SUBTITLES
    flags = GST_PLAY_FLAG_VIDEO;
    if (media_.hasaudio && audio_enabled_)
        flags |= GST_PLAY_FLAG_AUDIO;
    // ENABLE DEINTERLACING
    if (media_.interlaced)
        flags |= GST_PLAY_FLAG_DEINTERLACE;
    // ENABLE/DISABLE FORCE SOFTWARE DECODING
    if (force_software_decoding_ || media_.isimage) {
        flags |= GST_PLAY_FLAG_SW_COLOR;
        flags |= GST_PLAY_FLAG_SW_DECODER;
    }
    g_object_set( G_OBJECT (pipeline_), "flags", flags, NULL);

    // hack to compensate for lack of PTS in gif animations
    if (media_.codec_name.compare("image/gst-libav-gif") == 0) {
        video_filter_ = "videorate rate=1.0";
        video_filter_available_ = false;
    }

    // hack to simulate a playable and seekable stream with an image
    if (media_.isimage && !singleFrame()) {
        media_.seekable = true;
        video_filter_ = "imagefreeze";
        video_filter_available_ = false;
    }

    // Add a filter to playbin if provided
    if ( !video_filter_.empty()) {
        // try to create the pipeline element given
        GError *error = NULL;
        GstElement *effect = gst_parse_launch( video_filter_.c_str(), &error);
        if (effect == NULL || error != NULL) {
            if (error != NULL)
                Log::Warning("MediaPlayer %s Error constructing filter %s:\n%s", std::to_string(id_).c_str(), video_filter_.c_str(), error->message);
            g_clear_error (&error);
            if (effect)
                gst_object_unref(effect);
        }
        else {
            g_object_set( G_OBJECT (pipeline_), "video-filter", effect, nullptr);
            Log::Info("MediaPlayer %s Added filter '%s'", std::to_string(id_).c_str(), video_filter_.c_str());
        }
    }

    // setup appsink
    GstElement *sink = gst_element_factory_make ("appsink", "appsink");
    if (!sink) {
        Log::Warning("MediaPlayer %s Could not configure  sink", std::to_string(id_).c_str());
        failed_ = true;
        return;
    }

    // instruct the sink to send samples synched in time
    gst_base_sink_set_sync (GST_BASE_SINK(sink), true);

    // Configure appsink caps
    GstCaps *caps = nullptr;

#ifdef USE_GST_OPENGL_SYNC_HANDLER
    if (force_software_decoding_ || media_.isimage)
        use_gl_memory_ = false; // disable GLMemory for images to avoid issues with some plugins

    // Try to create glsinkbin to automatically handle GL upload and conversion
    GstElement *glsinkbin = gst_element_factory_make("glsinkbin", "glsink");
    if (!glsinkbin) {
        use_gl_memory_ = false;
        Log::Info("MediaPlayer %s failed to get glsinkbin for %s decoder.", std::to_string(id_).c_str(), decoderName().c_str());
    }

    // When using glsinkbin, appsink must accept GLMemory caps
    if (use_gl_memory_) {
        // Create caps that accept BOTH GLMemory and system memory
        // This allows glsinkbin to link properly
        caps = gst_caps_new_simple("video/x-raw",
                                   "format", G_TYPE_STRING, "RGBA",
                                   "width",  G_TYPE_INT, media_.width,
                                   "height", G_TYPE_INT, media_.height,
                                   NULL);

        // Add GLMemory variant
        GstCaps *gl_caps = gst_caps_new_simple("video/x-raw",
                                               "format", G_TYPE_STRING, "RGBA",
                                                "width",  G_TYPE_INT, media_.width,
                                                "height", G_TYPE_INT, media_.height,
                                               NULL);
        GstCapsFeatures *gl_features = gst_caps_features_new("memory:GLMemory", NULL);
        gst_caps_set_features(gl_caps, 0, gl_features);

        // Append GLMemory caps as preferred option
        gst_caps_append(gl_caps, caps);
        caps = gl_caps;
    }
    else
#endif
    {
        // Standard CPU caps with dimensions
        caps = gst_caps_new_simple("video/x-raw",
                                   "format", G_TYPE_STRING, "RGBA",
                                   "width",  G_TYPE_INT, media_.width,
                                   "height", G_TYPE_INT, media_.height,
                                   NULL);
    }

    gst_app_sink_set_caps (GST_APP_SINK(sink), caps);
    gst_caps_unref (caps);

    // Instruct appsink to drop old buffers when the maximum amount of queued buffers is reached.
    gst_app_sink_set_max_buffers( GST_APP_SINK(sink), 5);
    gst_app_sink_set_drop (GST_APP_SINK(sink), true);

    // set message handler for the pipeline's bus
    bus_ = gst_element_get_bus(pipeline_);
    gst_bus_set_sync_handler(bus_, MediaPlayer::signal_handler, this, NULL);

    // set the callbacks
    GstAppSinkCallbacks callbacks;
#if GST_VERSION_MINOR > 18 && GST_VERSION_MAJOR > 0
    callbacks.new_event = NULL;
#if GST_VERSION_MINOR > 23
    callbacks.propose_allocation = NULL;
#endif
#endif
    callbacks.new_preroll = callback_new_preroll;
    if (singleFrame()) {
        callbacks.eos = NULL;
        callbacks.new_sample = NULL;
    }
    else {
        callbacks.eos = callback_end_of_stream;
        callbacks.new_sample = callback_new_sample;
    }
    gst_app_sink_set_callbacks (GST_APP_SINK(sink), &callbacks, this, NULL);
    gst_app_sink_set_emit_signals (GST_APP_SINK(sink), false);

    // Wrap appsink with glsinkbin for automatic GLMemory handling
    GstElement *video_sink = sink;  // Default to appsink

#ifdef USE_GST_OPENGL_SYNC_HANDLER
    if (use_gl_memory_) {
        // glsinkbin wraps our appsink and automatically inserts:
        // - glupload (for GLMemory)
        // - glcolorconvert (for NV12/I420 -> RGBA conversion in shaders)
        g_object_set(G_OBJECT(glsinkbin), "sink", sink, NULL);
        video_sink = glsinkbin;
    }
#endif

    // set playbin sink (either glsinkbin wrapper or direct appsink)
    g_object_set ( G_OBJECT (pipeline_), "video-sink", video_sink, NULL);

    // set to desired state (PLAY or PAUSE)
    GstStateChangeReturn ret = gst_element_set_state (GST_ELEMENT(pipeline_), desired_state_);
    if (ret == GST_STATE_CHANGE_FAILURE) {
        Log::Warning("MediaPlayer %s Could not open '%s'", std::to_string(id_).c_str(), uri_.c_str());
        failed_ = true;
        return;
    }

    // in case discoverer failed to get duration of a video
    if (!media_.isimage && timeline_.end() == GST_CLOCK_TIME_NONE) {
        gint64 d = GST_CLOCK_TIME_NONE;
        if ( gst_element_query_duration(pipeline_, GST_FORMAT_TIME, &d) )
            timeline_.setEnd(d);
    }

    // all good
    Log::Info("MediaPlayer %s Opened '%s' (%s %d x %d, %d kbps)", std::to_string(id_).c_str(),
              SystemToolkit::filename(uri_).c_str(), media_.codec_name.c_str(), 
              media_.width, media_.height, media_.bitrate / 1000);

    if (!singleFrame())
        Log::Info("MediaPlayer %s Timeline [%ld %ld] %ld frames, %d gaps", std::to_string(id_).c_str(),
                  timeline_.begin(), timeline_.end(), timeline_.numFrames(), timeline_.numGaps());

    if (media_.hasaudio)
        Log::Info("MediaPlayer %s Audio track %s", std::to_string(id_).c_str(), audio_enabled_ ? "enabled" : "disabled");

    // keep name in pipeline
    gst_element_set_name(pipeline_, std::to_string(id_).c_str());

    // register media player
    MediaPlayer::registered_.push_back(pipeline_);

    opened_ = true;
}

#else

//
// Setup a media player using gstreamer custom pipeline
//
void MediaPlayer::execute_open()
{
    // filters and audio are not available without playbin
    video_filter_available_ = false;
    audio_enabled_ = false;
    media_.hasaudio = false;

    // Create gstreamer pipeline :
    //         "uridecodebin uri=file:///path_to_file/filename.mp4 ! videoconvert ! appsink "
    // equivalent to command line
    //         "gst-launch-1.0 uridecodebin uri=file:///path_to_file/filename.mp4 ! videoconvert ! ximagesink"
    std::string description = "uridecodebin name=decoder uri=" + uri_ + " ! ";
    // NB: queue adds some control over the buffer, thereby limiting the frame delay. zero size means no buffering

    // video deinterlacing method (if media is interlaced)
    //      tomsmocomp (0) – Motion Adaptive: Motion Search
    //      greedyh (1) – Motion Adaptive: Advanced Detection
    //      greedyl (2) – Motion Adaptive: Simple Detection
    //      vfir (3) – Blur Vertical
    //      linear (4) – Linear
    //      scalerbob (6) – Double lines
    if (media_.interlaced)
        description += "deinterlace method=2 ! ";

    // Add a filter to playbin if provided
    if ( !video_filter_.empty()) {
        description += "videoconvert chroma-resampler=1 dither=0 ! "; // fast
        description += video_filter_ + " ! ";
    }

    // video convertion algorithm (should only do colorspace conversion, no scaling)
    // chroma-resampler:
    //      Duplicates the samples when upsampling and drops when downsampling 0
    //      Uses linear interpolation 1 (default)
    //      Uses cubic interpolation 2
    //      Uses sinc interpolation 3
    //  dither:
    //      no dithering 0
    //      propagate rounding errors downwards 1
    //      Dither with floyd-steinberg error diffusion 2
    //      Dither with Sierra Lite error diffusion 3
    //      ordered dither using a bayer pattern 4 (default)
    description += "videoconvert chroma-resampler=1 dither=0 ! "; // fast

    // hack to compensate for lack of PTS in gif animations
    if (media_.codec_name.compare("image/gst-libav-gif") == 0){
        description += "videorate ! video/x-raw,framerate=";
        description += std::to_string(media_.framerate_n) + "/";
        description += std::to_string(media_.framerate_d) + " ! ";
    }

    // hack to simulate a playable and seekable stream with an image
    if (media_.isimage && !singleFrame()) {
        media_.seekable = true;
        description += "imagefreeze ! ";
    }

    // set app sink
    description += "queue ! appsink name=sink";

    // parse pipeline descriptor
    GError *error = NULL;
    pipeline_ = gst_parse_launch (description.c_str(), &error);
    if (error != NULL) {
        Log::Warning("MediaPlayer %s Could not construct pipeline %s:\n%s", std::to_string(id_).c_str(), description.c_str(), error->message);
        g_clear_error (&error);
        failed_ = true;
        return;
    }
#ifdef MEDIA_PLAYER_DEBUG
    Log::Info("MediaPlayer %s Pipeline [%s]", std::to_string(id_).c_str(), description.c_str());
#endif
    gst_object_ref(pipeline_);

    // setup pipeline
    g_object_set(G_OBJECT(pipeline_), "name", std::to_string(id_).c_str(), NULL);
    gst_pipeline_set_auto_flush_bus( GST_PIPELINE(pipeline_), true);

    // setup software decode
    if (force_software_decoding_ || media_.isimage) {
        g_object_set (G_OBJECT (gst_bin_get_by_name (GST_BIN (pipeline_), "decoder")), "force-sw-decoders", true,  NULL);
    }

    // setup appsink
    GstElement *sink = gst_bin_get_by_name (GST_BIN (pipeline_), "sink");
    if (!sink) {
        Log::Warning("MediaPlayer %s Could not configure  sink", std::to_string(id_).c_str());
        failed_ = true;
        return;
    }

    // instruct the sink to send samples synched in time
    gst_base_sink_set_sync (GST_BASE_SINK(sink), true);

    // instruct sink to use the required caps
    std::string capstring = "video/x-raw,format=RGBA,width="+ std::to_string(media_.width) +
            ",height=" + std::to_string(media_.height);
    GstCaps *caps = gst_caps_from_string(capstring.c_str());
    gst_app_sink_set_caps (GST_APP_SINK(sink), caps);

    // Instruct appsink to drop old buffers when the maximum amount of queued buffers is reached.
    gst_app_sink_set_max_buffers( GST_APP_SINK(sink), 5);
    gst_app_sink_set_drop (GST_APP_SINK(sink), true);

    // set the callbacks
    GstAppSinkCallbacks callbacks;
#if GST_VERSION_MINOR > 18 && GST_VERSION_MAJOR > 0
    callbacks.new_event = NULL;
#if GST_VERSION_MINOR > 23
    callbacks.propose_allocation = NULL;
#endif
#endif
    callbacks.new_preroll = callback_new_preroll;
    if (singleFrame()) {
        callbacks.eos = NULL;
        callbacks.new_sample = NULL;
    }
    else {
        callbacks.eos = callback_end_of_stream;
        callbacks.new_sample = callback_new_sample;
    }
    gst_app_sink_set_callbacks (GST_APP_SINK(sink), &callbacks, this, NULL);
    gst_app_sink_set_emit_signals (GST_APP_SINK(sink), false);

    // done with ref to sink
    gst_object_unref (sink);
    gst_caps_unref (caps);

    // set to desired state (PLAY or PAUSE)
    GstStateChangeReturn ret = gst_element_set_state (pipeline_, desired_state_);
    if (ret == GST_STATE_CHANGE_FAILURE) {
        Log::Warning("MediaPlayer %s Could not open '%s'", std::to_string(id_).c_str(), uri_.c_str());
        failed_ = true;
        return;
    }

    // in case discoverer failed to get duration of a video
    if (!media_.isimage && timeline_.end() == GST_CLOCK_TIME_NONE) {
        gint64 d = GST_CLOCK_TIME_NONE;
        if ( gst_element_query_duration(pipeline_, GST_FORMAT_TIME, &d) )
            timeline_.setEnd(d);
    }

    bus_ = gst_element_get_bus(pipeline_);
    // set message handler for the pipeline's bus
    gst_bus_set_sync_handler(bus_, MediaPlayer::signal_handler, this, NULL);

    // all good
    Log::Info("MediaPlayer %s Opened '%s' (%s %d x %d)", std::to_string(id_).c_str(),
              SystemToolkit::filename(uri_).c_str(), media_.codec_name.c_str(), media_.width, media_.height);

    if (!singleFrame())
        Log::Info("MediaPlayer %s Timeline [%ld %ld] %ld frames, %d gaps", std::to_string(id_).c_str(),
                  timeline_.begin(), timeline_.end(), timeline_.numFrames(), timeline_.numGaps());

    opened_ = true;

    // keep name in pipeline
    gst_element_set_name(pipeline_, std::to_string(id_).c_str());

    // register media player
    MediaPlayer::registered_.push_back(pipeline_);
}

#endif


void MediaPlayer::setVideoEffect(const std::string &pipeline_element)
{
    if ( !pipeline_element.empty() ) {
        GError *error = NULL;
        GstElement *effect = gst_parse_launch( pipeline_element.c_str(), &error);
        if (effect == NULL || error != NULL) {
            Log::Warning("MediaPlayer %s Error constructing %s:\n%s", std::to_string(id_).c_str(), video_filter_.c_str(), error->message);
            g_clear_error (&error);
            if (effect)
                gst_object_unref(effect);
            return ;
        }
        // cleanup
        g_clear_error (&error);
        if (effect)
            gst_object_unref(effect);
    }

    video_filter_ = pipeline_element;
    reopen();
}

bool MediaPlayer::isOpen() const
{
    return opened_;
}

bool MediaPlayer::failed() const
{
    return failed_;
}

void MediaPlayer::pipeline_terminate( GstElement *p, GstBus *b )
{
    gchar *name = gst_element_get_name(p);
#ifdef MEDIA_PLAYER_DEBUG
    g_printerr("MediaPlayer %s closing\n", name);
#endif

    // end pipeline
    GstStateChangeReturn ret = gst_element_set_state(p, GST_STATE_NULL);
    if (ret == GST_STATE_CHANGE_ASYNC)
        gst_element_get_state(p, NULL, NULL, 1000000);

    // empty pipeline bus 
    GstMessage *msg = NULL;
    do {
        if (msg)
            gst_message_unref (msg);
        msg = gst_bus_timed_pop_filtered(b, 1000000, GST_MESSAGE_ANY);
    } while (msg != NULL);

    // unref bus
    gst_object_unref( GST_OBJECT(b) );

    // unref to free pipeline
    while (GST_OBJECT_REFCOUNT_VALUE(p) > 0)
        gst_object_unref( GST_OBJECT(p) );

    // all done
    Log::Info("MediaPlayer %s Closed", name);
    g_free(name);

    // unregister
    MediaPlayer::registered_.remove(p);
}

void MediaPlayer::close()
{
    // not opened?
    if (!opened_) {
        // wait for loading to finish
        if (discoverer_.valid())
            if ( discoverer_.wait_for(std::chrono::seconds(DISCOVER_TIMOUT)) == std::future_status::timeout )
                 failed_ = true;
        // nothing else to change
        return;
    }

    // un-ready the media player
    opened_ = false;
    failed_ = false;
    pending_ = false;
    seeking_ = false;
    force_update_ = false;
    rate_ = 1.0;
    rate_change_ = RATE_CHANGE_NONE;
    position_ = GST_CLOCK_TIME_NONE;

    // cleanup eventual remaining frame memory
    for(guint i = 0; i < N_VFRAME; i++) {
        frame_[i].access.lock();
        if (frame_[i].buffer) {
            gst_buffer_unref(frame_[i].buffer);
            frame_[i].buffer = NULL;
        }
        frame_[i].status = INVALID;
        frame_[i].access.unlock();
    }
    write_index_ = 0;
    last_index_ = 0;

    // clean up GST
    if (pipeline_ != nullptr) {
        // end pipeline asynchronously
        std::thread(MediaPlayer::pipeline_terminate, pipeline_, bus_).detach();
        // immediately invalidate access for other methods
        pipeline_ = nullptr;
        bus_ = nullptr;
    }

    // cleanup opengl texture
    if (textureindex_) {
        glDeleteTextures(1, &textureindex_);
        textureindex_ = 0;
    }

    // cleanup picture buffer
    if (pbo_[0]) {
        glDeleteBuffers(2, pbo_);
        pbo_[0] = 0;
        pbo_size_ = 0;
    }
}


guint MediaPlayer::width() const
{
    return media_.width;
}

guint MediaPlayer::height() const
{
    return media_.height;
}

float MediaPlayer::aspectRatio() const
{
    return static_cast<float>(media_.par_width) / static_cast<float>(media_.height);
}

GstClockTime MediaPlayer::position()
{
    if (position_ == GST_CLOCK_TIME_NONE && pipeline_ != nullptr) {
        gint64 p = GST_CLOCK_TIME_NONE;
        if ( gst_element_query_position (pipeline_, GST_FORMAT_TIME, &p) )
            position_ = p;
    }

    return position_;
}

void MediaPlayer::enable(bool on)
{
    if ( !opened_ || pipeline_ == nullptr)
        return;

    if ( enabled_ != on ) {

        // apply change
        enabled_ = on;

        // default to pause
        GstState requested_state = GST_STATE_PAUSED;

        // unpause only if enabled
        if (enabled_)
            requested_state = desired_state_;

        //  apply state change
        GstStateChangeReturn ret = gst_element_set_state (pipeline_, requested_state);
        if (ret == GST_STATE_CHANGE_FAILURE) {
            Log::Warning("MediaPlayer %s Failed to enable", std::to_string(id_).c_str());
            failed_ = true;
        }

    }
}

bool MediaPlayer::isEnabled() const
{
    return enabled_;
}

bool MediaPlayer::isImage() const
{
    return media_.isimage;
}

bool MediaPlayer::singleFrame() const
{
    if (media_.isimage)
        return timeline_.end() == GST_CLOCK_TIME_NONE;
    return false;
}

std::string MediaPlayer::decoderName()
{
    if (decoder_name_.empty()) {
        // try to know if it is a hardware decoder
        if (pipeline_)
            decoder_name_ = GstToolkit::used_gpu_decoding_plugins(pipeline_);
        // nope, then it is a sofware decoder
        if (decoder_name_.empty())
            decoder_name_ = "software";
    }

    return decoder_name_;
}

bool MediaPlayer::softwareDecodingForced()
{
    return force_software_decoding_;
}

void MediaPlayer::setSoftwareDecodingForced(bool on)
{
    bool need_reload = force_software_decoding_ != on;

    // set parameter
    force_software_decoding_ = on;
    decoder_name_ = "";

    // changing state requires reload
    if (need_reload)
        reopen();
}

void MediaPlayer::execute_play_command(bool on)
{
    // request state
    GstState requested_state = on ? GST_STATE_PLAYING : GST_STATE_PAUSED;

    // ignore if requesting twice same state
    if (desired_state_ == requested_state)
        return;

    // accept request to the desired state
    desired_state_ = requested_state;

    // if not ready yet, the requested state will be handled later
    if ( !opened_ || pipeline_ == nullptr )
        return;

    // requesting to play, but stopped at end of stream : rewind first !
    if ( desired_state_ == GST_STATE_PLAYING) {
        if (rate_ > 0.0 && position_ >= timeline_.previous(timeline_.last()))
            execute_seek_command(timeline_.next(0));
        else if ( rate_ < 0.0 && position_ <= timeline_.next(0)  )
            execute_seek_command(timeline_.previous(timeline_.last()));
    }

    // all ready, apply state change immediately
    GstStateChangeReturn ret = gst_element_set_state (pipeline_, desired_state_);
    if (ret == GST_STATE_CHANGE_FAILURE) {
        Log::Warning("MediaPlayer %s Failed to play", std::to_string(id_).c_str());
        failed_ = true;
    }
#ifdef MEDIA_PLAYER_DEBUG
    else if (on)
        Log::Info("MediaPlayer %s Start", std::to_string(id_).c_str());
    else
        Log::Info("MediaPlayer %s Stop [%ld]", std::to_string(id_).c_str(), position());
#endif

    // Revert loop status to default when playing
    if (on)
        loop_status_ = LoopStatus::LOOP_STATUS_DEFAULT;
}

void MediaPlayer::play(bool on)
{
    // ignore if disabled, and cannot play a single frame
    if (!enabled_ || pending_ || singleFrame())
        return;

    // Metronome
    if (metro_sync_ > Metronome::SYNC_NONE) {
        // busy with this delayed action
        pending_ = true;

        // delayed execution function
         std::function<void()> playlater = std::bind([](MediaPlayer *p, bool o) {
                 p->execute_play_command(o); p->pending_=false; }, this, on);
        // Execute: sync to Metronome
        if (metro_sync_ > Metronome::SYNC_BEAT)
            Metronome::manager().executeAtPhase( playlater );
        else
            Metronome::manager().executeAtBeat( playlater );
    }
    else
        // execute immediately
        execute_play_command( on );
}

bool MediaPlayer::isPlaying(bool testpipeline) const
{
    // image cannot play
    if (singleFrame())
        return false;

    // if not ready yet, answer with requested state
    if ( !testpipeline || !opened_ || pipeline_ == nullptr || !enabled_)
        return desired_state_ == GST_STATE_PLAYING;

    // if ready, answer with actual state
    GstState state;
    gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
    return state == GST_STATE_PLAYING;
}


MediaPlayer::LoopMode MediaPlayer::loop() const
{
    return loop_;
}

void MediaPlayer::setLoop(MediaPlayer::LoopMode mode)
{
    loop_ = mode;
}

void MediaPlayer::rewind(bool force)
{
    if (!enabled_ || !media_.seekable || pending_)
        return;

    // playing forward, loop to begin;
    //          begin is the end of a gab which includes the first PTS (if exists)
    //          normal case, begin is zero
    // playing backward, loop to endTimeInterval gap;
    //          end is the start of a gab which includes the last PTS (if exists)
    //          normal case, end is last frame
    GstClockTime target = (rate_ > 0.0) ? timeline_.next(0) : timeline_.previous(timeline_.last());

    // Metronome
    if (metro_sync_) {
        // busy with this delayed action
        pending_ = true;
        // delayed execution function
         std::function<void()> rewindlater = std::bind([](MediaPlayer *p, GstClockTime t, bool f) {
                 p->execute_seek_command( t, f ); p->pending_=false; }, this, target, force);
        // Execute: sync to Metronome
        if (metro_sync_ > Metronome::SYNC_BEAT)
            Metronome::manager().executeAtPhase( rewindlater );
        else
            Metronome::manager().executeAtBeat( rewindlater );
    }
    else
        // execute immediately
        execute_seek_command( target, force );
}


void MediaPlayer::step(uint milisecond)
{
    // useful only when Paused
    if (!enabled_ || isPlaying() || pending_)
        return;

    if ( ( rate_ < 0.0 && position_ <= timeline_.next(0)  )
         || ( rate_ > 0.0 && position_ >= timeline_.previous(timeline_.last()) ) )
        rewind();
    else {
        // step event
        if (milisecond < media_.dt)
            milisecond = timeline_.step() < media_.dt ? timeline_.step() : media_.dt;
        GstEvent *stepevent = gst_event_new_step (GST_FORMAT_TIME, milisecond, ABS(rate_), TRUE,  FALSE);

        // Metronome
        if (metro_sync_) {
            // busy with this delayed action
            pending_ = true;
            // delayed execution function
             std::function<void()> steplater = std::bind([](MediaPlayer *p, GstEvent *e) {
                     gst_element_send_event(p->pipeline_, e); p->pending_=false; }, this, stepevent) ;
            // Execute: sync to Metronome
            if (metro_sync_ > Metronome::SYNC_BEAT)
                Metronome::manager().executeAtPhase( steplater );
            else
                Metronome::manager().executeAtBeat( steplater );
        }
        else
            // execute immediately
            gst_element_send_event (pipeline_, stepevent);

    }
}

bool MediaPlayer::go_to(GstClockTime pos)
{
    bool ret = false;
    TimeInterval gap;
    if (pos != GST_CLOCK_TIME_NONE ) {

        GstClockTime jumpPts = pos;

        // jump in a gap
        if (timeline_.getGapAt(pos, gap)) {
            // if in a gap, find closest seek target
            if (gap.is_valid()) {
                // jump in one or the other direction
                jumpPts = (rate_>0.f) ? gap.end : gap.begin;
            }
        }

        if (ABS_DIFF (position_, jumpPts) > 2 * timeline_.step() ) {
            ret = true;
            seek( jumpPts );
            
            // Revert loop status to default
            loop_status_ = LoopStatus::LOOP_STATUS_DEFAULT;
        }
    }
    return ret;
}

void MediaPlayer::seek(GstClockTime pos)
{
    if (!enabled_ || !media_.seekable || seeking_)
        return;

    // apply seek
    GstClockTime target = CLAMP(pos, timeline_.begin(), timeline_.end());
    execute_seek_command(target);

}

void MediaPlayer::jump(uint milisecond)
{
    if (!enabled_ || !isPlaying())
        return;

    gst_element_send_event (pipeline_, gst_event_new_step (GST_FORMAT_TIME,
                                                           CLAMP(milisecond, 1, 1000) * GST_MSECOND,
                                                           ABS(rate_),
                                                           TRUE,  FALSE));

}

void MediaPlayer::init_texture(guint index)
{
    glActiveTexture(GL_TEXTURE0);
    glGenTextures(1, &textureindex_);
    glBindTexture(GL_TEXTURE_2D, textureindex_);
    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, media_.width, media_.height);

    // fill texture frame with frame at given index
    if (frame_[index].buffer) {

#ifdef USE_GST_OPENGL_SYNC_HANDLER
        // Try GLMemory fast path first
        bool gl_memory_used = false;
        if ( use_gl_memory_ ) {
            GstMemory *mem = gst_buffer_peek_memory(frame_[index].buffer, 0);

            if (mem && gst_is_gl_memory(mem)) {
                GstGLMemory *gl_mem = (GstGLMemory*) mem;
                guint gst_tex_id = gst_gl_memory_get_texture_id(gl_mem);

                if (gst_tex_id > 0) {
                    // Copy from GStreamer GL texture to our texture using FBO
                    GLuint fbo;
                    glGenFramebuffers(1, &fbo);
                    glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
                    glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                                          GL_TEXTURE_2D, gst_tex_id, 0);

                    glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0,
                                       media_.width, media_.height);

                    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
                    glDeleteFramebuffers(1, &fbo);

                    gl_memory_used = true;
                }
            } else {
                // Not GLMemory - disable optimization
                use_gl_memory_ = false;
                Log::Info("MediaPlayer %s failed to access GLMemory with %s decoder.", std::to_string(id_).c_str(), decoderName().c_str());
            }
        }

        // Fallback to CPU path if GLMemory not available
        if (!gl_memory_used)
#endif
        {
            GstMapInfo map;
            gst_buffer_map(frame_[index].buffer, &map, GST_MAP_READ);
            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, media_.width, media_.height,
                            GL_RGBA, GL_UNSIGNED_BYTE, map.data);
            gst_buffer_unmap (frame_[index].buffer, &map);
        }
    }

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    // initialize decoderName once (forced update)
    decoder_name_ = "";

    // use Pixel Buffer Objects only for performance needs of videos
    if ( !isImage() ) {
#ifdef USE_GST_OPENGL_SYNC_HANDLER
        if (use_gl_memory_)
            Log::Info("MediaPlayer %s Uses %s decoding and OpenGL GLMemory texturing (zero-copy).", std::to_string(id_).c_str(), decoderName().c_str());
        else
#endif 
        {
            // set pbo image size
            pbo_size_ = media_.height * media_.width * 4;

            // create pixel buffer objects,
            if (pbo_[0])
                glDeleteBuffers(2, pbo_);
            glGenBuffers(2, pbo_);

            // should be good to go
            pbo_index_ = 0;
            pbo_next_index_ = 1;

            Log::Info("MediaPlayer %s Uses %s decoding and OpenGL PBO texturing.", std::to_string(id_).c_str(), decoderName().c_str());
        }
    }
    else
        Log::Info("MediaPlayer %s Uses %s decoding and standard OpenGL texturing.", std::to_string(id_).c_str(), decoderName().c_str());

    glBindTexture(GL_TEXTURE_2D, 0);
}


void MediaPlayer::fill_texture(guint index)
{
    // is this the first frame ?
    if (textureindex_ < 1)
    {
        // initialize texture on first run
        // (this also fills the texture with frame at index)
        init_texture(index);
    }
    else if (!isImage()) {

#ifdef USE_GST_OPENGL_SYNC_HANDLER
        // Try GLMemory fast path first (zero-copy GPU texture)
        if (use_gl_memory_ && frame_[index].buffer) {
            GstMemory *mem = gst_buffer_peek_memory(frame_[index].buffer, 0);

            if (mem && gst_is_gl_memory(mem)) {
                // FAST PATH: Direct GL texture extraction from GStreamer
                GstGLMemory *gl_mem = (GstGLMemory*) mem;
                guint gst_tex_id = gst_gl_memory_get_texture_id(gl_mem);

                if (gst_tex_id > 0) {
                    // Use FBO to copy from GStreamer's texture to our texture
                    // This ensures we maintain ownership and texture lifecycle
                    GLuint fbo;
                    glGenFramebuffers(1, &fbo);
                    glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
                    glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                                          GL_TEXTURE_2D, gst_tex_id, 0);

                    glBindTexture(GL_TEXTURE_2D, textureindex_);
                    glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0,
                                       media_.width, media_.height);

                    glBindTexture(GL_TEXTURE_2D, 0);
                    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
                    glDeleteFramebuffers(1, &fbo);

                    // Success - no need for CPU path
                    return;
                }
            }
            else {
                // First buffer is not GLMemory - disable GLMemory optimization for this source
                // This happens with images, some network streams, or when glupload fails
                use_gl_memory_ = false;
                Log::Info("MediaPlayer %s GLMemory not available, falling back to CPU transfer.",
                         std::to_string(id_).c_str());
            }
        }
#endif

        // FALLBACK: CPU path (standard PBO or direct upload)
        // Use GST mapping to access pointer to RGBA data
        GstMapInfo map;
        gst_buffer_map(frame_[index].buffer, &map, GST_MAP_READ);

        // bind texture for writing
        glBindTexture(GL_TEXTURE_2D, textureindex_);

        // use dual Pixel Buffer Object (faster)
        if (pbo_size_ > 0 && map.size == pbo_size_) {

            // In dual PBO mode, increment current index first then get the next index
            pbo_index_ = (pbo_index_ + 1) % 2;
            pbo_next_index_ = (pbo_index_ + 1) % 2;

            // bind PBO to read pixels
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[pbo_index_]);
            // copy pixels from PBO to texture object
            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, media_.width, media_.height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
            // bind the next PBO to write pixels
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[pbo_next_index_]);

#ifdef USE_GL_BUFFER_SUBDATA
            glBufferSubData(GL_PIXEL_UNPACK_BUFFER, 0, pbo_size_, map.data);
#else
            // update data directly on the mapped buffer
            // NB : equivalent but faster than glBufferSubData (memmove instead of memcpy ?)
            // See http://www.songho.ca/opengl/gl_pbo.html#map for more details
            glBufferData(GL_PIXEL_UNPACK_BUFFER, pbo_size_, 0, GL_STREAM_DRAW);
            // map the buffer object into client's memory
            GLubyte* ptr = (GLubyte*) glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
            if (ptr)
                memmove(ptr, map.data, pbo_size_);

            // release pointer to mapping buffer
            glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
#endif
            // done with PBO
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
        }
        else {
            // without PBO, use standard opengl (slower)
            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, media_.width, media_.height,
                            GL_RGBA, GL_UNSIGNED_BYTE, map.data);
        }

        // unbind texture
        glBindTexture(GL_TEXTURE_2D, 0);

        // unmap buffer to let it free
        gst_buffer_unmap (frame_[index].buffer, &map);

    }
}

void MediaPlayer::update()
{
    // discard
    if (failed_)
        return;

    // not ready yet
    if (!opened_) {
        if (discoverer_.valid()) {
            // try to get info from discoverer
            if (discoverer_.wait_for( std::chrono::milliseconds(4) ) == std::future_status::ready )
            {
                media_ = discoverer_.get();
                // if its ok, open the media
                if (media_.valid) {
                    if (!media_.log.empty())
                        Log::Info("'%s' : %s", uri().c_str(), media_.log.c_str());
                    if (!media_.isimage)
                        timeline_.setTiming( TimeInterval(0, media_.end), media_.dt);
                    execute_open();
                }
                else {
                    Log::Warning("'%s' : %s", uri().c_str(), media_.log.c_str());
                    Log::Notify("MediaPlayer %s Loading failed.", std::to_string(id_).c_str());
                    failed_ = true;
                }
            }
        }
        // wait next frame to display
        return;
    }

    // prevent unnecessary updates: disabled or already filled image
    if ( (!enabled_ && !force_update_) || (singleFrame() && textureindex_>0 ) )
        return;

    // local variables before trying to update
    guint read_index = 0;
    bool need_loop = false;

    // locked access to current index
    index_lock_.lock();
    // get the last frame filled from fill_frame()
    read_index = last_index_;
    // unlock access to index change
    index_lock_.unlock();

    // lock frame while reading it
    frame_[read_index].access.lock();

    // do not fill a frame twice
    if (frame_[read_index].status != INVALID ) {

        // is this an End-of-Stream frame ?
        if (frame_[read_index].status == EOS )
        {
            // will execute seek command below (after unlock)
            need_loop = true;
        }
        // otherwise just fill non-empty SAMPLE or PREROLL
        else if (frame_[read_index].is_new)
        {
            // fill the texture with the frame at reading index
            fill_texture(read_index);

            // double update for pre-roll frame and dual PBO (ensure frame is displayed now)
            if ( (frame_[read_index].status == PREROLL || seeking_ ) && pbo_size_ > 0)
                fill_texture(read_index);

            // // free frame
            // frame_[read_index].unmap();
            frame_[read_index].is_new = false;
        }

        // we just displayed a vframe : set position time to frame PTS
        position_ = frame_[read_index].position;

        // avoid reading it again
        frame_[read_index].status = INVALID;
    }

    // unkock frame after reading it
    frame_[read_index].access.unlock();

    // if already seeking (asynch)
    if (seeking_) {
        // fail counter
        static uint counter = 0;
        counter++;
        // request ASYNC status update to pipeline (re-sync gst thread)
        GstStateChangeReturn ret = gst_element_get_state(pipeline_, NULL, NULL, GST_MSECOND * 5);
        if (ret == GST_STATE_CHANGE_SUCCESS) {
            // finished ! seek is resolved for next frame
            seeking_ = false;
            counter = 0;
        }
        else if (counter > 150) {
            Log::Notify("MediaPlayer %s Seek failed. Reseting media...", std::to_string(id_).c_str());
            counter = 0;
            reopen();
        }
        // do NOT do another seek yet
        need_loop = false;
    }
    // otherwise check for need to seek (pipeline management)
    else if (position_ != GST_CLOCK_TIME_NONE) {
        // manage timeline:
        TimeInterval gap;
        // ensure we remain within the begin to end range
        if (position_ > timeline_.last() || position_ < timeline_.first()) {
            need_loop = true;
        }
        // test if position falls into a gap
        else if (timeline_.getGapAt(position_, gap)) {
            // if in a gap, seek to next section
            if (gap.is_valid()) {
                // jump in one or the other direction
                GstClockTime jumpPts = timeline_.step(); // round jump time to frame pts
                if ( rate_ > 0.f )
                    jumpPts *= ( gap.end / timeline_.step() ) + 1; // FWD: go to end of gap
                else
                    jumpPts *= ( gap.begin / timeline_.step() );   // BWD: go to begin of gap
                // (if not beginnig or end of timeline)
                if (jumpPts > timeline_.first() && jumpPts < timeline_.last())
                    // seek to jump PTS time
                    seek( jumpPts );
                // otherwise, we should loop
                else
                    need_loop = true;
            }
        }
        // detect flags if playing 
        else if ( isPlaying() ) {
            // check if position is flagged 
            TimeInterval flag = timeline_.getFlagAt(position_);
            // if type of flag requires stop and not already current
            if ( flag.is_valid() && flag.type > 0 ) {
              if (flag != current_flag_) {
                // set flag as current
                current_flag_ = flag;
                // set timeline to temporary state (stop or blackout)
                loop_status_ = (LoopStatus)current_flag_.type;
                // effectively stop playing
                play(false);
              }
            } 
            else 
                current_flag_.reset();
        }
    }

    // manage loop mode
    if (need_loop && desired_state_ == GST_STATE_PLAYING)  // avoid repeated call
        execute_loop_command();

    GstMessage *msg = gst_bus_pop_filtered(bus_, GST_MESSAGE_ANY);
    if (msg != NULL)
        gst_message_unref(msg);

    force_update_ = false;
}

void MediaPlayer::execute_loop_command()
{
    if (loop_==LOOP_REWIND) {
        rewind();
    }
    else if (loop_ == LOOP_BIDIRECTIONAL) {
        rate_ *= -1.f;
        execute_seek_command();
    }
    else {
        if (loop_ == LOOP_BLACKOUT)
            loop_status_ = LoopStatus::LOOP_STATUS_BLACKOUT;
        else
            loop_status_ = LoopStatus::LOOP_STATUS_STOPPED;
        // stop
        play(false);
    }
}

void MediaPlayer::execute_seek_command(GstClockTime target, bool force)
{
    if ( !opened_ || pipeline_ == nullptr || !media_.seekable )
        return;

    // ignore request to current position
    if ( ABS_DIFF(target, position_) < timeline_.step())
        return;

    // seek position : default to target
    GstClockTime seek_pos = target;

    // seek with flush (always)
    int seek_flags = GST_SEEK_FLAG_FLUSH;

    // no target given
    if (target == GST_CLOCK_TIME_NONE) {
        // create seek event with current position (called for rate changed)
        // CLAMP the time to ensure we do not bounce outside of timeline
        seek_pos = CLAMP(position_, timeline_.first(), timeline_.last() - timeline_.step());
        seek_flags |= GST_SEEK_FLAG_KEY_UNIT;
        seek_flags |= GST_SEEK_FLAG_SNAP_NEAREST;
    }
    else
        // seek with accurate timing if given target
        seek_flags |= GST_SEEK_FLAG_ACCURATE;

    // create seek event depending on direction
    GstEvent *seek_event = nullptr;
    if (rate_ > 0) {
        seek_event = gst_event_new_seek (rate_, GST_FORMAT_TIME, (GstSeekFlags) seek_flags,
            GST_SEEK_TYPE_SET, seek_pos, GST_SEEK_TYPE_END, 0);
    }
    else {
        seek_event = gst_event_new_seek (rate_, GST_FORMAT_TIME, (GstSeekFlags) seek_flags,
            GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_SET, seek_pos);
    }

    // Send the event (ASYNC)
    if (seek_event && gst_element_send_event(pipeline_, seek_event) ) {
        seeking_ = true;

        // moving to a new position, necessarily leaves any flag we were at
        current_flag_.reset();
    }

    // Force update
    if (force) {
        gst_element_get_state (pipeline_, NULL, NULL, GST_CLOCK_TIME_NONE);
        force_update_ = true;
    }

}


void MediaPlayer::setRate(double s)
{
    // bound to interval [-MAX_PLAY_SPEED MAX_PLAY_SPEED]
    rate_ = CLAMP(s, -MAX_PLAY_SPEED, MAX_PLAY_SPEED);
    // skip interval [-MIN_PLAY_SPEED MIN_PLAY_SPEED]
    if (ABS(rate_) < MIN_PLAY_SPEED)
        rate_ = SIGN(rate_) * MIN_PLAY_SPEED;

}

void MediaPlayer::setPlaySpeed(double s)
{
#ifdef USE_GST_PLAYBIN
    bool change_direction =  s * rate_ < 0.0;
#endif

    // Set rate to requested value (may be executed later)
    setRate(s);

    // discard if too early or not possible
    if (pipeline_ == nullptr || !media_.seekable)
        return;

    //
    // Apply rate change with gstreamer seek
    //

#ifdef USE_GST_PLAYBIN
    //  GST_SEEK_FLAG_INSTANT_RATE_CHANGE: Signals that a rate change should be
    //  applied immediately. Only valid if start/stop position
    //  are GST_CLOCK_TIME_NONE, the playback direction does not change
    //  and the seek is not flushing. (Since: 1.18)
    // if all conditions to use GST_SEEK_FLAG_INSTANT_RATE_CHANGE are met
    if ( rate_change_ == RATE_CHANGE_INSTANT && !change_direction && !media_.isimage) {

        int seek_flags = GST_SEEK_FLAG_INSTANT_RATE_CHANGE;
        seek_flags |= GST_SEEK_FLAG_TRICKMODE;

        GstEvent *seek_event = gst_event_new_seek (rate_, GST_FORMAT_TIME,
                                                   (GstSeekFlags) seek_flags,
                                                   GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE,
                                                   GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE);

        if (seek_event && gst_element_send_event(pipeline_, seek_event) )
            seeking_ = true;
        else {
            Log::Info("MediaPlayer %s; cannot perform instantaneous speed change. Change of play speed will not be smooth.", std::to_string(id_).c_str());
            rate_change_ = RATE_CHANGE_FLUSH;
        }
    }
    // Generic way is a flush seek
    // following the example from
    // https://gstreamer.freedesktop.org/documentation/tutorials/basic/playback-speed.html
    else {
        execute_seek_command();
    }

    // Set after initialization (will be used next time)
    if (rate_change_ == RATE_CHANGE_NONE)
        rate_change_ = RATE_CHANGE_INSTANT;
#else
    execute_seek_command();
#endif
}

double MediaPlayer::playSpeed() const
{
    return rate_;
}

Timeline *MediaPlayer::timeline()
{
    return &timeline_;
}

float MediaPlayer::currentTimelineFading()
{
    if (loop_status_ == LOOP_STATUS_BLACKOUT && !isPlaying())
        return 0.f;

    return timeline_.fadingAt(position_);
}

void MediaPlayer::setTimeline(const Timeline &tl)
{
    timeline_ = tl;
}

bool MediaPlayer::go_to_flag(TimeInterval flag)
{
    bool ret = false;
    if ( flag.is_valid() ) {

        // flag target position
        GstClockTime flagPts = flag.midpoint();

        if (ABS_DIFF (position_, flagPts) > 2 * timeline_.step() ) {

            // seek if valid target position
            ret = true;
            seek( flagPts );

            // will reach that flag
            current_flag_ = flag;

            // change timeline status accordingly
            if ( flag.type == (int) LoopStatus::LOOP_STATUS_BLACKOUT) {
                loop_status_ = LoopStatus::LOOP_STATUS_BLACKOUT;
                execute_play_command(false);
            }
            else if ( flag.type == (int) LoopStatus::LOOP_STATUS_STOPPED) {
                loop_status_ = LoopStatus::LOOP_STATUS_STOPPED;
                execute_play_command(false);
            }
        }
    }
    return ret;
}

MediaInfo MediaPlayer::media() const
{
    return media_;
}

std::string MediaPlayer::uri() const
{
    return uri_;
}

std::string MediaPlayer::filename() const
{
    return filename_;
}

double MediaPlayer::frameRate() const
{
    return static_cast<double>(media_.framerate_n) / static_cast<double>(media_.framerate_d);;
}

double MediaPlayer::updateFrameRate() const
{
    return timecount_.frameRate();
}


// CALLBACKS

bool MediaPlayer::fill_frame(GstBuffer *buf, FrameStatus status)
{
    // Do NOT overwrite an unread EOS
    if ( frame_[write_index_].status == EOS )
        write_index_ = (write_index_ + 1) % N_VFRAME;

    // lock access to frame
    frame_[write_index_].access.lock();

    // always empty frame before filling it again
    if (frame_[write_index_].buffer) {
        gst_buffer_unref(frame_[write_index_].buffer);
        frame_[write_index_].buffer = NULL;
    }

    // accept status of frame received
    frame_[write_index_].status = status;

    // a buffer is given (not EOS)
    if (buf != NULL) {

        // copy the buffer in the frame
        frame_[write_index_].buffer = gst_buffer_copy(buf);

        // indicate to update loop that buffer is new
        frame_[write_index_].is_new = true;

        // set presentation time stamp
        frame_[write_index_].position = buf->pts;

        // set the start position (i.e. pts of first frame we got)
        if (timeline_.first() == GST_CLOCK_TIME_NONE) {
            timeline_.setFirst(buf->pts);
            // add a gap to show that before
            if (buf->pts > 0 && !timeline_.gapAt( buf->pts ))
                timeline_.addGap(0, buf->pts);
        }

    }
    // else; null buffer for EOS: give a position
    else {
        frame_[write_index_].status = EOS;
        frame_[write_index_].position = rate_ > 0.0 ? timeline_.end() : timeline_.begin();
    }

    // unlock access to frame
    frame_[write_index_].access.unlock();

    // lock access to change current index (very quick)
    index_lock_.lock();
    // indicate update() that this is the last frame filled (and unlocked)
    last_index_ = write_index_;
    // unlock access to index change
    index_lock_.unlock();

    // for writing, we will access the next in stack
    write_index_ = (write_index_ + 1) % N_VFRAME;

    // calculate actual FPS of update
    timecount_.tic();

    return true;
}

void MediaPlayer::callback_end_of_stream (GstAppSink *, gpointer p)
{
    MediaPlayer *m = static_cast<MediaPlayer *>(p);
    if (m && m->opened_) {
        m->fill_frame(NULL, MediaPlayer::EOS);
    }
}

GstFlowReturn MediaPlayer::callback_new_preroll (GstAppSink *sink, gpointer p)
{
    GstFlowReturn ret = GST_FLOW_OK;

    // blocking read pre-roll samples
    GstSample *sample = gst_app_sink_pull_preroll(sink);

    // if got a valid sample
    if (sample != NULL) {

        // get buffer from sample
        GstBuffer *buf = gst_sample_get_buffer (sample);

        // send frames to media player only if ready
        MediaPlayer *m = static_cast<MediaPlayer *>(p);
        if (m && m->opened_) {

#ifdef USE_GST_OPENGL_SYNC_HANDLER
            // Debug: Log negotiated caps on first frame to verify GLMemory
            if (m->use_gl_memory_) {
                GstCaps *caps = gst_sample_get_caps(sample);
                if (caps) {
                    // Check if GLMemory was actually negotiated
                    GstCapsFeatures *features = gst_caps_get_features(caps, 0);
                    if (!features || !gst_caps_features_contains(features, "memory:GLMemory")) {
                        m->use_gl_memory_ = false;
                        ret = GST_FLOW_ERROR;
                        Log::Info("MediaPlayer %s failed to negotiate GLMemory caps with %s decoder.", std::to_string(m->id_).c_str(), m->decoderName().c_str());
                    }
                }
            }
#endif

            // fill frame from buffer
            if ( !m->fill_frame(buf, MediaPlayer::PREROLL) )
                ret = GST_FLOW_ERROR;
            // loop negative rate: emulate an EOS
            else if (m->playSpeed() < 0.f && !(buf->pts > 0) ) {
                m->fill_frame(NULL, MediaPlayer::EOS);
            }
        }
    }
    else
        ret = GST_FLOW_FLUSHING;

    // release sample
    gst_sample_unref (sample);

    return ret;
}

GstFlowReturn MediaPlayer::callback_new_sample (GstAppSink *sink, gpointer p)
{
    GstFlowReturn ret = GST_FLOW_OK;

    // non-blocking read new sample
    GstSample *sample = gst_app_sink_pull_sample(sink);

    // if got a valid sample
    if (sample != NULL && !gst_app_sink_is_eos (sink)) {

        // send frames to media player only if ready
        MediaPlayer *m = static_cast<MediaPlayer *>(p);
        if (m && m->opened_) {

            // get buffer from sample (valid until sample is released)
            GstBuffer *buf = gst_sample_get_buffer (sample) ;

            // fill frame with buffer
            if ( !m->fill_frame(buf, MediaPlayer::SAMPLE) )
                ret = GST_FLOW_ERROR;
            // loop negative rate: emulate an EOS
            else if (m->playSpeed() < 0.f && !(buf->pts > 0) ) {
                m->fill_frame(NULL, MediaPlayer::EOS);
            }
        }
    }
    else
        ret = GST_FLOW_FLUSHING;

    // release sample
    gst_sample_unref (sample);

    return ret;
}



MediaPlayer::TimeCounter::TimeCounter(): fps(1.f)
{
    timer = g_timer_new ();
}

MediaPlayer::TimeCounter::~TimeCounter()
{
    g_free(timer);
}

void MediaPlayer::TimeCounter::tic ()
{
    const double dt = g_timer_elapsed (timer, NULL) * 1000.0;

    // ignore refresh after too little time
    if (dt > 3.0){
        // restart timer
        g_timer_start(timer);
        // calculate instantaneous framerate
        // Exponential moving averate with previous framerate to filter jitter
        fps = CLAMP( 0.5 * fps + 500.0 / dt, 0.0, 1000.0);
    }
}

void MediaPlayer::setAudioEnabled(bool on)
{
    // in case of change
    if (audio_enabled_ != on) {
        // toggle
        audio_enabled_ = on;
        // if openned
        if ( media_.hasaudio && opened_) {
            // apply
            reopen();
        }
    }
}

void MediaPlayer::setAudioVolume(gdouble vol)
{
    if (pipeline_ && media_.hasaudio){

#ifdef APPLE
        // BUGFIX FOR APPLE OSXAUDIOSINK
        GstElement *asink = NULL;
        g_object_get (G_OBJECT(pipeline_), "audio-sink", &asink, NULL);
        if ( asink )
            g_object_set (G_OBJECT(asink), "volume", vol, NULL);
#endif

        g_object_set (G_OBJECT(pipeline_), "volume", vol, NULL);
    }
    // gst_stream_volume_set_volume (GST_STREAM_VOLUME (pipeline_), GST_STREAM_VOLUME_FORMAT_LINEAR, vol);
}

gdouble MediaPlayer::audioVolume() const
{
    gdouble vol = 1.0;
    if (pipeline_ && media_.hasaudio)
        g_object_get (G_OBJECT(pipeline_), "volume", &vol, NULL);

    return vol;
}

void MediaPlayer::callback_element_setup (GstElement *pipeline, GstElement *element, MediaPlayer *mp)
{
    if (pipeline && element && mp)
    {

#ifdef APPLE
        // BUGFIX FOR APPLE OSXAUDIOSINK
        // detect setup of audio sink
        GstElement *audio_sink = NULL;
        g_object_get (G_OBJECT(pipeline), "audio-sink", &audio_sink, NULL);
        if (element == audio_sink) {
            // force volume to pipeline volume
            mp->setAudioVolume( mp->audioVolume() );
        }
#endif
    }
}

