Omar Gazali
Omar Gazali

Reputation: 1

Code on TCP doesn't work. What is my problem with live audio stream?

I use LiveAudioPlayer to stream audio over TCP with Indy. This is the code of the client:

function TForm1.LiveAudioPlayerDataPtr(Sender: TObject;
 var Buffer: Pointer; var NumLoops: DWORD;
 var FreeIt: Boolean): DWORD;
var
  buf:TIdBytes;
begin
  if not IdTCPClient1.Connected then
    Result := 0    // Stops LiveAudioPlayer
  else
    IdTCPClient1.Socket.ReadBytes(buf, sizeof(buf), TRUE);

  BytesToRaw(buf, WaveFormat, sizeof(WaveFormat));

  if AudioBuffer.Get(Buffer, Result) then
    FreeIt := True
  else
  begin
    Buffer := nil; // When Buffer is nil,
    Result := 10   // Result will be considered as silence milliseconds.
  end
end;

On the server side, I use LiveAudioRecorder:

procedure TForm2.IdTCPServer1Execute(AContext: TIdContext);
type
  TWaveFormatInfo = packed record
    WaveFormatSize: Integer;
    WaveFormat: TWaveFormatEx;
  end;
var
  WFI: TWaveFormatInfo;
  Buf: TIdBytes;
  Clients : TList;
  i: integer;
begin
  SetPCMAudioFormatS(@WFI.WaveFormat, LiveAudioRecorder.PCMFormat);
  WFI.WaveFormatSize := SizeOf(WFI.WaveFormat);
  Buf := RawToBytes(WFI, SizeOf(WFI));
  Clients := IdTCPServer1.Contexts.LockList;
  try
    for i := 0 to Clients.Count-1 do
    try
      TIdContext(Clients[i]).Connection.IOHandler.Write(buf);
    except
    end;
  finally
    IdTCPServer1.Contexts.UnlockList;
  end;

But I need some code on the server side here to work:

procedure TForm2.LiveAudioRecorderData(Sender: TObject;
  const Buffer: Pointer;
   BufferSize: Cardinal; var FreeIt: Boolean);
var
  I: Integer;
  Clients : TList;
begin
   FreeIt := True;
   //** here
end;

How do I change from WinSock to Indy TCP to stream sound?

Upvotes: 0

Views: 148

Answers (1)

Remy Lebeau
Remy Lebeau

Reputation: 597036

The code you currently have in IdTCPServer1Execute() really belongs in LiveAudioRecorderData() instead, where the data is.

Though, I strongly suggest NEVER broadcasting data to multiple TCP clients using an IOHandler.Write() loop, like you are doing. It slows down your bandwidth usage to just 1 packet at a time, there is no parallel processing at all. While you are sending a packet to 1 client, all of the other clients are blocked waiting to send their own packets.

A better solution is to give each client its own thread-safe queue for outbound data, then LiveAudioRecorderData() can push a copy of the data into each client's queue as needed, and IdTCPServer1Execute() can send the calling client's queue independently of what other clients are doing.

For example:

type
  TWaveFormatInfo = packed record
    WaveFormatSize: Integer;
    WaveFormat: TWaveFormatEx;
  end;

  TMyContext = class(TIdServerContext)
  private
    // on modern Delphi versions, consider using
    // TThreadList<TIdBytes> instead...
    Queue: TThreadList;
    QueueHasData: Boolean;
  public    
    constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdThreadList = nil); override;
    destructor Destroy; override;

    procedure AddToQueue(const WFI: TWaveFormatInfo; const Buffer: Pointer; BufferSize: Cardinal);
    procedure CheckQueue;
  end;

...

constructor TMyContext.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdThreadList = nil);
begin
  inherited;
  Queue := TThreadList.Create;
end;

destructor TMyContext.Destroy;
var
  List: TList;
  i: Integer;
begin
  // if not using TThreadList<TIdBytes>...
  if QueueHasData then
  begin
    List := Queue.LockList;
    try
      for i := 0 to List.Count-1 do
        TIdBytes(List[i]) := nil; // decrement the array's refcount
    finally
      Queue.UnlockList;
    end;
  end;
  // end if

  Queue.Free;
  inherited;
end;

procedure TMyContext.AddToQueue(const WFI: TWaveFormatInfo; const Buffer: Pointer; BufferSize: Cardinal);
begin
  Buf: TIdBytes;
  Offset: Integer;
  P: Pointer; // if not using TThreadList<TIdBytes>...
end;
  // each client needs its own local copy of the
  // input Buffer, so copy the data
  // and add it to the queue...
  Offset := Sizeof(Integer) + WFI.WaveFormatSize;
  SetLength(Buf, Offset + BufferSize);
  Move(WFI, Buf[0], Offset);
  if BufferSize > 0 then
    Move(Buffer^, Buf[Offset], BufferSize);

  // if using TThreadList<TIdBytes>...

  {with Queue.LockList do
  try
    Add(Buf);
    QueueHasData := True;
  finally
    Queue.UnlockList;
  end;}

  // else

  TIdBytes(P) := Buf;  // increment the array's refcount
  try
    with Queue.LockList do
    try
      Add(P);
      QueueHasData := True;
    finally
      Queue.UnlockList;
    end;
  except
    TIdBytes(P) := nil; // decrement the array's refcount
    raise;
  end;

  // end if

end;

procedure TMyContext.CheckQueue;
var
  List: TList;
  P: Pointer; // if not using TThreadList<TIdBytes>...
begin
  if QueueHasData then
  begin
    List := Queue.LockList;
    try
      while List.Count > 0 do
      begin
        // if using TThreadList<TIdBytes>... 

        {Connection.IOHandler.Write(List[0]);
        List.Delete(0);} 

        // else

        P := List[0];
        List.Delete(0);
        try
          Connection.IOHandler.Write(TIdBytes(P));
        finally
          TIdBytes(P) := nil; // decrement the array's refcount
        end;

        // end if
      end;
    finally
      QueueHasData := List.Count > 0;
      Queue.UnlockList;
    end;
  end;
end;

private
  WFI: TWaveFormatInfo;

...

procedure TForm2.FormCreate(Sender: TObject);
begin
  SetPCMAudioFormatS(@WFI.WaveFormat, LiveAudioRecorder.PCMFormat);
  WFI.WaveFormatSize := SizeOf(WFI.WaveFormat);
  IdTCPServer1.ContextClass := TMyContext;
end;

procedure TForm2.IdTCPServer1Execute(AContext: TIdContext);
begin
  TMyContext(AContext).CheckQueue;  
  with AContext.Connection.IOHandler do
  begin
    if CheckForDataOnSource(0) then
      InputBuffer.Clear;
    CheckForDisconnect;
  end;
end;

procedure TForm2.LiveAudioRecorderData(Sender: TObject; const Buffer: Pointer; BufferSize: Cardinal; var FreeIt: Boolean);
var
  Clients : TList;
  i: integer;
begin
  FreeIt := True;
  Clients := IdTCPServer1.Contexts.LockList;
  try
    for i := 0 to Clients.Count-1 do
      TMyContext(TIdContext(Clients[i])).AddToQueue(WFI, Buffer, BufferSize);
  finally
    IdTCPServer1.Contexts.UnlockList;
  end;
end;

On the other hand, TCP is a really poor choice for broadcasting live media to multiple clients. You really should use UDP instead so that you can utilize subnet broadcasts or multicasts instead. That would allow your server code to send only 1 packet per block of audio data to a special broadcast/multicast IP address, and have that packet automatically delivered by the network to every client that is interested in it, rather then you doing the delivery yourself.

Upvotes: 1

Related Questions