Files
curios/tools/harmony-stream.test.py
T

88 lines
3.9 KiB
Python

import importlib.util
from pathlib import Path
import re
import shutil
import socket
import struct
import subprocess
import time
import unittest
spec = importlib.util.spec_from_file_location("harmony_stream", Path(__file__).with_name("harmony-stream.py"))
stream = importlib.util.module_from_spec(spec)
spec.loader.exec_module(stream)
class StreamProtocolTests(unittest.TestCase):
def setUp(self):
self.sender, self.receiver = socket.socketpair()
self.deadline = time.monotonic() + 1
def tearDown(self):
self.sender.close()
self.receiver.close()
def test_negotiated_dimensions(self):
self.sender.sendall(struct.pack(">4s5I", b"HCS1", 566, 1280, 1260, 2844, 30))
hello = stream.read_hello(self.receiver, self.deadline)
self.assertEqual((hello["width"], hello["displayHeight"], hello["configuredFps"]), (566, 2844, 30))
def test_unknown_version_rejected(self):
self.sender.sendall(struct.pack(">4s5I", b"HCS2", 640, 480, 1280, 960, 30))
with self.assertRaises(ValueError):
stream.read_hello(self.receiver, self.deadline)
def test_dimensions_bounded_before_video_allocations(self):
self.sender.sendall(struct.pack(">4s5I", b"HCS1", 65536, 480, 1280, 960, 30))
with self.assertRaises(ValueError):
stream.read_hello(self.receiver, self.deadline)
def test_oversized_packet_rejected_without_reading_payload(self):
self.sender.sendall(struct.pack(">IIq", stream.MAX_PACKET + 1, 2, 0))
with self.assertRaises(ValueError):
stream.read_packet(self.receiver, self.deadline)
def test_disconnect_in_packet_is_not_a_frame(self):
self.sender.sendall(struct.pack(">IIq", 32, 2, 42) + b"\0\0\1\x65")
self.sender.shutdown(socket.SHUT_WR)
with self.assertRaises(EOFError):
stream.read_packet(self.receiver, self.deadline)
def test_silent_connection_has_bounded_wait(self):
with self.assertRaises(TimeoutError):
stream.read_packet(self.receiver, time.monotonic() + 0.02)
def test_non_h264_payload_rejected(self):
self.sender.sendall(struct.pack(">IIq", 8, 2, 42) + b"JPEGdata")
with self.assertRaises(ValueError):
stream.read_packet(self.receiver, self.deadline)
@unittest.skipUnless(shutil.which("ffmpeg"), "ffmpeg required for actual decoder validation")
def test_twenty_generated_frames_survive_transport_and_decode(self):
encoded = subprocess.run([
"ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i",
"testsrc2=size=160x96:rate=30", "-frames:v", "20", "-c:v", "libx264",
"-preset", "ultrafast", "-tune", "zerolatency", "-profile:v", "baseline",
"-g", "10", "-bf", "0", "-x264-params", "aud=1:repeat-headers=1",
"-f", "h264", "pipe:1"], capture_output=True, check=True, timeout=10).stdout
# Access unit delimiters separate independently timestamped encoder callbacks.
boundaries = [match.start() for match in re.finditer(b"\x00\x00\x00\x01\x09", encoded)]
self.assertEqual(len(boundaries), 20)
decoder = stream.Decoder(False)
try:
for index, (start, end) in enumerate(zip(boundaries, boundaries[1:] + [len(encoded)])):
data = encoded[start:end]
flags = 2 if index % 10 == 0 else 0
self.sender.sendall(struct.pack(">IIq", len(data), flags, index * 33333) + data)
actual_flags, pts, actual_data = stream.read_packet(self.receiver, time.monotonic() + 1)
self.assertEqual((actual_flags, pts), (flags, index * 33333))
decoder.write(actual_data)
finally:
decoder.close()
self.assertEqual(decoder.process.returncode, 0, decoder.error.decode())
self.assertEqual(decoder.frames, 20, decoder.error.decode())
if __name__ == "__main__":
unittest.main()