hedgewars/uVideoRec.pas
branchhedgeroid
changeset 15532 7030706266df
parent 15357 f53bfe04065e
child 15488 a7d54832fad6
--- a/hedgewars/uVideoRec.pas	Sun Oct 28 15:18:26 2012 +0100
+++ b/hedgewars/uVideoRec.pas	Fri Dec 06 22:20:53 2019 +0100
@@ -1,6 +1,6 @@
 (*
  * Hedgewars, a free turn based strategy game
- * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
+ * Copyright (c) 2004-2015 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
@@ -13,7 +13,7 @@
  *
  * 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
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  *)
 
 
@@ -27,13 +27,8 @@
 end.
 {$ELSE}
 
-{$IFNDEF WIN32}
-    {$LINKLIB ../bin/libavwrapper.a}
-{$ENDIF}
-{$IFDEF DARWIN}
-    {$LINKLIB bz2}
-    {$LINKFRAMEWORK CoreVideo}
-    {$LINKFRAMEWORK VideoDecodeAcceleration}
+{$IFNDEF WINDOWS}
+    {$linklib avwrapper}
 {$ENDIF}
 
 interface
@@ -41,7 +36,7 @@
 var flagPrerecording: boolean = false;
 
 function BeginVideoRecording: Boolean;
-function LoadNextCameraPosition(out newRealTicks, newGameTicks: LongInt): Boolean;
+function LoadNextCameraPosition(var newRealTicks, newGameTicks: LongInt): Boolean;
 procedure EncodeFrame;
 procedure StopVideoRecording;
 
@@ -53,17 +48,17 @@
 procedure freeModule;
 
 implementation
-
-uses uVariables, uUtils, GLunit, SDLh, SysUtils, uIO, uMisc, uTypes;
+uses uVariables, GLunit, SDLh, SysUtils, uUtils, uSound, uIO, uMisc, uTypes, uDebug;
 
 type TAddFileLogRaw = procedure (s: pchar); cdecl;
+const AvwrapperLibName = {$IFDEF WIN32_VCPKG}'avwrapper'{$ELSE}'libavwrapper'{$ENDIF};
 
-procedure AVWrapper_Init(
+function AVWrapper_Init(
               AddLog: TAddFileLogRaw;
               filename, desc, soundFile, format, vcodec, acodec: PChar;
-              width, height, framerateNum, framerateDen, vquality: LongInt); cdecl; external {$IFDEF WIN32}'libavwrapper.dll'{$ENDIF};
-procedure AVWrapper_Close; cdecl; external {$IFDEF WIN32}'libavwrapper.dll'{$ENDIF};
-procedure AVWrapper_WriteFrame( pY, pCb, pCr: PByte ); cdecl; external {$IFDEF WIN32}'libavwrapper.dll'{$ENDIF};
+              width, height, framerateNum, framerateDen, vquality: LongInt): LongInt; cdecl; external AvwrapperLibName;
+function AVWrapper_Close: LongInt; cdecl; external AvwrapperLibName;
+function AVWrapper_WriteFrame(rgb: PByte): LongInt; cdecl; external AvwrapperLibName;
 
 type TFrame = record
                   realTicks: LongWord;
@@ -72,34 +67,43 @@
                   zoom: single;
               end;
 
-var YCbCr_Planes: array[0..2] of PByte;
-    RGB_Buffer: PByte;
-    cameraFile: File of TFrame;
+var RGB_Buffer: PByte;
+    cameraFile: File;
+    cameraFileName: shortstring;
     audioFile: File;
     numPixels: LongWord;
     startTime, numFrames, curTime, progress, maxProgress: LongWord;
     soundFilePath: shortstring;
-    thumbnailSaved : Boolean;
+    thumbnailSaved: boolean;
+    recordAudio: boolean;
 
 function BeginVideoRecording: Boolean;
 var filename, desc: shortstring;
+    filenameA, descA, soundFilePathA, cAVFormatA, cVideoCodecA, cAudioCodecA: ansistring;
 begin
     AddFileLog('BeginVideoRecording');
 
 {$IOCHECKS OFF}
     // open file with prerecorded camera positions
-    filename:= UserPathPrefix + '/VideoTemp/' + RecPrefix + '.txtin';
-    Assign(cameraFile, filename);
-    Reset(cameraFile);
+    cameraFileName:= shortstring(UserPathPrefix) + '/VideoTemp/' + shortstring(RecPrefix) + '.txtin';
+    Assign(cameraFile, cameraFileName);
+    Reset(cameraFile, SizeOf(TFrame));
     maxProgress:= FileSize(cameraFile);
     if IOResult <> 0 then
     begin
-        AddFileLog('Error: Could not read from ' + filename);
+        AddFileLog('Error: Could not read from ' + cameraFileName);
         exit(false);
     end;
 {$IOCHECKS ON}
 
-    // store some description in output file
+    { Store some description in output file.
+    The comment must follow a particular format and must be in English.
+    This will be parsed by the frontend.
+    The frontend will parse lines of this format:
+        Key: Value
+    The key names will be localized in the frontend.
+    If you add a key/value pair, don't forget to add a localization
+    in the frontend! }
     desc:= '';
     if UserNick <> '' then
         desc:= desc + 'Player: ' + UserNick + #10;
@@ -111,28 +115,32 @@
         desc:= desc + 'Theme: ' + Theme + #10;
     desc:= desc + 'prefix[' + RecPrefix + ']prefix';
 
-    filename:= UserPathPrefix + '/VideoTemp/' + RecPrefix;
-    soundFilePath:= UserPathPrefix + '/VideoTemp/' + RecPrefix + '.sw';
+    filename:= shortstring(UserPathPrefix) + '/VideoTemp/' + shortstring(RecPrefix);
+
+    recordAudio:= (cAudioCodec <> 'no');
+    if recordAudio then
+        soundFilePath:= shortstring(UserPathPrefix) + '/VideoTemp/' + shortstring(RecPrefix) + '.sw'
+    else
+        soundFilePath:= '';
 
-    AVWrapper_Init(@AddFileLogRaw
-        , PChar(ansistring(filename))
-        , PChar(ansistring(desc))
-        , PChar(ansistring(soundFilePath))
-        , PChar(ansistring(cAVFormat))
-        , PChar(ansistring(cVideoCodec))
-        , PChar(ansistring(cAudioCodec))
-        , cScreenWidth, cScreenHeight, cVideoFramerateNum, cVideoFramerateDen, cVideoQuality);
+    filenameA:= ansistring(filename);
+    descA:= ansistring(desc);
+    soundFilePathA:= ansistring(soundFilePath);
+    cAVFormatA:= ansistring(cAVFormat);
+    cVideoCodecA:= ansistring(cVideoCodec);
+    cAudioCodecA:= ansistring(cAudioCodec);
+    if checkFails(AVWrapper_Init(@AddFileLogRaw
+        , PChar(filenameA)
+        , PChar(descA)
+        , PChar(soundFilePathA)
+        , PChar(cAVFormatA)
+        , PChar(cVideoCodecA)
+        , PChar(cAudioCodecA)
+        , cScreenWidth, cScreenHeight, cVideoFramerateNum, cVideoFramerateDen, cVideoQuality) >= 0,
+        'AVWrapper_Init failed',
+        true) then exit(false);
 
     numPixels:= cScreenWidth*cScreenHeight;
-    YCbCr_Planes[0]:= GetMem(numPixels);
-    YCbCr_Planes[1]:= GetMem(numPixels div 4);
-    YCbCr_Planes[2]:= GetMem(numPixels div 4);
-
-    if (YCbCr_Planes[0] = nil) or (YCbCr_Planes[1] = nil) or (YCbCr_Planes[2] = nil) then
-    begin
-        AddFileLog('Error: Could not allocate memory for video recording (YCbCr buffer).');
-        exit(false);
-    end;
 
     RGB_Buffer:= GetMem(4*numPixels);
     if RGB_Buffer = nil then
@@ -150,48 +158,34 @@
 procedure StopVideoRecording;
 begin
     AddFileLog('StopVideoRecording');
-    FreeMem(YCbCr_Planes[0], numPixels);
-    FreeMem(YCbCr_Planes[1], numPixels div 4);
-    FreeMem(YCbCr_Planes[2], numPixels div 4);
     FreeMem(RGB_Buffer, 4*numPixels);
     Close(cameraFile);
-    AVWrapper_Close();
-    Erase(cameraFile);
-    DeleteFile(soundFilePath);
+    if AVWrapper_Close() < 0 then
+        begin
+        OutError('AVWrapper_Close() has failed.', true);
+        end;
+{$IOCHECKS OFF}
+    if FileExists(cameraFileName) then
+        DeleteFile(cameraFileName)
+    else
+        AddFileLog('Warning: Tried to delete the cameraFile but it was already deleted');
+{$IOCHECKS ON}
+    if recordAudio and FileExists(soundFilePath) then
+        DeleteFile(soundFilePath);
     SendIPC(_S'v'); // inform frontend that we finished
 end;
 
-function pixel(x, y, color: LongInt): LongInt;
-begin
-    pixel:= RGB_Buffer[(cScreenHeight-y-1)*cScreenWidth*4 + x*4 + color];
-end;
-
 procedure EncodeFrame;
-var x, y, r, g, b: LongInt;
-    s: shortstring;
+var s: shortstring;
 begin
     // read pixels from OpenGL
     glReadPixels(0, 0, cScreenWidth, cScreenHeight, GL_RGBA, GL_UNSIGNED_BYTE, RGB_Buffer);
 
-    // convert to YCbCr 4:2:0 format
-    // Y
-    for y := 0 to cScreenHeight-1 do
-        for x := 0 to cScreenWidth-1 do
-            YCbCr_Planes[0][y*cScreenWidth + x]:= Byte(16 + ((16828*pixel(x,y,0) + 33038*pixel(x,y,1) + 6416*pixel(x,y,2)) shr 16));
-
-    // Cb and Cr
-    for y := 0 to cScreenHeight div 2 - 1 do
-        for x := 0 to cScreenWidth div 2 - 1 do
+    if AVWrapper_WriteFrame(RGB_Buffer) < 0 then
         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));
+        OutError('AVWrapper_WriteFrame(RGB_Buffer) has failed.', true);
         end;
 
-    AVWrapper_WriteFrame(YCbCr_Planes[0], YCbCr_Planes[1], YCbCr_Planes[2]);
-
     // inform frontend that we have encoded new frame
     s[0]:= #3;
     s[1]:= 'p'; // p for progress
@@ -200,16 +194,18 @@
     inc(numFrames);
 end;
 
-function LoadNextCameraPosition(out newRealTicks, newGameTicks: LongInt): Boolean;
-var frame: TFrame;
+function LoadNextCameraPosition(var newRealTicks, newGameTicks: LongInt): Boolean;
+var frame: TFrame = (realTicks: 0; gameTicks: 0; CamX: 0; CamY: 0; zoom: 0);
+    res: LongInt;
 begin
     // we need to skip or duplicate frames to match target framerate
     while Int64(curTime)*cVideoFramerateNum <= Int64(numFrames)*cVideoFramerateDen*1000 do
     begin
+    res:= 0;
     {$IOCHECKS OFF}
         if eof(cameraFile) then
             exit(false);
-        BlockRead(cameraFile, frame, 1);
+        BlockRead(cameraFile, frame, 1, res);
     {$IOCHECKS ON}
         curTime:= frame.realTicks;
         WorldDx:= frame.CamX;
@@ -226,10 +222,12 @@
 // Callback which records sound.
 // This procedure may be called from different thread.
 procedure RecordPostMix(udata: pointer; stream: PByte; len: LongInt); cdecl;
+var result: LongInt;
 begin
+    result:= 0; // avoid warning
     udata:= udata; // avoid warning
 {$IOCHECKS OFF}
-    BlockWrite(audioFile, stream^, len);
+    BlockWrite(audioFile, stream^, len, result);
 {$IOCHECKS ON}
 end;
 
@@ -237,10 +235,10 @@
 var thumbpath: shortstring;
     k: LongInt;
 begin
-    thumbpath:= '/VideoTemp/' + RecPrefix;
+    thumbpath:= '/VideoThumbnails/' + RecPrefix;
     AddFileLog('Saving thumbnail ' + thumbpath);
     k:= max(max(cScreenWidth, cScreenHeight) div 400, 1); // here 400 is minimum size of thumbnail
-    MakeScreenshot(thumbpath, k);
+    MakeScreenshot(thumbpath, k, 0);
     thumbnailSaved:= true;
 end;
 
@@ -248,10 +246,14 @@
 procedure CopyFile(src, dest: shortstring);
 var inF, outF: file;
     buffer: array[0..1023] of byte;
-    result: LongInt;
+    result, result2: LongInt;
+    i: integer;
 begin
 {$IOCHECKS OFF}
-    result:= 0; // avoid compiler hint
+    result:= 0; // avoid compiler hint and warning
+    result2:= 0; // avoid compiler hint and warning
+    for i:= 0 to 1023 do
+        buffer[i]:= 0;
 
     Assign(inF, src);
     Reset(inF, 1);
@@ -271,7 +273,7 @@
 
     repeat
         BlockRead(inF, buffer, 1024, result);
-        BlockWrite(outF, buffer, result);
+        BlockWrite(outF, buffer, result, result2);
     until result < 1024;
 {$IOCHECKS ON}
 end;
@@ -280,11 +282,21 @@
 var format: word;
     filename: shortstring;
     frequency, channels: LongInt;
+    result: LongInt;
 begin
+    result:= 0;
     AddFileLog('BeginPreRecording');
+    // Videos don't work if /lua command was used, so we forbid them
+    if luaCmdUsed then
+        begin
+        // TODO: Show message to player
+        PlaySound(sndDenied);
+        AddFileLog('Pre-recording prevented; /lua command was used before');
+        exit;
+        end;
 
     thumbnailSaved:= false;
-    RecPrefix:= 'hw-' + FormatDateTime('YYYY-MM-DD_HH-mm-ss-z', Now());
+    RecPrefix:= 'hw-' + FormatDateTime('YYYY-MM-DD_HH-mm-ss-z', TDateTime(Now()));
 
     // If this video is recorded from demo executed directly (without frontend)
     // then we need to copy demo so that frontend will be able to find it later.
@@ -292,46 +304,52 @@
     begin
         if GameType <> gmtDemo then // this is save and game demo is not recording, abort
             exit;
-        CopyFile(recordFileName, UserPathPrefix + '/VideoTemp/' + RecPrefix + '.hwd');
+        CopyFile(recordFileName, shortstring(UserPathPrefix) + '/VideoTemp/' + shortstring(RecPrefix) + '.hwd');
     end;
 
-    Mix_QuerySpec(@frequency, @format, @channels);
-    AddFileLog('sound: frequency = ' + IntToStr(frequency) + ', format = ' + IntToStr(format) + ', channels = ' + IntToStr(channels));
-    if format <> $8010 then
-    begin
-        // TODO: support any audio format
-        AddFileLog('Error: Unexpected audio format ' + IntToStr(format));
-        exit;
-    end;
+    if cIsSoundEnabled then
+        begin
+        Mix_QuerySpec(@frequency, @format, @channels);
+        AddFileLog('sound: frequency = ' + IntToStr(frequency) + ', format = ' + IntToStr(format) + ', channels = ' + IntToStr(channels));
+        if format <> $8010 then
+            begin
+            // TODO: support any audio format
+            AddFileLog('Error: Unexpected audio format ' + IntToStr(format));
+            exit;
+            end;
 
 {$IOCHECKS OFF}
-    // create sound file
-    filename:= UserPathPrefix + '/VideoTemp/' + RecPrefix + '.sw';
-    Assign(audioFile, filename);
-    Rewrite(audioFile, 1);
-    if IOResult <> 0 then
-    begin
-        AddFileLog('Error: Could not write to ' + filename);
-        exit;
-    end;
+        // create sound file
+        filename:= shortstring(UserPathPrefix) + '/VideoTemp/' + shortstring(RecPrefix) + '.sw';
+        Assign(audioFile, filename);
+        Rewrite(audioFile, 1);
+        if IOResult <> 0 then
+            begin
+            AddFileLog('Error: Could not write to ' + filename);
+            exit;
+            end;
+        end;
 
     // create file with camera positions
-    filename:= UserPathPrefix + '/VideoTemp/' + RecPrefix + '.txtout';
+    filename:= shortstring(UserPathPrefix) + '/VideoTemp/' + shortstring(RecPrefix) + '.txtout';
     Assign(cameraFile, filename);
-    Rewrite(cameraFile);
+    Rewrite(cameraFile, SizeOf(TFrame));
     if IOResult <> 0 then
-    begin
+        begin
         AddFileLog('Error: Could not write to ' + filename);
         exit;
-    end;
+        end;
 
-    // save audio parameters in sound file
-    BlockWrite(audioFile, frequency, 4);
-    BlockWrite(audioFile, channels, 4);
+    if cIsSoundEnabled then
+        begin
+        // save audio parameters in sound file
+        BlockWrite(audioFile, frequency, 4, result);
+        BlockWrite(audioFile, channels, 4, result);
 {$IOCHECKS ON}
 
-    // register callback for actual audio recording
-    Mix_SetPostMix(@RecordPostMix, nil);
+        // register callback for actual audio recording
+        Mix_SetPostMix(@RecordPostMix, nil);
+        end;
 
     startTime:= SDL_GetTicks();
     flagPrerecording:= true;
@@ -342,12 +360,18 @@
     AddFileLog('StopPreRecording');
     flagPrerecording:= false;
 
-    // call SDL_LockAudio because RecordPostMix may be executing right now
-    SDL_LockAudio();
-    Close(audioFile);
+    if cIsSoundEnabled then
+        begin
+        // call SDL_LockAudio because RecordPostMix may be executing right now
+        SDL_LockAudio();
+        Close(audioFile);
+        end;
     Close(cameraFile);
-    Mix_SetPostMix(nil, nil);
-    SDL_UnlockAudio();
+    if cIsSoundEnabled then
+        begin
+        Mix_SetPostMix(nil, nil);
+        SDL_UnlockAudio();
+        end;
 
     if not thumbnailSaved then
         SaveThumbnail();
@@ -355,7 +379,9 @@
 
 procedure SaveCameraPosition;
 var frame: TFrame;
+    result: LongInt;
 begin
+    result:= 0;
     if (not thumbnailSaved) and (ScreenFade = sfNone) then
         SaveThumbnail();
 
@@ -364,11 +390,15 @@
     frame.CamX:= WorldDx;
     frame.CamY:= WorldDy - cScreenHeight div 2;
     frame.zoom:= zoom/cScreenWidth;
-    BlockWrite(cameraFile, frame, 1);
+    BlockWrite(cameraFile, frame, 1, result);
 end;
 
 procedure initModule;
 begin
+    // we need to make sure these variables are initialized before the main loop
+    // or the wrapper will keep the default values of preinit
+    cScreenWidth:= max(cWindowedWidth, 640);
+    cScreenHeight:= max(cWindowedHeight, 480);
 end;
 
 procedure freeModule;