/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QtMultimedia/private/qtmultimediaglobal_p.h>
#include "qgstutils_p.h"

#include <QtCore/qdatetime.h>
#include <QtCore/qdir.h>
#include <QtCore/qbytearray.h>
#include <QtCore/qvariant.h>
#include <QtCore/qregularexpression.h>
#include <QtCore/qsize.h>
#include <QtCore/qset.h>
#include <QtCore/qstringlist.h>
#include <QtGui/qimage.h>
#include <qaudioformat.h>
#include <QtCore/qelapsedtimer.h>
#include <QtMultimedia/qvideosurfaceformat.h>
#include <private/qmultimediautils_p.h>

#include <gst/audio/audio.h>
#include <gst/video/video.h>

template<typename T, int N> static int lengthOf(const T (&)[N]) { return N; }

#if QT_CONFIG(linux_v4l)
#  include <private/qcore_unix_p.h>
#  include <linux/videodev2.h>
#endif

#include "qgstreamervideoinputdevicecontrol_p.h"

QT_BEGIN_NAMESPACE

//internal
static void addTagToMap(const GstTagList *list,
                        const gchar *tag,
                        gpointer user_data)
{
    QMap<QByteArray, QVariant> *map = reinterpret_cast<QMap<QByteArray, QVariant>* >(user_data);

    GValue val;
    val.g_type = 0;
    gst_tag_list_copy_value(&val,list,tag);

    switch( G_VALUE_TYPE(&val) ) {
        case G_TYPE_STRING:
        {
            const gchar *str_value = g_value_get_string(&val);
            map->insert(QByteArray(tag), QString::fromUtf8(str_value));
            break;
        }
        case G_TYPE_INT:
            map->insert(QByteArray(tag), g_value_get_int(&val));
            break;
        case G_TYPE_UINT:
            map->insert(QByteArray(tag), g_value_get_uint(&val));
            break;
        case G_TYPE_LONG:
            map->insert(QByteArray(tag), qint64(g_value_get_long(&val)));
            break;
        case G_TYPE_BOOLEAN:
            map->insert(QByteArray(tag), g_value_get_boolean(&val));
            break;
        case G_TYPE_CHAR:
#if GLIB_CHECK_VERSION(2,32,0)
            map->insert(QByteArray(tag), g_value_get_schar(&val));
#else
            map->insert(QByteArray(tag), g_value_get_char(&val));
#endif
            break;
        case G_TYPE_DOUBLE:
            map->insert(QByteArray(tag), g_value_get_double(&val));
            break;
        default:
            // GST_TYPE_DATE is a function, not a constant, so pull it out of the switch
#if GST_CHECK_VERSION(1,0,0)
            if (G_VALUE_TYPE(&val) == G_TYPE_DATE) {
                const GDate *date = (const GDate *)g_value_get_boxed(&val);
#else
            if (G_VALUE_TYPE(&val) == GST_TYPE_DATE) {
                const GDate *date = gst_value_get_date(&val);
#endif
                if (g_date_valid(date)) {
                    int year = g_date_get_year(date);
                    int month = g_date_get_month(date);
                    int day = g_date_get_day(date);
                    map->insert(QByteArray(tag), QDate(year,month,day));
                    if (!map->contains("year"))
                        map->insert("year", year);
                }
#if GST_CHECK_VERSION(1,0,0)
            } else if (G_VALUE_TYPE(&val) == GST_TYPE_DATE_TIME) {
                const GstDateTime *dateTime = (const GstDateTime *)g_value_get_boxed(&val);
                int year = gst_date_time_has_year(dateTime) ? gst_date_time_get_year(dateTime) : 0;
                int month = gst_date_time_has_month(dateTime) ? gst_date_time_get_month(dateTime) : 0;
                int day = gst_date_time_has_day(dateTime) ? gst_date_time_get_day(dateTime) : 0;
                if (gst_date_time_has_time(dateTime)) {
                    int hour = gst_date_time_get_hour(dateTime);
                    int minute = gst_date_time_get_minute(dateTime);
                    int second = gst_date_time_get_second(dateTime);
                    float tz = gst_date_time_get_time_zone_offset(dateTime);
                    QDateTime dateTime(QDate(year, month, day), QTime(hour, minute, second),
                                       Qt::OffsetFromUTC, tz * 60 * 60);
                    map->insert(QByteArray(tag), dateTime);
                } else if (year > 0 && month > 0 && day > 0) {
                    map->insert(QByteArray(tag), QDate(year,month,day));
                }
                if (!map->contains("year") && year > 0)
                    map->insert("year", year);
            } else if (G_VALUE_TYPE(&val) == GST_TYPE_SAMPLE) {
                GstSample *sample = (GstSample *)g_value_get_boxed(&val);
                GstCaps* caps = gst_sample_get_caps(sample);
                if (caps && !gst_caps_is_empty(caps)) {
                    GstStructure *structure = gst_caps_get_structure(caps, 0);
                    const gchar *name = gst_structure_get_name(structure);
                    if (QByteArray(name).startsWith("image/")) {
                        GstBuffer *buffer = gst_sample_get_buffer(sample);
                        if (buffer) {
                            GstMapInfo info;
                            gst_buffer_map(buffer, &info, GST_MAP_READ);
                            map->insert(QByteArray(tag), QImage::fromData(info.data, info.size, name));
                            gst_buffer_unmap(buffer, &info);
                        }
                    }
                }
#endif
            } else if (G_VALUE_TYPE(&val) == GST_TYPE_FRACTION) {
                int nom = gst_value_get_fraction_numerator(&val);
                int denom = gst_value_get_fraction_denominator(&val);

                if (denom > 0) {
                    map->insert(QByteArray(tag), double(nom)/denom);
                }
            }
            break;
    }

    g_value_unset(&val);
}

/*!
    \class QGstUtils
    \internal
*/

/*!
  Convert GstTagList structure to QMap<QByteArray, QVariant>.

  Mapping to int, bool, char, string, fractions and date are supported.
  Fraction values are converted to doubles.
*/
QMap<QByteArray, QVariant> QGstUtils::gstTagListToMap(const GstTagList *tags)
{
    QMap<QByteArray, QVariant> res;
    gst_tag_list_foreach(tags, addTagToMap, &res);

    return res;
}

/*!
  Returns resolution of \a caps.
  If caps doesn't have a valid size, an empty QSize is returned.
*/
QSize QGstUtils::capsResolution(const GstCaps *caps)
{
    if (gst_caps_get_size(caps) == 0)
        return QSize();

    return structureResolution(gst_caps_get_structure(caps, 0));
}

/*!
  Returns aspect ratio corrected resolution of \a caps.
  If caps doesn't have a valid size, an empty QSize is returned.
*/
QSize QGstUtils::capsCorrectedResolution(const GstCaps *caps)
{
    QSize size;

    if (caps) {
        size = capsResolution(caps);

        gint aspectNum = 0;
        gint aspectDenum = 0;
        if (!size.isEmpty() && gst_structure_get_fraction(
                    gst_caps_get_structure(caps, 0), "pixel-aspect-ratio", &aspectNum, &aspectDenum)) {
            if (aspectDenum > 0)
                size.setWidth(size.width()*aspectNum/aspectDenum);
        }
    }

    return size;
}


#if GST_CHECK_VERSION(1,0,0)
namespace {

struct AudioFormat
{
    GstAudioFormat format;
    QAudioFormat::SampleType sampleType;
    QAudioFormat::Endian byteOrder;
    int sampleSize;
};
static const AudioFormat qt_audioLookup[] =
{
    { GST_AUDIO_FORMAT_S8   , QAudioFormat::SignedInt  , QAudioFormat::LittleEndian, 8  },
    { GST_AUDIO_FORMAT_U8   , QAudioFormat::UnSignedInt, QAudioFormat::LittleEndian, 8  },
    { GST_AUDIO_FORMAT_S16LE, QAudioFormat::SignedInt  , QAudioFormat::LittleEndian, 16 },
    { GST_AUDIO_FORMAT_S16BE, QAudioFormat::SignedInt  , QAudioFormat::BigEndian   , 16 },
    { GST_AUDIO_FORMAT_U16LE, QAudioFormat::UnSignedInt, QAudioFormat::LittleEndian, 16 },
    { GST_AUDIO_FORMAT_U16BE, QAudioFormat::UnSignedInt, QAudioFormat::BigEndian   , 16 },
    { GST_AUDIO_FORMAT_S32LE, QAudioFormat::SignedInt  , QAudioFormat::LittleEndian, 32 },
    { GST_AUDIO_FORMAT_S32BE, QAudioFormat::SignedInt  , QAudioFormat::BigEndian   , 32 },
    { GST_AUDIO_FORMAT_U32LE, QAudioFormat::UnSignedInt, QAudioFormat::LittleEndian, 32 },
    { GST_AUDIO_FORMAT_U32BE, QAudioFormat::UnSignedInt, QAudioFormat::BigEndian   , 32 },
    { GST_AUDIO_FORMAT_S24LE, QAudioFormat::SignedInt  , QAudioFormat::LittleEndian, 24 },
    { GST_AUDIO_FORMAT_S24BE, QAudioFormat::SignedInt  , QAudioFormat::BigEndian   , 24 },
    { GST_AUDIO_FORMAT_U24LE, QAudioFormat::UnSignedInt, QAudioFormat::LittleEndian, 24 },
    { GST_AUDIO_FORMAT_U24BE, QAudioFormat::UnSignedInt, QAudioFormat::BigEndian   , 24 },
    { GST_AUDIO_FORMAT_F32LE, QAudioFormat::Float      , QAudioFormat::LittleEndian, 32 },
    { GST_AUDIO_FORMAT_F32BE, QAudioFormat::Float      , QAudioFormat::BigEndian   , 32 },
    { GST_AUDIO_FORMAT_F64LE, QAudioFormat::Float      , QAudioFormat::LittleEndian, 64 },
    { GST_AUDIO_FORMAT_F64BE, QAudioFormat::Float      , QAudioFormat::BigEndian   , 64 }
};

}
#endif

/*!
  Returns audio format for caps.
  If caps doesn't have a valid audio format, an empty QAudioFormat is returned.
*/

QAudioFormat QGstUtils::audioFormatForCaps(const GstCaps *caps)
{
    QAudioFormat format;
#if GST_CHECK_VERSION(1,0,0)
    GstAudioInfo info;
    if (gst_audio_info_from_caps(&info, caps)) {
        for (int i = 0; i < lengthOf(qt_audioLookup); ++i) {
            if (qt_audioLookup[i].format != info.finfo->format)
                continue;

            format.setSampleType(qt_audioLookup[i].sampleType);
            format.setByteOrder(qt_audioLookup[i].byteOrder);
            format.setSampleSize(qt_audioLookup[i].sampleSize);
            format.setSampleRate(info.rate);
            format.setChannelCount(info.channels);
            format.setCodec(QStringLiteral("audio/pcm"));

            return format;
        }
    }
#else
    const GstStructure *structure = gst_caps_get_structure(caps, 0);

    if (qstrcmp(gst_structure_get_name(structure), "audio/x-raw-int") == 0) {

        format.setCodec("audio/pcm");

        int endianness = 0;
        gst_structure_get_int(structure, "endianness", &endianness);
        if (endianness == 1234)
            format.setByteOrder(QAudioFormat::LittleEndian);
        else if (endianness == 4321)
            format.setByteOrder(QAudioFormat::BigEndian);

        gboolean isSigned = FALSE;
        gst_structure_get_boolean(structure, "signed", &isSigned);
        if (isSigned)
            format.setSampleType(QAudioFormat::SignedInt);
        else
            format.setSampleType(QAudioFormat::UnSignedInt);

        // Number of bits allocated per sample.
        int width = 0;
        gst_structure_get_int(structure, "width", &width);

        // The number of bits used per sample. This must be less than or equal to the width.
        int depth = 0;
        gst_structure_get_int(structure, "depth", &depth);

        if (width != depth) {
            // Unsupported sample layout.
            return QAudioFormat();
        }
        format.setSampleSize(width);

        int rate = 0;
        gst_structure_get_int(structure, "rate", &rate);
        format.setSampleRate(rate);

        int channels = 0;
        gst_structure_get_int(structure, "channels", &channels);
        format.setChannelCount(channels);

    } else if (qstrcmp(gst_structure_get_name(structure), "audio/x-raw-float") == 0) {

        format.setCodec("audio/pcm");

        int endianness = 0;
        gst_structure_get_int(structure, "endianness", &endianness);
        if (endianness == 1234)
            format.setByteOrder(QAudioFormat::LittleEndian);
        else if (endianness == 4321)
            format.setByteOrder(QAudioFormat::BigEndian);

        format.setSampleType(QAudioFormat::Float);

        int width = 0;
        gst_structure_get_int(structure, "width", &width);

        format.setSampleSize(width);

        int rate = 0;
        gst_structure_get_int(structure, "rate", &rate);
        format.setSampleRate(rate);

        int channels = 0;
        gst_structure_get_int(structure, "channels", &channels);
        format.setChannelCount(channels);

    } else {
        return QAudioFormat();
    }
#endif
    return format;
}

#if GST_CHECK_VERSION(1,0,0)
/*
  Returns audio format for a sample.
  If the buffer doesn't have a valid audio format, an empty QAudioFormat is returned.
*/
QAudioFormat QGstUtils::audioFormatForSample(GstSample *sample)
{
    GstCaps* caps = gst_sample_get_caps(sample);
    if (!caps)
        return QAudioFormat();

    return QGstUtils::audioFormatForCaps(caps);
}
#else
/*!
  Returns audio format for a buffer.
  If the buffer doesn't have a valid audio format, an empty QAudioFormat is returned.
*/
QAudioFormat QGstUtils::audioFormatForBuffer(GstBuffer *buffer)
{
    GstCaps* caps = gst_buffer_get_caps(buffer);
    if (!caps)
        return QAudioFormat();

    QAudioFormat format = QGstUtils::audioFormatForCaps(caps);
    gst_caps_unref(caps);
    return format;
}
#endif

/*!
  Builds GstCaps for an audio format.
  Returns 0 if the audio format is not valid.
  Caller must unref GstCaps.
*/

GstCaps *QGstUtils::capsForAudioFormat(const QAudioFormat &format)
{
    if (!format.isValid())
        return 0;

#if GST_CHECK_VERSION(1,0,0)
    const QAudioFormat::SampleType sampleType = format.sampleType();
    const QAudioFormat::Endian byteOrder = format.byteOrder();
    const int sampleSize = format.sampleSize();

    for (int i = 0; i < lengthOf(qt_audioLookup); ++i) {
        if (qt_audioLookup[i].sampleType != sampleType
                || qt_audioLookup[i].byteOrder != byteOrder
                || qt_audioLookup[i].sampleSize != sampleSize) {
            continue;
        }

        return gst_caps_new_simple(
                    "audio/x-raw",
                    "format"  , G_TYPE_STRING, gst_audio_format_to_string(qt_audioLookup[i].format),
                    "rate"    , G_TYPE_INT   , format.sampleRate(),
                    "channels", G_TYPE_INT   , format.channelCount(),
                    nullptr);
    }
    return 0;
#else
    GstStructure *structure = 0;

    if (format.isValid()) {
        if (format.sampleType() == QAudioFormat::SignedInt || format.sampleType() == QAudioFormat::UnSignedInt) {
            structure = gst_structure_new("audio/x-raw-int", nullptr);
        } else if (format.sampleType() == QAudioFormat::Float) {
            structure = gst_structure_new("audio/x-raw-float", nullptr);
        }
    }

    GstCaps *caps = 0;

    if (structure) {
        gst_structure_set(structure, "rate", G_TYPE_INT, format.sampleRate(), nullptr);
        gst_structure_set(structure, "channels", G_TYPE_INT, format.channelCount(), nullptr);
        gst_structure_set(structure, "width", G_TYPE_INT, format.sampleSize(), nullptr);
        gst_structure_set(structure, "depth", G_TYPE_INT, format.sampleSize(), nullptr);

        if (format.byteOrder() == QAudioFormat::LittleEndian)
            gst_structure_set(structure, "endianness", G_TYPE_INT, 1234, nullptr);
        else if (format.byteOrder() == QAudioFormat::BigEndian)
            gst_structure_set(structure, "endianness", G_TYPE_INT, 4321, nullptr);

        if (format.sampleType() == QAudioFormat::SignedInt)
            gst_structure_set(structure, "signed", G_TYPE_BOOLEAN, TRUE, nullptr);
        else if (format.sampleType() == QAudioFormat::UnSignedInt)
            gst_structure_set(structure, "signed", G_TYPE_BOOLEAN, FALSE, nullptr);

        caps = gst_caps_new_empty();
        Q_ASSERT(caps);
        gst_caps_append_structure(caps, structure);
    }

    return caps;
#endif
}

void QGstUtils::initializeGst()
{
    static bool initialized = false;
    if (!initialized) {
        initialized = true;
        gst_init(nullptr, nullptr);
    }
}

namespace {
    const char* getCodecAlias(const QString &codec)
    {
        if (codec.startsWith(QLatin1String("avc1.")))
            return "video/x-h264";

        if (codec.startsWith(QLatin1String("mp4a.")))
            return "audio/mpeg4";

        if (codec.startsWith(QLatin1String("mp4v.20.")))
            return "video/mpeg4";

        if (codec == QLatin1String("samr"))
            return "audio/amr";

        return 0;
    }

    const char* getMimeTypeAlias(const QString &mimeType)
    {
        if (mimeType == QLatin1String("video/mp4"))
            return "video/mpeg4";

        if (mimeType == QLatin1String("audio/mp4"))
            return "audio/mpeg4";

        if (mimeType == QLatin1String("video/ogg")
            || mimeType == QLatin1String("audio/ogg"))
            return "application/ogg";

        return 0;
    }
}

QMultimedia::SupportEstimate QGstUtils::hasSupport(const QString &mimeType,
                                                    const QStringList &codecs,
                                                    const QSet<QString> &supportedMimeTypeSet)
{
    if (supportedMimeTypeSet.isEmpty())
        return QMultimedia::NotSupported;

    QString mimeTypeLowcase = mimeType.toLower();
    bool containsMimeType = supportedMimeTypeSet.contains(mimeTypeLowcase);
    if (!containsMimeType) {
        const char* mimeTypeAlias = getMimeTypeAlias(mimeTypeLowcase);
        containsMimeType = supportedMimeTypeSet.contains(QLatin1String(mimeTypeAlias));
        if (!containsMimeType) {
            containsMimeType = supportedMimeTypeSet.contains(QLatin1String("video/") + mimeTypeLowcase)
                               || supportedMimeTypeSet.contains(QLatin1String("video/x-") + mimeTypeLowcase)
                               || supportedMimeTypeSet.contains(QLatin1String("audio/") + mimeTypeLowcase)
                               || supportedMimeTypeSet.contains(QLatin1String("audio/x-") + mimeTypeLowcase);
        }
    }

    int supportedCodecCount = 0;
    for (const QString &codec : codecs) {
        QString codecLowcase = codec.toLower();
        const char* codecAlias = getCodecAlias(codecLowcase);
        if (codecAlias) {
            if (supportedMimeTypeSet.contains(QLatin1String(codecAlias)))
                supportedCodecCount++;
        } else if (supportedMimeTypeSet.contains(QLatin1String("video/") + codecLowcase)
                   || supportedMimeTypeSet.contains(QLatin1String("video/x-") + codecLowcase)
                   || supportedMimeTypeSet.contains(QLatin1String("audio/") + codecLowcase)
                   || supportedMimeTypeSet.contains(QLatin1String("audio/x-") + codecLowcase)) {
            supportedCodecCount++;
        }
    }
    if (supportedCodecCount > 0 && supportedCodecCount == codecs.size())
        return QMultimedia::ProbablySupported;

    if (supportedCodecCount == 0 && !containsMimeType)
        return QMultimedia::NotSupported;

    return QMultimedia::MaybeSupported;
}

namespace {

typedef QHash<GstElementFactory *, QVector<QGstUtils::CameraInfo> > FactoryCameraInfoMap;

Q_GLOBAL_STATIC(FactoryCameraInfoMap, qt_camera_device_info);

}

QVector<QGstUtils::CameraInfo> QGstUtils::enumerateCameras(GstElementFactory *factory)
{
    static QElapsedTimer camerasCacheAgeTimer;
    if (camerasCacheAgeTimer.isValid() && camerasCacheAgeTimer.elapsed() > 500) // ms
        qt_camera_device_info()->clear();

    FactoryCameraInfoMap::const_iterator it = qt_camera_device_info()->constFind(factory);
    if (it != qt_camera_device_info()->constEnd())
        return *it;

    QVector<CameraInfo> &devices = (*qt_camera_device_info())[factory];

    if (factory) {
        bool hasVideoSource = false;

        const GType type = gst_element_factory_get_element_type(factory);
        GObjectClass * const objectClass = type
                ? static_cast<GObjectClass *>(g_type_class_ref(type))
                : 0;
        if (objectClass) {
            if (g_object_class_find_property(objectClass, "camera-device")) {
                const CameraInfo primary = {
                    QStringLiteral("primary"),
                    QGstreamerVideoInputDeviceControl::primaryCamera(),
                    0,
                    QCamera::BackFace,
                    QByteArray()
                };
                const CameraInfo secondary = {
                    QStringLiteral("secondary"),
                    QGstreamerVideoInputDeviceControl::secondaryCamera(),
                    0,
                    QCamera::FrontFace,
                    QByteArray()
                };

                devices.append(primary);
                devices.append(secondary);

                GstElement *camera = g_object_class_find_property(objectClass, "sensor-mount-angle")
                        ? gst_element_factory_create(factory, 0)
                        : 0;
                if (camera) {
                    if (gst_element_set_state(camera, GST_STATE_READY) != GST_STATE_CHANGE_SUCCESS) {
                        // no-op
                    } else for (int i = 0; i < 2; ++i) {
                        gint orientation = 0;
                        g_object_set(G_OBJECT(camera), "camera-device", i, nullptr);
                        g_object_get(G_OBJECT(camera), "sensor-mount-angle", &orientation, nullptr);

                        devices[i].orientation = (720 - orientation) % 360;
                    }
                    gst_element_set_state(camera, GST_STATE_NULL);
                    gst_object_unref(GST_OBJECT(camera));

                }
            } else if (g_object_class_find_property(objectClass, "video-source")) {
                hasVideoSource = true;
            }

            g_type_class_unref(objectClass);
        }

        if (!devices.isEmpty() || !hasVideoSource) {
            camerasCacheAgeTimer.restart();
            return devices;
        }
    }

#if QT_CONFIG(linux_v4l)
    QDir devDir(QStringLiteral("/dev"));
    devDir.setFilter(QDir::System);

    const QFileInfoList entries = devDir.entryInfoList(QStringList()
                << QStringLiteral("video*"));

    for (const QFileInfo &entryInfo : entries) {
        //qDebug() << "Try" << entryInfo.filePath();

        int fd = qt_safe_open(entryInfo.filePath().toLatin1().constData(), O_RDWR );
        if (fd == -1)
            continue;

        bool isCamera = false;

        v4l2_input input;
        memset(&input, 0, sizeof(input));
        for (; ::ioctl(fd, VIDIOC_ENUMINPUT, &input) >= 0; ++input.index) {
            if (input.type == V4L2_INPUT_TYPE_CAMERA || input.type == 0) {
                const int ret = ::ioctl(fd, VIDIOC_S_INPUT, &input.index);
                isCamera = (ret == 0 || errno == ENOTTY || errno == EBUSY);
                break;
            }
        }

        if (isCamera) {
            // find out its driver "name"
            QByteArray driver;
            QString name;
            struct v4l2_capability vcap;
            memset(&vcap, 0, sizeof(struct v4l2_capability));

            if (ioctl(fd, VIDIOC_QUERYCAP, &vcap) != 0) {
                name = entryInfo.fileName();
            } else {
                driver = QByteArray((const char*)vcap.driver);
                name = QString::fromUtf8((const char*)vcap.card);
                if (name.isEmpty())
                    name = entryInfo.fileName();
            }
            //qDebug() << "found camera: " << name;


            CameraInfo device = {
                entryInfo.absoluteFilePath(),
                name,
                0,
                QCamera::UnspecifiedPosition,
                driver
            };
            devices.append(device);
        }
        qt_safe_close(fd);
    }
    camerasCacheAgeTimer.restart();
#endif // linux_v4l

#if GST_CHECK_VERSION(1,4,0) && (defined(Q_OS_WIN) || defined(Q_OS_MACOS))
    if (!devices.isEmpty())
        return devices;

#if defined(Q_OS_WIN)
    const char *propName = "device-path";
    auto deviceDesc = [](GValue *value) {
        gchar *desc = g_value_dup_string(value);
        const QString id = QLatin1String(desc);
        g_free(desc);
        return id;
    };
#elif defined(Q_OS_MACOS)
    const char *propName = "device-index";
    auto deviceDesc = [](GValue *value) {
        return QString::number(g_value_get_int(value));
    };
#endif

    QGstUtils::initializeGst();
    GstDeviceMonitor *monitor = gst_device_monitor_new();
    auto caps = gst_caps_new_empty_simple("video/x-raw");
    gst_device_monitor_add_filter(monitor, "Video/Source", caps);
    gst_caps_unref(caps);

    GList *devs = gst_device_monitor_get_devices(monitor);
    while (devs) {
        GstDevice *dev = reinterpret_cast<GstDevice*>(devs->data);
        GstElement *element = gst_device_create_element(dev, nullptr);
        if (element) {
            gchar *name = gst_device_get_display_name(dev);
            const QString deviceName = QLatin1String(name);
            g_free(name);
            GParamSpec *prop = g_object_class_find_property(G_OBJECT_GET_CLASS(element), propName);
            if (prop) {
                GValue value = G_VALUE_INIT;
                g_value_init(&value, prop->value_type);
                g_object_get_property(G_OBJECT(element), prop->name, &value);
                const QString deviceId = deviceDesc(&value);
                g_value_unset(&value);

                CameraInfo device = {
                    deviceId,
                    deviceName,
                    0,
                    QCamera::UnspecifiedPosition,
                    QByteArray()
                };

                devices.append(device);
            }

            gst_object_unref(element);
        }

        gst_object_unref(dev);
        devs = g_list_delete_link(devs, devs);
    }
    gst_object_unref(monitor);
#endif // GST_CHECK_VERSION(1,4,0) && (defined(Q_OS_WIN) || defined(Q_OS_MACOS))

    return devices;
}

QList<QByteArray> QGstUtils::cameraDevices(GstElementFactory * factory)
{
    QList<QByteArray> devices;

    const auto cameras = enumerateCameras(factory);
    devices.reserve(cameras.size());
    for (const CameraInfo &camera : cameras)
        devices.append(camera.name.toUtf8());

    return devices;
}

QString QGstUtils::cameraDescription(const QString &device, GstElementFactory * factory)
{
    const auto cameras = enumerateCameras(factory);
    for (const CameraInfo &camera : cameras) {
        if (camera.name == device)
            return camera.description;
    }
    return QString();
}

QCamera::Position QGstUtils::cameraPosition(const QString &device, GstElementFactory * factory)
{
    const auto cameras = enumerateCameras(factory);
    for (const CameraInfo &camera : cameras) {
        if (camera.name == device)
            return camera.position;
    }
    return QCamera::UnspecifiedPosition;
}

int QGstUtils::cameraOrientation(const QString &device, GstElementFactory * factory)
{
    const auto cameras = enumerateCameras(factory);
    for (const CameraInfo &camera : cameras) {
        if (camera.name == device)
            return camera.orientation;
    }
    return 0;
}

QByteArray QGstUtils::cameraDriver(const QString &device, GstElementFactory *factory)
{
    const auto cameras = enumerateCameras(factory);
    for (const CameraInfo &camera : cameras) {
        if (camera.name == device)
            return camera.driver;
    }
    return QByteArray();
}

QSet<QString> QGstUtils::supportedMimeTypes(bool (*isValidFactory)(GstElementFactory *factory))
{
    QSet<QString> supportedMimeTypes;

    //enumerate supported mime types
    gst_init(nullptr, nullptr);

#if GST_CHECK_VERSION(1,0,0)
    GstRegistry *registry = gst_registry_get();
    GList *orig_plugins = gst_registry_get_plugin_list(registry);
#else
    GstRegistry *registry = gst_registry_get_default();
    GList *orig_plugins = gst_default_registry_get_plugin_list ();
#endif
    for (GList *plugins = orig_plugins; plugins; plugins = g_list_next(plugins)) {
        GstPlugin *plugin = (GstPlugin *) (plugins->data);
#if GST_CHECK_VERSION(1,0,0)
        if (GST_OBJECT_FLAG_IS_SET(GST_OBJECT(plugin), GST_PLUGIN_FLAG_BLACKLISTED))
            continue;
#else
        if (plugin->flags & (1<<1)) //GST_PLUGIN_FLAG_BLACKLISTED
            continue;
#endif

        GList *orig_features = gst_registry_get_feature_list_by_plugin(
                    registry, gst_plugin_get_name(plugin));
        for (GList *features = orig_features; features; features = g_list_next(features)) {
            if (G_UNLIKELY(features->data == nullptr))
                continue;

            GstPluginFeature *feature = GST_PLUGIN_FEATURE(features->data);
            GstElementFactory *factory;

            if (GST_IS_TYPE_FIND_FACTORY(feature)) {
                QString name(QLatin1String(gst_plugin_feature_get_name(feature)));
                if (name.contains(QLatin1Char('/'))) //filter out any string without '/' which is obviously not a mime type
                    supportedMimeTypes.insert(name.toLower());
                continue;
            } else if (!GST_IS_ELEMENT_FACTORY (feature)
                        || !(factory = GST_ELEMENT_FACTORY(gst_plugin_feature_load(feature)))) {
                continue;
            } else if (!isValidFactory(factory)) {
                // Do nothing
            } else for (const GList *pads = gst_element_factory_get_static_pad_templates(factory);
                        pads;
                        pads = g_list_next(pads)) {
                GstStaticPadTemplate *padtemplate = static_cast<GstStaticPadTemplate *>(pads->data);

                if (padtemplate->direction == GST_PAD_SINK && padtemplate->static_caps.string) {
                    GstCaps *caps = gst_static_caps_get(&padtemplate->static_caps);
                    if (gst_caps_is_any(caps) || gst_caps_is_empty(caps)) {
                    } else for (guint i = 0; i < gst_caps_get_size(caps); i++) {
                        GstStructure *structure = gst_caps_get_structure(caps, i);
                        QString nameLowcase = QString::fromLatin1(gst_structure_get_name(structure)).toLower();

                        supportedMimeTypes.insert(nameLowcase);
                        if (nameLowcase.contains(QLatin1String("mpeg"))) {
                            //Because mpeg version number is only included in the detail
                            //description,  it is necessary to manually extract this information
                            //in order to match the mime type of mpeg4.
                            const GValue *value = gst_structure_get_value(structure, "mpegversion");
                            if (value) {
                                gchar *str = gst_value_serialize(value);
                                QString versions = QLatin1String(str);
                                const QStringList elements = versions.split(QRegularExpression(QLatin1String("\\D+")), Qt::SkipEmptyParts);
                                for (const QString &e : elements)
                                    supportedMimeTypes.insert(nameLowcase + e);
                                g_free(str);
                            }
                        }
                    }
                }
            }
            gst_object_unref(factory);
        }
        gst_plugin_feature_list_free(orig_features);
    }
    gst_plugin_list_free (orig_plugins);

#if defined QT_SUPPORTEDMIMETYPES_DEBUG
    QStringList list = supportedMimeTypes.toList();
    list.sort();
    if (qgetenv("QT_DEBUG_PLUGINS").toInt() > 0) {
        for (const QString &type : qAsConst(list))
            qDebug() << type;
    }
#endif
    return supportedMimeTypes;
}

#if GST_CHECK_VERSION(1, 0, 0)
namespace {

struct ColorFormat { QImage::Format imageFormat; GstVideoFormat gstFormat; };
static const ColorFormat qt_colorLookup[] =
{
    { QImage::Format_RGBX8888, GST_VIDEO_FORMAT_RGBx  },
    { QImage::Format_RGBA8888, GST_VIDEO_FORMAT_RGBA  },
    { QImage::Format_RGB888  , GST_VIDEO_FORMAT_RGB   },
    { QImage::Format_RGB16   , GST_VIDEO_FORMAT_RGB16 }
};

}
#endif

#if GST_CHECK_VERSION(1,0,0)
QImage QGstUtils::bufferToImage(GstBuffer *buffer, const GstVideoInfo &videoInfo)
#else
QImage QGstUtils::bufferToImage(GstBuffer *buffer)
#endif
{
    QImage img;

#if GST_CHECK_VERSION(1,0,0)
    GstVideoInfo info = videoInfo;
    GstVideoFrame frame;
    if (!gst_video_frame_map(&frame, &info, buffer, GST_MAP_READ))
        return img;
#else
    GstCaps *caps = gst_buffer_get_caps(buffer);
    if (!caps)
        return img;

    GstStructure *structure = gst_caps_get_structure (caps, 0);
    gint width = 0;
    gint height = 0;

    if (!structure
            || !gst_structure_get_int(structure, "width", &width)
            || !gst_structure_get_int(structure, "height", &height)
            || width <= 0
            || height <= 0) {
        gst_caps_unref(caps);
        return img;
    }
    gst_caps_unref(caps);
#endif

#if GST_CHECK_VERSION(1,0,0)
    if (videoInfo.finfo->format == GST_VIDEO_FORMAT_I420) {
        const int width = videoInfo.width;
        const int height = videoInfo.height;

        const int stride[] = { frame.info.stride[0], frame.info.stride[1], frame.info.stride[2] };
        const uchar *data[] = {
            static_cast<const uchar *>(frame.data[0]),
            static_cast<const uchar *>(frame.data[1]),
            static_cast<const uchar *>(frame.data[2])
        };
#else
    if (qstrcmp(gst_structure_get_name(structure), "video/x-raw-yuv") == 0) {
        const int stride[] = { width, width / 2, width / 2 };
        const uchar *data[] = {
            (const uchar *)buffer->data,
            (const uchar *)buffer->data + width * height,
            (const uchar *)buffer->data + width * height * 5 / 4
        };
#endif
        img = QImage(width/2, height/2, QImage::Format_RGB32);

        for (int y=0; y<height; y+=2) {
            const uchar *yLine = data[0] + (y * stride[0]);
            const uchar *uLine = data[1] + (y * stride[1] / 2);
            const uchar *vLine = data[2] + (y * stride[2] / 2);

            for (int x=0; x<width; x+=2) {
                const qreal Y = 1.164*(yLine[x]-16);
                const int U = uLine[x/2]-128;
                const int V = vLine[x/2]-128;

                int b = qBound(0, int(Y + 2.018*U), 255);
                int g = qBound(0, int(Y - 0.813*V - 0.391*U), 255);
                int r = qBound(0, int(Y + 1.596*V), 255);

                img.setPixel(x/2,y/2,qRgb(r,g,b));
            }
        }
#if GST_CHECK_VERSION(1,0,0)
    } else for (int i = 0; i < lengthOf(qt_colorLookup); ++i) {
        if (qt_colorLookup[i].gstFormat != videoInfo.finfo->format)
            continue;

        const QImage image(
                    static_cast<const uchar *>(frame.data[0]),
                    videoInfo.width,
                    videoInfo.height,
                    frame.info.stride[0],
                    qt_colorLookup[i].imageFormat);
        img = image;
        img.detach();

        break;
    }

    gst_video_frame_unmap(&frame);
#else
    } else if (qstrcmp(gst_structure_get_name(structure), "video/x-raw-rgb") == 0) {
        QImage::Format format = QImage::Format_Invalid;
        int bpp = 0;
        gst_structure_get_int(structure, "bpp", &bpp);

        if (bpp == 24)
            format = QImage::Format_RGB888;
        else if (bpp == 32)
            format = QImage::Format_RGB32;

        if (format != QImage::Format_Invalid) {
            img = QImage((const uchar *)buffer->data,
                         width,
                         height,
                         format);
            img.bits(); //detach
        }
    }
#endif
    return img;
}


namespace {

#if GST_CHECK_VERSION(1,0,0)

struct VideoFormat
{
    QVideoFrame::PixelFormat pixelFormat;
    GstVideoFormat gstFormat;
};

static const VideoFormat qt_videoFormatLookup[] =
{
    { QVideoFrame::Format_YUV420P, GST_VIDEO_FORMAT_I420 },
    { QVideoFrame::Format_YUV422P, GST_VIDEO_FORMAT_Y42B },
    { QVideoFrame::Format_YV12   , GST_VIDEO_FORMAT_YV12 },
    { QVideoFrame::Format_UYVY   , GST_VIDEO_FORMAT_UYVY },
    { QVideoFrame::Format_YUYV   , GST_VIDEO_FORMAT_YUY2 },
    { QVideoFrame::Format_NV12   , GST_VIDEO_FORMAT_NV12 },
    { QVideoFrame::Format_NV21   , GST_VIDEO_FORMAT_NV21 },
    { QVideoFrame::Format_AYUV444, GST_VIDEO_FORMAT_AYUV },
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
    { QVideoFrame::Format_RGB32 ,  GST_VIDEO_FORMAT_BGRx },
    { QVideoFrame::Format_BGR32 ,  GST_VIDEO_FORMAT_RGBx },
    { QVideoFrame::Format_ARGB32,  GST_VIDEO_FORMAT_BGRA },
    { QVideoFrame::Format_ABGR32,  GST_VIDEO_FORMAT_RGBA },
    { QVideoFrame::Format_BGRA32,  GST_VIDEO_FORMAT_ARGB },
#else
    { QVideoFrame::Format_RGB32 ,  GST_VIDEO_FORMAT_xRGB },
    { QVideoFrame::Format_BGR32 ,  GST_VIDEO_FORMAT_xBGR },
    { QVideoFrame::Format_ARGB32,  GST_VIDEO_FORMAT_ARGB },
    { QVideoFrame::Format_ABGR32,  GST_VIDEO_FORMAT_ABGR },
    { QVideoFrame::Format_BGRA32,  GST_VIDEO_FORMAT_BGRA },
#endif
    { QVideoFrame::Format_RGB24 ,  GST_VIDEO_FORMAT_RGB },
    { QVideoFrame::Format_BGR24 ,  GST_VIDEO_FORMAT_BGR },
    { QVideoFrame::Format_RGB565,  GST_VIDEO_FORMAT_RGB16 }
};

static int indexOfVideoFormat(QVideoFrame::PixelFormat format)
{
    for (int i = 0; i < lengthOf(qt_videoFormatLookup); ++i)
        if (qt_videoFormatLookup[i].pixelFormat == format)
            return i;

    return -1;
}

static int indexOfVideoFormat(GstVideoFormat format)
{
    for (int i = 0; i < lengthOf(qt_videoFormatLookup); ++i)
        if (qt_videoFormatLookup[i].gstFormat == format)
            return i;

    return -1;
}

#else

struct YuvFormat
{
    QVideoFrame::PixelFormat pixelFormat;
    guint32 fourcc;
    int bitsPerPixel;
};

static const YuvFormat qt_yuvColorLookup[] =
{
    { QVideoFrame::Format_YUV420P, GST_MAKE_FOURCC('I','4','2','0'), 8 },
    { QVideoFrame::Format_YUV422P, GST_MAKE_FOURCC('Y','4','2','B'), 8 },
    { QVideoFrame::Format_YV12,    GST_MAKE_FOURCC('Y','V','1','2'), 8 },
    { QVideoFrame::Format_UYVY,    GST_MAKE_FOURCC('U','Y','V','Y'), 16 },
    { QVideoFrame::Format_YUYV,    GST_MAKE_FOURCC('Y','U','Y','2'), 16 },
    { QVideoFrame::Format_NV12,    GST_MAKE_FOURCC('N','V','1','2'), 8 },
    { QVideoFrame::Format_NV21,    GST_MAKE_FOURCC('N','V','2','1'), 8 },
    { QVideoFrame::Format_AYUV444, GST_MAKE_FOURCC('A','Y','U','V'), 32 }
};

static int indexOfYuvColor(QVideoFrame::PixelFormat format)
{
    const int count = sizeof(qt_yuvColorLookup) / sizeof(YuvFormat);

    for (int i = 0; i < count; ++i)
        if (qt_yuvColorLookup[i].pixelFormat == format)
            return i;

    return -1;
}

static int indexOfYuvColor(guint32 fourcc)
{
    const int count = sizeof(qt_yuvColorLookup) / sizeof(YuvFormat);

    for (int i = 0; i < count; ++i)
        if (qt_yuvColorLookup[i].fourcc == fourcc)
            return i;

    return -1;
}

struct RgbFormat
{
    QVideoFrame::PixelFormat pixelFormat;
    int bitsPerPixel;
    int depth;
    int endianness;
    int red;
    int green;
    int blue;
    int alpha;
};

static const RgbFormat qt_rgbColorLookup[] =
{
    { QVideoFrame::Format_RGB32 , 32, 24, 4321, 0x0000FF00, 0x00FF0000, int(0xFF000000), 0x00000000 },
    { QVideoFrame::Format_RGB32 , 32, 24, 1234, 0x00FF0000, 0x0000FF00, 0x000000FF, 0x00000000 },
    { QVideoFrame::Format_BGR32 , 32, 24, 4321, int(0xFF000000), 0x00FF0000, 0x0000FF00, 0x00000000 },
    { QVideoFrame::Format_BGR32 , 32, 24, 1234, 0x000000FF, 0x0000FF00, 0x00FF0000, 0x00000000 },
    { QVideoFrame::Format_ARGB32, 32, 24, 4321, 0x0000FF00, 0x00FF0000, int(0xFF000000), 0x000000FF },
    { QVideoFrame::Format_ARGB32, 32, 24, 1234, 0x00FF0000, 0x0000FF00, 0x000000FF, int(0xFF000000) },
    { QVideoFrame::Format_RGB24 , 24, 24, 4321, 0x00FF0000, 0x0000FF00, 0x000000FF, 0x00000000 },
    { QVideoFrame::Format_BGR24 , 24, 24, 4321, 0x000000FF, 0x0000FF00, 0x00FF0000, 0x00000000 },
    { QVideoFrame::Format_RGB565, 16, 16, 1234, 0x0000F800, 0x000007E0, 0x0000001F, 0x00000000 }
};

static int indexOfRgbColor(
        int bits, int depth, int endianness, int red, int green, int blue, int alpha)
{
    const int count = sizeof(qt_rgbColorLookup) / sizeof(RgbFormat);

    for (int i = 0; i < count; ++i) {
        if (qt_rgbColorLookup[i].bitsPerPixel == bits
            && qt_rgbColorLookup[i].depth == depth
            && qt_rgbColorLookup[i].endianness == endianness
            && qt_rgbColorLookup[i].red == red
            && qt_rgbColorLookup[i].green == green
            && qt_rgbColorLookup[i].blue == blue
            && qt_rgbColorLookup[i].alpha == alpha) {
            return i;
        }
    }
    return -1;
}
#endif

}

#if GST_CHECK_VERSION(1,0,0)

QVideoSurfaceFormat QGstUtils::formatForCaps(
        GstCaps *caps, GstVideoInfo *info, QAbstractVideoBuffer::HandleType handleType)
{
    GstVideoInfo vidInfo;
    GstVideoInfo *infoPtr = info ? info : &vidInfo;

    if (gst_video_info_from_caps(infoPtr, caps)) {
        int index = indexOfVideoFormat(infoPtr->finfo->format);

        if (index != -1) {
            QVideoSurfaceFormat format(
                        QSize(infoPtr->width, infoPtr->height),
                        qt_videoFormatLookup[index].pixelFormat,
                        handleType);

            if (infoPtr->fps_d > 0)
                format.setFrameRate(qreal(infoPtr->fps_n) / infoPtr->fps_d);

            if (infoPtr->par_d > 0)
                format.setPixelAspectRatio(infoPtr->par_n, infoPtr->par_d);

            return format;
        }
    }
    return QVideoSurfaceFormat();
}

#else

QVideoSurfaceFormat QGstUtils::formatForCaps(
        GstCaps *caps, int *bytesPerLine, QAbstractVideoBuffer::HandleType handleType)
{
    const GstStructure *structure = gst_caps_get_structure(caps, 0);

    int bitsPerPixel = 0;
    QSize size = structureResolution(structure);
    QVideoFrame::PixelFormat pixelFormat = structurePixelFormat(structure, &bitsPerPixel);

    if (pixelFormat != QVideoFrame::Format_Invalid) {
        QVideoSurfaceFormat format(size, pixelFormat, handleType);

        QPair<qreal, qreal> rate = structureFrameRateRange(structure);
        if (rate.second)
            format.setFrameRate(rate.second);

        format.setPixelAspectRatio(structurePixelAspectRatio(structure));

        if (bytesPerLine)
            *bytesPerLine = ((size.width() * bitsPerPixel / 8) + 3) & ~3;

        return format;
    }
    return QVideoSurfaceFormat();
}

#endif

GstCaps *QGstUtils::capsForFormats(const QList<QVideoFrame::PixelFormat> &formats)
{
    GstCaps *caps = gst_caps_new_empty();

#if GST_CHECK_VERSION(1,0,0)
    for (QVideoFrame::PixelFormat format : formats) {
        int index = indexOfVideoFormat(format);

        if (index != -1) {
            gst_caps_append_structure(caps, gst_structure_new(
                    "video/x-raw",
                    "format"   , G_TYPE_STRING, gst_video_format_to_string(qt_videoFormatLookup[index].gstFormat),
                    nullptr));
        }
    }
#else
    for (QVideoFrame::PixelFormat format : formats) {
        int index = indexOfYuvColor(format);

        if (index != -1) {
            gst_caps_append_structure(caps, gst_structure_new(
                    "video/x-raw-yuv",
                    "format", GST_TYPE_FOURCC, qt_yuvColorLookup[index].fourcc,
                    nullptr));
            continue;
        }

        const int count = sizeof(qt_rgbColorLookup) / sizeof(RgbFormat);

        for (int i = 0; i < count; ++i) {
            if (qt_rgbColorLookup[i].pixelFormat == format) {
                GstStructure *structure = gst_structure_new(
                        "video/x-raw-rgb",
                        "bpp"       , G_TYPE_INT, qt_rgbColorLookup[i].bitsPerPixel,
                        "depth"     , G_TYPE_INT, qt_rgbColorLookup[i].depth,
                        "endianness", G_TYPE_INT, qt_rgbColorLookup[i].endianness,
                        "red_mask"  , G_TYPE_INT, qt_rgbColorLookup[i].red,
                        "green_mask", G_TYPE_INT, qt_rgbColorLookup[i].green,
                        "blue_mask" , G_TYPE_INT, qt_rgbColorLookup[i].blue,
                        nullptr);

                if (qt_rgbColorLookup[i].alpha != 0) {
                    gst_structure_set(
                            structure, "alpha_mask", G_TYPE_INT, qt_rgbColorLookup[i].alpha, nullptr);
                }
                gst_caps_append_structure(caps, structure);
            }
        }
    }
#endif

    gst_caps_set_simple(
                caps,
                "framerate", GST_TYPE_FRACTION_RANGE, 0, 1, INT_MAX, 1,
                "width"    , GST_TYPE_INT_RANGE, 1, INT_MAX,
                "height"   , GST_TYPE_INT_RANGE, 1, INT_MAX,
                nullptr);

    return caps;
}

void QGstUtils::setFrameTimeStamps(QVideoFrame *frame, GstBuffer *buffer)
{
    // GStreamer uses nanoseconds, Qt uses microseconds
    qint64 startTime = GST_BUFFER_TIMESTAMP(buffer);
    if (startTime >= 0) {
        frame->setStartTime(startTime/G_GINT64_CONSTANT (1000));

        qint64 duration = GST_BUFFER_DURATION(buffer);
        if (duration >= 0)
            frame->setEndTime((startTime + duration)/G_GINT64_CONSTANT (1000));
    }
}

void QGstUtils::setMetaData(GstElement *element, const QMap<QByteArray, QVariant> &data)
{
    if (!GST_IS_TAG_SETTER(element))
        return;

    gst_tag_setter_reset_tags(GST_TAG_SETTER(element));

    for (auto it = data.cbegin(), end = data.cend(); it != end; ++it) {
        const QString tagName = QString::fromLatin1(it.key());
        const QVariant &tagValue = it.value();

        switch (tagValue.type()) {
            case QVariant::String:
                gst_tag_setter_add_tags(GST_TAG_SETTER(element),
                    GST_TAG_MERGE_REPLACE,
                    tagName.toUtf8().constData(),
                    tagValue.toString().toUtf8().constData(),
                    nullptr);
                break;
            case QVariant::Int:
            case QVariant::LongLong:
                gst_tag_setter_add_tags(GST_TAG_SETTER(element),
                    GST_TAG_MERGE_REPLACE,
                    tagName.toUtf8().constData(),
                    tagValue.toInt(),
                    nullptr);
                break;
            case QVariant::Double:
                gst_tag_setter_add_tags(GST_TAG_SETTER(element),
                    GST_TAG_MERGE_REPLACE,
                    tagName.toUtf8().constData(),
                    tagValue.toDouble(),
                    nullptr);
                break;
#if GST_CHECK_VERSION(0, 10, 31)
            case QVariant::DateTime: {
                QDateTime date = tagValue.toDateTime().toLocalTime();
                gst_tag_setter_add_tags(GST_TAG_SETTER(element),
                    GST_TAG_MERGE_REPLACE,
                    tagName.toUtf8().constData(),
                    gst_date_time_new_local_time(
                                date.date().year(), date.date().month(), date.date().day(),
                                date.time().hour(), date.time().minute(), date.time().second()),
                    nullptr);
                break;
            }
#endif
            default:
                break;
        }
    }
}

void QGstUtils::setMetaData(GstBin *bin, const QMap<QByteArray, QVariant> &data)
{
    GstIterator *elements = gst_bin_iterate_all_by_interface(bin, GST_TYPE_TAG_SETTER);
#if GST_CHECK_VERSION(1,0,0)
    GValue item = G_VALUE_INIT;
    while (gst_iterator_next(elements, &item) == GST_ITERATOR_OK) {
        GstElement * const element = GST_ELEMENT(g_value_get_object(&item));
#else
    GstElement *element = 0;
    while (gst_iterator_next(elements, (void**)&element) == GST_ITERATOR_OK) {
#endif
        setMetaData(element, data);
    }
    gst_iterator_free(elements);
}


GstCaps *QGstUtils::videoFilterCaps()
{
    const char *caps =
#if GST_CHECK_VERSION(1,2,0)
        "video/x-raw(ANY);"
#elif GST_CHECK_VERSION(1,0,0)
        "video/x-raw;"
#else
        "video/x-raw-yuv;"
        "video/x-raw-rgb;"
        "video/x-raw-data;"
        "video/x-android-buffer;"
#endif
        "image/jpeg;"
        "video/x-h264";
    static GstStaticCaps staticCaps = GST_STATIC_CAPS(caps);

    return gst_caps_make_writable(gst_static_caps_get(&staticCaps));
}

QSize QGstUtils::structureResolution(const GstStructure *s)
{
    QSize size;

    int w, h;
    if (s && gst_structure_get_int(s, "width", &w) && gst_structure_get_int(s, "height", &h)) {
        size.rwidth() = w;
        size.rheight() = h;
    }

    return size;
}

QVideoFrame::PixelFormat QGstUtils::structurePixelFormat(const GstStructure *structure, int *bpp)
{
    QVideoFrame::PixelFormat pixelFormat = QVideoFrame::Format_Invalid;

    if (!structure)
        return pixelFormat;

#if GST_CHECK_VERSION(1,0,0)
    Q_UNUSED(bpp);

    if (gst_structure_has_name(structure, "video/x-raw")) {
        const gchar *s = gst_structure_get_string(structure, "format");
        if (s) {
            GstVideoFormat format = gst_video_format_from_string(s);
            int index = indexOfVideoFormat(format);

            if (index != -1)
                pixelFormat = qt_videoFormatLookup[index].pixelFormat;
        }
    }
#else
    if (qstrcmp(gst_structure_get_name(structure), "video/x-raw-yuv") == 0) {
        guint32 fourcc = 0;
        gst_structure_get_fourcc(structure, "format", &fourcc);

        int index = indexOfYuvColor(fourcc);
        if (index != -1) {
            pixelFormat = qt_yuvColorLookup[index].pixelFormat;
            if (bpp)
                *bpp = qt_yuvColorLookup[index].bitsPerPixel;
        }
    } else if (qstrcmp(gst_structure_get_name(structure), "video/x-raw-rgb") == 0) {
        int bitsPerPixel = 0;
        int depth = 0;
        int endianness = 0;
        int red = 0;
        int green = 0;
        int blue = 0;
        int alpha = 0;

        gst_structure_get_int(structure, "bpp", &bitsPerPixel);
        gst_structure_get_int(structure, "depth", &depth);
        gst_structure_get_int(structure, "endianness", &endianness);
        gst_structure_get_int(structure, "red_mask", &red);
        gst_structure_get_int(structure, "green_mask", &green);
        gst_structure_get_int(structure, "blue_mask", &blue);
        gst_structure_get_int(structure, "alpha_mask", &alpha);

        int index = indexOfRgbColor(bitsPerPixel, depth, endianness, red, green, blue, alpha);

        if (index != -1) {
            pixelFormat = qt_rgbColorLookup[index].pixelFormat;
            if (bpp)
                *bpp = qt_rgbColorLookup[index].bitsPerPixel;
        }
    }
#endif

    return pixelFormat;
}

QSize QGstUtils::structurePixelAspectRatio(const GstStructure *s)
{
    QSize ratio(1, 1);

    gint aspectNum = 0;
    gint aspectDenum = 0;
    if (s && gst_structure_get_fraction(s, "pixel-aspect-ratio", &aspectNum, &aspectDenum)) {
        if (aspectDenum > 0) {
            ratio.rwidth() = aspectNum;
            ratio.rheight() = aspectDenum;
        }
    }

    return ratio;
}

QPair<qreal, qreal> QGstUtils::structureFrameRateRange(const GstStructure *s)
{
    QPair<qreal, qreal> rate;

    if (!s)
        return rate;

    int n, d;
    if (gst_structure_get_fraction(s, "framerate", &n, &d)) {
        rate.second = qreal(n) / d;
        rate.first = rate.second;
    } else if (gst_structure_get_fraction(s, "max-framerate", &n, &d)) {
        rate.second = qreal(n) / d;
        if (gst_structure_get_fraction(s, "min-framerate", &n, &d))
            rate.first = qreal(n) / d;
        else
            rate.first = qreal(1);
    }

    return rate;
}

typedef QMap<QString, QString> FileExtensionMap;
Q_GLOBAL_STATIC(FileExtensionMap, fileExtensionMap)

QString QGstUtils::fileExtensionForMimeType(const QString &mimeType)
{
    if (fileExtensionMap->isEmpty()) {
        //extension for containers hard to guess from mimetype
        fileExtensionMap->insert(QStringLiteral("video/x-matroska"), QLatin1String("mkv"));
        fileExtensionMap->insert(QStringLiteral("video/quicktime"), QLatin1String("mov"));
        fileExtensionMap->insert(QStringLiteral("video/x-msvideo"), QLatin1String("avi"));
        fileExtensionMap->insert(QStringLiteral("video/msvideo"), QLatin1String("avi"));
        fileExtensionMap->insert(QStringLiteral("audio/mpeg"), QLatin1String("mp3"));
        fileExtensionMap->insert(QStringLiteral("application/x-shockwave-flash"), QLatin1String("swf"));
        fileExtensionMap->insert(QStringLiteral("application/x-pn-realmedia"), QLatin1String("rm"));
    }

    //for container names like avi instead of video/x-msvideo, use it as extension
    if (!mimeType.contains(QLatin1Char('/')))
        return mimeType;

    QString format = mimeType.left(mimeType.indexOf(QLatin1Char(',')));
    QString extension = fileExtensionMap->value(format);

    if (!extension.isEmpty() || format.isEmpty())
        return extension;

    QRegularExpression rx(QStringLiteral("[-/]([\\w]+)$"));
    QRegularExpressionMatch match = rx.match(format);

    if (match.hasMatch())
        extension = match.captured(1);

    return extension;
}

#if GST_CHECK_VERSION(0,10,30)
QVariant QGstUtils::fromGStreamerOrientation(const QVariant &value)
{
    // Note gstreamer tokens either describe the counter clockwise rotation of the
    // image or the clockwise transform to apply to correct the image.  The orientation
    // value returned is the clockwise rotation of the image.
    const QString token = value.toString();
    if (token == QStringLiteral("rotate-90"))
        return 270;
    if (token == QStringLiteral("rotate-180"))
        return 180;
    if (token == QStringLiteral("rotate-270"))
        return 90;
    return 0;
}

QVariant QGstUtils::toGStreamerOrientation(const QVariant &value)
{
    switch (value.toInt()) {
    case 90:
        return QStringLiteral("rotate-270");
    case 180:
        return QStringLiteral("rotate-180");
    case 270:
        return QStringLiteral("rotate-90");
    default:
        return QStringLiteral("rotate-0");
    }
}
#endif

bool QGstUtils::useOpenGL()
{
    static bool result = qEnvironmentVariableIntValue("QT_GSTREAMER_USE_OPENGL_PLUGIN");
    return result;
}

void qt_gst_object_ref_sink(gpointer object)
{
#if GST_CHECK_VERSION(0,10,24)
    gst_object_ref_sink(object);
#else
    g_return_if_fail (GST_IS_OBJECT(object));

    GST_OBJECT_LOCK(object);
    if (G_LIKELY(GST_OBJECT_IS_FLOATING(object))) {
        GST_OBJECT_FLAG_UNSET(object, GST_OBJECT_FLOATING);
        GST_OBJECT_UNLOCK(object);
    } else {
        GST_OBJECT_UNLOCK(object);
        gst_object_ref(object);
    }
#endif
}

GstCaps *qt_gst_pad_get_current_caps(GstPad *pad)
{
#if GST_CHECK_VERSION(1,0,0)
    return gst_pad_get_current_caps(pad);
#else
    return gst_pad_get_negotiated_caps(pad);
#endif
}

GstCaps *qt_gst_pad_get_caps(GstPad *pad)
{
#if GST_CHECK_VERSION(1,0,0)
    return gst_pad_query_caps(pad, nullptr);
#elif GST_CHECK_VERSION(0, 10, 26)
    return gst_pad_get_caps_reffed(pad);
#else
    return gst_pad_get_caps(pad);
#endif
}

GstStructure *qt_gst_structure_new_empty(const char *name)
{
#if GST_CHECK_VERSION(1,0,0)
    return gst_structure_new_empty(name);
#else
    return gst_structure_new(name, nullptr);
#endif
}

gboolean qt_gst_element_query_position(GstElement *element, GstFormat format, gint64 *cur)
{
#if GST_CHECK_VERSION(1,0,0)
    return gst_element_query_position(element, format, cur);
#else
    return gst_element_query_position(element, &format, cur);
#endif
}

gboolean qt_gst_element_query_duration(GstElement *element, GstFormat format, gint64 *cur)
{
#if GST_CHECK_VERSION(1,0,0)
    return gst_element_query_duration(element, format, cur);
#else
    return gst_element_query_duration(element, &format, cur);
#endif
}

GstCaps *qt_gst_caps_normalize(GstCaps *caps)
{
#if GST_CHECK_VERSION(1,0,0)
    // gst_caps_normalize() takes ownership of the argument in 1.0
    return gst_caps_normalize(caps);
#else
    // in 0.10, it doesn't. Unref the argument to mimic the 1.0 behavior
    GstCaps *res = gst_caps_normalize(caps);
    gst_caps_unref(caps);
    return res;
#endif
}

const gchar *qt_gst_element_get_factory_name(GstElement *element)
{
    const gchar *name = 0;
    const GstElementFactory *factory = 0;

    if (element && (factory = gst_element_get_factory(element)))
        name = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory));

    return name;
}

gboolean qt_gst_caps_can_intersect(const GstCaps * caps1, const GstCaps * caps2)
{
#if GST_CHECK_VERSION(0, 10, 25)
    return gst_caps_can_intersect(caps1, caps2);
#else
    GstCaps *intersection = gst_caps_intersect(caps1, caps2);
    gboolean res = !gst_caps_is_empty(intersection);
    gst_caps_unref(intersection);
    return res;
#endif
}

#if !GST_CHECK_VERSION(0, 10, 31)
static gboolean qt_gst_videosink_factory_filter(GstPluginFeature *feature, gpointer)
{
  guint rank;
  const gchar *klass;

  if (!GST_IS_ELEMENT_FACTORY(feature))
    return FALSE;

  klass = gst_element_factory_get_klass(GST_ELEMENT_FACTORY(feature));
  if (!(strstr(klass, "Sink") && strstr(klass, "Video")))
    return FALSE;

  rank = gst_plugin_feature_get_rank(feature);
  if (rank < GST_RANK_MARGINAL)
    return FALSE;

  return TRUE;
}

static gint qt_gst_compare_ranks(GstPluginFeature *f1, GstPluginFeature *f2)
{
  gint diff;

  diff = gst_plugin_feature_get_rank(f2) - gst_plugin_feature_get_rank(f1);
  if (diff != 0)
    return diff;

  return strcmp(gst_plugin_feature_get_name(f2), gst_plugin_feature_get_name (f1));
}
#endif

GList *qt_gst_video_sinks()
{
    GList *list = nullptr;

#if GST_CHECK_VERSION(0, 10, 31)
    list = gst_element_factory_list_get_elements(GST_ELEMENT_FACTORY_TYPE_SINK | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO,
                                                 GST_RANK_MARGINAL);
#else
    list = gst_registry_feature_filter(gst_registry_get_default(),
                                       (GstPluginFeatureFilter)qt_gst_videosink_factory_filter,
                                       FALSE, nullptr);
    list = g_list_sort(list, (GCompareFunc)qt_gst_compare_ranks);
#endif

    return list;
}

void qt_gst_util_double_to_fraction(gdouble src, gint *dest_n, gint *dest_d)
{
#if GST_CHECK_VERSION(0, 10, 26)
    gst_util_double_to_fraction(src, dest_n, dest_d);
#else
    qt_real_to_fraction(src, dest_n, dest_d);
#endif
}

QDebug operator <<(QDebug debug, GstCaps *caps)
{
    if (caps) {
        gchar *string = gst_caps_to_string(caps);
        debug = debug << string;
        g_free(string);
    }
    return debug;
}

QT_END_NAMESPACE
