GXS.AVIRecorder.pas 15 KB

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