Download Delphi FFmpeg VCL Components

Source of the Video Converter Demo Main Form

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, ExtCtrls, Menus, FFmpeg, FFmpegVCL, FFmpegLogger;

type
  TfrmMain = class(TForm)
    btnAdd: TButton;
    btnRemove: TButton;
    btnClear: TButton;
    btnStart: TButton;
    btnStop: TButton;
    btnPause: TButton;
    btnResume: TButton;
    btnExit: TButton;
    btnWebSite: TButton;
    lvFiles: TListView;
    mmoLog: TMemo;
    grpOption: TGroupBox;
    Label1: TLabel;
    chkThreadMode: TCheckBox;
    edtThreadCount: TEdit;
    chkPreview: TCheckBox;
    grpLogLevel: TRadioGroup;
    grpPriority: TRadioGroup;
    FFVCL: TFFmpegVCL;
    OpenDialog1: TOpenDialog;
    PopupMenu1: TPopupMenu;
    mnuAdd: TMenuItem;
    mnuRemove: TMenuItem;
    mnuClear: TMenuItem;
    N1: TMenuItem;
    mnuPlay: TMenuItem;
    mnuOpenFolder: TMenuItem;
    FFLogger: TFFLogger;
    procedure FormCreate(Sender: TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure FormDestroy(Sender: TObject);
    procedure btnAddClick(Sender: TObject);
    procedure btnRemoveClick(Sender: TObject);
    procedure btnClearClick(Sender: TObject);
    procedure btnStartClick(Sender: TObject);
    procedure btnStopClick(Sender: TObject);
    procedure btnPauseClick(Sender: TObject);
    procedure btnResumeClick(Sender: TObject);
    procedure btnExitClick(Sender: TObject);
    procedure btnWebSiteClick(Sender: TObject);
    procedure chkPreviewClick(Sender: TObject);
    procedure PopupMenu1Popup(Sender: TObject);
    procedure mnuPlayClick(Sender: TObject);
    procedure mnuOpenFolderClick(Sender: TObject);
    procedure FFVCLCustomHook(Sender: TObject; AHookInfo: PHookInfo);
    procedure FFVCLProgress(Sender: TObject; AProgressInfo: PProgressInfo);
    procedure FFVCLTerminate(Sender: TObject; const ATerminateInfo: TTerminateInfo);
    procedure grpLogLevelClick(Sender: TObject);
    procedure FFLoggerLog(Sender: TObject; AThreadID: Cardinal;
      ALogLevel: TLogLevel; const ALogMsg: string);
  private
    FFolderList: TStringList;
    FOutFileList: TStringList;
    procedure DoAddFile(const AIO: TInputOptions; const AOO: TOutputOptions);
  public
  end;

var
  frmMain: TfrmMain;

implementation

{$R *.dfm}

uses
{$IF NOT (DEFINED(VER140) OR DEFINED(VER185) OR DEFINED(VER200) OR DEFINED(VER210))}
  XPMan,
{$IFEND}
  ShellAPI,
  OptionFrm,
  MyUtils;

var
  GfrmOption: TfrmOption = nil;

const
  CLibAVPath = 'LibAV';

  SAppTitle = 'Demo of FFVCL %s';
  SCaption = 'Demo of FFVCL %s - Delphi FFmpeg VCL Components';
  SWebSiteC = 'http://www.CCAVC.com';
  SWebSiteE = 'http://www.DelphiFFmpeg.com';
  DEMO_INFO =
    '* The Demo Version FFVCL only works in Delphi IDE' +
      #13#10 +
    '* Thread Mode means creating new threads to do converting tasks, ' +
      'it dose not using the main thread so you can Stop/Pause/Resume ' +
      'converting without block.' +
      #13#10 +
    '  * Thread Count means the quantity of converting jobs in the same time.' +
      #13#10 +
    '  * WARNING: if you want to debug your code in IDE such as Breakpoints, ' +
      'please do not check Thread Mode, or choose Thread Priority below "Normal", ' +
      'because Delphi Debugger maybe fall into trouble in Thread Mode.' +
      #13#10 +
    '* Log Level means the log messages level' +
      #13#10 +
    '* Thread Priority only works in Thread Mode' +
      #13#10 +
      #13#10;

  // License sample (this is a fake license key)
  // The full source version doesn't need license key.
//BOMB: you should replace the seed and the key with your own ones.
  LICENSE_SEED = $D24E9E33;
  LICENSE_KEY =
    '39EA968465F26B6CDCA1E51EC8FAC6392100A838813BD0E0F5575E31AC38AEE4' +
    '34E3AF85FDC4B84FBC8BC88078E83D482D8CD226F013CF85BA90F88765D91977' +
    'A8C2E0E345B052AFC342FF244A8D7A95306623716DDBB1B512A1F44F32D6731C' +
    '49308768679B36FC8F6AEF3207ED6CC8EA5EF8F4D2F2AA0F2DC5F13654B78322';

  CDialogOptions = [ofHideReadOnly, ofFileMustExist, ofEnableSizing];
  CPictureFiles = '*.BMP;*.GIF;*.JPEG;*.JPG;*.PNG;';
  CAudioFiles = '*.MP3;*.AAC;*.WAV;*.WMA;*.CDA;*.FLAC;*.M4A;*.MID;*.MKA;' +
      '*.MP2;*.MPA;*.MPC;*.APE;*.OFR;*.OGG;*.RA;*.WV;*.TTA;*.AC3;*.DTS;';
  CVideoFiles = '*.AVI;*.AVM;*.ASF;*.WMV;*.AVS;*.FLV;*.MKV;*.MOV;*.3GP;' +
      '*.MP4;*.MPG;*.MPEG;*.DAT;*.OGM;*.VOB;*.RM;*.RMVB;*.TS;*.TP;*.IFO;*.NSV;';
  CDialogFilter =
      'Video/Audio/Picture Files|' + CVideoFiles + CAudioFiles + CPictureFiles +
      '|Video Files|' + CVideoFiles +
      '|Audio Files|' + CAudioFiles +
      '|Picture Files|' + CPictureFiles +
      '|All Files|*.*';

  SHookFrameNumber = 'FFVCL - Frame Number: %d';
  SHookTimeStamp = 'FFVCL - Time Stamp: %d';
  SHookTextImage = 'FFVCL';
  SHookReverseVertical = 'Reverse Picture Vertically';
  SHookReverseHorizontal = 'Reverse Picture Horizontally';

var
  SWebSite: string = SWebSiteE;

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  // initialize
  Application.Title := Format(SAppTitle, [FFVCL.Version]);
  Self.Caption := Format(SCaption, [FFVCL.Version]);

  if SysUtils.SysLocale.PriLangID = LANG_CHINESE then
    SWebSite := SWebSiteC
  else
    SWebSite := SWebSiteE;
  btnWebSite.Caption := SWebSite;

  mmoLog.Text := DEMO_INFO;

  FFolderList := TStringList.Create;
  FOutFileList := TStringList.Create;

  // open dialog setting
  OpenDialog1.Options := CDialogOptions;
  OpenDialog1.Filter := CDialogFilter;

  // Set License Key
  // The full source version doesn't need license key.
  FFVCL.SetLicenseKey(LICENSE_KEY, LICENSE_SEED);
end;

procedure TfrmMain.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  with FFVCL do
  begin
    // Clear the event handlers
    OnCustomHook := nil;
    OnProgress := nil;
    OnTerminate := nil;

    // Break converting
    Stop;
  end;
  FFLogger.OnLog := nil;
end;

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  FreeAndNil(FFolderList);
  FreeAndNil(FOutFileList);
end;

procedure TfrmMain.chkPreviewClick(Sender: TObject);
begin
  FFVCL.Preview := chkPreview.Checked;
end;

procedure TfrmMain.btnAddClick(Sender: TObject);
begin
  // Load dynamic link libraries
  if not FFVCL.AVLibLoaded then
  begin
    // TPathFileName = type WideString;
    // FFVCL.LoadAVLib(const APath: TPathFileName): Boolean;
    // APath: Full path indicates location of FFmpeg DLLs.
    //        It can be empty, let Windows search DLLs in current dir or environment <PATH>
    //if not FFVCL.LoadAVLib(ExtractFilePath(Application.ExeName) + CLibAVPath) then
    // the routine ExePath() is implemented in unit MyUtils which returns WideString type
    // of ExtractFilePath(Application.ExeName)
    if not FFVCL.LoadAVLib(ExePath + CLibAVPath) then
    begin
      mmoLog.Lines.Add(FFVCL.LastErrMsg);
      Exit;
    end;
  end;

  // open media file
  if not OpenDialog1.Execute then
    // cancel open file
    Exit;

  // now, try to load the input file selected to get AVFileInfo,
  // and then generate input options and output options for conversion

  // FFVCL.AVProbe is an instance of TAVProbe class defined in unit AVProbe.pas
  // you can create your own AVProbe instance according your purpose
  if not FFVCL.AVProbe.LoadFile(OpenDialog1.FileName) then
  begin // load av file failed
    mmoLog.Lines.Add('');
    mmoLog.Lines.Add('***File load error: ' + FFVCL.AVProbe.LastErrMsg);
    mmoLog.Lines.Add('');
    Exit;
  end;

  try
    if not Assigned(GfrmOption) then
      // need to create option form
      GfrmOption := TfrmOption.Create(Self);

    // init option form with AVProbe
    GfrmOption.AVProbe := FFVCL.AVProbe;

    if GfrmOption.ShowModal = mrOk then
      // do add file to conversion list
      DoAddFile(GfrmOption.InputOptions, GfrmOption.OutputOptions);
  finally
    // close the file handle in AVProbe
    FFVCL.AVProbe.CloseFile;
  end;
end;

procedure TfrmMain.DoAddFile(const AIO: TInputOptions; const AOO: TOutputOptions);
var
  LIndex: Integer;
begin
  // try to open input file with input options
  LIndex := FFVCL.AddTask(AIO.FileName, @AIO);
  if LIndex < 0 then
  begin // open failed
    mmoLog.Lines.Add('');
    mmoLog.Lines.Add('***File open error: ' + FFVCL.LastErrMsg);
    mmoLog.Lines.Add('');
    Exit;
  end;

  // try to set output file with output options
  if not FFVCL.SetOutputFile(LIndex, AOO.FileName, @AOO) then
  begin // cannot do output file, do not forget to remove input file!!!
    FFVCL.RemoveTask(LIndex);
    mmoLog.Lines.Add('');
    mmoLog.Lines.Add('***Cannot do convert, error: ' + FFVCL.LastErrMsg);
    mmoLog.Lines.Add('');
    Exit;
  end;

  with lvFiles.Items.Add do
  begin
    // display some information of input file and output file
    Caption := ExtractFileName(AIO.FileName);
    // microseconds to seconds
    if FFVCL.AVProbe.FileStreamInfo.Duration > 0 then
      SubItems.Add(IntToStr(FFVCL.AVProbe.FileStreamInfo.Duration div 1000000))
    else
      SubItems.Add('N/A');
    SubItems.Add(IntToStr(FFVCL.AVProbe.FileSize));
    SubItems.Add(ExtractFileName(AOO.FileName));
    SubItems.Add('');
    SubItems.Add('');
    SubItems.Add('');
    SubItems.Add('');
  end;

  mmoLog.Lines.Add('');
  mmoLog.Lines.Add('***File has been added to convert list.');
  mmoLog.Lines.Add('');

  // set status of buttons
  btnRemove.Enabled := True;
  btnClear.Enabled := True;
  btnStart.Enabled := True;

  // add output file path to folder list
  FFolderList.Add(ExtractFilePath(AOO.FileName));
  FOutFileList.AddObject(AOO.FileName, nil);
end;

procedure TfrmMain.btnRemoveClick(Sender: TObject);
begin
  if lvFiles.ItemIndex >= 0 then
  begin
    // remove input file
    FFVCL.RemoveTask(lvFiles.ItemIndex);

    FFolderList.Delete(lvFiles.ItemIndex);
    FOutFileList.Delete(lvFiles.ItemIndex);

    lvFiles.Items.Delete(lvFiles.ItemIndex);

    // set status of buttons
    btnRemove.Enabled := lvFiles.Items.Count > 0;
    btnClear.Enabled := lvFiles.Items.Count > 0;
    btnStart.Enabled := lvFiles.Items.Count > 0;
  end;
end;

procedure TfrmMain.btnClearClick(Sender: TObject);
begin
  // clear all input files
  FFVCL.ClearTasks;

  FFolderList.Clear;
  FOutFileList.Clear;

  lvFiles.Items.Clear;

  // set status of buttons
  btnAdd.Enabled := True;
  btnRemove.Enabled := False;
  btnClear.Enabled := False;
  btnStart.Enabled := False;
end;

procedure TfrmMain.btnStartClick(Sender: TObject);
begin
  FFVCL.ThreadPriority := TThreadPriority(grpPriority.ItemIndex);

  // set status of buttons
  btnAdd.Enabled := False;
  btnRemove.Enabled := False;
  btnClear.Enabled := False;

  btnStart.Enabled := False;
  btnStop.Enabled := chkThreadMode.Checked;
  btnPause.Enabled := chkThreadMode.Checked;
  btnResume.Enabled := False;

  // procedure Start(AThreadCount: Integer);
  // AThreadCount: >  0, means do converting task in thread mode
  //               >  1, means do converting task with multiple files in the same time
  //               <= 0, means do converting task in main thread
  if chkThreadMode.Checked then
    // do converting task in thread mode
    FFVCL.Start(StrToIntDef(edtThreadCount.Text, 1))
  else
    // do converting task in main thread
    FFVCL.Start(0);
end;

procedure TfrmMain.btnStopClick(Sender: TObject);
begin
  btnStop.Enabled := False;
  FFVCL.Stop; // only works in thread mode
end;

procedure TfrmMain.btnPauseClick(Sender: TObject);
begin
  btnPause.Enabled := False;
  btnResume.Enabled := True;
  FFVCL.Pause; // only works in thread mode
end;

procedure TfrmMain.btnResumeClick(Sender: TObject);
begin
  btnPause.Enabled := True;
  btnResume.Enabled := False;
  FFVCL.Resume; // only works in thread mode
end;

procedure TfrmMain.btnWebSiteClick(Sender: TObject);
begin
  ShellExecute(Application.Handle, 'Open',   {Do not Localize}
    PChar(LowerCase(SWebSite)), '',
    PChar(ExtractFilePath(Application.ExeName)), 1);
end;

procedure TfrmMain.btnExitClick(Sender: TObject);
begin
  Close;
end;

procedure TfrmMain.PopupMenu1Popup(Sender: TObject);
var
  I: Integer;
begin
  mnuAdd.Enabled := btnAdd.Enabled;
  mnuRemove.Enabled := btnRemove.Enabled;
  mnuClear.Enabled := btnClear.Enabled;
  mnuOpenFolder.Enabled := lvFiles.Items.Count > 0;

  if (lvFiles.ItemIndex >= 0) and (lvFiles.ItemIndex < FFolderList.Count) then
    I := lvFiles.ItemIndex
  else
    I := FOutFileList.Count - 1;

  mnuPlay.Enabled := (I >= 0) and Assigned(FOutFileList.Objects[I]);
end;

procedure TfrmMain.mnuPlayClick(Sender: TObject);
var
  I: Integer;
begin
  if (lvFiles.ItemIndex >= 0) and (lvFiles.ItemIndex < FFolderList.Count) then
    I := lvFiles.ItemIndex
  else
    I := FOutFileList.Count - 1;

  if (I >= 0) and Assigned(FOutFileList.Objects[I]) and FileExists(FOutFileList.Strings[I]) then
    ShellExecute(Application.Handle, 'Open',   {Do not Localize}
      PChar(FOutFileList.Strings[I]), '',
      PChar(ExtractFilePath(FOutFileList.Strings[I])), SW_SHOWNORMAL);
end;

procedure TfrmMain.mnuOpenFolderClick(Sender: TObject);
var
  LPath: string;
begin
  if (lvFiles.ItemIndex >= 0) and (lvFiles.ItemIndex < FFolderList.Count) then
    LPath := FFolderList.Strings[lvFiles.ItemIndex]
  else if FFolderList.Count > 0 then
    LPath := FFolderList.Strings[FFolderList.Count - 1]
  else
    LPath := '';

  if (LPath <> '') and DirectoryExists(LPath) then
    ShellExecute(Application.Handle, 'Open', // {Do not Localize}
      PChar(LPath), nil,
      PChar(LPath), SW_SHOWDEFAULT);
end;

procedure TfrmMain.FFVCLCustomHook(Sender: TObject; AHookInfo: PHookInfo);
const
  CBytes = 3; // ABitmap.PiexlFormat is pf24bit
var
  H, W: Integer;
  I, J, K: Integer;
  P1, P2: PAnsiChar;
  B: AnsiChar;
begin
  // OnCustomHook event handler, only triggered with OutputOptions.CustomHook = True
{
  PHookInfo = ^THookInfo;
  THookInfo = record
    TaskIndex: Integer;     // index of converting tasks
    Bitmap: TBitmap;        // bitmap filled with the original video picture, you can save it,
                            //  or change it by drawing text or image on bitmap.Canvas,
                            //  but you must NOT change the size and the PixelFormat of the bitmap!
    FrameNumber: Integer;   // frame index number, first is 1 not 0
    PTS: Int64;             // presentation time stamp of current picture, in microseconds
    Update: Boolean;        // whether update the bitmap back to original video picture, default True
    StopHook: Boolean;      // whether stop video hook, if true then CustomHook event will not
                            //  be triggered with this file, default False
  end;
}
  if AHookInfo.FrameNumber = 1 then
  begin // first frame number
    with AHookInfo.Bitmap.Canvas.Font do
    begin // setup font example
      Color := clWhite;
      Name := 'Tahoma';
      Size := 12;
      Style := [fsBold, fsUnderline];
    end;
    // capture video frame example
    //AHookInfo.Bitmap.SaveToFile('C:\CustomHook_' + FloatToStr(Now) + '.bmp');
  end;

  if AHookInfo.FrameNumber < 100 then
    // text out with frame number example
    AHookInfo.Bitmap.Canvas.TextOut(10, 10, Format(SHookFrameNumber, [AHookInfo.FrameNumber]))
  else if AHookInfo.FrameNumber < 150 then
    // do not change the original video picture example
    AHookInfo.Update := False
  else if AHookInfo.FrameNumber < 250 then
    // text out with Presentation Time Stamp example
    AHookInfo.Bitmap.Canvas.TextOut(10, 10, Format(SHookTimeStamp, [AHookInfo.PTS]))
  else if AHookInfo.FrameNumber < 350 then
  begin
    // draw text and graphic example
    if AHookInfo.FrameNumber = 250 then
      with AHookInfo.Bitmap.Canvas.Font do
      begin // change font example
        Color := clRed;
        Size := 16;
      end;
    AHookInfo.Bitmap.Canvas.TextOut(10, 10, SHookTextImage);
    AHookInfo.Bitmap.Canvas.Draw(AHookInfo.Bitmap.Width - Application.Icon.Width - 10,
                        AHookInfo.Bitmap.Height - Application.Icon.Height - 10,
                        Application.Icon);
  end
  else if AHookInfo.FrameNumber < 450 then
  begin
    if AHookInfo.FrameNumber = 350 then
      with AHookInfo.Bitmap.Canvas.Font do
      begin // change font example
        Color := clWhite;
        Style := [fsBold];
      end;
    AHookInfo.Bitmap.Canvas.TextOut(10, 10, SHookReverseVertical);
    // reverse picture vertically example
    H := AHookInfo.Bitmap.Height;
    W := AHookInfo.Bitmap.Width;
    for I := 0 to H div 2 - 1 do
    begin
      P1 := PAnsiChar(AHookInfo.Bitmap.ScanLine[I]);
      P2 := PAnsiChar(AHookInfo.Bitmap.ScanLine[H - 1 - I]);
      for J := 0 to CBytes * W - 1 do
      begin
        B := P1^;
        P1^ := P2^;
        P2^ := B;
        Inc(P1);
        Inc(P2);
      end;
    end;
  end
  else if AHookInfo.FrameNumber < 550 then
  begin
    if AHookInfo.FrameNumber = 350 then
      with AHookInfo.Bitmap.Canvas.Font do
      begin // change font example
        Style := [];
      end;
    AHookInfo.Bitmap.Canvas.TextOut(10, 10, SHookReverseHorizontal);
    // reverse picture horizontally example
    H := AHookInfo.Bitmap.Height;
    W := AHookInfo.Bitmap.Width;
    for I := 0 to H - 1 do
    begin
      P1 := PAnsiChar(AHookInfo.Bitmap.ScanLine[I]);
      P2 := P1 + CBytes * (W - 1);
      for J := 0 to W div 2 - 1 do
      begin
        for K := 0 to CBytes - 1 do
        begin
          B := (P1 + K)^;
          (P1 + K)^ := (P2 + K)^;
          (P2 + K)^ := B;
        end;
        Inc(P1, CBytes);
        Dec(P2, CBytes);
      end;
    end;
  end
  else
    // stop video hook exmaple, the converting will speedup
    AHookInfo.StopHook := True;
end;

procedure TfrmMain.FFVCLProgress(Sender: TObject; AProgressInfo: PProgressInfo);
begin
  // OnProgress event handler
{
  PProgressInfo = ^TProgressInfo;
  TProgressInfo = record
    TaskIndex: Integer;     // index of converting tasks
    FileIndex: Integer;     // index of input files in the current task
    FrameNumber: Integer;   // current frame number
    FPS: Integer;           // video converting speed, frames per second, not valid when only audio output
    Quality: Single;        // quality
    BitRate: Single;        // bitrate
    CurrentSize: Int64;     // current output file size in bytes
    CurrentDuration: Int64; // current duration time in microsecond
    TotalDuration: Int64;   // total output duration time in microsecond
  end;
}
  with lvFiles.Items.Item[AProgressInfo.TaskIndex].SubItems, AProgressInfo^ do
  begin
    if TotalDuration > 0 then
      Strings[3] := IntToStr(CurrentDuration * 100 div TotalDuration) + '%'
    else
      Strings[3] := 'N/A';

    if FPS > 0 then
      Strings[4] := IntToStr(FPS);

    Strings[5] := IntToStr(CurrentDuration div 1000000); // display as seconds
    Strings[6] := IntToStr(CurrentSize);
  end;

  if not chkThreadMode.Checked then
    // make sure update converting status when converting without thread mode
    lvFiles.Repaint;
end;

procedure TfrmMain.FFVCLTerminate(Sender: TObject; const ATerminateInfo: TTerminateInfo);
begin
  // OnTerminate event handler
{
  TTerminateInfo = record
    TaskIndex: Integer;     // index of converting tasks, (-1) means all tasks are terminated
    Finished: Boolean;      // True means converted success, False means converting breaked
    Exception: Boolean;     // True means Exception occured, False please ignore
    ExceptionMsg: string;   // Exception message 
  end;
}
  if ATerminateInfo.TaskIndex < 0 then
  begin
    // set status of buttons
    btnAdd.Enabled := False;
    btnRemove.Enabled := False;
    btnClear.Enabled := True;
    btnStart.Enabled := False;
    btnStop.Enabled := False;
    btnPause.Enabled := False;
    btnResume.Enabled := False;
  end
  else if ATerminateInfo.Finished then
  begin // TaskIndex task converted success
    lvFiles.Items.Item[ATerminateInfo.TaskIndex].SubItems.Strings[3] := '100%';
    if not chkThreadMode.Checked then
    begin // make sure update converting status when converting without thread mode
      lvFiles.Repaint;
    end;
  end;

  if (ATerminateInfo.TaskIndex >= 0) and not ATerminateInfo.Exception then
    // indicate the output file is ready to play
    FOutFileList.Objects[ATerminateInfo.TaskIndex] := TObject(1);

  if ATerminateInfo.Exception then // Exception occured, show exception message
    Application.MessageBox(PChar(ATerminateInfo.ExceptionMsg), PChar(Application.Title), MB_OK + MB_ICONWARNING);
end;

procedure TfrmMain.grpLogLevelClick(Sender: TObject);
begin
  // TLogLevel = (llQuiet, llPanic, llFatal, llError, llWarning, llInfo, llVerbose, llDebug);
  FFLogger.LogLevel := TLogLevel(grpLogLevel.ItemIndex);
end;

procedure TfrmMain.FFLoggerLog(Sender: TObject; AThreadID: Cardinal;
  ALogLevel: TLogLevel; const ALogMsg: string);
begin
  // OnLog event handler
  // TLogLevel = (llQuiet, llPanic, llFatal, llError, llWarning, llInfo, llVerbose, llDebug);
  mmoLog.Lines.Add('#' + IntToStr(AThreadID) + '.' + IntToStr(Ord(ALogLevel)) + ': ' + ALogMsg);
end;

end.