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