/***************************************************************
 * Copyright (C) 2020, 2025, Val Doroshchuk <valbok@gmail.com> *
 *                                                             *
 * This file is part of QtAVPlayer.                            *
 * Free Qt Media Player based on FFmpeg.                       *
 ***************************************************************/

#include "qavdemuxer_p.h"
#include "qavformatcontext_p.h"
#include "qavvideocodec_p.h"
#include "qavaudiocodec_p.h"
#include "qavsubtitlecodec_p.h"
#include "qavhwdevice_p.h"
#include "qaviodevice.h"
#include <QtAVPlayer/qtavplayerglobal.h>

#if defined(QT_AVPLAYER_VA_X11) && QT_CONFIG(opengl)
#include "qavhwdevice_vaapi_x11_glx_p.h"
#endif

#if defined(QT_AVPLAYER_VA_DRM) && QT_CONFIG(egl)
#include "qavhwdevice_vaapi_drm_egl_p.h"
#endif

#if defined(QT_AVPLAYER_VDPAU)
#include "qavhwdevice_vdpau_p.h"
#endif

#if defined(Q_OS_MACOS) || defined(Q_OS_IOS)
#include "qavhwdevice_videotoolbox_p.h"
#endif

#if defined(Q_OS_WIN)
#include "qavhwdevice_d3d11_p.h"
#endif

#if defined(Q_OS_ANDROID)
#include "qavhwdevice_mediacodec_p.h"
#include <QtCore/private/qjnihelpers_p.h>
extern "C" {
#include "libavcodec/jni.h"
}
#endif

#include "qavhwdevice_cuda_p.h"

#include <QDir>
#include <QSharedPointer>
#include <QMutexLocker>
#include <atomic>
#include <cstring>
#include <QDebug>

extern "C" {
#include <libavformat/avformat.h>
#include <libavdevice/avdevice.h>
#include <libavcodec/avcodec.h>
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 0, 0)
#include <libavcodec/bsf.h>
#endif
}

QT_BEGIN_NAMESPACE

namespace {

struct QAVDictionaryHolder
{
    AVDictionary *dict = nullptr;

    QAVDictionaryHolder() = default;

#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
    Q_DISABLE_COPY_MOVE(QAVDictionaryHolder)
#else
    Q_DISABLE_COPY(QAVDictionaryHolder)
    QAVDictionaryHolder(QAVDictionaryHolder &&) Q_DECL_EQ_DELETE;
    QAVDictionaryHolder &operator=(QAVDictionaryHolder &&) Q_DECL_EQ_DELETE;
#endif

    ~QAVDictionaryHolder()
    {
        if (dict) {
            av_dict_free(&dict);
        }
    }
};

} // namespace

class QAVDemuxerPrivate
{
    Q_DECLARE_PUBLIC(QAVDemuxer)
public:
    QAVDemuxerPrivate(QAVDemuxer *q)
        : q_ptr(q)
    {
    }

    QAVDemuxer *q_ptr = nullptr;
    QSharedPointer<QAVFormatContext> ctx;
    AVBSFContext *bsf_ctx = nullptr;

    std::atomic_bool abortRequest = false;
    mutable QMutex mutex;

    bool seekable = false;
    QList<QAVStream> availableStreams;
    QList<QAVStream> currentVideoStreams;
    QList<QAVStream> currentAudioStreams;
    QList<QAVStream> currentSubtitleStreams;
    QList<QAVStream::Progress> progress;
    QString inputFormat;
    QString inputVideoCodec;
    QMap<QString, QString> inputOptions;
    QMap<QString, QString> videoCodecOptions;

    bool eof = false;
    QList<QAVPacket> packets;
    QString bsfs;
};

static void log_callback(void *ptr, int level, const char *fmt, va_list vl)
{
    /* Do we need to log ? */
    if(level > av_log_get_level()){
        return;
    }

    /* Format log line */
    char line[1024];
    static int print_prefix = 1;

    av_log_format_line(ptr, level, fmt, vl, line, sizeof(line), &print_prefix);

    /* Remove trailing newline */
    size_t len = std::strlen(line);
    if (len > 0 && line[len - 1] == '\n') {
        line[len - 1] = '\0';
    }

    /* Adapt it to Qt log format */
    switch(level)
    {
        case AV_LOG_PANIC:{
            qFatal("[ffmpeg] %s", line);
        }break;
        
        case AV_LOG_FATAL:
        case AV_LOG_ERROR:{
            qCritical("[ffmpeg] %s", line);
        }break;

        case AV_LOG_WARNING:{
            qWarning("[ffmpeg] %s", line);
        }break;

        case AV_LOG_INFO:{
            qInfo("[ffmpeg] %s", line);
        }break;

        default:{
            qDebug("[ffmpeg] %s", line);
        }break;
    }
}

static int decode_interrupt_cb(void *ctx)
{
    auto d = reinterpret_cast<QAVDemuxerPrivate *>(ctx);
    return d ? int(d->abortRequest) : 0;
}

QAVDemuxer::QAVDemuxer()
    : d_ptr(new QAVDemuxerPrivate(this))
{
    static bool loaded = false;
    if (!loaded) {
#if (LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58,9,100))
        av_register_all();
        avcodec_register_all();
#endif
        avdevice_register_all();
        av_log_set_callback(log_callback);
        loaded = true;
    }
}

QAVDemuxer::~QAVDemuxer()
{
    abort(false);
    unload();
}

void QAVDemuxer::abort(bool stop)
{
    Q_D(QAVDemuxer);
    d->abortRequest = stop;
}

static int setup_video_codec(const QString &inputVideoCodec, const QAVStream &stream, QAVVideoCodec &codec, AVDictionary **codecOpts)
{
    bool ignoreHW = qEnvironmentVariableIsSet("QT_AVPLAYER_NO_HWDEVICE");
    const AVCodec *videoCodec = nullptr;
    auto streamInfo = stream.info();
    if (streamInfo.title.isEmpty())
        streamInfo.title = QLatin1String("Stream %1").arg(QString::number(stream.index()));
    if (!inputVideoCodec.isEmpty()) {
        if (inputVideoCodec == QLatin1String("software")) {
            qDebug() << "[" << streamInfo.title << "] Ignoring hardware device context";
            ignoreHW = true;
        } else {
            qDebug() << "Loading: -vcodec" << inputVideoCodec;
            videoCodec = avcodec_find_decoder_by_name(inputVideoCodec.toUtf8().constData());
            if (!videoCodec) {
                qWarning() << "Could not find decoder:" << inputVideoCodec;
                return AVERROR(EINVAL);
            }
        }
    }

    if (videoCodec)
        codec.setCodec(videoCodec);

    // Implemented HW devices
    QMap<AVHWDeviceType, QSharedPointer<QAVHWDevice>> devices;
    // Ordered supported devices
    QList<AVHWDeviceType> preferredDevices;
    QAVDictionaryHolder opts;
    Q_UNUSED(opts);

#if defined(QT_AVPLAYER_VA_X11) && QT_CONFIG(opengl)
    devices[AV_HWDEVICE_TYPE_VAAPI].reset(new QAVHWDevice_VAAPI_X11_GLX);
    preferredDevices.push_back(AV_HWDEVICE_TYPE_VAAPI);
    av_dict_set(&opts.dict, "connection_type", "x11", 0);
#endif
#if defined(QT_AVPLAYER_VDPAU)
    devices[AV_HWDEVICE_TYPE_VDPAU].reset(new QAVHWDevice_VDPAU);
    preferredDevices.push_back(AV_HWDEVICE_TYPE_VDPAU);
#endif
#if defined(QT_AVPLAYER_VA_DRM) && QT_CONFIG(egl)
    devices[AV_HWDEVICE_TYPE_VAAPI].reset(new QAVHWDevice_VAAPI_DRM_EGL);
    preferredDevices.push_back(AV_HWDEVICE_TYPE_VAAPI);
#endif
#if defined(Q_OS_MACOS) || defined(Q_OS_IOS)
    devices[AV_HWDEVICE_TYPE_VIDEOTOOLBOX].reset(new QAVHWDevice_VideoToolbox);
    preferredDevices.push_back(AV_HWDEVICE_TYPE_VIDEOTOOLBOX);
#endif
#if defined(Q_OS_WIN)
    devices[AV_HWDEVICE_TYPE_D3D11VA].reset(new QAVHWDevice_D3D11);
    preferredDevices.push_back(AV_HWDEVICE_TYPE_D3D11VA);
#endif
#if defined(Q_OS_ANDROID)
    devices[AV_HWDEVICE_TYPE_MEDIACODEC].reset(new QAVHWDevice_MediaCodec);
    preferredDevices.push_back(AV_HWDEVICE_TYPE_MEDIACODEC);
    if (!ignoreHW && !codec.codec())
        codec.setCodec(avcodec_find_decoder_by_name("h264_mediacodec"));
    auto vm = QtAndroidPrivate::javaVM();
    av_jni_set_java_vm(vm, NULL);
#endif
#if defined(QT_AVPLAYER_CUDA)
    devices[AV_HWDEVICE_TYPE_CUDA].reset(new QAVHWDevice_CUDA);
    preferredDevices.push_back(AV_HWDEVICE_TYPE_CUDA);
#endif

    if (!ignoreHW) {
        if (videoCodec) {
            // Negotiate the devices from the video codec
            preferredDevices = QAVVideoCodec::supportedHWDevices(videoCodec);
        }
        AVBufferRef *hw_device_ctx = nullptr;
        for (auto &supported : preferredDevices) {
            auto it = devices.find(supported);
            if (it == devices.end())
                continue;
            auto device = *it;
            auto deviceName = av_hwdevice_get_type_name(device->type());
            if (av_hwdevice_ctx_create(&hw_device_ctx, device->type(), nullptr, opts.dict, 0) >= 0) {
                qDebug() << "[" << streamInfo.title << "] Using" << deviceName << "hardware device context";
                codec.avctx()->hw_device_ctx = hw_device_ctx;
                codec.avctx()->pix_fmt = device->format();
                codec.setDevice(device);
                break;
            }
            av_buffer_unref(&hw_device_ctx);
        }
    }

    // Open codec after hwdevices
    if (!codec.open(stream.stream(), codecOpts)) {
        qWarning() << "Could not open video codec for stream";
        return AVERROR(EINVAL);
    }

    return 0;
}

QStringList QAVDemuxer::supportedFormats()
{
    static QStringList values;
    if (values.isEmpty()) {
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 0, 0)
        const AVInputFormat *fmt = nullptr;
        void *it = nullptr;
        while ((fmt = av_demuxer_iterate(&it))) {
            if (fmt->name)
                values << QString::fromLatin1(fmt->name).split(QLatin1Char(','),
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
                    QString::SkipEmptyParts
#else
                    Qt::SkipEmptyParts
#endif
                );
        }
#endif
    }

    return values;
}

QStringList QAVDemuxer::supportedVideoCodecs()
{
    static QStringList values;
    if (values.isEmpty()) {
        const AVCodec *c = nullptr;
        void *it = nullptr;
        while ((c = av_codec_iterate(&it))) {
            if (!av_codec_is_decoder(c) || c->type != AVMEDIA_TYPE_VIDEO)
                continue;
            values.append(QString::fromLatin1(c->name));
        }
    }

    return values;
}

QStringList QAVDemuxer::supportedProtocols()
{
    static QStringList values;
    if (values.isEmpty()) {
        void *opq = 0;
        const char *value = nullptr;
        while ((value = avio_enum_protocols(&opq, 0)))
            values << QString::fromUtf8(value);
    }

    return values;
}

static int init_output_bsfs(AVBSFContext *ctx, AVStream *st)
{
    if (!ctx)
        return 0;

    int ret = avcodec_parameters_copy(ctx->par_in, st->codecpar);
    if (ret < 0)
        return ret;

    ctx->time_base_in = st->time_base;

    ret = av_bsf_init(ctx);
    if (ret < 0) {
        qWarning() << "Error initializing bitstream filter:" << ctx->filter->name;
        return ret;
    }

    ret = avcodec_parameters_copy(st->codecpar, ctx->par_out);
    if (ret < 0)
        return ret;

    st->time_base = ctx->time_base_out;
    return 0;
}

static int apply_bsf(const QString &bsf, AVFormatContext *ctx, AVBSFContext *&bsf_ctx)
{
    int ret = !bsf.isEmpty() ? av_bsf_list_parse_str(bsf.toUtf8().constData(), &bsf_ctx) : 0;
    if (ret < 0) {
        qWarning() << "Error parsing bitstream filter sequence:" << bsf;
        return ret;
    }

    for (std::size_t i = 0; i < ctx->nb_streams; ++i) {
        switch (ctx->streams[i]->codecpar->codec_type) {
            case AVMEDIA_TYPE_VIDEO:
            case AVMEDIA_TYPE_AUDIO:
                ret = init_output_bsfs(bsf_ctx, ctx->streams[i]);
                break;
            default:
                break;
        }
    }

    return ret;
}

int QAVDemuxer::load(const QString &url, QAVIODevice *dev)
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    Q_ASSERT(!d->ctx);
    d->ctx = QAVFormatContext::alloc();
    d->ctx->ctx()->flags |= AVFMT_FLAG_GENPTS;
    d->ctx->ctx()->interrupt_callback.callback = decode_interrupt_cb;
    d->ctx->ctx()->interrupt_callback.opaque = d;
    if (dev) {
        d->ctx->ctx()->pb = dev->ctx();
        d->ctx->ctx()->flags |= AVFMT_FLAG_CUSTOM_IO;
    }

#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 0, 0)
    const
#endif
    AVInputFormat *inputFormat = nullptr;
    if (!d->inputFormat.isEmpty()) {
        qDebug() << "Loading: -f" << d->inputFormat;
        inputFormat = av_find_input_format(d->inputFormat.toUtf8().constData());
        if (!inputFormat) {
            qWarning() << "Could not find input format:" << d->inputFormat;
            return AVERROR(EINVAL);
        }
    }

    QAVDictionaryHolder opts;
    for (const auto & key: d->inputOptions.keys())
        av_dict_set(&opts.dict, key.toUtf8().constData(), d->inputOptions[key].toUtf8().constData(),
                    0);
    int ret = avformat_open_input(&d->ctx->ctx(), url.toUtf8().constData(), inputFormat, &opts.dict);
    if (ret < 0)
        return ret;

    ret = avformat_find_stream_info(d->ctx->ctx(), NULL);
    if (ret < 0)
        return ret;

#if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(59, 8, 0)
    d->seekable = d->ctx->ctx()->iformat->read_seek || d->ctx->ctx()->iformat->read_seek2;
    if (d->ctx->ctx()->pb)
        d->seekable |= bool(d->ctx->ctx()->pb->seekable);
#else
    // TODO: Search and implement replacement function for seek
    d->seekable = true;
#endif

    ret = resetCodecs();
    if (ret < 0)
        return ret;

    const int videoStreamIndex = av_find_best_stream(
        d->ctx->ctx(),
        AVMEDIA_TYPE_VIDEO,
        -1,
        -1,
        nullptr,
        0);
    if (videoStreamIndex >= 0)
        d->currentVideoStreams.push_back(d->availableStreams[videoStreamIndex]);

    const int audioStreamIndex = av_find_best_stream(
        d->ctx->ctx(),
        AVMEDIA_TYPE_AUDIO,
        -1,
        videoStreamIndex,
        nullptr,
        0);
    if (audioStreamIndex >= 0)
        d->currentAudioStreams.push_back(d->availableStreams[audioStreamIndex]);

    const int subtitleStreamIndex = av_find_best_stream(
        d->ctx->ctx(),
        AVMEDIA_TYPE_SUBTITLE,
        -1,
        audioStreamIndex >= 0 ? audioStreamIndex : videoStreamIndex,
        nullptr,
        0);
    if (subtitleStreamIndex >= 0)
        d->currentSubtitleStreams.push_back(d->availableStreams[subtitleStreamIndex]);

    if (ret < 0)
        return ret;

    if (!d->bsfs.isEmpty())
        return apply_bsf(d->bsfs, d->ctx->ctx(), d->bsf_ctx);

    return 0;
}

int QAVDemuxer::resetCodecs()
{
    Q_D(QAVDemuxer);
    int ret = 0;
    for (std::size_t i = 0; i < d->ctx->ctx()->nb_streams && ret >= 0; ++i) {
        if (!d->ctx->ctx()->streams[i]->codecpar) {
            qWarning() << "Could not find codecpar";
            return AVERROR(EINVAL);
        }

        const AVMediaType type = d->ctx->ctx()->streams[i]->codecpar->codec_type;
        switch (type) {
            case AVMEDIA_TYPE_VIDEO:
            {
                QAVDictionaryHolder opts;
                for (const auto & key: d->videoCodecOptions.keys())
                    av_dict_set(&opts.dict, key.toUtf8().constData(), d->videoCodecOptions[key].toUtf8().constData(), 0);

                QSharedPointer<QAVCodec> codec(new QAVVideoCodec);
                d->availableStreams.push_back({ int(i), d->ctx, codec });
                ret = setup_video_codec(d->inputVideoCodec, d->availableStreams.last(), *static_cast<QAVVideoCodec *>(codec.data()), &opts.dict);
            } break;
            case AVMEDIA_TYPE_AUDIO:
                d->availableStreams.push_back({ int(i), d->ctx, QSharedPointer<QAVCodec>(new QAVAudioCodec) });
                if (!d->availableStreams.last().codec()->open(d->ctx->ctx()->streams[i]))
                    qWarning() << "Could not open audio codec for stream:" << i;
                break;
            case AVMEDIA_TYPE_SUBTITLE:
                d->availableStreams.push_back({ int(i), d->ctx, QSharedPointer<QAVCodec>(new QAVSubtitleCodec) });
                if (!d->availableStreams.last().codec()->open(d->ctx->ctx()->streams[i]))
                    qWarning() << "Could not open subtitle codec for stream:" << i;
                break;
            default:
                // Adding default stream
                d->availableStreams.push_back({ int(i), d->ctx, nullptr });
                break;
        }
        auto &s = d->availableStreams[int(i)];
        d->progress.push_back({ s.duration(), s.framesCount(), s.frameRate() });
    }

    return ret;
}

static bool findStream(
    const QList<QAVStream> &streams,
    int index)
{
    for (const auto &stream: streams) {
        if (index == stream.index())
            return true;
    }
    return false;
}

AVMediaType QAVDemuxer::currentCodecType(int index) const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    // TODO:
    if (findStream(d->currentVideoStreams, index))
        return AVMEDIA_TYPE_VIDEO;
    if (findStream(d->currentAudioStreams, index))
        return AVMEDIA_TYPE_AUDIO;
    if (findStream(d->currentSubtitleStreams, index))
        return AVMEDIA_TYPE_SUBTITLE;
    return AVMEDIA_TYPE_UNKNOWN;
}

static QList<QAVStream> availableStreamsByType(
    const QList<QAVStream> &streams,
    AVMediaType type)
{
    QList<QAVStream> ret;
    for (auto &stream : streams) {
        if (stream.stream()->codecpar->codec_type == type)
            ret.push_back(stream);
    }

    return ret;
}

QList<QAVStream> QAVDemuxer::availableStreams() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return d->availableStreams;
}

QList<QAVStream> QAVDemuxer::availableVideoStreams() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return availableStreamsByType(d->availableStreams, AVMEDIA_TYPE_VIDEO);
}

QList<QAVStream> QAVDemuxer::currentVideoStreams() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return d->currentVideoStreams;
}

static bool setCurrentStreams(
    const QList<QAVStream> &streams,
    const QList<QAVStream> &availableStreams,
    AVMediaType type,
    QList<QAVStream> &currentStreams)
{
    QList<QAVStream> ret;
    for (const auto &stream: streams) {
        if (stream.index() >= 0
            && stream.index() < availableStreams.size()
            && availableStreams[stream.index()].stream()->codecpar->codec_type == type)
        {
            ret.push_back(availableStreams[stream.index()]);
        }
    }
    if (!ret.isEmpty() || streams.isEmpty()) {
        currentStreams = ret;
        return true;
    }

    return false;
}

bool QAVDemuxer::setVideoStreams(const QList<QAVStream> &streams)
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return setCurrentStreams(
        streams,
        d->availableStreams,
        AVMEDIA_TYPE_VIDEO,
        d->currentVideoStreams);
}

QList<QAVStream> QAVDemuxer::availableAudioStreams() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return availableStreamsByType(
        d->availableStreams,
        AVMEDIA_TYPE_AUDIO);
}

QList<QAVStream> QAVDemuxer::currentAudioStreams() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return d->currentAudioStreams;
}

bool QAVDemuxer::setAudioStreams(const QList<QAVStream> &streams)
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return setCurrentStreams(
        streams,
        d->availableStreams,
        AVMEDIA_TYPE_AUDIO,
        d->currentAudioStreams);
}

QList<QAVStream> QAVDemuxer::availableSubtitleStreams() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return availableStreamsByType(
        d->availableStreams,
        AVMEDIA_TYPE_SUBTITLE);
}

QList<QAVStream> QAVDemuxer::currentSubtitleStreams() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return d->currentSubtitleStreams;
}

bool QAVDemuxer::setSubtitleStreams(const QList<QAVStream> &streams)
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return setCurrentStreams(
        streams,
        d->availableStreams,
        AVMEDIA_TYPE_SUBTITLE,
        d->currentSubtitleStreams);
}

AVFormatContext *QAVDemuxer::avctx() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return d->ctx ? d->ctx->ctx() : nullptr;
}

void QAVDemuxer::unload()
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    d->ctx = nullptr;
    d->eof = false;
    d->abortRequest = 0;
    d->currentVideoStreams.clear();
    d->currentAudioStreams.clear();
    d->currentSubtitleStreams.clear();
    d->availableStreams.clear();
    d->progress.clear();
    av_bsf_free(&d->bsf_ctx);
    d->bsf_ctx = nullptr;
}

bool QAVDemuxer::eof() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return d->eof;
}

int QAVDemuxer::read(QAVPacket &pkt)
{
    Q_D(QAVDemuxer);
    {
        QMutexLocker locker(&d->mutex);
        if (!d->packets.isEmpty()) {
            pkt = d->packets.takeFirst();
            return 0;
        }

        if (!d->ctx || !d->ctx->ctx() || d->eof) {
            pkt = QAVPacket();
            return AVERROR_EOF;
        }
    }

    av_packet_unref(pkt.packet());
    bool eof = false;
    int ret = av_read_frame(d->ctx->ctx(), pkt.packet());
    if (ret < 0) {
        if (ret == AVERROR_EOF || avio_feof(d->ctx->ctx()->pb)) {
            eof = true;
        } else {
            qDebug() << "av_read_frame: unexpected result:" << ret;
        }
    }
    {
        QMutexLocker locker(&d->mutex);
        d->eof = eof;
        if ((ret >= 0 || eof) && pkt.packet()->stream_index < d->availableStreams.size()) {
            auto stream = d->availableStreams[pkt.packet()->stream_index];
            Q_ASSERT(stream.stream());
            pkt.setStream(stream);
            // Normalize pts if it is based on the start time of the stream.
            // F.e. these packets could be returned from the micro or video.
            if (stream.stream()->duration == AV_NOPTS_VALUE
                && stream.stream()->start_time != AV_NOPTS_VALUE
                && pkt.packet()->pts >= stream.stream()->start_time) {
                pkt.packet()->pts -= stream.stream()->start_time;
                pkt.packet()->dts -= stream.stream()->start_time;
            }
        }
        // Allow EOF to flush BSF (send NULL)
        if ((ret >= 0 || eof) && d->bsf_ctx) {
            int bsf_ret = av_bsf_send_packet(d->bsf_ctx, d->eof ? NULL : pkt.packet());
            if (bsf_ret >= 0) {
                while ((bsf_ret = av_bsf_receive_packet(d->bsf_ctx, pkt.packet())) >= 0)
                    d->packets.append(pkt);
            }
            if (bsf_ret < 0 && bsf_ret != AVERROR_EOF && bsf_ret != AVERROR(EAGAIN)) {
                qWarning() << "Error applying bitstream filters to an output:" << bsf_ret;
                return bsf_ret;
            }
            if (!d->packets.isEmpty())
                pkt = d->packets.takeFirst();
            else if (!eof)
                pkt = QAVPacket();
        } else if (ret < 0 && !eof) {
            pkt = QAVPacket();
        }
    }

    return ret;
}

void QAVDemuxer::decode(const QAVPacket &pkt, QList<QAVFrame> &frames)
{
    if (!pkt.stream())
        return;
    int sent = 0;
    do {
        sent = pkt.send();
        // AVERROR(EAGAIN): input is not accepted in the current state - user must read output with avcodec_receive_frame()
        // (once all output is read, the packet should be resent, and the call will not fail with EAGAIN)
        if (sent < 0 && sent != AVERROR(EAGAIN))
            return;

        while (true) {
            QAVFrame frame;
            frame.setStream(pkt.stream());
            // AVERROR(EAGAIN): output is not available in this state - user must try to send new input
            int received = frame.receive();
            if (received < 0)
                break;
            frames.push_back(frame);
        }
    } while (sent == AVERROR(EAGAIN));
}

void QAVDemuxer::decode(const QAVPacket &pkt, QList<QAVSubtitleFrame> &frames)
{
    if (!pkt.stream())
        return;
    int sent = pkt.send();
    if (sent < 0 && sent != AVERROR(EAGAIN))
        return;

    QAVSubtitleFrame frame;
    frame.setStream(pkt.stream());
    if (frame.receive() >= 0)
        frames.push_back(frame);
}

void QAVDemuxer::flushCodecBuffers()
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    for (auto &s: d->availableStreams) {
        auto c = s.codec();
        if (c)
            c->flushBuffers();
    }
}

bool QAVDemuxer::seekable() const
{
    return d_func()->seekable;
}

int QAVDemuxer::seek(double sec)
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    if (!d->ctx || !d->ctx->ctx() || !d->seekable)
        return AVERROR(EINVAL);

    d->eof = false;
    locker.unlock();

    int flags = AVSEEK_FLAG_BACKWARD;
    int64_t target = sec * AV_TIME_BASE;
    int64_t min = INT_MIN;
    int64_t max = target;
    return avformat_seek_file(d->ctx->ctx(), -1, min, target, max, flags);
}

double QAVDemuxer::duration() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    if (!d->ctx || !d->ctx->ctx() || d->ctx->ctx()->duration == AV_NOPTS_VALUE)
        return 0.0;

    return d->ctx->ctx()->duration * av_q2d({1, AV_TIME_BASE});
}

double QAVDemuxer::videoFrameRate() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    if (d->currentVideoStreams.isEmpty())
        return 0.0;
    // TODO:
    double ret = std::numeric_limits<double>::max();
    for (const auto &stream: d->currentVideoStreams) {
        AVRational fr = av_guess_frame_rate(d->ctx->ctx(), d->ctx->ctx()->streams[stream.index()], NULL);
        double rate = fr.num && fr.den ? av_q2d({fr.den, fr.num}) : 0.0;
        if (rate < ret)
            ret = rate;
    }

    return ret;
}

QMap<QString, QString> QAVDemuxer::metadata() const
{
    Q_D(const QAVDemuxer);
    QMap<QString, QString> result;
    if (!d->ctx || !d->ctx->ctx())
        return result;

    AVDictionaryEntry *tag = nullptr;
    while ((tag = av_dict_get(d->ctx->ctx()->metadata, "", tag, AV_DICT_IGNORE_SUFFIX)))
        result[QString::fromUtf8(tag->key)] = QString::fromUtf8(tag->value);

    return result;
}

QString QAVDemuxer::bitstreamFilter() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return d->bsfs;
}

int QAVDemuxer::applyBitstreamFilter(const QString &bsfs)
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    d->bsfs = bsfs;
    int ret = 0;
    if (d->ctx && d->ctx->ctx()) {
        av_bsf_free(&d->bsf_ctx);
        d->bsf_ctx = nullptr;
        ret = apply_bsf(d->bsfs, d->ctx->ctx(), d->bsf_ctx);
    }
    return ret;
}

QString QAVDemuxer::inputFormat() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return d->inputFormat;
}

void QAVDemuxer::setInputFormat(const QString &format)
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    d->inputFormat = format;
}

QString QAVDemuxer::inputVideoCodec() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return d->inputVideoCodec;
}

void QAVDemuxer::setInputVideoCodec(const QString &codec)
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    d->inputVideoCodec = codec;
}

QMap<QString, QString> QAVDemuxer::inputOptions() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return d->inputOptions;
}

void QAVDemuxer::setInputOptions(const QMap<QString, QString> &opts)
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    d->inputOptions = opts;
}

QMap<QString, QString> QAVDemuxer::videoCodecOptions() const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    return d->videoCodecOptions;
}

void QAVDemuxer::setVideoCodecOptions(const QMap<QString, QString> &opts)
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    d->videoCodecOptions = opts;
}

void QAVDemuxer::onFrameSent(const QAVStreamFrame &frame)
{
    Q_D(QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    int index = frame.stream().index();
    if (index >= 0 && index < d->progress.size())
        d->progress[index].onFrameSent(frame.pts());
}

bool QAVDemuxer::isMasterStream(const QAVStream &stream) const
{
    auto s = stream.stream();
    switch (s->codecpar->codec_type) {
        case AVMEDIA_TYPE_VIDEO:
            return s->disposition != AV_DISPOSITION_ATTACHED_PIC;
        case AVMEDIA_TYPE_AUDIO:
            // Check if there are any video streams available
            for (const auto &vs: currentVideoStreams()) {
                if (vs.stream()->disposition != AV_DISPOSITION_ATTACHED_PIC)
                    return false;
            }
            return true;
        default:
            Q_ASSERT(false);
            return false;
    }
}

QAVStream::Progress QAVDemuxer::progress(const QAVStream &s) const
{
    Q_D(const QAVDemuxer);
    QMutexLocker locker(&d->mutex);
    int index = s.index();
    if (index >= 0 && index < d->progress.size())
        return d->progress[index];
    return {};
}

QStringList QAVDemuxer::supportedBitstreamFilters()
{
    QStringList result;
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(58, 0, 0)
    const AVBitStreamFilter *bsf = NULL;
    void *opaque = NULL;

    while ((bsf = av_bsf_iterate(&opaque)))
        result.append(QString::fromUtf8(bsf->name));
#endif
    return result;
}

QT_END_NAMESPACE
