script element packet spike (#2703)

* script element packet spike

* fixes
This commit is contained in:
Jason Dove
2025-12-04 15:40:18 -06:00
committed by GitHub
parent 7a263ddaed
commit a90fe26d50
5 changed files with 163 additions and 39 deletions
@@ -1,4 +1,5 @@
using System.Buffers;
using System.Buffers.Binary;
using System.IO.Pipelines;
using System.Text.Json;
using CliWrap;
@@ -13,6 +14,8 @@ namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public class ScriptElement(ScriptGraphicsElement scriptElement, ILogger logger)
: GraphicsElement, IDisposable
{
private const uint EtvGraphicsMagic = 0x47565445;
private CancellationTokenSource _cancellationTokenSource;
private CommandTask<CommandResult> _commandTask;
private int _frameSize;
@@ -20,6 +23,7 @@ public class ScriptElement(ScriptGraphicsElement scriptElement, ILogger logger)
private SKBitmap _canvasBitmap;
private TimeSpan _startTime;
private TimeSpan _endTime;
private int _repeatCount;
public void Dispose()
{
@@ -126,48 +130,146 @@ public class ScriptElement(ScriptGraphicsElement scriptElement, ILogger logger)
return Option<PreparedElementImage>.None;
}
while (true)
{
ReadResult readResult = await _pipeReader.ReadAsync(cancellationToken);
ReadOnlySequence<byte> buffer = readResult.Buffer;
SequencePosition consumed = buffer.Start;
SequencePosition examined = buffer.End;
try
{
if (buffer.Length >= _frameSize)
{
ReadOnlySequence<byte> sequence = buffer.Slice(0, _frameSize);
using (SKPixmap pixmap = _canvasBitmap.PeekPixels())
{
sequence.CopyTo(pixmap.GetPixelSpan());
}
// mark this frame as consumed
consumed = sequence.End;
// we are done, return the frame
return new PreparedElementImage(_canvasBitmap, SKPointI.Empty, 1.0f, ZIndex, false);
}
if (readResult.IsCompleted)
{
await _pipeReader.CompleteAsync();
return Option<PreparedElementImage>.None;
}
}
finally
{
// advance the reader, consuming the processed frame and examining the entire buffer
_pipeReader.AdvanceTo(consumed, examined);
}
}
return scriptElement.Format is ScriptGraphicsFormat.Raw
? await PrepareFromRaw(cancellationToken)
: await PrepareFromPacket(cancellationToken);
}
catch (TaskCanceledException)
{
return Option<PreparedElementImage>.None;
}
}
private async Task<Option<PreparedElementImage>> PrepareFromRaw(CancellationToken cancellationToken)
{
while (true)
{
ReadResult readResult = await _pipeReader.ReadAsync(cancellationToken);
ReadOnlySequence<byte> buffer = readResult.Buffer;
SequencePosition consumed = buffer.Start;
SequencePosition examined = buffer.End;
try
{
if (buffer.Length >= _frameSize)
{
ReadOnlySequence<byte> sequence = buffer.Slice(0, _frameSize);
using (SKPixmap pixmap = _canvasBitmap.PeekPixels())
{
sequence.CopyTo(pixmap.GetPixelSpan());
}
// mark this frame as consumed
consumed = sequence.End;
// we are done, return the frame
return new PreparedElementImage(_canvasBitmap, SKPointI.Empty, 1.0f, ZIndex, false);
}
if (readResult.IsCompleted)
{
await _pipeReader.CompleteAsync();
return Option<PreparedElementImage>.None;
}
}
finally
{
// advance the reader, consuming the processed frame and examining the entire buffer
_pipeReader.AdvanceTo(consumed, examined);
}
}
}
private async Task<Option<PreparedElementImage>> PrepareFromPacket(CancellationToken cancellationToken)
{
if (_repeatCount > 0 || await ReadPacket(cancellationToken))
{
if (_repeatCount > 0)
{
_repeatCount--;
}
return new PreparedElementImage(_canvasBitmap, SKPointI.Empty, 1.0f, ZIndex, false);
}
IsFinished = true;
await _pipeReader.CompleteAsync();
return Option<PreparedElementImage>.None;
}
private async Task<bool> ReadPacket(CancellationToken cancellationToken)
{
// need 11 bytes - 4 magic, 2 version, 1 packet type, 4 payload len
var result = await _pipeReader.ReadAtLeastAsync(11, cancellationToken);
ReadOnlySequence<byte> buffer = result.Buffer;
if (buffer.Length < 11)
{
return false;
}
Span<byte> headerBytes = stackalloc byte[11];
buffer.Slice(0, 11).CopyTo(headerBytes);
uint magic = BinaryPrimitives.ReadUInt32BigEndian(headerBytes[..4]);
ushort version = BinaryPrimitives.ReadUInt16BigEndian(headerBytes.Slice(4, 2));
byte type = headerBytes[6];
uint payloadLen = BinaryPrimitives.ReadUInt32BigEndian(headerBytes.Slice(7, 4));
if (magic != EtvGraphicsMagic || version != 1)
{
logger.LogWarning("Invalid graphics packet received: magic {Magic}, version {Version}", magic, version);
return false;
}
// consume header
_pipeReader.AdvanceTo(buffer.GetPosition(11));
var success = true;
if (payloadLen > 0)
{
result = await _pipeReader.ReadAtLeastAsync((int)payloadLen, cancellationToken);
buffer = result.Buffer;
switch (type)
{
case (byte)ScriptPayloadType.Full:
using (SKPixmap pixmap = _canvasBitmap.PeekPixels())
{
buffer.Slice(0, payloadLen).CopyTo(pixmap.GetPixelSpan());
}
break;
case (byte)ScriptPayloadType.Repeat:
Span<byte> repeatBytes = stackalloc byte[4];
buffer.Slice(0, 4).CopyTo(repeatBytes);
_repeatCount = (int)BinaryPrimitives.ReadUInt32BigEndian(repeatBytes);
break;
case (byte)ScriptPayloadType.Rectangles:
// TODO: support rectangles
logger.LogWarning("Unsupported graphics packet type: {Type}", type);
success = false;
break;
}
// consume payload
_pipeReader.AdvanceTo(buffer.GetPosition(payloadLen));
}
else
{
if (type == (byte)ScriptPayloadType.Clear)
{
_canvasBitmap.Erase(SKColors.Transparent);
}
else
{
logger.LogWarning("Unexpected zero-length payload for type {Type}", type);
success = false;
}
}
return success;
}
}