Hi everyone,
I am writing a class to load a bitmap from a window and save it as a timestamped compreessed image in disk. So far my current approach is slow, because I am saving individual files. Here is a quick benchmark:
Load (Avg) Save (Avg) Sum
0,019 0,049 0,068
This approach gives ~14fps. I would like to have at least 30 fps, but I am really not sure if it is feasible. Any suggestions would be great.
My current approach is using a TBitmap and TJPEGImage in a custom TFPCustomImage descendent:
TBMPtoJPEGConversor = class(TBitmap)
protected
class function GetWriterClass: TFPCustomImageWriterClass; override;
procedure InitializeWriter(AImage: TLazIntfImage; AWriter: TFPCustomImageWriter); override;
end;
implementation
{ TBMPtoJPEGConversor }
class function TBMPtoJPEGConversor.GetWriterClass: TFPCustomImageWriterClass;
begin
Result := TFPWriterJPEG;
end;
procedure TBMPtoJPEGConversor.InitializeWriter(AImage: TLazIntfImage;
AWriter: TFPCustomImageWriter);
var
LFPWriterJPEG : TFPWriterJPEG;
begin
inherited InitializeWriter(AImage, AWriter);
LFPWriterJPEG := TFPWriterJPEG(AWriter);
LFPWriterJPEG.CompressionQuality := 90;
LFPWriterJPEG.GrayScale := False;
LFPWriterJPEG.ProgressiveEncoding := False;
end;
After an initial setup, I am calling it from inside a thread:
procedure TVideoWriter.Execute;
var
LConversor : TBMPtoJPEGConversor;
procedure SaveToFile;
var
LStart : Int64;
LEnd : Int64;
//hFile: THandle;
//dwBytesWritten: DWORD = 0;
function Elapsed : string;
begin
Result := FloatToStrF(
FTimestamp - StartTimestamp, ffFixed, 0, 9)+'.jpeg';
end;
begin
with FFrame do begin
// load from DC
LStart := SDL_GetTicks64;
LConversor.LoadFromDevice(WindowDeviceContextHandle);
LEnd := SDL_GetTicks64;
FTimestamp := GetTimestamp;
Write('LoadFromDevice' + '|' + ((LEnd - LStart)*1e-3).ToStringF);
// JPEG conversion then save to file
LStart := SDL_GetTicks64;
LConversor.SaveToFile(Elapsed);
LEnd := SDL_GetTicks64;
WriteLn('|SaveToFile' + '|' + ((LEnd - LStart)*1e-3).ToStringF);
end;
end;
begin
NameThreadForDebugging(ClassName);
LConversor := TBMPtoJPEGConversor.Create;
try
while not Terminated do begin
SaveToFile;
SDL_Delay(1000); // will adjust later
end;
finally
LConversor.Free;
end;
Log(ClassName+': Terminated ...');
end;
PS.:
Hi everyone,
Here is some more background information about what I am doing. This time I wrote some direct questions.
TLDR
- What is the fastest way you know to make a copy of some part of the screen (like a window) for video recording?
- Would the libpng implementation of TCastleImage be faster for reading from screen and writing to file?
- Do you known any up-to-date av (ffmpeg) bindings for Free Pascal?
- Castle Engine have a wrapper around ffmpeg for calling it with `TProcess` and save some short effects. Do you know if this same approach can be used to save a continuous stream of frames?
- I am sharing my PyAv blueprints for a video recorder in Free Pascal.
- I am sharing some ideas for an image conversor/compressor based on TBitmap from Graphics (LCL).
___
I am trying to write a fast screen recorder for
https://github.com/cpicanco/stimulus-control-sdl2. This program is an experiment builder and I am dealing with eye movements tracking. For my urgent needs, I need to record timestamped frames with something around 30fps. I did some tests with Windows APIs (mainly based on reading a device context (DC), then `BitBlt` and `GetDIBits`). This approach is fast (~30ms) for copying from memory and save to file, but 4mb bitmap files are not feasible, I am expecting 40min-60min recordings. So, I need some fast compression before saving a frame.
I also did some tests with `TBitmap` from `Graphics` unit (LCL) and its dependencies (`FPImage` writers and readers). It was really easy to create a "Conversor" class, for example:
unit GraphicsBMPtoPNG;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Graphics, IntfGraphics, FPImage, FPWritePNG, FPReadBMP;
type
{ TBMPtoPNGConversor }
TBMPtoPNGConversor = class(TBitmap)
protected
class function GetWriterClass: TFPCustomImageWriterClass; override;
procedure InitializeWriter(AImage: TLazIntfImage; AWriter: TFPCustomImageWriter); override;
end;
implementation
uses ZStream;
{ TBMPtoPNGConversor }
class function TBMPtoPNGConversor.GetWriterClass: TFPCustomImageWriterClass;
begin
Result := TFPWriterPNG;
end;
procedure TBMPtoPNGConversor.InitializeWriter(AImage: TLazIntfImage;
AWriter: TFPCustomImageWriter);
var
LFPWriterPNG : TFPWriterPNG;
begin
inherited InitializeWriter(AImage, AWriter);
LFPWriterPNG := AWriter as TFPWriterPNG;
LFPWriterPNG.CompressionLevel := clmax;
LFPWriterPNG.GrayScale := False;
LFPWriterPNG.UseAlpha := False;
end;
end.
So, I thought I could write a reader/writer based on libpng to test if it will improve the speed of my frame by frame recorder.
The frame-by-frame recorder uses a TThread. The TThread.Execute method creates a timestamp, loads the image from screen and save it to a file:
// TVideoWriter = class (TThread)
procedure TVideoWriter.Execute;
var
LConversor : TBMPtoPNGConversor;
LStart : Int64;
LEnd : Int64;
function Elapsed : string;
begin
Result := 'recording'+DirectorySeparator+FloatToStrF(
FFrame.Timestamp - StartTimestamp, ffFixed, 0, 9)+'.png';
end;
begin
NameThreadForDebugging(ClassName);
LConversor := TBMPtoPNGConversor.Create;
try
while not Terminated do begin
FFrame.Timestamp := ET.Elapsed;
LConversor.LoadFromDevice(FFrame.WindowDeviceContext);
LConversor.SaveToFile(Elapsed);
end;
finally
LConversor.Free;
end;
Log(ClassName+': Terminated ...');
end;
After making sure that my frame-by-frame recorder is doing a good work, it would be great to have a proper video recorder writter in Free Pascal. Right now, I have a prototype in PyAv:
import os
import av
import glob
from PIL import Image
from fractions import Fraction
input_dir = os.path.dirname(os.path.abspath(__file__))
image_files = glob.glob(f"{input_dir}/*.png")
image_files_timestamps = [float(os.path.basename(i).replace(',', '.').replace('.png', '')) for i in image_files]
image_files_timestamps_dts = [t - image_files_timestamps[0] for t in image_files_timestamps]
image_files = [(x, t, d) for t, x, d in sorted(zip(image_files_timestamps, image_files, image_files_timestamps_dts))]
video_duration = image_files[-1][1]-image_files[0][1]
fps_p = len(image_files)/video_duration
fps = round(fps_p)
container = av.open("output.mp4", 'w')
video_stream = container.add_stream('libx265', rate=fps)
image = Image.open(os.path.join(input_dir, image_files[0][0]))
video_stream.width = image.width
video_stream.height = image.height
video_stream.codec_context.time_base = Fraction(1, 1000000000)
first_timestamp = None
for (image_file, timestamp, dts) in image_files:
image_path = os.path.join(input_dir, image_file)
frame_pts = timestamp
frame_pts *= 1 + (fps_p - fps) / fps_p
frame = av.VideoFrame.from_image(Image.open(image_file))
frame.time_base = video_stream.codec_context.time_base
frame.pts = int(round(frame_pts / video_stream.codec_context.time_base))
frame.dts = int(round(dts / video_stream.codec_context.time_base))
packet = video_stream.encode(frame)
container.mux(packet)
container.mux(video_stream.encode())
container.close()
However, I am not sure if there are any up-to-date av (ffmpeg) bindings for free pascal (compatible with this prototype).