Browse Source

script element packet spike (#2703)

* script element packet spike

* fixes
pull/2704/head
Jason Dove 4 weeks ago committed by GitHub
parent
commit
a90fe26d50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 3
      ErsatzTV.Core/Graphics/ScriptGraphicsElement.cs
  3. 7
      ErsatzTV.Core/Graphics/ScriptGraphicsFormat.cs
  4. 162
      ErsatzTV.Infrastructure/Streaming/Graphics/Script/ScriptElement.cs
  5. 9
      ErsatzTV.Infrastructure/Streaming/Graphics/Script/ScriptPayloadType.cs

5
CHANGELOG.md

@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Graphics Engine:
- Add `script` graphics element type
- Supported in playback troubleshooting and all scheduling types
- Supports arbitrary scripts or executables that output BGRA data to stdout
- Supports arbitrary scripts or executables that output graphics to ETV via stdout
- Supports EPG and Media Item replacement in entire template
- EPG data is sourced from XMLTV for the current time
- EPG data can also load a configurable number of subsequent (up next) entries
@ -18,6 +18,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -18,6 +18,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Script and arguments (`command` and `args`)
- Draw order (`z_index`)
- Timing (`start_seconds` and `duration_seconds`)
- Data format (`format`)
- `raw` format means full frames of BGRA data to stdout
- `packet` format means ETV graphics packets to stdout
- Add framerate template data
- `RFrameRate` - the real content framerate (or channel normalized framerate) as reported by ffmpeg, e.g. `30000/1001`
- `FrameRate` - the decimal representation of `RFrameRate`, e.g. `29.97002997`

3
ErsatzTV.Core/Graphics/ScriptGraphicsElement.cs

@ -24,4 +24,7 @@ public class ScriptGraphicsElement : BaseGraphicsElement @@ -24,4 +24,7 @@ public class ScriptGraphicsElement : BaseGraphicsElement
[YamlMember(Alias = "pixel_format", ApplyNamingConventions = false)]
public string PixelFormat { get; set; }
[YamlMember(Alias = "format", ApplyNamingConventions = false)]
public ScriptGraphicsFormat Format { get; set; }
}

7
ErsatzTV.Core/Graphics/ScriptGraphicsFormat.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.Core.Graphics;
public enum ScriptGraphicsFormat
{
Raw = 0,
Packet = 1
}

162
ErsatzTV.Infrastructure/Streaming/Graphics/Script/ScriptElement.cs

@ -1,4 +1,5 @@ @@ -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; @@ -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) @@ -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) @@ -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);
return scriptElement.Format is ScriptGraphicsFormat.Raw
? await PrepareFromRaw(cancellationToken)
: await PrepareFromPacket(cancellationToken);
}
catch (TaskCanceledException)
{
return Option<PreparedElementImage>.None;
}
}
using (SKPixmap pixmap = _canvasBitmap.PeekPixels())
{
sequence.CopyTo(pixmap.GetPixelSpan());
}
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;
// mark this frame as consumed
consumed = sequence.End;
try
{
if (buffer.Length >= _frameSize)
{
ReadOnlySequence<byte> sequence = buffer.Slice(0, _frameSize);
// we are done, return the frame
return new PreparedElementImage(_canvasBitmap, SKPointI.Empty, 1.0f, ZIndex, false);
using (SKPixmap pixmap = _canvasBitmap.PeekPixels())
{
sequence.CopyTo(pixmap.GetPixelSpan());
}
if (readResult.IsCompleted)
{
await _pipeReader.CompleteAsync();
// mark this frame as consumed
consumed = sequence.End;
return Option<PreparedElementImage>.None;
}
// we are done, return the frame
return new PreparedElementImage(_canvasBitmap, SKPointI.Empty, 1.0f, ZIndex, false);
}
finally
if (readResult.IsCompleted)
{
// advance the reader, consuming the processed frame and examining the entire buffer
_pipeReader.AdvanceTo(consumed, examined);
await _pipeReader.CompleteAsync();
return Option<PreparedElementImage>.None;
}
}
finally
{
// advance the reader, consuming the processed frame and examining the entire buffer
_pipeReader.AdvanceTo(consumed, examined);
}
}
catch (TaskCanceledException)
}
private async Task<Option<PreparedElementImage>> PrepareFromPacket(CancellationToken cancellationToken)
{
if (_repeatCount > 0 || await ReadPacket(cancellationToken))
{
return Option<PreparedElementImage>.None;
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;
}
}

9
ErsatzTV.Infrastructure/Streaming/Graphics/Script/ScriptPayloadType.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public enum ScriptPayloadType
{
Full = 0,
Repeat = 1,
Clear = 2,
Rectangles = 3
}
Loading…
Cancel
Save