feat(android): share podcast playback and foreground broadcasting
Add native Camera2 capture, MPEG-TS segmentation and foreground service with notification stop. Reuse shared authenticated uploads and replay finalization, add capture regression tests and document platform limitations. Update Avalonia to 12.1.4. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
2dcbcd35b3
commit
ad92744a94
24 changed files with 1495 additions and 70 deletions
146
src/PostIt/PostIt.Tests/TransportStreamCaptureTests.cs
Normal file
146
src/PostIt/PostIt.Tests/TransportStreamCaptureTests.cs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
using PostIt.Services;
|
||||
|
||||
namespace PostIt.Tests;
|
||||
|
||||
public sealed class TransportStreamCaptureTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Segments_on_IDR_and_flushes_final_segment_with_real_PTS_durations()
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), $"postit-ts-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(directory);
|
||||
try
|
||||
{
|
||||
using var input = new MemoryStream();
|
||||
input.Write(Pat());
|
||||
input.Write(Pmt());
|
||||
for (var i = 0; i <= 120; i++) input.Write(Video(i * 3000L, i % 60 == 0));
|
||||
input.Position = 0;
|
||||
await new TransportStreamCapture(directory).RunAsync(input, TestContext.Current.CancellationToken);
|
||||
var segments = HlsCapturePlaylist.Parse(await File.ReadAllTextAsync(Path.Combine(directory, "index.m3u8"), TestContext.Current.CancellationToken));
|
||||
Assert.Equal(3, segments.Count);
|
||||
Assert.Equal(2, segments[0].DurationSeconds);
|
||||
Assert.Equal(2, segments[1].DurationSeconds);
|
||||
Assert.InRange(segments[2].DurationSeconds, 0.033, 0.034);
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
var bytes = await File.ReadAllBytesAsync(Path.Combine(directory, segment.FileName), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(0, bytes.Length % 188);
|
||||
Assert.Equal(Pat(), bytes[..188]);
|
||||
Assert.Equal(Pmt(), bytes[188..376]);
|
||||
Assert.Contains((byte)0x65, bytes[376..]);
|
||||
}
|
||||
}
|
||||
finally { Directory.Delete(directory, recursive: true); }
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task Rejects_corrupt_or_truncated_transport_without_publishing_a_segment(bool truncated)
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), $"postit-ts-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(directory);
|
||||
try
|
||||
{
|
||||
using var input = new MemoryStream(new byte[truncated ? 187 : 188]);
|
||||
await Assert.ThrowsAsync<InvalidDataException>(() =>
|
||||
new TransportStreamCapture(directory).RunAsync(input, TestContext.Current.CancellationToken));
|
||||
Assert.False(File.Exists(Path.Combine(directory, "index.m3u8")));
|
||||
}
|
||||
finally { Directory.Delete(directory, recursive: true); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Preserves_out_of_band_H264_configuration_at_every_segment_start()
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), $"postit-ts-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(directory);
|
||||
try
|
||||
{
|
||||
using var input = new MemoryStream();
|
||||
input.Write(Pat());
|
||||
input.Write(Pmt());
|
||||
var configuration = Video(0, false);
|
||||
configuration[22] = 0x67;
|
||||
new byte[] { 0, 0, 0, 1, 0x68, 0x80 }.CopyTo(configuration, 25);
|
||||
input.Write(configuration);
|
||||
for (var i = 0; i <= 120; i++) input.Write(Video(90000 + i * 3000L, i % 60 == 0));
|
||||
input.Position = 0;
|
||||
await new TransportStreamCapture(directory).RunAsync(input, TestContext.Current.CancellationToken);
|
||||
var segments = HlsCapturePlaylist.Parse(await File.ReadAllTextAsync(Path.Combine(directory, "index.m3u8"), TestContext.Current.CancellationToken));
|
||||
Assert.Equal(3, segments.Count);
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
var bytes = await File.ReadAllBytesAsync(Path.Combine(directory, segment.FileName), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(0x67, bytes[376 + 22]);
|
||||
Assert.Equal(0x68, bytes[376 + 29]);
|
||||
Assert.Equal(0x65, bytes[564 + 22]);
|
||||
Assert.Equal((bytes[564 + 3] - 1) & 0x0f, bytes[376 + 3] & 0x0f);
|
||||
Assert.Equal(bytes[(564 + 13)..(564 + 18)], bytes[(376 + 13)..(376 + 18)]);
|
||||
}
|
||||
}
|
||||
finally { Directory.Delete(directory, recursive: true); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handles_33_bit_PTS_wrap_without_negative_duration()
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), $"postit-ts-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(directory);
|
||||
try
|
||||
{
|
||||
using var input = new MemoryStream();
|
||||
input.Write(Pat());
|
||||
input.Write(Pmt());
|
||||
for (var i = 0; i <= 120; i++)
|
||||
input.Write(Video(((1L << 33) - 90000 + i * 3000L) & ((1L << 33) - 1), i % 60 == 0));
|
||||
input.Position = 0;
|
||||
await new TransportStreamCapture(directory).RunAsync(input, TestContext.Current.CancellationToken);
|
||||
var segments = HlsCapturePlaylist.Parse(await File.ReadAllTextAsync(Path.Combine(directory, "index.m3u8"), TestContext.Current.CancellationToken));
|
||||
Assert.Equal(3, segments.Count);
|
||||
Assert.Equal(2, segments[0].DurationSeconds);
|
||||
}
|
||||
finally { Directory.Delete(directory, recursive: true); }
|
||||
}
|
||||
|
||||
private static byte[] Packet(int pid)
|
||||
{
|
||||
var packet = Enumerable.Repeat((byte)0xff, 188).ToArray();
|
||||
packet[0] = 0x47;
|
||||
packet[1] = (byte)(0x40 | (pid >> 8));
|
||||
packet[2] = (byte)pid;
|
||||
packet[3] = 0x10;
|
||||
return packet;
|
||||
}
|
||||
|
||||
private static byte[] Pat()
|
||||
{
|
||||
var packet = Packet(0);
|
||||
packet[4] = 0;
|
||||
new byte[] { 0, 0xb0, 13, 0, 1, 0xc1, 0, 0, 0, 1, 0xe1, 0 }.CopyTo(packet, 5);
|
||||
return packet;
|
||||
}
|
||||
|
||||
private static byte[] Pmt()
|
||||
{
|
||||
var packet = Packet(256);
|
||||
packet[4] = 0;
|
||||
new byte[] { 2, 0xb0, 18, 0, 1, 0xc1, 0, 0, 0xe1, 1, 0xf0, 0,
|
||||
0x1b, 0xe1, 1, 0xf0, 0 }.CopyTo(packet, 5);
|
||||
return packet;
|
||||
}
|
||||
|
||||
private static byte[] Video(long pts, bool idr)
|
||||
{
|
||||
var packet = Packet(257);
|
||||
new byte[] { 0, 0, 1, 0xe0, 0, 0, 0x84, 0x80, 5 }.CopyTo(packet, 4);
|
||||
packet[13] = (byte)(0x21 | ((pts >> 29) & 0xe));
|
||||
packet[14] = (byte)(pts >> 22);
|
||||
packet[15] = (byte)(1 | ((pts >> 14) & 0xfe));
|
||||
packet[16] = (byte)(pts >> 7);
|
||||
packet[17] = (byte)(1 | ((pts << 1) & 0xfe));
|
||||
new byte[] { 0, 0, 0, 1, idr ? (byte)0x65 : (byte)0x41, 0x88 }.CopyTo(packet, 18);
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue