Add call recording integration
This commit is contained in:
1 parent
ce9e6816f8
commit
b081ab275b
15 files changed
+1058
-19
No files matched your search
@@ -0,0 +1,184 @@
|
||||
using System.Buffers.Binary;
|
||||
using IpPbx.CLMgrLib;
|
||||
using SwyxTray.Recording;
|
||||
using SwyxTray.Swyx;
|
||||
|
||||
internal static class OutboxChecks
|
||||
{
|
||||
public static void Run(Action<bool, string> check)
|
||||
{
|
||||
var temp = Path.Combine(Path.GetTempPath(), "swyx-outbox-check-" + Guid.NewGuid());
|
||||
Directory.CreateDirectory(temp);
|
||||
try
|
||||
{
|
||||
for (int scenario = 0; scenario < 7; scenario++)
|
||||
{
|
||||
var folder = Path.Combine(temp, scenario.ToString());
|
||||
Directory.CreateDirectory(folder);
|
||||
var queue = Path.Combine(folder, "queue");
|
||||
var now = DateTime.UtcNow;
|
||||
var wav = Path.Combine(folder, "conversation.wav");
|
||||
WriteWav(Path.Combine(folder, "old.wav"), now.AddDays(-1));
|
||||
using var box = new RecordingOutbox(queue, () => now);
|
||||
var id = box.Begin(1, 41, "123", "Test", folder, null);
|
||||
check(box.Begin(1, 41, "123", "Test", folder, Guid.NewGuid()) == id, "duplicate start keeps identity");
|
||||
WriteWav(wav, now);
|
||||
box.Process();
|
||||
check(box.Notices().Length == 0, "no transfer during recording");
|
||||
box.Observe(SwyxSnapshot.Offline);
|
||||
check(box.CurrentId(1, 41) == id, "disconnect does not end recording");
|
||||
if (scenario == 1) WriteWav(Path.Combine(folder, "ambiguous.wav"), now);
|
||||
if (scenario == 2) File.Delete(wav);
|
||||
if (scenario == 3)
|
||||
{
|
||||
// Overlapping recording windows cannot claim the same WAV.
|
||||
File.Delete(wav);
|
||||
box.Begin(2, 42, "456", "Other", folder, null);
|
||||
WriteWav(wav, now);
|
||||
box.End(2, 42);
|
||||
}
|
||||
box.Observe(Snapshot(CLMgrLineStates.LSOnHold));
|
||||
check(box.CurrentId(1, 41) == id, "hold does not end recording");
|
||||
box.Observe(Snapshot(CLMgrLineStates.LSInactive));
|
||||
check(box.CurrentId(1, 41) == null, "call end finishes recording section");
|
||||
now = now.AddSeconds(4);
|
||||
box.Process();
|
||||
if (scenario == 1 || scenario == 3)
|
||||
{
|
||||
check(box.Notices().First(n => n.RecordingId == id).State == "error", "ambiguous file is never offered");
|
||||
continue;
|
||||
}
|
||||
if (scenario == 2)
|
||||
{
|
||||
now = now.AddMinutes(3); box.Process();
|
||||
check(box.Notices()[0].State == "error", "missing new WAV does not select old file");
|
||||
box.Acknowledge(id, new string('a', 32));
|
||||
check(box.Notices()[0].State == "submitted", "manual fallback receipt persists too");
|
||||
continue;
|
||||
}
|
||||
now = now.AddSeconds(3);
|
||||
if (scenario == 4)
|
||||
{
|
||||
using (var writer = new FileStream(wav, FileMode.Open, FileAccess.Write, FileShare.Read))
|
||||
{
|
||||
box.Process();
|
||||
check(box.Notices()[0].State == "waiting", "writer must close before ready");
|
||||
}
|
||||
}
|
||||
if (scenario == 5)
|
||||
{
|
||||
var content = File.ReadAllBytes(wav);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(content.AsSpan(4), (uint)content.Length + 100);
|
||||
File.WriteAllBytes(wav, content);
|
||||
box.Process(); now = now.AddSeconds(3); box.Process();
|
||||
check(box.Notices()[0].State == "waiting", "unfinished RIFF header waits");
|
||||
WriteWav(wav, now.AddSeconds(-7));
|
||||
box.Process(); now = now.AddSeconds(3);
|
||||
}
|
||||
if (scenario == 6)
|
||||
{
|
||||
using var restarted = new RecordingOutbox(queue, () => now);
|
||||
restarted.Process(); now = now.AddSeconds(3); restarted.Process();
|
||||
check(restarted.Notices()[0].State == "ready", "pending section survives tray restart");
|
||||
continue;
|
||||
}
|
||||
box.Process();
|
||||
check(box.Notices()[0].State == "ready", "stable closed WAV becomes ready");
|
||||
using (var input = box.Open(id)) check(input.Length == 2044, "copy contains complete audio");
|
||||
using (var reloaded = new RecordingOutbox(queue, () => now))
|
||||
check(reloaded.Notices()[0].State == "ready", "ready replays after restart");
|
||||
box.Acknowledge(id, new string('a', 32));
|
||||
box.Acknowledge(id, new string('a', 32));
|
||||
using var received = new RecordingOutbox(queue, () => now);
|
||||
check(received.Notices()[0].JobId == new string('a', 32), "durable idempotent receipt");
|
||||
check(File.Exists(wav) && !File.Exists(Path.Combine(queue, id + ".wav")), "remove only acknowledged temporary copy");
|
||||
}
|
||||
// A call can end outside SwyxWeb. No recording.stop/hangup command
|
||||
// is needed: the same outbox must produce a downloadable WAV.
|
||||
var externalEnds = new[] {
|
||||
("remote", CLMgrLineStates.LSTerminated, 41),
|
||||
("local", CLMgrLineStates.LSInactive, 41),
|
||||
("disabled", CLMgrLineStates.LSDisabled, 41),
|
||||
("none", CLMgrLineStates.LSNone, 41),
|
||||
("replaced", CLMgrLineStates.LSRinging, 42),
|
||||
("reconnected", CLMgrLineStates.LSInactive, 41)
|
||||
};
|
||||
foreach (var (reason, state, callId) in externalEnds)
|
||||
{
|
||||
var folder = Path.Combine(temp, "external-" + reason);
|
||||
Directory.CreateDirectory(folder);
|
||||
var now = DateTime.UtcNow;
|
||||
using var box = new RecordingOutbox(Path.Combine(folder, "queue"), () => now);
|
||||
var id = box.Begin(1, 41, "123", "Test", folder, null);
|
||||
WriteWav(Path.Combine(folder, "call.wav"), now);
|
||||
box.Observe(Snapshot(CLMgrLineStates.LSActive));
|
||||
if (reason == "reconnected")
|
||||
{
|
||||
box.Observe(SwyxSnapshot.Offline);
|
||||
check(box.Notices().Length == 0, "connection loss alone does not release audio");
|
||||
}
|
||||
box.Observe(Snapshot(state, callId));
|
||||
now = now.AddSeconds(4); box.Process(); now = now.AddSeconds(3); box.Process();
|
||||
check(box.Notices().Single() is { State: "ready", CallEnded: true },
|
||||
"automatic handoff without local hangup: " + reason);
|
||||
using var audio = box.Open(id);
|
||||
check(audio.Length == 2044, "complete WAV after external end: " + reason);
|
||||
}
|
||||
var sequential = Path.Combine(temp, "sequential");
|
||||
Directory.CreateDirectory(sequential);
|
||||
var clock = DateTime.UtcNow;
|
||||
using var sections = new RecordingOutbox(Path.Combine(sequential, "queue"), () => clock);
|
||||
sections.Begin(1, 88, "123", "Test", sequential, null);
|
||||
WriteWav(Path.Combine(sequential, "first.wav"), clock);
|
||||
sections.End(1, 88);
|
||||
clock = clock.AddMilliseconds(100);
|
||||
sections.Begin(1, 88, "123", "Test", sequential, null);
|
||||
WriteWav(Path.Combine(sequential, "second.wav"), clock);
|
||||
sections.End(1, 88);
|
||||
clock = clock.AddSeconds(4); sections.Process();
|
||||
clock = clock.AddSeconds(3); sections.Process();
|
||||
check(sections.Notices().Length == 2 && sections.Notices().All(n => n.State == "waiting_call_end" && !n.CallEnded),
|
||||
"all stopped recording sections wait for hangup");
|
||||
sections.Observe(Snapshot(CLMgrLineStates.LSOnHold, 88));
|
||||
sections.Observe(SwyxSnapshot.Offline);
|
||||
check(sections.Notices().All(n => n.State == "waiting_call_end"), "hold/disconnect do not release stopped sections");
|
||||
using (var restarted = new RecordingOutbox(Path.Combine(sequential, "queue"), () => clock))
|
||||
{
|
||||
restarted.Observe(Snapshot(CLMgrLineStates.LSActive, 88));
|
||||
check(restarted.Notices().All(n => n.State == "waiting_call_end"), "stopped sections wait across restart");
|
||||
foreach (var pending in restarted.Notices())
|
||||
{
|
||||
bool refused = false;
|
||||
try { using var blockedInput = restarted.Open(pending.RecordingId); }
|
||||
catch (InvalidOperationException) { refused = true; }
|
||||
check(refused, "download denied before confirmed call end");
|
||||
refused = false;
|
||||
try { restarted.Acknowledge(pending.RecordingId, new string('b', 32)); }
|
||||
catch (InvalidOperationException) { refused = true; }
|
||||
check(refused, "receipt denied before confirmed call end");
|
||||
}
|
||||
restarted.Observe(Snapshot(CLMgrLineStates.LSInactive, 88));
|
||||
check(restarted.Notices().Length == 2 && restarted.Notices().All(n => n.State == "ready" && n.CallEnded),
|
||||
"hangup releases all finalized sections of the ended call");
|
||||
using var input = restarted.Open(restarted.Notices()[0].RecordingId);
|
||||
check(input.Length == 2044, "complete WAV available after hangup");
|
||||
}
|
||||
}
|
||||
finally { Directory.Delete(temp, true); }
|
||||
}
|
||||
|
||||
public static void WriteWav(string path, DateTime created, int samples = 1000)
|
||||
{
|
||||
using (var writer = new BinaryWriter(File.Create(path)))
|
||||
{
|
||||
writer.Write("RIFF"u8); writer.Write(36 + samples * 2); writer.Write("WAVEfmt "u8);
|
||||
writer.Write(16); writer.Write((short)1); writer.Write((short)1); writer.Write(16000);
|
||||
writer.Write(32000); writer.Write((short)2); writer.Write((short)16);
|
||||
writer.Write("data"u8); writer.Write(samples * 2); writer.Write(new byte[samples * 2]);
|
||||
}
|
||||
File.SetCreationTimeUtc(path, created);
|
||||
}
|
||||
|
||||
private static SwyxSnapshot Snapshot(CLMgrLineStates state, int callId = 41) =>
|
||||
new(true, true, "test", "test", [new SwyxLineInfo(0, state, "123", "Test", true, callId)]);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using IpPbx.CLMgrLib;
|
||||
using SwyxTray.Swyx;
|
||||
using SwyxTray.Web;
|
||||
|
||||
if (args.Contains("--serve")) { await TransportChecks.Serve(); return; }
|
||||
|
||||
int checks = 0;
|
||||
void Check(bool condition, string name) { if (!condition) throw new Exception(name); checks++; }
|
||||
void Reject(Action action, string name) {
|
||||
try { action(); } catch (InvalidOperationException) { checks++; return; }
|
||||
throw new Exception(name);
|
||||
}
|
||||
var recordings = new CallRecordingTracker();
|
||||
int invocations = 0;
|
||||
Action invoke = () => invocations++;
|
||||
Reject(() => recordings.Set(0, 41, 42, true, false, true, invoke), "stale call");
|
||||
Reject(() => recordings.Set(0, 41, 41, false, false, true, invoke), "ringing");
|
||||
Reject(() => recordings.Set(0, 41, 41, false, true, true, invoke), "held start");
|
||||
Check(invocations == 0, "invalid calls do not reach SDK");
|
||||
recordings.Set(0, 41, 41, true, false, true, invoke);
|
||||
recordings.Set(0, 41, 41, true, false, true, invoke);
|
||||
Check(invocations == 1 && recordings.IsRequested(0, 41), "idempotent start");
|
||||
recordings.Set(0, 41, 41, false, true, false, invoke);
|
||||
Check(invocations == 2 && !recordings.IsRequested(0, 41), "stop while held");
|
||||
Reject(() => recordings.Set(0, 41, 41, true, false, true, () => throw new InvalidOperationException()), "SDK error");
|
||||
Check(!recordings.IsRequested(0, 41), "failed start not reported as success");
|
||||
recordings.Set(0, 41, 41, true, false, true, invoke);
|
||||
Reject(() => recordings.Set(0, 41, 41, true, false, false, () => throw new InvalidOperationException()), "stop error");
|
||||
Check(recordings.IsRequested(0, 41), "failed stop retains request state");
|
||||
recordings.Retain(new[] { (0, 42) });
|
||||
Check(!recordings.IsRequested(0, 41), "call ended clears request state");
|
||||
recordings.Set(0, 42, 42, true, false, false, invoke);
|
||||
Check(invocations == 4, "stop external recording reaches SDK too");
|
||||
|
||||
SwyxSnapshot Snapshot(CLMgrLineStates state, int id = 41, bool recording = false) =>
|
||||
new(true, true, "test", "test", new[] { new SwyxLineInfo(0, state, "123", "Test", true, id, recording) });
|
||||
var active = Snapshot(CLMgrLineStates.LSActive, recording: true);
|
||||
var end = CallEventMessage.Diff(active, Snapshot(CLMgrLineStates.LSInactive));
|
||||
Check(end.Count == 1 && end[0].Event == "ended" && end[0].CallId == 41 && end[0].PeerNumber == "123", "end preserves call identity");
|
||||
Check(CallEventMessage.Diff(active, SwyxSnapshot.Offline).Count == 0, "disconnect is not call end");
|
||||
Check(CallEventMessage.Diff(active, active with { IsServerUp = false }).Count == 0, "server down is not call end");
|
||||
Check(CallEventMessage.Diff(active, Snapshot(CLMgrLineStates.LSOnHold)).Count == 0, "holding is not call end");
|
||||
Check(CallEventMessage.Diff(Snapshot(CLMgrLineStates.LSOnHold), active).Count == 0, "resume is not new call");
|
||||
var replaced = CallEventMessage.Diff(active, Snapshot(CLMgrLineStates.LSRinging, 42));
|
||||
Check(replaced.Count == 2 && replaced.Any(e => e.Event == "ended" && e.CallId == 41)
|
||||
&& replaced.Any(e => e.Event == "incoming" && e.CallId == 42), "reused line keeps calls distinct");
|
||||
var json = Protocol.Serialize(SnapshotMessage.From(active));
|
||||
Check(json.Contains("\"callId\":41") && json.Contains("\"recordingRequested\":true"), "wire fields");
|
||||
OutboxChecks.Run(Check);
|
||||
await TransportChecks.Run(Check);
|
||||
Console.WriteLine($"{checks} recording/protocol checks passed (no COM calls).");
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Platform>x64</Platform>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swyx.Client.ClmgrAPI" Version="14.21.0" />
|
||||
<Compile Include="../../src/SwyxTray/Swyx/CallRecordingTracker.cs" Link="CallRecordingTracker.cs" />
|
||||
<Compile Include="../../src/SwyxTray/Swyx/SwyxModels.cs" Link="SwyxModels.cs" />
|
||||
<Compile Include="../../src/SwyxTray/Swyx/LineStateExtensions.cs" Link="LineStateExtensions.cs" />
|
||||
<Compile Include="../../src/SwyxTray/Web/Protocol.cs" Link="Protocol.cs" />
|
||||
<Compile Include="../../src/SwyxTray/Recording/RecordingOutbox.cs" Link="RecordingOutbox.cs" />
|
||||
<Compile Include="../../src/SwyxTray/Log.cs" Link="Log.cs" />
|
||||
<Compile Include="../../src/SwyxTray/Web/WebSocketSession.cs" Link="WebSocketSession.cs" />
|
||||
<Compile Include="../../src/SwyxTray/Web/WebSocketConfig.cs" Link="WebSocketConfig.cs" />
|
||||
<Compile Include="../../src/SwyxTray/Web/WebSocketHandshake.cs" Link="WebSocketHandshake.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,154 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using SwyxTray.Recording;
|
||||
using SwyxTray.Web;
|
||||
|
||||
internal static class TransportChecks
|
||||
{
|
||||
// Local-only fixture for browser -> real SwyxWeb -> isolated SwyxTrans integration checks.
|
||||
public static async Task Serve()
|
||||
{
|
||||
var folder = Path.Combine(Path.GetTempPath(), "swyx-browser-check-" + Guid.NewGuid());
|
||||
Directory.CreateDirectory(folder);
|
||||
var now = DateTime.UtcNow;
|
||||
using var outbox = new RecordingOutbox(Path.Combine(folder, "queue"), () => now);
|
||||
var id = outbox.Begin(1, 123, "123", "WebSocket-Testgespräch", folder, null);
|
||||
OutboxChecks.WriteWav(Path.Combine(folder, "call.wav"), now, 32_000);
|
||||
outbox.Observe(new SwyxTray.Swyx.SwyxSnapshot(true, true, "test", "test",
|
||||
[new SwyxTray.Swyx.SwyxLineInfo(0, IpPbx.CLMgrLib.CLMgrLineStates.LSInactive, "123", "Test", true, 123)]));
|
||||
now = now.AddSeconds(4); outbox.Process(); now = now.AddSeconds(3); outbox.Process();
|
||||
using var listener = new TcpListener(IPAddress.Loopback, 17658);
|
||||
listener.Start();
|
||||
Console.WriteLine("Synthetic WAV fixture ready: " + folder);
|
||||
while (true)
|
||||
{
|
||||
var tcp = await listener.AcceptTcpClientAsync();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
using (tcp)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stream = tcp.GetStream();
|
||||
var request = await WebSocketHandshake.ReadAsync(stream, CancellationToken.None);
|
||||
if (request == null) return;
|
||||
await WebSocketHandshake.AcceptAsync(stream, request, CancellationToken.None);
|
||||
using var session = new WebSocketSession(1, WebSocket.CreateFromStream(stream, true, null,
|
||||
TimeSpan.FromSeconds(15)), new CommandExecutor(outbox), new PluginTabChannel(), request.Origin, false);
|
||||
await session.RunAsync(CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine(ex.GetType().Name); }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task Run(Action<bool, string> check)
|
||||
{
|
||||
var folder = Path.Combine(Path.GetTempPath(), "swyx-transport-" + Guid.NewGuid());
|
||||
Directory.CreateDirectory(folder);
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
using var outbox = new RecordingOutbox(Path.Combine(folder, "queue"), () => now);
|
||||
var id = outbox.Begin(1, 123, "123", "Test", folder, null);
|
||||
var path = Path.Combine(folder, "call.wav");
|
||||
OutboxChecks.WriteWav(path, now, samples: 1_000_000);
|
||||
outbox.Observe(new SwyxTray.Swyx.SwyxSnapshot(true, true, "test", "test",
|
||||
[new SwyxTray.Swyx.SwyxLineInfo(0, IpPbx.CLMgrLib.CLMgrLineStates.LSInactive, "123", "Test", true, 123)]));
|
||||
now = now.AddSeconds(4); outbox.Process(); now = now.AddSeconds(3); outbox.Process();
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
for (int round = 0; round < 3; round++)
|
||||
{
|
||||
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
using var tcp = new TcpClient();
|
||||
var connection = tcp.ConnectAsync((IPEndPoint)listener.LocalEndpoint);
|
||||
using var remote = await listener.AcceptTcpClientAsync(timeout.Token);
|
||||
await connection;
|
||||
using var socket = WebSocket.CreateFromStream(tcp.GetStream(), false, null, TimeSpan.FromSeconds(15));
|
||||
using var server = new WebSocketSession(round, WebSocket.CreateFromStream(remote.GetStream(), true, null,
|
||||
TimeSpan.FromSeconds(15)), new CommandExecutor(outbox), new PluginTabChannel(), "http://test", false);
|
||||
var running = server.RunAsync(timeout.Token);
|
||||
var notice = await Until(socket, e => e.TryGetProperty("recordingId", out _), timeout.Token);
|
||||
check(notice.GetProperty("recordingId").GetGuid() == id, "recording replay uses original UUID");
|
||||
if (round == 2)
|
||||
{
|
||||
check(notice.GetProperty("state").GetString() == "submitted", "receipt replay on reconnect");
|
||||
}
|
||||
else
|
||||
{
|
||||
await Send(socket, new { id = 7, cmd = "recording.download", recordingId = id }, timeout.Token);
|
||||
await Send(socket, new { id = 8, cmd = "ping" }, timeout.Token);
|
||||
using var received = new MemoryStream();
|
||||
bool done = false, ping = false;
|
||||
while (!done || !ping)
|
||||
{
|
||||
var (type, bytes) = await Receive(socket, timeout.Token);
|
||||
if (type == WebSocketMessageType.Binary)
|
||||
{
|
||||
check(bytes.AsSpan(0, 4).SequenceEqual("SWYX"u8), "binary block magic");
|
||||
check(BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4)) == 7, "binary request id");
|
||||
check(BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(8)) == received.Length, "binary offset");
|
||||
received.Write(bytes, 12, bytes.Length - 12);
|
||||
if (round == 0) break; // Interrupt, then request the same retained WAV on another session.
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = JsonDocument.Parse(bytes).RootElement;
|
||||
if (result.TryGetProperty("id", out var requestId))
|
||||
{
|
||||
check(result.GetProperty("ok").GetBoolean(), "transfer/control command succeeds");
|
||||
if (requestId.GetInt32() == 7) done = true;
|
||||
if (requestId.GetInt32() == 8) ping = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (round == 1)
|
||||
{
|
||||
check(received.ToArray().SequenceEqual(File.ReadAllBytes(path)), "actual WebSocket preserves entire WAV");
|
||||
check(ping, "control commands remain responsive during transfer");
|
||||
await Send(socket, new { id = 9, cmd = "recording.ack", recordingId = id, jobId = new string('a', 32) }, timeout.Token);
|
||||
var ack = await Until(socket, e => e.TryGetProperty("id", out var n) && n.GetInt32() == 9, timeout.Token);
|
||||
check(ack.GetProperty("ok").GetBoolean(), "transfer receipt accepted");
|
||||
}
|
||||
}
|
||||
socket.Abort(); server.Dispose();
|
||||
try { await running; } catch (Exception ex) when (ex is WebSocketException or IOException or ObjectDisposedException) { }
|
||||
}
|
||||
check(File.Exists(path), "original WAV survives websocket acknowledgements");
|
||||
}
|
||||
finally { Directory.Delete(folder, true); }
|
||||
}
|
||||
|
||||
private static Task Send(WebSocket socket, object value, CancellationToken token) =>
|
||||
socket.SendAsync(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value)), WebSocketMessageType.Text, true, token);
|
||||
|
||||
private static async Task<JsonElement> Until(WebSocket socket, Func<JsonElement, bool> predicate, CancellationToken token)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var (type, bytes) = await Receive(socket, token);
|
||||
if (type != WebSocketMessageType.Text) continue;
|
||||
var element = JsonDocument.Parse(bytes).RootElement;
|
||||
if (predicate(element)) return element;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<(WebSocketMessageType, byte[])> Receive(WebSocket socket, CancellationToken token)
|
||||
{
|
||||
using var message = new MemoryStream();
|
||||
var buffer = new byte[32 * 1024];
|
||||
WebSocketReceiveResult result;
|
||||
do
|
||||
{
|
||||
result = await socket.ReceiveAsync(buffer, token);
|
||||
message.Write(buffer, 0, result.Count);
|
||||
} while (!result.EndOfMessage);
|
||||
return (result.MessageType, message.ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Only the COM executor and browser-plugin channel are replaced. The outbox,
|
||||
// session, framing, cancellation and persistence are production code.
|
||||
using SwyxTray.Recording;
|
||||
using SwyxTray.Swyx;
|
||||
namespace SwyxTray.Web;
|
||||
internal sealed class CommandExecutor(RecordingOutbox outbox)
|
||||
{
|
||||
public RecordingOutbox Outbox => outbox;
|
||||
public Task<SwyxSnapshot> GetSnapshotAsync(CancellationToken _) => Task.FromResult(SwyxSnapshot.Offline);
|
||||
public Task<IReadOnlyList<SwyxContact>> GetAddressesAsync(CancellationToken _) => Task.FromResult<IReadOnlyList<SwyxContact>>([]);
|
||||
public Task<ResultMessage> ExecuteAsync(ClientCommand command, CancellationToken _)
|
||||
{
|
||||
if (command.Cmd == "recording.ack") Outbox.Acknowledge(command.RecordingId!.Value, command.JobId);
|
||||
return Task.FromResult(new ResultMessage(command.Id, true));
|
||||
}
|
||||
}
|
||||
internal sealed class PluginTabChannel { public bool TryComplete(ClientCommand _) => false; }
|
||||
Reference in new issue
Block a user