#include "src/Backends/MPVCommon/MPVCommon.hpp" #include <QByteArray> #include <QCoreApplication> #include <QJsonObject> #include <QList> #include <QLocale> #include <QMap> #include <QMetaObject> #include <QMetaMethod> #include <QObject> #include <QSettings> #include <spdlog/fmt/fmt.h> #include <string.h> #include <exception> #include <memory> #include "spdlog/logger.h" #include "src/backendinterface.hpp" #include "src/logger.h" #include "src/utils.hpp" auto mpvLogger = initLogger("mpv"); QString humanSize(uint64_t bytes) { const char *suffix[5] = {"B", "KB", "MB", "GB", "TB"}; char length = sizeof(suffix) / sizeof(suffix[0]); int i = 0; double dblBytes = bytes; if (bytes > 1024) { for (i = 0; (bytes / 1024) > 0 && i<length-1; i++, bytes /= 1024) dblBytes = bytes / 1024.0; } static char output[200]; sprintf(output, "%.02lf %s", dblBytes, suffix[i]); return QString(output); } static inline QVariant mpvnode_to_variant(const mpv_node *node) { if (!node) { return QVariant(); } switch (node->format) { case MPV_FORMAT_STRING: return QVariant(QString::fromUtf8(node->u.string)); case MPV_FORMAT_FLAG: return QVariant(static_cast<bool>(node->u.flag)); case MPV_FORMAT_INT64: return QVariant(static_cast<qlonglong>(node->u.int64)); case MPV_FORMAT_DOUBLE: return QVariant(node->u.double_); case MPV_FORMAT_NODE_ARRAY: { mpv_node_list *list = node->u.list; QVariantList qlist; for (int n = 0; n < list->num; n++) qlist.append(mpvnode_to_variant(&list->values[n])); return QVariant(qlist); } case MPV_FORMAT_NODE_MAP: { mpv_node_list *list = node->u.list; QVariantMap qmap; for (int n = 0; n < list->num; n++) { qmap.insert(QString::fromUtf8(list->keys[n]), mpvnode_to_variant(&list->values[n])); } return QVariant(qmap); } default: // MPV_FORMAT_NONE, unknown values (e.g. future extensions) return QVariant(); } } namespace MPVCommon { QString getStats(BackendInterface *b) { QString stats; stats = "<style> blockquote { text-indent: 0px; margin-left:40px; margin-top: 0px; " "margin-bottom: 0px; padding-bottom: 0px; padding-top: 0px; padding-left: " "0px; } b span p br { margin-bottom: 0px; margin-top: 0px; padding-top: " "0px; padding-botom: 0px; text-indent: 0px; } </style>"; QString filename = b->getProperty("filename").toString(); // File Info stats += "<b>File:</b> " + filename; stats += "<blockquote>"; QString title = b->getProperty("media-title").toString(); if (title != filename) { stats += "<b>Title:</b> " + title + "<br>"; } QString fileFormat = b->getProperty("file-format").toString(); stats += "<b>Format/Protocol:</b> " + fileFormat + "<br>"; double cacheUsed = b->getProperty("cache-used").toDouble(); int demuxerSecs = b->getProperty("demuxer-cache-duration").toInt(); QVariantMap demuxerState = b->getProperty("demuxer-cache-state").toMap(); int demuxerCache = demuxerState.value("fw-bytes", QVariant(0)).toInt(); if (demuxerSecs + demuxerCache + cacheUsed > 0) { QString cacheStats; cacheStats += "<b>Total Cache:</b> "; cacheStats += humanSize(demuxerCache + cacheUsed); cacheStats += " (<b>Demuxer:</b> "; cacheStats += humanSize(demuxerCache); cacheStats += ", "; cacheStats += QString::number(demuxerSecs) + "s) "; double cacheSpeed = b->getProperty("cache-speed").toDouble(); if (cacheSpeed > 0) { cacheStats += "<b>Speed:</b> "; cacheStats += humanSize(demuxerSecs); cacheStats += "/s"; } cacheStats += "<br>"; stats += cacheStats; } QString fileSize = humanSize(b->getProperty("file-size").toInt()).remove("-"); stats += "<b>Size:</b> " + fileSize + "<br>"; stats += "</blockquote>"; // Video Info QVariant videoParams = b->getProperty("video-params"); if (videoParams.isNull()) { videoParams = b->getProperty("video-out-params"); } if (!videoParams.isNull()) { stats += "<b>Video:</b> " + b->getProperty("video-codec").toString(); stats += "<blockquote>"; QString avsync = QString::number(b->getProperty("avsync").toDouble(), 'f', 3); stats += "<b>A-V:</b> " + QString(avsync) + "<br>"; stats += "<b>Dropped Frames:</b> "; int dFDC = b->getProperty("decoder-frame-drop-count").toInt(); if (dFDC > 0) { stats += QString::number(dFDC) + " (decoder) "; } int fDC = b->getProperty("frame-drop-count").toInt(); if (fDC > 0) { stats += QString::number(fDC) + " (output)"; } stats += "<br>"; int dFPS = b->getProperty("display-fps").toInt(); int eDFPS = b->getProperty("estimated-display-fps").toInt(); if ((dFPS + eDFPS) > 0) { stats += "<b>Display FPS:</b> "; if (dFPS > 0) { stats += QString::number(dFPS); stats += " (specified) "; } if (eDFPS > 0) { stats += QString::number(eDFPS); stats += " (estimated)"; } stats += "<br>"; } int cFPS = b->getProperty("container-fps").toInt(); int eVFPS = b->getProperty("estimated-vf-fps").toInt(); if ((cFPS + eVFPS) > 0) { stats += "<b>FPS:</b> "; if (cFPS > 0) { stats += QString::number(cFPS); stats += " (specified) "; } if (eVFPS > 0) { stats += QString::number(eVFPS); stats += " (estimated)"; } stats += "<br>"; } QVariantMap vPM = videoParams.toMap(); stats += "<b>Native Resolution:</b> "; stats += vPM["w"].toString() + " x " + vPM["h"].toString(); stats += "<br>"; stats += "<b>Window Scale:</b> "; stats += vPM["window-scale"].toString(); stats += "<br>"; stats += "<b>Aspect Ratio:</b> "; stats += vPM["aspect"].toString(); stats += "<br>"; stats += "<b>Pixel Format:</b> "; stats += vPM["pixelformat"].toString(); stats += "<br>"; stats += "<b>Primaries:</b> "; stats += vPM["primaries"].toString(); stats += " <b>Colormatrix:</b> "; stats += vPM["colormatrix"].toString(); stats += "<br>"; stats += "<b>Levels:</b> "; stats += vPM["colorlevels"].toString(); double sigPeak = vPM.value("sig-peak", QVariant(0.0)).toInt(); if (sigPeak > 0) { stats += " (HDR Peak: " + QString::number(sigPeak) + ")"; } stats += "<br>"; stats += "<b>Gamma:</b> "; stats += vPM["gamma"].toString(); stats += "<br>"; int pVB = b->getProperty("packet-video-bitrate").toInt(); if (pVB > 0) { stats += "<b>Bitrate:</b> "; stats += humanSize(pVB) + "/s"; stats += "<br>"; } stats += "</blockquote>"; } QVariant audioParams = b->getProperty("audio-params"); if (audioParams.isNull()) { audioParams = b->getProperty("audio-out-params"); } if (!audioParams.isNull()) { stats += "<b>Audio:</b> " + b->getProperty("audio-codec").toString(); stats += "<blockquote>"; QVariantMap aPM = audioParams.toMap(); stats += "<b>Format:</b> "; stats += aPM["format"].toString(); stats += "<br>"; stats += "<b>Sample Rate:</b> "; stats += aPM["samplerate"].toString() + " Hz"; stats += "<br>"; stats += "<b>Channels:</b> "; stats += aPM["chanel-count"].toString(); stats += "<br>"; int pAB = b->getProperty("packet-audio-bitrate").toInt(); if (pAB > 0) { stats += "<b>Bitrate:</b> "; stats += humanSize(pAB) + "/s"; stats += "<br>"; } stats += "</blockquote>"; } return stats; } QVariant playerCommand(BackendInterface *b, const Enums::Commands& cmd, const QVariant& args) { switch (cmd) { case Enums::Commands::TogglePlayPause: { b->command(QVariantList() << "cycle" << "pause"); break; } case Enums::Commands::ToggleMute: { b->command(QVariantList() << "cycle" << "mute"); break; } case Enums::Commands::SetAudioDevice: { b->setProperty("audio-device", args.toString()); break; } case Enums::Commands::SetVolume: { b->command(QVariantList() << "set" << "volume" << args); break; } case Enums::Commands::AddVolume: { b->command(QVariantList() << "add" << "volume" << args); break; } case Enums::Commands::AddSpeed: { QString speedString = QString::number(b->getProperty("speed").toDouble() + args.toDouble()); QVariant newSpeed = QVariant(speedString.left(speedString.lastIndexOf('.') + 2)); b->playerCommand(Enums::Commands::SetSpeed, newSpeed); break; } case Enums::Commands::SubtractSpeed: { QString speedString = QString::number(b->getProperty("speed").toDouble() - args.toDouble()); QVariant newSpeed = QVariant(speedString.left(speedString.lastIndexOf('.') + 2)); b->playerCommand(Enums::Commands::SetSpeed, newSpeed); break; } case Enums::Commands::ChangeSpeed: { b->playerCommand( Enums::Commands::SetSpeed, QVariant(b->getProperty("speed").toDouble() * args.toDouble())); break; } case Enums::Commands::SetSpeed: { b->command(QVariantList() << "set" << "speed" << args.toString()); break; } case Enums::Commands::ToggleStats: { b->command(QVariantList() << "script-binding" << "stats/display-stats-toggle"); break; } case Enums::Commands::NextAudioTrack: { b->command(QVariantList() << "cycle" << "audio"); break; } case Enums::Commands::NextSubtitleTrack: { b->command(QVariantList() << "cycle" << "sub"); break; } case Enums::Commands::NextVideoTrack: { b->command(QVariantList() << "cycle" << "video"); break; } case Enums::Commands::PreviousPlaylistItem: { b->command(QVariantList() << "playlist-prev"); break; } case Enums::Commands::NextPlaylistItem: { b->command(QVariantList() << "playlist-next" << "force"); break; } case Enums::Commands::LoadFile: { b->command(QVariantList() << "loadfile" << args); break; } case Enums::Commands::AppendFile: { b->command(QVariantList() << "loadfile" << args << "append-play"); break; } case Enums::Commands::Seek: { b->command(QVariantList() << "seek" << args); break; } case Enums::Commands::SeekAbsolute: { b->command(QVariantList() << "seek" << args << "absolute"); break; } case Enums::Commands::ForwardFrame: { b->command(QVariantList() << "frame-step"); break; } case Enums::Commands::BackwardFrame: { b->command(QVariantList() << "frame-back-step"); break; } case Enums::Commands::SetTrack: { b->command(QVariantList() << "set" << args.toList()[0] << args.toList()[1]); break; } case Enums::Commands::SetPlaylistPos: { b->command(QVariantList() << "set" << "playlist-pos" << args); break; } case Enums::Commands::ForcePause: { b->command(QVariantList() << "set" << "pause" << "yes"); break; } default: { //qDebug() << "Command not found: " << cmd; break; } } return QVariant("NoOutput"); } void updateDurationString(BackendInterface *b, int numTime, QMetaMethod metaMethod) { QVariant speed = b->getProperty("speed"); QSettings settings; if (metaMethod.name() == "positionChanged") { if (speed != b->lastSpeed) { b->lastSpeed = speed.toDouble(); } else { if (numTime == b->lastTime) { return; } } b->lastTime = numTime; b->lastPositionString = Utils::createTimestamp(b->lastTime); } else if (metaMethod.name() == "durationChanged") { b->totalDurationString = Utils::createTimestamp(numTime); } QString durationString; durationString += b->lastPositionString; durationString += " / "; durationString += b->totalDurationString; if (b->lastSpeed != 1) { if (settings.value("Appearance/themeName", "").toString() != "RoosterTeeth") { durationString += " (" + speed.toString() + "x)"; } } emit b->durationStringChanged(durationString); } void handle_mpv_event(BackendInterface *b, mpv_event* event) { switch (event->event_id) { case MPV_EVENT_PROPERTY_CHANGE: { mpv_event_property* prop = (mpv_event_property*)event->data; if (strcmp(prop->name, "time-pos") == 0) { if (prop->format == MPV_FORMAT_DOUBLE) { double time = *(double*)prop->data; emit b->positionChanged(time); } } else if (strcmp(prop->name, "duration") == 0) { if (prop->format == MPV_FORMAT_DOUBLE) { double time = *(double*)prop->data; emit b->durationChanged(time); } } else if (strcmp(prop->name, "mute") == 0 || strcmp(prop->name, "volume") == 0) { double volume = b->getProperty("volume").toDouble(); bool mute = b->getProperty("mute").toBool(); if (mute || volume == 0) { emit b->volumeStatusChanged(Enums::VolumeStatus::Muted); } else { if (volume < 25) { emit b->volumeStatusChanged(Enums::VolumeStatus::Low); } else { emit b->volumeStatusChanged(Enums::VolumeStatus::Normal); } } // emit volumeChanged(volume); } else if (strcmp(prop->name, "media-title") == 0) { if (prop->format == MPV_FORMAT_STRING) { char* title = *(char**)prop->data; emit b->titleChanged(QString(title)); } } else if (strcmp(prop->name, "sub-text") == 0) { if (prop->format == MPV_FORMAT_STRING) { char* subs = *(char**)prop->data; emit b->subtitlesChanged(QString(subs)); } } else if (strcmp(prop->name, "demuxer-cache-duration") == 0) { if (prop->format == MPV_FORMAT_DOUBLE) { double duration = *(double*)prop->data; emit b->cachedDurationChanged(duration); } } else if (strcmp(prop->name, "playlist-pos") == 0) { if (prop->format == MPV_FORMAT_DOUBLE) { double pos = *(double*)prop->data; emit b->playlistPositionChanged(pos); } } else if (strcmp(prop->name, "pause") == 0) { mpv_node* nod = (mpv_node*)prop->data; if (mpvnode_to_variant(nod).toBool()) { emit b->playStatusChanged(Enums::PlayStatus::Paused); // Utils::SetScreensaver(window()->winId(), true); } else { emit b->playStatusChanged(Enums::PlayStatus::Playing); // Utils::SetScreensaver(window()->winId(), false); } } else if (strcmp(prop->name, "track-list") == 0) { mpv_node* nod = (mpv_node*)prop->data; emit b->tracksChanged(mpvnode_to_variant(nod).toList()); } else if (strcmp(prop->name, "audio-device-list") == 0) { mpv_node* nod = (mpv_node*)prop->data; emit b->audioDevicesChanged(b->getAudioDevices(mpvnode_to_variant(nod))); } else if (strcmp(prop->name, "playlist") == 0) { mpv_node* nod = (mpv_node*)prop->data; emit b->playlistChanged(mpvnode_to_variant(nod).toList()); } else if (strcmp(prop->name, "chapter-list") == 0) { mpv_node* nod = (mpv_node*)prop->data; emit b->chaptersChanged(mpvnode_to_variant(nod).toList()); } else if (strcmp(prop->name, "speed") == 0) { double speed = *(double*)prop->data; emit b->speedChanged(speed); } break; } case MPV_EVENT_LOG_MESSAGE: { struct mpv_event_log_message* msg = (struct mpv_event_log_message*)event->data; QString logMsg = "[" + QString(msg->prefix) + "] " + QString(msg->text); QString msgLevel = QString(msg->level); if (msgLevel.startsWith("d") || msgLevel.startsWith("t")) { mpvLogger->info("{}", logMsg.toStdString()); } else if (msgLevel.startsWith("v") || msgLevel.startsWith("i")) { mpvLogger->info("{}", logMsg.toStdString()); } else { mpvLogger->debug("{}", logMsg.toStdString()); } break; } case MPV_EVENT_SHUTDOWN: { qApp->exit(); break; } default: { break; } } } QVariantMap getAudioDevices(const QVariant& drivers) { QVariantMap newDrivers; if (drivers.isNull()) { return newDrivers; } QSequentialIterable iterable = drivers.value<QSequentialIterable>(); foreach (const QVariant& v, iterable) { QVariantMap item = v.toMap(); newDrivers[item["description"].toString()] = item; } return newDrivers; } }