mirror of
https://github.com/wang-bin/QtAV.git
synced 2026-10-18 13:11:13 +08:00
e898e6af15
most algorithms and containers are fine. no std::string, exception
1542 lines
51 KiB
C++
1542 lines
51 KiB
C++
/******************************************************************************
|
|
QtAV Player Demo: this file is part of QtAV examples
|
|
Copyright (C) 2012-2015 Wang Bin <wbsecg1@gmail.com>
|
|
|
|
* This file is part of QtAV
|
|
|
|
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 <http://www.gnu.org/licenses/>.
|
|
******************************************************************************/
|
|
#include "MainWindow.h"
|
|
#include "EventFilter.h"
|
|
#include <QtAV>
|
|
#include <QtAVWidgets>
|
|
#include <QtCore/QtDebug>
|
|
#include <QtCore/QLocale>
|
|
#include <QtCore/QTimer>
|
|
#include <QTimeEdit>
|
|
#include <QLabel>
|
|
#include <QApplication>
|
|
#include <QDesktopWidget>
|
|
#include <QDesktopServices>
|
|
#include <QtCore/QFileInfo>
|
|
#include <QtCore/QTextCodec>
|
|
#include <QtCore/QTextStream>
|
|
#include <QtCore/QUrl>
|
|
#include <QGraphicsOpacityEffect>
|
|
#include <QComboBox>
|
|
#include <QResizeEvent>
|
|
#include <QWidgetAction>
|
|
#include <QLayout>
|
|
#include <QPushButton>
|
|
#include <QDoubleSpinBox>
|
|
#include <QFileDialog>
|
|
#include <QInputDialog>
|
|
#include <QMenu>
|
|
#include <QMessageBox>
|
|
#include <QToolButton>
|
|
#include <QToolTip>
|
|
#include <QKeyEvent>
|
|
#include <QWheelEvent>
|
|
#include "ClickableMenu.h"
|
|
#include "Slider.h"
|
|
#include "StatisticsView.h"
|
|
#include "TVView.h"
|
|
#include "config/DecoderConfigPage.h"
|
|
#include "config/VideoEQConfigPage.h"
|
|
#include "config/ConfigDialog.h"
|
|
#include "filters/OSDFilter.h"
|
|
//#include "filters/AVFilterSubtitle.h"
|
|
#include "playlist/PlayList.h"
|
|
#include "../common/common.h"
|
|
|
|
/*
|
|
*TODO:
|
|
* disable a/v actions if player is 0;
|
|
* use action's value to set player's parameters when start to play a new file
|
|
*/
|
|
|
|
#define AVDEBUG() \
|
|
qDebug("%s %s @%d", __FILE__, __FUNCTION__, __LINE__);
|
|
|
|
using namespace QtAV;
|
|
const qreal kVolumeInterval = 0.04;
|
|
|
|
extern QStringList idsToNames(QVector<VideoDecoderId> ids);
|
|
extern QVector<VideoDecoderId> idsFromNames(const QStringList& names);
|
|
|
|
void QLabelSetElideText(QLabel *label, QString text, int W = 0)
|
|
{
|
|
QFontMetrics metrix(label->font());
|
|
int width = label->width() - label->indent() - label->margin();
|
|
if (W || label->parent()) {
|
|
int w = W;
|
|
if (!w)
|
|
w = ((QWidget*)label->parent())->width();
|
|
width = qMax(w - label->indent() - label->margin() - 13*(30), 0); //TODO: why 30?
|
|
}
|
|
QString clippedText = metrix.elidedText(text, Qt::ElideRight, width);
|
|
label->setText(clippedText);
|
|
}
|
|
|
|
MainWindow::MainWindow(QWidget *parent) :
|
|
QWidget(parent)
|
|
, mIsReady(false)
|
|
, mHasPendingPlay(false)
|
|
, mControlOn(false)
|
|
, mShowControl(2)
|
|
, mRepeateMax(0)
|
|
, mpPlayer(0)
|
|
, mpRenderer(0)
|
|
, mpTempRenderer(0)
|
|
, mpVideoFilter(0)
|
|
, mpAudioFilter(0)
|
|
, mpStatisticsView(0)
|
|
, mpOSD(0)
|
|
, mpSubtitle(0)
|
|
, m_preview(0)
|
|
{
|
|
setWindowIcon(QIcon(QString::fromLatin1(":/QtAV.svg")));
|
|
mpOSD = new OSDFilterQPainter(this);
|
|
mpSubtitle = new SubtitleFilter(this);
|
|
mpChannelAction = 0;
|
|
mpChannelMenu = 0;
|
|
mpAudioTrackAction = 0;
|
|
setMouseTracking(true); //mouseMoveEvent without press.
|
|
connect(this, SIGNAL(ready()), SLOT(processPendingActions()));
|
|
//QTimer::singleShot(10, this, SLOT(setupUi()));
|
|
setupUi();
|
|
//setToolTip(tr("Click black area to use shortcut(see right click menu)"));
|
|
WindowEventFilter *we = new WindowEventFilter(this);
|
|
installEventFilter(we);
|
|
connect(we, SIGNAL(fullscreenChanged()), SLOT(handleFullscreenChange()));
|
|
}
|
|
|
|
MainWindow::~MainWindow()
|
|
{
|
|
if (m_preview) {
|
|
m_preview->close();
|
|
delete m_preview;
|
|
}
|
|
mpHistory->save();
|
|
mpPlayList->save();
|
|
if (mpVolumeSlider && !mpVolumeSlider->parentWidget()) {
|
|
mpVolumeSlider->close();
|
|
delete mpVolumeSlider;
|
|
mpVolumeSlider = 0;
|
|
}
|
|
if (mpStatisticsView) {
|
|
delete mpStatisticsView;
|
|
mpStatisticsView = 0;
|
|
}
|
|
}
|
|
|
|
void MainWindow::initPlayer()
|
|
{
|
|
mpPlayer = new AVPlayer(this);
|
|
mIsReady = true;
|
|
//mpSubtitle->installTo(mpPlayer); //filter on frame
|
|
mpSubtitle->setPlayer(mpPlayer);
|
|
//mpPlayer->setAudioOutput(AudioOutputFactory::create(AudioOutputId_OpenAL));
|
|
EventFilter *ef = new EventFilter(mpPlayer);
|
|
qApp->installEventFilter(ef);
|
|
connect(ef, SIGNAL(helpRequested()), SLOT(help()));
|
|
connect(ef, SIGNAL(showNextOSD()), SLOT(showNextOSD()));
|
|
onCaptureConfigChanged();
|
|
onAVFilterVideoConfigChanged();
|
|
onAVFilterAudioConfigChanged();
|
|
connect(&Config::instance(), SIGNAL(captureDirChanged(QString)), SLOT(onCaptureConfigChanged()));
|
|
connect(&Config::instance(), SIGNAL(captureFormatChanged(QString)), SLOT(onCaptureConfigChanged()));
|
|
connect(&Config::instance(), SIGNAL(captureQualityChanged(int)), SLOT(onCaptureConfigChanged()));
|
|
connect(&Config::instance(), SIGNAL(avfilterVideoChanged()), SLOT(onAVFilterVideoConfigChanged()));
|
|
connect(&Config::instance(), SIGNAL(avfilterAudioChanged()), SLOT(onAVFilterAudioConfigChanged()));
|
|
connect(&Config::instance(), SIGNAL(bufferValueChanged()), SLOT(onBufferValueChanged()));
|
|
connect(&Config::instance(), SIGNAL(abortOnTimeoutChanged()), SLOT(onAbortOnTimeoutChanged()));
|
|
connect(mpStopBtn, SIGNAL(clicked()), this, SLOT(stopUnload()));
|
|
connect(mpForwardBtn, SIGNAL(clicked()), mpPlayer, SLOT(seekForward()));
|
|
connect(mpBackwardBtn, SIGNAL(clicked()), mpPlayer, SLOT(seekBackward()));
|
|
connect(mpVolumeBtn, SIGNAL(clicked()), SLOT(showHideVolumeBar()));
|
|
connect(mpVolumeSlider, SIGNAL(sliderPressed()), SLOT(setVolume()));
|
|
connect(mpVolumeSlider, SIGNAL(valueChanged(int)), SLOT(setVolume()));
|
|
|
|
connect(mpPlayer, SIGNAL(seekFinished()), SLOT(onSeekFinished()));
|
|
connect(mpPlayer, SIGNAL(mediaStatusChanged(QtAV::MediaStatus)), SLOT(onMediaStatusChanged()));
|
|
connect(mpPlayer, SIGNAL(bufferProgressChanged(qreal)), SLOT(onBufferProgress(qreal)));
|
|
connect(mpPlayer, SIGNAL(error(QtAV::AVError)), this, SLOT(handleError(QtAV::AVError)));
|
|
connect(mpPlayer, SIGNAL(started()), this, SLOT(onStartPlay()));
|
|
connect(mpPlayer, SIGNAL(stopped()), this, SLOT(onStopPlay()));
|
|
connect(mpPlayer, SIGNAL(paused(bool)), this, SLOT(onPaused(bool)));
|
|
connect(mpPlayer, SIGNAL(speedChanged(qreal)), this, SLOT(onSpeedChange(qreal)));
|
|
connect(mpPlayer, SIGNAL(positionChanged(qint64)), this, SLOT(onPositionChange(qint64)));
|
|
//connect(mpPlayer, SIGNAL(volumeChanged(qreal)), SLOT(syncVolumeUi(qreal)));
|
|
connect(mpVideoEQ, SIGNAL(brightnessChanged(int)), this, SLOT(onBrightnessChanged(int)));
|
|
connect(mpVideoEQ, SIGNAL(contrastChanged(int)), this, SLOT(onContrastChanged(int)));
|
|
connect(mpVideoEQ, SIGNAL(hueChanegd(int)), this, SLOT(onHueChanged(int)));
|
|
connect(mpVideoEQ, SIGNAL(saturationChanged(int)), this, SLOT(onSaturationChanged(int)));
|
|
|
|
connect(mpCaptureBtn, SIGNAL(clicked()), mpPlayer->videoCapture(), SLOT(request()));
|
|
|
|
emit ready(); //emit this signal after connection. otherwise the slots may not be called for the first time
|
|
}
|
|
|
|
void MainWindow::onSeekFinished()
|
|
{
|
|
qDebug("seek finished at %lld", mpPlayer->position());
|
|
}
|
|
|
|
void MainWindow::stopUnload()
|
|
{
|
|
mpPlayer->stop();
|
|
mpPlayer->unload();
|
|
}
|
|
|
|
void MainWindow::setupUi()
|
|
{
|
|
QVBoxLayout *mainLayout = new QVBoxLayout();
|
|
mainLayout->setSpacing(0);
|
|
mainLayout->setMargin(0);
|
|
setLayout(mainLayout);
|
|
|
|
mpPlayerLayout = new QVBoxLayout();
|
|
mpControl = new QWidget(this);
|
|
mpControl->setMaximumHeight(30);
|
|
|
|
//mpPreview = new QLable(this);
|
|
|
|
mpTimeSlider = new Slider(mpControl);
|
|
mpTimeSlider->setDisabled(true);
|
|
mpTimeSlider->setTracking(true);
|
|
mpTimeSlider->setOrientation(Qt::Horizontal);
|
|
mpTimeSlider->setMinimum(0);
|
|
mpCurrent = new QLabel(mpControl);
|
|
mpCurrent->setToolTip(tr("Current time"));
|
|
mpCurrent->setMargin(2);
|
|
mpCurrent->setText(QString::fromLatin1("00:00:00"));
|
|
mpEnd = new QLabel(mpControl);
|
|
mpEnd->setToolTip(tr("Duration"));
|
|
mpEnd->setMargin(2);
|
|
mpEnd->setText(QString::fromLatin1("00:00:00"));
|
|
mpTitle = new QLabel(mpControl);
|
|
mpTitle->setToolTip(tr("Render engine"));
|
|
mpTitle->setText(QString::fromLatin1("QPainter"));
|
|
mpTitle->setIndent(8);
|
|
mpSpeed = new QLabel(QString::fromLatin1("1.00"));
|
|
mpSpeed->setMargin(1);
|
|
mpSpeed->setToolTip(tr("Speed. Ctrl+Up/Down"));
|
|
|
|
mpPlayPauseBtn = new QToolButton(mpControl);
|
|
mpPlayPauseBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/play.svg")));
|
|
mpStopBtn = new QToolButton(mpControl);
|
|
mpStopBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/stop.svg")));
|
|
mpBackwardBtn = new QToolButton(mpControl);
|
|
mpBackwardBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/backward.svg")));
|
|
mpForwardBtn = new QToolButton(mpControl);
|
|
mpForwardBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/forward.svg")));
|
|
mpOpenBtn = new QToolButton(mpControl);
|
|
mpOpenBtn->setToolTip(tr("Open"));
|
|
mpOpenBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/open.svg")));
|
|
|
|
mpInfoBtn = new QToolButton();
|
|
mpInfoBtn->setToolTip(QString::fromLatin1("Media information"));
|
|
mpInfoBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/info.svg")));
|
|
mpCaptureBtn = new QToolButton();
|
|
mpCaptureBtn->setToolTip(tr("Capture"));
|
|
mpCaptureBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/capture.svg")));
|
|
mpVolumeBtn = new QToolButton();
|
|
mpVolumeBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/sound.svg")));
|
|
|
|
mpVolumeSlider = new Slider();
|
|
mpVolumeSlider->hide();
|
|
mpVolumeSlider->setOrientation(Qt::Horizontal);
|
|
mpVolumeSlider->setMinimum(0);
|
|
const int kVolumeSliderMax = 100;
|
|
mpVolumeSlider->setMaximum(kVolumeSliderMax);
|
|
//mpVolumeSlider->setMaximumHeight(12);
|
|
mpVolumeSlider->setMaximumWidth(88);
|
|
mpVolumeSlider->setValue(int(1.0/kVolumeInterval*qreal(kVolumeSliderMax)/100.0));
|
|
setVolume();
|
|
|
|
mpMenuBtn = new QToolButton();
|
|
mpMenuBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/menu.svg")));
|
|
mpMenuBtn->setAutoRaise(true);
|
|
mpMenuBtn->setPopupMode(QToolButton::InstantPopup);
|
|
|
|
QMenu *subMenu = 0;
|
|
QWidgetAction *pWA = 0;
|
|
mpMenu = new QMenu(mpMenuBtn);
|
|
mpMenu->addAction(tr("Open Url"), this, SLOT(openUrl()));
|
|
//mpMenu->addAction(tr("Online channels"), this, SLOT(onTVMenuClick()));
|
|
mpMenu->addSeparator();
|
|
|
|
subMenu = new QMenu(tr("Play list"));
|
|
mpMenu->addMenu(subMenu);
|
|
mpPlayList = new PlayList(this);
|
|
mpPlayList->setSaveFile(Config::instance().defaultDir() + QString::fromLatin1("/playlist.qds"));
|
|
mpPlayList->load();
|
|
connect(mpPlayList, SIGNAL(aboutToPlay(QString)), SLOT(play(QString)));
|
|
pWA = new QWidgetAction(0);
|
|
pWA->setDefaultWidget(mpPlayList);
|
|
subMenu->addAction(pWA); //must add action after the widget action is ready. is it a Qt bug?
|
|
|
|
subMenu = new QMenu(tr("History"));
|
|
mpMenu->addMenu(subMenu);
|
|
mpHistory = new PlayList(this);
|
|
mpHistory->setMaxRows(20);
|
|
mpHistory->setSaveFile(Config::instance().defaultDir() + QString::fromLatin1("/history.qds"));
|
|
mpHistory->load();
|
|
connect(mpHistory, SIGNAL(aboutToPlay(QString)), SLOT(play(QString)));
|
|
pWA = new QWidgetAction(0);
|
|
pWA->setDefaultWidget(mpHistory);
|
|
subMenu->addAction(pWA); //must add action after the widget action is ready. is it a Qt bug?
|
|
|
|
mpMenu->addSeparator();
|
|
|
|
//mpMenu->addAction(tr("Report"))->setEnabled(false); //report bug, suggestions etc. using maillist?
|
|
mpMenu->addAction(tr("About"), this, SLOT(about()));
|
|
mpMenu->addAction(tr("Help"), this, SLOT(help()));
|
|
mpMenu->addAction(tr("Donate"), this, SLOT(donate()));
|
|
mpMenu->addAction(tr("Setup"), this, SLOT(setup()));
|
|
mpMenu->addSeparator();
|
|
mpMenuBtn->setMenu(mpMenu);
|
|
mpMenu->addSeparator();
|
|
|
|
subMenu = new QMenu(tr("Speed"));
|
|
mpMenu->addMenu(subMenu);
|
|
QDoubleSpinBox *pSpeedBox = new QDoubleSpinBox(0);
|
|
pSpeedBox->setRange(0.01, 20);
|
|
pSpeedBox->setValue(1.0);
|
|
pSpeedBox->setSingleStep(0.01);
|
|
pSpeedBox->setCorrectionMode(QAbstractSpinBox::CorrectToPreviousValue);
|
|
pWA = new QWidgetAction(0);
|
|
pWA->setDefaultWidget(pSpeedBox);
|
|
subMenu->addAction(pWA); //must add action after the widget action is ready. is it a Qt bug?
|
|
|
|
subMenu = new ClickableMenu(tr("Repeat"));
|
|
mpMenu->addMenu(subMenu);
|
|
//subMenu->setEnabled(false);
|
|
mpRepeatEnableAction = subMenu->addAction(tr("Enable"));
|
|
mpRepeatEnableAction->setCheckable(true);
|
|
connect(mpRepeatEnableAction, SIGNAL(toggled(bool)), SLOT(toggleRepeat(bool)));
|
|
// TODO: move to a func or class
|
|
mpRepeatBox = new QSpinBox(0);
|
|
mpRepeatBox->setMinimum(-1);
|
|
mpRepeatBox->setValue(-1);
|
|
mpRepeatBox->setToolTip(QString::fromLatin1("-1: ") + tr("infinity"));
|
|
connect(mpRepeatBox, SIGNAL(valueChanged(int)), SLOT(setRepeateMax(int)));
|
|
QLabel *pRepeatLabel = new QLabel(tr("Times"));
|
|
QHBoxLayout *hb = new QHBoxLayout;
|
|
hb->addWidget(pRepeatLabel);
|
|
hb->addWidget(mpRepeatBox);
|
|
QVBoxLayout *vb = new QVBoxLayout;
|
|
vb->addLayout(hb);
|
|
pRepeatLabel = new QLabel(tr("From"));
|
|
mpRepeatA = new QTimeEdit();
|
|
mpRepeatA->setDisplayFormat(QString::fromLatin1("HH:mm:ss"));
|
|
mpRepeatA->setToolTip(tr("negative value means from the end"));
|
|
connect(mpRepeatA, SIGNAL(timeChanged(QTime)), SLOT(repeatAChanged(QTime)));
|
|
hb = new QHBoxLayout;
|
|
hb->addWidget(pRepeatLabel);
|
|
hb->addWidget(mpRepeatA);
|
|
vb->addLayout(hb);
|
|
pRepeatLabel = new QLabel(tr("To"));
|
|
mpRepeatB = new QTimeEdit();
|
|
mpRepeatB->setDisplayFormat(QString::fromLatin1("HH:mm:ss"));
|
|
mpRepeatB->setToolTip(tr("negative value means from the end"));
|
|
connect(mpRepeatB, SIGNAL(timeChanged(QTime)), SLOT(repeatBChanged(QTime)));
|
|
hb = new QHBoxLayout;
|
|
hb->addWidget(pRepeatLabel);
|
|
hb->addWidget(mpRepeatB);
|
|
vb->addLayout(hb);
|
|
QWidget *wgt = new QWidget;
|
|
wgt->setLayout(vb);
|
|
|
|
pWA = new QWidgetAction(0);
|
|
pWA->setDefaultWidget(wgt);
|
|
pWA->defaultWidget()->setEnabled(false);
|
|
subMenu->addAction(pWA); //must add action after the widget action is ready. is it a Qt bug?
|
|
mpRepeatAction = pWA;
|
|
|
|
mpMenu->addSeparator();
|
|
|
|
subMenu = new ClickableMenu(tr("Clock"));
|
|
mpMenu->addMenu(subMenu);
|
|
QActionGroup *ag = new QActionGroup(subMenu);
|
|
ag->setExclusive(true);
|
|
connect(subMenu, SIGNAL(triggered(QAction*)), SLOT(changeClockType(QAction*)));
|
|
subMenu->addAction(tr("Auto"))->setData(-1);
|
|
subMenu->addAction(tr("Audio"))->setData(AVClock::AudioClock);
|
|
subMenu->addAction(tr("Video"))->setData(AVClock::VideoClock);
|
|
foreach(QAction* action, subMenu->actions()) {
|
|
action->setActionGroup(ag);
|
|
action->setCheckable(true);
|
|
}
|
|
QAction *autoClockAction = subMenu->actions().at(0);
|
|
autoClockAction->setChecked(true);
|
|
autoClockAction->setToolTip(tr("Take effect in next playback"));
|
|
|
|
subMenu = new ClickableMenu(tr("Subtitle"));
|
|
mpMenu->addMenu(subMenu);
|
|
QAction *act = subMenu->addAction(tr("Enable"));
|
|
act->setCheckable(true);
|
|
act->setChecked(mpSubtitle->isEnabled());
|
|
connect(act, SIGNAL(toggled(bool)), SLOT(toggoleSubtitleEnabled(bool)));
|
|
act = subMenu->addAction(tr("Auto load"));
|
|
act->setCheckable(true);
|
|
act->setChecked(mpSubtitle->autoLoad());
|
|
connect(act, SIGNAL(toggled(bool)), SLOT(toggleSubtitleAutoLoad(bool)));
|
|
subMenu->addAction(tr("Open"), this, SLOT(openSubtitle()));
|
|
|
|
wgt = new QWidget();
|
|
hb = new QHBoxLayout();
|
|
wgt->setLayout(hb);
|
|
hb->addWidget(new QLabel(tr("Engine")));
|
|
QComboBox *box = new QComboBox();
|
|
hb->addWidget(box);
|
|
pWA = new QWidgetAction(0);
|
|
pWA->setDefaultWidget(wgt);
|
|
subMenu->addAction(pWA); //must add action after the widget action is ready. is it a Qt bug?
|
|
box->addItem(QString::fromLatin1("FFmpeg"), QString::fromLatin1("FFmpeg"));
|
|
box->addItem(QString::fromLatin1("LibASS"), QString::fromLatin1("LibASS"));
|
|
connect(box, SIGNAL(activated(QString)), SLOT(setSubtitleEngine(QString)));
|
|
mpSubtitle->setEngines(QStringList() << box->itemData(box->currentIndex()).toString());
|
|
box->setToolTip(tr("FFmpeg supports more subtitles but only render plain text") + QString::fromLatin1("\n") + tr("LibASS supports ass styles"));
|
|
|
|
wgt = new QWidget();
|
|
hb = new QHBoxLayout();
|
|
wgt->setLayout(hb);
|
|
hb->addWidget(new QLabel(tr("Charset")));
|
|
box = new QComboBox();
|
|
hb->addWidget(box);
|
|
pWA = new QWidgetAction(0);
|
|
pWA->setDefaultWidget(wgt);
|
|
subMenu->addAction(pWA); //must add action after the widget action is ready. is it a Qt bug?
|
|
box->addItem(tr("Auto detect"), QString::fromLatin1("AutoDetect"));
|
|
box->addItem(tr("System"), QString::fromLatin1("System"));
|
|
foreach (const QByteArray& cs, QTextCodec::availableCodecs()) {
|
|
box->addItem(QString::fromLatin1(cs), QString::fromLatin1(cs));
|
|
}
|
|
connect(box, SIGNAL(activated(QString)), SLOT(setSubtitleCharset(QString)));
|
|
mpSubtitle->setCodec(box->itemData(box->currentIndex()).toByteArray());
|
|
box->setToolTip(tr("Auto detect requires libchardet"));
|
|
|
|
subMenu = new ClickableMenu(tr("Audio track"));
|
|
mpMenu->addMenu(subMenu);
|
|
mpAudioTrackMenu = subMenu;
|
|
connect(subMenu, SIGNAL(triggered(QAction*)), SLOT(changeAudioTrack(QAction*)));
|
|
initAudioTrackMenu();
|
|
|
|
subMenu = new ClickableMenu(tr("Channel"));
|
|
mpMenu->addMenu(subMenu);
|
|
mpChannelMenu = subMenu;
|
|
connect(subMenu, SIGNAL(triggered(QAction*)), SLOT(changeChannel(QAction*)));
|
|
subMenu->addAction(tr("As input"))->setData(AudioFormat::ChannelLayout_Unsupported); //will set to input in resampler if not supported.
|
|
subMenu->addAction(tr("Stero"))->setData(AudioFormat::ChannelLayout_Stero);
|
|
subMenu->addAction(tr("Mono (center)"))->setData(AudioFormat::ChannelLayout_Center);
|
|
subMenu->addAction(tr("Left"))->setData(AudioFormat::ChannelLayout_Left);
|
|
subMenu->addAction(tr("Right"))->setData(AudioFormat::ChannelLayout_Right);
|
|
ag = new QActionGroup(subMenu);
|
|
ag->setExclusive(true);
|
|
foreach(QAction* action, subMenu->actions()) {
|
|
ag->addAction(action);
|
|
action->setCheckable(true);
|
|
}
|
|
|
|
subMenu = new QMenu(tr("Aspect ratio"), mpMenu);
|
|
mpMenu->addMenu(subMenu);
|
|
connect(subMenu, SIGNAL(triggered(QAction*)), SLOT(switchAspectRatio(QAction*)));
|
|
mpARAction = subMenu->addAction(tr("Video"));
|
|
mpARAction->setData(0);
|
|
subMenu->addAction(tr("Window"))->setData(-1);
|
|
subMenu->addAction(QString::fromLatin1("4:3"))->setData(4.0/3.0);
|
|
subMenu->addAction(QString::fromLatin1("16:9"))->setData(16.0/9.0);
|
|
subMenu->addAction(tr("Custom"))->setData(-2);
|
|
foreach(QAction* action, subMenu->actions()) {
|
|
action->setCheckable(true);
|
|
}
|
|
mpARAction->setChecked(true);
|
|
|
|
subMenu = new ClickableMenu(tr("Color space"));
|
|
mpMenu->addMenu(subMenu);
|
|
mpVideoEQ = new VideoEQConfigPage();
|
|
connect(mpVideoEQ, SIGNAL(engineChanged()), SLOT(onVideoEQEngineChanged()));
|
|
pWA = new QWidgetAction(0);
|
|
pWA->setDefaultWidget(mpVideoEQ);
|
|
subMenu->addAction(pWA);
|
|
|
|
subMenu = new ClickableMenu(tr("Decoder"));
|
|
mpMenu->addMenu(subMenu);
|
|
mpDecoderConfigPage = new DecoderConfigPage();
|
|
pWA = new QWidgetAction(0);
|
|
pWA->setDefaultWidget(mpDecoderConfigPage);
|
|
subMenu->addAction(pWA);
|
|
|
|
subMenu = new ClickableMenu(tr("Renderer"));
|
|
mpMenu->addMenu(subMenu);
|
|
connect(subMenu, SIGNAL(triggered(QAction*)), SLOT(changeVO(QAction*)));
|
|
//TODO: AVOutput.name,detail(description). check whether it is available
|
|
mpVOAction = subMenu->addAction(QString::fromLatin1("QPainter"));
|
|
mpVOAction->setData(VideoRendererId_Widget);
|
|
subMenu->addAction(QString::fromLatin1("OpenGLWidget"))->setData(VideoRendererId_OpenGLWidget);
|
|
subMenu->addAction(QString::fromLatin1("GLWidget 2"))->setData(VideoRendererId_GLWidget2);
|
|
subMenu->addAction(QString::fromLatin1("GLWidget"))->setData(VideoRendererId_GLWidget);
|
|
subMenu->addAction(QString::fromLatin1("GDI+"))->setData(VideoRendererId_GDI);
|
|
subMenu->addAction(QString::fromLatin1("Direct2D"))->setData(VideoRendererId_Direct2D);
|
|
subMenu->addAction(QString::fromLatin1("XV"))->setData(VideoRendererId_XV);
|
|
mVOActions = subMenu->actions();
|
|
foreach(QAction* action, subMenu->actions()) {
|
|
action->setCheckable(true);
|
|
}
|
|
mpVOAction->setChecked(true);
|
|
|
|
|
|
mainLayout->addLayout(mpPlayerLayout);
|
|
mainLayout->addWidget(mpTimeSlider);
|
|
mainLayout->addWidget(mpControl);
|
|
|
|
QHBoxLayout *controlLayout = new QHBoxLayout();
|
|
controlLayout->setSpacing(0);
|
|
controlLayout->setMargin(1);
|
|
mpControl->setLayout(controlLayout);
|
|
controlLayout->addWidget(mpCurrent);
|
|
controlLayout->addWidget(mpTitle);
|
|
QSpacerItem *space = new QSpacerItem(mpPlayPauseBtn->width(), mpPlayPauseBtn->height(), QSizePolicy::MinimumExpanding);
|
|
controlLayout->addSpacerItem(space);
|
|
controlLayout->addWidget(mpVolumeSlider);
|
|
controlLayout->addWidget(mpVolumeBtn);
|
|
controlLayout->addWidget(mpCaptureBtn);
|
|
controlLayout->addWidget(mpPlayPauseBtn);
|
|
controlLayout->addWidget(mpStopBtn);
|
|
controlLayout->addWidget(mpBackwardBtn);
|
|
controlLayout->addWidget(mpForwardBtn);
|
|
controlLayout->addWidget(mpOpenBtn);
|
|
controlLayout->addWidget(mpInfoBtn);
|
|
controlLayout->addWidget(mpSpeed);
|
|
//controlLayout->addWidget(mpSetupBtn);
|
|
controlLayout->addWidget(mpMenuBtn);
|
|
controlLayout->addWidget(mpEnd);
|
|
|
|
connect(pSpeedBox, SIGNAL(valueChanged(double)), SLOT(onSpinBoxChanged(double)));
|
|
connect(mpOpenBtn, SIGNAL(clicked()), SLOT(openFile()));
|
|
connect(mpPlayPauseBtn, SIGNAL(clicked()), SLOT(togglePlayPause()));
|
|
connect(mpInfoBtn, SIGNAL(clicked()), SLOT(showInfo()));
|
|
//valueChanged can be triggered by non-mouse event
|
|
//TODO: connect sliderMoved(int) to preview(int)
|
|
connect(mpTimeSlider, SIGNAL(sliderPressed()), SLOT(seek()));
|
|
connect(mpTimeSlider, SIGNAL(sliderReleased()), SLOT(seek()));
|
|
connect(mpTimeSlider, SIGNAL(onLeave()), SLOT(onTimeSliderLeave()));
|
|
connect(mpTimeSlider, SIGNAL(onHover(int,int)), SLOT(onTimeSliderHover(int,int)));
|
|
QTimer::singleShot(0, this, SLOT(initPlayer()));
|
|
}
|
|
|
|
void MainWindow::changeChannel(QAction *action)
|
|
{
|
|
if (action == mpChannelAction) {
|
|
action->toggle();
|
|
return;
|
|
}
|
|
AudioFormat::ChannelLayout cl = (AudioFormat::ChannelLayout)action->data().toInt();
|
|
AudioOutput *ao = mpPlayer ? mpPlayer->audio() : 0; //getAO()?
|
|
if (!ao) {
|
|
qWarning("No audio output!");
|
|
return;
|
|
}
|
|
mpChannelAction->setChecked(false);
|
|
mpChannelAction = action;
|
|
mpChannelAction->setChecked(true);
|
|
if (!ao->close()) {
|
|
qWarning("close audio failed");
|
|
return;
|
|
}
|
|
ao->audioFormat().setChannelLayout(cl);
|
|
if (!ao->open()) {
|
|
qWarning("open audio failed");
|
|
return;
|
|
}
|
|
}
|
|
|
|
void MainWindow::changeAudioTrack(QAction *action)
|
|
{
|
|
int track = action->data().toInt();
|
|
if (mpAudioTrackAction == action && track >= 0) { // external action is always clickable
|
|
action->toggle();
|
|
return;
|
|
}
|
|
if (track < 0) {
|
|
QString f = QFileDialog::getOpenFileName(0, tr("Open an external audio track"));
|
|
if (f.isEmpty()) {
|
|
action->toggle();
|
|
return;
|
|
}
|
|
mpPlayer->setExternalAudio(f);
|
|
} else {
|
|
mpPlayer->setExternalAudio(QString());
|
|
if (!mpPlayer->setAudioStream(track)) {
|
|
action->toggle();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (mpAudioTrackAction)
|
|
mpAudioTrackAction->setChecked(false);
|
|
mpAudioTrackAction = action;
|
|
mpAudioTrackAction->setChecked(true);
|
|
if (mpStatisticsView && mpStatisticsView->isVisible())
|
|
mpStatisticsView->setStatistics(mpPlayer->statistics());
|
|
}
|
|
|
|
void MainWindow::changeVO(QAction *action)
|
|
{
|
|
if (action == mpVOAction) {
|
|
action->toggle(); //check state changes if clicked
|
|
return;
|
|
}
|
|
VideoRendererId vid = (VideoRendererId)action->data().toInt();
|
|
VideoRenderer *vo = VideoRenderer::create(vid);
|
|
if (vo && vo->isAvailable()) {
|
|
|
|
setRenderer(vo);
|
|
} else {
|
|
action->toggle(); //check state changes if clicked
|
|
QMessageBox::critical(0, QString::fromLatin1("QtAV"), tr("not availabe on your platform!"));
|
|
return;
|
|
}
|
|
}
|
|
|
|
void MainWindow::processPendingActions()
|
|
{
|
|
if (!mpTempRenderer)
|
|
return;
|
|
setRenderer(mpTempRenderer);
|
|
mpTempRenderer = 0;
|
|
if (mHasPendingPlay) {
|
|
mHasPendingPlay = false;
|
|
play(mFile);
|
|
}
|
|
}
|
|
|
|
void MainWindow::setAudioBackends(const QStringList& backends)
|
|
{
|
|
mAudioBackends = backends;
|
|
}
|
|
|
|
void MainWindow::setRenderer(QtAV::VideoRenderer *renderer)
|
|
{
|
|
if (!mIsReady) {
|
|
mpTempRenderer = renderer;
|
|
return;
|
|
}
|
|
if (!renderer)
|
|
return;
|
|
mpOSD->uninstall();
|
|
mpSubtitle->uninstall();
|
|
renderer->widget()->setMouseTracking(true); //mouseMoveEvent without press.
|
|
mpPlayer->setRenderer(renderer);
|
|
QWidget *r = 0;
|
|
if (mpRenderer)
|
|
r = mpRenderer->widget();
|
|
//release old renderer and add new
|
|
if (r) {
|
|
mpPlayerLayout->removeWidget(r);
|
|
if (r->testAttribute(Qt::WA_DeleteOnClose)) {
|
|
r->close();
|
|
} else {
|
|
r->close();
|
|
delete r;
|
|
}
|
|
r = 0;
|
|
}
|
|
mpRenderer = renderer;
|
|
//setInSize?
|
|
mpPlayerLayout->addWidget(renderer->widget());
|
|
if (mpVOAction) {
|
|
mpVOAction->setChecked(false);
|
|
}
|
|
foreach (QAction *action, mVOActions) {
|
|
if (action->data() == renderer->id()) {
|
|
mpVOAction = action;
|
|
break;
|
|
}
|
|
}
|
|
mpVOAction->setChecked(true);
|
|
mpTitle->setText(mpVOAction->text());
|
|
const VideoRendererId vid = mpPlayer->renderer()->id();
|
|
if (vid == VideoRendererId_GLWidget
|
|
|| vid == VideoRendererId_GLWidget2
|
|
|| vid == VideoRendererId_OpenGLWidget
|
|
) {
|
|
mpVideoEQ->setEngines(QVector<VideoEQConfigPage::Engine>() << VideoEQConfigPage::SWScale << VideoEQConfigPage::GLSL);
|
|
mpVideoEQ->setEngine(VideoEQConfigPage::GLSL);
|
|
mpPlayer->renderer()->forcePreferredPixelFormat(true);
|
|
} else if (vid == VideoRendererId_XV) {
|
|
mpVideoEQ->setEngines(QVector<VideoEQConfigPage::Engine>() << VideoEQConfigPage::XV);
|
|
mpVideoEQ->setEngine(VideoEQConfigPage::XV);
|
|
mpPlayer->renderer()->forcePreferredPixelFormat(true);
|
|
} else {
|
|
mpVideoEQ->setEngines(QVector<VideoEQConfigPage::Engine>() << VideoEQConfigPage::SWScale);
|
|
mpVideoEQ->setEngine(VideoEQConfigPage::SWScale);
|
|
mpPlayer->renderer()->forcePreferredPixelFormat(false);
|
|
}
|
|
onVideoEQEngineChanged();
|
|
mpOSD->installTo(mpRenderer);
|
|
mpSubtitle->installTo(mpRenderer);
|
|
}
|
|
|
|
void MainWindow::play(const QString &name)
|
|
{
|
|
mFile = name;
|
|
if (!mIsReady) {
|
|
mHasPendingPlay = true;
|
|
return;
|
|
}
|
|
mTitle = mFile;
|
|
if (!mFile.contains(QLatin1String("://")) || mFile.startsWith(QLatin1String("file://"))) {
|
|
mTitle = QFileInfo(mFile).fileName();
|
|
}
|
|
setWindowTitle(mTitle);
|
|
mpPlayer->stop(); //if no stop, in setPriority decoder will reopen
|
|
mpPlayer->setFrameRate(Config::instance().forceFrameRate());
|
|
if (!mAudioBackends.isEmpty())
|
|
mpPlayer->audio()->setBackends(mAudioBackends);
|
|
if (!mpRepeatEnableAction->isChecked())
|
|
mRepeateMax = 0;
|
|
mpPlayer->setInterruptOnTimeout(Config::instance().abortOnTimeout());
|
|
mpPlayer->setInterruptTimeout(Config::instance().timeout()*1000.0);
|
|
mpPlayer->setBufferMode(QtAV::BufferPackets);
|
|
mpPlayer->setBufferValue(Config::instance().bufferValue());
|
|
mpPlayer->setRepeat(mRepeateMax);
|
|
mpPlayer->setPriority(idsFromNames(Config::instance().decoderPriorityNames()));
|
|
mpPlayer->setOptionsForAudioCodec(mpDecoderConfigPage->audioDecoderOptions());
|
|
mpPlayer->setOptionsForVideoCodec(mpDecoderConfigPage->videoDecoderOptions());
|
|
if (Config::instance().avformatOptionsEnabled())
|
|
mpPlayer->setOptionsForFormat(Config::instance().avformatOptions());
|
|
PlayListItem item;
|
|
item.setUrl(mFile);
|
|
item.setTitle(mTitle);
|
|
item.setLastTime(0);
|
|
mpHistory->remove(mFile);
|
|
mpHistory->insertItemAt(item, 0);
|
|
mpPlayer->play(name);
|
|
}
|
|
|
|
void MainWindow::play(const QUrl &url)
|
|
{
|
|
play(QUrl::fromPercentEncoding(url.toEncoded()));
|
|
}
|
|
|
|
void MainWindow::setVideoDecoderNames(const QStringList &vd)
|
|
{
|
|
QStringList vdnames;
|
|
foreach (const QString& v, vd) {
|
|
vdnames << v.toLower();
|
|
}
|
|
QStringList vidp;
|
|
QStringList vids = idsToNames(VideoDecoder::registered());
|
|
foreach (const QString& v, vids) {
|
|
if (vdnames.contains(v.toLower())) {
|
|
vidp.append(v);
|
|
}
|
|
}
|
|
Config::instance().setDecoderPriorityNames(vidp);
|
|
}
|
|
|
|
void MainWindow::openFile()
|
|
{
|
|
QString file = QFileDialog::getOpenFileName(0, tr("Open a media file"), Config::instance().lastFile());
|
|
if (file.isEmpty())
|
|
return;
|
|
Config::instance().setLastFile(file);
|
|
play(file);
|
|
}
|
|
|
|
void MainWindow::togglePlayPause()
|
|
{
|
|
if (mpPlayer->isPlaying()) {
|
|
qDebug("isPaused = %d", mpPlayer->isPaused());
|
|
mpPlayer->pause(!mpPlayer->isPaused());
|
|
} else {
|
|
if (mFile.isEmpty())
|
|
return;
|
|
if (!mpPlayer->isPlaying())
|
|
play(mFile);
|
|
else
|
|
mpPlayer->play();
|
|
mpPlayPauseBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/pause.svg")));
|
|
}
|
|
}
|
|
|
|
void MainWindow::showNextOSD()
|
|
{
|
|
if (!mpOSD)
|
|
return;
|
|
mpOSD->useNextShowType();
|
|
}
|
|
|
|
void MainWindow::onSpinBoxChanged(double v)
|
|
{
|
|
if (!mpPlayer)
|
|
return;
|
|
mpPlayer->setSpeed(v);
|
|
}
|
|
|
|
void MainWindow::onPaused(bool p)
|
|
{
|
|
if (p) {
|
|
qDebug("start pausing...");
|
|
mpPlayPauseBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/play.svg")));
|
|
} else {
|
|
qDebug("stop pausing...");
|
|
mpPlayPauseBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/pause.svg")));
|
|
}
|
|
}
|
|
|
|
void MainWindow::onStartPlay()
|
|
{
|
|
mpRenderer->setRegionOfInterest(QRectF());
|
|
mFile = mpPlayer->file(); //open from EventFilter's menu
|
|
mTitle = mFile;
|
|
if (!mFile.contains(QLatin1String("://")) || mFile.startsWith(QLatin1String("file://")))
|
|
mTitle = QFileInfo(mFile).fileName();
|
|
setWindowTitle(mTitle);
|
|
|
|
mpPlayPauseBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/pause.svg")));
|
|
mpTimeSlider->setMinimum(mpPlayer->mediaStartPosition());
|
|
mpTimeSlider->setMaximum(mpPlayer->mediaStopPosition());
|
|
mpTimeSlider->setValue(0);
|
|
mpTimeSlider->setEnabled(mpPlayer->isSeekable());
|
|
mpEnd->setText(QTime(0, 0, 0).addMSecs(mpPlayer->mediaStopPosition()).toString(QString::fromLatin1("HH:mm:ss")));
|
|
setVolume();
|
|
mShowControl = 0;
|
|
QTimer::singleShot(3000, this, SLOT(tryHideControlBar()));
|
|
ScreenSaver::instance().disable();
|
|
initAudioTrackMenu();
|
|
mpRepeatA->setMinimumTime(QTime(0, 0, 0).addMSecs(mpPlayer->mediaStartPosition()));
|
|
mpRepeatA->setMaximumTime(QTime(0, 0, 0).addMSecs(mpPlayer->mediaStopPosition()));
|
|
mpRepeatB->setMinimumTime(QTime(0, 0, 0).addMSecs(mpPlayer->mediaStartPosition()));
|
|
mpRepeatB->setMaximumTime(QTime(0, 0, 0).addMSecs(mpPlayer->mediaStopPosition()));
|
|
mpRepeatA->setTime(QTime(0, 0, 0).addMSecs(mpPlayer->startPosition()));
|
|
mpRepeatB->setTime(QTime(0, 0, 0).addMSecs(mpPlayer->stopPosition()));
|
|
mCursorTimer = startTimer(3000);
|
|
PlayListItem item = mpHistory->itemAt(0);
|
|
item.setUrl(mFile);
|
|
item.setTitle(mTitle);
|
|
item.setDuration(mpPlayer->duration());
|
|
mpHistory->setItemAt(item, 0);
|
|
updateChannelMenu();
|
|
|
|
if (mpStatisticsView && mpStatisticsView->isVisible())
|
|
mpStatisticsView->setStatistics(mpPlayer->statistics());
|
|
}
|
|
|
|
void MainWindow::onStopPlay()
|
|
{
|
|
mpPlayer->setPriority(idsFromNames(Config::instance().decoderPriorityNames()));
|
|
if (mpPlayer->currentRepeat() < mpPlayer->repeat())
|
|
return;
|
|
// use shortcut to replay in EventFilter, the options will not be set, so set here
|
|
mpPlayer->setFrameRate(Config::instance().forceFrameRate());
|
|
mpPlayer->setOptionsForAudioCodec(mpDecoderConfigPage->audioDecoderOptions());
|
|
mpPlayer->setOptionsForVideoCodec(mpDecoderConfigPage->videoDecoderOptions());
|
|
if (Config::instance().avformatOptionsEnabled())
|
|
mpPlayer->setOptionsForFormat(Config::instance().avformatOptions());
|
|
|
|
mpPlayPauseBtn->setIcon(QIcon(QString::fromLatin1(":/theme/dark/play.svg")));
|
|
mpTimeSlider->setValue(0);
|
|
qDebug(">>>>>>>>>>>>>>disable slider");
|
|
mpTimeSlider->setDisabled(true);
|
|
mpTimeSlider->setMinimum(0);
|
|
mpTimeSlider->setMaximum(0);
|
|
mpCurrent->setText(QString::fromLatin1("00:00:00"));
|
|
mpEnd->setText(QString::fromLatin1("00:00:00"));
|
|
tryShowControlBar();
|
|
ScreenSaver::instance().enable();
|
|
toggleRepeat(false);
|
|
//mRepeateMax = 0;
|
|
killTimer(mCursorTimer);
|
|
unsetCursor();
|
|
if (m_preview)
|
|
m_preview->setFile(QString());
|
|
}
|
|
|
|
void MainWindow::onSpeedChange(qreal speed)
|
|
{
|
|
mpSpeed->setText(QString::fromLatin1("%1").arg(speed, 4, 'f', 2, QLatin1Char('0')));
|
|
}
|
|
|
|
void MainWindow::seek()
|
|
{
|
|
mpPlayer->setSeekType(AccurateSeek);
|
|
mpPlayer->seek((qint64)mpTimeSlider->value());
|
|
if (!m_preview || !Config::instance().previewEnabled())
|
|
return;
|
|
m_preview->setTimestamp(mpTimeSlider->value());
|
|
m_preview->preview();
|
|
m_preview->setWindowFlags(m_preview->windowFlags() |Qt::FramelessWindowHint|Qt::WindowStaysOnTopHint);
|
|
m_preview->resize(Config::instance().previewWidth(), Config::instance().previewHeight());
|
|
m_preview->show();
|
|
}
|
|
|
|
void MainWindow::showHideVolumeBar()
|
|
{
|
|
if (mpVolumeSlider->isHidden()) {
|
|
mpVolumeSlider->show();
|
|
} else {
|
|
mpVolumeSlider->hide();
|
|
}
|
|
}
|
|
|
|
void MainWindow::setVolume()
|
|
{
|
|
AudioOutput *ao = mpPlayer ? mpPlayer->audio() : 0;
|
|
qreal v = qreal(mpVolumeSlider->value())*kVolumeInterval;
|
|
if (ao) {
|
|
if (qAbs(int(ao->volume()/kVolumeInterval) - mpVolumeSlider->value()) >= int(0.1/kVolumeInterval)) {
|
|
ao->setVolume(v);
|
|
}
|
|
}
|
|
mpVolumeSlider->setToolTip(QString::number(v));
|
|
mpVolumeBtn->setToolTip(QString::number(v));
|
|
}
|
|
|
|
void MainWindow::closeEvent(QCloseEvent *e)
|
|
{
|
|
Q_UNUSED(e);
|
|
if (mpPlayer)
|
|
mpPlayer->stop();
|
|
qApp->quit();
|
|
}
|
|
|
|
void MainWindow::resizeEvent(QResizeEvent *e)
|
|
{
|
|
Q_UNUSED(e);
|
|
QWidget::resizeEvent(e);
|
|
/*
|
|
if (mpTitle)
|
|
QLabelSetElideText(mpTitle, QFileInfo(mFile).fileName(), e->size().width());
|
|
*/
|
|
}
|
|
|
|
void MainWindow::timerEvent(QTimerEvent *e)
|
|
{
|
|
if (e->timerId() == mCursorTimer) {
|
|
if (mpControl->isVisible())
|
|
return;
|
|
setCursor(Qt::BlankCursor);
|
|
}
|
|
}
|
|
|
|
void MainWindow::onPositionChange(qint64 pos)
|
|
{
|
|
if (mpPlayer->isSeekable())
|
|
mpTimeSlider->setValue(pos);
|
|
mpCurrent->setText(QTime(0, 0, 0).addMSecs(pos).toString(QString::fromLatin1("HH:mm:ss")));
|
|
}
|
|
|
|
void MainWindow::repeatAChanged(const QTime& t)
|
|
{
|
|
if (!mpPlayer)
|
|
return;
|
|
mpPlayer->setStartPosition(QTime(0, 0, 0).msecsTo(t));
|
|
}
|
|
|
|
void MainWindow::repeatBChanged(const QTime& t)
|
|
{
|
|
if (!mpPlayer)
|
|
return;
|
|
// when this slot is called? even if only range is set?
|
|
if (t <= mpRepeatA->time())
|
|
return;
|
|
mpPlayer->setStopPosition(QTime(0, 0, 0).msecsTo(t));
|
|
}
|
|
|
|
void MainWindow::keyPressEvent(QKeyEvent *e)
|
|
{
|
|
mControlOn = e->key() == Qt::Key_Control;
|
|
}
|
|
|
|
void MainWindow::keyReleaseEvent(QKeyEvent *e)
|
|
{
|
|
Q_UNUSED(e);
|
|
mControlOn = false;
|
|
}
|
|
|
|
void MainWindow::mousePressEvent(QMouseEvent *e)
|
|
{
|
|
if (!mControlOn)
|
|
return;
|
|
mGlobalMouse = e->globalPos();
|
|
}
|
|
|
|
void MainWindow::mouseMoveEvent(QMouseEvent *e)
|
|
{
|
|
unsetCursor();
|
|
if (e->pos().y() > height() - mpTimeSlider->height() - mpControl->height()) {
|
|
if (mShowControl == 0) {
|
|
mShowControl = 1;
|
|
tryShowControlBar();
|
|
}
|
|
} else {
|
|
if (mShowControl == 1) {
|
|
mShowControl = 0;
|
|
QTimer::singleShot(3000, this, SLOT(tryHideControlBar()));
|
|
}
|
|
}
|
|
if (mControlOn && e->button() == Qt::LeftButton) {
|
|
if (!mpRenderer || !mpRenderer->widget())
|
|
return;
|
|
QRectF roi = mpRenderer->realROI();
|
|
QPointF delta = e->pos() - mGlobalMouse;
|
|
roi.moveLeft(-delta.x());
|
|
roi.moveTop(-delta.y());
|
|
mpRenderer->setRegionOfInterest(roi);
|
|
}
|
|
}
|
|
|
|
void MainWindow::wheelEvent(QWheelEvent *e)
|
|
{
|
|
if (!mpRenderer || !mpRenderer->widget()) {
|
|
return;
|
|
}
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
|
|
qreal deg = e->angleDelta().y()/8;
|
|
#else
|
|
qreal deg = e->delta()/8;
|
|
#endif //QT_VERSION
|
|
#if WHEEL_SPEED
|
|
if (!mControlOn) {
|
|
qreal speed = mpPlayer->speed();
|
|
mpPlayer->setSpeed(qMax(0.01, speed + deg/15.0*0.02));
|
|
return;
|
|
}
|
|
#endif //WHEEL_SPEED
|
|
QPointF p = mpRenderer->widget()->mapFrom(this, e->pos());
|
|
QPointF fp = mpRenderer->mapToFrame(p);
|
|
if (fp.x() < 0)
|
|
fp.setX(0);
|
|
if (fp.y() < 0)
|
|
fp.setY(0);
|
|
if (fp.x() > mpRenderer->frameSize().width())
|
|
fp.setX(mpRenderer->frameSize().width());
|
|
if (fp.y() > mpRenderer->frameSize().height())
|
|
fp.setY(mpRenderer->frameSize().height());
|
|
|
|
QRectF viewport = QRectF(mpRenderer->mapToFrame(QPointF(0, 0)), mpRenderer->mapToFrame(QPointF(mpRenderer->rendererWidth(), mpRenderer->rendererHeight())));
|
|
//qDebug("vo: (%.1f, %.1f)=> frame: (%.1f, %.1f)", p.x(), p.y(), fp.x(), fp.y());
|
|
|
|
qreal zoom = 1.0 + deg*3.14/180.0;
|
|
//qDebug("deg: %d, %d zoom: %.2f", e->angleDelta().x(), e->angleDelta().y(), zoom);
|
|
QTransform m;
|
|
m.translate(fp.x(), fp.y());
|
|
m.scale(1.0/zoom, 1.0/zoom);
|
|
m.translate(-fp.x(), -fp.y());
|
|
QRectF r = m.mapRect(mpRenderer->realROI());
|
|
mpRenderer->setRegionOfInterest((r | m.mapRect(viewport))&QRectF(QPointF(0,0), mpRenderer->frameSize()));
|
|
}
|
|
|
|
void MainWindow::about()
|
|
{
|
|
QtAV::about();
|
|
}
|
|
|
|
void MainWindow::help()
|
|
{
|
|
QString name = QString::fromLatin1("help-%1.html").arg(QLocale::system().name());
|
|
QFile f(qApp->applicationDirPath() + QString::fromLatin1("/") + name);
|
|
if (!f.exists()) {
|
|
f.setFileName(QString::fromLatin1(":/") + name);
|
|
}
|
|
if (!f.exists()) {
|
|
f.setFileName(qApp->applicationDirPath() + QString::fromLatin1("/help.html"));
|
|
}
|
|
if (!f.exists()) {
|
|
f.setFileName(QString::fromLatin1(":/help.html"));
|
|
}
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
qWarning("Failed to open help-%s.html and help.html: %s", qPrintable(QLocale::system().name()), qPrintable(f.errorString()));
|
|
return;
|
|
}
|
|
QTextStream ts(&f);
|
|
ts.setCodec("UTF-8");
|
|
QString text = ts.readAll();
|
|
QMessageBox::information(0, tr("Help"), text);
|
|
}
|
|
|
|
void MainWindow::openUrl()
|
|
{
|
|
QString url = QInputDialog::getText(0, tr("Open an url"), tr("Url"));
|
|
if (url.isEmpty())
|
|
return;
|
|
play(url);
|
|
}
|
|
|
|
void MainWindow::updateChannelMenu()
|
|
{
|
|
if (mpChannelAction)
|
|
mpChannelAction->setChecked(false);
|
|
AudioOutput *ao = mpPlayer ? mpPlayer->audio() : 0; //getAO()?
|
|
if (!ao) {
|
|
return;
|
|
}
|
|
AudioFormat::ChannelLayout cl = ao->audioFormat().channelLayout();
|
|
QList<QAction*> as = mpChannelMenu->actions();
|
|
foreach (QAction *action, as) {
|
|
if (action->data().toInt() != (int)cl)
|
|
continue;
|
|
action->setChecked(true);
|
|
mpChannelAction = action;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void MainWindow::initAudioTrackMenu()
|
|
{
|
|
int track = -2;
|
|
QAction *a = 0;
|
|
QList<QAction*> as;
|
|
int tracks = 0;
|
|
if (!mpPlayer) {
|
|
a = mpAudioTrackMenu->addAction(tr("External"));
|
|
a->setData(-1);
|
|
a->setCheckable(true);
|
|
a->setChecked(false);
|
|
as.push_back(a);
|
|
mpAudioTrackAction = 0;
|
|
goto end;
|
|
}
|
|
track = mpPlayer->currentAudioStream();
|
|
as = mpAudioTrackMenu->actions();
|
|
tracks = mpPlayer->audioStreamCount();
|
|
if (mpAudioTrackAction && tracks == as.size()-1 && mpAudioTrackAction->data().toInt() == track)
|
|
return;
|
|
while (tracks + 1 < as.size()) {
|
|
a = as.takeLast();
|
|
mpAudioTrackMenu->removeAction(a);
|
|
delete a;
|
|
}
|
|
if (as.isEmpty()) {
|
|
a = mpAudioTrackMenu->addAction(tr("External"));
|
|
a->setData(-1);
|
|
a->setCheckable(true);
|
|
a->setChecked(false);
|
|
as.push_back(a);
|
|
mpAudioTrackAction = 0;
|
|
}
|
|
while (tracks + 1 > as.size()) {
|
|
a = mpAudioTrackMenu->addAction(QString::number(as.size()-1));
|
|
a->setData(as.size()-1);
|
|
a->setCheckable(true);
|
|
a->setChecked(false);
|
|
as.push_back(a);
|
|
}
|
|
end:
|
|
foreach(QAction *ac, as) {
|
|
if (ac->data().toInt() == track && track >= 0) {
|
|
if (mpPlayer && mpPlayer->externalAudio().isEmpty()) {
|
|
qDebug("track found!!!!!");
|
|
mpAudioTrackAction = ac;
|
|
ac->setChecked(true);
|
|
}
|
|
} else {
|
|
ac->setChecked(false);
|
|
}
|
|
}
|
|
if (mpPlayer && !mpPlayer->externalAudio().isEmpty()) {
|
|
mpAudioTrackAction = as.first();
|
|
}
|
|
if (mpAudioTrackAction)
|
|
mpAudioTrackAction->setChecked(true);
|
|
}
|
|
|
|
void MainWindow::switchAspectRatio(QAction *action)
|
|
{
|
|
qreal r = action->data().toDouble();
|
|
if (action == mpARAction && r != -2) {
|
|
action->toggle(); //check state changes if clicked
|
|
return;
|
|
}
|
|
if (r == 0) {
|
|
mpPlayer->renderer()->setOutAspectRatioMode(VideoRenderer::VideoAspectRatio);
|
|
} else if (r == -1) {
|
|
mpPlayer->renderer()->setOutAspectRatioMode(VideoRenderer::RendererAspectRatio);
|
|
} else {
|
|
if (r == -2)
|
|
r = QInputDialog::getDouble(0, tr("Aspect ratio"), QString(), 1.0);
|
|
mpPlayer->renderer()->setOutAspectRatioMode(VideoRenderer::CustomAspectRation);
|
|
mpPlayer->renderer()->setOutAspectRatio(r);
|
|
}
|
|
mpARAction->setChecked(false);
|
|
mpARAction = action;
|
|
mpARAction->setChecked(true);
|
|
}
|
|
|
|
void MainWindow::toggleRepeat(bool r)
|
|
{
|
|
mpRepeatEnableAction->setChecked(r);
|
|
mpRepeatAction->defaultWidget()->setEnabled(r); //why need defaultWidget?
|
|
if (r) {
|
|
mRepeateMax = mpRepeatBox->value();
|
|
} else {
|
|
mRepeateMax = 0;
|
|
}
|
|
if (mpPlayer) {
|
|
mpPlayer->setRepeat(mRepeateMax);
|
|
}
|
|
}
|
|
|
|
void MainWindow::setRepeateMax(int m)
|
|
{
|
|
mRepeateMax = m;
|
|
if (mpPlayer) {
|
|
mpPlayer->setRepeat(m);
|
|
}
|
|
}
|
|
|
|
void MainWindow::playOnlineVideo(QAction *action)
|
|
{
|
|
mTitle = action->text();
|
|
play(action->data().toString());
|
|
}
|
|
|
|
void MainWindow::onTVMenuClick()
|
|
{
|
|
static TVView *tvv = new TVView;
|
|
tvv->show();
|
|
connect(tvv, SIGNAL(clicked(QString,QString)), SLOT(onPlayListClick(QString,QString)));
|
|
tvv->setMaximumHeight(qApp->desktop()->height());
|
|
tvv->setMinimumHeight(tvv->width()*2);
|
|
}
|
|
|
|
void MainWindow::onPlayListClick(const QString &key, const QString &value)
|
|
{
|
|
mTitle = key;
|
|
play(value);
|
|
}
|
|
|
|
void MainWindow::tryHideControlBar()
|
|
{
|
|
if (mShowControl > 0) {
|
|
return;
|
|
}
|
|
if (mpControl->isHidden() && mpTimeSlider->isHidden())
|
|
return;
|
|
mpControl->hide();
|
|
mpTimeSlider->hide();
|
|
workaroundRendererSize();
|
|
}
|
|
|
|
void MainWindow::tryShowControlBar()
|
|
{
|
|
unsetCursor();
|
|
if (mpTimeSlider && mpTimeSlider->isHidden())
|
|
mpTimeSlider->show();
|
|
if (mpControl && mpControl->isHidden())
|
|
mpControl->show();
|
|
}
|
|
|
|
void MainWindow::showInfo()
|
|
{
|
|
if (!mpStatisticsView)
|
|
mpStatisticsView = new StatisticsView();
|
|
if (mpPlayer)
|
|
mpStatisticsView->setStatistics(mpPlayer->statistics());
|
|
mpStatisticsView->show();
|
|
}
|
|
|
|
void MainWindow::onTimeSliderHover(int pos, int value)
|
|
{
|
|
QPoint gpos = mapToGlobal(mpTimeSlider->pos() + QPoint(pos, 0));
|
|
QToolTip::showText(gpos, QTime(0, 0, 0).addMSecs(value).toString(QString::fromLatin1("HH:mm:ss")));
|
|
if (!Config::instance().previewEnabled())
|
|
return;
|
|
if (!m_preview)
|
|
m_preview = new VideoPreviewWidget();
|
|
m_preview->setFile(mpPlayer->file());
|
|
m_preview->setTimestamp(value);
|
|
m_preview->preview();
|
|
const int w = Config::instance().previewWidth();
|
|
const int h = Config::instance().previewHeight();
|
|
m_preview->setWindowFlags(m_preview->windowFlags() |Qt::FramelessWindowHint|Qt::WindowStaysOnTopHint);
|
|
m_preview->resize(w, h);
|
|
m_preview->move(gpos - QPoint(w/2, h));
|
|
m_preview->show();
|
|
}
|
|
|
|
void MainWindow::onTimeSliderLeave()
|
|
{
|
|
if (m_preview && m_preview->isVisible())
|
|
m_preview->hide();
|
|
}
|
|
|
|
void MainWindow::handleError(const AVError &e)
|
|
{
|
|
QMessageBox::warning(0, tr("Player error"), e.string());
|
|
}
|
|
|
|
void MainWindow::onMediaStatusChanged()
|
|
{
|
|
QString status;
|
|
AVPlayer *player = qobject_cast<AVPlayer*>(sender());
|
|
switch (player->mediaStatus()) {
|
|
case NoMedia:
|
|
status = tr("No media");
|
|
break;
|
|
case InvalidMedia:
|
|
status = tr("Invalid meida");
|
|
break;
|
|
case BufferingMedia:
|
|
status = tr("Buffering...");
|
|
break;
|
|
case BufferedMedia:
|
|
status = tr("Buffered");
|
|
break;
|
|
case LoadingMedia:
|
|
status = tr("Loading...");
|
|
break;
|
|
case LoadedMedia:
|
|
status = tr("Loaded");
|
|
break;
|
|
case StalledMedia:
|
|
status = tr("Stalled");
|
|
break;
|
|
default:
|
|
status = QString();
|
|
onStopPlay();
|
|
break;
|
|
}
|
|
qDebug() << "status changed " << status;
|
|
setWindowTitle(status + QString::fromLatin1(" ") + mTitle);
|
|
}
|
|
|
|
void MainWindow::onBufferProgress(qreal percent)
|
|
{
|
|
setWindowTitle(QString::fromLatin1("Buffering... %1% ").arg(percent*100.0, 0, 'f', 1) + mTitle);
|
|
}
|
|
|
|
void MainWindow::onVideoEQEngineChanged()
|
|
{
|
|
VideoRenderer *vo = mpPlayer->renderer();
|
|
VideoEQConfigPage::Engine e = mpVideoEQ->engine();
|
|
if (e == VideoEQConfigPage::SWScale) {
|
|
vo->forcePreferredPixelFormat(true);
|
|
vo->setPreferredPixelFormat(VideoFormat::Format_RGB32);
|
|
} else {
|
|
vo->forcePreferredPixelFormat(false);
|
|
}
|
|
onBrightnessChanged(mpVideoEQ->brightness()*100.0);
|
|
onContrastChanged(mpVideoEQ->contrast()*100.0);
|
|
onHueChanged(mpVideoEQ->hue()*100.0);
|
|
onSaturationChanged(mpVideoEQ->saturation()*100.0);
|
|
}
|
|
|
|
void MainWindow::onBrightnessChanged(int b)
|
|
{
|
|
VideoRenderer *vo = mpPlayer->renderer();
|
|
if (mpVideoEQ->engine() != VideoEQConfigPage::SWScale
|
|
&& vo->setBrightness(mpVideoEQ->brightness())) {
|
|
mpPlayer->setBrightness(0);
|
|
} else {
|
|
vo->setBrightness(0);
|
|
mpPlayer->setBrightness(b);
|
|
}
|
|
}
|
|
|
|
void MainWindow::onContrastChanged(int c)
|
|
{
|
|
VideoRenderer *vo = mpPlayer->renderer();
|
|
if (mpVideoEQ->engine() != VideoEQConfigPage::SWScale
|
|
&& vo->setContrast(mpVideoEQ->contrast())) {
|
|
mpPlayer->setContrast(0);
|
|
} else {
|
|
vo->setContrast(0);
|
|
mpPlayer->setContrast(c);
|
|
}
|
|
}
|
|
|
|
void MainWindow::onHueChanged(int h)
|
|
{
|
|
VideoRenderer *vo = mpPlayer->renderer();
|
|
if (mpVideoEQ->engine() != VideoEQConfigPage::SWScale
|
|
&& vo->setHue(mpVideoEQ->hue())) {
|
|
mpPlayer->setHue(0);
|
|
} else {
|
|
vo->setHue(0);
|
|
mpPlayer->setHue(h);
|
|
}
|
|
}
|
|
|
|
void MainWindow::onSaturationChanged(int s)
|
|
{
|
|
VideoRenderer *vo = mpPlayer->renderer();
|
|
if (mpVideoEQ->engine() != VideoEQConfigPage::SWScale
|
|
&& vo->setSaturation(mpVideoEQ->saturation())) {
|
|
mpPlayer->setSaturation(0);
|
|
} else {
|
|
vo->setSaturation(0);
|
|
mpPlayer->setSaturation(s);
|
|
}
|
|
}
|
|
|
|
void MainWindow::onCaptureConfigChanged()
|
|
{
|
|
mpPlayer->videoCapture()->setCaptureDir(Config::instance().captureDir());
|
|
mpPlayer->videoCapture()->setQuality(Config::instance().captureQuality());
|
|
if (Config::instance().captureFormat().toLower() == QLatin1String("original")) {
|
|
mpPlayer->videoCapture()->setOriginalFormat(true);
|
|
} else {
|
|
mpPlayer->videoCapture()->setOriginalFormat(false);
|
|
mpPlayer->videoCapture()->setSaveFormat(Config::instance().captureFormat());
|
|
}
|
|
mpCaptureBtn->setToolTip(QString::fromLatin1("%1\n%2: %3\n%4: %5")
|
|
.arg(tr("Capture video frame"))
|
|
.arg(tr("Save to"))
|
|
.arg(mpPlayer->videoCapture()->captureDir())
|
|
.arg(tr("Format"))
|
|
.arg(Config::instance().captureFormat()));
|
|
|
|
}
|
|
|
|
void MainWindow::onAVFilterVideoConfigChanged()
|
|
{
|
|
if (Config::instance().avfilterVideoEnable()) {
|
|
if (!mpVideoFilter) {
|
|
mpVideoFilter = new LibAVFilterVideo(this);
|
|
}
|
|
mpVideoFilter->setEnabled(true);
|
|
mpPlayer->installFilter(mpVideoFilter);
|
|
mpVideoFilter->setOptions(Config::instance().avfilterVideoOptions());
|
|
} else {
|
|
if (mpVideoFilter) {
|
|
mpVideoFilter->setEnabled(false);
|
|
}
|
|
mpPlayer->uninstallFilter(mpVideoFilter);
|
|
}
|
|
}
|
|
|
|
void MainWindow::onAVFilterAudioConfigChanged()
|
|
{
|
|
if (Config::instance().avfilterAudioEnable()) {
|
|
if (!mpAudioFilter) {
|
|
mpAudioFilter = new LibAVFilterAudio(this);
|
|
}
|
|
mpAudioFilter->setEnabled(true);
|
|
mpPlayer->installFilter(mpAudioFilter);
|
|
mpAudioFilter->setOptions(Config::instance().avfilterAudioOptions());
|
|
} else {
|
|
if (mpAudioFilter) {
|
|
mpAudioFilter->setEnabled(false);
|
|
}
|
|
mpPlayer->uninstallFilter(mpAudioFilter);
|
|
}
|
|
}
|
|
|
|
void MainWindow::donate()
|
|
{
|
|
//QDesktopServices::openUrl(QUrl("https://sourceforge.net/p/qtav/wiki/Donate%20%E6%8D%90%E8%B5%A0/"));
|
|
QDesktopServices::openUrl(QUrl(QString::fromLatin1("http://www.qtav.org/donate.html")));
|
|
}
|
|
|
|
void MainWindow::onBufferValueChanged()
|
|
{
|
|
if (!mpPlayer)
|
|
return;
|
|
mpPlayer->setBufferValue(Config::instance().bufferValue());
|
|
}
|
|
|
|
void MainWindow::onAbortOnTimeoutChanged()
|
|
{
|
|
if (!mpPlayer)
|
|
return;
|
|
mpPlayer->setInterruptOnTimeout(Config::instance().abortOnTimeout());
|
|
}
|
|
|
|
void MainWindow::setup()
|
|
{
|
|
ConfigDialog::display();
|
|
}
|
|
|
|
void MainWindow::handleFullscreenChange()
|
|
{
|
|
workaroundRendererSize();
|
|
|
|
// workaround renderer display size for ubuntu
|
|
tryShowControlBar();
|
|
QTimer::singleShot(3000, this, SLOT(tryHideControlBar()));
|
|
}
|
|
|
|
void MainWindow::toggoleSubtitleEnabled(bool value)
|
|
{
|
|
mpSubtitle->setEnabled(value);
|
|
}
|
|
|
|
void MainWindow::toggleSubtitleAutoLoad(bool value)
|
|
{
|
|
mpSubtitle->setAutoLoad(value);
|
|
}
|
|
|
|
void MainWindow::openSubtitle()
|
|
{
|
|
QString file = QFileDialog::getOpenFileName(0, tr("Open a subtitle file"));
|
|
if (file.isEmpty())
|
|
return;
|
|
mpSubtitle->setFile(file);
|
|
}
|
|
|
|
void MainWindow::setSubtitleCharset(const QString &charSet)
|
|
{
|
|
Q_UNUSED(charSet);
|
|
QComboBox *box = qobject_cast<QComboBox*>(sender());
|
|
if (!box)
|
|
return;
|
|
mpSubtitle->setCodec(box->itemData(box->currentIndex()).toByteArray());
|
|
}
|
|
|
|
void MainWindow::setSubtitleEngine(const QString &value)
|
|
{
|
|
Q_UNUSED(value)
|
|
QComboBox *box = qobject_cast<QComboBox*>(sender());
|
|
if (!box)
|
|
return;
|
|
mpSubtitle->setEngines(QStringList() << box->itemData(box->currentIndex()).toString());
|
|
}
|
|
|
|
void MainWindow::changeClockType(QAction *action)
|
|
{
|
|
action->setChecked(true);
|
|
int value = action->data().toInt();
|
|
if (value < 0) {
|
|
mpPlayer->masterClock()->setClockAuto(true);
|
|
// TODO: guess clock type
|
|
return;
|
|
}
|
|
mpPlayer->masterClock()->setClockAuto(false);
|
|
mpPlayer->masterClock()->setClockType(AVClock::ClockType(value));
|
|
}
|
|
|
|
void MainWindow::syncVolumeUi(qreal value)
|
|
{
|
|
const int v(value/kVolumeInterval);
|
|
if (mpVolumeSlider->value() == v)
|
|
return;
|
|
mpVolumeSlider->setValue(v);
|
|
}
|
|
|
|
void MainWindow::workaroundRendererSize()
|
|
{
|
|
if (!mpRenderer)
|
|
return;
|
|
QSize s = rect().size();
|
|
//resize(QSize(s.width()-1, s.height()-1));
|
|
//resize(s); //window resize to fullscreen size will create another fullScreenChange event
|
|
mpRenderer->widget()->resize(QSize(s.width()+1, s.height()+1));
|
|
mpRenderer->widget()->resize(s);
|
|
}
|