2
0

GLAVIRecorder.pas 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. //
  2. // This unit is part of the GLScene Engine, http://glscene.org
  3. //
  4. unit GLAVIRecorder;
  5. (* Component to make it easy to record GLScene frames into an AVI file *)
  6. interface
  7. {$I GLScene.inc}
  8. uses
  9. Winapi.Windows,
  10. WinApi.Messages,
  11. System.Classes,
  12. System.SysUtils,
  13. System.UITypes,
  14. VCL.Controls,
  15. VCL.Forms,
  16. VCL.Extctrls,
  17. VCL.Graphics,
  18. VCL.Dialogs,
  19. FileVFW,
  20. GLGraphics,
  21. GLScene,
  22. GLSceneViewer;
  23. type
  24. TAVICompressor = (acDefault, acShowDialog, acDivX);
  25. PAVIStream = ^IAVIStream;
  26. (*Frame size restriction.
  27. Forces frame dimensions to be a multiple of 2, 4, or 8. Some compressors
  28. require this. e.g. DivX 5.2.1 requires mutiples of 2. *)
  29. TAVISizeRestriction = (srNoRestriction, srForceBlock2x2, srForceBlock4x4,
  30. srForceBlock8x8);
  31. TAVIRecorderState = (rsNone, rsRecording);
  32. (* Image retrieval mode for frame capture.
  33. Following modes are supported:
  34. irmSnapShot : retrieve OpenGL framebuffer content using glReadPixels
  35. irmRenderToBitmap : renders the whole scene to a bitmap, this is
  36. the slowest mode, but it won't be affected by driver-side specifics.
  37. irmBitBlt : tranfers the framebuffer using the BitBlt function,
  38. usually the fastest solution *)
  39. TAVIImageRetrievalMode = (irmSnapShot, irmRenderToBitmap, irmBitBlt);
  40. TAVIRecorderPostProcessEvent = procedure(Sender: TObject; frame: TBitmap)
  41. of object;
  42. // Component to make it easy to record GLScene frames into an AVI file.
  43. TGLAVIRecorder = class(TComponent)
  44. private
  45. AVIBitmap: TBitmap;
  46. AVIFrameIndex: integer;
  47. AVI_DPI: integer;
  48. asi: TAVIStreamInfo;
  49. pfile: IAVIFile;
  50. Stream, Stream_c: IAVIStream; // AVI stream and stream to be compressed
  51. FBitmapInfo: PBitmapInfoHeader;
  52. FBitmapBits: Pointer;
  53. FBitmapSize: Dword;
  54. FTempName: String;
  55. // so that we know the filename to delete case of user abort
  56. FAVIFilename: string;
  57. FFPS: byte;
  58. FWidth: integer;
  59. FHeight: integer;
  60. FSizeRestriction: TAVISizeRestriction;
  61. FImageRetrievalMode: TAVIImageRetrievalMode;
  62. RecorderState: TAVIRecorderState;
  63. FOnPostProcessEvent: TAVIRecorderPostProcessEvent;
  64. FBuffer: TGLSceneBuffer;
  65. procedure SetHeight(const val: integer);
  66. procedure SetWidth(const val: integer);
  67. procedure SetSizeRestriction(const val: TAVISizeRestriction);
  68. procedure SetGLSceneViewer(const Value: TGLSceneViewer);
  69. procedure SetGLNonVisualViewer(const Value: TGLNonVisualViewer);
  70. protected
  71. // Maybe we should make a generic TGLAVIRecorder, and then use sub-class of it
  72. FGLSceneViewer: TGLSceneViewer;
  73. // FGLNonVisualViewer accepts GLNonVisualViewer and GLFullScreenViewer
  74. FGLNonVisualViewer: TGLNonVisualViewer;
  75. (* FCompressor determines if the user is to choose a compressor via a dialog box, or
  76. just use a default compressor without showing a dialog box. *)
  77. FCompressor: TAVICompressor;
  78. (* Some video compressor assumes input dimensions to be multiple of 2, 4 or 8.
  79. Restricted() is for rounding off the width and height.
  80. Currently I can't find a simple way to know which compressor imposes
  81. what resiction, so the SizeRestiction property is there for the user to set.
  82. The source code of VirtualDub (http://www.virtualdub.org/)
  83. may give us some cues on this.
  84. ( BTW, VirtualDub is an excellent freeware for editing your AVI. For
  85. converting AVI into MPG, try AVI2MPG1 - http://www.mnsi.net/~jschlic1 *)
  86. function Restricted(s: integer): integer;
  87. procedure InternalAddAVIFrame;
  88. public
  89. constructor Create(AOwner: TComponent); override;
  90. destructor Destroy; override;
  91. function CreateAVIFile(DPI: integer = 0): boolean;
  92. procedure AddAVIFrame; overload;
  93. procedure AddAVIFrame(bmp: TBitmap); overload;
  94. procedure CloseAVIFile(UserAbort: boolean = false);
  95. function Recording: boolean;
  96. published
  97. property FPS: byte read FFPS write FFPS default 25;
  98. property GLSceneViewer: TGLSceneViewer read FGLSceneViewer
  99. write SetGLSceneViewer;
  100. property GLNonVisualViewer: TGLNonVisualViewer read FGLNonVisualViewer
  101. write SetGLNonVisualViewer;
  102. property Width: integer read FWidth write SetWidth;
  103. property Height: integer read FHeight write SetHeight;
  104. property Filename: String read FAVIFilename write FAVIFilename;
  105. property Compressor: TAVICompressor read FCompressor write FCompressor
  106. default acDefault;
  107. property SizeRestriction: TAVISizeRestriction read FSizeRestriction
  108. write SetSizeRestriction default srForceBlock8x8;
  109. property ImageRetrievalMode: TAVIImageRetrievalMode read FImageRetrievalMode
  110. write FImageRetrievalMode default irmBitBlt;
  111. property OnPostProcessEvent: TAVIRecorderPostProcessEvent
  112. read FOnPostProcessEvent write FOnPostProcessEvent;
  113. end;
  114. // ---------------------------------------------------------------------
  115. implementation
  116. // ---------------------------------------------------------------------
  117. // DIB support rountines for AVI output
  118. procedure InitializeBitmapInfoHeader(Bitmap: HBITMAP;
  119. var BI: TBitmapInfoHeader);
  120. var
  121. BM: Winapi.Windows.TBitmap;
  122. begin
  123. GetObject(Bitmap, SizeOf(BM), @BM);
  124. with BI do
  125. begin
  126. biSize := SizeOf(BI);
  127. biWidth := BM.bmWidth;
  128. biHeight := BM.bmHeight;
  129. biPlanes := 1;
  130. biXPelsPerMeter := 0;
  131. biYPelsPerMeter := 0;
  132. biClrUsed := 0;
  133. biClrImportant := 0;
  134. biCompression := BI_RGB;
  135. biBitCount := 24;
  136. // force 24 bits. Most video compressors would deal with 24-bit frames only.
  137. biSizeImage := (((biWidth * biBitCount) + 31) div 32) * 4 * biHeight;
  138. end;
  139. end;
  140. procedure InternalGetDIBSizes(Bitmap: HBITMAP; var InfoHeaderSize: integer;
  141. var ImageSize: Dword);
  142. var
  143. BI: TBitmapInfoHeader;
  144. begin
  145. InitializeBitmapInfoHeader(Bitmap, BI);
  146. InfoHeaderSize := SizeOf(TBitmapInfoHeader);
  147. ImageSize := BI.biSizeImage;
  148. end;
  149. function InternalGetDIB(Bitmap: HBITMAP; var bitmapInfo; var bits): boolean;
  150. var
  151. focus: HWND;
  152. dc: HDC;
  153. errCode: integer;
  154. begin
  155. InitializeBitmapInfoHeader(Bitmap, TBitmapInfoHeader(bitmapInfo));
  156. focus := GetFocus;
  157. dc := GetDC(focus);
  158. try
  159. errCode := GetDIBits(dc, Bitmap, 0, TBitmapInfoHeader(bitmapInfo).biHeight,
  160. @bits, TBitmapInfo(bitmapInfo), DIB_RGB_COLORS);
  161. Result := (errCode <> 0);
  162. finally
  163. ReleaseDC(focus, dc);
  164. end;
  165. end;
  166. // ------------------
  167. // ------------------ TAVIRecorder ------------------
  168. // ------------------
  169. constructor TGLAVIRecorder.Create(AOwner: TComponent);
  170. begin
  171. inherited;
  172. FWidth := 320; // default values
  173. FHeight := 200;
  174. FFPS := 25;
  175. FCompressor := acDefault;
  176. RecorderState := rsNone;
  177. FSizeRestriction := srForceBlock8x8;
  178. FImageRetrievalMode := irmBitBlt;
  179. end;
  180. destructor TGLAVIRecorder.Destroy;
  181. begin
  182. // if still open here, abort it
  183. if RecorderState = rsRecording then
  184. CloseAVIFile(True);
  185. inherited;
  186. end;
  187. function TGLAVIRecorder.Restricted(s: integer): integer;
  188. begin
  189. case FSizeRestriction of
  190. srForceBlock2x2:
  191. Result := (s div 2) * 2;
  192. srForceBlock4x4:
  193. Result := (s div 4) * 4;
  194. srForceBlock8x8:
  195. Result := (s div 8) * 8;
  196. else
  197. Result := s;
  198. end;
  199. end;
  200. procedure TGLAVIRecorder.SetHeight(const val: integer);
  201. begin
  202. if (RecorderState <> rsRecording) and (val <> FHeight) and (val > 0) then
  203. FHeight := Restricted(val);
  204. end;
  205. procedure TGLAVIRecorder.SetWidth(const val: integer);
  206. begin
  207. if (RecorderState <> rsRecording) and (val <> FWidth) and (val > 0) then
  208. FWidth := Restricted(val);
  209. end;
  210. procedure TGLAVIRecorder.SetSizeRestriction(const val: TAVISizeRestriction);
  211. begin
  212. if val <> FSizeRestriction then
  213. begin
  214. FSizeRestriction := val;
  215. FHeight := Restricted(FHeight);
  216. FWidth := Restricted(FWidth);
  217. end;
  218. end;
  219. procedure TGLAVIRecorder.AddAVIFrame;
  220. var
  221. bmp32: TGLBitmap32;
  222. bmp: TBitmap;
  223. begin
  224. if RecorderState <> rsRecording then
  225. raise Exception.Create('Cannot add frame to AVI. AVI file not created.');
  226. if FBuffer <> nil then
  227. case ImageRetrievalMode of
  228. irmSnapShot:
  229. begin
  230. bmp32 := FBuffer.CreateSnapShot;
  231. try
  232. bmp := bmp32.Create32BitsBitmap;
  233. try
  234. AVIBitmap.Canvas.Draw(0, 0, bmp);
  235. finally
  236. bmp.Free;
  237. end;
  238. finally
  239. bmp32.Free;
  240. end;
  241. end;
  242. irmBitBlt:
  243. begin
  244. FBuffer.RenderingContext.Activate;
  245. try
  246. BitBlt(AVIBitmap.Canvas.Handle, 0, 0, AVIBitmap.Width,
  247. AVIBitmap.Height, wglGetCurrentDC, 0, 0, SRCCOPY);
  248. finally
  249. FBuffer.RenderingContext.Deactivate;
  250. end;
  251. end;
  252. irmRenderToBitmap:
  253. begin
  254. FBuffer.RenderToBitmap(AVIBitmap, AVI_DPI);
  255. end;
  256. else
  257. Assert(false);
  258. end;
  259. InternalAddAVIFrame;
  260. end;
  261. procedure TGLAVIRecorder.AddAVIFrame(bmp: TBitmap);
  262. begin
  263. if RecorderState <> rsRecording then
  264. raise Exception.Create('Cannot add frame to AVI. AVI file not created.');
  265. AVIBitmap.Canvas.Draw(0, 0, bmp);
  266. InternalAddAVIFrame;
  267. end;
  268. procedure TGLAVIRecorder.InternalAddAVIFrame;
  269. begin
  270. if Assigned(FOnPostProcessEvent) then
  271. FOnPostProcessEvent(Self, AVIBitmap);
  272. with AVIBitmap do
  273. begin
  274. InternalGetDIB(Handle, FBitmapInfo^, FBitmapBits^);
  275. if AVIStreamWrite(Stream_c, AVIFrameIndex, 1, FBitmapBits, FBitmapSize,
  276. AVIIF_KEYFRAME, nil, nil) <> AVIERR_OK then
  277. raise Exception.Create('Add Frame Error');
  278. Inc(AVIFrameIndex);
  279. end;
  280. end;
  281. function TGLAVIRecorder.CreateAVIFile(DPI: integer = 0): boolean;
  282. var
  283. SaveDialog: TSaveDialog;
  284. gaAVIOptions: TAVICOMPRESSOPTIONS;
  285. galpAVIOptions: PAVICOMPRESSOPTIONS;
  286. bitmapInfoSize: integer;
  287. AVIResult: Cardinal;
  288. ResultString: String;
  289. begin
  290. FTempName := FAVIFilename;
  291. if FTempName = '' then
  292. begin
  293. // if user didn't supply a filename, then ask for it
  294. SaveDialog := TSaveDialog.Create(Application);
  295. try
  296. with SaveDialog do
  297. begin
  298. Options := [ofHideReadOnly, ofNoReadOnlyReturn];
  299. DefaultExt := '.avi';
  300. Filter := 'AVI Files (*.avi)|*.avi';
  301. if Execute then
  302. FTempName := SaveDialog.Filename;
  303. end;
  304. finally
  305. SaveDialog.Free;
  306. end;
  307. end;
  308. Result := (FTempName <> '');
  309. if Result then
  310. begin
  311. if FileExists(FTempName) then
  312. begin
  313. Result := (MessageDlg(Format('Overwrite file %s?', [FTempName]),
  314. mtConfirmation, [mbYes, mbNo], 0) = mrYes);
  315. // AVI streamers don't trim the file they're written to, so start from zero
  316. if Result then
  317. DeleteFile(FTempName);
  318. end;
  319. end;
  320. if not Result then
  321. Exit;
  322. AVIFileInit; // initialize the AVI lib.
  323. AVIBitmap := TBitmap.Create;
  324. AVIFrameIndex := 0;
  325. RecorderState := rsRecording;
  326. try
  327. AVIBitmap.PixelFormat := pf24Bit;
  328. AVIBitmap.Width := FWidth;
  329. AVIBitmap.Height := FHeight;
  330. // note: a filename with extension other then AVI give generate an error.
  331. if AVIFileOpen(pfile, PChar(FTempName), OF_WRITE or OF_CREATE, nil) <> AVIERR_OK
  332. then
  333. raise Exception.Create
  334. ('Cannot create AVI file. Disk full or file in use?');
  335. with AVIBitmap do
  336. begin
  337. InternalGetDIBSizes(Handle, bitmapInfoSize, FBitmapSize);
  338. FBitmapInfo := AllocMem(bitmapInfoSize);
  339. FBitmapBits := AllocMem(FBitmapSize);
  340. InternalGetDIB(Handle, FBitmapInfo^, FBitmapBits^);
  341. end;
  342. FillChar(asi, SizeOf(asi), 0);
  343. with asi do
  344. begin
  345. fccType := streamtypeVIDEO; // Now prepare the stream
  346. fccHandler := 0;
  347. dwScale := 1; // dwRate / dwScale = frames/second
  348. dwRate := FFPS;
  349. dwSuggestedBufferSize := FBitmapSize;
  350. rcFrame.Right := FBitmapInfo.biWidth;
  351. rcFrame.Bottom := FBitmapInfo.biHeight;
  352. end;
  353. if AVIFileCreateStream(pfile, Stream, asi) <> AVIERR_OK then
  354. raise Exception.Create('Cannot create AVI stream.');
  355. with AVIBitmap do
  356. InternalGetDIB(Handle, FBitmapInfo^, FBitmapBits^);
  357. galpAVIOptions := @gaAVIOptions;
  358. FillChar(gaAVIOptions, SizeOf(gaAVIOptions), 0);
  359. gaAVIOptions.fccType := streamtypeVIDEO;
  360. case FCompressor of
  361. acShowDialog:
  362. begin
  363. // call a dialog box for the user to choose the compressor options
  364. AVISaveOptions(0, ICMF_CHOOSE_KEYFRAME or ICMF_CHOOSE_DATARATE, 1,
  365. Stream, galpAVIOptions);
  366. end;
  367. acDivX:
  368. with gaAVIOptions do
  369. begin
  370. // ask for generic divx, using current default settings
  371. fccHandler := mmioFOURCC('d', 'i', 'v', 'x');
  372. end;
  373. else
  374. with gaAVIOptions do
  375. begin // or, you may want to fill the compression options yourself
  376. fccHandler := mmioFOURCC('M', 'S', 'V', 'C');
  377. // User MS video 1 as default.
  378. // I guess it is installed on every Win95 or later.
  379. dwQuality := 7500; // compress quality 0-10,000
  380. dwFlags := 0;
  381. // setting dwFlags to 0 would lead to some default settings
  382. end;
  383. end;
  384. AVIResult := AVIMakeCompressedStream(Stream_c, Stream, galpAVIOptions, nil);
  385. if AVIResult <> AVIERR_OK then
  386. begin
  387. if AVIResult = AVIERR_NOCOMPRESSOR then
  388. ResultString := 'No such compressor found'
  389. else
  390. ResultString := '';
  391. raise Exception.Create('Cannot make compressed stream. ' + ResultString);
  392. end;
  393. if AVIStreamSetFormat(Stream_c, 0, FBitmapInfo, bitmapInfoSize) <> AVIERR_OK
  394. then
  395. raise Exception.Create('AVIStreamSetFormat Error');
  396. // no error description found in MSDN.
  397. AVI_DPI := DPI;
  398. except
  399. CloseAVIFile(True);
  400. raise;
  401. end;
  402. end;
  403. procedure TGLAVIRecorder.CloseAVIFile(UserAbort: boolean = false);
  404. begin
  405. // if UserAbort, CloseAVIFile will also delete the unfinished file.
  406. try
  407. if RecorderState <> rsRecording then
  408. raise Exception.Create('Cannot close AVI file. AVI file not created.');
  409. AVIBitmap.Free;
  410. FreeMem(FBitmapInfo);
  411. FreeMem(FBitmapBits);
  412. AVIFileExit; // finalize the AVI lib.
  413. // release the interfaces explicitly (can't rely on automatic release)
  414. Stream := nil;
  415. Stream_c := nil;
  416. pfile := nil;
  417. if UserAbort then
  418. DeleteFile(FTempName);
  419. finally
  420. RecorderState := rsNone;
  421. end;
  422. end;
  423. function TGLAVIRecorder.Recording: boolean;
  424. begin
  425. Result := (RecorderState = rsRecording);
  426. end;
  427. procedure TGLAVIRecorder.SetGLSceneViewer(const Value: TGLSceneViewer);
  428. begin
  429. FGLSceneViewer := Value;
  430. if Assigned(FGLSceneViewer) then
  431. FBuffer := FGLSceneViewer.Buffer
  432. else
  433. FBuffer := nil;
  434. end;
  435. procedure TGLAVIRecorder.SetGLNonVisualViewer(const Value: TGLNonVisualViewer);
  436. begin
  437. FGLNonVisualViewer := Value;
  438. if Assigned(FGLNonVisualViewer) then
  439. FBuffer := FGLNonVisualViewer.Buffer
  440. else
  441. FBuffer := nil;
  442. end;
  443. end.