merge
authorStepan777 <stepik-777@mail.ru>
Thu, 07 Jun 2012 17:42:32 +0400
changeset 7194 d8e68cbca7ee
parent 7180 53ffc8853008 (diff)
parent 7192 e6c379b486d5 (current diff)
child 7196 4fba5519c37f
merge
hedgewars/SDLh.pas
hedgewars/uCommandHandlers.pas
hedgewars/uGearsHedgehog.pas
hedgewars/uInputHandler.pas
hedgewars/uTeams.pas
hedgewars/uTypes.pas
hedgewars/uUtils.pas
hedgewars/uWorld.pas
--- a/QTfrontend/binds.cpp	Thu Jun 07 01:28:39 2012 +0200
+++ b/QTfrontend/binds.cpp	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/QTfrontend/binds.h	Thu Jun 07 17:42:32 2012 +0400
@@ -21,7 +21,7 @@
 
 #include <QString>
 
-#define BINDS_NUMBER 44
+#define BINDS_NUMBER 45
 
 struct BindAction
 {
--- a/QTfrontend/game.cpp	Thu Jun 07 01:28:39 2012 +0200
+++ b/QTfrontend/game.cpp	Thu Jun 07 17:42:32 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);
 }
--- a/QTfrontend/game.h	Thu Jun 07 01:28:39 2012 +0200
+++ b/QTfrontend/game.h	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/QTfrontend/hwform.cpp	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/QTfrontend/hwform.h	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/QTfrontend/main.cpp	Thu Jun 07 17:42:32 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	Thu Jun 07 17:42:32 2012 +0400
@@ -0,0 +1,93 @@
+/*
+ * 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 << prefix;
+
+    return arguments;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/net/recorder.h	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/QTfrontend/net/tcpBase.cpp	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/QTfrontend/net/tcpBase.h	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/ArgParsers.inc	Thu Jun 07 17:42:32 2012 +0400
@@ -61,6 +61,11 @@
     else 
         cStereoMode:= TStereoMode(max(0, min(ord(high(TStereoMode)), tmp-6)));
     cLocaleFName:= ParamStr(17);
+    if ParamCount > 17 then
+        begin
+        cRecPrefix:= ParamStr(18);
+        GameType:= gmtRecord;
+        end;
 end;
 
 procedure setVideo(screenWidth: LongInt; screenHeight: LongInt; bitsStr: LongInt);
--- a/hedgewars/CMakeLists.txt	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/CMakeLists.txt	Thu Jun 07 17:42:32 2012 +0400
@@ -59,6 +59,7 @@
     uTypes.pas
     uUtils.pas
     uVariables.pas
+    uVideoRec.pas
     uVisualGears.pas
     uWorld.pas
     GSHandlers.inc
@@ -182,6 +183,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
@@ -222,10 +232,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	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/SDLh.pas	Thu Jun 07 17:42:32 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	Thu Jun 07 17:42:32 2012 +0400
@@ -0,0 +1,470 @@
+
+#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, g_Framerate;
+static int g_Frequency, g_Channels;
+
+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(enum CodecID codec_id)
+{
+#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 = codec_id;
+
+    // put parameters
+    g_pAudio->sample_fmt = AV_SAMPLE_FMT_S16;
+ //   pContext->bit_rate = 128000;
+    g_pAudio->sample_rate = g_Frequency;
+    g_pAudio->channels = g_Channels;
+
+    // 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()
+{
+    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(enum CodecID codec_id)
+{
+#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 = codec_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;
+    g_pVideo->time_base.num = 1;
+    //g_pVideo->gop_size = 12; /* emit one intra frame every twelve frames at most */
+    g_pVideo->pix_fmt = PIX_FMT_YUV420P;
+
+    // some formats want stream headers to be separate
+    if (g_pFormat->flags & AVFMT_GLOBALHEADER)
+        g_pVideo->flags |= CODEC_FLAG_GLOBAL_HEADER;
+
+    AVDictionary* pDict = NULL;
+    if (codec_id == CODEC_ID_H264)
+    {
+       // av_dict_set(&pDict, "tune", "animation", 0);
+       // av_dict_set(&pDict, "preset", "veryslow", 0);
+       av_dict_set(&pDict, "crf", "20", 0);
+    }
+    else
+    {
+        g_pVideo->flags |= CODEC_FLAG_QSCALE;
+       // g_pVideo->bit_rate = g_Width*g_Height*g_Framerate/4;
+        g_pVideo->global_quality = 15*FF_QP2LAMBDA;
+    }
+
+    // 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());
+    }
+
+    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_GetList()
+{
+    // initialize libav and register all codecs and formats
+    av_register_all();
+
+#if 0
+    AVOutputFormat* pFormat = NULL;
+    while (pFormat = av_oformat_next(pFormat))
+    {
+        Log("%s; %s; %s;\n", pFormat->name, pFormat->long_name, pFormat->mime_type);
+        
+        AVCodec* pCodec = NULL;
+        while (pCodec = av_codec_next(pCodec))
+        {
+            if (!av_codec_is_encoder(pCodec))
+                continue;
+            if (avformat_query_codec(pFormat, pCodec->id, FF_COMPLIANCE_NORMAL) != 1)
+                continue;
+            if (pCodec->type = AVMEDIA_TYPE_VIDEO)
+            {
+                if (pCodec->supported_framerate != NULL)
+                    continue;
+                Log("    Video: %s; %s;\n", pCodec->name, pCodec->long_name);
+            }
+            if (pCodec->type = AVMEDIA_TYPE_AUDIO)
+            {
+               /* if (pCodec->supported_samplerates == NULL)
+                    continue;
+                int i;
+                for(i = 0; i <)
+                    supported_samplerates*/
+                Log("    Audio: %s; %s;\n", pCodec->name, pCodec->long_name);
+            }
+        }
+   /*     struct AVCodecTag** pTags = pCur->codec_tag;
+        int i;
+        for (i = 0; ; i++)
+        {
+            enum CodecID id = av_codec_get_id(pTags, i);
+            if (id == CODEC_ID_NONE)
+                break;
+            AVCodec* pCodec = avcodec_find_encoder(id);
+            Log("    %i: %s; %s;\n", id, pCodec->name, pCodec->long_name);
+        }*/
+    }
+#endif
+}
+
+void AVWrapper_Init(void (*pAddFileLogRaw)(const char*), const char* pFilename, const char* pSoundFile, int Width, int Height, int Framerate, int Frequency, int Channels)
+{    
+    AddFileLogRaw = pAddFileLogRaw;
+    av_log_set_callback( &LogCallback );
+
+    g_Width = Width;
+    g_Height = Height;
+    g_Framerate = Framerate;
+    g_Frequency = Frequency;
+    g_Channels = Channels;
+
+    // initialize libav and register all codecs and formats
+    av_register_all();
+    
+    AVWrapper_GetList();
+
+    // allocate the output media context
+#if LIBAVCODEC_VERSION_MAJOR >= 54
+    avformat_alloc_output_context2(&g_pContainer, NULL, "mp4", pFilename);
+#else
+    g_pFormat = av_guess_format(NULL, pFilename, NULL);
+    if (!g_pFormat)
+        FatalError("guess_format");
+
+    // allocate the output media context
+    g_pContainer = avformat_alloc_context();
+    if (g_pContainer)
+    {
+        g_pContainer->oformat = g_pFormat;
+        snprintf(g_pContainer->filename, sizeof(g_pContainer->filename), "%s", pFilename);
+    }
+#endif
+    if (!g_pContainer)
+        FatalError("Could not allocate output context");
+
+    g_pFormat = g_pContainer->oformat;
+
+    enum CodecID VideoCodecID = g_pFormat->video_codec;//CODEC_ID_H264;
+    enum CodecID AudioCodecID = g_pFormat->audio_codec;
+
+    g_pVStream = NULL;
+    g_pAStream = NULL;
+    if (VideoCodecID != CODEC_ID_NONE)
+    {
+        g_pVCodec = avcodec_find_encoder(VideoCodecID);
+        if (!g_pVCodec)
+            FatalError("Video codec not found");
+        AddVideoStream(VideoCodecID);
+    }
+
+    if (AudioCodecID != CODEC_ID_NONE)
+    {
+        g_pACodec = avcodec_find_encoder(AudioCodecID);
+        if (!g_pACodec)
+            FatalError("Audio codec not found");
+        AddAudioStream(AudioCodecID);
+    }
+
+    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, pFilename, 1);
+
+    // open the output file, if needed
+    if (!(g_pFormat->flags & AVFMT_NOFILE))
+    {
+        if (avio_open(&g_pContainer->pb, pFilename, 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 each codec
+    if( g_pVStream )
+    {
+        avcodec_close(g_pVStream->codec);
+        av_free(g_pVFrame);
+    }
+    if( g_pAStream )
+    {
+        avcodec_close(g_pAStream->codec);
+        av_free(g_pAFrame);
+        av_free(g_pSamples);
+        fclose(g_pSoundFile);
+    }
+
+    // free the streams
+    int i;
+    for (i = 0; i < g_pContainer->nb_streams; i++)
+    {
+        av_freep(&g_pContainer->streams[i]->codec);
+        av_freep(&g_pContainer->streams[i]);
+    }
+
+    // close the output file
+    if (!(g_pFormat->flags & AVFMT_NOFILE))
+        avio_close(g_pContainer->pb);
+
+    // free the stream 
+    av_free(g_pContainer);
+}
--- a/hedgewars/hwengine.pas	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/hwengine.pas	Thu Jun 07 17:42:32 2012 +0400
@@ -31,7 +31,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
+     SysUtils, uTypes, uVariables, uCommands, uUtils, uCaptions, uDebug, uCommandHandlers, uLandPainted, uVideoRec
      {$IFDEF SDL13}, uTouch{$ENDIF}{$IFDEF ANDROID}, GLUnit{$ENDIF};
 
 {$IFDEF HWLIBRARY}
@@ -101,6 +101,9 @@
 
     SwapBuffers;
 
+    if flagPrerecording then
+        SaveCameraPosition;
+
     if flagMakeCapture then
         begin
         flagMakeCapture:= false;
@@ -261,6 +264,32 @@
     end;
 end;
 
+////////////////
+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;
+
 ///////////////
 procedure Game{$IFDEF HWLIBRARY}(gameArgs: PPChar); cdecl; export{$ENDIF};
 var p: TPathType;
@@ -327,11 +356,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 +402,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 +498,7 @@
         //uAIAmmoTests does not need to be freed
         //uAIActions does not need to be freed
         uStore.freeModule;
+        uVideoRec.freeModule;
     end;
 
     uIO.freeModule;
@@ -530,7 +573,7 @@
         if (ParamCount = 3) and ((ParamStr(3) = '--stats-only') or (ParamStr(3) = 'landpreview')) then
             internalSetGameTypeLandPreviewFromParameters()
         else
-            if (ParamCount = cDefaultParamNum) then
+            if (ParamCount = cDefaultParamNum) or (ParamCount = cDefaultParamNum+1) then
                 internalStartGameWithParameters()
             else
                 playReplayFileWithParameters();
--- a/hedgewars/uCommandHandlers.pas	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/uCommandHandlers.pas	Thu Jun 07 17:42:32 2012 +0400
@@ -26,7 +26,7 @@
 procedure freeModule;
 
 implementation
-uses uCommands, uTypes, uVariables, uIO, uDebug, uConsts, uScript, uUtils, SDLh, uRandom, uCaptions;
+uses SysUtils, uCommands, uTypes, uVariables, uIO, uDebug, uConsts, uScript, uUtils, SDLh, uRandom, uCaptions, uVideoRec;
 
 var prevGState: TGameState = gsConfirm;
 
@@ -529,6 +529,15 @@
 flagMakeCapture:= true
 end;
 
+procedure chRecord(var s: shortstring);
+begin
+s:= s; // avoid compiler hint
+if flagPrerecording then
+    StopPreRecording
+else
+    BeginPreRecording(FormatDateTime('YYYY-MM-DD_HH-mm-ss', Now()));
+end;
+
 procedure chSetMap(var s: shortstring);
 begin
 if isDeveloperMode then
@@ -864,6 +873,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/uGame.pas	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/uGame.pas	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/uGearsHedgehog.pas	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/uIO.pas	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/uInputHandler.pas	Thu Jun 07 17:42:32 2012 +0400
@@ -237,6 +237,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	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/uStore.pas	Thu Jun 07 17:42:32 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}, glut;
 
 //type TGPUVendor = (gvUnknown, gvNVIDIA, gvATI, gvIntel, gvApple);
 
@@ -438,6 +439,29 @@
 IMG_Quit();
 end;
 
+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;
+
 procedure StoreRelease(reload: boolean);
 var ii: TSprite;
     ai: TAmmoType;
@@ -511,15 +535,13 @@
                 end;
             end;
         end;
+    if defaultFrame <> 0 then
+        DeleteFramebuffer(defaultFrame, depthv, texv);
 {$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 +650,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,51 +696,53 @@
 {$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))));
+    AddFileLog('  |----- Number of auxilary buffers: ' + inttostr(AuxBufNum));
+    AddFileLog('  \----- Extensions: ');
+    AddFileLogRaw(glGetString(GL_EXTENSIONS));
+    AddFileLog('');
     //TODO: don't have the Extensions line trimmed but slipt it into multiple lines
 
+    defaultFrame:= 0;
+    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;
+
 {$IFNDEF S3D_DISABLED}
     if (cStereoMode = smHorizontal) or (cStereoMode = smVertical) or (cStereoMode = smAFR) then
     begin
         // 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 +1016,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 +1209,8 @@
 
 procedure SwapBuffers; inline;
 begin
+    if GameType = gmtRecord then
+        exit;
 {$IFDEF SDL13}
     SDL_GL_SwapWindow(SDLwindow);
 {$ELSE}
--- a/hedgewars/uTeams.pas	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/uTeams.pas	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/uTypes.pas	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/uUtils.pas	Thu Jun 07 17:42:32 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,7 @@
 
 {$IFDEF DEBUGFILE}
 var f: textfile;
+    logMutex: TRTLCriticalSection; // mutex for debug file
 {$ENDIF}
 var CharArray: array[byte] of Char;
 
@@ -303,11 +305,23 @@
 begin
 s:= s;
 {$IFDEF DEBUGFILE}
+EnterCriticalSection(logMutex);
 writeln(f, inttostr(GameTicks)  + ': ' + s);
-flush(f)
+flush(f);
+LeaveCriticalSection(logMutex);
 {$ENDIF}
 end;
 
+procedure AddFileLogRaw(s: pchar); cdecl;
+begin
+s:= s;
+{$IFDEF DEBUGFILE}
+EnterCriticalSection(logMutex);
+write(f, s);
+flush(f);
+LeaveCriticalSection(logMutex);
+{$ENDIF}
+end;
 
 function CheckCJKFont(s: ansistring; font: THWFont): THWFont;
 var l, i : LongInt;
@@ -400,6 +414,7 @@
         logfileBase:= 'game'
     else
         logfileBase:= 'preview';
+    InitCriticalSection(logMutex);
 {$I-}
 {$IFDEF MOBILE}
     {$IFDEF IPHONEOS} Assign(f,'../Documents/hw-' + logfileBase + '.log'); {$ENDIF}
@@ -436,6 +451,7 @@
     writeln(f, 'halt at ' + inttostr(GameTicks) + ' ticks. TurnTimeLeft = ' + inttostr(TurnTimeLeft));
     flush(f);
     close(f);
+    DoneCriticalSection(logMutex);
 {$ENDIF}
 end;
 
--- a/hedgewars/uVariables.pas	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/uVariables.pas	Thu Jun 07 17:42:32 2012 +0400
@@ -52,6 +52,7 @@
     cReadyDelay     : Longword    = 5000;
     cStereoMode     : TStereoMode = smNone;
     cOnlyStats      : boolean = False;
+    cRecPrefix      : shortstring = '';
 //////////////////////////
     cMapName        : shortstring = '';
 
@@ -62,6 +63,7 @@
     isSpeed         : boolean;
 
     fastUntilLag    : boolean;
+    fastScrolling   : boolean;
     autoCameraOn    : boolean;
 
     GameTicks       : LongWord;
@@ -2437,6 +2439,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;
@@ -2561,7 +2567,7 @@
     cExplosives     := 2;
 
     GameState       := Low(TGameState);
-    GameType        := gmtLocal;
+//    GameType        := gmtLocal;
     zoom            := cDefaultZoomLevel;
     ZoomValue       := cDefaultZoomLevel;
     WeaponTooltipTex:= nil;
@@ -2577,6 +2583,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	Thu Jun 07 17:42:32 2012 +0400
@@ -0,0 +1,272 @@
+(*
+ * 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;
+
+function BeginPreRecording(filePrefix: shortstring): Boolean;
+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: PChar; width, height, framerate, frequency, channels: 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: PChar; width, height, framerate, frequency, channels: 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;
+
+    framerate: Int64 = 30;
+    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 + '.mp4' + #0;
+    soundFilePath:= UserPathPrefix + '/Videos/' + cRecPrefix + '.hwsound' + #0;
+    AVWrapper_Init(@AddFileLogRaw, @filename[1], @soundFilePath[1], cScreenWidth, cScreenHeight, framerate, frequency, channels);
+
+    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;
+
+function BeginPreRecording(filePrefix: shortstring): Boolean;
+var format: word;
+    filename: shortstring;
+begin
+    AddFileLog('BeginPreRecording');
+
+    nframes:= 0;
+    firstTick:= SDL_GetTicks();
+
+    Mix_QuerySpec(@frequency, @format, @channels);
+    if format <> $8010 then
+    begin
+        // TODO: support any audio format
+        AddFileLog('Error: Unexpected audio format ' + IntToStr(format));
+        exit(false);
+    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(false);
+    end;
+
+    filename:= UserPathPrefix + '/Videos/' + filePrefix + '.txtout';
+    Assign(cameraFile, filename);
+    Rewrite(cameraFile);
+    if IOResult <> 0 then
+    begin
+        AddFileLog('Error: Could not write to ' + filename);
+        exit(false);
+    end;
+{$IOCHECKS ON}
+    WriteLn(cameraFile, inttostr(frequency) + ' ' + inttostr(channels));
+
+    // register callback for actual audio recording
+    Mix_SetPostMix(@RecordPostMix, nil);
+
+    flagPrerecording:= true;
+    BeginPreRecording:= 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)*framerate > nframes*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	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/uVisualGears.pas	Thu Jun 07 17:42:32 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	Thu Jun 07 01:28:39 2012 +0200
+++ b/hedgewars/uWorld.pas	Thu Jun 07 17:42:32 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,31 @@
         end
     end;
 
+// 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;
+
 SetScale(zoom);
 
 // Cursor
@@ -1752,8 +1781,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 +1843,7 @@
 procedure initModule;
 begin
     fpsTexture:= nil;
+    recTexture:= nil;
     FollowGear:= nil;
     WindBarWidth:= 0;
     bShowAmmoMenu:= false;
@@ -1840,7 +1874,9 @@
     FreeTexture(timeTexture);
     timeTexture:= nil;
     FreeTexture(missionTex);
-    missionTex:= nil
+    missionTex:= nil;
+    FreeTexture(recTexture);
+    recTexture:= nil;
 end;
 
 end.
--- a/project_files/hedgewars.pro	Thu Jun 07 01:28:39 2012 +0200
+++ b/project_files/hedgewars.pro	Thu Jun 07 17:42:32 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