merge
authorStepan777 <stepik-777@mail.ru>
Mon, 11 Jun 2012 17:56:10 +0400
changeset 7231 f484455dd055
parent 7198 5debd5fe526e (diff)
parent 7226 c3b4fc19fcd4 (current diff)
child 7235 baa69bd025d9
merge
QTfrontend/game.cpp
hedgewars/CMakeLists.txt
hedgewars/options.inc
hedgewars/uCommandHandlers.pas
hedgewars/uConsts.pas
hedgewars/uInputHandler.pas
hedgewars/uVariables.pas
--- a/QTfrontend/binds.cpp	Mon Jun 11 01:16:43 2012 +0200
+++ b/QTfrontend/binds.cpp	Mon Jun 11 17:56:10 2012 +0400
@@ -64,5 +64,6 @@
     {"+volup",  "0",    QT_TRANSLATE_NOOP("binds", "volume up"),    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 Jun 11 01:16:43 2012 +0200
+++ b/QTfrontend/binds.h	Mon Jun 11 17:56:10 2012 +0400
@@ -21,7 +21,7 @@
 
 #include <QString>
 
-#define BINDS_NUMBER 44
+#define BINDS_NUMBER 45
 
 struct BindAction
 {
--- a/QTfrontend/game.cpp	Mon Jun 11 01:16:43 2012 +0200
+++ b/QTfrontend/game.cpp	Mon Jun 11 17:56:10 2012 +0400
@@ -53,20 +53,18 @@
 {
     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);
     }
     SetGameState(gsStopped);
 }
@@ -335,6 +333,8 @@
     arguments << QString::number(config->translateQuality());
     arguments << QString::number(config->stereoMode());
     arguments << tr("en.txt");
+    arguments << "30"; // framerate num
+    arguments << "1";  // framerate den
 
     return arguments;
 }
--- a/QTfrontend/game.h	Mon Jun 11 01:16:43 2012 +0200
+++ b/QTfrontend/game.h	Mon Jun 11 17:56:10 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/hwform.cpp	Mon Jun 11 01:16:43 2012 +0200
+++ b/QTfrontend/hwform.cpp	Mon Jun 11 17:56:10 2012 +0400
@@ -90,6 +90,7 @@
 #include "drawmapwidget.h"
 #include "mouseoverfilter.h"
 #include "roomslistmodel.h"
+#include "recorder.h"
 
 #include "DataManager.h"
 
@@ -1357,7 +1358,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 +1369,56 @@
                          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))
+    QDir videosDir(cfgdir->absolutePath() + "/Videos/");
+    QStringList files = videosDir.entryList(QStringList("*.txtout"), QDir::Files);
+    for (QStringList::iterator str = files.begin(); str != files.end(); str++)
     {
-        ShowErrorMessage(tr("Cannot save record to file %1").arg(filename));
-        return ;
+        str->chop(7); // remove ".txtout"
+        // need to rename this file to not open it twice
+        videosDir.rename(*str + ".txtout", *str + ".txtin");
+        HWRecorder* pRecorder = new HWRecorder(config);
+        pRecorder->EncodeVideo(record, *str);
     }
-    demofile.write(demo);
-    demofile.close();
 }
 
 void HWForm::startTraining(const QString & scriptName)
--- a/QTfrontend/hwform.h	Mon Jun 11 01:16:43 2012 +0200
+++ b/QTfrontend/hwform.h	Mon Jun 11 17:56:10 2012 +0400
@@ -114,7 +114,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);
--- a/QTfrontend/main.cpp	Mon Jun 11 01:16:43 2012 +0200
+++ b/QTfrontend/main.cpp	Mon Jun 11 17:56:10 2012 +0400
@@ -199,6 +199,7 @@
         checkForDir(cfgdir->absolutePath() + "/Screenshots");
         checkForDir(cfgdir->absolutePath() + "/Teams");
         checkForDir(cfgdir->absolutePath() + "/Logs");
+        checkForDir(cfgdir->absolutePath() + "/Videos");
     }
 
     datadir->cd(bindir->absolutePath());
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/net/recorder.cpp	Mon Jun 11 17:56:10 2012 +0400
@@ -0,0 +1,101 @@
+/*
+ * 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 "recorder.h"
+#include "gameuiconfig.h"
+#include "hwconsts.h"
+#include "game.h"
+
+HWRecorder::HWRecorder(GameUIConfig * config) :
+    TCPBase(false)
+{
+    this->config = config;
+}
+
+HWRecorder::~HWRecorder()
+{
+}
+
+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);
+        if (msg.at(1) == '?')
+            SendIPC("!");
+    }
+}
+
+void HWRecorder::EncodeVideo( const QByteArray & record, const QString & prefix )
+{
+    this->prefix = prefix;
+
+    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"));
+
+    // run engine
+    Start();
+}
+
+QStringList HWRecorder::getArguments()
+{
+    QStringList arguments;
+    QRect resolution = config->vid_Resolution();
+    arguments << cfgdir->absolutePath();
+    arguments << QString::number(resolution.width());
+    arguments << QString::number(resolution.height());
+    arguments << QString::number(config->bitDepth()); // 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 << "30"; // framerate num
+    arguments << "1";  // framerate den
+    arguments << prefix;
+    arguments << "mp4";
+    arguments << "mpeg4"; // arguments << "libx264";
+    arguments << "5"; // video quality
+    arguments << "medium";
+    arguments << "libmp3lame";
+    arguments << "5"; // audio quality
+
+    return arguments;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/net/recorder.h	Mon Jun 11 17:56:10 2012 +0400
@@ -0,0 +1,52 @@
+/*
+ * 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 HWRecorder : public TCPBase
+{
+        Q_OBJECT
+    public:
+        HWRecorder(GameUIConfig * config);
+        virtual ~HWRecorder();
+
+        void EncodeVideo(const QByteArray & record, const QString & prefix);
+
+    protected:
+        virtual QStringList getArguments();
+        virtual void onClientRead();
+        virtual void onClientDisconnect();
+
+    signals:
+
+    public slots:
+
+    private:
+        GameUIConfig * config;
+        QString prefix;
+};
+
+#endif // RECORDER_H
--- a/QTfrontend/net/tcpBase.cpp	Mon Jun 11 01:16:43 2012 +0200
+++ b/QTfrontend/net/tcpBase.cpp	Mon Jun 11 17:56:10 2012 +0400
@@ -26,8 +26,8 @@
 
 #include "hwconsts.h"
 
-QList<TCPBase*> srvsList;
-QPointer<QTcpServer> TCPBase::IPCServer(0);
+//QList<TCPBase*> srvsList;
+//QPointer<QTcpServer> TCPBase::IPCServer(0);
 
 TCPBase::~TCPBase()
 {
@@ -35,7 +35,7 @@
 
 TCPBase::TCPBase(bool demoMode) :
     m_isDemoMode(demoMode),
-    IPCSocket(0)
+    IPCSocket(0), IPCServer(0)
 {
     if(!IPCServer)
     {
@@ -67,7 +67,7 @@
     SendToClientFirst();
 }
 
-void TCPBase::RealStart()
+void TCPBase::/*Real*/Start()
 {
     connect(IPCServer, SIGNAL(newConnection()), this, SLOT(NewConnection()));
     IPCSocket = 0;
@@ -88,8 +88,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();
 }
@@ -109,13 +109,13 @@
                           .arg(error) + bindir->absolutePath() + "/hwengine)");
 }
 
+/*
 void TCPBase::tcpServerReady()
 {
     disconnect(srvsList.takeFirst(), SIGNAL(isReadyNow()), this, SLOT(tcpServerReady()));
 
     RealStart();
 }
-
 void TCPBase::Start()
 {
     if(srvsList.isEmpty())
@@ -130,7 +130,7 @@
     }
 
     RealStart();
-}
+}*/
 
 void TCPBase::onClientRead()
 {
--- a/QTfrontend/net/tcpBase.h	Mon Jun 11 01:16:43 2012 +0200
+++ b/QTfrontend/net/tcpBase.h	Mon Jun 11 17:56:10 2012 +0400
@@ -62,11 +62,11 @@
         virtual void onClientDisconnect();
         virtual void SendToClientFirst();
 
+      //  void RealStart();
     private:
-        static QPointer<QTcpServer> IPCServer;
+        /*static*/ QPointer<QTcpServer> IPCServer;
 
         bool m_isDemoMode;
-        void RealStart();
         QPointer<QTcpSocket> IPCSocket;
 
     private slots:
@@ -75,7 +75,7 @@
         void ClientRead();
         void StartProcessError(QProcess::ProcessError error);
 
-        void tcpServerReady();
+      //  void tcpServerReady();
 };
 
 #endif // _TCPBASE_INCLUDED
--- a/hedgewars/ArgParsers.inc	Mon Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/ArgParsers.inc	Mon Jun 11 17:56:10 2012 +0400
@@ -61,8 +61,28 @@
     else 
         cStereoMode:= TStereoMode(max(0, min(ord(high(TStereoMode)), tmp-6)));
     cLocaleFName:= ParamStr(17);
+{$IFDEF USE_VIDEO_RECORDING}
+    cVideoFramerateNum:= StrToInt(ParamStr(18));
+    cVideoFramerateDen:= StrToInt(ParamStr(19));
+{$ENDIF}
 end;
 
+{$IFDEF USE_VIDEO_RECORDING}
+procedure internalStartVideoRecordingWithParameters();
+begin
+    internalStartGameWithParameters();
+    GameType:= gmtRecord;
+    cRecPrefix:= ParamStr(20);
+    cAVFormat:= ParamStr(21);
+    cVideoCodec:= ParamStr(22);
+    cVideoQuality:= StrToInt(ParamStr(23));
+    cVideoPreset:= ParamStr(24);
+    cAudioCodec:= ParamStr(25);
+  // cRecordAudio:= cAudioCodec <> 'no';
+    cAudioQuality:= StrToInt(ParamStr(26));
+end;
+{$ENDIF}
+
 procedure setVideo(screenWidth: LongInt; screenHeight: LongInt; bitsStr: LongInt);
 begin
     cScreenWidth:= screenWidth;
--- a/hedgewars/CMakeLists.txt	Mon Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/CMakeLists.txt	Mon Jun 11 17:56:10 2012 +0400
@@ -59,6 +59,7 @@
     uTypes.pas
     uUtils.pas
     uVariables.pas
+    uVideoRec.pas
     uVisualGears.pas
     uWorld.pas
     GSHandlers.inc
@@ -184,6 +185,15 @@
 
 set(fpc_flags ${noexecstack_flags} ${pascal_flags} ${hwengine_project})
 
+IF (WIN32)
+    set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)
+    include_directories(${CMAKE_SOURCE_DIR}/misc/winutils/include)
+    link_directories(${CMAKE_SOURCE_DIR}/misc/winutils/lib)
+    add_library(avwrapper SHARED avwrapper.c)
+    target_link_libraries(avwrapper avcodec avformat avutil)
+ELSE()
+    add_library(avwrapper STATIC avwrapper.c)
+ENDIF()
 
 IF(NOT APPLE)
     #here is the command for standard executables or for shared library
@@ -224,10 +234,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 Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/SDLh.pas	Mon Jun 11 17:56:10 2012 +0400
@@ -829,6 +829,8 @@
     TMixMusic = record
                 end;
 
+    TPostMix = procedure(udata: pointer; stream: PByte; len: LongInt); cdecl;
+
     {* SDL_net *}
     TIPAddress = record
                 host: LongWord;
@@ -959,6 +961,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;
@@ -1013,6 +1018,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;
@@ -1040,6 +1046,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	Mon Jun 11 17:56:10 2012 +0400
@@ -0,0 +1,446 @@
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdarg.h>
+#include "libavformat/avformat.h"
+
+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 int g_Frequency, g_Channels;
+static int g_VQuality, g_AQuality;
+static AVRational g_Framerate;
+static const char* g_pPreset;
+
+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 LIBAVCODEC_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)
+        FatalError("Could not allocate audio stream");
+    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
+    if (g_AQuality > 100)
+        g_pAudio->bit_rate = g_AQuality;
+    else
+    {
+        g_pAudio->flags |= CODEC_FLAG_QSCALE;
+        g_pAudio->global_quality = g_AQuality*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 (avcodec_open2(g_pAudio, g_pACodec, NULL) < 0)
+        FatalError("Could not open audio codec %s", g_pACodec->long_name);
+
+#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)
+        FatalError("Could not allocate frame");
+}
+
+// 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 LIBAVCODEC_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;
+    g_pVideo->height = g_Height;
+    /* 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;
+    }
+
+    AVDictionary* pDict = NULL;
+    if (strcmp(g_pVCodec->name, "libx264") == 0)
+        av_dict_set(&pDict, "preset", g_pPreset, 0);
+
+    // some formats want stream headers to be separate
+    if (g_pFormat->flags & AVFMT_GLOBALHEADER)
+        g_pVideo->flags |= CODEC_FLAG_GLOBAL_HEADER;
+
+    // open the codec
+    if (avcodec_open2(g_pVideo, g_pVCodec, &pDict) < 0)
+        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* pSoundFile,
+         const char* pFormatName,
+         const char* pVCodecName,
+         const char* pACodecName,
+         const char* pVPreset,
+         int Width, int Height,
+         int FramerateNum, int FramerateDen,
+         int Frequency, int Channels,
+         int VQuality, int AQuality)
+{    
+    AddFileLogRaw = pAddFileLogRaw;
+    av_log_set_callback( &LogCallback );
+
+    g_Width = Width;
+    g_Height = Height;
+    g_Framerate.num = FramerateNum;
+    g_Framerate.den = FramerateDen;
+    g_Frequency = Frequency;
+    g_Channels = Channels;
+    g_VQuality = VQuality;
+    g_AQuality = AQuality;
+    g_pPreset = pVPreset;
+
+    // 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;
+
+    // append extesnion to filename
+    snprintf(g_pContainer->filename, sizeof(g_pContainer->filename),
+             "%s.%*s",
+             pFilename,
+             strcspn(g_pFormat->extensions, ","), g_pFormat->extensions);
+
+    // 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)
+        AddAudioStream();
+    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...");
+
+    if (g_pAStream)
+    {
+        g_pSoundFile = fopen(pSoundFile, "rb");
+        if (!g_pSoundFile)
+            FatalError("Could not open %s", pSoundFile);
+    }
+
+    // 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)", pFilename);
+    }
+
+    // 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 Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/hwengine.pas	Mon Jun 11 17:56:10 2012 +0400
@@ -32,6 +32,7 @@
 uses SDLh, uMisc, uConsole, uGame, uConsts, uLand, uAmmos, uVisualGears, uGears, uStore, uWorld, uInputHandler, uSound,
      uScript, uTeams, uStats, uIO, uLocale, uChat, uAI, uAIMisc, uRandom, 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};
 
 {$IFDEF HWLIBRARY}
@@ -101,6 +102,11 @@
 
     SwapBuffers;
 
+{$IFDEF USE_VIDEO_RECORDING}
+    if flagPrerecording then
+        SaveCameraPosition;
+{$ENDIF}
+
     if flagMakeCapture then
         begin
         flagMakeCapture:= false;
@@ -261,6 +267,33 @@
     end;
 end;
 
+{$IFDEF USE_VIDEO_RECORDING}
+procedure RecorderMainLoop;
+var CurrTime, PrevTime: LongInt;
+begin
+    if not BeginVideoRecording() then
+        exit;
+    DoTimer(0); // gsLandGen -> gsStart
+    DoTimer(0); // gsStart -> gsGame
+
+    CurrTime:= LoadNextCameraPosition();
+    fastScrolling:= true;
+    DoTimer(CurrTime);
+    fastScrolling:= false;
+    while true do
+    begin
+        EncodeFrame();
+        PrevTime:= CurrTime;
+        CurrTime:= LoadNextCameraPosition();
+        if CurrTime = -1 then
+            break;
+        DoTimer(CurrTime - PrevTime);
+        IPCCheckSock();
+    end;
+    StopVideoRecording();
+end;
+{$ENDIF}
+
 ///////////////
 procedure Game{$IFDEF HWLIBRARY}(gameArgs: PPChar); cdecl; export{$ENDIF};
 var p: TPathType;
@@ -327,11 +360,16 @@
     SDLTry(TTF_Init() <> -1, true);
     WriteLnToConsole(msgOK);
 
-    // show main window
-    if cFullScreen then
-        ParseCommand('fullscr 1', true)
+    if GameType = gmtRecord then
+        InitOffscreenOpenGL()
     else
-        ParseCommand('fullscr 0', true);
+        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();
@@ -368,12 +406,20 @@
 
     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();
+
+    if GameType = gmtRecord then
+        RecorderMainLoop()
+    else
+        MainLoop();
 
     // clean up all the memory allocated
     freeEverything(true);
@@ -456,6 +502,7 @@
         //uAIAmmoTests does not need to be freed
         //uAIActions does not need to be freed
         uStore.freeModule;
+        uVideoRec.freeModule;
     end;
 
     uIO.freeModule;
@@ -529,11 +576,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/options.inc	Mon Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/options.inc	Mon Jun 11 17:56:10 2012 +0400
@@ -63,6 +63,7 @@
     {$ENDIF}
 {$ENDIF}
 
+    {$DEFINE USE_VIDEO_RECORDING}
 
 {$IFDEF PAS2C}
     {$DEFINE NOCONSOLE}
--- a/hedgewars/uCommandHandlers.pas	Mon Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uCommandHandlers.pas	Mon Jun 11 17:56:10 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;
 
@@ -529,6 +530,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
@@ -865,6 +877,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 Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uConsts.pas	Mon Jun 11 17:56:10 2012 +0400
@@ -27,7 +27,12 @@
 
 const
     sfMax = 1000;
+{$IFNDEF USE_VIDEO_RECORDING}
     cDefaultParamNum = 17;
+{$ELSE}
+    cDefaultParamNum = 19;
+    cVideorecParamNum = cDefaultParamNum + 7;
+{$ENDIF}
 
     // message constants
     errmsgCreateSurface   = 'Error creating SDL surface';
--- a/hedgewars/uGame.pas	Mon Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uGame.pas	Mon Jun 11 17:56:10 2012 +0400
@@ -39,17 +39,20 @@
     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
-        Lag:= Lag * 10
-    else
-        if cOnlyStats then
-            Lag:= High(LongInt);
+    if (GameType = gmtDemo) then 
+        if isSpeed then
+            Lag:= Lag * 10
+        else
+            if cOnlyStats then
+                Lag:= High(LongInt);
+    end;
 PlayNextVoice;
 i:= 1;
 while (GameState <> gsExit) and (i <= Lag) do
--- a/hedgewars/uGearsHedgehog.pas	Mon Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uGearsHedgehog.pas	Mon Jun 11 17:56:10 2012 +0400
@@ -601,7 +601,7 @@
                     if (not (HH^.Hedgehog^.Team^.ExtDriven 
                     or (HH^.Hedgehog^.BotLevel > 0)))
                     or (HH^.Hedgehog^.Team^.Clan^.ClanIndex = LocalClan)
-                    or (GameType = gmtDemo)  then
+                    or (GameType in [gmtDemo, gmtRecord])  then
                         begin
                         s:= trammo[Ammoz[a].NameId] + ' (+' + IntToStr(Ammoz[a].NumberInCase) + ')';
                         AddCaption(s, HH^.Hedgehog^.Team^.Clan^.Color, capgrpAmmoinfo);
--- a/hedgewars/uIO.pas	Mon Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uIO.pas	Mon Jun 11 17:56:10 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 Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uInputHandler.pas	Mon Jun 11 17:56:10 2012 +0400
@@ -249,6 +249,7 @@
 DefaultBinds[KeyNameToCode(_S'0')]:= '+volup';
 DefaultBinds[KeyNameToCode(_S'9')]:= '+voldown';
 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/uStore.pas	Mon Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uStore.pas	Mon Jun 11 17:56:10 2012 +0400
@@ -40,13 +40,14 @@
 procedure ShowWeaponTooltip(x, y: LongInt);
 procedure FreeWeaponTooltip;
 procedure MakeCrossHairs;
+procedure InitOffscreenOpenGL;
 
 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} {$IFDEF USE_VIDEO_RECORDING}, glut {$ENDIF};
 
 //type TGPUVendor = (gvUnknown, gvNVIDIA, gvATI, gvIntel, gvApple);
 
@@ -438,6 +439,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;
@@ -511,15 +537,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;
@@ -628,6 +654,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))) + ')');
@@ -673,14 +700,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 AuxBufNum > 0 then
+        begin
+            glDrawBuffer(GL_AUX0);
+            glReadBuffer(GL_AUX0);
+            AddFileLog('Using auxilary buffer for video recording.');
+        end 
+        else 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
+        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
@@ -688,36 +744,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;
@@ -991,6 +1022,19 @@
 WeaponTooltipTex:= nil
 end;
 
+procedure InitOffscreenOpenGL;
+var ArgCount: LongInt;
+    PrgName: pchar;
+begin
+    ArgCount:= 1;
+    PrgName:= 'hwengine';
+    glutInit(@ArgCount, @PrgName);
+    glutInitWindowSize(cScreenWidth, cScreenHeight);
+    glutCreateWindow('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;
+
 procedure chFullScr(var s: shortstring);
 var flags: Longword = 0;
     reinit: boolean = false;
@@ -1171,6 +1215,8 @@
 
 procedure SwapBuffers; inline;
 begin
+    if GameType = gmtRecord then
+        exit;
 {$IFDEF SDL13}
     SDL_GL_SwapWindow(SDLwindow);
 {$ELSE}
--- a/hedgewars/uTeams.pas	Mon Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uTeams.pas	Mon Jun 11 17:56:10 2012 +0400
@@ -530,7 +530,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 Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uTypes.pas	Mon Jun 11 17:56:10 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 Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uUtils.pas	Mon Jun 11 17:56:10 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;
 
@@ -303,11 +307,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;
@@ -400,6 +424,9 @@
         logfileBase:= 'game'
     else
         logfileBase:= 'preview';
+{$IFDEF USE_VIDEO_RECORDING}
+    InitCriticalSection(logMutex);
+{$ENDIF}
 {$I-}
 {$IFDEF MOBILE}
     {$IFDEF IPHONEOS} Assign(f,'../Documents/hw-' + logfileBase + '.log'); {$ENDIF}
@@ -436,6 +463,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 Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uVariables.pas	Mon Jun 11 17:56:10 2012 +0400
@@ -52,6 +52,17 @@
     cReadyDelay     : Longword    = 5000;
     cStereoMode     : TStereoMode = smNone;
     cOnlyStats      : boolean = False;
+{$IFDEF USE_VIDEO_RECORDING}
+    cRecPrefix      : shortstring;
+    cAVFormat       : shortstring;
+    cVideoCodec     : shortstring;
+    cVideoFramerateNum : Int64;
+    cVideoFramerateDen : Int64;
+    cVideoQuality      : LongInt;
+    cVideoPreset    : shortstring;
+    cAudioCodec     : shortstring;
+    cAudioQuality   : LongInt;
+{$ENDIF}
 //////////////////////////
     cMapName        : shortstring = '';
 
@@ -62,6 +73,7 @@
     isSpeed         : boolean;
 
     fastUntilLag    : boolean;
+    fastScrolling   : boolean;
     autoCameraOn    : boolean;
 
     GameTicks       : LongWord;
@@ -2439,6 +2451,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;
@@ -2563,7 +2579,7 @@
     cExplosives     := 2;
 
     GameState       := Low(TGameState);
-    GameType        := gmtLocal;
+//    GameType        := gmtLocal;
     zoom            := cDefaultZoomLevel;
     ZoomValue       := cDefaultZoomLevel;
     WeaponTooltipTex:= nil;
@@ -2579,6 +2595,7 @@
     isInMultiShoot  := false;
     isSpeed         := false;
     fastUntilLag    := false;
+    fastScrolling   := false;
     autoCameraOn    := true;
     cScriptName     := '';
     cSeed           := '';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hedgewars/uVideoRec.pas	Mon Jun 11 17:56:10 2012 +0400
@@ -0,0 +1,283 @@
+(*
+ * 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;
+
+{$IFDEF UNIX}
+    {$LINKLIB avwrapper}
+    {$LINKLIB avutil}
+    {$LINKLIB avcodec}
+    {$LINKLIB avformat}
+{$ENDIF}
+
+interface
+
+var flagPrerecording: boolean = false;
+
+function BeginVideoRecording: Boolean;
+function LoadNextCameraPosition: LongInt;
+procedure EncodeFrame;
+procedure StopVideoRecording;
+
+procedure BeginPreRecording;
+procedure StopPreRecording;
+procedure SaveCameraPosition;
+
+procedure freeModule;
+
+implementation
+
+uses uVariables, uUtils, GLunit, SDLh, SysUtils;
+
+{$IFDEF WIN32}
+const AVWrapperLibName = 'libavwrapper.dll';
+{$ENDIF}
+
+type TAddFileLogRaw = procedure (s: pchar); cdecl;
+
+{$IFDEF WIN32}
+procedure AVWrapper_Init(
+              AddLog: TAddFileLogRaw;
+              filename, soundFile, format, vcodec, acodec, preset: PChar;
+              width, height, framerateNum, framerateDen, frequency, channels, vquality, aquality: LongInt); cdecl; external AVWrapperLibName;
+procedure AVWrapper_Close; cdecl; external AVWrapperLibName;
+procedure AVWrapper_WriteFrame( pY, pCb, pCr: PByte ); cdecl; external AVWrapperLibName;
+{$ELSE}
+procedure AVWrapper_Init(
+              AddLog: TAddFileLogRaw;
+              filename, soundFile, format, vcodec, acodec, preset: PChar;
+              width, height, framerateNum, framerateDen, frequency, channels, vquality, aquality: LongInt); cdecl; external;
+procedure AVWrapper_Close; cdecl; external;
+procedure AVWrapper_WriteFrame( pY, pCb, pCr: PByte ); cdecl; external;
+{$ENDIF}
+
+var YCbCr_Planes: array[0..2] of PByte;
+    RGB_Buffer: PByte;
+
+    frequency, channels: LongInt;
+
+    cameraFile: TextFile;
+    audioFile: File;
+    
+    numPixels: LongInt;
+
+    firstTick, nframes: Int64;
+    
+    cameraFilePath, soundFilePath: shortstring;
+
+function BeginVideoRecording: Boolean;
+var filename: shortstring;
+begin
+    AddFileLog('BeginVideoRecording');
+
+    numPixels:= cScreenWidth*cScreenHeight;
+
+{$IOCHECKS OFF}
+    // open file with prerecorded camera positions
+    cameraFilePath:= UserPathPrefix + '/Videos/' + cRecPrefix + '.txtin';
+    Assign(cameraFile, cameraFilePath);
+    Reset(cameraFile);
+    if IOResult <> 0 then
+    begin
+        AddFileLog('Error: Could not read from ' + cameraFilePath);
+        exit(false);
+    end;
+
+    ReadLn(cameraFile, frequency, channels);
+{$IOCHECKS ON}
+
+    filename:= UserPathPrefix + '/Videos/' + cRecPrefix + #0;
+    soundFilePath:= UserPathPrefix + '/Videos/' + cRecPrefix + '.hwsound' + #0;
+    cAVFormat+= #0;
+    cAudioCodec+= #0;
+    cVideoCodec+= #0;
+    cVideoPreset+= #0;
+    AVWrapper_Init(@AddFileLogRaw, @filename[1], @soundFilePath[1], @cAVFormat[1], @cVideoCodec[1], @cAudioCodec[1], @cVideoPreset[1],
+                   cScreenWidth, cScreenHeight, cVideoFramerateNum, cVideoFramerateDen, frequency, channels, cAudioQuality, cVideoQuality);
+
+    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;
+
+    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();
+    DeleteFile(cameraFilePath);
+    DeleteFile(soundFilePath);
+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;
+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]);
+end;
+
+function LoadNextCameraPosition: LongInt;
+var NextTime: LongInt;
+    NextZoom: LongInt;
+    NextWorldDx, NextWorldDy: LongInt;
+begin
+{$IOCHECKS OFF}
+    if eof(cameraFile) then
+        exit(-1);
+    ReadLn(cameraFile, NextTime, NextWorldDx, NextWorldDy, NextZoom);
+{$IOCHECKS ON}
+    if NextTime = 0 then
+        exit(-1);
+    WorldDx:= NextWorldDx;
+    WorldDy:= NextWorldDy;
+    zoom:= NextZoom/10000;
+    ZoomValue:= NextZoom/10000;
+    LoadNextCameraPosition:= NextTime;
+end;
+
+// this procedure may be called from different thread
+procedure RecordPostMix(udata: pointer; stream: PByte; len: LongInt); cdecl;
+begin
+    udata:= udata;
+{$IOCHECKS OFF}
+    BlockWrite(audioFile, stream^, len);
+{$IOCHECKS ON}
+end;
+
+procedure BeginPreRecording;
+var format: word;
+    filePrefix, filename: shortstring;
+begin
+    AddFileLog('BeginPreRecording');
+    
+    nframes:= 0;
+    firstTick:= SDL_GetTicks();
+
+    filePrefix:= FormatDateTime('YYYY-MM-DD_HH-mm-ss', Now());
+
+    Mix_QuerySpec(@frequency, @format, @channels);
+    if format <> $8010 then
+    begin
+        // TODO: support any audio format
+        AddFileLog('Error: Unexpected audio format ' + IntToStr(format));
+        exit;
+    end;
+
+{$IOCHECKS OFF}
+    filename:= UserPathPrefix + '/Videos/' + filePrefix + '.hwsound';
+    Assign(audioFile, filename);
+    Rewrite(audioFile, 1);
+    if IOResult <> 0 then
+    begin
+        AddFileLog('Error: Could not write to ' + filename);
+        exit;
+    end;
+
+    filename:= UserPathPrefix + '/Videos/' + filePrefix + '.txtout';
+    Assign(cameraFile, filename);
+    Rewrite(cameraFile);
+    if IOResult <> 0 then
+    begin
+        AddFileLog('Error: Could not write to ' + filename);
+        exit;
+    end;
+{$IOCHECKS ON}
+    WriteLn(cameraFile, inttostr(frequency) + ' ' + inttostr(channels));
+
+    // register callback for actual audio recording
+    Mix_SetPostMix(@RecordPostMix, nil);
+
+    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();
+end;
+
+procedure SaveCameraPosition;
+var Ticks: LongInt;
+begin
+    Ticks:= SDL_GetTicks();
+    while (Ticks - firstTick)*cVideoFramerateNum > nframes*cVideoFramerateDen*1000 do
+    begin
+        WriteLn(cameraFile, inttostr(GameTicks) + ' ' + inttostr(WorldDx) + ' ' + inttostr(WorldDy) + ' ' + inttostr(Round(zoom*10000)));
+        inc(nframes);
+    end;
+end;
+
+procedure freeModule;
+begin
+    if flagPrerecording then
+        StopPreRecording();
+end;
+
+end.
--- a/hedgewars/uVisualGears.pas	Mon Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uVisualGears.pas	Mon Jun 11 17:56:10 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 Jun 11 01:16:43 2012 +0200
+++ b/hedgewars/uWorld.pas	Mon Jun 11 17:56:10 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;
@@ -381,6 +383,8 @@
     timeTexture:= nil;
     FreeTexture(missionTex);
     missionTex:= nil;
+    FreeTexture(recTexture);
+    recTexture:= nil;
 end;
 
 function GetAmmoMenuTexture(Ammo: PHHAmmo): PTexture;
@@ -958,7 +962,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
@@ -989,7 +993,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);
 
@@ -1570,6 +1574,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
@@ -1752,8 +1783,12 @@
 if (not cHasFocus) and (GameState <> gsConfirm) then
     ParseCommand('quit', true);
 
-if not cHasFocus then DampenAudio()
-else UndampenAudio();
+// do not change volume during prerecording as it will affect sound in video file
+if not flagPrerecording then
+    begin
+    if not cHasFocus then DampenAudio()
+    else UndampenAudio();
+    end;
 end;
 
 procedure SetUtilityWidgetState(ammoType: TAmmoType);
@@ -1810,6 +1845,7 @@
 procedure initModule;
 begin
     fpsTexture:= nil;
+    recTexture:= nil;
     FollowGear:= nil;
     WindBarWidth:= 0;
     bShowAmmoMenu:= false;
@@ -1840,7 +1876,9 @@
     FreeTexture(timeTexture);
     timeTexture:= nil;
     FreeTexture(missionTex);
-    missionTex:= nil
+    missionTex:= nil;
+    FreeTexture(recTexture);
+    recTexture:= nil;
 end;
 
 end.
--- a/project_files/hedgewars.pro	Mon Jun 11 01:16:43 2012 +0200
+++ b/project_files/hedgewars.pro	Mon Jun 11 17:56:10 2012 +0400
@@ -104,7 +104,8 @@
     ../QTfrontend/ui/dialog/input_password.h \
     ../QTfrontend/ui/widget/colorwidget.h \
     ../QTfrontend/model/HatModel.h \
-    ../QTfrontend/model/GameStyleModel.h
+    ../QTfrontend/model/GameStyleModel.h \
+    ../QTfrontend/recorder.h
 
 SOURCES += ../QTfrontend/model/ammoSchemeModel.cpp \
     ../QTfrontend/model/MapModel.cpp \
@@ -186,7 +187,8 @@
     ../QTfrontend/ui/dialog/input_password.cpp \
     ../QTfrontend/ui/widget/colorwidget.cpp \
     ../QTfrontend/model/HatModel.cpp \
-    ../QTfrontend/model/GameStyleModel.cpp
+    ../QTfrontend/model/GameStyleModel.cpp \
+    ../QTfrontend/recorder.cpp
 
 win32 {
     SOURCES += ../QTfrontend/xfire.cpp