1 (*
2  * Hedgewars, a free turn based strategy game
3  * Copyright (c) 2004-2015 Andrey Korotaev <unC0Rr@gmail.com>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; version 2 of the License
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17  *)
18 
19 
20 {$INCLUDE "options.inc"}
21 
22 unit uVideoRec;
23 
24 {$IFNDEF USE_VIDEO_RECORDING}
25 interface
26 implementation
27 end.
28 {$ELSE}
29 
30 {$IFNDEF WINDOWS}
31     {$linklib avwrapper}
32 {$ENDIF}
33 
34 interface
35 
36 var flagPrerecording: boolean = false;
37 
BeginVideoRecordingnull38 function BeginVideoRecording: Boolean;
LoadNextCameraPositionnull39 function LoadNextCameraPosition(var newRealTicks, newGameTicks: LongInt): Boolean;
40 procedure EncodeFrame;
41 procedure StopVideoRecording;
42 
43 procedure BeginPreRecording;
44 procedure StopPreRecording;
45 procedure SaveCameraPosition;
46 
47 procedure initModule;
48 procedure freeModule;
49 
50 implementation
51 uses uVariables, GLunit, SDLh, SysUtils, uUtils, uSound, uIO, uMisc, uTypes, uDebug;
52 
53 type TAddFileLogRaw = procedure (s: pchar); cdecl;
54 const AvwrapperLibName = {$IFDEF WIN32_VCPKG}'avwrapper'{$ELSE}'libavwrapper'{$ENDIF};
55 
AVWrapper_Initnull56 function AVWrapper_Init(
57               AddLog: TAddFileLogRaw;
58               filename, desc, soundFile, format, vcodec, acodec: PChar;
59               width, height, framerateNum, framerateDen, vquality: LongInt): LongInt; cdecl; external AvwrapperLibName;
AVWrapper_Closenull60 function AVWrapper_Close: LongInt; cdecl; external AvwrapperLibName;
AVWrapper_WriteFramenull61 function AVWrapper_WriteFrame(rgb: PByte): LongInt; cdecl; external AvwrapperLibName;
62 
63 type TFrame = record
64                   realTicks: LongWord;
65                   gameTicks: LongWord;
66                   CamX, CamY: LongInt;
67                   zoom: single;
68               end;
69 
70 var RGB_Buffer: PByte;
71     cameraFile: File;
72     cameraFileName: shortstring;
73     audioFile: File;
74     numPixels: LongWord;
75     startTime, numFrames, curTime, progress, maxProgress: LongWord;
76     soundFilePath: shortstring;
77     thumbnailSaved: boolean;
78     recordAudio: boolean;
79 
BeginVideoRecordingnull80 function BeginVideoRecording: Boolean;
81 var filename, desc: shortstring;
82     filenameA, descA, soundFilePathA, cAVFormatA, cVideoCodecA, cAudioCodecA: ansistring;
83 begin
84     AddFileLog('BeginVideoRecording');
85 
86 {$IOCHECKS OFF}
87     // open file with prerecorded camera positions
88     cameraFileName:= shortstring(UserPathPrefix) + '/VideoTemp/' + shortstring(RecPrefix) + '.txtin';
89     Assign(cameraFile, cameraFileName);
90     Reset(cameraFile, SizeOf(TFrame));
91     maxProgress:= FileSize(cameraFile);
92     if IOResult <> 0 then
93     begin
94         AddFileLog('Error: Could not read from ' + cameraFileName);
95         exit(false);
96     end;
97 {$IOCHECKS ON}
98 
99     { Store some description in output file.
100     The comment must follow a particular format and must be in English.
101     This will be parsed by the frontend.
102     The frontend will parse lines of this format:
103         Key: Value
104     The key names will be localized in the frontend.
105     If you add a key/value pair, don't forget to add a localization
106     in the frontend! }
107     desc:= '';
108     if UserNick <> '' then
109         desc:= desc + 'Player: ' + UserNick + #10;
110     if recordFileName <> '' then
111         desc:= desc + 'Record: ' + recordFileName + #10;
112     if cMapName <> '' then
113         desc:= desc + 'Map: ' + cMapName + #10;
114     if Theme <> '' then
115         desc:= desc + 'Theme: ' + Theme + #10;
116     desc:= desc + 'prefix[' + RecPrefix + ']prefix';
117 
118     filename:= shortstring(UserPathPrefix) + '/VideoTemp/' + shortstring(RecPrefix);
119 
120     recordAudio:= (cAudioCodec <> 'no');
121     if recordAudio then
122         soundFilePath:= shortstring(UserPathPrefix) + '/VideoTemp/' + shortstring(RecPrefix) + '.sw'
123     else
124         soundFilePath:= '';
125 
126     filenameA:= ansistring(filename);
127     descA:= ansistring(desc);
128     soundFilePathA:= ansistring(soundFilePath);
129     cAVFormatA:= ansistring(cAVFormat);
130     cVideoCodecA:= ansistring(cVideoCodec);
131     cAudioCodecA:= ansistring(cAudioCodec);
132     if checkFails(AVWrapper_Init(@AddFileLogRaw
133         , PChar(filenameA)
134         , PChar(descA)
135         , PChar(soundFilePathA)
136         , PChar(cAVFormatA)
137         , PChar(cVideoCodecA)
138         , PChar(cAudioCodecA)
139         , cScreenWidth, cScreenHeight, cVideoFramerateNum, cVideoFramerateDen, cVideoQuality) >= 0,
140         'AVWrapper_Init failed',
141         true) then exit(false);
142 
143     numPixels:= cScreenWidth*cScreenHeight;
144 
145     RGB_Buffer:= GetMem(4*numPixels);
146     if RGB_Buffer = nil then
147     begin
148         AddFileLog('Error: Could not allocate memory for video recording (RGB buffer).');
149         exit(false);
150     end;
151 
152     curTime:= 0;
153     numFrames:= 0;
154     progress:= 0;
155     BeginVideoRecording:= true;
156 end;
157 
158 procedure StopVideoRecording;
159 begin
160     AddFileLog('StopVideoRecording');
161     FreeMem(RGB_Buffer, 4*numPixels);
162     Close(cameraFile);
163     if AVWrapper_Close() < 0 then
164         begin
165         OutError('AVWrapper_Close() has failed.', true);
166         end;
167 {$IOCHECKS OFF}
168     if FileExists(cameraFileName) then
169         DeleteFile(cameraFileName)
170     else
171         AddFileLog('Warning: Tried to delete the cameraFile but it was already deleted');
172 {$IOCHECKS ON}
173     if recordAudio and FileExists(soundFilePath) then
174         DeleteFile(soundFilePath);
175     SendIPC(_S'v'); // inform frontend that we finished
176 end;
177 
178 procedure EncodeFrame;
179 var s: shortstring;
180 begin
181     // read pixels from OpenGL
182     glReadPixels(0, 0, cScreenWidth, cScreenHeight, GL_RGBA, GL_UNSIGNED_BYTE, RGB_Buffer);
183 
184     if AVWrapper_WriteFrame(RGB_Buffer) < 0 then
185         begin
186         OutError('AVWrapper_WriteFrame(RGB_Buffer) has failed.', true);
187         end;
188 
189     // inform frontend that we have encoded new frame
190     s[0]:= #3;
191     s[1]:= 'p'; // p for progress
192     SDLNet_Write16(progress*10000 div maxProgress, @s[2]);
193     SendIPC(s);
194     inc(numFrames);
195 end;
196 
LoadNextCameraPositionnull197 function LoadNextCameraPosition(var newRealTicks, newGameTicks: LongInt): Boolean;
198 var frame: TFrame = (realTicks: 0; gameTicks: 0; CamX: 0; CamY: 0; zoom: 0);
199     res: LongInt;
200 begin
201     // we need to skip or duplicate frames to match target framerate
202     while Int64(curTime)*cVideoFramerateNum <= Int64(numFrames)*cVideoFramerateDen*1000 do
203     begin
204     res:= 0;
205     {$IOCHECKS OFF}
206         if eof(cameraFile) then
207             exit(false);
208         BlockRead(cameraFile, frame, 1, res);
209     {$IOCHECKS ON}
210         curTime:= frame.realTicks;
211         WorldDx:= frame.CamX;
212         WorldDy:= frame.CamY + cScreenHeight div 2;
213         zoom:= frame.zoom*cScreenWidth;
214         ZoomValue:= zoom;
215         inc(progress);
216         newRealTicks:= frame.realTicks;
217         newGameTicks:= frame.gameTicks;
218     end;
219     LoadNextCameraPosition:= true;
220 end;
221 
222 // Callback which records sound.
223 // This procedure may be called from different thread.
224 procedure RecordPostMix(udata: pointer; stream: PByte; len: LongInt); cdecl;
225 var result: LongInt;
226 begin
227     result:= 0; // avoid warning
228     udata:= udata; // avoid warning
229 {$IOCHECKS OFF}
230     BlockWrite(audioFile, stream^, len, result);
231 {$IOCHECKS ON}
232 end;
233 
234 procedure SaveThumbnail;
235 var thumbpath: shortstring;
236     k: LongInt;
237 begin
238     thumbpath:= '/VideoThumbnails/' + RecPrefix;
239     AddFileLog('Saving thumbnail ' + thumbpath);
240     k:= max(max(cScreenWidth, cScreenHeight) div 400, 1); // here 400 is minimum size of thumbnail
241     MakeScreenshot(thumbpath, k, 0);
242     thumbnailSaved:= true;
243 end;
244 
245 // copy file (free pascal doesn't have copy file function)
246 procedure CopyFile(src, dest: shortstring);
247 var inF, outF: file;
248     buffer: array[0..1023] of byte;
249     result, result2: LongInt;
250     i: integer;
251 begin
252 {$IOCHECKS OFF}
253     result:= 0; // avoid compiler hint and warning
254     result2:= 0; // avoid compiler hint and warning
255     for i:= 0 to 1023 do
256         buffer[i]:= 0;
257 
258     Assign(inF, src);
259     Reset(inF, 1);
260     if IOResult <> 0 then
261     begin
262         AddFileLog('Error: Could not read from ' + src);
263         exit;
264     end;
265 
266     Assign(outF, dest);
267     Rewrite(outF, 1);
268     if IOResult <> 0 then
269     begin
270         AddFileLog('Error: Could not write to ' + dest);
271         exit;
272     end;
273 
274     repeat
275         BlockRead(inF, buffer, 1024, result);
276         BlockWrite(outF, buffer, result, result2);
277     until result < 1024;
278 {$IOCHECKS ON}
279 end;
280 
281 procedure BeginPreRecording;
282 var format: word;
283     filename: shortstring;
284     frequency, channels: LongInt;
285     result: LongInt;
286 begin
287     result:= 0;
288     AddFileLog('BeginPreRecording');
289     // Videos don't work if /lua command was used, so we forbid them
290     if luaCmdUsed then
291         begin
292         // TODO: Show message to player
293         PlaySound(sndDenied);
294         AddFileLog('Pre-recording prevented; /lua command was used before');
295         exit;
296         end;
297 
298     thumbnailSaved:= false;
299     RecPrefix:= 'hw-' + FormatDateTime('YYYY-MM-DD_HH-mm-ss-z', TDateTime(Now()));
300 
301     // If this video is recorded from demo executed directly (without frontend)
302     // then we need to copy demo so that frontend will be able to find it later.
303     if recordFileName <> '' then
304     begin
305         if GameType <> gmtDemo then // this is save and game demo is not recording, abort
306             exit;
307         CopyFile(recordFileName, shortstring(UserPathPrefix) + '/VideoTemp/' + shortstring(RecPrefix) + '.hwd');
308     end;
309 
310     if cIsSoundEnabled then
311         begin
312         Mix_QuerySpec(@frequency, @format, @channels);
313         AddFileLog('sound: frequency = ' + IntToStr(frequency) + ', format = ' + IntToStr(format) + ', channels = ' + IntToStr(channels));
314         if format <> $8010 then
315             begin
316             // TODO: support any audio format
317             AddFileLog('Error: Unexpected audio format ' + IntToStr(format));
318             exit;
319             end;
320 
321 {$IOCHECKS OFF}
322         // create sound file
323         filename:= shortstring(UserPathPrefix) + '/VideoTemp/' + shortstring(RecPrefix) + '.sw';
324         Assign(audioFile, filename);
325         Rewrite(audioFile, 1);
326         if IOResult <> 0 then
327             begin
328             AddFileLog('Error: Could not write to ' + filename);
329             exit;
330             end;
331         end;
332 
333     // create file with camera positions
334     filename:= shortstring(UserPathPrefix) + '/VideoTemp/' + shortstring(RecPrefix) + '.txtout';
335     Assign(cameraFile, filename);
336     Rewrite(cameraFile, SizeOf(TFrame));
337     if IOResult <> 0 then
338         begin
339         AddFileLog('Error: Could not write to ' + filename);
340         exit;
341         end;
342 
343     if cIsSoundEnabled then
344         begin
345         // save audio parameters in sound file
346         BlockWrite(audioFile, frequency, 4, result);
347         BlockWrite(audioFile, channels, 4, result);
348 {$IOCHECKS ON}
349 
350         // register callback for actual audio recording
351         Mix_SetPostMix(@RecordPostMix, nil);
352         end;
353 
354     startTime:= SDL_GetTicks();
355     flagPrerecording:= true;
356 end;
357 
358 procedure StopPreRecording;
359 begin
360     AddFileLog('StopPreRecording');
361     flagPrerecording:= false;
362 
363     if cIsSoundEnabled then
364         begin
365         // call SDL_LockAudio because RecordPostMix may be executing right now
366         SDL_LockAudio();
367         Close(audioFile);
368         end;
369     Close(cameraFile);
370     if cIsSoundEnabled then
371         begin
372         Mix_SetPostMix(nil, nil);
373         SDL_UnlockAudio();
374         end;
375 
376     if not thumbnailSaved then
377         SaveThumbnail();
378 end;
379 
380 procedure SaveCameraPosition;
381 var frame: TFrame;
382     result: LongInt;
383 begin
384     result:= 0;
385     if (not thumbnailSaved) and (ScreenFade = sfNone) then
386         SaveThumbnail();
387 
388     frame.realTicks:= SDL_GetTicks() - startTime;
389     frame.gameTicks:= GameTicks;
390     frame.CamX:= WorldDx;
391     frame.CamY:= WorldDy - cScreenHeight div 2;
392     frame.zoom:= zoom/cScreenWidth;
393     BlockWrite(cameraFile, frame, 1, result);
394 end;
395 
396 procedure initModule;
397 begin
398     // we need to make sure these variables are initialized before the main loop
399     // or the wrapper will keep the default values of preinit
400     cScreenWidth:= max(cWindowedWidth, 640);
401     cScreenHeight:= max(cWindowedHeight, 480);
402 end;
403 
404 procedure freeModule;
405 begin
406     if flagPrerecording then
407         StopPreRecording();
408 end;
409 
410 end.
411 
412 {$ENDIF} // USE_VIDEO_RECORDING
413