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