--- a/QTfrontend/CMakeLists.txt Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/CMakeLists.txt Tue Aug 28 20:30:57 2012 +0400
@@ -28,6 +28,7 @@
# Configure for SDL
find_package(SDL REQUIRED)
find_package(SDL_mixer REQUIRED)
+find_package(FFMPEG REQUIRED)
include_directories(.)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/model)
@@ -39,6 +40,7 @@
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/util)
include_directories(${SDL_INCLUDE_DIR})
include_directories(${SDLMIXER_INCLUDE_DIR})
+include_directories(${FFMPEG_INCLUDE_DIR})
include_directories(${CMAKE_SOURCE_DIR}/misc/quazip)
if(UNIX)
# HACK: in freebsd cannot find iconv.h included via SDL.h
@@ -170,6 +172,7 @@
${QT_LIBRARIES}
${SDL_LIBRARY}
${SDLMIXER_LIBRARY}
+ ${FFMPEG_LIBRARIES}
${HW_LINK_LIBS}
)
--- a/QTfrontend/binds.cpp Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/binds.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -65,5 +65,6 @@
{"mute", "8", QT_TRANSLATE_NOOP("binds", "mute audio"), NULL, NULL},
{"fullscr", "f12", QT_TRANSLATE_NOOP("binds", "change mode"), NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Toggle fullscreen mode:")},
{"capture", "c", QT_TRANSLATE_NOOP("binds", "capture"), NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Take a screenshot:")},
- {"rotmask", "delete", QT_TRANSLATE_NOOP("binds", "hedgehogs\ninfo"), NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Toggle labels above hedgehogs:")}
+ {"rotmask", "delete", QT_TRANSLATE_NOOP("binds", "hedgehogs\ninfo"), NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Toggle labels above hedgehogs:")},
+ {"record", "r", QT_TRANSLATE_NOOP("binds", "record"), NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Record video:")}
};
--- a/QTfrontend/binds.h Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/binds.h Tue Aug 28 20:30:57 2012 +0400
@@ -21,7 +21,7 @@
#include <QString>
-#define BINDS_NUMBER 45
+#define BINDS_NUMBER 46
struct BindAction
{
--- a/QTfrontend/game.cpp Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/game.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -53,20 +53,20 @@
{
switch (gameType)
{
- case gtSave:
- if (gameState == gsInterrupted || gameState == gsHalted)
- emit HaveRecord(false, demo);
- else if (gameState == gsFinished)
- emit HaveRecord(true, demo);
- break;
case gtDemo:
+ // for video recording we need demo anyway
+ emit HaveRecord(rtNeither, demo);
break;
case gtNet:
- emit HaveRecord(true, demo);
+ emit HaveRecord(rtDemo, demo);
break;
default:
- if (gameState == gsInterrupted || gameState == gsHalted) emit HaveRecord(false, demo);
- else if (gameState == gsFinished) emit HaveRecord(true, demo);
+ if (gameState == gsInterrupted || gameState == gsHalted)
+ emit HaveRecord(rtSave, demo);
+ else if (gameState == gsFinished)
+ emit HaveRecord(rtDemo, demo);
+ else
+ emit HaveRecord(rtNeither, demo);
}
SetGameState(gsStopped);
}
--- a/QTfrontend/game.h Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/game.h Tue Aug 28 20:30:57 2012 +0400
@@ -40,6 +40,13 @@
gsHalted = 6
};
+enum RecordType
+{
+ rtDemo,
+ rtSave,
+ rtNeither,
+};
+
bool checkForDir(const QString & dir);
class HWGame : public TCPBase
@@ -70,7 +77,7 @@
void SendTeamMessage(const QString & msg);
void GameStateChanged(GameState gameState);
void GameStats(char type, const QString & info);
- void HaveRecord(bool isDemo, const QByteArray & record);
+ void HaveRecord(RecordType type, const QByteArray & record);
void ErrorMessage(const QString &);
public slots:
--- a/QTfrontend/gameuiconfig.cpp Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/gameuiconfig.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -27,6 +27,7 @@
#include "gameuiconfig.h"
#include "hwform.h"
#include "pageoptions.h"
+#include "pagevideos.h"
#include "pagenetserver.h"
#include "hwconsts.h"
#include "fpsedit.h"
@@ -44,6 +45,7 @@
resizeToConfigValues();
reloadValues();
+ reloadVideosValues();
}
void GameUIConfig::reloadValues(void)
@@ -118,6 +120,29 @@
}
}
+void GameUIConfig::reloadVideosValues(void)
+{
+ Form->ui.pageVideos->framerateBox->setValue(value("videorec/fps",25).toUInt());
+ bool useGameRes = value("videorec/usegameres",true).toBool();
+ if (useGameRes)
+ {
+ QRect res = vid_Resolution();
+ Form->ui.pageVideos->widthEdit->setText(QString::number(res.width()));
+ Form->ui.pageVideos->heightEdit->setText(QString::number(res.height()));
+ }
+ else
+ {
+ Form->ui.pageVideos->widthEdit->setText(value("videorec/width","800").toString());
+ Form->ui.pageVideos->heightEdit->setText(value("videorec/height","600").toString());
+ }
+ Form->ui.pageVideos->checkUseGameRes->setChecked(useGameRes);
+ Form->ui.pageVideos->checkRecordAudio->setChecked(value("videorec/audio",true).toBool());
+ if (!Form->ui.pageVideos->tryCodecs(value("videorec/format","no").toString(),
+ value("videorec/videocodec","no").toString(),
+ value("videorec/audiocodec","no").toString()))
+ Form->ui.pageVideos->setDefaultCodecs();
+}
+
QStringList GameUIConfig::GetTeamsList()
{
QDir teamdir;
@@ -200,6 +225,21 @@
Form->gameSettings->sync();
}
+void GameUIConfig::SaveVideosOptions()
+{
+ QRect res = rec_Resolution();
+ setValue("videorec/format", AVFormat());
+ setValue("videorec/videocodec", videoCodec());
+ setValue("videorec/audiocodec", audioCodec());
+ setValue("videorec/fps", rec_Framerate());
+ setValue("videorec/width", res.width());
+ setValue("videorec/height", res.height());
+ setValue("videorec/usegameres", Form->ui.pageVideos->checkUseGameRes->isChecked());
+ setValue("videorec/audio", recordAudio());
+
+ Form->gameSettings->sync();
+}
+
QString GameUIConfig::language()
{
return Form->ui.pageOptions->CBLanguage->itemData(Form->ui.pageOptions->CBLanguage->currentIndex()).toString();
@@ -395,3 +435,38 @@
{
return Form->ui.pageOptions->volumeBox->value() * 128 / 100;
}
+
+QString GameUIConfig::AVFormat()
+{
+ return Form->ui.pageVideos->format();
+}
+
+QString GameUIConfig::videoCodec()
+{
+ return Form->ui.pageVideos->videoCodec();
+}
+
+QString GameUIConfig::audioCodec()
+{
+ return Form->ui.pageVideos->audioCodec();
+}
+
+QRect GameUIConfig::rec_Resolution()
+{
+ if (Form->ui.pageVideos->checkUseGameRes->isChecked())
+ return vid_Resolution();
+ QRect res(0,0,0,0);
+ res.setWidth(Form->ui.pageVideos->widthEdit->text().toUInt());
+ res.setHeight(Form->ui.pageVideos->heightEdit->text().toUInt());
+ return res;
+}
+
+int GameUIConfig::rec_Framerate()
+{
+ return Form->ui.pageVideos->framerateBox->value();
+}
+
+bool GameUIConfig::recordAudio()
+{
+ return Form->ui.pageVideos->checkRecordAudio->isChecked();
+}
--- a/QTfrontend/gameuiconfig.h Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/gameuiconfig.h Tue Aug 28 20:30:57 2012 +0400
@@ -59,18 +59,27 @@
void resizeToConfigValues();
quint32 stereoMode() const;
+ QString AVFormat();
+ QString videoCodec();
+ QString audioCodec();
+ QRect rec_Resolution();
+ int rec_Framerate();
+ bool recordAudio();
+
#ifdef __APPLE__
#ifdef SPARKLE_ENABLED
bool isAutoUpdateEnabled();
#endif
#endif
- void reloadValues(void);
+ void reloadValues();
+ void reloadVideosValues();
signals:
void frontendFullscreen(bool value);
public slots:
void SaveOptions();
+ void SaveVideosOptions();
void updNetNick();
private:
bool netPasswordIsValid();
--- a/QTfrontend/hwform.cpp Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/hwform.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -76,6 +76,7 @@
#include "pagegamestats.h"
#include "pageplayrecord.h"
#include "pagedata.h"
+#include "pagevideos.h"
#include "hwconsts.h"
#include "newnetclient.h"
#include "gamecfgwidget.h"
@@ -90,6 +91,7 @@
#include "drawmapwidget.h"
#include "mouseoverfilter.h"
#include "roomslistmodel.h"
+#include "recorder.h"
#include "DataManager.h"
@@ -140,6 +142,8 @@
config = new GameUIConfig(this, cfgdir->absolutePath() + "/hedgewars.ini");
+ ui.pageVideos->init(config);
+
#ifdef __APPLE__
panel = new M3Panel;
@@ -198,6 +202,9 @@
connect(ui.pageNetGame, SIGNAL(DLCClicked()), pageSwitchMapper, SLOT(map()));
pageSwitchMapper->setMapping(ui.pageNetGame, ID_PAGE_DATADOWNLOAD);
+ connect(ui.pageMain->BtnVideos, SIGNAL(clicked()), pageSwitchMapper, SLOT(map()));
+ pageSwitchMapper->setMapping(ui.pageMain->BtnVideos, ID_PAGE_VIDEOS);
+
//connect(ui.pageMain->BtnExit, SIGNAL(pressed()), this, SLOT(btnExitPressed()));
//connect(ui.pageMain->BtnExit, SIGNAL(clicked()), this, SLOT(btnExitClicked()));
@@ -288,6 +295,7 @@
connect(ui.pageConnecting, SIGNAL(cancelConnection()), this, SLOT(GoBack()));
+ connect(ui.pageVideos, SIGNAL(goBack()), config, SLOT(SaveVideosOptions()));
ammoSchemeModel = new AmmoSchemeModel(this, cfgdir->absolutePath() + "/schemes.ini");
ui.pageScheme->setModel(ammoSchemeModel);
@@ -512,6 +520,11 @@
GoToPage(ID_PAGE_SCHEME);
}
+void HWForm::GoToVideos()
+{
+ GoToPage(ID_PAGE_VIDEOS);
+}
+
void HWForm::OnPageShown(quint8 id, quint8 lastid)
{
#ifdef USE_XFIRE
@@ -603,6 +616,11 @@
config->reloadValues();
}
+ if (id == ID_PAGE_VIDEOS )
+ {
+ config->reloadVideosValues();
+ }
+
// load and save ignore/friends lists
if (lastid == ID_PAGE_NETGAME) // leaving a room
ui.pageNetGame->pChatWidget->saveLists(ui.pageOptions->editNetNick->text());
@@ -704,6 +722,8 @@
int curid = ui.Pages->currentIndex();
if (curid == ID_PAGE_MAIN)
{
+ if (!ui.pageVideos->tryQuit(this))
+ return;
stopAnim = true;
exit();
}
@@ -1357,7 +1377,7 @@
connect(game, SIGNAL(GameStateChanged(GameState)), this, SLOT(GameStateChanged(GameState)));
connect(game, SIGNAL(GameStats(char, const QString &)), ui.pageGameStats, SLOT(GameStats(char, const QString &)));
connect(game, SIGNAL(ErrorMessage(const QString &)), this, SLOT(ShowErrorMessage(const QString &)), Qt::QueuedConnection);
- connect(game, SIGNAL(HaveRecord(bool, const QByteArray &)), this, SLOT(GetRecord(bool, const QByteArray &)));
+ connect(game, SIGNAL(HaveRecord(RecordType, const QByteArray &)), this, SLOT(GetRecord(RecordType, const QByteArray &)));
m_lastDemo = QByteArray();
}
@@ -1368,43 +1388,47 @@
msg);
}
-void HWForm::GetRecord(bool isDemo, const QByteArray & record)
+void HWForm::GetRecord(RecordType type, const QByteArray & record)
{
- QString filename;
- QByteArray demo = record;
- QString recordFileName =
- config->appendDateTimeToRecordName() ?
- QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm") :
- "LastRound";
+ if (type != rtNeither)
+ {
+ QString filename;
+ QByteArray demo = record;
+ QString recordFileName =
+ config->appendDateTimeToRecordName() ?
+ QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm") :
+ "LastRound";
- QStringList versionParts = cVersionString->split('-');
- if ( (versionParts.size() == 2) && (!versionParts[1].isEmpty()) && (versionParts[1].contains(':')) )
- recordFileName = recordFileName + "_" + versionParts[1].replace(':','-');
+ QStringList versionParts = cVersionString->split('-');
+ if ( (versionParts.size() == 2) && (!versionParts[1].isEmpty()) && (versionParts[1].contains(':')) )
+ recordFileName = recordFileName + "_" + versionParts[1].replace(':','-');
- if (isDemo)
- {
- demo.replace(QByteArray("\x02TL"), QByteArray("\x02TD"));
- demo.replace(QByteArray("\x02TN"), QByteArray("\x02TD"));
- demo.replace(QByteArray("\x02TS"), QByteArray("\x02TD"));
- filename = cfgdir->absolutePath() + "/Demos/" + recordFileName + "." + *cProtoVer + ".hwd";
- m_lastDemo = demo;
- }
- else
- {
- demo.replace(QByteArray("\x02TL"), QByteArray("\x02TS"));
- demo.replace(QByteArray("\x02TN"), QByteArray("\x02TS"));
- filename = cfgdir->absolutePath() + "/Saves/" + recordFileName + "." + *cProtoVer + ".hws";
+ if (type == rtDemo)
+ {
+ demo.replace(QByteArray("\x02TL"), QByteArray("\x02TD"));
+ demo.replace(QByteArray("\x02TN"), QByteArray("\x02TD"));
+ demo.replace(QByteArray("\x02TS"), QByteArray("\x02TD"));
+ filename = cfgdir->absolutePath() + "/Demos/" + recordFileName + "." + *cProtoVer + ".hwd";
+ m_lastDemo = demo;
+ }
+ else
+ {
+ demo.replace(QByteArray("\x02TL"), QByteArray("\x02TS"));
+ demo.replace(QByteArray("\x02TN"), QByteArray("\x02TS"));
+ filename = cfgdir->absolutePath() + "/Saves/" + recordFileName + "." + *cProtoVer + ".hws";
+ }
+
+ QFile demofile(filename);
+ if (!demofile.open(QIODevice::WriteOnly))
+ ShowErrorMessage(tr("Cannot save record to file %1").arg(filename));
+ else
+ {
+ demofile.write(demo);
+ demofile.close();
+ }
}
-
- QFile demofile(filename);
- if (!demofile.open(QIODevice::WriteOnly))
- {
- ShowErrorMessage(tr("Cannot save record to file %1").arg(filename));
- return ;
- }
- demofile.write(demo);
- demofile.close();
+ ui.pageVideos->startEncoding(record);
}
void HWForm::startTraining(const QString & scriptName)
@@ -1445,6 +1469,7 @@
xfire_free();
#endif
config->SaveOptions();
+ config->SaveVideosOptions();
event->accept();
}
--- a/QTfrontend/hwform.h Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/hwform.h Tue Aug 28 20:30:57 2012 +0400
@@ -67,6 +67,7 @@
void exit();
void setButtonDescription(QString desc);
void backDescription();
+ void GoToVideos();
private slots:
void GoToSaves();
@@ -114,7 +115,7 @@
void GameStateChanged(GameState gameState);
void ForcedDisconnect(const QString & reason);
void ShowErrorMessage(const QString &);
- void GetRecord(bool isDemo, const QByteArray & record);
+ void GetRecord(RecordType type, const QByteArray & record);
void CreateNetGame();
void UpdateWeapons();
void onFrontendFullscreen(bool value);
@@ -175,6 +176,7 @@
ID_PAGE_DRAWMAP ,
ID_PAGE_DATADOWNLOAD ,
ID_PAGE_FEEDBACK ,
+ ID_PAGE_VIDEOS,
MAX_PAGE
};
QPointer<HWGame> game;
--- a/QTfrontend/main.cpp Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/main.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -197,6 +197,8 @@
checkForDir(cfgdir->absolutePath() + "/Screenshots");
checkForDir(cfgdir->absolutePath() + "/Teams");
checkForDir(cfgdir->absolutePath() + "/Logs");
+ checkForDir(cfgdir->absolutePath() + "/Videos");
+ checkForDir(cfgdir->absolutePath() + "/VideoTemp");
}
datadir->cd(bindir->absolutePath());
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/net/recorder.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,129 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * 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; version 2 of the License
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#include <QString>
+#include <QByteArray>
+//#include <QMessageBox>
+
+#include "recorder.h"
+#include "gameuiconfig.h"
+#include "hwconsts.h"
+#include "game.h"
+#include "libav_iteraction.h"
+
+// Encoding is memory expensive process, so we need to limit maximum number
+// of simultaneous encoders.
+static const int maxRecorders = 3;
+static int numRecorders = 0;
+
+static QList<HWRecorder*> queue;
+
+HWRecorder::HWRecorder(GameUIConfig * config, const QString &prefix) :
+ TCPBase(false)
+{
+ this->config = config;
+ this->prefix = prefix;
+ finished = false;
+ name = prefix + "." + LibavIteraction::instance().getExtension(config->AVFormat());
+}
+
+HWRecorder::~HWRecorder()
+{
+ emit encodingFinished(finished);
+ if (queue.empty())
+ numRecorders--;
+ else
+ queue.takeFirst()->Start();
+}
+
+void HWRecorder::onClientDisconnect()
+{
+}
+
+void HWRecorder::onClientRead()
+{
+ quint8 msglen;
+ quint32 bufsize;
+ while (!readbuffer.isEmpty() && ((bufsize = readbuffer.size()) > 0) &&
+ ((msglen = readbuffer.data()[0]) < bufsize))
+ {
+ QByteArray msg = readbuffer.left(msglen + 1);
+ readbuffer.remove(0, msglen + 1);
+ switch (msg.at(1))
+ {
+ case '?':
+ SendIPC("!");
+ break;
+ case 'p':
+ emit onProgress((quint8(msg.at(2))*256.0 + quint8(msg.at(3)))*0.0001);
+ break;
+ case 'v':
+ finished = true;
+ break;
+ }
+ }
+}
+
+void HWRecorder::EncodeVideo(const QByteArray & record)
+{
+ toSendBuf = record;
+ toSendBuf.replace(QByteArray("\x02TD"), QByteArray("\x02TV"));
+ toSendBuf.replace(QByteArray("\x02TL"), QByteArray("\x02TV"));
+ toSendBuf.replace(QByteArray("\x02TN"), QByteArray("\x02TV"));
+ toSendBuf.replace(QByteArray("\x02TS"), QByteArray("\x02TV"));
+
+ if (numRecorders < maxRecorders)
+ {
+ numRecorders++;
+ Start(); // run engine
+ }
+ else
+ queue.push_back(this);
+}
+
+QStringList HWRecorder::getArguments()
+{
+ QStringList arguments;
+ QRect resolution = config->rec_Resolution();
+ arguments << cfgdir->absolutePath();
+ arguments << QString::number(resolution.width());
+ arguments << QString::number(resolution.height());
+ arguments << "32"; // bpp
+ arguments << QString("%1").arg(ipc_port);
+ arguments << "0"; // fullscreen
+ arguments << "0"; // sound
+ arguments << "0"; // music
+ arguments << "0"; // sound volume
+ arguments << QString::number(config->timerInterval());
+ arguments << datadir->absolutePath();
+ arguments << (config->isShowFPSEnabled() ? "1" : "0");
+ arguments << (config->isAltDamageEnabled() ? "1" : "0");
+ arguments << config->netNick().toUtf8().toBase64();
+ arguments << QString::number(config->translateQuality());
+ arguments << QString::number(config->stereoMode());
+ arguments << HWGame::tr("en.txt");
+ arguments << QString::number(config->rec_Framerate()); // framerate numerator
+ arguments << "1"; // framerate denominator
+ arguments << prefix;
+ arguments << config->AVFormat();
+ arguments << config->videoCodec();
+ arguments << "5"; // video quality
+ arguments << (config->recordAudio()? config->audioCodec() : "no");
+
+ return arguments;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/net/recorder.h Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,58 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * 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; version 2 of the License
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#ifndef RECORDER_H
+#define RECORDER_H
+
+#include <QString>
+#include <QByteArray>
+
+#include "tcpBase.h"
+
+class GameUIConfig;
+class VideoItem;
+
+class HWRecorder : public TCPBase
+{
+ Q_OBJECT
+ public:
+ HWRecorder(GameUIConfig * config, const QString & prefix);
+ virtual ~HWRecorder();
+
+ void EncodeVideo(const QByteArray & record);
+
+ VideoItem * item; // used by pagevideos
+ QString name;
+ QString prefix;
+
+ protected:
+ // virtuals from TCPBase
+ virtual QStringList getArguments();
+ virtual void onClientRead();
+ virtual void onClientDisconnect();
+
+ signals:
+ void onProgress(float progress); // 0 < progress < 1
+ void encodingFinished(bool success);
+
+ private:
+ bool finished;
+ GameUIConfig * config;
+};
+
+#endif // RECORDER_H
--- a/QTfrontend/net/tcpBase.cpp Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/net/tcpBase.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -31,6 +31,8 @@
TCPBase::~TCPBase()
{
+ if (IPCSocket)
+ IPCSocket->deleteLater();
}
TCPBase::TCPBase(bool demoMode) :
@@ -65,6 +67,9 @@
connect(IPCSocket, SIGNAL(disconnected()), this, SLOT(ClientDisconnect()));
connect(IPCSocket, SIGNAL(readyRead()), this, SLOT(ClientRead()));
SendToClientFirst();
+
+ if(srvsList.size()==1) srvsList.pop_front();
+ emit isReadyNow();
}
void TCPBase::RealStart()
@@ -88,8 +93,8 @@
disconnect(IPCSocket, SIGNAL(readyRead()), this, SLOT(ClientRead()));
onClientDisconnect();
- if(srvsList.size()==1) srvsList.pop_front();
- emit isReadyNow();
+ /* if(srvsList.size()==1) srvsList.pop_front();
+ emit isReadyNow();*/
IPCSocket->deleteLater();
deleteLater();
}
--- a/QTfrontend/res/css/birthday.css Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/res/css/birthday.css Tue Aug 28 20:30:57 2012 +0400
@@ -33,7 +33,7 @@
a { color:#c8c8ff; }
QLineEdit, QListWidget, QTableView, QTextBrowser, QSpinBox, QComboBox,
-QComboBox QAbstractItemView, QMenu::item {
+QComboBox QAbstractItemView, QPlainTextEdit, QMenu::item {
background-color: rgba(13, 5, 68, 70%);
}
@@ -42,7 +42,7 @@
}
QPushButton, QListWidget, QTableView, QLineEdit, QHeaderView,
-QTextBrowser, QSpinBox, QToolBox, QComboBox,
+QTextBrowser, QSpinBox, QToolBox, QComboBox, QPlainTextEdit,
QComboBox QAbstractItemView, IconedGroupBox,
.QGroupBox, GameCFGWidget, TeamSelWidget, SelWeaponWidget,
QTabWidget::pane, QTabBar::tab {
@@ -57,14 +57,14 @@
}
QLineEdit, QListWidget,QTableView, QTextBrowser,
-QSpinBox, QToolBox {
+QSpinBox, QToolBox, QPlainTextEdit {
border-radius: 10px;
}
QLineEdit, QLabel, QHeaderView, QListWidget, QTableView,
QSpinBox, QToolBox::tab, QComboBox, QComboBox QAbstractItemView,
IconedGroupBox, .QGroupBox, GameCFGWidget, TeamSelWidget,
-SelWeaponWidget, QCheckBox, QRadioButton, QPushButton {
+SelWeaponWidget, QCheckBox, QRadioButton, QPushButton, QPlainTextEdit {
font: bold 13px;
}
SelWeaponWidget QTabWidget::pane, SelWeaponWidget QTabBar::tab:selected {
--- a/QTfrontend/res/css/christmas.css Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/res/css/christmas.css Tue Aug 28 20:30:57 2012 +0400
@@ -33,7 +33,7 @@
a { color:#c8c8ff; }
QLineEdit, QListWidget, QTableView, QTextBrowser, QSpinBox, QComboBox,
-QComboBox QAbstractItemView, QMenu::item {
+QComboBox QAbstractItemView, QPlainTextEdit, QMenu::item {
background-color: rgba(13, 5, 68, 70%);
}
@@ -42,7 +42,7 @@
}
QPushButton, QListWidget, QTableView, QLineEdit, QHeaderView,
-QTextBrowser, QSpinBox, QToolBox, QComboBox,
+QTextBrowser, QSpinBox, QToolBox, QComboBox, QPlainTextEdit,
QComboBox QAbstractItemView, IconedGroupBox,
.QGroupBox, GameCFGWidget, TeamSelWidget, SelWeaponWidget,
QTabWidget::pane, QTabBar::tab {
@@ -57,14 +57,14 @@
}
QLineEdit, QListWidget,QTableView, QTextBrowser,
-QSpinBox, QToolBox {
+QSpinBox, QToolBox, QPlainTextEdit {
border-radius: 10px;
}
QLineEdit, QLabel, QHeaderView, QListWidget, QTableView,
QSpinBox, QToolBox::tab, QComboBox, QComboBox QAbstractItemView,
IconedGroupBox, .QGroupBox, GameCFGWidget, TeamSelWidget,
-SelWeaponWidget, QCheckBox, QRadioButton, QPushButton {
+SelWeaponWidget, QCheckBox, QRadioButton, QPushButton, QPlainTextEdit {
font: bold 13px;
}
SelWeaponWidget QTabWidget::pane, SelWeaponWidget QTabBar::tab:selected {
--- a/QTfrontend/res/css/easter.css Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/res/css/easter.css Tue Aug 28 20:30:57 2012 +0400
@@ -33,7 +33,7 @@
a { color:#c8c8ff; }
QLineEdit, QListWidget, QTableView, QTextBrowser, QSpinBox, QComboBox,
-QComboBox QAbstractItemView, QMenu::item {
+QComboBox QAbstractItemView, QPlainTextEdit, QMenu::item {
background-color: rgba(13, 5, 68, 70%);
}
@@ -42,7 +42,7 @@
}
QPushButton, QListWidget, QTableView, QLineEdit, QHeaderView,
-QTextBrowser, QSpinBox, QToolBox, QComboBox,
+QTextBrowser, QSpinBox, QToolBox, QComboBox, QPlainTextEdit,
QComboBox QAbstractItemView, IconedGroupBox,
.QGroupBox, GameCFGWidget, TeamSelWidget, SelWeaponWidget,
QTabWidget::pane, QTabBar::tab {
@@ -57,14 +57,14 @@
}
QLineEdit, QListWidget,QTableView, QTextBrowser,
-QSpinBox, QToolBox {
+QSpinBox, QToolBox, QPlainTextEdit {
border-radius: 10px;
}
QLineEdit, QLabel, QHeaderView, QListWidget, QTableView,
QSpinBox, QToolBox::tab, QComboBox, QComboBox QAbstractItemView,
IconedGroupBox, .QGroupBox, GameCFGWidget, TeamSelWidget,
-SelWeaponWidget, QCheckBox, QRadioButton, QPushButton {
+SelWeaponWidget, QCheckBox, QRadioButton, QPushButton, QPlainTextEdit {
font: bold 13px;
}
SelWeaponWidget QTabWidget::pane, SelWeaponWidget QTabBar::tab:selected {
--- a/QTfrontend/res/css/qt.css Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/res/css/qt.css Tue Aug 28 20:30:57 2012 +0400
@@ -33,7 +33,7 @@
a { color:#c8c8ff; }
QLineEdit, QListWidget, QTableView, QTextBrowser, QSpinBox, QComboBox,
-QComboBox QAbstractItemView, QMenu::item {
+QComboBox QAbstractItemView, QPlainTextEdit, QMenu::item {
background-color: rgba(13, 5, 68, 70%);
}
@@ -42,7 +42,7 @@
}
QPushButton, QListWidget, QTableView, QLineEdit, QHeaderView,
-QTextBrowser, QSpinBox, QToolBox, QComboBox,
+QTextBrowser, QSpinBox, QToolBox, QComboBox, QPlainTextEdit,
QComboBox QAbstractItemView, IconedGroupBox,
.QGroupBox, GameCFGWidget, TeamSelWidget, SelWeaponWidget,
QTabWidget::pane, QTabBar::tab {
@@ -57,14 +57,14 @@
}
QLineEdit, QListWidget,QTableView, QTextBrowser,
-QSpinBox, QToolBox {
+QSpinBox, QToolBox, QPlainTextEdit {
border-radius: 10px;
}
QLineEdit, QLabel, QHeaderView, QListWidget, QTableView,
QSpinBox, QToolBox::tab, QComboBox, QComboBox QAbstractItemView,
IconedGroupBox, .QGroupBox, GameCFGWidget, TeamSelWidget,
-SelWeaponWidget, QCheckBox, QRadioButton, QPushButton {
+SelWeaponWidget, QCheckBox, QRadioButton, QPushButton, QPlainTextEdit {
font: bold 13px;
}
SelWeaponWidget QTabWidget::pane, SelWeaponWidget QTabBar::tab:selected {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/ui/dialog/ask_quit.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,79 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * 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; version 2 of the License
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#include <QVBoxLayout>
+#include <QLabel>
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QTimer>
+
+#include "hwform.h"
+#include "ask_quit.h"
+#include "pagevideos.h"
+
+HWAskQuitDialog::HWAskQuitDialog(QWidget* parent, HWForm * form) : QDialog(parent)
+{
+ this->form = form;
+
+ setWindowTitle(tr("Do yot really want to quit?"));
+
+ QVBoxLayout * layout = new QVBoxLayout(this);
+
+ QLabel * lbLabel = new QLabel(this);
+ lbLabel->setText(QLabel::tr("There are videos that are currently being processed.\n"
+ "Exiting now will abort them.\n"
+ "Do yot really want to quit?"));
+ layout->addWidget(lbLabel);
+
+ lbList = new QLabel(this);
+ layout->addWidget(lbList);
+ updateList();
+
+ QDialogButtonBox* dbbButtons = new QDialogButtonBox(this);
+ QPushButton * pbYes = dbbButtons->addButton(QDialogButtonBox::Yes);
+ QPushButton * pbNo = dbbButtons->addButton(QDialogButtonBox::No);
+ QPushButton * pbMore = dbbButtons->addButton(QPushButton::tr("More info"), QDialogButtonBox::HelpRole);
+ layout->addWidget(dbbButtons);
+
+ connect(pbYes, SIGNAL(clicked()), this, SLOT(accept()));
+ connect(pbNo, SIGNAL(clicked()), this, SLOT(reject()));
+ connect(pbMore, SIGNAL(clicked()), this, SLOT(goToPageVideos()));
+
+ // update list periodically
+ QTimer * timer = new QTimer(this);
+ connect(timer, SIGNAL(timeout()), this, SLOT(updateList()));
+ timer->start(200);
+}
+
+void HWAskQuitDialog::goToPageVideos()
+{
+ reject();
+ form->GoToVideos();
+}
+
+void HWAskQuitDialog::updateList()
+{
+ QString text = form->ui.pageVideos->getVideosInProgress();
+ if (text.isEmpty())
+ {
+ // automatically exit when everything is finished
+ accept();
+ return;
+ }
+ lbList->setText(text);
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/ui/dialog/ask_quit.h Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,45 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * 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; version 2 of the License
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#ifndef ASK_QUIT_H
+#define ASK_QUIT_H
+
+#include <QDialog>
+
+class QLabel;
+class HWForm;
+class PageVideos;
+
+class HWAskQuitDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ HWAskQuitDialog(QWidget* parent, HWForm *form);
+
+ private slots:
+ void goToPageVideos();
+ void updateList();
+
+ private:
+ HWForm * form;
+ QLabel * lbList;
+};
+
+
+#endif // INPUT_PASSWORD_H
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/ui/dialog/upload_video.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,297 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * 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; version 2 of the License
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#include <QLineEdit>
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QGridLayout>
+#include <QCheckBox>
+#include <QLabel>
+#include <QFrame>
+#include <QPlainTextEdit>
+#include <QSslError>
+#include <QUrl>
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QMessageBox>
+#include <QRegExp>
+#include <QRegExpValidator>
+#include <QMessageBox>
+
+#include "upload_video.h"
+#include "hwconsts.h"
+
+// User-agent string used in http requests.
+// Don't make it a global varibale - crash on linux because of cVersionString
+#define USER_AGENT ("Hedgewars-QtFrontend/" + *cVersionString).toAscii()
+
+// This is developer key obtained from http://code.google.com/apis/youtube/dashboard/
+// If you are reusing this code outside Hedgewars, don't use this developer key,
+// obtain you own at http://code.google.com/apis/youtube/dashboard/
+static const QByteArray devKey = "AI39si5pKjxR0XgNIlmrEFF-LyYD31rps4g2O5dZTxLgD0fvJ2rHxrMrNFY8FYTZrzeI3VlaFVQLKfFnSBugvdZmy8vFzRDefQ";
+
+HWUploadVideoDialog::HWUploadVideoDialog(QWidget* parent, const QString &filename, QNetworkAccessManager* netManager) : QDialog(parent)
+{
+ this->filename = filename;
+ this->netManager = netManager;
+
+ setWindowTitle(tr("Upload video"));
+
+ // Google requires us to display this, see https://developers.google.com/youtube/terms
+ QString GoogleNotice =
+ "<p>By clicking 'upload,' you certify that you own all rights to the content or that "
+ "you are authorized by the owner to make the content publicly available on YouTube, "
+ "and that it otherwise complies with the YouTube Terms of Service located at "
+ "<a href=\"http://www.youtube.com/t/terms\" style=\"color: white;\">http://www.youtube.com/t/terms</a>.</p>";
+
+ // youtube doesn't understand this characters, even when they are properly escaped
+ // (either with CDATA or with < or >)
+ QRegExp rx("[^<>]*");
+
+ int row = 0;
+
+ QGridLayout * layout = new QGridLayout(this);
+ layout->setColumnStretch(0, 1);
+ layout->setColumnStretch(1, 2);
+
+ QLabel * lbLabel = new QLabel(this);
+ lbLabel->setWordWrap(true);
+ lbLabel->setText(QLabel::tr(
+ "Please provide either the YouTube account name "
+ "or the email address associated with the Google Account."));
+ layout->addWidget(lbLabel, row++, 0, 1, 2);
+
+ lbLabel = new QLabel(this);
+ lbLabel->setText(QLabel::tr("Account name (or email): "));
+ layout->addWidget(lbLabel, row, 0);
+
+ leAccount = new QLineEdit(this);
+ layout->addWidget(leAccount, row++, 1);
+
+ lbLabel = new QLabel(this);
+ lbLabel->setText(QLabel::tr("Password: "));
+ layout->addWidget(lbLabel, row, 0);
+
+ lePassword = new QLineEdit(this);
+ lePassword->setEchoMode(QLineEdit::Password);
+ layout->addWidget(lePassword, row++, 1);
+
+ cbSave = new QCheckBox(this);
+ cbSave->setText(QCheckBox::tr("Save account name and password"));
+ layout->addWidget(cbSave, row++, 0, 1, 2);
+
+ QFrame * hr = new QFrame(this);
+ hr->setFrameStyle(QFrame::HLine);
+ hr->setLineWidth(3);
+ hr->setFixedHeight(10);
+ layout->addWidget(hr, row++, 0, 1, 2);
+
+ lbLabel = new QLabel(this);
+ lbLabel->setText(QLabel::tr("Video title: "));
+ layout->addWidget(lbLabel, row, 0);
+
+ leTitle = new QLineEdit(this);
+ leTitle->setText(filename);
+ leTitle->setValidator(new QRegExpValidator(rx, leTitle));
+ layout->addWidget(leTitle, row++, 1);
+
+ lbLabel = new QLabel(this);
+ lbLabel->setText(QLabel::tr("Video description: "));
+ layout->addWidget(lbLabel, row++, 0, 1, 2);
+
+ teDescription = new QPlainTextEdit(this);
+ layout->addWidget(teDescription, row++, 0, 1, 2);
+
+ lbLabel = new QLabel(this);
+ lbLabel->setText(QLabel::tr("Tags (comma separated): "));
+ layout->addWidget(lbLabel, row, 0);
+
+ leTags = new QLineEdit(this);
+ leTags->setText("hedgewars");
+ leTags->setMaxLength(500);
+ leTags->setValidator(new QRegExpValidator(rx, leTags));
+ layout->addWidget(leTags, row++, 1);
+
+ cbPrivate = new QCheckBox(this);
+ cbPrivate->setText(QCheckBox::tr("Video is private"));
+ layout->addWidget(cbPrivate, row++, 0, 1, 2);
+
+ hr = new QFrame(this);
+ hr->setFrameStyle(QFrame::HLine);
+ hr->setLineWidth(3);
+ hr->setFixedHeight(10);
+ layout->addWidget(hr, row++, 0, 1, 2);
+
+ lbLabel = new QLabel(this);
+ lbLabel->setWordWrap(true);
+ lbLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
+ lbLabel->setTextFormat(Qt::RichText);
+ lbLabel->setOpenExternalLinks(true);
+ lbLabel->setText(GoogleNotice);
+ layout->addWidget(lbLabel, row++, 0, 1, 2);
+
+ QDialogButtonBox* dbbButtons = new QDialogButtonBox(this);
+ btnUpload = dbbButtons->addButton(tr("Upload"), QDialogButtonBox::ActionRole);
+ QPushButton * pbCancel = dbbButtons->addButton(QDialogButtonBox::Cancel);
+ layout->addWidget(dbbButtons, row++, 0, 1, 2);
+
+ /* hr = new QFrame(this);
+ hr->setFrameStyle(QFrame::HLine);
+ hr->setLineWidth(3);
+ hr->setFixedHeight(10);
+ layout->addWidget(hr, row++, 0, 1, 2);*/
+
+ connect(btnUpload, SIGNAL(clicked()), this, SLOT(upload()));
+ connect(pbCancel, SIGNAL(clicked()), this, SLOT(reject()));
+}
+
+void HWUploadVideoDialog::showEvent(QShowEvent * event)
+{
+ QDialog::showEvent(event);
+
+ // set width to the same value as height (otherwise dialog has too small width)
+ QSize s = size();
+ QPoint p = pos();
+ resize(s.height(), s.height());
+ move(p.x() - (s.height() - s.width())/2, p.y());
+}
+
+void HWUploadVideoDialog::setEditable(bool editable)
+{
+ leTitle->setEnabled(editable);
+ leAccount->setEnabled(editable);
+ lePassword->setEnabled(editable);
+ btnUpload->setEnabled(editable);
+}
+
+void HWUploadVideoDialog::upload()
+{
+ setEditable(false);
+
+ // Documentation is at https://developers.google.com/youtube/2.0/developers_guide_protocol_clientlogin#ClientLogin_Authentication
+ QNetworkRequest request;
+ request.setUrl(QUrl("https://www.google.com/accounts/ClientLogin"));
+ request.setRawHeader("User-Agent", USER_AGENT);
+ request.setRawHeader("Content-Type", "application/x-www-form-urlencoded");
+
+ QString account(QUrl::toPercentEncoding(leAccount->text()));
+ QString pass(QUrl::toPercentEncoding(lePassword->text()));
+ QByteArray data = QString("Email=%1&Passwd=%2&service=youtube&source=Hedgewars").arg(account).arg(pass).toAscii();
+
+ QNetworkReply *reply = netManager->post(request, data);
+ connect(reply, SIGNAL(finished()), this, SLOT(authFinished()));
+}
+
+static QString XmlEscape(const QString& str)
+{
+ QString str2 = str;
+ // youtube doesn't understand this characters, even when they are properly escaped
+ // (either with CDATA or with < >)
+ str2.replace('<', ' ').replace('>', ' ');
+ return "<![CDATA[" + str2.replace("]]>", "]]]]><![CDATA[>") + "]]>";
+}
+
+void HWUploadVideoDialog::authFinished()
+{
+ QNetworkReply *reply = (QNetworkReply*)sender();
+ reply->deleteLater();
+
+ int HttpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ QByteArray answer = reply->readAll();
+ QString authToken = "";
+ QList<QByteArray> lines = answer.split('\n');
+ foreach (const QByteArray& line, lines)
+ {
+ QString str(line);
+ if (!str.startsWith("Auth=", Qt::CaseInsensitive))
+ continue;
+ str.remove(0, 5);
+ authToken = str;
+ break;
+ }
+ if (authToken.isEmpty())
+ {
+ QString errorStr = QMessageBox::tr("Error while authenticating at google.com:\n");
+ if (HttpCode == 403)
+ errorStr += QMessageBox::tr("Login or password is incorrect");
+ else
+ errorStr += reply->errorString();
+ QMessageBox::warning(this, QMessageBox::tr("Error"), errorStr);
+ setEditable(true);
+ return;
+ }
+
+ QByteArray auth = ("GoogleLogin auth=" + authToken).toAscii();
+
+ // We have authenticated, now we can send metadata and start upload
+ // Documentation is here: https://developers.google.com/youtube/2.0/developers_guide_protocol_resumable_uploads#Resumable_uploads
+ QByteArray body =
+ "<?xml version=\"1.0\"?>"
+ "<entry xmlns=\"http://www.w3.org/2005/Atom\" "
+ "xmlns:media=\"http://search.yahoo.com/mrss/\" "
+ "xmlns:yt=\"http://gdata.youtube.com/schemas/2007\">"
+ "<media:group>"
+ // "<yt:incomplete/>"
+ "<media:category "
+ "scheme=\"http://gdata.youtube.com/schemas/2007/categories.cat\">Games"
+ "</media:category>"
+ "<media:title type=\"plain\">"
+ + XmlEscape(leTitle->text()).toUtf8() +
+ "</media:title>"
+ "<media:description type=\"plain\">"
+ + XmlEscape(teDescription->toPlainText()).toUtf8() +
+ "</media:description>"
+ "<media:keywords type=\"plain\">"
+ + XmlEscape(leTags->text()).toUtf8() +
+ "</media:keywords>"
+ + (cbPrivate->isChecked()? "<yt:private/>" : "") +
+ "</media:group>"
+ "</entry>";
+
+ QNetworkRequest request;
+ request.setUrl(QUrl("http://uploads.gdata.youtube.com/resumable/feeds/api/users/default/uploads"));
+ request.setRawHeader("User-Agent", USER_AGENT);
+ request.setRawHeader("Authorization", auth);
+ request.setRawHeader("GData-Version", "2");
+ request.setRawHeader("X-GData-Key", "key=" + devKey);
+ request.setRawHeader("Slug", filename.toUtf8());
+ request.setRawHeader("Content-Type", "application/atom+xml; charset=UTF-8");
+
+ reply = netManager->post(request, body);
+ connect(reply, SIGNAL(finished()), this, SLOT(startUpload()));
+}
+
+void HWUploadVideoDialog::startUpload()
+{
+ QNetworkReply *reply = (QNetworkReply*)sender();
+ reply->deleteLater();
+
+ location = QString::fromAscii(reply->rawHeader("Location"));
+ if (location.isEmpty())
+ {
+ QString errorStr = QMessageBox::tr("Error while sending metadata to youtube.com:\n");
+ errorStr += reply->errorString();
+ QMessageBox::warning(this, QMessageBox::tr("Error"), errorStr);
+ setEditable(true);
+ return;
+ }
+
+ accept();
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/ui/dialog/upload_video.h Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,65 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * 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; version 2 of the License
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#ifndef UPLOAD_VIDEO_H
+#define UPLOAD_VIDEO_H
+
+#include <QDialog>
+
+class QLineEdit;
+class QCheckBox;
+class QPlainTextEdit;
+class QLabel;
+class QNetworkAccessManager;
+
+class HWUploadVideoDialog : public QDialog
+{
+ Q_OBJECT
+ public:
+ HWUploadVideoDialog(QWidget* parent, const QString& filename, QNetworkAccessManager* netManager);
+
+ QLineEdit* leAccount;
+ QLineEdit* lePassword;
+ QCheckBox* cbSave;
+
+ QLineEdit* leTitle;
+ QPlainTextEdit* teDescription;
+ QLineEdit* leTags;
+ QCheckBox* cbPrivate;
+
+ QPushButton* btnUpload;
+
+ QString location;
+
+ private:
+ QNetworkAccessManager* netManager;
+ QString filename;
+
+ void setEditable(bool editable);
+
+ protected:
+ // virtual from QWidget
+ void showEvent(QShowEvent * event);
+
+ private slots:
+ void upload();
+ void authFinished();
+ void startUpload();
+};
+
+#endif // UPLOAD_VIDEO_H
--- a/QTfrontend/ui/page/pagemain.cpp Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/ui/page/pagemain.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -85,9 +85,13 @@
bottomLayout->setStretch(0,1);
btnBack->setWhatsThis(tr("Exit game"));
-
- BtnSetup = addButton(":/res/Settings.png", bottomLayout, 1, true);
+
+ BtnVideos = addButton(":/res/Record.png", bottomLayout, 1, true);
+ BtnVideos->setWhatsThis(tr("Manage videos recorded from game"));
+
+ BtnSetup = addButton(":/res/Settings.png", bottomLayout, 2, true);
BtnSetup->setWhatsThis(tr("Edit game preferences"));
+
return bottomLayout;
}
--- a/QTfrontend/ui/page/pagemain.h Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/ui/page/pagemain.h Tue Aug 28 20:30:57 2012 +0400
@@ -34,6 +34,7 @@
QPushButton * BtnFeedback;
QPushButton * BtnInfo;
QPushButton * BtnDataDownload;
+ QPushButton * BtnVideos;
QLabel * mainNote;
private:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/ui/page/pagevideos.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,1136 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * 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; version 2 of the License
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#include <QGridLayout>
+#include <QPushButton>
+#include <QGroupBox>
+#include <QComboBox>
+#include <QCheckBox>
+#include <QLabel>
+#include <QLineEdit>
+#include <QSpinBox>
+#include <QTableWidget>
+#include <QDir>
+#include <QProgressBar>
+#include <QStringList>
+#include <QDesktopServices>
+#include <QUrl>
+#include <QList>
+#include <QMessageBox>
+#include <QHeaderView>
+#include <QKeyEvent>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QFileSystemWatcher>
+#include <QDateTime>
+#include <QRegExp>
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QXmlStreamReader>
+
+#include "hwconsts.h"
+#include "pagevideos.h"
+#include "igbox.h"
+#include "libav_iteraction.h"
+#include "gameuiconfig.h"
+#include "recorder.h"
+#include "ask_quit.h"
+#include "upload_video.h"
+
+static const QSize ThumbnailSize(350, 350*3/5);
+
+// columns in table with list of video files
+enum VideosColumns
+{
+ vcName,
+ vcSize,
+ vcProgress, // either encoding or uploading
+
+ vcNumColumns,
+};
+
+// this class is used for items in first column in file-table
+class VideoItem : public QTableWidgetItem
+{
+ // note: QTableWidgetItem is not Q_OBJECT
+
+ public:
+ VideoItem(const QString& name);
+ ~VideoItem();
+
+ QString name;
+ QString prefix; // original filename without extension
+ QString desc; // description (duration, resolution, etc...)
+ QString uploadUrl; // http://youtu.be/???????
+ HWRecorder * pRecorder; // non NULL if file is being encoded
+ QNetworkReply * pUploading; // non NULL if file is being uploaded
+ bool seen; // used when updating directory
+ float lastSizeUpdate;
+ float progress;
+
+ bool ready()
+ { return !pRecorder; }
+
+ QString path()
+ { return cfgdir->absoluteFilePath("Videos/" + name); }
+};
+
+VideoItem::VideoItem(const QString& name)
+ : QTableWidgetItem(name, UserType)
+{
+ this->name = name;
+ pRecorder = NULL;
+ pUploading = NULL;
+ lastSizeUpdate = 0;
+ progress = 0;
+}
+
+VideoItem::~VideoItem()
+{}
+
+QLayout * PageVideos::bodyLayoutDefinition()
+{
+ QGridLayout * pPageLayout = new QGridLayout();
+ pPageLayout->setColumnStretch(0, 1);
+ pPageLayout->setColumnStretch(1, 2);
+ pPageLayout->setRowStretch(0, 1);
+ pPageLayout->setRowStretch(1, 1);
+
+ // options
+ {
+ IconedGroupBox* pOptionsGroup = new IconedGroupBox(this);
+ pOptionsGroup->setIcon(QIcon(":/res/Settings.png")); // FIXME
+ pOptionsGroup->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ pOptionsGroup->setTitle(QGroupBox::tr("Video recording options"));
+ QGridLayout * pOptLayout = new QGridLayout(pOptionsGroup);
+
+ // label for format
+ QLabel *labelFormat = new QLabel(pOptionsGroup);
+ labelFormat->setText(QLabel::tr("Format"));
+ pOptLayout->addWidget(labelFormat, 0, 0);
+
+ // list of supported formats
+ comboAVFormats = new QComboBox(pOptionsGroup);
+ pOptLayout->addWidget(comboAVFormats, 0, 1, 1, 4);
+ LibavIteraction::instance().fillFormats(comboAVFormats);
+
+ // separator
+ QFrame * hr = new QFrame(pOptionsGroup);
+ hr->setFrameStyle(QFrame::HLine);
+ hr->setLineWidth(3);
+ hr->setFixedHeight(10);
+ pOptLayout->addWidget(hr, 1, 0, 1, 5);
+
+ // label for audio codec
+ QLabel *labelACodec = new QLabel(pOptionsGroup);
+ labelACodec->setText(QLabel::tr("Audio codec"));
+ pOptLayout->addWidget(labelACodec, 2, 0);
+
+ // list of supported audio codecs
+ comboAudioCodecs = new QComboBox(pOptionsGroup);
+ pOptLayout->addWidget(comboAudioCodecs, 2, 1, 1, 3);
+
+ // checkbox 'record audio'
+ checkRecordAudio = new QCheckBox(pOptionsGroup);
+ checkRecordAudio->setText(QCheckBox::tr("Record audio"));
+ pOptLayout->addWidget(checkRecordAudio, 2, 4);
+
+ // separator
+ hr = new QFrame(pOptionsGroup);
+ hr->setFrameStyle(QFrame::HLine);
+ hr->setLineWidth(3);
+ hr->setFixedHeight(10);
+ pOptLayout->addWidget(hr, 3, 0, 1, 5);
+
+ // label for video codec
+ QLabel *labelVCodec = new QLabel(pOptionsGroup);
+ labelVCodec->setText(QLabel::tr("Video codec"));
+ pOptLayout->addWidget(labelVCodec, 4, 0);
+
+ // list of supported video codecs
+ comboVideoCodecs = new QComboBox(pOptionsGroup);
+ pOptLayout->addWidget(comboVideoCodecs, 4, 1, 1, 4);
+
+ // label for resolution
+ QLabel *labelRes = new QLabel(pOptionsGroup);
+ labelRes->setText(QLabel::tr("Resolution"));
+ pOptLayout->addWidget(labelRes, 5, 0);
+
+ // width
+ widthEdit = new QLineEdit(pOptionsGroup);
+ widthEdit->setValidator(new QIntValidator(this));
+ pOptLayout->addWidget(widthEdit, 5, 1);
+
+ // x
+ QLabel *labelX = new QLabel(pOptionsGroup);
+ labelX->setText("X");
+ pOptLayout->addWidget(labelX, 5, 2);
+
+ // height
+ heightEdit = new QLineEdit(pOptionsGroup);
+ heightEdit->setValidator(new QIntValidator(pOptionsGroup));
+ pOptLayout->addWidget(heightEdit, 5, 3);
+
+ // checkbox 'use game resolution'
+ checkUseGameRes = new QCheckBox(pOptionsGroup);
+ checkUseGameRes->setText(QCheckBox::tr("Use game resolution"));
+ pOptLayout->addWidget(checkUseGameRes, 5, 4);
+
+ // label for framerate
+ QLabel *labelFramerate = new QLabel(pOptionsGroup);
+ labelFramerate->setText(QLabel::tr("Framerate"));
+ pOptLayout->addWidget(labelFramerate, 6, 0);
+
+ // framerate
+ framerateBox = new QSpinBox(pOptionsGroup);
+ framerateBox->setRange(1, 200);
+ framerateBox->setSingleStep(1);
+ pOptLayout->addWidget(framerateBox, 6, 1);
+
+ // button 'set default options'
+ btnDefaults = new QPushButton(pOptionsGroup);
+ btnDefaults->setText(QPushButton::tr("Set default options"));
+ pOptLayout->addWidget(btnDefaults, 7, 0, 1, 5);
+
+ pPageLayout->addWidget(pOptionsGroup, 1, 0);
+ }
+
+ // list of videos
+ {
+ IconedGroupBox* pTableGroup = new IconedGroupBox(this);
+ pTableGroup->setIcon(QIcon(":/res/graphicsicon.png")); // FIXME
+ pTableGroup->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ pTableGroup->setTitle(QGroupBox::tr("Videos"));
+
+ QStringList columns;
+ columns << tr("Name");
+ columns << tr("Size");
+ columns << "";
+
+ filesTable = new QTableWidget(pTableGroup);
+ filesTable->setColumnCount(vcNumColumns);
+ filesTable->setHorizontalHeaderLabels(columns);
+ filesTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+ filesTable->setSelectionMode(QAbstractItemView::SingleSelection);
+ filesTable->setEditTriggers(QAbstractItemView::SelectedClicked);
+ filesTable->verticalHeader()->hide();
+ filesTable->setMinimumWidth(400);
+
+ QHeaderView * header = filesTable->horizontalHeader();
+ header->setResizeMode(vcName, QHeaderView::ResizeToContents);
+ header->setResizeMode(vcSize, QHeaderView::Fixed);
+ header->resizeSection(vcSize, 100);
+ header->setStretchLastSection(true);
+
+ btnOpenDir = new QPushButton(QPushButton::tr("Open videos directory"), pTableGroup);
+
+ QVBoxLayout *box = new QVBoxLayout(pTableGroup);
+ box->addWidget(filesTable);
+ box->addWidget(btnOpenDir);
+
+ pPageLayout->addWidget(pTableGroup, 0, 1, 2, 1);
+ }
+
+ // description
+ {
+ IconedGroupBox* pDescGroup = new IconedGroupBox(this);
+ pDescGroup->setIcon(QIcon(":/res/graphicsicon.png")); // FIXME
+ pDescGroup->setTitle(QGroupBox::tr("Description"));
+
+ QVBoxLayout* pDescLayout = new QVBoxLayout(pDescGroup);
+ QHBoxLayout* pTopDescLayout = new QHBoxLayout(0); // picture and text
+ QHBoxLayout* pBottomDescLayout = new QHBoxLayout(0); // buttons
+
+ // label with thumbnail picture
+ labelThumbnail = new QLabel(pDescGroup);
+ labelThumbnail->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
+ labelThumbnail->setMaximumSize(ThumbnailSize);
+ labelThumbnail->setStyleSheet(
+ "QFrame {"
+ "border: solid;"
+ "border-width: 3px;"
+ "border-color: #ffcc00;"
+ "border-radius: 4px;"
+ "}" );
+ clearThumbnail();
+ pTopDescLayout->addWidget(labelThumbnail, 2);
+
+ // label with file description
+ labelDesc = new QLabel(pDescGroup);
+ labelDesc->setAlignment(Qt::AlignLeft | Qt::AlignTop);
+ labelDesc->setTextInteractionFlags(Qt::TextSelectableByMouse |
+ Qt::TextSelectableByKeyboard |
+ Qt::LinksAccessibleByMouse |
+ Qt::LinksAccessibleByKeyboard);
+ labelDesc->setTextFormat(Qt::RichText);
+ labelDesc->setOpenExternalLinks(true);
+ pTopDescLayout->addWidget(labelDesc, 1);
+
+ // buttons: play and delete
+ btnPlay = new QPushButton(QPushButton::tr("Play"), pDescGroup);
+ btnPlay->setEnabled(false);
+ pBottomDescLayout->addWidget(btnPlay);
+ btnDelete = new QPushButton(QPushButton::tr("Delete"), pDescGroup);
+ btnDelete->setEnabled(false);
+ pBottomDescLayout->addWidget(btnDelete);
+ btnToYouTube = new QPushButton(QPushButton::tr("Upload to YouTube"), pDescGroup);
+ btnToYouTube->setEnabled(false);
+ pBottomDescLayout->addWidget(btnToYouTube);
+
+ pDescLayout->addStretch(1);
+ pDescLayout->addLayout(pTopDescLayout, 0);
+ pDescLayout->addStretch(1);
+ pDescLayout->addLayout(pBottomDescLayout, 0);
+
+ pPageLayout->addWidget(pDescGroup, 0, 0);
+ }
+
+ return pPageLayout;
+}
+
+QLayout * PageVideos::footerLayoutDefinition()
+{
+ return NULL;
+}
+
+void PageVideos::connectSignals()
+{
+ connect(checkUseGameRes, SIGNAL(stateChanged(int)), this, SLOT(changeUseGameRes(int)));
+ connect(checkRecordAudio, SIGNAL(stateChanged(int)), this, SLOT(changeRecordAudio(int)));
+ connect(comboAVFormats, SIGNAL(currentIndexChanged(int)), this, SLOT(changeAVFormat(int)));
+ connect(btnDefaults, SIGNAL(clicked()), this, SLOT(setDefaultOptions()));
+ connect(filesTable, SIGNAL(cellDoubleClicked(int, int)), this, SLOT(cellDoubleClicked(int, int)));
+ connect(filesTable, SIGNAL(cellChanged(int,int)), this, SLOT(cellChanged(int, int)));
+ connect(filesTable, SIGNAL(currentCellChanged(int,int,int,int)), this, SLOT(currentCellChanged(int,int,int,int)));
+ connect(btnPlay, SIGNAL(clicked()), this, SLOT(playSelectedFile()));
+ connect(btnDelete, SIGNAL(clicked()), this, SLOT(deleteSelectedFiles()));
+ connect(btnToYouTube, SIGNAL(clicked()), this, SLOT(uploadToYouTube()));
+ connect(btnOpenDir, SIGNAL(clicked()), this, SLOT(openVideosDirectory()));
+}
+
+PageVideos::PageVideos(QWidget* parent) : AbstractPage(parent),
+ config(0), netManager(0)
+{
+ nameChangedFromCode = false;
+ numRecorders = 0;
+ numUploads = 0;
+ initPage();
+}
+
+void PageVideos::init(GameUIConfig * config)
+{
+ this->config = config;
+
+ QString path = cfgdir->absolutePath() + "/Videos";
+ QFileSystemWatcher * pWatcher = new QFileSystemWatcher(this);
+ pWatcher->addPath(path);
+ connect(pWatcher, SIGNAL(directoryChanged(const QString &)), this, SLOT(updateFileList(const QString &)));
+ updateFileList(path);
+
+ startEncoding(); // this is for videos recorded from demos which were executed directly (without frontend)
+}
+
+// user changed file format, we need to update list of codecs
+void PageVideos::changeAVFormat(int index)
+{
+ // remember selected codecs
+ QString prevVCodec = videoCodec();
+ QString prevACodec = audioCodec();
+
+ // clear lists of codecs
+ comboVideoCodecs->clear();
+ comboAudioCodecs->clear();
+
+ // get list of codecs for specified format
+ LibavIteraction::instance().fillCodecs(comboAVFormats->itemData(index).toString(), comboVideoCodecs, comboAudioCodecs);
+
+ // disable audio if there is no audio codec
+ if (comboAudioCodecs->count() == 0)
+ {
+ checkRecordAudio->setChecked(false);
+ checkRecordAudio->setEnabled(false);
+ }
+ else
+ checkRecordAudio->setEnabled(true);
+
+ // restore selected codecs if possible
+ int iVCodec = comboVideoCodecs->findData(prevVCodec);
+ if (iVCodec != -1)
+ comboVideoCodecs->setCurrentIndex(iVCodec);
+ int iACodec = comboAudioCodecs->findData(prevACodec);
+ if (iACodec != -1)
+ comboAudioCodecs->setCurrentIndex(iACodec);
+}
+
+// user switched checkbox 'use game resolution'
+void PageVideos::changeUseGameRes(int state)
+{
+ if (state && config)
+ {
+ // set resolution to game resolution
+ QRect resolution = config->vid_Resolution();
+ widthEdit->setText(QString::number(resolution.width()));
+ heightEdit->setText(QString::number(resolution.height()));
+ }
+ widthEdit->setEnabled(!state);
+ heightEdit->setEnabled(!state);
+}
+
+// user switched checkbox 'record audio'
+void PageVideos::changeRecordAudio(int state)
+{
+ comboAudioCodecs->setEnabled(!!state);
+}
+
+void PageVideos::setDefaultCodecs()
+{
+ if (tryCodecs("mp4", "libx264", "libmp3lame"))
+ return;
+ if (tryCodecs("mp4", "libx264", "libfaac"))
+ return;
+ if (tryCodecs("mp4", "libx264", "libvo_aacenc"))
+ return;
+ if (tryCodecs("mp4", "libx264", "aac"))
+ return;
+ if (tryCodecs("mp4", "libx264", "mp2"))
+ return;
+ if (tryCodecs("avi", "libxvid", "libmp3lame"))
+ return;
+ if (tryCodecs("avi", "libxvid", "ac3_fixed"))
+ return;
+ if (tryCodecs("avi", "libxvid", "mp2"))
+ return;
+ if (tryCodecs("avi", "mpeg4", "libmp3lame"))
+ return;
+ if (tryCodecs("avi", "mpeg4", "ac3_fixed"))
+ return;
+ if (tryCodecs("avi", "mpeg4", "mp2"))
+ return;
+
+ // this shouldn't happen, just in case
+ if (tryCodecs("ogg", "libtheora", "libvorbis"))
+ return;
+ tryCodecs("ogg", "libtheora", "flac");
+}
+
+void PageVideos::setDefaultOptions()
+{
+ framerateBox->setValue(25);
+ checkRecordAudio->setChecked(true);
+ checkUseGameRes->setChecked(true);
+ setDefaultCodecs();
+}
+
+bool PageVideos::tryCodecs(const QString & format, const QString & vcodec, const QString & acodec)
+{
+ // first we should change format
+ int iFormat = comboAVFormats->findData(format);
+ if (iFormat == -1)
+ return false;
+ comboAVFormats->setCurrentIndex(iFormat);
+ // format was changed, so lists of codecs were automatically updated to codecs supported by this format
+
+ // try to find video codec
+ int iVCodec = comboVideoCodecs->findData(vcodec);
+ if (iVCodec == -1)
+ return false;
+ comboVideoCodecs->setCurrentIndex(iVCodec);
+
+ // try to find audio codec
+ int iACodec = comboAudioCodecs->findData(acodec);
+ if (iACodec == -1 && checkRecordAudio->isChecked())
+ return false;
+ if (iACodec != -1)
+ comboAudioCodecs->setCurrentIndex(iACodec);
+
+ return true;
+}
+
+// get file size as string
+static QString FileSizeStr(const QString & path)
+{
+ quint64 size = QFileInfo(path).size();
+
+ quint64 KiB = 1024;
+ quint64 MiB = 1024*KiB;
+ quint64 GiB = 1024*MiB;
+ QString sizeStr;
+ if (size >= GiB)
+ return QString("%1 GiB").arg(QString::number(float(size)/GiB, 'f', 2));
+ if (size >= MiB)
+ return QString("%1 MiB").arg(QString::number(float(size)/MiB, 'f', 2));
+ if (size >= KiB)
+ return QString("%1 KiB").arg(QString::number(float(size)/KiB, 'f', 2));
+ return PageVideos::tr("%1 bytes").arg(QString::number(size));
+}
+
+// set file size in file list in specified row
+void PageVideos::updateSize(int row)
+{
+ VideoItem * item = nameItem(row);
+ QString path = item->ready()? item->path() : cfgdir->absoluteFilePath("VideoTemp/" + item->pRecorder->name);
+ filesTable->item(row, vcSize)->setText(FileSizeStr(path));
+}
+
+// There is a button 'Open videos dir', so it is possible that user will open
+// this dir and rename/delete some files there, so we should handle this.
+void PageVideos::updateFileList(const QString & path)
+{
+ // mark all files as non seen
+ int numRows = filesTable->rowCount();
+ for (int i = 0; i < numRows; i++)
+ nameItem(i)->seen = false;
+
+ QStringList files = QDir(path).entryList(QDir::Files);
+ foreach (const QString & name, files)
+ {
+ int row = -1;
+ foreach (QTableWidgetItem * item, filesTable->findItems(name, Qt::MatchExactly))
+ {
+ if (item->type() != QTableWidgetItem::UserType || !((VideoItem*)item)->ready())
+ continue;
+ row = item->row();
+ break;
+ }
+ if (row == -1)
+ row = appendRow(name);
+ VideoItem * item = nameItem(row);
+ item->seen = true;
+ item->desc = "";
+ updateSize(row);
+ }
+
+ // remove all non seen files
+ for (int i = 0; i < filesTable->rowCount();)
+ {
+ VideoItem * item = nameItem(i);
+ if (item->ready() && !item->seen)
+ filesTable->removeRow(i);
+ else
+ i++;
+ }
+}
+
+void PageVideos::addRecorder(HWRecorder* pRecorder)
+{
+ int row = appendRow(pRecorder->name);
+ VideoItem * item = nameItem(row);
+ item->pRecorder = pRecorder;
+ pRecorder->item = item;
+
+ // add progress bar
+ QProgressBar * progressBar = new QProgressBar(filesTable);
+ progressBar->setMinimum(0);
+ progressBar->setMaximum(10000);
+ progressBar->setValue(0);
+ connect(pRecorder, SIGNAL(onProgress(float)), this, SLOT(updateProgress(float)));
+ connect(pRecorder, SIGNAL(encodingFinished(bool)), this, SLOT(encodingFinished(bool)));
+ filesTable->setCellWidget(row, vcProgress, progressBar);
+
+ numRecorders++;
+}
+
+void PageVideos::setProgress(int row, VideoItem* item, float value)
+{
+ QProgressBar * progressBar = (QProgressBar*)filesTable->cellWidget(row, vcProgress);
+ progressBar->setValue(value*10000);
+ progressBar->setFormat(QString("%1%").arg(value*100, 0, 'f', 2));
+ item->progress = value;
+}
+
+void PageVideos::updateProgress(float value)
+{
+ HWRecorder * pRecorder = (HWRecorder*)sender();
+ VideoItem * item = pRecorder->item;
+ int row = filesTable->row(item);
+
+ // update file size every percent
+ if (value - item->lastSizeUpdate > 0.01)
+ {
+ updateSize(row);
+ item->lastSizeUpdate = value;
+ }
+
+ setProgress(row, item, value);
+}
+
+void PageVideos::encodingFinished(bool success)
+{
+ numRecorders--;
+
+ HWRecorder * pRecorder = (HWRecorder*)sender();
+ VideoItem * item = (VideoItem*)pRecorder->item;
+ int row = filesTable->row(item);
+
+ if (success)
+ {
+ // move file to destination
+ success = cfgdir->rename("VideoTemp/" + pRecorder->name, "Videos/" + item->name);
+ if (!success)
+ {
+ // unable to rename for some reason (maybe user entered incorrect name);
+ // try to use temp name instead.
+ success = cfgdir->rename("VideoTemp/" + pRecorder->name, "Videos/" + pRecorder->name);
+ if (success)
+ setName(item, pRecorder->name);
+ }
+ }
+
+ if (!success)
+ {
+ filesTable->removeRow(row);
+ return;
+ }
+
+ filesTable->setCellWidget(row, vcProgress, NULL); // remove progress bar
+ item->pRecorder = NULL;
+ updateSize(row);
+ updateDescription();
+}
+
+void PageVideos::cellDoubleClicked(int row, int column)
+{
+ play(row);
+}
+
+void PageVideos::cellChanged(int row, int column)
+{
+ // user can only edit name
+ if (column != vcName || nameChangedFromCode)
+ return;
+
+ // user has edited filename, so we should rename the file
+ VideoItem * item = nameItem(row);
+ QString oldName = item->name;
+ QString newName = item->text();
+ if (!newName.contains('.')) // user forgot an extension
+ {
+ // restore old extension
+ int pt = oldName.lastIndexOf('.');
+ if (pt != -1)
+ {
+ newName += oldName.right(oldName.length() - pt);
+ setName(item, newName);
+ }
+ }
+#ifdef Q_WS_WIN
+ // there is a bug in qt, QDir::rename() doesn't fail on such names but damages files
+ if (newName.contains(QRegExp("[\"*:<>?\/|]")))
+ {
+ setName(item, oldName);
+ return;
+ }
+#endif
+ if (item->ready() && !cfgdir->rename("Videos/" + oldName, "Videos/" + newName))
+ {
+ // unable to rename for some reason (maybe user entered incorrect name),
+ // therefore restore old name in cell
+ setName(item, oldName);
+ return;
+ }
+ item->name = newName;
+ updateDescription();
+}
+
+void PageVideos::setName(VideoItem * item, const QString & newName)
+{
+ nameChangedFromCode = true;
+ item->setText(newName);
+ nameChangedFromCode = false;
+ item->name = newName;
+}
+
+int PageVideos::appendRow(const QString & name)
+{
+ int row = filesTable->rowCount();
+ filesTable->setRowCount(row+1);
+
+ // add 'name' item
+ QTableWidgetItem * item = new VideoItem(name);
+ item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
+ nameChangedFromCode = true;
+ filesTable->setItem(row, vcName, item);
+ nameChangedFromCode = false;
+
+ // add 'size' item
+ item = new QTableWidgetItem();
+ item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
+ item->setTextAlignment(Qt::AlignRight);
+ filesTable->setItem(row, vcSize, item);
+
+ // add 'progress' item
+ item = new QTableWidgetItem();
+ item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
+ filesTable->setItem(row, vcProgress, item);
+
+ return row;
+}
+
+VideoItem* PageVideos::nameItem(int row)
+{
+ return (VideoItem*)filesTable->item(row, vcName);
+}
+
+void PageVideos::clearThumbnail()
+{
+ // add empty (transparent) image for proper sizing
+ QPixmap pic(ThumbnailSize);
+ pic.fill(QColor(0,0,0,0));
+ labelThumbnail->setPixmap(pic);
+}
+
+void PageVideos::updateDescription()
+{
+ VideoItem * item = nameItem(filesTable->currentRow());
+ if (!item)
+ {
+ // nothing is selected => clear description and return
+ labelDesc->clear();
+ clearThumbnail();
+ btnPlay->setEnabled(false);
+ btnDelete->setEnabled(false);
+ btnToYouTube->setEnabled(false);
+ return;
+ }
+
+ btnPlay->setEnabled(item->ready());
+ btnToYouTube->setEnabled(item->ready());
+ btnDelete->setEnabled(true);
+ btnDelete->setText(item->ready()? QPushButton::tr("Delete") : QPushButton::tr("Cancel"));
+ btnToYouTube->setText(item->pUploading? QPushButton::tr("Cancel uploading") : QPushButton::tr("Upload to YouTube"));
+
+ // construct string with desctiption of this file to display it
+ QString desc = item->name + "\n\n";
+
+ if (!item->ready())
+ desc += tr("(in progress...)");
+ else
+ {
+ QString path = item->path();
+ desc += tr("Date: ") + QFileInfo(path).created().toString(Qt::DefaultLocaleLongDate) + '\n';
+ desc += tr("Size: ") + FileSizeStr(path) + '\n';
+ if (item->desc.isEmpty())
+ {
+ // Extract description from file;
+ // It will contain duration, resolution, etc and also comment added by hwengine.
+ item->desc = LibavIteraction::instance().getFileInfo(path);
+
+ // extract prefix (original name) from description (it is enclosed in prefix[???]prefix)
+ int prefixBegin = item->desc.indexOf("prefix[");
+ int prefixEnd = item->desc.indexOf("]prefix");
+ if (prefixBegin != -1 && prefixEnd != -1)
+ {
+ item->prefix = item->desc.mid(prefixBegin + 7, prefixEnd - (prefixBegin + 7));
+ item->desc.remove(prefixBegin, prefixEnd + 7 - prefixBegin);
+ }
+ }
+ desc += item->desc + '\n';
+ }
+
+ if (item->prefix.isEmpty())
+ {
+ // try to extract prefix from file name instead
+ if (item->ready())
+ item->prefix = item->name;
+ else
+ item->prefix = item->pRecorder->name;
+
+ // remove extension
+ int pt = item->prefix.lastIndexOf('.');
+ if (pt != -1)
+ item->prefix.truncate(pt);
+ }
+
+ if (item->ready() && item->uploadUrl.isEmpty())
+ {
+ // try to load url from file
+ QFile * file = new QFile(cfgdir->absoluteFilePath("VideoTemp/" + item->prefix + "-url.txt"), this);
+ if (!file->open(QIODevice::ReadOnly))
+ item->uploadUrl = "no";
+ else
+ {
+ QByteArray data = file->readAll();
+ file->close();
+ item->uploadUrl = QString::fromUtf8(data.data());
+ }
+ }
+ if (item->uploadUrl != "no")
+ desc += QString("<a href=\"%1\" style=\"color: white;\">%1</a>").arg(item->uploadUrl);
+ desc.replace("\n", "<br/>");
+
+ labelDesc->setText(desc);
+
+ if (!item->prefix.isEmpty())
+ {
+ QString thumbName = cfgdir->absoluteFilePath("VideoTemp/" + item->prefix);
+ QPixmap pic;
+ if (pic.load(thumbName + ".png") || pic.load(thumbName + ".bmp"))
+ {
+ if (pic.height()*ThumbnailSize.width() > pic.width()*ThumbnailSize.height())
+ pic = pic.scaledToWidth(ThumbnailSize.width());
+ else
+ pic = pic.scaledToHeight(ThumbnailSize.height());
+ labelThumbnail->setPixmap(pic);
+ }
+ else
+ clearThumbnail();
+ }
+}
+
+// user selected another cell, so we should change description
+void PageVideos::currentCellChanged(int row, int column, int previousRow, int previousColumn)
+{
+ updateDescription();
+}
+
+// open video file in external media player
+void PageVideos::play(int row)
+{
+ VideoItem * item = nameItem(row);
+ if (item && item->ready())
+ QDesktopServices::openUrl(QUrl("file:///" + QDir::toNativeSeparators(item->path())));
+}
+
+void PageVideos::playSelectedFile()
+{
+ int index = filesTable->currentRow();
+ if (index != -1)
+ play(index);
+}
+
+void PageVideos::deleteSelectedFiles()
+{
+ int index = filesTable->currentRow();
+ if (index == -1)
+ return;
+
+ VideoItem * item = nameItem(index);
+ if (!item)
+ return;
+
+ // ask user if (s)he is serious
+ if (QMessageBox::question(this,
+ tr("Are you sure?"),
+ tr("Do you really want do remove %1?").arg(item->name),
+ QMessageBox::Yes | QMessageBox::No)
+ != QMessageBox::Yes)
+ return;
+
+ // remove
+ if (!item->ready())
+ item->pRecorder->deleteLater();
+ else
+ cfgdir->remove("Videos/" + item->name);
+
+// this code is for removing several files when multiple selection is enabled
+#if 0
+ QList<QTableWidgetItem*> items = filesTable->selectedItems();
+ int num = items.size() / vcNumColumns;
+ if (num == 0)
+ return;
+
+ // ask user if (s)he is serious
+ if (QMessageBox::question(this,
+ tr("Are you sure?"),
+ tr("Do you really want do remove %1 file(s)?").arg(num),
+ QMessageBox::Yes | QMessageBox::No)
+ != QMessageBox::Yes)
+ return;
+
+ // remove
+ foreach (QTableWidgetItem * witem, items)
+ {
+ if (witem->type() != QTableWidgetItem::UserType)
+ continue;
+ VideoItem * item = (VideoItem*)witem;
+ if (!item->ready())
+ item->pRecorder->deleteLater();
+ else
+ cfgdir->remove("Videos/" + item->name);
+ }
+#endif
+}
+
+void PageVideos::keyPressEvent(QKeyEvent * pEvent)
+{
+ if (filesTable->hasFocus())
+ {
+ if (pEvent->key() == Qt::Key_Delete)
+ {
+ deleteSelectedFiles();
+ return;
+ }
+ if (pEvent->key() == Qt::Key_Enter) // doesn't work
+ {
+ playSelectedFile();
+ return;
+ }
+ }
+ AbstractPage::keyPressEvent(pEvent);
+}
+
+void PageVideos::openVideosDirectory()
+{
+ QString path = QDir::toNativeSeparators(cfgdir->absolutePath() + "/Videos");
+ QDesktopServices::openUrl(QUrl("file:///" + path));
+}
+
+// clear VideoTemp directory (except for thumbnails and upload links)
+void PageVideos::clearTemp()
+{
+ QDir temp(cfgdir->absolutePath() + "/VideoTemp");
+ QStringList files = temp.entryList(QDir::Files);
+ foreach (const QString& file, files)
+ {
+ if (!file.endsWith(".bmp") && !file.endsWith(".png") && !file.endsWith("-url.txt"))
+ temp.remove(file);
+ }
+}
+
+bool PageVideos::tryQuit(HWForm * form)
+{
+ bool quit = true;
+ if (numRecorders != 0 || numUploads != 0)
+ {
+ // ask user what to do - abort or wait
+ HWAskQuitDialog * askd = new HWAskQuitDialog(this, form);
+ askd->deleteLater();
+ quit = askd->exec();
+ }
+ if (quit)
+ clearTemp();
+ return quit;
+}
+
+// returns multi-line string with list of videos in progress
+/* it will look like this:
+foo.avi (15.21% - encoding)
+bar.avi (18.21% - uploading)
+*/
+QString PageVideos::getVideosInProgress()
+{
+ QString list = "";
+ int count = filesTable->rowCount();
+ for (int i = 0; i < count; i++)
+ {
+ VideoItem * item = nameItem(i);
+ QString process;
+ if (!item->ready())
+ process = tr("encoding");
+ else if (item->pUploading)
+ process = tr("uploading");
+ else
+ continue;
+ float progress = 100*item->progress;
+ if (progress > 99.99)
+ progress = 99.99; // displaying 100% may be confusing
+ list += item->name + " (" + QString::number(progress, 'f', 2) + "% - " + process + ")\n";
+ }
+ return list;
+}
+
+void PageVideos::startEncoding(const QByteArray & record)
+{
+ QDir videoTempDir(cfgdir->absolutePath() + "/VideoTemp/");
+ QStringList files = videoTempDir.entryList(QStringList("*.txtout"), QDir::Files);
+ foreach (const QString & str, files)
+ {
+ QString prefix = str;
+ prefix.chop(7); // remove ".txtout"
+ videoTempDir.rename(prefix + ".txtout", prefix + ".txtin"); // rename this file to not open it twice
+
+ HWRecorder* pRecorder = new HWRecorder(config, prefix);
+
+ if (!record.isEmpty())
+ pRecorder->EncodeVideo(record);
+ else
+ {
+ // this is for videos recorded from demos which were executed directly (without frontend)
+ QFile demofile(videoTempDir.absoluteFilePath(prefix + ".hwd"));
+ if (!demofile.open(QIODevice::ReadOnly))
+ continue;
+ QByteArray demo = demofile.readAll();
+ if (demo.isEmpty())
+ continue;
+ pRecorder->EncodeVideo(demo);
+ }
+ addRecorder(pRecorder);
+ }
+}
+
+VideoItem * PageVideos::itemFromReply(QNetworkReply* reply, int & row)
+{
+ VideoItem * item = NULL;
+ int count = filesTable->rowCount();
+ // find corresponding item (maybe there is a better way to implement this?)
+ for (int i = 0; i < count; i++)
+ {
+ item = nameItem(i);
+ if (item->pUploading == reply)
+ {
+ row = i;
+ break;
+ }
+ }
+ return item;
+}
+
+void PageVideos::uploadProgress(qint64 bytesSent, qint64 bytesTotal)
+{
+ QNetworkReply* reply = (QNetworkReply*)sender();
+ int row;
+ VideoItem * item = itemFromReply(reply, row);
+ setProgress(row, item, bytesSent*1.0/bytesTotal);
+}
+
+void PageVideos::uploadFinished()
+{
+ QNetworkReply* reply = (QNetworkReply*)sender();
+ reply->deleteLater();
+
+ int row;
+ VideoItem * item = itemFromReply(reply, row);
+ if (!item)
+ return;
+
+ item->pUploading = NULL;
+
+ // extract video id from reply
+ QString videoid;
+ QXmlStreamReader xml(reply);
+ while (!xml.atEnd())
+ {
+ xml.readNext();
+ if (xml.qualifiedName() == "yt:videoid")
+ {
+ videoid = xml.readElementText();
+ break;
+ }
+ }
+
+ if (!videoid.isEmpty())
+ {
+ item->uploadUrl = "http://youtu.be/" + videoid;
+ updateDescription();
+
+ // save url in file
+ QFile * file = new QFile(cfgdir->absoluteFilePath("VideoTemp/" + item->prefix + "-url.txt"), this);
+ if (file->open(QIODevice::WriteOnly))
+ {
+ file->write(item->uploadUrl.toUtf8());
+ file->close();
+ }
+ }
+
+ filesTable->setCellWidget(row, vcProgress, NULL); // remove progress bar
+ numUploads--;
+}
+
+// this will protect saved youtube password from those who cannot read source code
+static QString protectPass(QString str)
+{
+ QByteArray array = str.toUtf8();
+ for (int i = 0; i < array.size(); i++)
+ array[i] = array[i] ^ 0xC4 ^ i;
+ array = array.toBase64();
+ return QString::fromAscii(array.data());
+}
+
+static QString unprotectPass(QString str)
+{
+ QByteArray array = QByteArray::fromBase64(str.toAscii());
+ for (int i = 0; i < array.size(); i++)
+ array[i] = array[i] ^ 0xC4 ^ i;
+ return QString::fromUtf8(array);
+}
+
+void PageVideos::uploadToYouTube()
+{
+ int row = filesTable->currentRow();
+ VideoItem * item = nameItem(row);
+
+ if (item->pUploading)
+ {
+ if (QMessageBox::question(this,
+ tr("Are you sure?"),
+ tr("Do you really want do cancel uploading %1?").arg(item->name),
+ QMessageBox::Yes | QMessageBox::No)
+ != QMessageBox::Yes)
+ return;
+ item->pUploading->deleteLater();
+ filesTable->setCellWidget(row, vcProgress, NULL); // remove progress bar
+ numUploads--;
+ return;
+ }
+
+ if (!netManager)
+ netManager = new QNetworkAccessManager(this);
+
+ HWUploadVideoDialog* dlg = new HWUploadVideoDialog(this, item->name, netManager);
+ dlg->deleteLater();
+ if (config->value("youtube/save").toBool())
+ {
+ dlg->cbSave->setChecked(true);
+ dlg->leAccount->setText(config->value("youtube/name").toString());
+ dlg->lePassword->setText(unprotectPass(config->value("youtube/pswd").toString()));
+ }
+
+ bool result = dlg->exec();
+
+ if (dlg->cbSave->isChecked())
+ {
+ config->setValue("youtube/save", true);
+ config->setValue("youtube/name", dlg->leAccount->text());
+ config->setValue("youtube/pswd", protectPass(dlg->lePassword->text()));
+ }
+ else
+ {
+ config->setValue("youtube/save", false);
+ config->setValue("youtube/name", "");
+ config->setValue("youtube/pswd", "");
+ }
+
+ if (!result)
+ return;
+
+ QNetworkRequest request(QUrl(dlg->location));
+ request.setRawHeader("Content-Type", "application/octet-stream");
+
+ QFile * file = new QFile(item->path(), this);
+ if (!file->open(QIODevice::ReadOnly))
+ return;
+
+ // add progress bar
+ QProgressBar * progressBar = new QProgressBar(filesTable);
+ progressBar->setMinimum(0);
+ progressBar->setMaximum(10000);
+ progressBar->setValue(0);
+ // make it different from progress-bar used during encoding (use blue color)
+ progressBar->setStyleSheet("* {color: #00ccff; selection-background-color: #00ccff;}" );
+ filesTable->setCellWidget(row, vcProgress, progressBar);
+
+ QNetworkReply* reply = netManager->put(request, file);
+ file->setParent(reply); // automatically close file when needed
+ item->pUploading = reply;
+ connect(reply, SIGNAL(uploadProgress(qint64, qint64)), this, SLOT(uploadProgress(qint64, qint64)));
+ connect(reply, SIGNAL(finished()), this, SLOT(uploadFinished()));
+ numUploads++;
+
+ updateDescription();
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/ui/page/pagevideos.h Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,125 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * 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; version 2 of the License
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+
+#ifndef PAGE_VIDEOS_H
+#define PAGE_VIDEOS_H
+
+#include "AbstractPage.h"
+
+class QNetworkAccessManager;
+class QNetworkReply;
+class GameUIConfig;
+class HWRecorder;
+class VideoItem;
+class HWForm;
+
+class PageVideos : public AbstractPage
+{
+ Q_OBJECT
+
+ public:
+ PageVideos(QWidget* parent = 0);
+
+ QSpinBox *framerateBox;
+ QLineEdit *widthEdit;
+ QLineEdit *heightEdit;
+ QCheckBox *checkUseGameRes;
+ QCheckBox *checkRecordAudio;
+
+ QString format()
+ { return comboAVFormats->itemData(comboAVFormats->currentIndex()).toString(); }
+
+ QString videoCodec()
+ { return comboVideoCodecs->itemData(comboVideoCodecs->currentIndex()).toString(); }
+
+ QString audioCodec()
+ { return comboAudioCodecs->itemData(comboAudioCodecs->currentIndex()).toString(); }
+
+ void setDefaultCodecs();
+ bool tryCodecs(const QString & format, const QString & vcodec, const QString & acodec);
+ void addRecorder(HWRecorder* pRecorder);
+ bool tryQuit(HWForm *form);
+ QString getVideosInProgress(); // get multi-line string with list of videos in progress
+ void startEncoding(const QByteArray & record = QByteArray());
+ void init(GameUIConfig * config);
+
+ private:
+ // virtuals from AbstractPage
+ QLayout * bodyLayoutDefinition();
+ QLayout * footerLayoutDefinition();
+ void connectSignals();
+
+ // virtual from QWidget
+ void keyPressEvent(QKeyEvent * pEvent);
+
+ void setName(VideoItem * item, const QString & newName);
+ void updateSize(int row);
+ int appendRow(const QString & name);
+ VideoItem* nameItem(int row);
+ void play(int row);
+ void updateDescription();
+ void clearTemp();
+ void clearThumbnail();
+ void setProgress(int row, VideoItem* item, float value);
+ VideoItem * itemFromReply(QNetworkReply* reply, int & row);
+
+ GameUIConfig * config;
+ QNetworkAccessManager* netManager;
+
+ // options group
+ QComboBox *comboAVFormats;
+ QComboBox *comboVideoCodecs;
+ QComboBox *comboAudioCodecs;
+ QPushButton *btnDefaults;
+
+ // file list group
+ QTableWidget *filesTable;
+ QPushButton *btnOpenDir;
+
+ // description group
+ QPushButton *btnPlay, *btnDelete, *btnToYouTube;
+ QLabel *labelDesc;
+ QLabel *labelThumbnail;
+
+ // this flag is used to distinguish if cell was changed from code or by user
+ // (in signal cellChanged)
+ bool nameChangedFromCode;
+
+ int numRecorders, numUploads;
+
+ private slots:
+ void changeAVFormat(int index);
+ void changeUseGameRes(int state);
+ void changeRecordAudio(int state);
+ void setDefaultOptions();
+ void encodingFinished(bool success);
+ void updateProgress(float value);
+ void cellDoubleClicked(int row, int column);
+ void cellChanged(int row, int column);
+ void currentCellChanged(int row, int column, int previousRow, int previousColumn);
+ void playSelectedFile();
+ void deleteSelectedFiles();
+ void openVideosDirectory();
+ void updateFileList(const QString & path);
+ void uploadToYouTube();
+ void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
+ void uploadFinished();
+};
+
+#endif // PAGE_VIDEOS_H
--- a/QTfrontend/ui_hwform.cpp Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/ui_hwform.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -46,6 +46,7 @@
#include "pagegamestats.h"
#include "pageplayrecord.h"
#include "pagedata.h"
+#include "pagevideos.h"
#include "hwconsts.h"
void Ui_HWForm::setupUi(HWForm *HWForm)
@@ -145,4 +146,7 @@
pageFeedback = new PageFeedback();
Pages->addWidget(pageFeedback);
+
+ pageVideos = new PageVideos();
+ Pages->addWidget(pageVideos);
}
--- a/QTfrontend/ui_hwform.h Mon Aug 27 16:52:50 2012 -0400
+++ b/QTfrontend/ui_hwform.h Tue Aug 28 20:30:57 2012 +0400
@@ -43,6 +43,7 @@
class PageAdmin;
class PageNetType;
class PageDrawMap;
+class PageVideos;
class QStackedLayout;
class QFont;
class QWidget;
@@ -78,6 +79,7 @@
PageNetType *pageNetType;
PageCampaign *pageCampaign;
PageDrawMap *pageDrawMap;
+ PageVideos *pageVideos;
QStackedLayout *Pages;
QFont *font14;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/util/libav_iteraction.cpp Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,327 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * 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; version 2 of the License
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#define __STDC_CONSTANT_MACROS
+extern "C"
+{
+#include "libavformat/avformat.h"
+}
+#include <QVector>
+#include <QList>
+#include <QMessageBox>
+#include <QComboBox>
+
+#include "libav_iteraction.h"
+#include "HWApplication.h"
+
+struct Codec
+{
+ CodecID id;
+ bool isAudio;
+ QString shortName; // used for identification
+ QString longName; // used for displaying to user
+ bool isRecomended;
+};
+
+struct Format
+{
+ QString shortName;
+ QString longName;
+ bool isRecomended;
+ QString extension;
+ QVector<Codec*> codecs;
+};
+
+QList<Codec> codecs;
+QMap<QString,Format> formats;
+
+LibavIteraction & LibavIteraction::instance()
+{
+ static LibavIteraction instance;
+ return instance;
+}
+
+// test if given format supports given codec
+bool FormatQueryCodec(AVOutputFormat *ofmt, enum CodecID codec_id)
+{
+#if LIBAVFORMAT_VERSION_MAJOR >= 54
+ return avformat_query_codec(ofmt, codec_id, FF_COMPLIANCE_NORMAL) == 1;
+#else
+ if (ofmt->codec_tag)
+ return !!av_codec_get_tag(ofmt->codec_tag, codec_id);
+ return codec_id == ofmt->video_codec || codec_id == ofmt->audio_codec;
+#endif
+}
+
+LibavIteraction::LibavIteraction()
+{
+ // initialize libav and register all codecs and formats
+ av_register_all();
+
+ // get list of all codecs
+ AVCodec* pCodec = NULL;
+ while (pCodec = av_codec_next(pCodec))
+ {
+#if LIBAVCODEC_VERSION_MAJOR >= 54
+ if (!av_codec_is_encoder(pCodec))
+#else
+ if (!pCodec->encode)
+#endif
+ continue;
+
+ if (pCodec->type != AVMEDIA_TYPE_VIDEO && pCodec->type != AVMEDIA_TYPE_AUDIO)
+ continue;
+
+ // this encoders seems to be buggy
+ if (strcmp(pCodec->name, "rv10") == 0 || strcmp(pCodec->name, "rv20") == 0)
+ continue;
+
+ // doesn't support stereo sound
+ if (strcmp(pCodec->name, "real_144") == 0)
+ continue;
+
+ if (!pCodec->long_name || strlen(pCodec->long_name) == 0)
+ continue;
+
+ if (pCodec->type == AVMEDIA_TYPE_VIDEO)
+ {
+ if (pCodec->supported_framerates != NULL)
+ continue;
+
+ // check if codec supports yuv 4:2:0 format
+ if (!pCodec->pix_fmts)
+ continue;
+ bool yuv420Supported = false;
+ for (const PixelFormat* pfmt = pCodec->pix_fmts; *pfmt != -1; pfmt++)
+ if (*pfmt == PIX_FMT_YUV420P)
+ {
+ yuv420Supported = true;
+ break;
+ }
+ if (!yuv420Supported)
+ continue;
+ }
+ if (pCodec->type == AVMEDIA_TYPE_AUDIO)
+ {
+ // check if codec supports signed 16-bit format
+ if (!pCodec->sample_fmts)
+ continue;
+ bool s16Supported = false;
+ for (const AVSampleFormat* pfmt = pCodec->sample_fmts; *pfmt != -1; pfmt++)
+ if (*pfmt == AV_SAMPLE_FMT_S16)
+ {
+ s16Supported = true;
+ break;
+ }
+ if (!s16Supported)
+ continue;
+ }
+ // add codec to list of codecs
+ codecs.push_back(Codec());
+ Codec & codec = codecs.back();
+ codec.id = pCodec->id;
+ codec.isAudio = pCodec->type == AVMEDIA_TYPE_AUDIO;
+ codec.shortName = pCodec->name;
+ codec.longName = pCodec->long_name;
+
+ codec.isRecomended = false;
+ if (strcmp(pCodec->name, "libx264") == 0)
+ {
+ codec.longName = "H.264/MPEG-4 Part 10 AVC (x264)";
+ codec.isRecomended = true;
+ }
+ else if (strcmp(pCodec->name, "libxvid") == 0)
+ {
+ codec.longName = "MPEG-4 Part 2 (Xvid)";
+ codec.isRecomended = true;
+ }
+ else if (strcmp(pCodec->name, "libmp3lame") == 0)
+ {
+ codec.longName = "MP3 (MPEG audio layer 3) (LAME)";
+ codec.isRecomended = true;
+ }
+ else
+ codec.longName = pCodec->long_name;
+
+ if (strcmp(pCodec->name, "mpeg4") == 0 || strcmp(pCodec->name, "ac3_fixed") == 0)
+ codec.isRecomended = true;
+
+ // FIXME: remove next line
+ //codec.longName += QString(" (%1)").arg(codec.shortName);
+ }
+
+ // get list of all formats
+ AVOutputFormat* pFormat = NULL;
+ while (pFormat = av_oformat_next(pFormat))
+ {
+ if (!pFormat->extensions)
+ continue;
+
+ // skip some strange formats to not confuse users
+ if (strstr(pFormat->long_name, "raw"))
+ continue;
+
+ Format format;
+ bool hasVideoCodec = false;
+ for (QList<Codec>::iterator codec = codecs.begin(); codec != codecs.end(); ++codec)
+ {
+ if (!FormatQueryCodec(pFormat, codec->id))
+ continue;
+ format.codecs.push_back(&*codec);
+ if (!codec->isAudio)
+ hasVideoCodec = true;
+ }
+ if (!hasVideoCodec)
+ continue;
+
+ QString ext(pFormat->extensions);
+ ext.truncate(strcspn(pFormat->extensions, ","));
+ format.extension = ext;
+ format.shortName = pFormat->name;
+ format.longName = QString("%1 (*.%2)").arg(pFormat->long_name).arg(ext);
+
+ // FIXME: remove next line
+ //format.longName += QString(" (%1)").arg(format.shortName);
+
+ format.isRecomended = strcmp(pFormat->name, "mp4") == 0 || strcmp(pFormat->name, "avi") == 0;
+
+ formats[pFormat->name] = format;
+ }
+}
+
+void LibavIteraction::fillFormats(QComboBox * pFormats)
+{
+ // first insert recomended formats
+ foreach(const Format & format, formats)
+ if (format.isRecomended)
+ pFormats->addItem(format.longName, format.shortName);
+
+ // remember where to place separator between recomended and other formats
+ int sep = pFormats->count();
+
+ // insert remaining formats
+ foreach(const Format & format, formats)
+ if (!format.isRecomended)
+ pFormats->addItem(format.longName, format.shortName);
+
+ // insert separator if necessary
+ if (sep != 0 && sep != pFormats->count())
+ pFormats->insertSeparator(sep);
+}
+
+void LibavIteraction::fillCodecs(const QString & fmt, QComboBox * pVCodecs, QComboBox * pACodecs)
+{
+ Format & format = formats[fmt];
+
+ // first insert recomended codecs
+ foreach(Codec * codec, format.codecs)
+ {
+ if (codec->isRecomended)
+ {
+ if (codec->isAudio)
+ pACodecs->addItem(codec->longName, codec->shortName);
+ else
+ pVCodecs->addItem(codec->longName, codec->shortName);
+ }
+ }
+
+ // remember where to place separators between recomended and other codecs
+ int vsep = pVCodecs->count();
+ int asep = pACodecs->count();
+
+ // insert remaining codecs
+ foreach(Codec * codec, format.codecs)
+ {
+ if (!codec->isRecomended)
+ {
+ if (codec->isAudio)
+ pACodecs->addItem(codec->longName, codec->shortName);
+ else
+ pVCodecs->addItem(codec->longName, codec->shortName);
+ }
+ }
+
+ // insert separators if necessary
+ if (vsep != 0 && vsep != pVCodecs->count())
+ pVCodecs->insertSeparator(vsep);
+ if (asep != 0 && asep != pACodecs->count())
+ pACodecs->insertSeparator(asep);
+}
+
+QString LibavIteraction::getExtension(const QString & format)
+{
+ return formats[format].extension;
+}
+
+QString tr(QString a)
+{
+ return a;
+}
+
+// get information abaout file (duration, resolution etc) in multiline string
+QString LibavIteraction::getFileInfo(const QString & filepath)
+{
+ AVFormatContext* pContext = NULL;
+ QByteArray utf8path = filepath.toUtf8();
+ if (avformat_open_input(&pContext, utf8path.data(), NULL, NULL) < 0)
+ return "";
+#if LIBAFORMAT_VERSION_MAJOR < 54
+ if (av_find_stream_info(pContext) < 0)
+#else
+ if (avformat_find_stream_info(pContext, NULL) < 0)
+#endif
+ return "";
+
+ int s = float(pContext->duration)/AV_TIME_BASE;
+ QString desc = QString(tr("Duration: %1m %2s\n")).arg(s/60).arg(s%60);
+ for (int i = 0; i < (int)pContext->nb_streams; i++)
+ {
+ AVStream* pStream = pContext->streams[i];
+ if (!pStream)
+ continue;
+ AVCodecContext* pCodec = pContext->streams[i]->codec;
+ if (!pCodec)
+ continue;
+
+ if (pCodec->codec_type == AVMEDIA_TYPE_VIDEO)
+ {
+ desc += QString(tr("Video: %1x%2, ")).arg(pCodec->width).arg(pCodec->height);
+ if (pStream->avg_frame_rate.den)
+ {
+ float fps = float(pStream->avg_frame_rate.num)/pStream->avg_frame_rate.den;
+ desc += QString(tr("%1 fps, ")).arg(fps, 0, 'f', 2);
+ }
+ }
+ else if (pCodec->codec_type == AVMEDIA_TYPE_AUDIO)
+ desc += tr("Audio: ");
+ else
+ continue;
+ AVCodec* pDecoder = avcodec_find_decoder(pCodec->codec_id);
+ desc += pDecoder? pDecoder->name : "unknown";
+ desc += "\n";
+ }
+ AVDictionaryEntry* pComment = av_dict_get(pContext->metadata, "comment", NULL, 0);
+ if (pComment)
+ desc += QString("\n") + pComment->value;
+#if LIBAFORMAT_VERSION_MAJOR < 54
+ av_close_input_file(pContext);
+#else
+ avformat_close_input(&pContext);
+#endif
+ return desc;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/util/libav_iteraction.h Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,49 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * 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; version 2 of the License
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#ifndef LIBAV_ITERACTION
+#define LIBAV_ITERACTION
+
+#include <QComboBox>
+
+/**
+ * @brief Class for interacting with ffmpeg/libav libraries
+ *
+ * @see <a href="http://en.wikipedia.org/wiki/Singleton_pattern">singleton pattern</a>
+ */
+class LibavIteraction
+{
+ LibavIteraction();
+
+public:
+
+ static LibavIteraction & instance();
+
+ // fill combo box with known file formats
+ void fillFormats(QComboBox * pFormats);
+
+ // fill combo boxes with known codecs for given formats
+ void fillCodecs(const QString & format, QComboBox * pVCodecs, QComboBox * pACodecs);
+
+ QString getExtension(const QString & format);
+
+ // get information about file (duration, resolution etc) in multiline string
+ QString getFileInfo(const QString & filepath);
+};
+
+#endif // LIBAV_ITERACTION
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmake_modules/FindFFMPEG.cmake Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,79 @@
+# - Try to find ffmpeg libraries (libavcodec, libavformat and libavutil)
+# Once done this will define
+#
+# FFMPEG_FOUND - system has ffmpeg or libav
+# FFMPEG_INCLUDE_DIR - the ffmpeg include directory
+# FFMPEG_LIBRARIES - Link these to use ffmpeg
+# FFMPEG_LIBAVCODEC
+# FFMPEG_LIBAVFORMAT
+# FFMPEG_LIBAVUTIL
+#
+# Copyright (c) 2008 Andreas Schneider <mail@cynapses.org>
+# Modified for other libraries by Lasse Kärkkäinen <tronic>
+# Modified for Hedgewars by Stepik777
+#
+# Redistribution and use is allowed according to the terms of the New
+# BSD license.
+#
+
+if (FFMPEG_LIBRARIES AND FFMPEG_INCLUDE_DIR)
+ # in cache already
+ set(FFMPEG_FOUND TRUE)
+else (FFMPEG_LIBRARIES AND FFMPEG_INCLUDE_DIR)
+ # use pkg-config to get the directories and then use these values
+ # in the FIND_PATH() and FIND_LIBRARY() calls
+ find_package(PkgConfig)
+ if (PKG_CONFIG_FOUND)
+ pkg_check_modules(_FFMPEG_AVCODEC libavcodec)
+ pkg_check_modules(_FFMPEG_AVFORMAT libavformat)
+ pkg_check_modules(_FFMPEG_AVUTIL libavutil)
+ endif (PKG_CONFIG_FOUND)
+
+ find_path(FFMPEG_AVCODEC_INCLUDE_DIR
+ NAMES libavcodec/avcodec.h
+ PATHS ${_FFMPEG_AVCODEC_INCLUDE_DIRS} /usr/include /usr/local/include /opt/local/include /sw/include
+ PATH_SUFFIXES ffmpeg libav
+ )
+
+ find_library(FFMPEG_LIBAVCODEC
+ NAMES avcodec
+ PATHS ${_FFMPEG_AVCODEC_LIBRARY_DIRS} /usr/lib /usr/local/lib /opt/local/lib /sw/lib
+ )
+
+ find_library(FFMPEG_LIBAVFORMAT
+ NAMES avformat
+ PATHS ${_FFMPEG_AVFORMAT_LIBRARY_DIRS} /usr/lib /usr/local/lib /opt/local/lib /sw/lib
+ )
+
+ find_library(FFMPEG_LIBAVUTIL
+ NAMES avutil
+ PATHS ${_FFMPEG_AVUTIL_LIBRARY_DIRS} /usr/lib /usr/local/lib /opt/local/lib /sw/lib
+ )
+
+ if (FFMPEG_LIBAVCODEC AND FFMPEG_LIBAVFORMAT)
+ set(FFMPEG_FOUND TRUE)
+ endif()
+
+ if (FFMPEG_FOUND)
+ set(FFMPEG_INCLUDE_DIR ${FFMPEG_AVCODEC_INCLUDE_DIR}/..)
+
+ set(FFMPEG_LIBRARIES
+ ${FFMPEG_LIBAVCODEC}
+ ${FFMPEG_LIBAVFORMAT}
+ ${FFMPEG_LIBAVUTIL}
+ )
+
+ endif (FFMPEG_FOUND)
+
+ if (FFMPEG_FOUND)
+ if (NOT FFMPEG_FIND_QUIETLY)
+ message(STATUS "Found FFMPEG or Libav: ${FFMPEG_LIBRARIES}, ${FFMPEG_INCLUDE_DIR}")
+ endif (NOT FFMPEG_FIND_QUIETLY)
+ else (FFMPEG_FOUND)
+ if (FFMPEG_FIND_REQUIRED)
+ message(FATAL_ERROR "Could not find libavcodec or libavformat or libavutil")
+ endif (FFMPEG_FIND_REQUIRED)
+ endif (FFMPEG_FOUND)
+
+endif (FFMPEG_LIBRARIES AND FFMPEG_INCLUDE_DIR)
+
--- a/hedgewars/ArgParsers.inc Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/ArgParsers.inc Tue Aug 28 20:30:57 2012 +0400
@@ -63,6 +63,21 @@
cLocaleFName:= ParamStr(17);
end;
+{$IFDEF USE_VIDEO_RECORDING}
+procedure internalStartVideoRecordingWithParameters();
+begin
+ internalStartGameWithParameters();
+ GameType:= gmtRecord;
+ cVideoFramerateNum:= StrToInt(ParamStr(18));
+ cVideoFramerateDen:= StrToInt(ParamStr(19));
+ RecPrefix:= ParamStr(20);
+ cAVFormat:= ParamStr(21);
+ cVideoCodec:= ParamStr(22);
+ cVideoQuality:= StrToInt(ParamStr(23));
+ cAudioCodec:= ParamStr(24);
+end;
+{$ENDIF}
+
procedure setVideo(screenWidth: LongInt; screenHeight: LongInt; bitsStr: LongInt);
begin
cScreenWidth:= screenWidth;
--- a/hedgewars/CMakeLists.txt Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/CMakeLists.txt Tue Aug 28 20:30:57 2012 +0400
@@ -3,6 +3,7 @@
find_package(SDL_net)
find_package(SDL_ttf)
find_package(SDL_mixer)
+find_package(FFMPEG)
include(${CMAKE_MODULE_PATH}/FindSDL_Extras.cmake)
@@ -62,6 +63,7 @@
uTypes.pas
uUtils.pas
uVariables.pas
+ uVideoRec.pas
uVisualGears.pas
uWorld.pas
GSHandlers.inc
@@ -185,9 +187,30 @@
message(STATUS "PNG screenshots disabled per user request, using BMP format")
endif()
+if(NOT NO_VIDEOREC)
+ if(${FFMPEG_FOUND})
+ message(STATUS "Compiling with video recording")
+ include_directories(${FFMPEG_INCLUDE_DIR})
+ set(pascal_flags "-dUSE_VIDEO_RECORDING" ${pascal_flags})
+ set(LIBRARY_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH})
+ IF (WIN32)
+ # there are some problems with linking our avwrapper as static lib, so link it as shared
+ add_library(avwrapper SHARED avwrapper.c)
+ target_link_libraries(avwrapper ${FFMPEG_LIBRARIES})
+ ELSE()
+ add_library(avwrapper STATIC avwrapper.c)
+ set(pascal_flags "-k${FFMPEG_LIBAVCODEC} ${FFMPEG_LIBAVFORMAT} ${FFMPEG_LIBAVUTIL}" ${pascal_flags})
+ # set(pascal_flags "-k${LIBRARY_OUTPUT_PATH}/${CMAKE_STATIC_LIBRARY_PREFIX}avwrapper${CMAKE_STATIC_LIBRARY_SUFFIX}" ${pascal_flags})
+ ENDIF()
+ else()
+ message(STATUS "FFMPEG library not found, video recording will be disabled")
+ endif()
+else()
+ message(STATUS "Video recording disabled by user")
+endif()
+
set(fpc_flags ${noexecstack_flags} ${pascal_flags} ${hwengine_project})
-
IF(NOT APPLE)
#here is the command for standard executables or for shared library
add_custom_command(OUTPUT "${EXECUTABLE_OUTPUT_PATH}/${engine_output_name}${CMAKE_EXECUTABLE_SUFFIX}"
@@ -227,10 +250,12 @@
#this command is a workaround to some inlining issues present in older
# FreePascal versions and fixed in 2.6, That version is mandatory on OSX,
# hence the command is not needed there
-if(NOT APPLE)
- add_custom_target(ENGINECLEAN COMMAND ${CMAKE_BUILD_TOOL} "clean" "${PROJECT_BINARY_DIR}" "${hedgewars_SOURCE_DIR}/hedgewars")
- add_dependencies(${engine_output_name} ENGINECLEAN)
-endif()
+# if(NOT APPLE)
+# add_custom_target(ENGINECLEAN COMMAND ${CMAKE_BUILD_TOOL} "clean" "${PROJECT_BINARY_DIR}" "${hedgewars_SOURCE_DIR}/hedgewars")
+# add_dependencies(${engine_output_name} ENGINECLEAN)
+# endif()
install(PROGRAMS "${EXECUTABLE_OUTPUT_PATH}/${engine_output_name}${CMAKE_EXECUTABLE_SUFFIX}" DESTINATION ${target_dir})
-
+IF (WIN32)
+ install(PROGRAMS "${EXECUTABLE_OUTPUT_PATH}/${CMAKE_SHARED_LIBRARY_PREFIX}avwrapper${CMAKE_SHARED_LIBRARY_SUFFIX}" DESTINATION ${target_dir})
+ENDIF()
--- a/hedgewars/SDLh.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/SDLh.pas Tue Aug 28 20:30:57 2012 +0400
@@ -827,6 +827,8 @@
TMixMusic = record
end;
+ TPostMix = procedure(udata: pointer; stream: PByte; len: LongInt); cdecl;
+
{* SDL_net *}
TIPAddress = record
host: LongWord;
@@ -957,6 +959,9 @@
function SDL_GL_SetAttribute(attr: TSDL_GLattr; value: LongInt): LongInt; cdecl; external SDLLibName;
procedure SDL_GL_SwapBuffers; cdecl; external SDLLibName;
+procedure SDL_LockAudio; cdecl; external SDLLibName;
+procedure SDL_UnlockAudio; cdecl; external SDLLibName;
+
function SDL_NumJoysticks: LongInt; cdecl; external SDLLibName;
function SDL_JoystickName(idx: LongInt): PChar; cdecl; external SDLLibName;
function SDL_JoystickOpen(idx: LongInt): PSDL_Joystick; cdecl; external SDLLibName;
@@ -1008,6 +1013,7 @@
function Mix_OpenAudio(frequency: LongInt; format: Word; channels: LongInt; chunksize: LongInt): LongInt; cdecl; external SDL_MixerLibName;
procedure Mix_CloseAudio; cdecl; external SDL_MixerLibName;
+function Mix_QuerySpec(frequency: PLongInt; format: PWord; channels: PLongInt): LongInt; cdecl; external SDL_MixerLibName;
function Mix_Volume(channel: LongInt; volume: LongInt): LongInt; cdecl; external SDL_MixerLibName;
function Mix_SetDistance(channel: LongInt; distance: Byte): LongInt; cdecl; external SDL_MixerLibName;
@@ -1035,6 +1041,8 @@
function Mix_FadeInChannelTimed(channel: LongInt; chunk: PMixChunk; loops: LongInt; fadems: LongInt; ticks: LongInt): LongInt; cdecl; external SDL_MixerLibName;
function Mix_FadeOutChannel(channel: LongInt; fadems: LongInt): LongInt; cdecl; external SDL_MixerLibName;
+procedure Mix_SetPostMix( mix_func: TPostMix; arg: pointer); cdecl; external SDL_MixerLibName;
+
(* SDL_image *)
function IMG_Init(flags: LongInt): LongInt; {$IFDEF SDL_IMAGE_NEWER}cdecl; external SDL_ImageLibName;{$ENDIF}
procedure IMG_Quit; {$IFDEF SDL_IMAGE_NEWER}cdecl; external SDL_ImageLibName;{$ENDIF}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hedgewars/avwrapper.c Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,515 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * 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; version 2 of the License
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdarg.h>
+#include "libavformat/avformat.h"
+
+#ifndef AVIO_FLAG_WRITE
+#define AVIO_FLAG_WRITE AVIO_WRONLY
+#endif
+
+static AVFormatContext* g_pContainer;
+static AVOutputFormat* g_pFormat;
+static AVStream* g_pAStream;
+static AVStream* g_pVStream;
+static AVFrame* g_pAFrame;
+static AVFrame* g_pVFrame;
+static AVCodec* g_pACodec;
+static AVCodec* g_pVCodec;
+static AVCodecContext* g_pAudio;
+static AVCodecContext* g_pVideo;
+
+static int g_Width, g_Height;
+static uint32_t g_Frequency, g_Channels;
+static int g_VQuality;
+static AVRational g_Framerate;
+
+static FILE* g_pSoundFile;
+static int16_t* g_pSamples;
+static int g_NumSamples;
+
+/*
+Initially I wrote code for latest ffmpeg, but on Linux (Ubuntu)
+only older version is available from repository. That's why you see here
+all of this #if LIBAVCODEC_VERSION_MAJOR < 54.
+Actually, it may be possible to remove code for newer version
+and use only code for older version.
+*/
+
+#if LIBAVCODEC_VERSION_MAJOR < 54
+#define OUTBUFFER_SIZE 200000
+static uint8_t g_OutBuffer[OUTBUFFER_SIZE];
+#endif
+
+// pointer to function from hwengine (uUtils.pas)
+static void (*AddFileLogRaw)(const char* pString);
+
+static void FatalError(const char* pFmt, ...)
+{
+ const char Buffer[1024];
+ va_list VaArgs;
+
+ va_start(VaArgs, pFmt);
+ vsnprintf(Buffer, 1024, pFmt, VaArgs);
+ va_end(VaArgs);
+
+ AddFileLogRaw("Error in av-wrapper: ");
+ AddFileLogRaw(Buffer);
+ AddFileLogRaw("\n");
+ exit(1);
+}
+
+// Function to be called from libav for logging.
+// Note: libav can call LogCallback from different threads
+// (there is mutex in AddFileLogRaw).
+static void LogCallback(void* p, int Level, const char* pFmt, va_list VaArgs)
+{
+ const char Buffer[1024];
+
+ vsnprintf(Buffer, 1024, pFmt, VaArgs);
+ AddFileLogRaw(Buffer);
+}
+
+static void Log(const char* pFmt, ...)
+{
+ const char Buffer[1024];
+ va_list VaArgs;
+
+ va_start(VaArgs, pFmt);
+ vsnprintf(Buffer, 1024, pFmt, VaArgs);
+ va_end(VaArgs);
+
+ AddFileLogRaw(Buffer);
+}
+
+static void AddAudioStream()
+{
+#if LIBAVFORMAT_VERSION_MAJOR >= 54
+ g_pAStream = avformat_new_stream(g_pContainer, g_pACodec);
+#else
+ g_pAStream = av_new_stream(g_pContainer, 1);
+#endif
+ if(!g_pAStream)
+ {
+ Log("Could not allocate audio stream\n");
+ return;
+ }
+ g_pAStream->id = 1;
+
+ g_pAudio = g_pAStream->codec;
+
+ avcodec_get_context_defaults3(g_pAudio, g_pACodec);
+ g_pAudio->codec_id = g_pACodec->id;
+
+ // put parameters
+ g_pAudio->sample_fmt = AV_SAMPLE_FMT_S16;
+ g_pAudio->sample_rate = g_Frequency;
+ g_pAudio->channels = g_Channels;
+
+ // set quality
+ g_pAudio->bit_rate = 160000;
+
+ // for codecs that support variable bitrate use it, it should be better
+ g_pAudio->flags |= CODEC_FLAG_QSCALE;
+ g_pAudio->global_quality = 1*FF_QP2LAMBDA;
+
+ // some formats want stream headers to be separate
+ if (g_pFormat->flags & AVFMT_GLOBALHEADER)
+ g_pAudio->flags |= CODEC_FLAG_GLOBAL_HEADER;
+
+ // open it
+#if LIBAVCODEC_VERSION_MAJOR >= 53
+ if (avcodec_open2(g_pAudio, g_pACodec, NULL) < 0)
+#else
+ if (avcodec_open(g_pAudio, g_pACodec) < 0)
+#endif
+ {
+ Log("Could not open audio codec %s\n", g_pACodec->long_name);
+ return;
+ }
+
+#if LIBAVCODEC_VERSION_MAJOR >= 54
+ if (g_pACodec->capabilities & CODEC_CAP_VARIABLE_FRAME_SIZE)
+#else
+ if (g_pAudio->frame_size == 0)
+#endif
+ g_NumSamples = 4096;
+ else
+ g_NumSamples = g_pAudio->frame_size;
+ g_pSamples = (int16_t*)av_malloc(g_NumSamples*g_Channels*sizeof(int16_t));
+ g_pAFrame = avcodec_alloc_frame();
+ if (!g_pAFrame)
+ {
+ Log("Could not allocate frame\n");
+ return;
+ }
+}
+
+// returns non-zero if there is more sound
+static int WriteAudioFrame()
+{
+ if (!g_pAStream)
+ return 0;
+
+ AVPacket Packet = { 0 };
+ av_init_packet(&Packet);
+
+ int NumSamples = fread(g_pSamples, 2*g_Channels, g_NumSamples, g_pSoundFile);
+
+#if LIBAVCODEC_VERSION_MAJOR >= 54
+ AVFrame* pFrame = NULL;
+ if (NumSamples > 0)
+ {
+ g_pAFrame->nb_samples = NumSamples;
+ avcodec_fill_audio_frame(g_pAFrame, g_Channels, AV_SAMPLE_FMT_S16,
+ (uint8_t*)g_pSamples, NumSamples*2*g_Channels, 1);
+ pFrame = g_pAFrame;
+ }
+ // when NumSamples == 0 we still need to call encode_audio2 to flush
+ int got_packet;
+ if (avcodec_encode_audio2(g_pAudio, &Packet, pFrame, &got_packet) != 0)
+ FatalError("avcodec_encode_audio2 failed");
+ if (!got_packet)
+ return 0;
+#else
+ if (NumSamples == 0)
+ return 0;
+ int BufferSize = OUTBUFFER_SIZE;
+ if (g_pAudio->frame_size == 0)
+ BufferSize = NumSamples*g_Channels*2;
+ Packet.size = avcodec_encode_audio(g_pAudio, g_OutBuffer, BufferSize, g_pSamples);
+ if (Packet.size == 0)
+ return 1;
+ if (g_pAudio->coded_frame && g_pAudio->coded_frame->pts != AV_NOPTS_VALUE)
+ Packet.pts = av_rescale_q(g_pAudio->coded_frame->pts, g_pAudio->time_base, g_pAStream->time_base);
+ Packet.flags |= AV_PKT_FLAG_KEY;
+ Packet.data = g_OutBuffer;
+#endif
+
+ // Write the compressed frame to the media file.
+ Packet.stream_index = g_pAStream->index;
+ if (av_interleaved_write_frame(g_pContainer, &Packet) != 0)
+ FatalError("Error while writing audio frame");
+ return 1;
+}
+
+// add a video output stream
+static void AddVideoStream()
+{
+#if LIBAVFORMAT_VERSION_MAJOR >= 54
+ g_pVStream = avformat_new_stream(g_pContainer, g_pVCodec);
+#else
+ g_pVStream = av_new_stream(g_pContainer, 0);
+#endif
+ if (!g_pVStream)
+ FatalError("Could not allocate video stream");
+
+ g_pVideo = g_pVStream->codec;
+
+ avcodec_get_context_defaults3(g_pVideo, g_pVCodec);
+ g_pVideo->codec_id = g_pVCodec->id;
+
+ // put parameters
+ // resolution must be a multiple of two
+ g_pVideo->width = g_Width & ~1; // make even (dimensions should be even)
+ g_pVideo->height = g_Height & ~1; // make even
+ /* time base: this is the fundamental unit of time (in seconds) in terms
+ of which frame timestamps are represented. for fixed-fps content,
+ timebase should be 1/framerate and timestamp increments should be
+ identically 1. */
+ g_pVideo->time_base.den = g_Framerate.num;
+ g_pVideo->time_base.num = g_Framerate.den;
+ //g_pVideo->gop_size = 12; /* emit one intra frame every twelve frames at most */
+ g_pVideo->pix_fmt = PIX_FMT_YUV420P;
+
+ // set quality
+ if (g_VQuality > 100)
+ g_pVideo->bit_rate = g_VQuality;
+ else
+ {
+ g_pVideo->flags |= CODEC_FLAG_QSCALE;
+ g_pVideo->global_quality = g_VQuality*FF_QP2LAMBDA;
+ }
+
+ // some formats want stream headers to be separate
+ if (g_pFormat->flags & AVFMT_GLOBALHEADER)
+ g_pVideo->flags |= CODEC_FLAG_GLOBAL_HEADER;
+
+#if LIBAVCODEC_VERSION_MAJOR < 54
+ // for some versions of ffmpeg x264 options must be set explicitly
+ if (strcmp(g_pVCodec->name, "libx264") == 0)
+ {
+ g_pVideo->coder_type = FF_CODER_TYPE_AC;
+ g_pVideo->flags |= CODEC_FLAG_LOOP_FILTER;
+ g_pVideo->crf = 23;
+ g_pVideo->thread_count = 3;
+ g_pVideo->me_cmp = FF_CMP_CHROMA;
+ g_pVideo->partitions = X264_PART_I8X8 | X264_PART_I4X4 | X264_PART_P8X8 | X264_PART_B8X8;
+ g_pVideo->me_method = ME_HEX;
+ g_pVideo->me_subpel_quality = 7;
+ g_pVideo->me_range = 16;
+ g_pVideo->gop_size = 250;
+ g_pVideo->keyint_min = 25;
+ g_pVideo->scenechange_threshold = 40;
+ g_pVideo->i_quant_factor = 0.71;
+ g_pVideo->b_frame_strategy = 1;
+ g_pVideo->qcompress = 0.6;
+ g_pVideo->qmin = 10;
+ g_pVideo->qmax = 51;
+ g_pVideo->max_qdiff = 4;
+ g_pVideo->max_b_frames = 3;
+ g_pVideo->refs = 3;
+ g_pVideo->directpred = 1;
+ g_pVideo->trellis = 1;
+ g_pVideo->flags2 = CODEC_FLAG2_BPYRAMID | CODEC_FLAG2_MIXED_REFS | CODEC_FLAG2_WPRED | CODEC_FLAG2_8X8DCT | CODEC_FLAG2_FASTPSKIP;
+ g_pVideo->weighted_p_pred = 2;
+ }
+#endif
+
+ // open the codec
+#if LIBAVCODEC_VERSION_MAJOR >= 53
+ AVDictionary* pDict = NULL;
+ if (strcmp(g_pVCodec->name, "libx264") == 0)
+ av_dict_set(&pDict, "preset", "medium", 0);
+
+ if (avcodec_open2(g_pVideo, g_pVCodec, &pDict) < 0)
+#else
+ if (avcodec_open(g_pVideo, g_pVCodec) < 0)
+#endif
+ FatalError("Could not open video codec %s", g_pVCodec->long_name);
+
+ g_pVFrame = avcodec_alloc_frame();
+ if (!g_pVFrame)
+ FatalError("Could not allocate frame");
+
+ g_pVFrame->linesize[0] = g_Width;
+ g_pVFrame->linesize[1] = g_Width/2;
+ g_pVFrame->linesize[2] = g_Width/2;
+ g_pVFrame->linesize[3] = 0;
+}
+
+static int WriteFrame(AVFrame* pFrame)
+{
+ double AudioTime, VideoTime;
+
+ // write interleaved audio frame
+ if (g_pAStream)
+ {
+ VideoTime = (double)g_pVStream->pts.val*g_pVStream->time_base.num/g_pVStream->time_base.den;
+ do
+ AudioTime = (double)g_pAStream->pts.val*g_pAStream->time_base.num/g_pAStream->time_base.den;
+ while (AudioTime < VideoTime && WriteAudioFrame());
+ }
+
+ if (!g_pVStream)
+ return 0;
+
+ AVPacket Packet;
+ av_init_packet(&Packet);
+ Packet.data = NULL;
+ Packet.size = 0;
+
+ g_pVFrame->pts++;
+ if (g_pFormat->flags & AVFMT_RAWPICTURE)
+ {
+ /* raw video case. The API will change slightly in the near
+ future for that. */
+ Packet.flags |= AV_PKT_FLAG_KEY;
+ Packet.stream_index = g_pVStream->index;
+ Packet.data = (uint8_t*)pFrame;
+ Packet.size = sizeof(AVPicture);
+
+ if (av_interleaved_write_frame(g_pContainer, &Packet) != 0)
+ FatalError("Error while writing video frame");
+ return 0;
+ }
+ else
+ {
+#if LIBAVCODEC_VERSION_MAJOR >= 54
+ int got_packet;
+ if (avcodec_encode_video2(g_pVideo, &Packet, pFrame, &got_packet) < 0)
+ FatalError("avcodec_encode_video2 failed");
+ if (!got_packet)
+ return 0;
+
+ if (Packet.pts != AV_NOPTS_VALUE)
+ Packet.pts = av_rescale_q(Packet.pts, g_pVideo->time_base, g_pVStream->time_base);
+ if (Packet.dts != AV_NOPTS_VALUE)
+ Packet.dts = av_rescale_q(Packet.dts, g_pVideo->time_base, g_pVStream->time_base);
+#else
+ Packet.size = avcodec_encode_video(g_pVideo, g_OutBuffer, OUTBUFFER_SIZE, pFrame);
+ if (Packet.size < 0)
+ FatalError("avcodec_encode_video failed");
+ if (Packet.size == 0)
+ return 0;
+
+ if( g_pVideo->coded_frame->pts != AV_NOPTS_VALUE)
+ Packet.pts = av_rescale_q(g_pVideo->coded_frame->pts, g_pVideo->time_base, g_pVStream->time_base);
+ if( g_pVideo->coded_frame->key_frame )
+ Packet.flags |= AV_PKT_FLAG_KEY;
+ Packet.data = g_OutBuffer;
+#endif
+ // write the compressed frame in the media file
+ Packet.stream_index = g_pVStream->index;
+ if (av_interleaved_write_frame(g_pContainer, &Packet) != 0)
+ FatalError("Error while writing video frame");
+
+ return 1;
+ }
+}
+
+void AVWrapper_WriteFrame(uint8_t* pY, uint8_t* pCb, uint8_t* pCr)
+{
+ g_pVFrame->data[0] = pY;
+ g_pVFrame->data[1] = pCb;
+ g_pVFrame->data[2] = pCr;
+ WriteFrame(g_pVFrame);
+}
+
+void AVWrapper_Init(
+ void (*pAddFileLogRaw)(const char*),
+ const char* pFilename,
+ const char* pDesc,
+ const char* pSoundFile,
+ const char* pFormatName,
+ const char* pVCodecName,
+ const char* pACodecName,
+ int Width, int Height,
+ int FramerateNum, int FramerateDen,
+ int VQuality)
+{
+ AddFileLogRaw = pAddFileLogRaw;
+ av_log_set_callback( &LogCallback );
+
+ g_Width = Width;
+ g_Height = Height;
+ g_Framerate.num = FramerateNum;
+ g_Framerate.den = FramerateDen;
+ g_VQuality = VQuality;
+
+ // initialize libav and register all codecs and formats
+ av_register_all();
+
+ // find format
+ g_pFormat = av_guess_format(pFormatName, NULL, NULL);
+ if (!g_pFormat)
+ FatalError("Format \"%s\" was not found", pFormatName);
+
+ // allocate the output media context
+ g_pContainer = avformat_alloc_context();
+ if (!g_pContainer)
+ FatalError("Could not allocate output context");
+
+ g_pContainer->oformat = g_pFormat;
+
+ // store description of file
+ av_dict_set(&g_pContainer->metadata, "comment", pDesc, 0);
+
+ // append extesnion to filename
+ char ext[16];
+ strncpy(ext, g_pFormat->extensions, 16);
+ ext[15] = 0;
+ ext[strcspn(ext,",")] = 0;
+ snprintf(g_pContainer->filename, sizeof(g_pContainer->filename), "%s.%s", pFilename, ext);
+
+ // find codecs
+ g_pVCodec = avcodec_find_encoder_by_name(pVCodecName);
+ g_pACodec = avcodec_find_encoder_by_name(pACodecName);
+
+ // add audio and video stream to container
+ g_pVStream = NULL;
+ g_pAStream = NULL;
+
+ if (g_pVCodec)
+ AddVideoStream();
+ else
+ Log("Video codec \"%s\" was not found; video will be ignored.\n", pVCodecName);
+
+ if (g_pACodec)
+ {
+ g_pSoundFile = fopen(pSoundFile, "rb");
+ if (g_pSoundFile)
+ {
+ fread(&g_Frequency, 4, 1, g_pSoundFile);
+ fread(&g_Channels, 4, 1, g_pSoundFile);
+ AddAudioStream();
+ }
+ else
+ Log("Could not open %s\n", pSoundFile);
+ }
+ else
+ Log("Audio codec \"%s\" was not found; audio will be ignored.\n", pACodecName);
+
+ if (!g_pAStream && !g_pVStream)
+ FatalError("No video, no audio, aborting...");
+
+ // write format info to log
+ av_dump_format(g_pContainer, 0, g_pContainer->filename, 1);
+
+ // open the output file, if needed
+ if (!(g_pFormat->flags & AVFMT_NOFILE))
+ {
+ if (avio_open(&g_pContainer->pb, g_pContainer->filename, AVIO_FLAG_WRITE) < 0)
+ FatalError("Could not open output file (%s)", g_pContainer->filename);
+ }
+
+ // write the stream header, if any
+ avformat_write_header(g_pContainer, NULL);
+
+ g_pVFrame->pts = -1;
+}
+
+void AVWrapper_Close()
+{
+ // output buffered frames
+ if (g_pVCodec->capabilities & CODEC_CAP_DELAY)
+ while( WriteFrame(NULL) );
+ // output any remaining audio
+ while( WriteAudioFrame() );
+
+ // write the trailer, if any.
+ av_write_trailer(g_pContainer);
+
+ // close the output file
+ if (!(g_pFormat->flags & AVFMT_NOFILE))
+ avio_close(g_pContainer->pb);
+
+ // free everything
+ if (g_pVStream)
+ {
+ avcodec_close(g_pVideo);
+ av_free(g_pVideo);
+ av_free(g_pVStream);
+ av_free(g_pVFrame);
+ }
+ if (g_pAStream)
+ {
+ avcodec_close(g_pAudio);
+ av_free(g_pAudio);
+ av_free(g_pAStream);
+ av_free(g_pAFrame);
+ av_free(g_pSamples);
+ fclose(g_pSoundFile);
+ }
+
+ av_free(g_pContainer);
+}
--- a/hedgewars/hwengine.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/hwengine.pas Tue Aug 28 20:30:57 2012 +0400
@@ -32,8 +32,10 @@
uses SDLh, uMisc, uConsole, uGame, uConsts, uLand, uAmmos, uVisualGears, uGears, uStore, uWorld, uInputHandler, uSound,
uScript, uTeams, uStats, uIO, uLocale, uChat, uAI, uAIMisc, uLandTexture, uCollisions,
SysUtils, uTypes, uVariables, uCommands, uUtils, uCaptions, uDebug, uCommandHandlers, uLandPainted
+ {$IFDEF USE_VIDEO_RECORDING}, uVideoRec {$ENDIF}
{$IFDEF SDL13}, uTouch{$ENDIF}{$IFDEF ANDROID}, GLUnit{$ENDIF}, uAILandMarks;
+
{$IFDEF HWLIBRARY}
procedure initEverything(complete:boolean);
procedure freeEverything(complete:boolean);
@@ -84,7 +86,7 @@
end;
gsConfirm, gsGame:
begin
- DrawWorld(Lag); // never place between ProcessKbd and DoGameTick - bugs due to /put cmd and isCursorVisible
+ DrawWorld(Lag);
DoGameTick(Lag);
ProcessVisualGears(Lag);
end;
@@ -104,18 +106,27 @@
SwapBuffers;
+{$IFDEF USE_VIDEO_RECORDING}
+ if flagPrerecording then
+ SaveCameraPosition;
+{$ENDIF}
+
if flagMakeCapture then
begin
flagMakeCapture:= false;
{$IFDEF PAS2C}
- s:= 'hw';
+ s:= '/Screenshots/hw';
{$ELSE}
- s:= 'hw_' + FormatDateTime('YYYY-MM-DD_HH-mm-ss', Now()) + inttostr(GameTicks);
+ s:= '/Screenshots/hw_' + FormatDateTime('YYYY-MM-DD_HH-mm-ss', Now()) + inttostr(GameTicks);
{$ENDIF}
+ // flash
playSound(sndShutter);
-
- if MakeScreenshot(s) then
+ ScreenFade:= sfFromWhite;
+ ScreenFadeValue:= sfMax;
+ ScreenFadeSpeed:= 5;
+
+ if MakeScreenshot(s, 1) then
WriteLnToConsole('Screenshot saved: ' + s)
else
begin
@@ -264,6 +275,39 @@
end;
end;
+{$IFDEF USE_VIDEO_RECORDING}
+procedure RecorderMainLoop;
+var oldGameTicks, oldRealTicks, newGameTicks, newRealTicks: LongInt;
+begin
+ if not BeginVideoRecording() then
+ exit;
+ DoTimer(0); // gsLandGen -> gsStart
+ DoTimer(0); // gsStart -> gsGame
+
+ if not LoadNextCameraPosition(newRealTicks, newGameTicks) then
+ exit;
+ fastScrolling:= true;
+ DoGameTick(newGameTicks);
+ fastScrolling:= false;
+ oldRealTicks:= 0;
+ oldGameTicks:= newGameTicks;
+
+ while LoadNextCameraPosition(newRealTicks, newGameTicks) do
+ begin
+ IPCCheckSock();
+ DoGameTick(newGameTicks - oldGameTicks);
+ if GameState = gsExit then
+ break;
+ ProcessVisualGears(newRealTicks - oldRealTicks);
+ DrawWorld(newRealTicks - oldRealTicks);
+ EncodeFrame();
+ oldRealTicks:= newRealTicks;
+ oldGameTicks:= newGameTicks;
+ end;
+ StopVideoRecording();
+end;
+{$ENDIF}
+
///////////////
procedure Game{$IFDEF HWLIBRARY}(gameArgs: PPChar); cdecl; export{$ENDIF};
var p: TPathType;
@@ -330,11 +374,18 @@
SDLTry(TTF_Init() <> -1, true);
WriteLnToConsole(msgOK);
- // show main window
- if cFullScreen then
- ParseCommand('fullscr 1', true)
+{$IFDEF USE_VIDEO_RECORDING}
+ if GameType = gmtRecord then
+ InitOffscreenOpenGL()
else
- ParseCommand('fullscr 0', true);
+{$ENDIF}
+ begin
+ // show main window
+ if cFullScreen then
+ ParseCommand('fullscr 1', true)
+ else
+ ParseCommand('fullscr 0', true);
+ end;
ControllerInit(); // has to happen before InitKbdKeyTable to map keys
InitKbdKeyTable();
@@ -371,12 +422,22 @@
InitTeams();
AssignStores();
+
+ if GameType = gmtRecord then
+ SetSound(false);
+
InitSound();
isDeveloperMode:= false;
TryDo(InitStepsFlags = cifAllInited, 'Some parameters not set (flags = ' + inttostr(InitStepsFlags) + ')', true);
ParseCommand('rotmask', true);
- MainLoop();
+
+{$IFDEF USE_VIDEO_RECORDING}
+ if GameType = gmtRecord then
+ RecorderMainLoop()
+ else
+{$ENDIF}
+ MainLoop();
// clean up all the memory allocated
freeEverything(true);
@@ -458,6 +519,7 @@
//uAIAmmoTests does not need to be freed
//uAIActions does not need to be freed
uStore.freeModule;
+{$IFDEF USE_VIDEO_RECORDING}uVideoRec.freeModule;{$ENDIF}
end;
uIO.freeModule;
@@ -531,11 +593,14 @@
else
if (ParamCount = 3) and ((ParamStr(3) = '--stats-only') or (ParamStr(3) = 'landpreview')) then
internalSetGameTypeLandPreviewFromParameters()
+ else if ParamCount = cDefaultParamNum then
+ internalStartGameWithParameters()
+{$IFDEF USE_VIDEO_RECORDING}
+ else if ParamCount = cVideorecParamNum then
+ internalStartVideoRecordingWithParameters()
+{$ENDIF}
else
- if (ParamCount = cDefaultParamNum) then
- internalStartGameWithParameters()
- else
- playReplayFileWithParameters();
+ playReplayFileWithParameters();
end;
////////////////////////////////////////////////////////////////////////////////
--- a/hedgewars/uCommandHandlers.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uCommandHandlers.pas Tue Aug 28 20:30:57 2012 +0400
@@ -26,7 +26,8 @@
procedure freeModule;
implementation
-uses uCommands, uTypes, uVariables, uIO, uDebug, uConsts, uScript, uUtils, SDLh, uRandom, uCaptions;
+uses uCommands, uTypes, uVariables, uIO, uDebug, uConsts, uScript, uUtils, SDLh, uRandom, uCaptions
+ {$IFDEF USE_VIDEO_RECORDING}, uVideoRec {$ENDIF};
var prevGState: TGameState = gsConfirm;
@@ -530,6 +531,17 @@
flagMakeCapture:= true
end;
+procedure chRecord(var s: shortstring);
+begin
+s:= s; // avoid compiler hint
+{$IFDEF USE_VIDEO_RECORDING}
+if flagPrerecording then
+ StopPreRecording()
+else
+ BeginPreRecording();
+{$ENDIF}
+end;
+
procedure chSetMap(var s: shortstring);
begin
if isDeveloperMode then
@@ -868,6 +880,7 @@
RegisterVariable('-cur_l' , @chCurL_m , true );
RegisterVariable('+cur_r' , @chCurR_p , true );
RegisterVariable('-cur_r' , @chCurR_m , true );
+ RegisterVariable('record' , @chRecord , true );
end;
procedure freeModule;
--- a/hedgewars/uConsts.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uConsts.pas Tue Aug 28 20:30:57 2012 +0400
@@ -28,6 +28,7 @@
const
sfMax = 1000;
cDefaultParamNum = 17;
+ cVideorecParamNum = cDefaultParamNum + 7;
// message constants
errmsgCreateSurface = 'Error creating SDL surface';
--- a/hedgewars/uGame.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uGame.pas Tue Aug 28 20:30:57 2012 +0400
@@ -39,13 +39,15 @@
isInLag:= false;
SendKeepAliveMessage(Lag)
end;
-if Lag > 100 then
- Lag:= 100
-else if (GameType = gmtSave) or (fastUntilLag and (GameType = gmtNet)) then
- Lag:= 2500;
+if GameType <> gmtRecord then
+ begin
+ if Lag > 100 then
+ Lag:= 100
+ else if (GameType = gmtSave) or (fastUntilLag and (GameType = gmtNet)) then
+ Lag:= 2500;
-if (GameType = gmtDemo) then
- if isSpeed then
+ if (GameType = gmtDemo) then
+ if isSpeed then
begin
i:= RealTicks-SpeedStart;
if i < 2000 then Lag:= Lag*5
@@ -54,9 +56,10 @@
else if i < 8000 then Lag:= Lag*40
else Lag:= Lag*80;
end
- else
- if cOnlyStats then
- Lag:= High(LongInt);
+ else
+ if cOnlyStats then
+ Lag:= High(LongInt);
+ end;
PlayNextVoice;
i:= 1;
while (GameState <> gsExit) and (i <= Lag) do
@@ -78,7 +81,7 @@
AddVisualGear(0, 0, vgtTeamHealthSorter);
break;
end;
- gmtDemo: begin
+ gmtDemo, gmtRecord: begin
GameState:= gsExit;
exit
end;
--- a/hedgewars/uIO.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uIO.pas Tue Aug 28 20:30:57 2012 +0400
@@ -126,6 +126,7 @@
'D': GameType:= gmtDemo;
'N': GameType:= gmtNet;
'S': GameType:= gmtSave;
+ 'V': GameType:= gmtRecord;
else OutError(errmsgIncorrectUse + ' IPC "T" :' + s[2], true) end;
else
loTicks:= SDLNet_Read16(@s[byte(s[0]) - 1]);
--- a/hedgewars/uInputHandler.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uInputHandler.pas Tue Aug 28 20:30:57 2012 +0400
@@ -256,6 +256,7 @@
DefaultBinds[KeyNameToCode(_S'9')]:= '+voldown';
DefaultBinds[KeyNameToCode(_S'8')]:= 'mute';
DefaultBinds[KeyNameToCode(_S'c')]:= 'capture';
+DefaultBinds[KeyNameToCode(_S'r')]:= 'record';
DefaultBinds[KeyNameToCode(_S'h')]:= 'findhh';
DefaultBinds[KeyNameToCode(_S'p')]:= 'pause';
DefaultBinds[KeyNameToCode(_S's')]:= '+speedup';
--- a/hedgewars/uMisc.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uMisc.pas Tue Aug 28 20:30:57 2012 +0400
@@ -28,7 +28,7 @@
procedure movecursor(dx, dy: LongInt);
function doSurfaceConversion(tmpsurf: PSDL_Surface): PSDL_Surface;
-function MakeScreenshot(filename: shortstring): boolean;
+function MakeScreenshot(filename: shortstring; k: LongInt): boolean;
function GetTeamStatString(p: PTeam): shortstring;
{$IFDEF SDL13}
function SDL_RectMake(x, y, width, height: LongInt): TSDL_Rect; inline;
@@ -186,19 +186,48 @@
{$ENDIF} // no PNG_SCREENSHOTS
+{$IFDEF USE_VIDEO_RECORDING}
+// make image k times smaller (useful for saving thumbnails)
+procedure ReduceImage(img: PByte; width, height, k: LongInt);
+var i, j, i0, j0, w, h, r, g, b: LongInt;
+begin
+ w:= width div k;
+ h:= height div k;
+
+ // rescale inplace
+ if k <> 1 then
+ begin
+ for i:= 0 to h-1 do
+ for j:= 0 to w-1 do
+ begin
+ r:= 0;
+ g:= 0;
+ b:= 0;
+ for i0:= 0 to k-1 do
+ for j0:= 0 to k-1 do
+ begin
+ r+= img[4*(width*(i*k+i0) + j*k+j0)+0];
+ g+= img[4*(width*(i*k+i0) + j*k+j0)+1];
+ b+= img[4*(width*(i*k+i0) + j*k+j0)+2];
+ end;
+ img[4*(w*i + j)+0]:= r div (k*k);
+ img[4*(w*i + j)+1]:= g div (k*k);
+ img[4*(w*i + j)+2]:= b div (k*k);
+ img[4*(w*i + j)+3]:= 255;
+ end;
+ end;
+end;
+{$ENDIF}
+
// captures and saves the screen. returns true on success.
-function MakeScreenshot(filename: shortstring): Boolean;
+// saved image will be k times smaller than original (useful for saving thumbnails).
+function MakeScreenshot(filename: shortstring; k: LongInt): Boolean;
var p: Pointer;
size: QWord;
image: PScreenshot;
format: GLenum;
ext: string[4];
begin
-// flash
-ScreenFade:= sfFromWhite;
-ScreenFadeValue:= sfMax;
-ScreenFadeSpeed:= 5;
-
{$IFDEF PNG_SCREENSHOTS}
format:= GL_RGBA;
ext:= '.png';
@@ -218,14 +247,18 @@
exit;
end;
-// read pixel from the front buffer
+// read pixels from the front buffer
glReadPixels(0, 0, cScreenWidth, cScreenHeight, format, GL_UNSIGNED_BYTE, p);
+{$IFDEF USE_VIDEO_RECORDING}
+ReduceImage(p, cScreenWidth, cScreenHeight, k);
+{$ENDIF}
+
// allocate and fill structure that will be passed to new thread
New(image); // will be disposed in SaveScreenshot()
-image^.filename:= UserPathPrefix + '/Screenshots/' + filename + ext;
-image^.width:= cScreenWidth;
-image^.height:= cScreenHeight;
+image^.filename:= UserPathPrefix + filename + ext;
+image^.width:= cScreenWidth div k;
+image^.height:= cScreenHeight div k;
image^.size:= size;
image^.buffer:= p;
--- a/hedgewars/uStore.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uStore.pas Tue Aug 28 20:30:57 2012 +0400
@@ -40,13 +40,17 @@
procedure ShowWeaponTooltip(x, y: LongInt);
procedure FreeWeaponTooltip;
procedure MakeCrossHairs;
+{$IFDEF USE_VIDEO_RECORDING}
+procedure InitOffscreenOpenGL;
+{$ENDIF}
procedure WarpMouse(x, y: Word); inline;
procedure SwapBuffers; inline;
implementation
uses uMisc, uConsole, uMobile, uVariables, uUtils, uTextures, uRender, uRenderUtils, uCommands,
- uDebug{$IFDEF USE_CONTEXT_RESTORE}, uWorld{$ENDIF};
+ uDebug{$IFDEF USE_CONTEXT_RESTORE}, uWorld{$ENDIF}
+ {$IF NOT DEFINED(SDL13) AND DEFINED(USE_VIDEO_RECORDING)}, glut {$ENDIF};
//type TGPUVendor = (gvUnknown, gvNVIDIA, gvATI, gvIntel, gvApple);
@@ -468,6 +472,31 @@
IMG_Quit();
end;
+{$IF NOT DEFINED(S3D_DISABLED) OR DEFINED(USE_VIDEO_RECORDING)}
+procedure CreateFramebuffer(var frame, depth, tex: GLuint);
+begin
+ glGenFramebuffersEXT(1, @frame);
+ glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, frame);
+ glGenRenderbuffersEXT(1, @depth);
+ glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depth);
+ glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, cScreenWidth, cScreenHeight);
+ glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, depth);
+ glGenTextures(1, @tex);
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, cScreenWidth, cScreenHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nil);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, tex, 0);
+end;
+
+procedure DeleteFramebuffer(var frame, depth, tex: GLuint);
+begin
+ glDeleteTextures(1, @tex);
+ glDeleteRenderbuffersEXT(1, @depth);
+ glDeleteFramebuffersEXT(1, @frame);
+end;
+{$ENDIF}
+
procedure StoreRelease(reload: boolean);
var ii: TSprite;
ai: TAmmoType;
@@ -541,15 +570,15 @@
end;
end;
end;
+{$IFDEF USE_VIDEO_RECORDING}
+ if defaultFrame <> 0 then
+ DeleteFramebuffer(defaultFrame, depthv, texv);
+{$ENDIF}
{$IFNDEF S3D_DISABLED}
if (cStereoMode = smHorizontal) or (cStereoMode = smVertical) or (cStereoMode = smAFR) then
begin
- glDeleteTextures(1, @texl);
- glDeleteRenderbuffersEXT(1, @depthl);
- glDeleteFramebuffersEXT(1, @framel);
- glDeleteTextures(1, @texr);
- glDeleteRenderbuffersEXT(1, @depthr);
- glDeleteFramebuffersEXT(1, @framer)
+ DeleteFramebuffer(framel, depthl, texl);
+ DeleteFramebuffer(framer, depthr, texr);
end
{$ENDIF}
end;
@@ -658,6 +687,7 @@
procedure SetupOpenGL;
//var vendor: shortstring = '';
var buf: array[byte] of char;
+ AuxBufNum: LongInt;
begin
buf[0]:= char(0); // avoid compiler hint
AddFileLog('Setting up OpenGL (using driver: ' + shortstring(SDL_VideoDriverName(buf, sizeof(buf))) + ')');
@@ -703,14 +733,43 @@
{$ENDIF}
//SupportNPOTT:= glLoadExtension('GL_ARB_texture_non_power_of_two');
*)
+ glGetIntegerv(GL_AUX_BUFFERS, @AuxBufNum);
// everyone love debugging
AddFileLog('OpenGL-- Renderer: ' + shortstring(pchar(glGetString(GL_RENDERER))));
AddFileLog(' |----- Vendor: ' + shortstring(pchar(glGetString(GL_VENDOR))));
AddFileLog(' |----- Version: ' + shortstring(pchar(glGetString(GL_VERSION))));
AddFileLog(' |----- Texture Size: ' + inttostr(MaxTextureSize));
- AddFileLog(' \----- Extensions: ' + shortstring(pchar(glGetString(GL_EXTENSIONS))));
- //TODO: don't have the Extensions line trimmed but slipt it into multiple lines
+ AddFileLog(' |----- Number of auxilary buffers: ' + inttostr(AuxBufNum));
+ AddFileLog(' \----- Extensions: ');
+ AddFileLogRaw(glGetString(GL_EXTENSIONS));
+ AddFileLog('');
+ //TODO: slipt Extensions line into multiple lines
+
+ defaultFrame:= 0;
+{$IFDEF USE_VIDEO_RECORDING}
+ if GameType = gmtRecord then
+ begin
+ if glLoadExtension('GL_EXT_framebuffer_object') then
+ begin
+ CreateFramebuffer(defaultFrame, depthv, texv);
+ glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, defaultFrame);
+ AddFileLog('Using framebuffer for video recording.');
+ end
+ else if AuxBufNum > 0 then
+ begin
+ glDrawBuffer(GL_AUX0);
+ glReadBuffer(GL_AUX0);
+ AddFileLog('Using auxilary buffer for video recording.');
+ end
+ else
+ begin
+ glDrawBuffer(GL_BACK);
+ glReadBuffer(GL_BACK);
+ AddFileLog('Warning: off-screen rendering is not supported; using back buffer but it may not work.');
+ end;
+ end;
+{$ENDIF}
{$IFNDEF S3D_DISABLED}
if (cStereoMode = smHorizontal) or (cStereoMode = smVertical) or (cStereoMode = smAFR) then
@@ -718,36 +777,11 @@
// prepare left and right frame buffers and associated textures
if glLoadExtension('GL_EXT_framebuffer_object') then
begin
- // left
- glGenFramebuffersEXT(1, @framel);
- glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framel);
- glGenRenderbuffersEXT(1, @depthl);
- glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depthl);
- glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, cScreenWidth, cScreenHeight);
- glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, depthl);
- glGenTextures(1, @texl);
- glBindTexture(GL_TEXTURE_2D, texl);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, cScreenWidth, cScreenHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nil);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, texl, 0);
-
- // right
- glGenFramebuffersEXT(1, @framer);
- glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framer);
- glGenRenderbuffersEXT(1, @depthr);
- glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depthr);
- glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, cScreenWidth, cScreenHeight);
- glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, depthr);
- glGenTextures(1, @texr);
- glBindTexture(GL_TEXTURE_2D, texr);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, cScreenWidth, cScreenHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nil);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, texr, 0);
+ CreateFramebuffer(framel, depthl, texl);
+ CreateFramebuffer(framer, depthr, texr);
// reset
- glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0)
+ glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, defaultFrame)
end
else
cStereoMode:= smNone;
@@ -1021,6 +1055,34 @@
WeaponTooltipTex:= nil
end;
+{$IFDEF USE_VIDEO_RECORDING}
+{$IFDEF SDL13}
+procedure InitOffscreenOpenGL;
+begin
+ // create hidden window
+ SDLwindow:= SDL_CreateWindow('hedgewars (you don''t see this)',
+ SDL_WINDOWPOS_CENTERED_MASK, SDL_WINDOWPOS_CENTERED_MASK,
+ cScreenWidth, cScreenHeight,
+ SDL_WINDOW_HIDDEN or SDL_WINDOW_OPENGL);
+ SDLTry(SDLwindow <> nil, true);
+ SetupOpenGL();
+end;
+{$ELSE}
+procedure InitOffscreenOpenGL;
+var ArgCount: LongInt;
+ PrgName: pchar;
+begin
+ ArgCount:= 1;
+ PrgName:= 'hwengine';
+ glutInit(@ArgCount, @PrgName);
+ glutInitWindowSize(cScreenWidth, cScreenHeight);
+ glutCreateWindow('hedgewars (you don''t see this)'); // we don't need a window, but if this function is not called then OpenGL will not be initialized
+ glutHideWindow();
+ SetupOpenGL();
+end;
+{$ENDIF} // SDL13
+{$ENDIF} // USE_VIDEO_RECORDING
+
procedure chFullScr(var s: shortstring);
var flags: Longword = 0;
reinit: boolean = false;
@@ -1201,6 +1263,8 @@
procedure SwapBuffers; inline;
begin
+ if GameType = gmtRecord then
+ exit;
{$IFDEF SDL13}
SDL_GL_SwapWindow(SDLwindow);
{$ELSE}
--- a/hedgewars/uTeams.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uTeams.pas Tue Aug 28 20:30:57 2012 +0400
@@ -531,7 +531,7 @@
AddTeam(Color);
CurrentTeam^.TeamName:= ts;
CurrentTeam^.PlayerHash:= s;
- if GameType in [gmtDemo, gmtSave] then
+ if GameType in [gmtDemo, gmtSave, gmtRecord] then
CurrentTeam^.ExtDriven:= true;
CurrentTeam^.voicepack:= AskForVoicepack('Default')
--- a/hedgewars/uTypes.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uTypes.pas Tue Aug 28 20:30:57 2012 +0400
@@ -39,7 +39,7 @@
TGameState = (gsLandGen, gsStart, gsGame, gsChat, gsConfirm, gsExit, gsSuspend);
// Game types that help determining what the engine is actually supposed to do
- TGameType = (gmtLocal, gmtDemo, gmtNet, gmtSave, gmtLandPreview, gmtSyntax);
+ TGameType = (gmtLocal, gmtDemo, gmtNet, gmtSave, gmtLandPreview, gmtSyntax, gmtRecord);
// Different files are stored in different folders, this enumeration is used to tell which folder to use
TPathType = (ptNone, ptData, ptGraphics, ptThemes, ptCurrTheme, ptTeams, ptMaps,
--- a/hedgewars/uUtils.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uUtils.pas Tue Aug 28 20:30:57 2012 +0400
@@ -61,6 +61,7 @@
function CheckCJKFont(s: ansistring; font: THWFont): THWFont;
procedure AddFileLog(s: shortstring);
+procedure AddFileLogRaw(s: pchar); cdecl;
function CheckNoTeamOrHH: boolean; inline;
@@ -81,6 +82,9 @@
{$IFDEF DEBUGFILE}
var f: textfile;
+{$IFDEF USE_VIDEO_RECORDING}
+ logMutex: TRTLCriticalSection; // mutex for debug file
+{$ENDIF}
{$ENDIF}
var CharArray: array[byte] of Char;
@@ -291,11 +295,31 @@
begin
s:= s;
{$IFDEF DEBUGFILE}
+{$IFDEF USE_VIDEO_RECORDING}
+EnterCriticalSection(logMutex);
+{$ENDIF}
writeln(f, inttostr(GameTicks) + ': ' + s);
-flush(f)
+flush(f);
+{$IFDEF USE_VIDEO_RECORDING}
+LeaveCriticalSection(logMutex);
+{$ENDIF}
{$ENDIF}
end;
+procedure AddFileLogRaw(s: pchar); cdecl;
+begin
+s:= s;
+{$IFDEF DEBUGFILE}
+{$IFDEF USE_VIDEO_RECORDING}
+EnterCriticalSection(logMutex);
+{$ENDIF}
+write(f, s);
+flush(f);
+{$IFDEF USE_VIDEO_RECORDING}
+LeaveCriticalSection(logMutex);
+{$ENDIF}
+{$ENDIF}
+end;
function CheckCJKFont(s: ansistring; font: THWFont): THWFont;
var l, i : LongInt;
@@ -385,9 +409,17 @@
begin
{$IFDEF DEBUGFILE}
if isGame then
- logfileBase:= 'game'
+ begin
+ if GameType = gmtRecord then
+ logfileBase:= 'rec'
+ else
+ logfileBase:= 'game';
+ end
else
logfileBase:= 'preview';
+{$IFDEF USE_VIDEO_RECORDING}
+ InitCriticalSection(logMutex);
+{$ENDIF}
{$I-}
{$IFDEF MOBILE}
{$IFDEF IPHONEOS} Assign(f,'../Documents/hw-' + logfileBase + '.log'); {$ENDIF}
@@ -424,6 +456,9 @@
writeln(f, 'halt at ' + inttostr(GameTicks) + ' ticks. TurnTimeLeft = ' + inttostr(TurnTimeLeft));
flush(f);
close(f);
+{$IFDEF USE_VIDEO_RECORDING}
+ DoneCriticalSection(logMutex);
+{$ENDIF}
{$ENDIF}
end;
--- a/hedgewars/uVariables.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uVariables.pas Tue Aug 28 20:30:57 2012 +0400
@@ -52,6 +52,15 @@
cReadyDelay : Longword = 5000;
cStereoMode : TStereoMode = smNone;
cOnlyStats : boolean = False;
+{$IFDEF USE_VIDEO_RECORDING}
+ RecPrefix : shortstring;
+ cAVFormat : shortstring;
+ cVideoCodec : shortstring;
+ cVideoFramerateNum : LongInt;
+ cVideoFramerateDen : LongInt;
+ cVideoQuality : LongInt;
+ cAudioCodec : shortstring;
+{$ENDIF}
//////////////////////////
cMapName : shortstring = '';
@@ -63,6 +72,7 @@
SpeedStart : LongWord;
fastUntilLag : boolean;
+ fastScrolling : boolean;
autoCameraOn : boolean;
CheckSum : LongWord;
@@ -2449,6 +2459,10 @@
framel, framer, depthl, depthr: GLuint;
texl, texr: GLuint;
+ // video recorder framebuffer and texture
+ defaultFrame, depthv: GLuint;
+ texv: GLuint;
+
VisualGearLayers: array[0..6] of PVisualGear;
lastVisualGearByUID: PVisualGear;
vobFrameTicks, vobFramesCount, vobCount: Longword;
@@ -2574,7 +2588,7 @@
cExplosives := 2;
GameState := Low(TGameState);
- GameType := gmtLocal;
+// GameType := gmtLocal;
zoom := cDefaultZoomLevel;
ZoomValue := cDefaultZoomLevel;
WeaponTooltipTex:= nil;
@@ -2591,6 +2605,7 @@
isSpeed := false;
SpeedStart := 0;
fastUntilLag := false;
+ fastScrolling := false;
autoCameraOn := true;
cScriptName := '';
cSeed := '';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hedgewars/uVideoRec.pas Tue Aug 28 20:30:57 2012 +0400
@@ -0,0 +1,369 @@
+(*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * 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; version 2 of the License
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ *)
+
+
+{$INCLUDE "options.inc"}
+
+unit uVideoRec;
+
+{$IFNDEF USE_VIDEO_RECORDING}
+interface
+implementation
+end.
+{$ELSE}
+
+{$IFNDEF WIN32}
+ {$LINKLIB ../bin/libavwrapper.a}
+{$ENDIF}
+
+interface
+
+var flagPrerecording: boolean = false;
+
+function BeginVideoRecording: Boolean;
+function LoadNextCameraPosition(var newRealTicks, newGameTicks: LongInt): Boolean;
+procedure EncodeFrame;
+procedure StopVideoRecording;
+
+procedure BeginPreRecording;
+procedure StopPreRecording;
+procedure SaveCameraPosition;
+
+procedure freeModule;
+
+implementation
+
+uses uVariables, uUtils, GLunit, SDLh, SysUtils, uIO, uMisc, uTypes;
+
+type TAddFileLogRaw = procedure (s: pchar); cdecl;
+
+procedure AVWrapper_Init(
+ AddLog: TAddFileLogRaw;
+ filename, desc, soundFile, format, vcodec, acodec: PChar;
+ width, height, framerateNum, framerateDen, vquality: LongInt); cdecl; external {$IFDEF WIN32}'libavwrapper.dll'{$ENDIF};
+procedure AVWrapper_Close; cdecl; external {$IFDEF WIN32}'libavwrapper.dll'{$ENDIF};
+procedure AVWrapper_WriteFrame( pY, pCb, pCr: PByte ); cdecl; external {$IFDEF WIN32}'libavwrapper.dll'{$ENDIF};
+
+type TFrame = record
+ realTicks: LongWord;
+ gameTicks: LongWord;
+ CamX, CamY: LongInt;
+ zoom: single;
+ end;
+
+var YCbCr_Planes: array[0..2] of PByte;
+ RGB_Buffer: PByte;
+ cameraFile: File of TFrame;
+ audioFile: File;
+ numPixels: LongWord;
+ startTime, numFrames, curTime, progress, maxProgress: LongWord;
+ soundFilePath: shortstring;
+ thumbnailSaved : Boolean;
+
+function BeginVideoRecording: Boolean;
+var filename, desc: shortstring;
+begin
+ AddFileLog('BeginVideoRecording');
+
+{$IOCHECKS OFF}
+ // open file with prerecorded camera positions
+ filename:= UserPathPrefix + '/VideoTemp/' + RecPrefix + '.txtin';
+ Assign(cameraFile, filename);
+ Reset(cameraFile);
+ maxProgress:= FileSize(cameraFile);
+ if IOResult <> 0 then
+ begin
+ AddFileLog('Error: Could not read from ' + filename);
+ exit(false);
+ end;
+{$IOCHECKS ON}
+
+ // store some description in output file
+ desc:= '';
+ if UserNick <> '' then
+ desc+= 'Player: ' + UserNick + #10;
+ if recordFileName <> '' then
+ desc+= 'Record: ' + recordFileName + #10;
+ if cMapName <> '' then
+ desc+= 'Map: ' + cMapName + #10;
+ if Theme <> '' then
+ desc+= 'Theme: ' + Theme + #10;
+ desc+= 'prefix[' + RecPrefix + ']prefix';
+ desc+= #0;
+
+ filename:= UserPathPrefix + '/VideoTemp/' + RecPrefix + #0;
+ soundFilePath:= UserPathPrefix + '/VideoTemp/' + RecPrefix + '.sw' + #0;
+ cAVFormat+= #0;
+ cAudioCodec+= #0;
+ cVideoCodec+= #0;
+ AVWrapper_Init(@AddFileLogRaw, @filename[1], @desc[1], @soundFilePath[1], @cAVFormat[1], @cVideoCodec[1], @cAudioCodec[1],
+ cScreenWidth, cScreenHeight, cVideoFramerateNum, cVideoFramerateDen, cVideoQuality);
+
+ numPixels:= cScreenWidth*cScreenHeight;
+ YCbCr_Planes[0]:= GetMem(numPixels);
+ YCbCr_Planes[1]:= GetMem(numPixels div 4);
+ YCbCr_Planes[2]:= GetMem(numPixels div 4);
+
+ if (YCbCr_Planes[0] = nil) or (YCbCr_Planes[1] = nil) or (YCbCr_Planes[2] = nil) then
+ begin
+ AddFileLog('Error: Could not allocate memory for video recording (YCbCr buffer).');
+ exit(false);
+ end;
+
+ RGB_Buffer:= GetMem(4*numPixels);
+ if RGB_Buffer = nil then
+ begin
+ AddFileLog('Error: Could not allocate memory for video recording (RGB buffer).');
+ exit(false);
+ end;
+
+ curTime:= 0;
+ numFrames:= 0;
+ progress:= 0;
+ BeginVideoRecording:= true;
+end;
+
+procedure StopVideoRecording;
+begin
+ AddFileLog('StopVideoRecording');
+ FreeMem(YCbCr_Planes[0], numPixels);
+ FreeMem(YCbCr_Planes[1], numPixels div 4);
+ FreeMem(YCbCr_Planes[2], numPixels div 4);
+ FreeMem(RGB_Buffer, 4*numPixels);
+ Close(cameraFile);
+ AVWrapper_Close();
+ Erase(cameraFile);
+ DeleteFile(soundFilePath);
+ SendIPC(_S'v'); // inform frontend that we finished
+end;
+
+function pixel(x, y, color: LongInt): LongInt;
+begin
+ pixel:= RGB_Buffer[(cScreenHeight-y-1)*cScreenWidth*4 + x*4 + color];
+end;
+
+procedure EncodeFrame;
+var x, y, r, g, b: LongInt;
+ s: shortstring;
+begin
+ // read pixels from OpenGL
+ glReadPixels(0, 0, cScreenWidth, cScreenHeight, GL_RGBA, GL_UNSIGNED_BYTE, RGB_Buffer);
+
+ // convert to YCbCr 4:2:0 format
+ // Y
+ for y := 0 to cScreenHeight-1 do
+ for x := 0 to cScreenWidth-1 do
+ YCbCr_Planes[0][y*cScreenWidth + x]:= Byte(16 + ((16828*pixel(x,y,0) + 33038*pixel(x,y,1) + 6416*pixel(x,y,2)) shr 16));
+
+ // Cb and Cr
+ for y := 0 to cScreenHeight div 2 - 1 do
+ for x := 0 to cScreenWidth div 2 - 1 do
+ begin
+ r:= pixel(2*x,2*y,0) + pixel(2*x+1,2*y,0) + pixel(2*x,2*y+1,0) + pixel(2*x+1,2*y+1,0);
+ g:= pixel(2*x,2*y,1) + pixel(2*x+1,2*y,1) + pixel(2*x,2*y+1,1) + pixel(2*x+1,2*y+1,1);
+ b:= pixel(2*x,2*y,2) + pixel(2*x+1,2*y,2) + pixel(2*x,2*y+1,2) + pixel(2*x+1,2*y+1,2);
+ YCbCr_Planes[1][y*(cScreenWidth div 2) + x]:= Byte(128 + ((-2428*r - 4768*g + 7196*b) shr 16));
+ YCbCr_Planes[2][y*(cScreenWidth div 2) + x]:= Byte(128 + (( 7196*r - 6026*g - 1170*b) shr 16));
+ end;
+
+ AVWrapper_WriteFrame(YCbCr_Planes[0], YCbCr_Planes[1], YCbCr_Planes[2]);
+
+ // inform frontend that we have encoded new frame
+ s[0]:= #3;
+ s[1]:= 'p'; // p for progress
+ SDLNet_Write16(progress*10000 div maxProgress, @s[2]);
+ SendIPC(s);
+ inc(numFrames);
+end;
+
+function LoadNextCameraPosition(var newRealTicks, newGameTicks: LongInt): Boolean;
+var frame: TFrame;
+begin
+ // we need to skip or duplicate frames to match target framerate
+ while Int64(curTime)*cVideoFramerateNum <= Int64(numFrames)*cVideoFramerateDen*1000 do
+ begin
+ {$IOCHECKS OFF}
+ if eof(cameraFile) then
+ exit(false);
+ BlockRead(cameraFile, frame, 1);
+ {$IOCHECKS ON}
+ curTime:= frame.realTicks;
+ WorldDx:= frame.CamX;
+ WorldDy:= frame.CamY + cScreenHeight div 2;
+ zoom:= frame.zoom*cScreenWidth;
+ ZoomValue:= zoom;
+ inc(progress);
+ newRealTicks:= frame.realTicks;
+ newGameTicks:= frame.gameTicks;
+ end;
+ LoadNextCameraPosition:= true;
+end;
+
+// Callback which records sound.
+// This procedure may be called from different thread.
+procedure RecordPostMix(udata: pointer; stream: PByte; len: LongInt); cdecl;
+begin
+ udata:= udata; // avoid warning
+{$IOCHECKS OFF}
+ BlockWrite(audioFile, stream^, len);
+{$IOCHECKS ON}
+end;
+
+procedure SaveThumbnail;
+var thumbpath: shortstring;
+ k: LongInt;
+begin
+ thumbpath:= '/VideoTemp/' + RecPrefix;
+ AddFileLog('Saving thumbnail ' + thumbpath);
+ k:= max(max(cScreenWidth, cScreenHeight) div 400, 1); // here 400 is minimum size of thumbnail
+ MakeScreenshot(thumbpath, k);
+ thumbnailSaved:= true;
+end;
+
+// copy file (free pascal doesn't have copy file function)
+procedure CopyFile(src, dest: shortstring);
+var inF, outF: file;
+ buffer: array[0..1023] of byte;
+ result: LongInt;
+begin
+{$IOCHECKS OFF}
+ result:= 0; // avoid compiler hint
+
+ Assign(inF, src);
+ Reset(inF, 1);
+ if IOResult <> 0 then
+ begin
+ AddFileLog('Error: Could not read from ' + src);
+ exit;
+ end;
+
+ Assign(outF, dest);
+ Rewrite(outF, 1);
+ if IOResult <> 0 then
+ begin
+ AddFileLog('Error: Could not write to ' + dest);
+ exit;
+ end;
+
+ repeat
+ BlockRead(inF, buffer, 1024, result);
+ BlockWrite(outF, buffer, result);
+ until result < 1024;
+{$IOCHECKS ON}
+end;
+
+procedure BeginPreRecording;
+var format: word;
+ filename: shortstring;
+ frequency, channels: LongInt;
+begin
+ AddFileLog('BeginPreRecording');
+
+ thumbnailSaved:= false;
+ RecPrefix:= 'hw-' + FormatDateTime('YYYY-MM-DD_HH-mm-ss-z', Now());
+
+ // If this video is recorded from demo executed directly (without frontend)
+ // then we need to copy demo so that frontend will be able to find it later.
+ if recordFileName <> '' then
+ begin
+ if GameType <> gmtDemo then // this is save and game demo is not recording, abort
+ exit;
+ CopyFile(recordFileName, UserPathPrefix + '/VideoTemp/' + RecPrefix + '.hwd');
+ end;
+
+ Mix_QuerySpec(@frequency, @format, @channels);
+ AddFileLog('sound: frequency = ' + IntToStr(frequency) + ', format = ' + IntToStr(format) + ', channels = ' + IntToStr(channels));
+ if format <> $8010 then
+ begin
+ // TODO: support any audio format
+ AddFileLog('Error: Unexpected audio format ' + IntToStr(format));
+ exit;
+ end;
+
+{$IOCHECKS OFF}
+ // create sound file
+ filename:= UserPathPrefix + '/VideoTemp/' + RecPrefix + '.sw';
+ Assign(audioFile, filename);
+ Rewrite(audioFile, 1);
+ if IOResult <> 0 then
+ begin
+ AddFileLog('Error: Could not write to ' + filename);
+ exit;
+ end;
+
+ // create file with camera positions
+ filename:= UserPathPrefix + '/VideoTemp/' + RecPrefix + '.txtout';
+ Assign(cameraFile, filename);
+ Rewrite(cameraFile);
+ if IOResult <> 0 then
+ begin
+ AddFileLog('Error: Could not write to ' + filename);
+ exit;
+ end;
+
+ // save audio parameters in sound file
+ BlockWrite(audioFile, frequency, 4);
+ BlockWrite(audioFile, channels, 4);
+{$IOCHECKS ON}
+
+ // register callback for actual audio recording
+ Mix_SetPostMix(@RecordPostMix, nil);
+
+ startTime:= SDL_GetTicks();
+ flagPrerecording:= true;
+end;
+
+procedure StopPreRecording;
+begin
+ AddFileLog('StopPreRecording');
+ flagPrerecording:= false;
+
+ // call SDL_LockAudio because RecordPostMix may be executing right now
+ SDL_LockAudio();
+ Close(audioFile);
+ Close(cameraFile);
+ Mix_SetPostMix(nil, nil);
+ SDL_UnlockAudio();
+
+ if not thumbnailSaved then
+ SaveThumbnail();
+end;
+
+procedure SaveCameraPosition;
+var frame: TFrame;
+begin
+ if (not thumbnailSaved) and (ScreenFade = sfNone) then
+ SaveThumbnail();
+
+ frame.realTicks:= SDL_GetTicks() - startTime;
+ frame.gameTicks:= GameTicks;
+ frame.CamX:= WorldDx;
+ frame.CamY:= WorldDy - cScreenHeight div 2;
+ frame.zoom:= zoom/cScreenWidth;
+ BlockWrite(cameraFile, frame, 1);
+end;
+
+procedure freeModule;
+begin
+ if flagPrerecording then
+ StopPreRecording();
+end;
+
+end.
+
+{$ENDIF} // USE_VIDEO_RECORDING
--- a/hedgewars/uVisualGears.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uVisualGears.pas Tue Aug 28 20:30:57 2012 +0400
@@ -135,7 +135,7 @@
sp: real;
begin
AddVisualGear:= nil;
-if ((GameType = gmtSave) or (fastUntilLag and (GameType = gmtNet))) and // we are scrolling now
+if ((GameType = gmtSave) or (fastUntilLag and (GameType = gmtNet)) or fastScrolling) and // we are scrolling now
((Kind <> vgtCloud) and (not Critical)) then
exit;
--- a/hedgewars/uWorld.pas Mon Aug 27 16:52:50 2012 -0400
+++ b/hedgewars/uWorld.pas Tue Aug 28 20:30:57 2012 +0400
@@ -60,7 +60,8 @@
uCaptions,
uCursor,
uCommands,
- uMobile
+ uMobile,
+ uVideoRec
;
var cWaveWidth, cWaveHeight: LongInt;
@@ -80,6 +81,7 @@
stereoDepth: GLfloat;
isFirstFrame: boolean;
AMAnimType: LongInt;
+ recTexture: PTexture;
const cStereo_Sky = 0.0500;
cStereo_Horizon = 0.0250;
@@ -377,6 +379,8 @@
timeTexture:= nil;
FreeTexture(missionTex);
missionTex:= nil;
+ FreeTexture(recTexture);
+ recTexture:= nil;
end;
function GetAmmoMenuTexture(Ammo: PHHAmmo): PTexture;
@@ -954,7 +958,7 @@
//glPushMatrix;
//glScalef(1.0, 1.0, 1.0);
- if not isPaused then
+ if (not isPaused) and (GameType <> gmtRecord) then
MoveCamera;
if cStereoMode = smNone then
@@ -985,7 +989,7 @@
DrawWorldStereo(0, rmRightEye);
// detatch drawing from fbs
- glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
+ glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, defaultFrame);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
SetScale(cDefaultZoomLevel);
@@ -1581,6 +1585,33 @@
end
end;
+{$IFDEF USE_VIDEO_RECORDING}
+// during video prerecording draw red blinking circle and text 'rec'
+if flagPrerecording then
+ begin
+ if recTexture = nil then
+ begin
+ s:= 'rec';
+ tmpSurface:= TTF_RenderUTF8_Blended(Fontz[fntBig].Handle, Str2PChar(s), cWhiteColorChannels);
+ tmpSurface:= doSurfaceConversion(tmpSurface);
+ FreeTexture(recTexture);
+ recTexture:= Surface2Tex(tmpSurface, false);
+ SDL_FreeSurface(tmpSurface)
+ end;
+ DrawTexture( -(cScreenWidth shr 1) + 50, 20, recTexture);
+
+ // draw red circle
+ glDisable(GL_TEXTURE_2D);
+ Tint($FF, $00, $00, Byte(Round(127*(1 + sin(SDL_GetTicks()*0.007)))));
+ glBegin(GL_POLYGON);
+ for i:= 0 to 20 do
+ glVertex2f(-(cScreenWidth shr 1) + 30 + sin(i*2*Pi/20)*10, 35 + cos(i*2*Pi/20)*10);
+ glEnd();
+ Tint($FF, $FF, $FF, $FF);
+ glEnable(GL_TEXTURE_2D);
+ end;
+{$ENDIF}
+
SetScale(zoom);
// Cursor
@@ -1761,8 +1792,14 @@
if (not cHasFocus) and (GameState <> gsConfirm) then
ParseCommand('quit', true);
-if not cHasFocus then DampenAudio()
-else UndampenAudio();
+{$IFDEF USE_VIDEO_RECORDING}
+// do not change volume during prerecording as it will affect sound in video file
+if not flagPrerecording then
+{$ENDIF}
+ begin
+ if not cHasFocus then DampenAudio()
+ else UndampenAudio();
+ end;
end;
procedure SetUtilityWidgetState(ammoType: TAmmoType);
@@ -1819,6 +1856,7 @@
procedure initModule;
begin
fpsTexture:= nil;
+ recTexture:= nil;
FollowGear:= nil;
WindBarWidth:= 0;
bShowAmmoMenu:= false;
@@ -1850,7 +1888,9 @@
FreeTexture(timeTexture);
timeTexture:= nil;
FreeTexture(missionTex);
- missionTex:= nil
+ missionTex:= nil;
+ FreeTexture(recTexture);
+ recTexture:= nil;
end;
end.
--- a/project_files/hedgewars.pro Mon Aug 27 16:52:50 2012 -0400
+++ b/project_files/hedgewars.pro Tue Aug 28 20:30:57 2012 +0400
@@ -104,7 +104,12 @@
../QTfrontend/ui/dialog/input_password.h \
../QTfrontend/ui/widget/colorwidget.h \
../QTfrontend/model/HatModel.h \
- ../QTfrontend/model/GameStyleModel.h
+ ../QTfrontend/model/GameStyleModel.h \
+ ../QTfrontend/util/libav_iteraction.h \
+ ../QTfrontend/ui/page/pagevideos.h \
+ ../QTfrontend/net/recorder.h \
+ ../QTfrontend/ui/dialog/ask_quit.h \
+ ../QTfrontend/ui/dialog/upload_video.h
SOURCES += ../QTfrontend/model/ammoSchemeModel.cpp \
../QTfrontend/model/MapModel.cpp \
@@ -186,7 +191,12 @@
../QTfrontend/ui/dialog/input_password.cpp \
../QTfrontend/ui/widget/colorwidget.cpp \
../QTfrontend/model/HatModel.cpp \
- ../QTfrontend/model/GameStyleModel.cpp
+ ../QTfrontend/model/GameStyleModel.cpp \
+ ../QTfrontend/util/libav_iteraction.cpp \
+ ../QTfrontend/ui/page/pagevideos.cpp \
+ ../QTfrontend/net/recorder.cpp \
+ ../QTfrontend/ui/dialog/ask_quit.cpp \
+ ../QTfrontend/ui/dialog/upload_video.cpp
win32 {
SOURCES += ../QTfrontend/xfire.cpp