Skip to content
Snippets Groups Projects
player.cpp 18.6 KiB
Newer Older
/*
 * PeTrack - Software for tracking pedestrians movement in videos
 * Copyright (C) 2023 Forschungszentrum Jülich GmbH, IAS-7
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "player.h"

#include "animation.h"
#include "control.h"
#include "logger.h"
#include "pMessageBox.h"
d.kilic's avatar
d.kilic committed
#include <QApplication>
#include <QIntValidator>
#include <QLabel>
#include <QLineEdit>
d.kilic's avatar
d.kilic committed
#include <QPixmap>
#include <QSlider>
#include <QStyle>
#include <QToolButton>
#include <QVBoxLayout>
d.kilic's avatar
d.kilic committed

Player::Player(Animation *anim, QWidget *parent) : QWidget(parent)
{
    int   size = style()->pixelMetric(QStyle::PM_ToolBarIconSize);
d.kilic's avatar
d.kilic committed
    QSize iconSize(size, size);

    // play forward Button
d.kilic's avatar
d.kilic committed
    mPlayForwardButton = new QToolButton;
    mPlayForwardButton->setIcon(QPixmap(":/playF"));
    mPlayForwardButton->setIconSize(iconSize);
    connect(mPlayForwardButton, &QToolButton::clicked, this, [&]() { this->play(PlayerState::FORWARD); });
d.kilic's avatar
d.kilic committed

    // play backward Button
d.kilic's avatar
d.kilic committed
    mPlayBackwardButton = new QToolButton;
    mPlayBackwardButton->setIcon(QPixmap(":/playB"));
    mPlayBackwardButton->setIconSize(iconSize);
    connect(mPlayBackwardButton, &QToolButton::clicked, this, [&]() { this->play(PlayerState::BACKWARD); });
d.kilic's avatar
d.kilic committed

    // frame forward Button
d.kilic's avatar
d.kilic committed
    mFrameForwardButton = new QToolButton;
    mFrameForwardButton->setAutoRepeat(true);
    mFrameForwardButton->setAutoRepeatDelay(400);                    // before repetition starts
    mFrameForwardButton->setAutoRepeatInterval(1000. / DEFAULT_FPS); // war: 40 // for 1000 ms / 25 fps
d.kilic's avatar
d.kilic committed
    mFrameForwardButton->setIcon(QPixmap(":/skipF"));
    mFrameForwardButton->setIconSize(iconSize);
    connect(mFrameForwardButton, SIGNAL(clicked()), this, SLOT(frameForward()));
d.kilic's avatar
d.kilic committed

    // frame backward Button
    mFrameBackwardButton = new QToolButton;
d.kilic's avatar
d.kilic committed
    mFrameBackwardButton->setAutoRepeat(true);
    mFrameBackwardButton->setAutoRepeatDelay(400);                    // before repetition starts
    mFrameBackwardButton->setAutoRepeatInterval(1000. / DEFAULT_FPS); // war: 40 // for 1000 ms / 25 fps
d.kilic's avatar
d.kilic committed
    mFrameBackwardButton->setIcon(QPixmap(":/skipB"));
    mFrameBackwardButton->setIconSize(iconSize);
    connect(mFrameBackwardButton, SIGNAL(clicked()), this, SLOT(frameBackward()));
d.kilic's avatar
d.kilic committed

    // pause button;
d.kilic's avatar
d.kilic committed
    mPauseButton = new QToolButton;
    mPauseButton->setIcon(QPixmap(":/pause"));
    mPauseButton->setIconSize(iconSize);
    connect(mPauseButton, SIGNAL(clicked()), this, SLOT(pause()));
d.kilic's avatar
d.kilic committed

    // rec button;
d.kilic's avatar
d.kilic committed
    mRecButton = new QToolButton;
    mRecButton->setIcon(QPixmap(":/record"));
    mRecButton->setIconSize(iconSize);
    connect(mRecButton, SIGNAL(clicked()), this, SLOT(recStream()));
d.kilic's avatar
d.kilic committed
    mRec = false;

d.kilic's avatar
d.kilic committed
    mSlider = new QSlider(Qt::Horizontal);
    mSlider->setTickPosition(QSlider::TicksAbove);
    mSlider->setMinimumWidth(100);
    connect(mSlider, SIGNAL(valueChanged(int)), this, SLOT(skipToFrame(int)));
d.kilic's avatar
d.kilic committed

    // frame number
    QFont f("Courier", 12, QFont::Bold); // Times Helvetica, Normal
    mFrameNumValidator    = new QIntValidator(0, 999999, this);
    mFrameInNumValidator  = new QIntValidator(0, 999999, this);
d.kilic's avatar
d.kilic committed
    mFrameOutNumValidator = new QIntValidator(0, 999999, this);

    mFrameInNum = new QLineEdit("");
    mFrameInNum->setMaxLength(6);
    mFrameInNum->setMaximumWidth(75);
    mFrameInNum->setAlignment(Qt::AlignRight);
    mFrameInNum->setValidator(mFrameInNumValidator);
    mFrameInNum->setFont(f);
    connect(mFrameInNum, SIGNAL(editingFinished()), this, SLOT(update()));
d.kilic's avatar
d.kilic committed

    mFrameOutNum = new QLineEdit("");
    mFrameOutNum->setMaxLength(8);
    mFrameOutNum->setMaximumWidth(75);
    mFrameOutNum->setAlignment(Qt::AlignRight);
    mFrameOutNum->setValidator(mFrameOutNumValidator);
    mFrameOutNum->setFont(f);
    connect(mFrameOutNum, SIGNAL(editingFinished()), this, SLOT(update()));
d.kilic's avatar
d.kilic committed

    mFrameNum = new QLineEdit("0");
    mFrameNum->setMaxLength(8);     // bedeutet maxminal 1,1 stunden
    mFrameNum->setMaximumWidth(75); // 5*sz.width() //62
d.kilic's avatar
d.kilic committed
    mFrameNum->setAlignment(Qt::AlignRight);
    mFrameNum->setValidator(mFrameNumValidator);
    mFrameNum->setFont(f);
    connect(mFrameNum, SIGNAL(editingFinished()), this, SLOT(skipToFrame()));
d.kilic's avatar
d.kilic committed

    // frame number
d.kilic's avatar
d.kilic committed
    mFpsNum = new QLineEdit(QString::number(DEFAULT_FPS));
    mFpsNum->setMaxLength(8);     // bedeutet maxminal 999,99
    mFpsNum->setMaximumWidth(62); // 5*sz.width()
d.kilic's avatar
d.kilic committed
    mFpsNum->setAlignment(Qt::AlignRight);
    mFpsNumValidator = new QDoubleValidator(0.0, 999.99, 2, this);
    mFpsNum->setValidator(mFpsNumValidator);
    mFpsNum->setFont(f);
    connect(mFpsNum, SIGNAL(editingFinished()), this, SLOT(setFPS()));
d.kilic's avatar
d.kilic committed

    QFont f2("Courier", 12, QFont::Normal); // Times Helvetica, Normal
d.kilic's avatar
d.kilic committed
    mAtLabel = new QLabel("@");
    mAtLabel->setFont(f2);

    mSourceInLabel = new QLabel("In:");
    mSourceInLabel->setFont(f2);

    mSourceOutLabel = new QLabel("Out:");
    mSourceOutLabel->setFont(f2);

    mFpsLabel = new QLabel("fps");
    mFpsLabel->setFont(f2);
    // default value
    mPlayerSpeedLimited = false;
d.kilic's avatar
d.kilic committed

    // player layout
d.kilic's avatar
d.kilic committed
    mPlayerLayout = new QHBoxLayout();
    mPlayerLayout->addWidget(mPlayBackwardButton);
    mPlayerLayout->addWidget(mFrameBackwardButton);
    mPlayerLayout->addWidget(mPauseButton);
    mPlayerLayout->addWidget(mFrameForwardButton);
    mPlayerLayout->addWidget(mPlayForwardButton);
    mPlayerLayout->addWidget(mRecButton);
    mPlayerLayout->addWidget(mSourceInLabel);
    mPlayerLayout->addWidget(mFrameInNum);
    mPlayerLayout->addWidget(mSourceOutLabel);
    mPlayerLayout->addWidget(mFrameOutNum);
    mPlayerLayout->addWidget(mSlider);
    mPlayerLayout->addWidget(mFrameNum);
    mPlayerLayout->addWidget(mAtLabel);
    mPlayerLayout->addWidget(mFpsNum);
    mPlayerLayout->addWidget(mFpsLabel);
    mPlayerLayout->setMargin(0);
    mMainWindow = (class Petrack *) parent;
d.kilic's avatar
d.kilic committed

    setLayout(mPlayerLayout);

    setAnim(anim);
}

void Player::setFPS(double fps) // default: double fps=-1.
{
d.kilic's avatar
d.kilic committed
    {
        mFpsNum->setText(QString::number(fps));
        mFrameForwardButton->setAutoRepeatInterval(1000. / fps);  // for 1000 ms / 25 fps
        mFrameBackwardButton->setAutoRepeatInterval(1000. / fps); // for 1000 ms / 25 fps
d.kilic's avatar
d.kilic committed
    }
    mAnimation->setFPS(mFpsNum->text().toDouble());
}
void Player::setPlayerSpeedLimited(bool fixed)
d.kilic's avatar
d.kilic committed
{
    mPlayerSpeedLimited = fixed;
d.kilic's avatar
d.kilic committed
}

void Player::togglePlayerSpeedLimited()
d.kilic's avatar
d.kilic committed
{
    setPlayerSpeedLimited(!mPlayerSpeedLimited);
d.kilic's avatar
d.kilic committed
}
bool Player::getPlayerSpeedLimited() const
d.kilic's avatar
d.kilic committed
{
    return mPlayerSpeedLimited;
d.kilic's avatar
d.kilic committed
}

void Player::setPlayerSpeedFixed(bool fixed)
{
    mPlayerSpeedFixed = fixed;
}

d.kilic's avatar
d.kilic committed
void Player::setLooping(bool looping)
{
    mLooping = looping;
}

void Player::setSpeedRelativeToRealtime(double factor)
    setFPS(mAnimation->getOriginalFPS() * factor);
d.kilic's avatar
d.kilic committed
void Player::setAnim(Animation *anim)
{
d.kilic's avatar
d.kilic committed
    {
        pause();
        mAnimation = anim;
        int max    = anim->getNumFrames() > 1 ? anim->getNumFrames() - 1 : 0;
d.kilic's avatar
d.kilic committed
        setSliderMax(max);
        mFrameNumValidator->setTop(max);
        mFrameInNumValidator->setTop(anim->getSourceOutFrameNum());
        mFrameOutNumValidator->setTop(anim->getMaxFrames());
    }
}

bool Player::getPaused()
{
    return mState == PlayerState::PAUSE;
d.kilic's avatar
d.kilic committed
}

void Player::setSliderMax(int max)
{
    mSlider->setMaximum(max);
}

/**
 * @brief Processes and displays the image mImg (set in forward() or backward())
 *
 * Heavy lifting is in Petrack::updateImage(). This method itself handles
 * recording and updating the value of the video-slider.
 *
 * @return Boolean indicating if an frame was processed and displayed
 */
d.kilic's avatar
d.kilic committed
bool Player::updateImage()
{
    if(mImg.empty())
d.kilic's avatar
d.kilic committed
    {
        pause();
        return false;
    }
    qApp->processEvents();
d.kilic's avatar
d.kilic committed
    {
        mAviFile.appendFrame((const unsigned char *) mImg.data, true);
d.kilic's avatar
d.kilic committed
    }
    mSlider->setValue(
        mAnimation->getCurrentFrameNum()); //(1000*mAnimation->getCurrentFrameNum())/mAnimation->getNumFrames());
d.kilic's avatar
d.kilic committed
    mFrameNum->setText(QString().number(mAnimation->getCurrentFrameNum()));

    return true;
}

bool Player::forward()
{
    qApp->processEvents();

    bool should_be_last_frame = mAnimation->getCurrentFrameNum() == mAnimation->getSourceOutFrameNum();

    mImg = mAnimation->getNextFrame();
d.kilic's avatar
d.kilic committed
    // check if animation is broken somewhere in the video
    if(mImg.empty())
d.kilic's avatar
d.kilic committed
    {
        if(!should_be_last_frame)
d.kilic's avatar
d.kilic committed
        {
            SPDLOG_WARN("video unexpected finished.");
d.kilic's avatar
d.kilic committed
        }
    }
d.kilic's avatar
d.kilic committed
    return updateImage();
}

bool Player::backward()
{
    qApp->processEvents();
    mImg = mAnimation->getPreviousFrame();
d.kilic's avatar
d.kilic committed
    return updateImage();
}

/**
 * @brief Sets the state of the video player
 *
 * @see PlayerState
 * @param state
 */
void Player::play(PlayerState state)
{
    if(mState == PlayerState::PAUSE)
    {
        mState = state;
        playVideo();
        mState = state;
    }
}

/**
 * @brief Quasi MainLoop: Plays video in accordance to set frame rate
 *
 * This method is (indirectly) initiating calls to Player::updateImage
 * and thus controls processing and display of video frames. The user has
 * the option to limit playback speed, which is enforced here as well.
 *
 * The method is left, when the video is paused and reentered, when playing
 * gets started again.
 */
void Player::playVideo()
{
    static QElapsedTimer timer;
    int                  currentFrame = mAnimation->getCurrentFrameNum();
    long long int        overtime     = 0;
    while(mState != PlayerState::PAUSE)
    {
        // slow down the player speed for extrem fast video sequences (Jiayue China or 16fps cam99 basigo grid video)
        if(mPlayerSpeedLimited || mPlayerSpeedFixed)
            auto supposedDiff = static_cast<long long int>(1'000 / mAnimation->getFPS());
            if(timer.isValid())
            {
                if(mPlayerSpeedFixed && mState == PlayerState::FORWARD)
                {
                    overtime = std::max(0LL, overtime + (timer.elapsed() - supposedDiff));
                    if(overtime >= supposedDiff)
                    {
                        mAnimation->skipFrame(static_cast<int>(overtime / supposedDiff));
                        overtime = overtime % supposedDiff;
                        currentFrame =
                            std::min(mAnimation->getCurrentFrameNum() + 1, mAnimation->getSourceOutFrameNum());
                while(!timer.hasExpired(supposedDiff))
                {
                    qApp->processEvents();
                }
            }
            timer.start();
            timer.invalidate();
        switch(mState)
        {
            case PlayerState::FORWARD:
                mImg = mAnimation->getFrameAtIndex(currentFrame);
                currentFrame++;
                break;
            case PlayerState::BACKWARD:
                mImg = mAnimation->getFrameAtIndex(currentFrame);
                currentFrame--;
                break;
            case PlayerState::PAUSE:
                break;
        if(!updateImage())
        {
            mState = PlayerState::PAUSE;
            if(mAnimation->getCurrentFrameNum() != 0 &&
               mAnimation->getCurrentFrameNum() != mAnimation->getSourceOutFrameNum())
            {
                SPDLOG_WARN("video unexpectedly finished.");
            if(mLooping && mMainWindow->getControlWidget()->isOnlineTrackingChecked())
                PWarning(
                    this,
                    "Error: No tracking while looping",
                    "Looping and tracking are incompatible. Please disable one first.");
d.kilic's avatar
d.kilic committed
                mState = PlayerState::PAUSE;
                break;
            }
            else if(mLooping)
                if(mState == PlayerState::FORWARD &&
                   mAnimation->getCurrentFrameNum() == mAnimation->getSourceOutFrameNum())
                    currentFrame = mAnimation->getSourceInFrameNum();
                else if(
                    mState == PlayerState::BACKWARD &&
                    mAnimation->getCurrentFrameNum() == mAnimation->getSourceInFrameNum())
d.kilic's avatar
d.kilic committed
                {
                    currentFrame = mAnimation->getSourceOutFrameNum();
                }

    timer.invalidate();
d.kilic's avatar
d.kilic committed
bool Player::frameForward()
{
    pause();
    return forward();
}
bool Player::frameBackward()
{
    pause();
    return backward();
}

void Player::pause()
{
    mState = PlayerState::PAUSE;
d.kilic's avatar
d.kilic committed
    mMainWindow->setShowFPS(0.);
}

/**
 * @brief Toggles pause/play for use via spacebar
 */
d.kilic's avatar
d.kilic committed
void Player::togglePlayPause()
{
    static PlayerState lastState;
d.kilic's avatar
d.kilic committed

    if(mState != PlayerState::PAUSE)
d.kilic's avatar
d.kilic committed
    {
        lastState = mState;
d.kilic's avatar
d.kilic committed
        pause();
    }
    else
    {
        play(lastState);
    }
d.kilic's avatar
d.kilic committed
}

/**
 * @brief Toggles recording and saving of recording
 *
 * If already recording, the method stops the recording and saves it to
 * a user-given file. If recording hasn't started, this method starts it.
 *
 * Actual recording happens in Player::updateImage()
 */
d.kilic's avatar
d.kilic committed
void Player::recStream()
{
    if(mAnimation->isCameraLiveStream() || mAnimation->isVideo() || mAnimation->isImageSequence() ||
       mAnimation->isStereoVideo())
d.kilic's avatar
d.kilic committed
    {
        // video temp path/file name
        QString videoTmp = QDir::tempPath() + "/petrack-video-record.avi";
d.kilic's avatar
d.kilic committed

        if(mRec) // stop recording and save recorded stream to disk
d.kilic's avatar
d.kilic committed
        {
            mRec = false;
            mRecButton->setIcon(QPixmap(":/record"));

            mAviFile.close();

            QString dest;

            QFileDialog fileDialog(
                this,
                tr("Select file for saving video output"),
                nullptr,
                tr("Video (*.*);;AVI-File (*.avi);;All supported types (*.avi *.mp4);;All files (*.*)"));
d.kilic's avatar
d.kilic committed
            fileDialog.setAcceptMode(QFileDialog::AcceptSave);
            fileDialog.setFileMode(QFileDialog::AnyFile);
            fileDialog.setDefaultSuffix("");

            if(fileDialog.exec())
d.kilic's avatar
d.kilic committed
                dest = fileDialog.selectedFiles().at(0);
d.kilic's avatar
d.kilic committed

            if(dest == nullptr)
d.kilic's avatar
d.kilic committed
                return;
d.kilic's avatar
d.kilic committed

            if(QFile::exists(dest))
d.kilic's avatar
d.kilic committed
                QFile::remove(dest);
d.kilic's avatar
d.kilic committed

            QProgressDialog progress("Save Video File", nullptr, 0, 2, mMainWindow);
d.kilic's avatar
d.kilic committed
            progress.setWindowTitle("Save Video File");
            progress.setWindowModality(Qt::WindowModal);
            progress.setVisible(true);
            progress.setValue(0);

            progress.setLabelText(QString("save video ..."));

            qApp->processEvents();
            progress.setValue(1);

            if(!QFile(videoTmp).copy(dest))
d.kilic's avatar
d.kilic committed
            {
                PCritical(this, tr("PeTrack"), tr("Error: Could not save video file!"));
            }
            else
d.kilic's avatar
d.kilic committed
            {
                mMainWindow->statusBar()->showMessage(tr("Saved video file to %1.").arg(dest), 5000);
                if(!QFile(videoTmp).remove())
                    SPDLOG_WARN("Could not remove tmp-file: {}", videoTmp);
d.kilic's avatar
d.kilic committed

                progress.setValue(2);
            }
        }
        else // open video writer
d.kilic's avatar
d.kilic committed
        {
            if(mAviFile.open(
                   videoTmp.toStdString().c_str(), mImg.cols, mImg.rows, 8 * mImg.channels(), mAnimation->getFPS()))
d.kilic's avatar
d.kilic committed
            {
                mRec = true;
                mRecButton->setIcon(QPixmap(":/stop-record"));
d.kilic's avatar
d.kilic committed
            {
                SPDLOG_ERROR("could not open video output file!");
d.kilic's avatar
d.kilic committed
            }
        }
    }
}

bool Player::skipToFrame(int f) // [0..mAnimation->getNumFrames()-1]
{
    if(f == mAnimation->getCurrentFrameNum())
d.kilic's avatar
d.kilic committed
    {
        return false;
    }
    pause();
    mImg = mAnimation->getFrameAtIndex(f);
d.kilic's avatar
d.kilic committed
    return updateImage();
}

bool Player::skipToFrame() // [0..mAnimation->getNumFrames()-1]
{
    if(mFrameNum->text().toInt() < getFrameInNum())
        mFrameNum->setText(QString::number(getFrameInNum()));
    if(mFrameNum->text().toInt() > getFrameOutNum())
        mFrameNum->setText(QString::number(getFrameOutNum()));
d.kilic's avatar
d.kilic committed

    return skipToFrame(mFrameNum->text().toInt());
}

/**
 * @brief Properly updates FrameInNum and FrameOutNum
 */
d.kilic's avatar
d.kilic committed
void Player::update()
{
    if constexpr(true || !mMainWindow->isLoading())
d.kilic's avatar
d.kilic committed
    {
        if(mFrameNum->text().toInt() < mFrameInNum->text().toInt())
d.kilic's avatar
d.kilic committed
        {
            mFrameNum->setText(mFrameInNum->text());
            skipToFrame(mFrameNum->text().toInt());
        }
        if(mFrameNum->text().toInt() > mFrameOutNum->text().toInt())
d.kilic's avatar
d.kilic committed
        {
            mFrameNum->setText(mFrameOutNum->text());
            skipToFrame(mFrameNum->text().toInt());
        }
        mSlider->setMinimum(getFrameInNum());
        mSlider->setMaximum(getFrameOutNum());

        mFrameInNumValidator->setTop(getFrameOutNum() - 1);
d.kilic's avatar
d.kilic committed
        mFrameNumValidator->setBottom(getFrameInNum());
        mFrameNumValidator->setTop(getFrameOutNum());

        mAnimation->updateSourceInFrameNum(mFrameInNum->text().toInt());
        mAnimation->updateSourceOutFrameNum(mFrameOutNum->text().toInt());

        mMainWindow->updateWindowTitle();
    }
}

int Player::getFrameInNum()
{
    if(mFrameInNum->text() == "")
d.kilic's avatar
d.kilic committed
        return -1;
d.kilic's avatar
d.kilic committed
    return mFrameInNum->text().toInt();
}
void Player::setFrameInNum(int in)
{
d.kilic's avatar
d.kilic committed
        in = 0;
d.kilic's avatar
d.kilic committed

    mFrameInNum->setText(QString::number(in));

    mFrameInNumValidator->setTop(getFrameOutNum() - 1);
d.kilic's avatar
d.kilic committed
    mFrameNumValidator->setBottom(getFrameInNum());
    mFrameNumValidator->setTop(getFrameOutNum());
}
int Player::getFrameOutNum()
{
    if(mFrameOutNum->text() == "")
d.kilic's avatar
d.kilic committed
        return -1;
d.kilic's avatar
d.kilic committed

    return mFrameOutNum->text().toInt();
}
void Player::setFrameOutNum(int out)
{
        out = mAnimation->getMaxFrames() - 1;
d.kilic's avatar
d.kilic committed

    mFrameOutNum->setText(QString::number(out));

    mFrameInNumValidator->setTop(/*out*/ getFrameOutNum() - 1);
d.kilic's avatar
d.kilic committed
    mFrameNumValidator->setBottom(getFrameInNum());
    mFrameNumValidator->setTop(/*out*/ getFrameOutNum());
d.kilic's avatar
d.kilic committed
}

int Player::getPos()
{
    return mAnimation->getCurrentFrameNum();
}

#include "moc_player.cpp"