unit uMediaRouterFrm;

{$I ..\..\..\source\cef.inc}

interface

uses
  {$IFDEF DELPHI16_UP}
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Menus,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, System.Types, Vcl.ComCtrls, Vcl.ClipBrd,
  System.UITypes, Vcl.AppEvnts, Winapi.ActiveX, Winapi.ShlObj, System.SyncObjs,
  {$ELSE}
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Menus,
  Controls, Forms, Dialogs, StdCtrls, ExtCtrls, Types, ComCtrls, ClipBrd,
  AppEvnts, ActiveX, ShlObj, SyncObjs,
  {$ENDIF}
  uCEFChromium, uCEFWindowParent, uCEFInterfaces, uCEFApplication, uCEFTypes, uCEFConstants,
  uCEFWinControl, uCEFChromiumCore;

const
  MEDIA_ROUTER_PENDING_LOG_LINES    = WM_APP + $B00;
  MEDIA_ROUTER_REFRESH_SINKS        = WM_APP + $B01;
  MEDIA_ROUTER_REFRESH_ROUTES       = WM_APP + $B02;
  MEDIA_ROUTER_UPDATE_BUTTONS       = WM_APP + $B03;

type
  TMediaRouterFrm = class(TForm)
    Chromium1: TChromium;
    CEFWindowParent1: TCEFWindowParent;
    Timer1: TTimer;
    MainPnl: TPanel;
    SinksGbx: TGroupBox;
    SinksLbx: TListBox;
    SinksButtonsPnl: TPanel;
    GetDeviceInfoBtn: TButton;
    CentralPnl: TPanel;
    SourcePnl: TPanel;
    SourceLblPnl: TPanel;
    SourceURNLbl: TLabel;
    RoutesGbx: TGroupBox;
    RoutesLbx: TListBox;
    RoutesButtonPnl: TPanel;
    TerminateRouteBtn: TButton;
    MessageGbx: TGroupBox;
    SendMessagePnl: TPanel;
    SendMsgBtn: TButton;
    MessageMem: TMemo;
    LogGbx: TGroupBox;
    LogMem: TMemo;
    SpacerPnl: TPanel;
    NotifySinksBtn: TButton;
    NotifyRoutesBtn: TButton;
    ClearLogPnl: TPanel;
    ClearLogBtn: TButton;
    SourceURNCbx: TComboBox;
    CreateRouteBtn: TButton;

    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormShow(Sender: TObject);

    procedure Chromium1AfterCreated(Sender: TObject; const browser: ICefBrowser);
    procedure Chromium1BeforeClose(Sender: TObject; const browser: ICefBrowser);
    procedure Chromium1Sinks(Sender: TObject; const sinks: TCefMediaSinkArray);
    procedure Chromium1Routes(Sender: TObject; const routes: TCefMediaRouteArray);
    procedure Chromium1RouteStateChanged(Sender: TObject; const route: ICefMediaRoute; state: TCefMediaRouteConnectionState);
    procedure Chromium1RouteMessageReceived(Sender: TObject; const route: ICefMediaRoute; const message_: ustring);
    procedure Chromium1MediaRouteCreateFinished(Sender: TObject; result: TCefMediaRouterCreateResult; const error: ustring; const route: ICefMediaRoute);
    procedure Chromium1MediaSinkDeviceInfo(Sender: TObject; const ip_address: ustring; port: Integer; const model_name: ustring);

    procedure Timer1Timer(Sender: TObject);
    procedure SourceURNEdtChange(Sender: TObject);
    procedure SinksLbxClick(Sender: TObject);
    procedure GetDeviceInfoBtnClick(Sender: TObject);
    procedure TerminateRouteBtnClick(Sender: TObject);
    procedure SendMsgBtnClick(Sender: TObject);
    procedure NotifySinksBtnClick(Sender: TObject);
    procedure NotifyRoutesBtnClick(Sender: TObject);
    procedure ClearLogBtnClick(Sender: TObject);
    procedure MessageMemChange(Sender: TObject);
    procedure RoutesLbxClick(Sender: TObject);
    procedure CreateRouteBtnClick(Sender: TObject);

  protected
    // Variables to control when can we destroy the form safely
    FCanClose : boolean;  // Set to True in TChromium.OnBeforeClose
    FClosing  : boolean;  // Set to True in the CloseQuery event.

    FMediaCS  : TCriticalSection;
    FLog      : TStringList;
    FSinks    : TCefMediaSinkInfoArray;
    FRoutes   : TCefMediaRouteInfoArray;

    procedure PendingLogLinesMsg(var aMessage : TMessage); message MEDIA_ROUTER_PENDING_LOG_LINES;
    procedure RefreshSinksMsg(var aMessage : TMessage); message MEDIA_ROUTER_REFRESH_SINKS;
    procedure RefreshRoutesMsg(var aMessage : TMessage); message MEDIA_ROUTER_REFRESH_ROUTES;
    procedure UpdateButtonsMsg(var aMessage : TMessage); message MEDIA_ROUTER_UPDATE_BUTTONS;

    procedure DestroySinksArray;
    procedure DestroyRoutesArray;
    procedure DestroyAllArrays;
    procedure CopySinksArray(const aSinks : TCefMediaSinkArray);
    procedure CopyRoutesArray(const aRoutes : TCefMediaRouteArray);
    procedure UpdateAvailableSinks;
    procedure UpdateAvailableRoutes;
    procedure UpdateButtons;
    procedure AppendPendingLogStrings;
    procedure AddLogEntry(const aMessage1, aMessage2 : string; aRec : boolean); overload;
    procedure AddLogEntry(const aMessage1 : string; const aMessage2 : string = ''); overload;
  end;

var
  MediaRouterFrm: TMediaRouterFrm;

procedure CreateGlobalCEFApp;

implementation

{$R *.dfm}

uses
  {$IFDEF DELPHI16_UP}
  System.Math,
  {$ELSE}
  Math,
  {$ENDIF}
  uCEFMiscFunctions;

// This demo can use a MediaRouter to communicate with devices on the local
// network via the CAST and DIAL protocols.

// TChromium exposes all the ICefMediaObserver events and the ICefMediaRouter
// methods to show the available devices, to established communication routes
// and to send messages to those devices.

// Follow these steps to test this demo :

// STEP 1
// ------
// Turn on your ChromeCast receiver. It can be an Android TV or similar device
// like a Xiaomi MiBox. It must be on the same local network as the computer
// running this demo.

// STEP 2
// ------
// Run this demo and wait a few seconds. It will show all the available devices
// in the local network and the protocol used to communicate with them.
// In the case of the MiBox device it shows a DIAL and a CAST "sink" but after
// a couple of secods the list is updated and it only shows the CAST sink.

// STEP 3
// ------
// Select the CAST sink and click on the "Create route" button. This button will
// use the "Source URN" value to create the route and that value has a CAST URN
// with the "Application ID" of the "Default Media Receiver", which is CC1AD845.
// This ID can be used for tests.

// STEP 4
// ------
// The "Established routes" box will show the routes that you created.

// STEP 5
// ------
// Select the route, type the message and click on the "Send message" button.

// STEP 6
// ------
// Click on the "Terminate route" button.

// WARNING
// =======
// This demo has a known bug when you close the form and it will crash.

// References :
// ============
// https://bitbucket.org/chromiumembedded/cef/issues/2900/add-mediarouter-support-for-cast-receiver
// https://blog.oakbits.com/google-cast-protocol-overview.html
// https://developers.google.com/cast/docs/developers
// https://gist.github.com/jloutsenhizer/8855258

procedure CreateGlobalCEFApp;
begin
  GlobalCEFApp             := TCefApplication.Create;
  GlobalCEFApp.LogFile     := 'debug.log';
  GlobalCEFApp.LogSeverity := LOGSEVERITY_INFO;
end;

procedure TMediaRouterFrm.Chromium1AfterCreated(Sender: TObject;
  const browser: ICefBrowser);
begin
  AddLogEntry('Browser initialized.');
end;

procedure TMediaRouterFrm.Chromium1BeforeClose(Sender: TObject; const browser: ICefBrowser);
begin
  FCanClose := True;
  PostMessage(Handle, WM_CLOSE, 0, 0);
end;

procedure TMediaRouterFrm.Chromium1MediaRouteCreateFinished(Sender: TObject;
  result: TCefMediaRouterCreateResult; const error: ustring; const route: ICefMediaRoute);
var
  TempMsg, TempID : string;
begin
  TempMsg := '';

  try
    FMediaCS.Acquire;

    if (result = CEF_MRCR_OK) then
      begin
        TempMsg := 'Route created';
        if (route <> nil) then TempID := 'Route ID : ' + route.ID;
      end
     else
      TempMsg := error;
  finally
    FMediaCS.Release;
    if (length(TempMsg) > 0) then AddLogEntry(TempID, TempMsg);
    PostMessage(Handle, MEDIA_ROUTER_UPDATE_BUTTONS, 0, 0);
  end;
end;

procedure TMediaRouterFrm.Chromium1MediaSinkDeviceInfo(Sender: TObject;
  const ip_address: ustring; port: Integer; const model_name: ustring);
begin
  try
    FMediaCS.Acquire;
    FLog.Add('Sink device info');
    FLog.Add('IP address : ' + ip_address);
    FLog.Add('Port : ' + inttostr(port));
    FLog.Add('Model name : ' + model_name);
    FLog.Add('------------------------------------------');
  finally
    PostMessage(Handle, MEDIA_ROUTER_PENDING_LOG_LINES, 0, 0);
    FMediaCS.Release;
  end;
end;

procedure TMediaRouterFrm.Chromium1RouteMessageReceived(Sender: TObject;
  const route: ICefMediaRoute; const message_: ustring);
var
  TempID : string;
begin
  if (route <> nil) then
    TempID := 'Route ID : ' + route.ID;

  AddLogEntry(TempID, 'Message contents : ' + message_, True);
end;

procedure TMediaRouterFrm.Chromium1Routes(Sender: TObject;
  const routes: TCefMediaRouteArray);
begin
  CopyRoutesArray(routes);
end;

procedure TMediaRouterFrm.Chromium1RouteStateChanged(Sender: TObject;
  const route: ICefMediaRoute; state: TCefMediaRouteConnectionState);
var
  TempMsg, TempID : string;
begin
  if (route <> nil) then TempID := 'Route ID : ' + route.ID;

  case state of
    CEF_MRCS_CONNECTING : TempMsg := 'Route state : Connecting.';
    CEF_MRCS_CONNECTED  : TempMsg := 'Route state : Connected.';
    CEF_MRCS_CLOSED     : TempMsg := 'Route state : Closed.';
    CEF_MRCS_TERMINATED : TempMsg := 'Route state : Terminated.';
    else                  TempMsg := 'Route state : Unknown.';
  end;

  TempMsg := TempMsg + ' ' + dateTimeToStr(now);

  AddLogEntry(TempID, TempMsg);
end;

procedure TMediaRouterFrm.Chromium1Sinks(Sender: TObject;
  const sinks: TCefMediaSinkArray);
begin
  CopySinksArray(sinks);
end;

procedure TMediaRouterFrm.ClearLogBtnClick(Sender: TObject);
begin
  LogMem.Lines.Clear;
end;

procedure TMediaRouterFrm.FormCloseQuery(Sender: TObject;
  var CanClose: Boolean);
begin
  CanClose := FCanClose;

  if not(FClosing) then
    begin
      FClosing := True;
      Visible  := False;
      DestroyAllArrays;
      Chromium1.ExecuteDevToolsMethod(0, 'Cast.disable', nil);
      sleep(500);
      Chromium1.CloseBrowser(True);
      CEFWindowParent1.Free;
    end;
end;

procedure TMediaRouterFrm.FormCreate(Sender: TObject);
begin
  FCanClose := False;
  FClosing  := False;
  FSinks    := nil;
  FRoutes   := nil;
  FMediaCS  := TCriticalSection.Create;
  FLog      := TStringList.Create;

  //Chromium1.RuntimeStyle := CEF_RUNTIME_STYLE_ALLOY;
end;

procedure TMediaRouterFrm.FormDestroy(Sender: TObject);
begin
  FLog.Free;
  FMediaCS.Free;
end;

procedure TMediaRouterFrm.FormShow(Sender: TObject);
begin
  UpdateButtons;

  if not(Chromium1.CreateBrowser(CEFWindowParent1)) then
    Timer1.Enabled := True;
end;

procedure TMediaRouterFrm.MessageMemChange(Sender: TObject);
begin
  UpdateButtons;
end;

procedure TMediaRouterFrm.NotifyRoutesBtnClick(Sender: TObject);
begin
  Chromium1.NotifyCurrentRoutes;
end;

procedure TMediaRouterFrm.NotifySinksBtnClick(Sender: TObject);
begin
  Chromium1.NotifyCurrentSinks;
end;

procedure TMediaRouterFrm.SendMsgBtnClick(Sender: TObject);
var
  TempMsg, TempID : string;
begin
  TempMsg := '';

  try
    FMediaCS.Acquire;

    if (RoutesLbx.Items.Count > 0) and
       (FRoutes <> nil) and
       (RoutesLbx.ItemIndex >= 0) and
       (RoutesLbx.ItemIndex < length(FRoutes)) and
       (MessageMem.Lines.Count > 0) then
      begin
        TempMsg := trim(MessageMem.Lines.Text);

        if (length(TempMsg) > 0) and
           (FRoutes[RoutesLbx.ItemIndex].RouteIntf <> nil) then
          try
            TempID := 'Route ID : ' + FRoutes[RoutesLbx.ItemIndex].RouteIntf.ID;
            FRoutes[RoutesLbx.ItemIndex].RouteIntf.SendRouteMessage(TempMsg);
            TempMsg := 'Message contents : ' + TempMsg;
          except
            on e : exception do
              if CustomExceptionHandler('TMediaRouterFrm.SendMsgBtnClick', e) then raise;
          end;
      end;
  finally
    FMediaCS.Release;
    if (length(TempMsg) > 0) then AddLogEntry(TempID, TempMsg, False);
  end;
end;

procedure TMediaRouterFrm.SinksLbxClick(Sender: TObject);
begin
  UpdateButtons;
end;

procedure TMediaRouterFrm.SourceURNEdtChange(Sender: TObject);
begin
  UpdateButtons;
end;

procedure TMediaRouterFrm.TerminateRouteBtnClick(Sender: TObject);
begin
  try
    FMediaCS.Acquire;

    if (RoutesLbx.Items.Count > 0) and
       (FRoutes <> nil) and
       (RoutesLbx.ItemIndex >= 0) and
       (RoutesLbx.ItemIndex < length(FRoutes)) and
       (FRoutes[RoutesLbx.ItemIndex].RouteIntf <> nil) then
      try
        FRoutes[RoutesLbx.ItemIndex].RouteIntf.Terminate;
      except
        on e : exception do
          if CustomExceptionHandler('TMediaRouterFrm.TerminateRouteBtnClick', e) then raise;
      end;
  finally
    FMediaCS.Release;
    UpdateButtons;
  end;
end;

procedure TMediaRouterFrm.Timer1Timer(Sender: TObject);
begin
  Timer1.Enabled := False;
  if not(Chromium1.CreateBrowser(CEFWindowParent1)) and not(Chromium1.Initialized) then
    Timer1.Enabled := True;
end;

procedure TMediaRouterFrm.PendingLogLinesMsg(var aMessage : TMessage);
begin
  if FClosing then exit;

  AppendPendingLogStrings;
end;

procedure TMediaRouterFrm.AppendPendingLogStrings;
begin
  try
    FMediaCS.Acquire;

    if (FLog <> nil) and (FLog.Count > 0) then
      begin
        LogMem.Lines.AddStrings(FLog);
        LogMem.SelLength := 0;
        LogMem.SelStart  := Length(LogMem.Text);
        FLog.Clear;
      end;
  finally
    FMediaCS.Release;
  end;
end;

procedure TMediaRouterFrm.RefreshSinksMsg(var aMessage : TMessage);
begin
  if FClosing then exit;

  UpdateAvailableSinks;
  AppendPendingLogStrings;
  UpdateButtons;
end;

procedure TMediaRouterFrm.RoutesLbxClick(Sender: TObject);
begin
  UpdateButtons;
end;

procedure TMediaRouterFrm.RefreshRoutesMsg(var aMessage : TMessage);
begin
  if FClosing then exit;

  UpdateAvailableRoutes;
  AppendPendingLogStrings;
  UpdateButtons;
end;

procedure TMediaRouterFrm.UpdateButtonsMsg(var aMessage : TMessage);
begin
  if FClosing then exit;

  AppendPendingLogStrings;
  UpdateButtons;
end;

procedure TMediaRouterFrm.DestroyAllArrays;
begin
  try
    FMediaCS.Acquire;
    DestroyRoutesArray;
    DestroySinksArray;
  finally
    FMediaCS.Release;
  end;
end;

procedure TMediaRouterFrm.DestroySinksArray;
var
  i : integer;
begin
  if (FSinks <> nil) then
    begin
      i := pred(length(FSinks));
      while (i >= 0) do
        begin
          FSinks[i].SinkIntf := nil;
          dec(i);
        end;

      Finalize(FSinks);
      FSinks := nil;
    end;
end;

procedure TMediaRouterFrm.DestroyRoutesArray;
var
  i : integer;
begin
  if (FRoutes <> nil) then
    begin
      i := pred(length(FRoutes));
      while (i >= 0) do
        begin
          FRoutes[i].RouteIntf := nil;
          dec(i);
        end;

      Finalize(FRoutes);
      FRoutes := nil;
    end;
end;

procedure TMediaRouterFrm.CreateRouteBtnClick(Sender: TObject);
var
  TempURN, TempErrorMsg : string;
  TempSource : ICefMediaSource;
begin
  TempURN := trim(SourceURNCbx.Text);

  if (length(TempURN) = 0) then
    begin
      AddLogEntry('Invalid URN');
      exit;
    end;

  TempErrorMsg := '';

  try
    try
      FMediaCS.Acquire;

      if (FSinks <> nil) and
         (SinksLbx.Items.Count > 0) and
         (SinksLbx.ItemIndex >= 0) and
         (SinksLbx.ItemIndex < length(FSinks)) then
        begin
          TempSource := Chromium1.GetSource(TempURN);

          if (TempSource <> nil) then
            begin
              if (FSinks[SinksLbx.ItemIndex].SinkIntf <> nil) then
                begin
                  if FSinks[SinksLbx.ItemIndex].SinkIntf.IsCompatibleWith(TempSource) then
                    Chromium1.CreateRoute(TempSource, FSinks[SinksLbx.ItemIndex].SinkIntf)
                   else
                    TempErrorMsg := 'The selected Sink is not compatible with the Media Source.';
                end
               else
                TempErrorMsg := 'The selected Sink is not valid.';
            end
           else
            TempErrorMsg := 'The Media Source is not valid.';
        end
       else
        TempErrorMsg := 'The sinks list is outdated.';
    except
      on e : exception do
        begin
          TempErrorMsg := e.Message;
          if CustomExceptionHandler('TMediaRouterFrm.CreateRouteBtnClick', e) then raise;
        end;
    end;
  finally
    TempSource := nil;
    FMediaCS.Release;
    if (length(TempErrorMsg) > 0) then AddLogEntry(TempErrorMsg);
  end;
end;

procedure TMediaRouterFrm.CopySinksArray(const aSinks : TCefMediaSinkArray);
var
  i, TempLen : integer;
begin
  try
    FMediaCS.Acquire;

    FLog.Add('Sinks available : ' + inttostr(length(aSinks)));
    FLog.Add('------------------------------------------');

    DestroySinksArray;

    if (aSinks <> nil) then
      begin
        TempLen := length(aSinks);
        SetLength(FSinks, TempLen);

        i := 0;
        while (i < TempLen) do
          begin
            FSinks[i].ID          := aSinks[i].ID;
            FSinks[i].Name        := aSinks[i].Name;
            FSinks[i].IconType    := aSinks[i].IconType;
            FSinks[i].SinkIntf    := aSinks[i];

            if aSinks[i].IsCastSink then
              FSinks[i].SinkType := mtCast
             else
              if aSinks[i].IsDialSink then
                FSinks[i].SinkType := mtDial
               else
                FSinks[i].SinkType := mtUnknown;

            inc(i);
          end;
      end;
  finally
    PostMessage(Handle, MEDIA_ROUTER_REFRESH_SINKS, 0, 0);
    FMediaCS.Release;
  end;
end;

procedure TMediaRouterFrm.CopyRoutesArray(const aRoutes : TCefMediaRouteArray);
var
  i, TempLen : integer;
begin
  try
    FMediaCS.Acquire;

    FLog.Add('Routes available : ' + inttostr(length(aRoutes)));
    FLog.Add('------------------------------------------');

    DestroyRoutesArray;

    if (aRoutes <> nil) then
      begin
        TempLen := length(aRoutes);
        SetLength(FRoutes, TempLen);

        i := 0;
        while (i < TempLen) do
          begin
            FRoutes[i].ID        := aRoutes[i].ID;
            FRoutes[i].SourceID  := aRoutes[i].GetSource.ID;
            FRoutes[i].SinkID    := aRoutes[i].GetSink.ID;
            FRoutes[i].RouteIntf := aRoutes[i];
            inc(i);
          end;
      end;
  finally
    PostMessage(Handle, MEDIA_ROUTER_REFRESH_ROUTES, 0, 0);
    FMediaCS.Release;
  end;
end;

procedure TMediaRouterFrm.GetDeviceInfoBtnClick(Sender: TObject);
begin
  try
    FMediaCS.Acquire;

    if (FSinks <> nil) and
       (SinksLbx.Items.Count > 0) and
       (SinksLbx.ItemIndex >= 0) and
       (SinksLbx.ItemIndex < length(FSinks)) then
      Chromium1.GetDeviceInfo(FSinks[SinksLbx.ItemIndex].SinkIntf);
  finally
    FMediaCS.Release;
  end;
end;

procedure TMediaRouterFrm.UpdateAvailableSinks;
var
  i : integer;
  TempItem : string;
begin
  try
    FMediaCS.Acquire;
    SinksLbx.Items.Clear;

    if (FSinks <> nil) then
      begin
        i := 0;
        while (i < length(FSinks)) do
          begin
            TempItem := FSinks[i].Name;

            case FSinks[i].SinkType of
              mtCast : TempItem := TempItem + ' (CAST';
              mtDial : TempItem := TempItem + ' (DIAL';
              else     TempItem := TempItem + ' (UNKNOWN';
            end;

            case FSinks[i].IconType of
              CEF_MSIT_CAST             : TempItem := TempItem + ', CAST)';
              CEF_MSIT_CAST_AUDIO_GROUP : TempItem := TempItem + ', CAST_AUDIO_GROUP)';
              CEF_MSIT_CAST_AUDIO       : TempItem := TempItem + ', CAST_AUDIO)';
              CEF_MSIT_MEETING          : TempItem := TempItem + ', MEETING)';
              CEF_MSIT_HANGOUT          : TempItem := TempItem + ', HANGOUT)';
              CEF_MSIT_EDUCATION        : TempItem := TempItem + ', EDUCATION)';
              CEF_MSIT_WIRED_DISPLAY    : TempItem := TempItem + ', WIRED_DISPLAY)';
              CEF_MSIT_GENERIC          : TempItem := TempItem + ', GENERIC)';
              else                        TempItem := TempItem + ', UNKNOWN)';
            end;

            SinksLbx.Items.Add(TempItem);
            inc(i);
          end;
      end;
  finally
    FMediaCS.Release;
  end;
end;

procedure TMediaRouterFrm.UpdateAvailableRoutes;
var
  i : integer;
begin
  try
    FMediaCS.Acquire;
    RoutesLbx.Items.Clear;

    if (FRoutes <> nil) then
      begin
        i := 0;
        while (i < length(FRoutes)) do
          begin
            RoutesLbx.Items.Add('ID : ' + quotedstr(FRoutes[i].ID));
            inc(i);
          end;
      end;
  finally
    FMediaCS.Release;
  end;
end;

procedure TMediaRouterFrm.UpdateButtons;
begin
  TerminateRouteBtn.Enabled := (RoutesLbx.ItemIndex >= 0) and
                               (RoutesLbx.Items.Count > 0);

  SendMsgBtn.Enabled := TerminateRouteBtn.Enabled and
                        (length(trim(MessageMem.Lines.Text)) > 0);

  GetDeviceInfoBtn.Enabled := (SinksLbx.ItemIndex >= 0) and
                              (SinksLbx.Items.Count > 0);

  CreateRouteBtn.Enabled := not(TerminateRouteBtn.Enabled) and
                            GetDeviceInfoBtn.Enabled and
                            (length(trim(SourceURNCbx.Text)) > 0);
end;

procedure TMediaRouterFrm.AddLogEntry(const aMessage1, aMessage2 : string; aRec : boolean);
begin
  try
    FMediaCS.Acquire;

    if aRec then
      FLog.Add('Message received ' + dateTimeToStr(now))
     else
      FLog.Add('Message sent ' + dateTimeToStr(now));

    FLog.Add(aMessage1);
    if (length(aMessage2) > 0) then FLog.Add(aMessage2);
    FLog.Add('------------------------------------------');
  finally
    PostMessage(Handle, MEDIA_ROUTER_PENDING_LOG_LINES, 0, 0);
    FMediaCS.Release;
  end;
end;

procedure TMediaRouterFrm.AddLogEntry(const aMessage1, aMessage2 : string);
begin
  try
    FMediaCS.Acquire;
    FLog.Add(aMessage1);
    if (length(aMessage2) > 0) then FLog.Add(aMessage2);
    FLog.Add('------------------------------------------');
  finally
    PostMessage(Handle, MEDIA_ROUTER_PENDING_LOG_LINES, 0, 0);
    FMediaCS.Release;
  end;
end;

end.
