GLS.AVIRecorder.pas 14 KB

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