1 // Feedback on playing videos:
2 // quicktime - ok
3 // vlc - ok
4 // xine - ok
5 // mplayer - ok
6 // totem - ok
7 // avidemux - ok - 3Apr09-RockKeyman:had to swap UV channels as it showed up blue
8 // kino - ok
9
10 #include "engine.h"
11 #include "SDL_mixer.h"
12
13 VAR(dbgmovie, 0, 0, 1);
14
15 struct aviindexentry
16 {
17 int frame, type, size;
18 uint offset;
19
aviindexentryaviindexentry20 aviindexentry() {}
aviindexentryaviindexentry21 aviindexentry(int frame, int type, int size, uint offset) : frame(frame), type(type), size(size), offset(offset) {}
22 };
23
24 struct avisegmentinfo
25 {
26 stream::offset offset, videoindexoffset, soundindexoffset;
27 int firstindex;
28 uint videoindexsize, soundindexsize, indexframes, videoframes, soundframes;
29
avisegmentinfoavisegmentinfo30 avisegmentinfo() {}
avisegmentinfoavisegmentinfo31 avisegmentinfo(stream::offset offset, int firstindex) : offset(offset), videoindexoffset(0), soundindexoffset(0), firstindex(firstindex), videoindexsize(0), soundindexsize(0), indexframes(0), videoframes(0), soundframes(0) {}
32 };
33
34 struct aviwriter
35 {
36 stream *f;
37 uchar *yuv;
38 uint videoframes;
39 stream::offset totalsize;
40 const uint videow, videoh, videofps;
41 string filename;
42
43 int soundfrequency, soundchannels;
44 Uint16 soundformat;
45
46 vector<aviindexentry> index;
47 vector<avisegmentinfo> segments;
48
49 stream::offset fileframesoffset, fileextframesoffset, filevideooffset, filesoundoffset, superindexvideooffset, superindexsoundoffset;
50
51 enum { MAX_CHUNK_DEPTH = 16, MAX_SUPER_INDEX = 1024 };
52 stream::offset chunkoffsets[MAX_CHUNK_DEPTH];
53 int chunkdepth;
54
addindexaviwriter55 aviindexentry &addindex(int frame, int type, int size)
56 {
57 avisegmentinfo &seg = segments.last();
58 int i = index.length();
59 while(--i >= seg.firstindex)
60 {
61 aviindexentry &e = index[i];
62 if(frame > e.frame || (frame == e.frame && type <= e.type)) break;
63 }
64 return index.insert(i + 1, aviindexentry(frame, type, size, uint(totalsize - chunkoffsets[chunkdepth])));
65 }
66
filespaceguessaviwriter67 double filespaceguess()
68 {
69 return double(totalsize);
70 }
71
startchunkaviwriter72 void startchunk(const char *fcc, uint size = 0)
73 {
74 f->write(fcc, 4);
75 f->putlil<uint>(size);
76 totalsize += 4 + 4;
77 chunkoffsets[++chunkdepth] = totalsize;
78 totalsize += size;
79 }
80
listchunkaviwriter81 void listchunk(const char *fcc, const char *lfcc)
82 {
83 startchunk(fcc);
84 f->write(lfcc, 4);
85 totalsize += 4;
86 }
87
endchunkaviwriter88 void endchunk()
89 {
90 ASSERT(chunkdepth >= 0);
91 --chunkdepth;
92 }
93
endlistchunkaviwriter94 void endlistchunk()
95 {
96 ASSERT(chunkdepth >= 0);
97 int size = int(totalsize - chunkoffsets[chunkdepth]);
98 f->seek(-4 - size, SEEK_CUR);
99 f->putlil(size);
100 f->seek(0, SEEK_END);
101 if(size & 1) { f->putchar(0x00); totalsize++; }
102 endchunk();
103 }
104
writechunkaviwriter105 void writechunk(const char *fcc, const void *data, uint len) // simplify startchunk()/endchunk() to avoid f->seek()
106 {
107 f->write(fcc, 4);
108 f->putlil(len);
109 f->write(data, len);
110 totalsize += 4 + 4 + len;
111 if(len & 1) { f->putchar(0x00); totalsize++; }
112 }
113
closeaviwriter114 void close()
115 {
116 if(!f) return;
117 flushsegment();
118
119 uint soundindexes = 0, videoindexes = 0, soundframes = 0, videoframes = 0, indexframes = 0;
120 loopv(segments)
121 {
122 avisegmentinfo &seg = segments[i];
123 if(seg.soundindexsize) soundindexes++;
124 videoindexes++;
125 soundframes += seg.soundframes;
126 videoframes += seg.videoframes;
127 indexframes += seg.indexframes;
128 }
129 if(dbgmovie) conoutf(CON_DEBUG, "fileframes: sound=%d, video=%d+%d(dups)\n", soundframes, videoframes, indexframes-videoframes);
130 f->seek(fileframesoffset, SEEK_SET);
131 f->putlil<uint>(segments[0].indexframes);
132 f->seek(filevideooffset, SEEK_SET);
133 f->putlil<uint>(segments[0].videoframes);
134 if(segments[0].soundframes > 0)
135 {
136 f->seek(filesoundoffset, SEEK_SET);
137 f->putlil<uint>(segments[0].soundframes);
138 }
139 f->seek(fileextframesoffset, SEEK_SET);
140 f->putlil<uint>(indexframes); // total video frames
141
142 f->seek(superindexvideooffset + 2 + 2, SEEK_SET);
143 f->putlil<uint>(videoindexes);
144 f->seek(superindexvideooffset + 2 + 2 + 4 + 4 + 4 + 4 + 4, SEEK_SET);
145 loopv(segments)
146 {
147 avisegmentinfo &seg = segments[i];
148 f->putlil<uint>(seg.videoindexoffset&stream::offset(0xFFFFFFFFU));
149 f->putlil<uint>(seg.videoindexoffset>>32);
150 f->putlil<uint>(seg.videoindexsize);
151 f->putlil<uint>(seg.indexframes);
152 }
153
154 if(soundindexes > 0)
155 {
156 f->seek(superindexsoundoffset + 2 + 2, SEEK_SET);
157 f->putlil<uint>(soundindexes);
158 f->seek(superindexsoundoffset + 2 + 2 + 4 + 4 + 4 + 4 + 4, SEEK_SET);
159 loopv(segments)
160 {
161 avisegmentinfo &seg = segments[i];
162 if(!seg.soundindexsize) continue;
163 f->putlil<uint>(seg.soundindexoffset&stream::offset(0xFFFFFFFFU));
164 f->putlil<uint>(seg.soundindexoffset>>32);
165 f->putlil<uint>(seg.soundindexsize);
166 f->putlil<uint>(seg.soundframes);
167 }
168 }
169
170 f->seek(0, SEEK_END);
171
172 DELETEP(f);
173 }
174
aviwriteraviwriter175 aviwriter(const char *name, uint w, uint h, uint fps, bool sound) : f(NULL), yuv(NULL), videoframes(0), totalsize(0), videow(w&~1), videoh(h&~1), videofps(fps), soundfrequency(0),soundchannels(0),soundformat(0)
176 {
177 copystring(filename, name);
178 path(filename);
179 if(!strrchr(filename, '.')) concatstring(filename, ".avi");
180
181 extern bool nosound; // sound.cpp
182 if(sound && !nosound)
183 {
184 Mix_QuerySpec(&soundfrequency, &soundformat, &soundchannels);
185 const char *desc;
186 switch(soundformat)
187 {
188 case AUDIO_U8: desc = "u8"; break;
189 case AUDIO_S8: desc = "s8"; break;
190 case AUDIO_U16LSB: desc = "u16l"; break;
191 case AUDIO_U16MSB: desc = "u16b"; break;
192 case AUDIO_S16LSB: desc = "s16l"; break;
193 case AUDIO_S16MSB: desc = "s16b"; break;
194 default: desc = "unkn";
195 }
196 if(dbgmovie) conoutf(CON_DEBUG, "soundspec: %dhz %s x %d", soundfrequency, desc, soundchannels);
197 }
198 }
199
~aviwriteraviwriter200 ~aviwriter()
201 {
202 close();
203 if(yuv) delete [] yuv;
204 }
205
openaviwriter206 bool open()
207 {
208 f = openfile(filename, "wb");
209 if(!f) return false;
210
211 chunkdepth = -1;
212
213 listchunk("RIFF", "AVI ");
214
215 listchunk("LIST", "hdrl");
216
217 startchunk("avih", 56);
218 f->putlil<uint>(1000000 / videofps); // microsecsperframe
219 f->putlil<uint>(0); // maxbytespersec
220 f->putlil<uint>(0); // reserved
221 f->putlil<uint>(0x10 | 0x20); // flags - hasindex|mustuseindex
222 fileframesoffset = f->tell();
223 f->putlil<uint>(0); // totalvideoframes
224 f->putlil<uint>(0); // initialframes
225 f->putlil<uint>(soundfrequency > 0 ? 2 : 1); // streams
226 f->putlil<uint>(0); // buffersize
227 f->putlil<uint>(videow); // video width
228 f->putlil<uint>(videoh); // video height
229 loopi(4) f->putlil<uint>(0); // reserved
230 endchunk(); // avih
231
232 listchunk("LIST", "strl");
233
234 startchunk("strh", 56);
235 f->write("vids", 4); // fcctype
236 f->write("I420", 4); // fcchandler
237 f->putlil<uint>(0); // flags
238 f->putlil<uint>(0); // priority
239 f->putlil<uint>(0); // initialframes
240 f->putlil<uint>(1); // scale
241 f->putlil<uint>(videofps); // rate
242 f->putlil<uint>(0); // start
243 filevideooffset = f->tell();
244 f->putlil<uint>(0); // length
245 f->putlil<uint>(videow*videoh*3/2); // suggested buffersize
246 f->putlil<uint>(0); // quality
247 f->putlil<uint>(0); // samplesize
248 f->putlil<ushort>(0); // frame left
249 f->putlil<ushort>(0); // frame top
250 f->putlil<ushort>(videow); // frame right
251 f->putlil<ushort>(videoh); // frame bottom
252 endchunk(); // strh
253
254 startchunk("strf", 40);
255 f->putlil<uint>(40); //headersize
256 f->putlil<uint>(videow); // width
257 f->putlil<uint>(videoh); // height
258 f->putlil<ushort>(3); // planes
259 f->putlil<ushort>(12); // bitcount
260 f->write("I420", 4); // compression
261 f->putlil<uint>(videow*videoh*3/2); // imagesize
262 f->putlil<uint>(0); // xres
263 f->putlil<uint>(0); // yres;
264 f->putlil<uint>(0); // colorsused
265 f->putlil<uint>(0); // colorsrequired
266 endchunk(); // strf
267
268 startchunk("indx", 24 + 16*MAX_SUPER_INDEX);
269 superindexvideooffset = f->tell();
270 f->putlil<ushort>(4); // longs per entry
271 f->putlil<ushort>(0); // index of indexes
272 f->putlil<uint>(0); // entries in use
273 f->write("00dc", 4); // chunk id
274 f->putlil<uint>(0); // reserved 1
275 f->putlil<uint>(0); // reserved 2
276 f->putlil<uint>(0); // reserved 3
277 loopi(MAX_SUPER_INDEX)
278 {
279 f->putlil<uint>(0); // offset low
280 f->putlil<uint>(0); // offset high
281 f->putlil<uint>(0); // size
282 f->putlil<uint>(0); // duration
283 }
284 endchunk(); // indx
285
286 startchunk("vprp", 68);
287 f->putlil<uint>(0); // video format token
288 f->putlil<uint>(0); // video standard
289 f->putlil<uint>(videofps); // vertical refresh rate
290 f->putlil<uint>(videow); // horizontal total
291 f->putlil<uint>(videoh); // vertical total
292 int gcd = screenw, rem = screenh;
293 while(rem > 0) { gcd %= rem; swap(gcd, rem); }
294 f->putlil<ushort>(screenh/gcd); // aspect denominator
295 f->putlil<ushort>(screenw/gcd); // aspect numerator
296 f->putlil<uint>(videow); // frame width
297 f->putlil<uint>(videoh); // frame height
298 f->putlil<uint>(1); // fields per frame
299 f->putlil<uint>(videoh); // compressed bitmap height
300 f->putlil<uint>(videow); // compressed bitmap width
301 f->putlil<uint>(videoh); // valid bitmap height
302 f->putlil<uint>(videow); // valid bitmap width
303 f->putlil<uint>(0); // valid bitmap x offset
304 f->putlil<uint>(0); // valid bitmap y offset
305 f->putlil<uint>(0); // video x offset
306 f->putlil<uint>(0); // video y start
307 endchunk(); // vprp
308
309 endlistchunk(); // LIST strl
310
311 if(soundfrequency > 0)
312 {
313 const int bps = (soundformat==AUDIO_U8 || soundformat == AUDIO_S8) ? 1 : 2;
314
315 listchunk("LIST", "strl");
316
317 startchunk("strh", 56);
318 f->write("auds", 4); // fcctype
319 f->putlil<uint>(1); // fcchandler - normally 4cc, but audio is a special case
320 f->putlil<uint>(0); // flags
321 f->putlil<uint>(0); // priority
322 f->putlil<uint>(0); // initialframes
323 f->putlil<uint>(1); // scale
324 f->putlil<uint>(soundfrequency); // rate
325 f->putlil<uint>(0); // start
326 filesoundoffset = f->tell();
327 f->putlil<uint>(0); // length
328 f->putlil<uint>(soundfrequency*bps*soundchannels/2); // suggested buffer size (this is a half second)
329 f->putlil<uint>(0); // quality
330 f->putlil<uint>(bps*soundchannels); // samplesize
331 f->putlil<ushort>(0); // frame left
332 f->putlil<ushort>(0); // frame top
333 f->putlil<ushort>(0); // frame right
334 f->putlil<ushort>(0); // frame bottom
335 endchunk(); // strh
336
337 startchunk("strf", 18);
338 f->putlil<ushort>(1); // format (uncompressed PCM)
339 f->putlil<ushort>(soundchannels); // channels
340 f->putlil<uint>(soundfrequency); // sampleframes per second
341 f->putlil<uint>(soundfrequency*bps*soundchannels); // average bytes per second
342 f->putlil<ushort>(bps*soundchannels); // block align <-- guess
343 f->putlil<ushort>(bps*8); // bits per sample
344 f->putlil<ushort>(0); // size
345 endchunk(); //strf
346
347 startchunk("indx", 24 + 16*MAX_SUPER_INDEX);
348 superindexsoundoffset = f->tell();
349 f->putlil<ushort>(4); // longs per entry
350 f->putlil<ushort>(0); // index of indexes
351 f->putlil<uint>(0); // entries in use
352 f->write("01wb", 4); // chunk id
353 f->putlil<uint>(0); // reserved 1
354 f->putlil<uint>(0); // reserved 2
355 f->putlil<uint>(0); // reserved 3
356 loopi(MAX_SUPER_INDEX)
357 {
358 f->putlil<uint>(0); // offset low
359 f->putlil<uint>(0); // offset high
360 f->putlil<uint>(0); // size
361 f->putlil<uint>(0); // duration
362 }
363 endchunk(); // indx
364
365 endlistchunk(); // LIST strl
366 }
367
368 listchunk("LIST", "odml");
369 startchunk("dmlh", 4);
370 fileextframesoffset = f->tell();
371 f->putlil<uint>(0);
372 endchunk(); // dmlh
373 endlistchunk(); // LIST odml
374
375 listchunk("LIST", "INFO");
376 const char *software = "Cube 2: Sauerbraten";
377 writechunk("ISFT", software, strlen(software)+1);
378 endlistchunk(); // LIST INFO
379
380 endlistchunk(); // LIST hdrl
381
382 nextsegment();
383
384 return true;
385 }
386
boxsampleaviwriter387 static inline void boxsample(const uchar *src, const uint stride,
388 const uint area, const uint w, uint h,
389 const uint xlow, const uint xhigh, const uint ylow, const uint yhigh,
390 uint &bdst, uint &gdst, uint &rdst)
391 {
392 const uchar *end = &src[w<<2];
393 uint bt = 0, gt = 0, rt = 0;
394 for(const uchar *cur = &src[4]; cur < end; cur += 4)
395 {
396 bt += cur[0];
397 gt += cur[1];
398 rt += cur[2];
399 }
400 bt = ylow*(bt + ((src[0]*xlow + end[0]*xhigh)>>12));
401 gt = ylow*(gt + ((src[1]*xlow + end[1]*xhigh)>>12));
402 rt = ylow*(rt + ((src[2]*xlow + end[2]*xhigh)>>12));
403 if(h)
404 {
405 for(src += stride, end += stride; --h; src += stride, end += stride)
406 {
407 uint b = 0, g = 0, r = 0;
408 for(const uchar *cur = &src[4]; cur < end; cur += 4)
409 {
410 b += cur[0];
411 g += cur[1];
412 r += cur[2];
413 }
414 bt += (b<<12) + src[0]*xlow + end[0]*xhigh;
415 gt += (g<<12) + src[1]*xlow + end[1]*xhigh;
416 rt += (r<<12) + src[2]*xlow + end[2]*xhigh;
417 }
418 uint b = 0, g = 0, r = 0;
419 for(const uchar *cur = &src[4]; cur < end; cur += 4)
420 {
421 b += cur[0];
422 g += cur[1];
423 r += cur[2];
424 }
425 bt += yhigh*(b + ((src[0]*xlow + end[0]*xhigh)>>12));
426 gt += yhigh*(g + ((src[1]*xlow + end[1]*xhigh)>>12));
427 rt += yhigh*(r + ((src[2]*xlow + end[2]*xhigh)>>12));
428 }
429 bdst = (bt*area)>>24;
430 gdst = (gt*area)>>24;
431 rdst = (rt*area)>>24;
432 }
433
scaleyuvaviwriter434 void scaleyuv(const uchar *pixels, uint srcw, uint srch)
435 {
436 const int flip = -1;
437 const uint planesize = videow * videoh;
438 if(!yuv) yuv = new uchar[(planesize*3)/2];
439 uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4;
440 const int ystride = flip*int(videow), uvstride = flip*int(videow)/2;
441 if(flip < 0) { yplane -= int(videoh-1)*ystride; uplane -= int(videoh/2-1)*uvstride; vplane -= int(videoh/2-1)*uvstride; }
442
443 const uint stride = srcw<<2;
444 srcw &= ~1;
445 srch &= ~1;
446 const uint wfrac = (srcw<<12)/videow, hfrac = (srch<<12)/videoh,
447 area = ((ullong)planesize<<12)/(srcw*srch + 1),
448 dw = videow*wfrac, dh = videoh*hfrac;
449
450 for(uint y = 0; y < dh;)
451 {
452 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;
453 y += hfrac;
454 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;
455 y += hfrac;
456
457 const uchar *src = &pixels[yi*stride], *src2 = &pixels[y2i*stride];
458 uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane;
459 for(uint x = 0; x < dw;)
460 {
461 uint xn = x + wfrac - 1, xi = x>>12, w = (xn>>12) - xi, xlow = ((w+0xFFFU)&0x1000U) - (x&0xFFFU), xhigh = (xn&0xFFFU) + 1;
462 x += wfrac;
463 uint x2n = x + wfrac - 1, x2i = x>>12, w2 = (x2n>>12) - x2i, x2low = ((w2+0xFFFU)&0x1000U) - (x&0xFFFU), x2high = (x2n&0xFFFU) + 1;
464 x += wfrac;
465
466 uint b1, g1, r1, b2, g2, r2, b3, g3, r3, b4, g4, r4;
467 boxsample(&src[xi<<2], stride, area, w, h, xlow, xhigh, ylow, yhigh, b1, g1, r1);
468 boxsample(&src[x2i<<2], stride, area, w2, h, x2low, x2high, ylow, yhigh, b2, g2, r2);
469 boxsample(&src2[xi<<2], stride, area, w, h2, xlow, xhigh, y2low, y2high, b3, g3, r3);
470 boxsample(&src2[x2i<<2], stride, area, w2, h2, x2low, x2high, y2low, y2high, b4, g4, r4);
471
472
473 // Y = 16 + 65.481*R + 128.553*G + 24.966*B
474 // Cb = 128 - 37.797*R - 74.203*G + 112.0*B
475 // Cr = 128 + 112.0*R - 93.786*G - 18.214*B
476 *ydst++ = ((16<<12) + 1052*r1 + 2065*g1 + 401*b1)>>12;
477 *ydst++ = ((16<<12) + 1052*r2 + 2065*g2 + 401*b2)>>12;
478 *ydst2++ = ((16<<12) + 1052*r3 + 2065*g3 + 401*b3)>>12;;
479 *ydst2++ = ((16<<12) + 1052*r4 + 2065*g4 + 401*b4)>>12;;
480
481 const uint b = b1 + b2 + b3 + b4,
482 g = g1 + g2 + g3 + g4,
483 r = r1 + r2 + r3 + r4;
484 // note: weights here are scaled by 1<<10, as opposed to 1<<12, since r/g/b are already *4
485 *udst++ = ((128<<12) - 152*r - 298*g + 450*b)>>12;
486 *vdst++ = ((128<<12) + 450*r - 377*g - 73*b)>>12;
487 }
488
489 yplane += 2*ystride;
490 uplane += uvstride;
491 vplane += uvstride;
492 }
493 }
494
encodeyuvaviwriter495 void encodeyuv(const uchar *pixels)
496 {
497 const int flip = -1;
498 const uint planesize = videow * videoh;
499 if(!yuv) yuv = new uchar[(planesize*3)/2];
500 uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4;
501 const int ystride = flip*int(videow), uvstride = flip*int(videow)/2;
502 if(flip < 0) { yplane -= int(videoh-1)*ystride; uplane -= int(videoh/2-1)*uvstride; vplane -= int(videoh/2-1)*uvstride; }
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 const uint b1 = src[0], g1 = src[1], r1 = src[2],
513 b2 = src[4], g2 = src[5], r2 = src[6],
514 b3 = src2[0], g3 = src2[1], r3 = src2[2],
515 b4 = src2[4], g4 = src2[5], r4 = src2[6];
516
517 // Y = 16 + 65.481*R + 128.553*G + 24.966*B
518 // Cb = 128 - 37.797*R - 74.203*G + 112.0*B
519 // Cr = 128 + 112.0*R - 93.786*G - 18.214*B
520 *ydst++ = ((16<<12) + 1052*r1 + 2065*g1 + 401*b1)>>12;
521 *ydst++ = ((16<<12) + 1052*r2 + 2065*g2 + 401*b2)>>12;
522 *ydst2++ = ((16<<12) + 1052*r3 + 2065*g3 + 401*b3)>>12;;
523 *ydst2++ = ((16<<12) + 1052*r4 + 2065*g4 + 401*b4)>>12;;
524
525 const uint b = b1 + b2 + b3 + b4,
526 g = g1 + g2 + g3 + g4,
527 r = r1 + r2 + r3 + r4;
528 // note: weights here are scaled by 1<<10, as opposed to 1<<12, since r/g/b are already *4
529 *udst++ = ((128<<12) - 152*r - 298*g + 450*b)>>12;
530 *vdst++ = ((128<<12) + 450*r - 377*g - 73*b)>>12;
531
532 src += 8;
533 src2 += 8;
534 }
535 src = src2;
536 yplane += 2*ystride;
537 uplane += uvstride;
538 vplane += uvstride;
539 }
540 }
541
compressyuvaviwriter542 void compressyuv(const uchar *pixels)
543 {
544 const int flip = -1;
545 const uint planesize = videow * videoh;
546 if(!yuv) yuv = new uchar[(planesize*3)/2];
547 uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4;
548 const int ystride = flip*int(videow), uvstride = flip*int(videow)/2;
549 if(flip < 0) { yplane -= int(videoh-1)*ystride; uplane -= int(videoh/2-1)*uvstride; vplane -= int(videoh/2-1)*uvstride; }
550
551 const uint stride = videow<<2;
552 const uchar *src = pixels, *yend = src + videoh*stride;
553 while(src < yend)
554 {
555 const uchar *src2 = src + stride, *xend = src2;
556 uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane;
557 while(src < xend)
558 {
559 *ydst++ = src[0];
560 *ydst++ = src[4];
561 *ydst2++ = src2[0];
562 *ydst2++ = src2[4];
563
564 *udst++ = (uint(src[1]) + uint(src[5]) + uint(src2[1]) + uint(src2[5])) >> 2;
565 *vdst++ = (uint(src[2]) + uint(src[6]) + uint(src2[2]) + uint(src2[6])) >> 2;
566
567 src += 8;
568 src2 += 8;
569 }
570 src = src2;
571 yplane += 2*ystride;
572 uplane += uvstride;
573 vplane += uvstride;
574 }
575 }
576
writesoundaviwriter577 bool writesound(uchar *data, uint framesize, uint frame)
578 {
579 // do conversion in-place to little endian format
580 // note that xoring by half the range yields the same bit pattern as subtracting the range regardless of signedness
581 // ... so can toggle signedness just by xoring the high byte with 0x80
582 switch(soundformat)
583 {
584 case AUDIO_U8:
585 for(uchar *dst = data, *end = &data[framesize]; dst < end; dst++) *dst ^= 0x80;
586 break;
587 case AUDIO_S8:
588 break;
589 case AUDIO_U16LSB:
590 for(uchar *dst = &data[1], *end = &data[framesize]; dst < end; dst += 2) *dst ^= 0x80;
591 break;
592 case AUDIO_U16MSB:
593 for(ushort *dst = (ushort *)data, *end = (ushort *)&data[framesize]; dst < end; dst++)
594 #if SDL_BYTEORDER == SDL_BIG_ENDIAN
595 *dst = endianswap(*dst) ^ 0x0080;
596 #else
597 *dst = endianswap(*dst) ^ 0x8000;
598 #endif
599 break;
600 case AUDIO_S16LSB:
601 break;
602 case AUDIO_S16MSB:
603 endianswap((short *)data, framesize/2);
604 break;
605 }
606
607 if(totalsize - segments.last().offset + framesize > 1000*1000*1000 && !nextsegment()) return false;
608
609 addindex(frame, 1, framesize);
610
611 writechunk("01wb", data, framesize);
612
613 return true;
614 }
615
616
617 enum
618 {
619 VID_RGB = 0,
620 VID_YUV,
621 VID_YUV420
622 };
623
flushsegmentaviwriter624 void flushsegment()
625 {
626 endlistchunk(); // LIST movi
627
628 avisegmentinfo &seg = segments.last();
629
630 uint indexframes = 0, videoframes = 0, soundframes = 0;
631 for(int i = seg.firstindex; i < index.length(); i++)
632 {
633 aviindexentry &e = index[i];
634 if(e.type) soundframes++;
635 else
636 {
637 if(i == seg.firstindex || e.offset != index[i-1].offset)
638 videoframes++;
639 indexframes++;
640 }
641 }
642
643 if(segments.length() == 1)
644 {
645 startchunk("idx1", index.length()*16);
646 loopv(index)
647 {
648 aviindexentry &entry = index[i];
649 // printf("%3d %s %08x\n", i, (entry.type==1)?"s":"v", entry.offset);
650 f->write(entry.type ? "01wb" : "00dc", 4); // chunkid
651 f->putlil<uint>(0x10); // flags - KEYFRAME
652 f->putlil<uint>(entry.offset); // offset (relative to movi)
653 f->putlil<uint>(entry.size); // size
654 }
655 endchunk();
656 }
657
658 seg.videoframes = videoframes;
659 seg.videoindexoffset = totalsize;
660 startchunk("ix00", 24 + indexframes*8);
661 f->putlil<ushort>(2); // longs per entry
662 f->putlil<ushort>(0x0100); // index of chunks
663 f->putlil<uint>(indexframes); // entries in use
664 f->write("00dc", 4); // chunk id
665 f->putlil<uint>(seg.offset&stream::offset(0xFFFFFFFFU)); // offset low
666 f->putlil<uint>(seg.offset>>32); // offset high
667 f->putlil<uint>(0); // reserved 3
668 for(int i = seg.firstindex; i < index.length(); i++)
669 {
670 aviindexentry &e = index[i];
671 if(e.type) continue;
672 f->putlil<uint>(e.offset + 4 + 4);
673 f->putlil<uint>(e.size);
674 }
675 endchunk(); // ix00
676 seg.videoindexsize = uint(totalsize - seg.videoindexoffset);
677
678 if(soundframes)
679 {
680 seg.soundframes = soundframes;
681 seg.soundindexoffset = totalsize;
682 startchunk("ix01", 24 + soundframes*8);
683 f->putlil<ushort>(2); // longs per entry
684 f->putlil<ushort>(0x0100); // index of chunks
685 f->putlil<uint>(soundframes); // entries in use
686 f->write("01wb", 4); // chunk id
687 f->putlil<uint>(seg.offset&stream::offset(0xFFFFFFFFU)); // offset low
688 f->putlil<uint>(seg.offset>>32); // offset high
689 f->putlil<uint>(0); // reserved 3
690 for(int i = seg.firstindex; i < index.length(); i++)
691 {
692 aviindexentry &e = index[i];
693 if(!e.type) continue;
694 f->putlil<uint>(e.offset + 4 + 4);
695 f->putlil<uint>(e.size);
696 }
697 endchunk(); // ix01
698 seg.soundindexsize = uint(totalsize - seg.soundindexoffset);
699 }
700
701 endlistchunk(); // RIFF AVI/AVIX
702 }
703
nextsegmentaviwriter704 bool nextsegment()
705 {
706 if(segments.length())
707 {
708 if(segments.length() >= MAX_SUPER_INDEX) return false;
709 flushsegment();
710 listchunk("RIFF", "AVIX");
711 }
712 listchunk("LIST", "movi");
713 segments.add(avisegmentinfo(chunkoffsets[chunkdepth], index.length()));
714 return true;
715 }
716
writevideoframeaviwriter717 bool writevideoframe(const uchar *pixels, uint srcw, uint srch, int format, uint frame)
718 {
719 if(frame < videoframes) return true;
720
721 switch(format)
722 {
723 case VID_RGB:
724 if(srcw != videow || srch != videoh) scaleyuv(pixels, srcw, srch);
725 else encodeyuv(pixels);
726 break;
727 case VID_YUV:
728 compressyuv(pixels);
729 break;
730 }
731
732 const uint framesize = (videow * videoh * 3) / 2;
733 if(totalsize - segments.last().offset + framesize > 1000*1000*1000 && !nextsegment()) return false;
734
735 while(videoframes <= frame) addindex(videoframes++, 0, framesize);
736
737 writechunk("00dc", format == VID_YUV420 ? pixels : yuv, framesize);
738
739 return true;
740 }
741
742 };
743
744 VAR(movieaccelblit, 0, 0, 1);
745 VAR(movieaccelyuv, 0, 1, 1);
746 VARP(movieaccel, 0, 1, 1);
747 VARP(moviesync, 0, 0, 1);
748 FVARP(movieminquality, 0, 0, 1);
749
750 namespace recorder
751 {
752 static enum { REC_OK = 0, REC_USERHALT, REC_TOOSLOW, REC_FILERROR } state = REC_OK;
753
754 static aviwriter *file = NULL;
755 static int starttime = 0;
756
757 static int stats[1000];
758 static int statsindex = 0;
759 static uint dps = 0; // dropped frames per sample
760
761 enum { MAXSOUNDBUFFERS = 128 }; // sounds queue up until there is a video frame, so at low fps you'll need a bigger queue
762 struct soundbuffer
763 {
764 uchar *sound;
765 uint size, maxsize;
766 uint frame;
767
soundbufferrecorder::soundbuffer768 soundbuffer() : sound(NULL), maxsize(0) {}
~soundbufferrecorder::soundbuffer769 ~soundbuffer() { cleanup(); }
770
loadrecorder::soundbuffer771 void load(uchar *stream, uint len, uint fnum)
772 {
773 if(len > maxsize)
774 {
775 DELETEA(sound);
776 sound = new uchar[len];
777 maxsize = len;
778 }
779 size = len;
780 frame = fnum;
781 memcpy(sound, stream, len);
782 }
783
cleanuprecorder::soundbuffer784 void cleanup() { DELETEA(sound); maxsize = 0; }
785 };
786 static queue<soundbuffer, MAXSOUNDBUFFERS> soundbuffers;
787 static SDL_mutex *soundlock = NULL;
788
789 enum { MAXVIDEOBUFFERS = 2 }; // double buffer
790 struct videobuffer
791 {
792 uchar *video;
793 uint w, h, bpp, frame;
794 int format;
795
videobufferrecorder::videobuffer796 videobuffer() : video(NULL){}
~videobufferrecorder::videobuffer797 ~videobuffer() { cleanup(); }
798
initrecorder::videobuffer799 void init(int nw, int nh, int nbpp)
800 {
801 DELETEA(video);
802 w = nw;
803 h = nh;
804 bpp = nbpp;
805 video = new uchar[w*h*bpp];
806 format = -1;
807 }
808
cleanuprecorder::videobuffer809 void cleanup() { DELETEA(video); }
810 };
811 static queue<videobuffer, MAXVIDEOBUFFERS> videobuffers;
812 static uint lastframe = ~0U;
813
814 static GLuint scalefb = 0, scaletex[2] = { 0, 0 };
815 static uint scalew = 0, scaleh = 0;
816 static GLuint encodefb = 0, encoderb = 0;
817
818 static SDL_Thread *thread = NULL;
819 static SDL_mutex *videolock = NULL;
820 static SDL_cond *shouldencode = NULL, *shouldread = NULL;
821
isrecording()822 bool isrecording() { return file != NULL; }
823
calcquality()824 float calcquality()
825 {
826 return 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
827 }
828
gettime()829 int gettime()
830 {
831 return inbetweenframes ? getclockmillis() : totalmillis;
832 }
833
videoencoder(void * data)834 int videoencoder(void *data) // runs on a separate thread
835 {
836 for(int numvid = 0, numsound = 0;;)
837 {
838 SDL_LockMutex(videolock);
839 for(; numvid > 0; numvid--) videobuffers.remove();
840 SDL_CondSignal(shouldread);
841 while(videobuffers.empty() && state == REC_OK) SDL_CondWait(shouldencode, videolock);
842 if(state != REC_OK) { SDL_UnlockMutex(videolock); break; }
843 videobuffer &m = videobuffers.removing();
844 numvid++;
845 SDL_UnlockMutex(videolock);
846
847 if(file->soundfrequency > 0)
848 {
849 // chug data from lock protected buffer to avoid holding lock while writing to file
850 SDL_LockMutex(soundlock);
851 for(; numsound > 0; numsound--) soundbuffers.remove();
852 for(; numsound < soundbuffers.length(); numsound++)
853 {
854 soundbuffer &s = soundbuffers.removing(numsound);
855 if(s.frame > m.frame) break; // sync with video
856 }
857 SDL_UnlockMutex(soundlock);
858 loopi(numsound)
859 {
860 soundbuffer &s = soundbuffers.removing(i);
861 if(!file->writesound(s.sound, s.size, s.frame)) state = REC_FILERROR;
862 }
863 }
864
865 int duplicates = m.frame - (int)file->videoframes + 1;
866 if(duplicates > 0) // determine how many frames have been dropped over the sample window
867 {
868 dps -= stats[statsindex];
869 stats[statsindex] = duplicates-1;
870 dps += stats[statsindex];
871 statsindex = (statsindex+1)%file->videofps;
872 }
873 //printf("frame %d->%d (%d dps): sound = %d bytes\n", file->videoframes, nextframenum, dps, m.soundlength);
874 if(calcquality() < movieminquality) state = REC_TOOSLOW;
875 else if(!file->writevideoframe(m.video, m.w, m.h, m.format, m.frame)) state = REC_FILERROR;
876
877 m.frame = ~0U;
878 }
879
880 return 0;
881 }
882
soundencoder(void * udata,Uint8 * stream,int len)883 void soundencoder(void *udata, Uint8 *stream, int len) // callback occurs on a separate thread
884 {
885 SDL_LockMutex(soundlock);
886 if(soundbuffers.full())
887 {
888 if(movieminquality >= 1) state = REC_TOOSLOW;
889 }
890 else if(state == REC_OK)
891 {
892 uint nextframe = (max(gettime() - starttime, 0)*file->videofps)/1000;
893 soundbuffer &s = soundbuffers.add();
894 s.load((uchar *)stream, len, nextframe);
895 }
896 SDL_UnlockMutex(soundlock);
897 }
898
start(const char * filename,int videofps,int videow,int videoh,bool sound)899 void start(const char *filename, int videofps, int videow, int videoh, bool sound)
900 {
901 if(file) return;
902
903 useshaderbyname("moviergb");
904 useshaderbyname("movieyuv");
905 useshaderbyname("moviey");
906 useshaderbyname("movieu");
907 useshaderbyname("moviev");
908
909 int fps, bestdiff, worstdiff;
910 getfps(fps, bestdiff, worstdiff);
911 if(videofps > fps) conoutf(CON_WARN, "frame rate may be too low to capture at %d fps", videofps);
912
913 if(videow%2) videow += 1;
914 if(videoh%2) videoh += 1;
915
916 file = new aviwriter(filename, videow, videoh, videofps, sound);
917 if(!file->open())
918 {
919 conoutf(CON_ERROR, "unable to create file %s", filename);
920 DELETEP(file);
921 return;
922 }
923 conoutf("movie recording to: %s %dx%d @ %dfps%s", file->filename, file->videow, file->videoh, file->videofps, (file->soundfrequency>0)?" + sound":"");
924
925 starttime = gettime();
926 loopi(file->videofps) stats[i] = 0;
927 statsindex = 0;
928 dps = 0;
929
930 lastframe = ~0U;
931 videobuffers.clear();
932 loopi(MAXVIDEOBUFFERS)
933 {
934 uint w = screenw, h = screenw;
935 videobuffers.data[i].init(w, h, 4);
936 videobuffers.data[i].frame = ~0U;
937 }
938
939 soundbuffers.clear();
940
941 soundlock = SDL_CreateMutex();
942 videolock = SDL_CreateMutex();
943 shouldencode = SDL_CreateCond();
944 shouldread = SDL_CreateCond();
945 thread = SDL_CreateThread(videoencoder, "video encoder", NULL);
946 if(file->soundfrequency > 0) Mix_SetPostMix(soundencoder, NULL);
947 }
948
cleanup()949 void cleanup()
950 {
951 if(scalefb) { glDeleteFramebuffers_(1, &scalefb); scalefb = 0; }
952 if(scaletex[0] || scaletex[1]) { glDeleteTextures(2, scaletex); memset(scaletex, 0, sizeof(scaletex)); }
953 scalew = scaleh = 0;
954 if(encodefb) { glDeleteFramebuffers_(1, &encodefb); encodefb = 0; }
955 if(encoderb) { glDeleteRenderbuffers_(1, &encoderb); encoderb = 0; }
956 }
957
stop()958 void stop()
959 {
960 if(!file) return;
961 if(state == REC_OK) state = REC_USERHALT;
962 if(file->soundfrequency > 0) Mix_SetPostMix(NULL, NULL);
963
964 SDL_LockMutex(videolock); // wakeup thread enough to kill it
965 SDL_CondSignal(shouldencode);
966 SDL_UnlockMutex(videolock);
967
968 SDL_WaitThread(thread, NULL); // block until thread is finished
969
970 cleanup();
971
972 loopi(MAXVIDEOBUFFERS) videobuffers.data[i].cleanup();
973 loopi(MAXSOUNDBUFFERS) soundbuffers.data[i].cleanup();
974
975 SDL_DestroyMutex(soundlock);
976 SDL_DestroyMutex(videolock);
977 SDL_DestroyCond(shouldencode);
978 SDL_DestroyCond(shouldread);
979
980 soundlock = videolock = NULL;
981 shouldencode = shouldread = NULL;
982 thread = NULL;
983
984 static const char * const mesgs[] = { "ok", "stopped", "computer too slow", "file error"};
985 conoutf("movie recording halted: %s, %d frames", mesgs[state], file->videoframes);
986
987 DELETEP(file);
988 state = REC_OK;
989 }
990
readbuffer(videobuffer & m,uint nextframe)991 void readbuffer(videobuffer &m, uint nextframe)
992 {
993 bool accelyuv = movieaccelyuv && !(m.w%8),
994 usefbo = movieaccel && file->videow <= (uint)screenw && file->videoh <= (uint)screenh && (accelyuv || file->videow < (uint)screenw || file->videoh < (uint)screenh);
995 uint w = screenw, h = screenh;
996 if(usefbo) { w = file->videow; h = file->videoh; }
997 if(w != m.w || h != m.h) m.init(w, h, 4);
998 m.format = aviwriter::VID_RGB;
999 m.frame = nextframe;
1000
1001 glPixelStorei(GL_PACK_ALIGNMENT, texalign(m.video, m.w, 4));
1002 if(usefbo)
1003 {
1004 uint tw = screenw, th = screenh;
1005 if(hasFBB && movieaccelblit) { tw = max(tw/2, m.w); th = max(th/2, m.h); }
1006 if(tw != scalew || th != scaleh)
1007 {
1008 if(!scalefb) glGenFramebuffers_(1, &scalefb);
1009 loopi(2)
1010 {
1011 if(!scaletex[i]) glGenTextures(1, &scaletex[i]);
1012 createtexture(scaletex[i], tw, th, NULL, 3, 1, GL_RGB);
1013 }
1014 scalew = tw;
1015 scaleh = th;
1016 }
1017 if(accelyuv && (!encodefb || !encoderb))
1018 {
1019 if(!encodefb) glGenFramebuffers_(1, &encodefb);
1020 glBindFramebuffer_(GL_FRAMEBUFFER, encodefb);
1021 if(!encoderb) glGenRenderbuffers_(1, &encoderb);
1022 glBindRenderbuffer_(GL_RENDERBUFFER, encoderb);
1023 glRenderbufferStorage_(GL_RENDERBUFFER, GL_RGBA, (m.w*3)/8, m.h);
1024 glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, encoderb);
1025 glBindRenderbuffer_(GL_RENDERBUFFER, 0);
1026 glBindFramebuffer_(GL_FRAMEBUFFER, 0);
1027 }
1028
1029 if(tw < (uint)screenw || th < (uint)screenh)
1030 {
1031 glBindFramebuffer_(GL_READ_FRAMEBUFFER, 0);
1032 glBindFramebuffer_(GL_DRAW_FRAMEBUFFER, scalefb);
1033 glFramebufferTexture2D_(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, scaletex[0], 0);
1034 glBlitFramebuffer_(0, 0, screenw, screenh, 0, 0, tw, th, GL_COLOR_BUFFER_BIT, GL_LINEAR);
1035 glBindFramebuffer_(GL_DRAW_FRAMEBUFFER, 0);
1036 }
1037 else
1038 {
1039 glBindTexture(GL_TEXTURE_2D, scaletex[0]);
1040 glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, screenw, screenh);
1041 }
1042
1043 GLOBALPARAMF(moviescale, 1.0f/scalew, 1.0f/scaleh);
1044 if(tw > m.w || th > m.h || (!accelyuv && tw >= m.w && th >= m.h))
1045 {
1046 glBindFramebuffer_(GL_FRAMEBUFFER, scalefb);
1047 do
1048 {
1049 uint dw = max(tw/2, m.w), dh = max(th/2, m.h);
1050 glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, scaletex[1], 0);
1051 glViewport(0, 0, dw, dh);
1052 glBindTexture(GL_TEXTURE_2D, scaletex[0]);
1053 if(dw == m.w && dh == m.h && !accelyuv) { SETSHADER(movieyuv); m.format = aviwriter::VID_YUV; }
1054 else SETSHADER(moviergb);
1055 screenquad(tw/float(scalew), th/float(scaleh));
1056 tw = dw;
1057 th = dh;
1058 swap(scaletex[0], scaletex[1]);
1059 } while(tw > m.w || th > m.h);
1060 }
1061 if(accelyuv)
1062 {
1063 glBindFramebuffer_(GL_FRAMEBUFFER, encodefb);
1064 glBindTexture(GL_TEXTURE_2D, scaletex[0]);
1065 glViewport(0, 0, m.w/4, m.h); SETSHADER(moviey); screenquadflipped(m.w/float(scalew), m.h/float(scaleh));
1066 glViewport(m.w/4, 0, m.w/8, m.h/2); SETSHADER(movieu); screenquadflipped(m.w/float(scalew), m.h/float(scaleh));
1067 glViewport(m.w/4, m.h/2, m.w/8, m.h/2); SETSHADER(moviev); screenquadflipped(m.w/float(scalew), m.h/float(scaleh));
1068 const uint planesize = m.w * m.h;
1069 glPixelStorei(GL_PACK_ALIGNMENT, texalign(m.video, m.w/4, 4));
1070 glReadPixels(0, 0, m.w/4, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video);
1071 glPixelStorei(GL_PACK_ALIGNMENT, texalign(&m.video[planesize], m.w/8, 4));
1072 glReadPixels(m.w/4, 0, m.w/8, m.h/2, GL_BGRA, GL_UNSIGNED_BYTE, &m.video[planesize]);
1073 glPixelStorei(GL_PACK_ALIGNMENT, texalign(&m.video[planesize + planesize/4], m.w/8, 4));
1074 glReadPixels(m.w/4, m.h/2, m.w/8, m.h/2, GL_BGRA, GL_UNSIGNED_BYTE, &m.video[planesize + planesize/4]);
1075 m.format = aviwriter::VID_YUV420;
1076 }
1077 else
1078 {
1079 glBindFramebuffer_(GL_FRAMEBUFFER, scalefb);
1080 glReadPixels(0, 0, m.w, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video);
1081 }
1082 glBindFramebuffer_(GL_FRAMEBUFFER, 0);
1083 glViewport(0, 0, screenw, screenh);
1084
1085 }
1086 else glReadPixels(0, 0, m.w, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video);
1087 }
1088
readbuffer()1089 bool readbuffer()
1090 {
1091 if(!file) return false;
1092 if(state != REC_OK)
1093 {
1094 stop();
1095 return false;
1096 }
1097 SDL_LockMutex(videolock);
1098 if(moviesync && videobuffers.full()) SDL_CondWait(shouldread, videolock);
1099 uint nextframe = (max(gettime() - starttime, 0)*file->videofps)/1000;
1100 if(!videobuffers.full() && (lastframe == ~0U || nextframe > lastframe))
1101 {
1102 videobuffer &m = videobuffers.adding();
1103 SDL_UnlockMutex(videolock);
1104 readbuffer(m, nextframe);
1105 SDL_LockMutex(videolock);
1106 lastframe = nextframe;
1107 videobuffers.add();
1108 SDL_CondSignal(shouldencode);
1109 }
1110 SDL_UnlockMutex(videolock);
1111 return true;
1112 }
1113
drawhud()1114 void drawhud()
1115 {
1116 int w = screenw, h = screenh;
1117 if(forceaspect) w = int(ceil(h*forceaspect));
1118 gettextres(w, h);
1119
1120 hudmatrix.ortho(0, w, h, 0, -1, 1);
1121 hudmatrix.scale(1/3.0f, 1/3.0f, 1);
1122 resethudmatrix();
1123 hudshader->set();
1124
1125 glEnable(GL_BLEND);
1126
1127 double totalsize = file->filespaceguess();
1128 const char *unit = "KB";
1129 if(totalsize >= 1e9) { totalsize /= 1e9; unit = "GB"; }
1130 else if(totalsize >= 1e6) { totalsize /= 1e6; unit = "MB"; }
1131 else totalsize /= 1e3;
1132
1133 draw_textf("recorded %.1f%s %d%%", w*3-10*FONTH, h*3-FONTH-FONTH*3/2, totalsize, unit, int(calcquality()*100));
1134
1135 glDisable(GL_BLEND);
1136 }
1137
capture(bool overlay)1138 void capture(bool overlay)
1139 {
1140 if(readbuffer() && overlay) drawhud();
1141 }
1142 }
1143
1144 VARP(moview, 0, 320, 10000);
1145 VARP(movieh, 0, 240, 10000);
1146 VARP(moviefps, 1, 24, 1000);
1147 VARP(moviesound, 0, 1, 1);
1148
movie(char * name)1149 void movie(char *name)
1150 {
1151 if(name[0] == '\0') recorder::stop();
1152 else if(!recorder::isrecording()) recorder::start(name, moviefps, moview ? moview : screenw, movieh ? movieh : screenh, moviesound!=0);
1153 }
1154
1155 COMMAND(movie, "s");
1156 ICOMMAND(movierecording, "", (), intret(recorder::isrecording() ? 1 : 0));
1157
1158