unit ucefbrowserthread;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, SyncObjs,
  uworkerthread, ucustommessage,
  uCEFChromium, uCEFTypes, uCEFInterfaces, uCEFConstants, uCEFBufferPanel,
  uCEFChromiumCore, uCEFMiscFunctions;

type
  TThreadStatus = (tsInitializing, tsIdle, tsLoading, tsClosing, tsDestroyed, tsInitError);

  TSize = record
    cx : integer;
    cy : integer;
  end;

  TCEFBrowserThread = class(TWorkerThread)
    protected
      FBrowser               : TChromium;
      FStatus                : TThreadStatus;
      FBrowserSize           : TSize;
      FBrowserCS             : TCriticalSection;
      FErrorCode             : integer;
      FErrorText             : string;
      FFailedURL             : string;
      FDefaultURL            : string;
      FFileName              : string;
      FMessageID             : integer;
      FOnInitialized         : TNotifyEvent;
      FOnSnapshotAvailable   : TNotifyEvent;
      FOnError               : TNotifyEvent;

      function  GetErrorCode : integer;
      function  GetErrorText : string;
      function  GetFailedURL : string;
      function  GetInitialized : boolean;
      function  GetClosing : boolean;
      function  GetStatus : TThreadStatus;
      function  GetFileName : string;

      procedure SetErrorText(const aValue : string);
      procedure SetFileName(const aValue : string);
      procedure SetStatus(aValue : TThreadStatus);

      procedure Browser_OnAfterCreated(Sender: TObject; const browser: ICefBrowser);
      procedure Browser_OnGetViewRect(Sender: TObject; const browser: ICefBrowser; var rect: TCefRect);
      procedure Browser_OnGetScreenInfo(Sender: TObject; const browser: ICefBrowser; var screenInfo: TCefScreenInfo; out Result: Boolean);
      procedure Browser_OnBeforePopup(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; popup_id: Integer; const targetUrl, targetFrameName: ustring; targetDisposition: TCefWindowOpenDisposition; userGesture: Boolean; const popupFeatures: TCefPopupFeatures; var windowInfo: TCefWindowInfo; var client: ICefClient; var settings: TCefBrowserSettings; var extra_info: ICefDictionaryValue; var noJavascriptAccess: Boolean; var Result: Boolean);
      procedure Browser_OnBeforeClose(Sender: TObject; const browser: ICefBrowser);
      procedure Browser_OnLoadError(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; errorCode: TCefErrorcode; const errorText, failedUrl: ustring);
      procedure Browser_OnLoadingStateChange(Sender: TObject; const browser: ICefBrowser; isLoading, canGoBack, canGoForward: Boolean);
      procedure Browser_OnDevToolsMethodResult(Sender: TObject; const browser: ICefBrowser; message_id: Integer; success: Boolean; const result: ICefValue);
      procedure Browser_OnOpenUrlFromTab(Sender: TObject; const browser: ICefBrowser; const frame: ICefFrame; const targetUrl: ustring; targetDisposition: TCefWindowOpenDisposition; userGesture: Boolean; out Result: Boolean);

      procedure DoOnInitialized;
      procedure DoOnError;
      procedure DoOnSnapshotAvailable;
                                           
      procedure ProcessValue(const aInfo : TMsgInfo); override;
      procedure DoLoadURL(const aURL : string);
      function  CreateBrowser : boolean;
      procedure CloseBrowser;
      procedure InitError;
      procedure Execute; override;

    public
      constructor Create(aWidth, aHeight : integer; const aDefaultURL, aFileName : string);
      destructor  Destroy; override;
      procedure   AfterConstruction; override;
      procedure   TerminateBrowserThread;
      procedure   LoadURL(const aURL : string);

      property ErrorCode             : integer           read GetErrorCode;
      property ErrorText             : string            read GetErrorText             write SetErrorText;
      property FailedUrl             : string            read GetFailedUrl;
      property Initialized           : boolean           read GetInitialized;
      property Closing               : boolean           read GetClosing;       
      property Status                : TThreadStatus     read GetStatus                write SetStatus;
      property FileName              : string            read GetFileName              write SetFileName;

      property OnInitialized         : TNotifyEvent      read FOnInitialized           write FOnInitialized;
      property OnSnapshotAvailable   : TNotifyEvent      read FOnSnapshotAvailable     write FOnSnapshotAvailable;
      property OnError               : TNotifyEvent      read FOnError                 write FOnError;
  end;

implementation                                

uses
  uCEFDictionaryValue, uCEFJson;

const
  WORKERTHREADMSG_LOADURL      = WORKERTHREADMSG_QUIT + 1;
  WORKERTHREADMSG_DOONERROR    = WORKERTHREADMSG_QUIT + 2;
  WORKERTHREADMSG_CLOSEBROWSER = WORKERTHREADMSG_QUIT + 3;

constructor TCEFBrowserThread.Create(aWidth, aHeight : integer; const aDefaultURL, aFileName : string);
begin
  inherited Create;

  FStatus                := tsInitializing;
  FBrowser               := nil;
  FBrowserSize.cx        := aWidth;
  FBrowserSize.cy        := aHeight;
  FDefaultURL            := aDefaultURL;
  FFileName              := aFileName;
  FBrowserCS             := nil;
  FMessageID             := -1;
  FOnInitialized         := nil;
  FOnSnapshotAvailable   := nil;
  FOnError               := nil;
end;

destructor TCEFBrowserThread.Destroy;
begin
  if (FBrowser <> nil) then
    FreeAndNil(FBrowser);

  if (FBrowserCS <> nil) then
    FreeAndNil(FBrowserCS);

  inherited Destroy;
end;

procedure TCEFBrowserThread.AfterConstruction;
begin
  inherited AfterConstruction;

  FBrowserCS                       := TCriticalSection.Create;

  FBrowser                         := TChromium.Create(nil);
  FBrowser.DefaultURL              := FDefaultURL;
  FBrowser.Options.BackgroundColor := CefColorSetARGB($FF, $FF, $FF, $FF);
  FBrowser.OnAfterCreated          := @Browser_OnAfterCreated;
  FBrowser.OnGetViewRect           := @Browser_OnGetViewRect;
  FBrowser.OnGetScreenInfo         := @Browser_OnGetScreenInfo;
  FBrowser.OnBeforePopup           := @Browser_OnBeforePopup;
  FBrowser.OnBeforeClose           := @Browser_OnBeforeClose;
  FBrowser.OnLoadError             := @Browser_OnLoadError;
  FBrowser.OnLoadingStateChange    := @Browser_OnLoadingStateChange;
  FBrowser.OnOpenUrlFromTab        := @Browser_OnOpenUrlFromTab;
  FBrowser.OnDevToolsMethodResult  := @Browser_OnDevToolsMethodResult;
end;

procedure TCEFBrowserThread.TerminateBrowserThread;
begin
  Terminate;
  EnqueueMessage(WORKERTHREADMSG_CLOSEBROWSER);
end;

procedure TCEFBrowserThread.LoadURL(const aURL : string);
begin
  EnqueueMessage(WORKERTHREADMSG_LOADURL, 0, aURL);
end;

function TCEFBrowserThread.GetErrorCode : integer;
begin
  FBrowserCS.Acquire;
  Result := FErrorCode;
  FBrowserCS.Release;
end;

function TCEFBrowserThread.GetErrorText : string;
begin
  FBrowserCS.Acquire;
  Result := FErrorText;
  FBrowserCS.Release;
end;

function TCEFBrowserThread.GetFailedURL : string;
begin
  FBrowserCS.Acquire;
  Result := FFailedURL;
  FBrowserCS.Release;
end;

function TCEFBrowserThread.GetInitialized : boolean;
begin
  FBrowserCS.Acquire;
  Result := not(Terminated) and (FStatus in [tsIdle, tsLoading]);
  FBrowserCS.Release;
end;

function TCEFBrowserThread.GetClosing : boolean;
begin
  FBrowserCS.Acquire;
  Result := (FStatus = tsClosing);
  FBrowserCS.Release;
end;

function TCEFBrowserThread.GetStatus : TThreadStatus;
begin
  FBrowserCS.Acquire;
  Result := FStatus;
  FBrowserCS.Release;
end;  

function TCEFBrowserThread.GetFileName : string;
begin
  FBrowserCS.Acquire;
  Result := FFileName;
  FBrowserCS.Release;
end;

procedure TCEFBrowserThread.SetErrorText(const aValue : string);
begin
  FBrowserCS.Acquire;
  FErrorText := aValue;
  FBrowserCS.Release;
end;          

procedure TCEFBrowserThread.SetFileName(const aValue : string);
begin
  FBrowserCS.Acquire;
  FFileName := aValue;
  FBrowserCS.Release;
end;

procedure TCEFBrowserThread.SetStatus(aValue : TThreadStatus);
begin
  FBrowserCS.Acquire;
  FStatus := aValue;
  FBrowserCS.Release;
end;

procedure TCEFBrowserThread.DoOnInitialized;
begin
  if assigned(FOnInitialized) then
    FOnInitialized(self);
end;

procedure TCEFBrowserThread.DoOnError;
begin
  if assigned(FOnError) then
    FOnError(self);
end;

procedure TCEFBrowserThread.DoOnSnapshotAvailable;
begin
  if assigned(FOnSnapshotAvailable) then
    FOnSnapshotAvailable(self);
end;

procedure TCEFBrowserThread.Browser_OnAfterCreated(Sender: TObject;
  const browser: ICefBrowser);
begin
  Status := tsIdle;
  DoOnInitialized;
end;

procedure TCEFBrowserThread.Browser_OnGetViewRect(Sender: TObject;
  const browser: ICefBrowser; var rect: TCefRect);
begin
  rect.x      := 0;
  rect.y      := 0;
  rect.width  := FBrowserSize.cx;
  rect.height := FBrowserSize.cy;
end;

procedure TCEFBrowserThread.Browser_OnGetScreenInfo(Sender: TObject;
  const browser: ICefBrowser; var screenInfo: TCefScreenInfo; out Result: Boolean);
var
  TempRect : TCEFRect;
begin
  TempRect.x      := 0;
  TempRect.y      := 0;
  TempRect.width  := FBrowserSize.cx;
  TempRect.height := FBrowserSize.cy;

  screenInfo.device_scale_factor := 1;
  screenInfo.depth               := 0;
  screenInfo.depth_per_component := 0;
  screenInfo.is_monochrome       := Ord(False);
  screenInfo.rect                := TempRect;
  screenInfo.available_rect      := TempRect;

  Result := True;
end;

procedure TCEFBrowserThread.Browser_OnBeforePopup(Sender: TObject;
  const browser: ICefBrowser; const frame: ICefFrame;
  popup_id: Integer; const targetUrl, targetFrameName: ustring;
  targetDisposition: TCefWindowOpenDisposition; userGesture: Boolean;
  const popupFeatures: TCefPopupFeatures; var windowInfo: TCefWindowInfo;
  var client: ICefClient; var settings: TCefBrowserSettings;
  var extra_info: ICefDictionaryValue; var noJavascriptAccess: Boolean;
  var Result: Boolean);
begin
  Result := (targetDisposition in [CEF_WOD_NEW_FOREGROUND_TAB,
                                   CEF_WOD_NEW_BACKGROUND_TAB,
                                   CEF_WOD_NEW_POPUP,
                                   CEF_WOD_NEW_WINDOW]);
end;

procedure TCEFBrowserThread.Browser_OnBeforeClose(Sender: TObject; const browser: ICefBrowser);
begin
  Status := tsDestroyed;
  EnqueueMessage(WORKERTHREADMSG_QUIT);
end;

procedure TCEFBrowserThread.Browser_OnLoadError(Sender: TObject;
  const browser: ICefBrowser; const frame: ICefFrame; errorCode: TCefErrorcode;
  const errorText, failedUrl: ustring);
begin
  if not(Terminated) and (frame <> nil) and frame.IsValid and frame.IsMain then
    try
      FBrowserCS.Acquire;
      FErrorCode := errorCode;
      FErrorText := errorText;
      FFailedUrl := failedUrl;
    finally
      FBrowserCS.Release;
      EnqueueMessage(WORKERTHREADMSG_DOONERROR);
    end;
end;

procedure TCEFBrowserThread.Browser_OnLoadingStateChange(Sender: TObject;
  const browser: ICefBrowser; isLoading, canGoBack, canGoForward: Boolean);
var
  TempParams : ICefDictionaryValue;
begin
  if isLoading then
    Status := tsLoading
   else
    begin
      Status     := tsIdle;
      TempParams := TCefDictionaryValueRef.New;
      TempParams.SetString('format', 'png');
      FMessageID := FBrowser.ExecuteDevToolsMethod(0, 'Page.captureScreenshot', TempParams);
      TempParams := nil;
    end;
end;

procedure TCEFBrowserThread.Browser_OnDevToolsMethodResult(Sender: TObject;
  const browser: ICefBrowser; message_id: Integer; success: Boolean;
  const result: ICefValue);
var
  TempRsltDict : ICefDictionaryValue;
  TempString   : ustring;
  TempBin      : ICefBinaryValue;
  TempStream   : TFileStream;
  TempSuccess  : boolean;
begin
  if not(success) or (FMessageID <> message_id) or not(assigned(result)) then exit;

  TempSuccess  := False;
  TempStream   := nil;
  TempRsltDict := result.GetDictionary;

  if assigned(TempRsltDict) then
    try
      if TCEFJson.ReadString(TempRsltDict, 'data', TempString) then
        try
          TempBin := CefBase64Decode(TempString);

          if assigned(TempBin) and (TempBin.Size > 0) then
            try
              try
                TempStream := TFileStream.Create(FileName, fmCreate);
                TempStream.WriteBuffer(TempBin.GetRawData^, TempBin.Size);
                TempSuccess := True;
              except
                on e : exception do
                  if CustomExceptionHandler('TCEFBrowserThread.Browser_OnDevToolsMethodResult', e) then raise;
              end;
            finally
              if assigned(TempStream) then
                FreeAndNil(TempStream);
            end;
        finally
          TempBin := nil;
        end;
    finally
      TempRsltDict := nil;
      if TempSuccess then DoOnSnapshotAvailable;
    end;
end;

procedure TCEFBrowserThread.Browser_OnOpenUrlFromTab(Sender: TObject;
  const browser: ICefBrowser; const frame: ICefFrame; const targetUrl: ustring;
  targetDisposition: TCefWindowOpenDisposition; userGesture: Boolean;
  out Result: Boolean);
begin
  Result := (targetDisposition in [CEF_WOD_NEW_FOREGROUND_TAB,
                                   CEF_WOD_NEW_BACKGROUND_TAB,
                                   CEF_WOD_NEW_POPUP,
                                   CEF_WOD_NEW_WINDOW]);
end;

procedure TCEFBrowserThread.ProcessValue(const aInfo : TMsgInfo);
begin
  case aInfo.Msg of
    WORKERTHREADMSG_LOADURL      : DoLoadURL(aInfo.StrParam);
    WORKERTHREADMSG_DOONERROR    : DoOnError;
    WORKERTHREADMSG_CLOSEBROWSER : CloseBrowser;
  end;
end;

function TCEFBrowserThread.CreateBrowser : boolean;
begin
  Result := assigned(FBrowser) and FBrowser.CreateBrowser;
end;

procedure TCEFBrowserThread.DoLoadURL(const aURL : string);
begin
  if not(Terminated) and Initialized and assigned(FBrowser) then
    FBrowser.LoadURL(aURL);
end;

procedure TCEFBrowserThread.CloseBrowser;
begin
  if Initialized then
    begin
      if assigned(FBrowser) then
        begin
          Status := tsClosing;
          FBrowser.CloseBrowser(True);
        end;
    end
   else
    if not(Closing) then
      EnqueueMessage(WORKERTHREADMSG_QUIT);
end;

procedure TCEFBrowserThread.InitError;
begin
  Status    := tsInitError;
  ErrorText := 'There was an error initializing the CEF browser.';
  DoOnError;
end;

procedure TCEFBrowserThread.Execute;
begin
  if CreateBrowser then
    inherited Execute
   else
    InitError;
end;

end.
