1 // records video to uncompressed avi files (will split across multiple files once size exceeds 1Gb)
2 // - people should post process the files because they will get large very rapidly
3
4
5 // Feedback on playing videos:
6 // quicktime - ok
7 // vlc - ok
8 // xine - ok
9 // mplayer - ok
10 // totem - ok
11 // avidemux - ok - 3Apr09-RockKeyman:had to swap UV channels as it showed up blue
12 // kino - ok
13
14 #include "engine.h"
15 #include "SDL_mixer.h"
16
17 VAR(dbgmovie, 0, 0, 1);
18
19 struct aviindexentry
20 {
21 int type, size;
22 long offset;
23 };
24
25 struct aviwriter
26 {
27 stream *f;
28 uchar *yuv;
29 uint videoframes;
30 uint filesequence;
31 const uint videow, videoh, videofps;
32 string filename;
33 uint physvideoframes;
34 uint physsoundbytes;
35
36 int soundfrequency, soundchannels;
37 Uint16 soundformat;
38
39 vector<aviindexentry> index;
40
41 long fileframesoffset, filevideooffset, filesoundoffset;
42
43 enum { MAX_CHUNK_DEPTH = 16 };
44 long chunkoffsets[MAX_CHUNK_DEPTH];
45 int chunkdepth;
46
makeindexaviwriter47 aviindexentry makeindex(int type, int size)
48 {
49 aviindexentry entry;
50 entry.offset = f->tell() - chunkoffsets[chunkdepth]; // as its relative to movi;
51 entry.type = type;
52 entry.size = size;
53 return entry;
54 }
55
filespaceguessaviwriter56 double filespaceguess()
57 {
58 return double(physvideoframes)*double(videow * videoh * 3 / 2) + double(physsoundbytes + index.length()*16 + 500);
59 }
60
startchunkaviwriter61 void startchunk(const char *fcc)
62 {
63 f->write(fcc, 4);
64 const uint size = 0;
65 f->write(&size, 4);
66 chunkoffsets[++chunkdepth] = f->tell();
67 }
68
listchunkaviwriter69 void listchunk(const char *fcc, const char *lfcc)
70 {
71 startchunk(fcc);
72 f->write(lfcc, 4);
73 }
74
endchunkaviwriter75 void endchunk()
76 {
77 assert(chunkdepth >= 0);
78 uint size = f->tell() - chunkoffsets[chunkdepth];
79 f->seek(chunkoffsets[chunkdepth] - 4, SEEK_SET);
80 f->putlil(size);
81 f->seek(0, SEEK_END);
82 if (size & 1) f->putchar(0x00);
83 --chunkdepth;
84 }
85
writechunkaviwriter86 void writechunk(const char *fcc, const void *data, uint len) // simplify startchunk()/endchunk() to avoid f->seek()
87 {
88 f->write(fcc, 4);
89 f->putlil(len);
90 f->write(data, len);
91 if (len & 1) f->putchar(0x00);
92 }
93
closeaviwriter94 void close()
95 {
96 if (!f) return;
97 assert(chunkdepth == 1);
98 endchunk(); // LIST movi
99
100 startchunk("idx1");
101 loopv(index)
102 {
103 aviindexentry &entry = index[i];
104 // printf("%3d %s %08x\n", i, (entry.type==1)?"s":"v", entry.offset);
105 f->write(entry.type?"01wb":"00dc", 4); // chunkid
106 f->putlil<uint>(0x10); // flags - KEYFRAME
107 f->putlil<uint>(entry.offset); // offset (relative to movi)
108 f->putlil<uint>(entry.size); // size
109 }
110 endchunk();
111
112 endchunk(); // RIFF AVI
113
114 uint soundframes = 0;
115 uint videoframes = 0;
116 long lastoffset = 0;
117 loopv(index)
118 {
119 aviindexentry &entry = index[i];
120 if (entry.type) soundframes++;
121 else if (entry.offset != lastoffset)
122 {
123 lastoffset = entry.offset;
124 videoframes++;
125 }
126 }
127 if (dbgmovie) conoutf("fileframes: sound=%d, video=%d+%d(dups)\n", soundframes, videoframes, index.length()-(soundframes+videoframes));
128
129 f->seek(fileframesoffset, SEEK_SET);
130 f->putlil(index.length()-soundframes); // videoframes including duplicates
131 f->seek(filevideooffset, SEEK_SET);
132 f->putlil(videoframes);
133 if (soundframes > 0)
134 {
135 f->seek(filesoundoffset, SEEK_SET);
136 f->putlil(soundframes);
137 }
138 f->seek(0, SEEK_END);
139
140 DELETEP(f);
141 }
142
aviwriteraviwriter143 aviwriter(const char *name, uint w, uint h, uint fps, bool sound) : f(NULL), yuv(NULL), videoframes(0), filesequence(0), videow(w&~1), videoh(h&~1), videofps(fps), physvideoframes(0), physsoundbytes(0), soundfrequency(0),soundchannels(0),soundformat(0)
144 {
145 copystring(filename, name);
146 path(filename);
147 if (!strrchr(filename, '.')) concatstring(filename, ".avi");
148
149 extern bool nosound; // sound.cpp
150 if (sound && !nosound)
151 {
152 Mix_QuerySpec(&soundfrequency, &soundformat, &soundchannels);
153 const char *desc;
154 switch (soundformat)
155 {
156 case AUDIO_U8:
157 desc = "u8";
158 break;
159 case AUDIO_S8:
160 desc = "s8";
161 break;
162 case AUDIO_U16LSB:
163 desc = "u16l";
164 break;
165 case AUDIO_U16MSB:
166 desc = "u16b";
167 break;
168 case AUDIO_S16LSB:
169 desc = "s16l";
170 break;
171 case AUDIO_S16MSB:
172 desc = "s16b";
173 break;
174 default:
175 desc = "unkn";
176 }
177 if (dbgmovie) conoutf("soundspec: %dhz %s x %d", soundfrequency, desc, soundchannels);
178 }
179 }
180
~aviwriteraviwriter181 ~aviwriter()
182 {
183 close();
184 if (yuv) delete [] yuv;
185 }
186
openaviwriter187 bool open()
188 {
189 close();
190 string seqfilename;
191 if (filesequence == 0) copystring(seqfilename, filename);
192 else
193 {
194 if (filesequence >= 999) return false;
195 char *ext = strrchr(filename, '.');
196 if (filesequence == 1)
197 {
198 string oldfilename;
199 copystring(oldfilename, findfile(filename, "wb"));
200 *ext = '\0';
201 conoutf("movie now recording to multiple: %s_XXX.%s files", filename, ext+1);
202 formatstring(seqfilename)("%s_%03d.%s", filename, 0, ext+1);
203 rename(oldfilename, findfile(seqfilename, "wb"));
204 }
205 *ext = '\0';
206 formatstring(seqfilename)("%s_%03d.%s", filename, filesequence, ext+1);
207 *ext = '.';
208 }
209 filesequence++;
210 f = openfile(seqfilename, "wb");
211 if (!f) return false;
212
213 index.setsize(0);
214 chunkdepth = -1;
215
216 listchunk("RIFF", "AVI ");
217
218 listchunk("LIST", "hdrl");
219
220 startchunk("avih");
221 f->putlil<uint>(1000000 / videofps); // microsecsperframe
222 f->putlil<uint>(0); // maxbytespersec
223 f->putlil<uint>(0); // reserved
224 f->putlil<uint>(0x10 | 0x20); // flags - hasindex|mustuseindex
225 fileframesoffset = f->tell();
226 f->putlil<uint>(0); // totalvideoframes
227 f->putlil<uint>(0); // initialframes
228 f->putlil<uint>(soundfrequency > 0 ? 2 : 1); // streams
229 f->putlil<uint>(0); // buffersize
230 f->putlil<uint>(videow); // video width
231 f->putlil<uint>(videoh); // video height
232 loopi(4) f->putlil<uint>(0); // reserved
233 endchunk(); // avih
234
235 listchunk("LIST", "strl");
236
237 startchunk("strh");
238 f->write("vids", 4); // fcctype
239 f->write("I420", 4); // fcchandler
240 f->putlil<uint>(0); // flags
241 f->putlil<uint>(0); // priority
242 f->putlil<uint>(0); // initialframes
243 f->putlil<uint>(1); // scale
244 f->putlil<uint>(videofps); // rate
245 f->putlil<uint>(0); // start
246 filevideooffset = f->tell();
247 f->putlil<uint>(0); // length
248 f->putlil<uint>(videow*videoh*3/2); // suggested buffersize
249 f->putlil<uint>(0); // quality
250 f->putlil<uint>(0); // samplesize
251 f->putlil<ushort>(0); // frame left
252 f->putlil<ushort>(0); // frame top
253 f->putlil<ushort>(videow); // frame right
254 f->putlil<ushort>(videoh); // frame bottom
255 endchunk(); // strh
256
257 startchunk("strf");
258 f->putlil<uint>(40); //headersize
259 f->putlil<uint>(videow); // width
260 f->putlil<uint>(videoh); // height
261 f->putlil<ushort>(3); // planes
262 f->putlil<ushort>(12); // bitcount
263 f->write("I420", 4); // compression
264 f->putlil<uint>(videow*videoh*3/2); // imagesize
265 f->putlil<uint>(0); // xres
266 f->putlil<uint>(0); // yres;
267 f->putlil<uint>(0); // colorsused
268 f->putlil<uint>(0); // colorsrequired
269 endchunk(); // strf
270
271 endchunk(); // LIST strl
272
273 if (soundfrequency > 0)
274 {
275 const int bps = (soundformat==AUDIO_U8 || soundformat == AUDIO_S8) ? 1 : 2;
276
277 listchunk("LIST", "strl");
278
279 startchunk("strh");
280 f->write("auds", 4); // fcctype
281 f->putlil<uint>(1); // fcchandler - normally 4cc, but audio is a special case
282 f->putlil<uint>(0); // flags
283 f->putlil<uint>(0); // priority
284 f->putlil<uint>(0); // initialframes
285 f->putlil<uint>(1); // scale
286 f->putlil<uint>(soundfrequency); // rate
287 f->putlil<uint>(0); // start
288 filesoundoffset = f->tell();
289 f->putlil<uint>(0); // length
290 f->putlil<uint>(soundfrequency*bps*soundchannels/2); // suggested buffer size (this is a half second)
291 f->putlil<uint>(0); // quality
292 f->putlil<uint>(bps*soundchannels); // samplesize
293 f->putlil<ushort>(0); // frame left
294 f->putlil<ushort>(0); // frame top
295 f->putlil<ushort>(0); // frame right
296 f->putlil<ushort>(0); // frame bottom
297 endchunk(); // strh
298
299 startchunk("strf");
300 f->putlil<ushort>(1); // format (uncompressed PCM)
301 f->putlil<ushort>(soundchannels); // channels
302 f->putlil<uint>(soundfrequency); // sampleframes per second
303 f->putlil<uint>(soundfrequency*bps*soundchannels); // average bytes per second
304 f->putlil<ushort>(bps*soundchannels); // block align <-- guess
305 f->putlil<ushort>(bps*8); // bits per sample
306 f->putlil<ushort>(0); // size
307 endchunk(); //strf
308
309 endchunk(); // LIST strl
310 }
311
312 listchunk("LIST", "INFO");
313
314 const char *software = "Blood Frontier";
315 writechunk("ISFT", software, strlen(software)+1);
316
317 endchunk(); // LIST INFO
318
319 endchunk(); // LIST hdrl
320
321 listchunk("LIST", "movi");
322
323 return true;
324 }
325
boxsampleaviwriter326 static inline void boxsample(const uchar *src, const uint stride,
327 const uint area, const uint w, uint h,
328 const uint xlow, const uint xhigh, const uint ylow, const uint yhigh,
329 uint &bdst, uint &gdst, uint &rdst)
330 {
331 const uchar *end = &src[w<<2];
332 uint bt = 0, gt = 0, rt = 0;
333 for (const uchar *cur = &src[4]; cur < end; cur += 4)
334 {
335 bt += cur[0];
336 gt += cur[1];
337 rt += cur[2];
338 }
339 bt = ylow*(bt + ((src[0]*xlow + end[0]*xhigh)>>12));
340 gt = ylow*(gt + ((src[1]*xlow + end[1]*xhigh)>>12));
341 rt = ylow*(rt + ((src[2]*xlow + end[2]*xhigh)>>12));
342 if (h)
343 {
344 for (src += stride, end += stride; --h; src += stride, end += stride)
345 {
346 uint b = 0, g = 0, r = 0;
347 for (const uchar *cur = &src[4]; cur < end; cur += 4)
348 {
349 b += cur[0];
350 g += cur[1];
351 r += cur[2];
352 }
353 bt += (b<<12) + src[0]*xlow + end[0]*xhigh;
354 gt += (g<<12) + src[1]*xlow + end[1]*xhigh;
355 rt += (r<<12) + src[2]*xlow + end[2]*xhigh;
356 }
357 uint b = 0, g = 0, r = 0;
358 for (const uchar *cur = &src[4]; cur < end; cur += 4)
359 {
360 b += cur[0];
361 g += cur[1];
362 r += cur[2];
363 }
364 bt += yhigh*(b + ((src[0]*xlow + end[0]*xhigh)>>12));
365 gt += yhigh*(g + ((src[1]*xlow + end[1]*xhigh)>>12));
366 rt += yhigh*(r + ((src[2]*xlow + end[2]*xhigh)>>12));
367 }
368 bdst = (bt*area)>>24;
369 gdst = (gt*area)>>24;
370 rdst = (rt*area)>>24;
371 }
372
scaleyuvaviwriter373 void scaleyuv(const uchar *pixels, uint srcw, uint srch)
374 {
375 const int flip = -1;
376 const uint planesize = videow * videoh;
377 if (!yuv) yuv = new uchar[(planesize*3)/2];
378 uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4;
379 const int ystride = flip*int(videow), uvstride = flip*int(videow)/2;
380 if (flip < 0)
381 {
382 yplane -= int(videoh-1)*ystride;
383 uplane -= int(videoh/2-1)*uvstride;
384 vplane -= int(videoh/2-1)*uvstride;
385 }
386
387 const uint stride = srcw<<2;
388 srcw &= ~1;
389 srch &= ~1;
390 const uint wfrac = (srcw<<12)/videow, hfrac = (srch<<12)/videoh,
391 area = ((unsigned long long int)planesize<<12)/(srcw*srch + 1),
392 dw = videow*wfrac, dh = videoh*hfrac;
393
394 for (uint y = 0; y < dh;)
395 {
396 uint yn = y + hfrac - 1, yi = y>>12, h = (yn>>12) - yi, ylow = ((yn|(-int(h)>>24))&0xFFFU) + 1 - (y&0xFFFU), yhigh = (yn&0xFFFU) + 1;
397 y += hfrac;
398 uint y2n = y + hfrac - 1, y2i = y>>12, h2 = (y2n>>12) - y2i, y2low = ((y2n|(-int(h2)>>24))&0xFFFU) + 1 - (y&0xFFFU), y2high = (y2n&0xFFFU) + 1;
399 y += hfrac;
400
401 const uchar *src = &pixels[yi*stride], *src2 = &pixels[y2i*stride];
402 uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane;
403 for (uint x = 0; x < dw;)
404 {
405 uint xn = x + wfrac - 1, xi = x>>12, w = (xn>>12) - xi, xlow = ((w+0xFFFU)&0x1000U) - (x&0xFFFU), xhigh = (xn&0xFFFU) + 1;
406 x += wfrac;
407 uint x2n = x + wfrac - 1, x2i = x>>12, w2 = (x2n>>12) - x2i, x2low = ((w2+0xFFFU)&0x1000U) - (x&0xFFFU), x2high = (x2n&0xFFFU) + 1;
408 x += wfrac;
409
410 uint b1, g1, r1, b2, g2, r2, b3, g3, r3, b4, g4, r4;
411 boxsample(&src[xi<<2], stride, area, w, h, xlow, xhigh, ylow, yhigh, b1, g1, r1);
412 boxsample(&src[x2i<<2], stride, area, w2, h, x2low, x2high, ylow, yhigh, b2, g2, r2);
413 boxsample(&src2[xi<<2], stride, area, w, h2, xlow, xhigh, y2low, y2high, b3, g3, r3);
414 boxsample(&src2[x2i<<2], stride, area, w2, h2, x2low, x2high, y2low, y2high, b4, g4, r4);
415
416 // 0.299*R + 0.587*G + 0.114*B
417 *ydst++ = (1225*r1 + 2404*g1 + 467*b1)>>12;
418 *ydst++ = (1225*r2 + 2404*g2 + 467*b2)>>12;
419 *ydst2++ = (1225*r3 + 2404*g3 + 467*b3)>>12;
420 *ydst2++ = (1225*r4 + 2404*g4 + 467*b4)>>12;
421
422 const uint b = b1 + b2 + b3 + b4,
423 g = g1 + g2 + g3 + g4,
424 r = r1 + r2 + r3 + r4;
425 // U = 0.500 + 0.500*B - 0.169*R - 0.331*G
426 // V = 0.500 + 0.500*R - 0.419*G - 0.081*B
427 // note: weights here are scaled by 1<<10, as opposed to 1<<12, since r/g/b are already *4
428 *udst++ = ((128<<12) + 512*b - 173*r - 339*g)>>12;
429 *vdst++ = ((128<<12) + 512*r - 429*g - 83*b)>>12;
430 }
431
432 yplane += 2*ystride;
433 uplane += uvstride;
434 vplane += uvstride;
435 }
436 }
437
encodeyuvaviwriter438 void encodeyuv(const uchar *pixels)
439 {
440 const int flip = -1;
441 const uint planesize = videow * videoh;
442 if (!yuv) yuv = new uchar[(planesize*3)/2];
443 uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4;
444 const int ystride = flip*int(videow), uvstride = flip*int(videow)/2;
445 if (flip < 0)
446 {
447 yplane -= int(videoh-1)*ystride;
448 uplane -= int(videoh/2-1)*uvstride;
449 vplane -= int(videoh/2-1)*uvstride;
450 }
451
452 const uint stride = videow<<2;
453 const uchar *src = pixels, *yend = src + videoh*stride;
454 while (src < yend)
455 {
456 const uchar *src2 = src + stride, *xend = src2;
457 uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane;
458 while (src < xend)
459 {
460 const uint b1 = src[0], g1 = src[1], r1 = src[2],
461 b2 = src[4], g2 = src[5], r2 = src[6],
462 b3 = src2[0], g3 = src2[1], r3 = src2[2],
463 b4 = src2[4], g4 = src2[5], r4 = src2[6];
464
465 // 0.299*R + 0.587*G + 0.114*B
466 *ydst++ = (1225*r1 + 2404*g1 + 467*b1)>>12;
467 *ydst++ = (1225*r2 + 2404*g2 + 467*b2)>>12;
468 *ydst2++ = (1225*r3 + 2404*g3 + 467*b3)>>12;
469 *ydst2++ = (1225*r4 + 2404*g4 + 467*b4)>>12;
470
471 const uint b = b1 + b2 + b3 + b4,
472 g = g1 + g2 + g3 + g4,
473 r = r1 + r2 + r3 + r4;
474 // U = 0.500 + 0.500*B - 0.169*R - 0.331*G
475 // V = 0.500 + 0.500*R - 0.419*G - 0.081*B
476 // note: weights here are scaled by 1<<10, as opposed to 1<<12, since r/g/b are already *4
477 *udst++ = ((128<<12) + 512*b - 173*r - 339*g)>>12;
478 *vdst++ = ((128<<12) + 512*r - 429*g - 83*b)>>12;
479
480 src += 8;
481 src2 += 8;
482 }
483 src = src2;
484 yplane += 2*ystride;
485 uplane += uvstride;
486 vplane += uvstride;
487 }
488 }
489
compressyuvaviwriter490 void compressyuv(const uchar *pixels)
491 {
492 const int flip = -1;
493 const uint planesize = videow * videoh;
494 if (!yuv) yuv = new uchar[(planesize*3)/2];
495 uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4;
496 const int ystride = flip*int(videow), uvstride = flip*int(videow)/2;
497 if (flip < 0)
498 {
499 yplane -= int(videoh-1)*ystride;
500 uplane -= int(videoh/2-1)*uvstride;
501 vplane -= int(videoh/2-1)*uvstride;
502 }
503
504 const uint stride = videow<<2;
505 const uchar *src = pixels, *yend = src + videoh*stride;
506 while (src < yend)
507 {
508 const uchar *src2 = src + stride, *xend = src2;
509 uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane;
510 while (src < xend)
511 {
512 *ydst++ = src[0];
513 *ydst++ = src[4];
514 *ydst2++ = src2[0];
515 *ydst2++ = src2[4];
516
517 *udst++ = (uint(src[1]) + uint(src[5]) + uint(src2[1]) + uint(src2[5])) >> 2;
518 *vdst++ = (uint(src[2]) + uint(src[6]) + uint(src2[2]) + uint(src2[6])) >> 2;
519
520 src += 8;
521 src2 += 8;
522 }
523 src = src2;
524 yplane += 2*ystride;
525 uplane += uvstride;
526 vplane += uvstride;
527 }
528 }
529
writesoundaviwriter530 void writesound(const void *data, uint framesize)
531 {
532 switch (soundformat) // do conversion inplace to little endian format
533 {
534 case AUDIO_U8:
535 loopi(framesize) ((Sint8*)data)[i] = ((Uint8*)data)[i] - 0x80;
536 break;
537 case AUDIO_S8:
538 break;
539 case AUDIO_U16LSB:
540 loopi(framesize/2) ((Sint16*)data)[i] = ((Uint16*)data)[i] - 0x8000;
541 break;
542 case AUDIO_U16MSB:
543 loopi(framesize/2) ((Sint16*)data)[i] = ((Uint16*)data)[i] - 0x8000;
544 lilswap((Sint16*)data, framesize/2);
545 break;
546 case AUDIO_S16LSB:
547 break;
548 case AUDIO_S16MSB:
549 lilswap((Sint16*)data, framesize/2);
550 break;
551 }
552
553 index.add(makeindex(1, framesize));
554
555 writechunk("01wb", data, framesize);
556 physsoundbytes += framesize;
557 }
558
559
560 enum
561 {
562 VID_RGB = 0,
563 VID_YUV,
564 VID_YUV420
565 };
566
writevideoframeaviwriter567 bool writevideoframe(const uchar *pixels, uint srcw, uint srch, int format, uint frame)
568 {
569 if (frame < videoframes) return true;
570
571 switch (format)
572 {
573 case VID_RGB:
574 if (srcw != videow || srch != videoh) scaleyuv(pixels, srcw, srch);
575 else encodeyuv(pixels);
576 break;
577 case VID_YUV:
578 compressyuv(pixels);
579 break;
580 }
581
582 const uint framesize = (videow * videoh * 3) / 2;
583 if (f->tell() + framesize > 1000*1000*1000 && !open()) return false; // check for overflow of 1Gb limit
584
585 aviindexentry entry = makeindex(0, framesize);
586 int vpos = index.length(), vnum = frame + 1 - videoframes;
587 loopi(vnum) index.add(entry);
588
589 if (vnum > 1) // experimental - detect sequence of sound frames that precede this sequence of video - interleave the sound
590 {
591 int snum = 0;
592 while (vpos > snum && index[vpos-snum-1].type == 1) snum++;
593 if (snum > 1)
594 {
595 if (dbgmovie) conoutf("movie: interleaving sound=%d x%d video=%d x%d\n", vpos-snum, snum, vpos, vnum);
596 int frac = 0, pos = index.length();
597 loopi(snum)
598 {
599 for (frac += vnum; frac >= snum; frac -= snum) index[--pos] = entry;
600 index[--pos] = index[vpos-1-i];
601 }
602 }
603 }
604
605 videoframes = frame + 1;
606
607 writechunk("00dc", format == VID_YUV420 ? pixels : yuv, framesize);
608 physvideoframes++;
609
610 return true;
611 }
612
613 };
614
615 VAR(movieaccelblit, 0, 0, 1);
616 VAR(movieaccelyuv, 0, 1, 1);
617 VARP(movieaccel, 0, 1, 1);
618 VARP(moviesync, 0, 0, 1);
619
620 namespace recorder
621 {
622 static enum { REC_OK = 0, REC_USERHALT, REC_TOOSLOW, REC_FILERROR } state = REC_OK;
623
624 static aviwriter *file = NULL;
625 static int starttime = 0;
626
627 static int stats[1000];
628 static int statsindex = 0;
629 static uint dps = 0; // dropped frames per sample
630
631 enum { MAXSOUNDBUFFERS = 32 }; // sounds queue up until there is a video frame, so at low fps you'll need a bigger queue
632 struct soundbuffer
633 {
634 Uint8 *sound;
635 uint size;
636 uint frame;
637
soundbufferrecorder::soundbuffer638 soundbuffer() : sound(NULL) {}
~soundbufferrecorder::soundbuffer639 ~soundbuffer()
640 {
641 cleanup();
642 }
643
loadrecorder::soundbuffer644 void load(Uint8 *stream, uint len, uint fnum)
645 {
646 DELETEA(sound);
647 size = len;
648 sound = new Uint8[len];
649 frame = fnum;
650 memcpy(sound, stream, len);
651 }
652
cleanuprecorder::soundbuffer653 void cleanup()
654 {
655 DELETEA(sound);
656 }
657 };
658 static queue<soundbuffer, MAXSOUNDBUFFERS> soundbuffers;
659 static SDL_mutex *soundlock = NULL;
660
661 enum { MAXVIDEOBUFFERS = 2 }; // double buffer
662 struct videobuffer
663 {
664 uchar *video;
665 uint w, h, bpp, frame;
666 int format;
667
videobufferrecorder::videobuffer668 videobuffer() : video(NULL){}
~videobufferrecorder::videobuffer669 ~videobuffer()
670 {
671 cleanup();
672 }
673
initrecorder::videobuffer674 void init(int nw, int nh, int nbpp)
675 {
676 DELETEA(video);
677 w = nw;
678 h = nh;
679 bpp = nbpp;
680 video = new uchar[w*h*bpp];
681 format = -1;
682 }
683
cleanuprecorder::videobuffer684 void cleanup()
685 {
686 DELETEA(video);
687 }
688 };
689 static queue<videobuffer, MAXVIDEOBUFFERS> videobuffers;
690 static uint lastframe = ~0U;
691
692 static GLuint scalefb = 0, scaletex[2] = { 0, 0 };
693 static uint scalew = 0, scaleh = 0;
694 static GLuint encodefb = 0, encoderb = 0;
695
696 static SDL_Thread *thread = NULL;
697 static SDL_mutex *videolock = NULL;
698 static SDL_cond *shouldencode = NULL, *shouldread = NULL;
699
isrecording()700 bool isrecording()
701 {
702 return file != NULL;
703 }
704
videoencoder(void * data)705 int videoencoder(void *data) // runs on a separate thread
706 {
707 for (int numvid = 0, numsound = 0;;)
708 {
709 SDL_LockMutex(videolock);
710 for (; numvid > 0; numvid--) videobuffers.remove();
711 SDL_CondSignal(shouldread);
712 while (videobuffers.empty() && state == REC_OK) SDL_CondWait(shouldencode, videolock);
713 if (state != REC_OK)
714 {
715 SDL_UnlockMutex(videolock);
716 break;
717 }
718 videobuffer &m = videobuffers.removing();
719 numvid++;
720 SDL_UnlockMutex(videolock);
721
722 if (file->soundfrequency > 0)
723 {
724 // chug data from lock protected buffer to avoid holding lock while writing to file
725 SDL_LockMutex(soundlock);
726 for (; numsound > 0; numsound--) soundbuffers.remove();
727 for (; numsound < soundbuffers.length(); numsound++)
728 {
729 soundbuffer &s = soundbuffers.removing(numsound);
730 if (s.frame > m.frame) break; // sync with video
731 }
732 SDL_UnlockMutex(soundlock);
733 loopi(numsound)
734 {
735 soundbuffer &s = soundbuffers.removing(i);
736 file->writesound(s.sound, s.size);
737 }
738 }
739
740 int duplicates = m.frame - (int)file->videoframes + 1;
741 if (duplicates > 0) // determine how many frames have been dropped over the sample window
742 {
743 dps -= stats[statsindex];
744 stats[statsindex] = duplicates-1;
745 dps += stats[statsindex];
746 statsindex = (statsindex+1)%file->videofps;
747 }
748 //printf("frame %d->%d (%d dps): sound = %d bytes\n", file->videoframes, nextframenum, dps, m.soundlength);
749 if (dps > file->videofps) state = REC_TOOSLOW;
750 else if (!file->writevideoframe(m.video, m.w, m.h, m.format, m.frame)) state = REC_FILERROR;
751
752 m.frame = ~0U;
753 }
754
755 return 0;
756 }
757
soundencoder(void * udata,Uint8 * stream,int len)758 void soundencoder(void *udata, Uint8 *stream, int len) // callback occurs on a separate thread
759 {
760 SDL_LockMutex(soundlock);
761 if (soundbuffers.full()) state = REC_TOOSLOW;
762 else if (state == REC_OK)
763 {
764 uint nextframe = ((totalmillis - starttime)*file->videofps)/1000;
765 soundbuffer &s = soundbuffers.add();
766 s.load(stream, len, nextframe);
767 }
768 SDL_UnlockMutex(soundlock);
769 }
770
start(const char * filename,int videofps,int videow,int videoh,bool sound)771 void start(const char *filename, int videofps, int videow, int videoh, bool sound)
772 {
773 if (file) return;
774
775 int fps, bestdiff, worstdiff;
776 getfps(fps, bestdiff, worstdiff);
777 if (videofps > fps) conoutf("frame rate may be too low to capture at %d fps", videofps);
778
779 if (videow%2) videow += 1;
780 if (videoh%2) videoh += 1;
781
782 file = new aviwriter(filename, videow, videoh, videofps, sound);
783 if (!file->open())
784 {
785 conoutf("unable to create file %s", filename);
786 DELETEP(file);
787 return;
788 }
789 conoutf("movie recording to: %s %dx%d @ %dfps%s", file->filename, file->videow, file->videoh, file->videofps, (file->soundfrequency>0)?" + sound":"");
790
791 useshaderbyname("moviergb");
792 useshaderbyname("movieyuv");
793 useshaderbyname("moviey");
794 useshaderbyname("movieu");
795 useshaderbyname("moviev");
796
797 starttime = totalmillis;
798 loopi(file->videofps) stats[i] = 0;
799 statsindex = 0;
800 dps = 0;
801
802 lastframe = ~0U;
803 videobuffers.clear();
804 loopi(MAXVIDEOBUFFERS)
805 {
806 uint w = screen->w, h = screen->w;
807 videobuffers.data[i].init(w, h, 4);
808 videobuffers.data[i].frame = ~0U;
809 }
810
811 soundbuffers.clear();
812
813 soundlock = SDL_CreateMutex();
814 videolock = SDL_CreateMutex();
815 shouldencode = SDL_CreateCond();
816 shouldread = SDL_CreateCond();
817 thread = SDL_CreateThread(videoencoder, NULL);
818 if (file->soundfrequency > 0) Mix_SetPostMix(soundencoder, NULL);
819 }
820
cleanup()821 void cleanup()
822 {
823 if(scalefb) { glDeleteFramebuffers_(1, &scalefb); scalefb = 0; }
824 if(scaletex[0] || scaletex[1]) { glDeleteTextures(2, scaletex); memset(scaletex, 0, sizeof(scaletex)); }
825 scalew = scaleh = 0;
826 if(encodefb) { glDeleteFramebuffers_(1, &encodefb); encodefb = 0; }
827 if(encoderb) { glDeleteRenderbuffers_(1, &encoderb); encoderb = 0; }
828 }
829
stop()830 void stop()
831 {
832 if(!file) return;
833 if(state == REC_OK) state = REC_USERHALT;
834 if(file->soundfrequency > 0) Mix_SetPostMix(NULL, NULL);
835
836 SDL_LockMutex(videolock); // wakeup thread enough to kill it
837 SDL_CondSignal(shouldencode);
838 SDL_UnlockMutex(videolock);
839
840 SDL_WaitThread(thread, NULL); // block until thread is finished
841
842 cleanup();
843
844 loopi(MAXVIDEOBUFFERS) videobuffers.data[i].cleanup();
845 loopi(MAXSOUNDBUFFERS) soundbuffers.data[i].cleanup();
846
847 SDL_DestroyMutex(soundlock);
848 SDL_DestroyMutex(videolock);
849 SDL_DestroyCond(shouldencode);
850 SDL_DestroyCond(shouldread);
851
852 soundlock = videolock = NULL;
853 shouldencode = shouldread = NULL;
854 thread = NULL;
855
856 static const char *mesgs[] = { "ok", "stopped", "computer too slow", "file error"};
857 conoutf("movie recording halted: %s, %d frames", mesgs[state], file->videoframes);
858
859 DELETEP(file);
860 state = REC_OK;
861 }
862
drawquad(float tw,float th,float x,float y,float w,float h)863 void drawquad(float tw, float th, float x, float y, float w, float h)
864 {
865 glBegin(GL_QUADS);
866 glTexCoord2f(0, 0);
867 glVertex2f(x, y);
868 glTexCoord2f(tw, 0);
869 glVertex2f(x+w, y);
870 glTexCoord2f(tw, th);
871 glVertex2f(x+w, y+h);
872 glTexCoord2f(0, th);
873 glVertex2f(x, y+h);
874 glEnd();
875 }
876
readbuffer(videobuffer & m,uint nextframe)877 void readbuffer(videobuffer &m, uint nextframe)
878 {
879 bool accelyuv = movieaccelyuv && renderpath!=R_FIXEDFUNCTION && !(m.w%8),
880 usefbo = movieaccel && hasFBO && hasTR && file->videow <= (uint)screen->w && file->videoh <= (uint)screen->h && (accelyuv || file->videow < (uint)screen->w || file->videoh < (uint)screen->h);
881 uint w = screen->w, h = screen->h;
882 if (usefbo)
883 {
884 w = file->videow;
885 h = file->videoh;
886 }
887 if (w != m.w || h != m.h) m.init(w, h, 4);
888 m.format = aviwriter::VID_RGB;
889 m.frame = nextframe;
890
891 glPixelStorei(GL_PACK_ALIGNMENT, texalign(m.video, m.w, 4));
892 if (usefbo)
893 {
894 uint tw = screen->w, th = screen->h;
895 if (hasFBB && movieaccelblit)
896 {
897 tw = max(tw/2, m.w);
898 th = max(th/2, m.h);
899 }
900 if (tw != scalew || th != scaleh)
901 {
902 if (!scalefb) glGenFramebuffers_(1, &scalefb);
903 loopi(2)
904 {
905 if (!scaletex[i]) glGenTextures(1, &scaletex[i]);
906 createtexture(scaletex[i], tw, th, NULL, 3, 1, GL_RGB, GL_TEXTURE_RECTANGLE_ARB);
907 }
908 scalew = tw;
909 scaleh = th;
910 }
911 if (accelyuv && (!encodefb || !encoderb))
912 {
913 if (!encodefb) glGenFramebuffers_(1, &encodefb);
914 glBindFramebuffer_(GL_FRAMEBUFFER_EXT, encodefb);
915 if (!encoderb) glGenRenderbuffers_(1, &encoderb);
916 glBindRenderbuffer_(GL_RENDERBUFFER_EXT, encoderb);
917 glRenderbufferStorage_(GL_RENDERBUFFER_EXT, GL_RGBA, (m.w*3)/8, m.h);
918 glFramebufferRenderbuffer_(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_RENDERBUFFER_EXT, encoderb);
919 glBindRenderbuffer_(GL_RENDERBUFFER_EXT, 0);
920 glBindFramebuffer_(GL_FRAMEBUFFER_EXT, 0);
921 }
922
923 if (tw < (uint)screen->w || th < (uint)screen->h)
924 {
925 glBindFramebuffer_(GL_READ_FRAMEBUFFER_EXT, 0);
926 glBindFramebuffer_(GL_DRAW_FRAMEBUFFER_EXT, scalefb);
927 glFramebufferTexture2D_(GL_DRAW_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_RECTANGLE_ARB, scaletex[0], 0);
928 glBlitFramebuffer_(0, 0, screen->w, screen->h, 0, 0, tw, th, GL_COLOR_BUFFER_BIT, GL_LINEAR);
929 glBindFramebuffer_(GL_DRAW_FRAMEBUFFER_EXT, 0);
930 }
931 else
932 {
933 glBindTexture(GL_TEXTURE_RECTANGLE_ARB, scaletex[0]);
934 glCopyTexSubImage2D(GL_TEXTURE_RECTANGLE_ARB, 0, 0, 0, 0, 0, screen->w, screen->h);
935 }
936
937 if (tw > m.w || th > m.h || (!accelyuv && renderpath != R_FIXEDFUNCTION && tw >= m.w && th >= m.h))
938 {
939 glBindFramebuffer_(GL_FRAMEBUFFER_EXT, scalefb);
940 glViewport(0, 0, tw, th);
941 glColor3f(1, 1, 1);
942 glMatrixMode(GL_PROJECTION);
943 glLoadIdentity();
944 glOrtho(0, tw, 0, th, -1, 1);
945 glMatrixMode(GL_MODELVIEW);
946 glLoadIdentity();
947 glEnable(GL_TEXTURE_RECTANGLE_ARB);
948 do
949 {
950 glFramebufferTexture2D_(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_RECTANGLE_ARB, scaletex[1], 0);
951 glBindTexture(GL_TEXTURE_RECTANGLE_ARB, scaletex[0]);
952 uint dw = max(tw/2, m.w), dh = max(th/2, m.h);
953 if (dw == m.w && dh == m.h && !accelyuv && renderpath != R_FIXEDFUNCTION)
954 {
955 SETSHADER(movieyuv);
956 m.format = aviwriter::VID_YUV;
957 }
958 else SETSHADER(moviergb);
959 drawquad(tw, th, 0, 0, dw, dh);
960 tw = dw;
961 th = dh;
962 swap(scaletex[0], scaletex[1]);
963 }
964 while (tw > m.w || th > m.h);
965 glDisable(GL_TEXTURE_RECTANGLE_ARB);
966 }
967 if (accelyuv)
968 {
969 glBindFramebuffer_(GL_FRAMEBUFFER_EXT, encodefb);
970 glViewport(0, 0, (m.w*3)/8, m.h);
971 glColor3f(1, 1, 1);
972 glMatrixMode(GL_PROJECTION);
973 glLoadIdentity();
974 glOrtho(0, (m.w*3)/8, m.h, 0, -1, 1);
975 glMatrixMode(GL_MODELVIEW);
976 glLoadIdentity();
977 glEnable(GL_TEXTURE_RECTANGLE_ARB);
978 glBindTexture(GL_TEXTURE_RECTANGLE_ARB, scaletex[0]);
979 SETSHADER(moviey);
980 drawquad(m.w, m.h, 0, 0, m.w/4, m.h);
981 SETSHADER(moviev);
982 drawquad(m.w, m.h, m.w/4, 0, m.w/8, m.h/2);
983 SETSHADER(movieu);
984 drawquad(m.w, m.h, m.w/4, m.h/2, m.w/8, m.h/2);
985 glDisable(GL_TEXTURE_RECTANGLE_ARB);
986 const uint planesize = m.w * m.h;
987 glPixelStorei(GL_PACK_ALIGNMENT, texalign(m.video, m.w/4, 4));
988 glReadPixels(0, 0, m.w/4, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video);
989 glPixelStorei(GL_PACK_ALIGNMENT, texalign(&m.video[planesize], m.w/8, 4));
990 glReadPixels(m.w/4, 0, m.w/8, m.h/2, GL_BGRA, GL_UNSIGNED_BYTE, &m.video[planesize]);
991 glPixelStorei(GL_PACK_ALIGNMENT, texalign(&m.video[planesize + planesize/4], m.w/8, 4));
992 glReadPixels(m.w/4, m.h/2, m.w/8, m.h/2, GL_BGRA, GL_UNSIGNED_BYTE, &m.video[planesize + planesize/4]);
993 m.format = aviwriter::VID_YUV420;
994 }
995 else
996 {
997 glBindFramebuffer_(GL_FRAMEBUFFER_EXT, scalefb);
998 glReadPixels(0, 0, m.w, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video);
999 }
1000 glBindFramebuffer_(GL_FRAMEBUFFER_EXT, 0);
1001 glViewport(0, 0, screen->w, screen->h);
1002
1003 }
1004 else glReadPixels(0, 0, m.w, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video);
1005 }
1006
readbuffer()1007 bool readbuffer()
1008 {
1009 if (!file) return false;
1010 if (state != REC_OK)
1011 {
1012 stop();
1013 return false;
1014 }
1015 SDL_LockMutex(videolock);
1016 if (moviesync && videobuffers.full()) SDL_CondWait(shouldread, videolock);
1017 uint nextframe = ((totalmillis - starttime)*file->videofps)/1000;
1018 if (!videobuffers.full() && (lastframe == ~0U || nextframe > lastframe))
1019 {
1020 videobuffer &m = videobuffers.adding();
1021 SDL_UnlockMutex(videolock);
1022 readbuffer(m, nextframe);
1023 SDL_LockMutex(videolock);
1024 lastframe = nextframe;
1025 videobuffers.add();
1026 SDL_CondSignal(shouldencode);
1027 }
1028 SDL_UnlockMutex(videolock);
1029 return true;
1030 }
1031
drawhud()1032 void drawhud()
1033 {
1034 int w = screen->w, h = screen->h;
1035
1036 gettextres(w, h);
1037
1038 glMatrixMode(GL_PROJECTION);
1039 glLoadIdentity();
1040 glOrtho(0, w, h, 0, -1, 1);
1041 glMatrixMode(GL_MODELVIEW);
1042 glLoadIdentity();
1043
1044 glEnable(GL_BLEND);
1045 glEnable(GL_TEXTURE_2D);
1046 defaultshader->set();
1047
1048 glPushMatrix();
1049 glScalef(1/3.0f, 1/3.0f, 1);
1050
1051 double totalsize = file->filespaceguess();
1052 const char *unit = "KB";
1053 if (totalsize >= 1e9)
1054 {
1055 totalsize /= 1e9;
1056 unit = "GB";
1057 }
1058 else if (totalsize >= 1e6)
1059 {
1060 totalsize /= 1e6;
1061 unit = "MB";
1062 }
1063 else totalsize /= 1e3;
1064
1065 float quality = 1.0f - float(dps)/float(dps+file->videofps); // strictly speaking should lock to read dps - 1.0=perfect, 0.5=half of frames are beingdropped
1066 draw_textx("recorded %.1f%s %d%%", w*3-FONTH*3/2, FONTH*3/2, 255, 255, 255, 255, TEXT_RIGHT_JUSTIFY, -1, -1, totalsize, unit, int(quality*100));
1067
1068 glPopMatrix();
1069
1070 glDisable(GL_TEXTURE_2D);
1071 glDisable(GL_BLEND);
1072 }
1073
capture()1074 void capture()
1075 {
1076 if (readbuffer()) drawhud();
1077 }
1078 }
1079
1080 VARP(moview, 0, 320, 10000);
1081 VARP(movieh, 0, 240, 10000);
1082 VARP(moviefps, 1, 24, 1000);
1083 VARP(moviesound, 0, 1, 1);
1084
movie(char * name)1085 void movie(char *name)
1086 {
1087 if (name[0] == '\0') recorder::stop();
1088 else if (!recorder::isrecording()) recorder::start(name, moviefps, moview ? moview : screen->w, movieh ? movieh : screen->h, moviesound!=0);
1089 }
1090
1091 COMMAND(movie, "s");
1092
1093