feat: add port_index and srt_destination_url to SCTE35 inject_marker

This commit is contained in:
Zac Gaetano 2026-04-14 10:01:57 -04:00
parent 5f2f61edfe
commit 9e9ed27c80

View file

@ -38,92 +38,61 @@ class SCTE35Manager:
""" """
Encode a SCTE35 splice_insert command as binary bytes. Encode a SCTE35 splice_insert command as binary bytes.
Returns the binary SCTE35 cue. Returns the binary SCTE35 cue.
Args:
event_id: Unique identifier for the splice event
duration_seconds: Duration of the splice in seconds
pts_time: PTS time in seconds (90kHz clock)
out_of_network: Whether this is an out-of-network splice
splice_immediate: Whether splice should happen immediately
Returns:
Binary SCTE35 cue bytes
""" """
# Validate inputs
if not (0 <= event_id <= 0xFFFFFFFF): if not (0 <= event_id <= 0xFFFFFFFF):
raise ValueError(f"event_id must be a 32-bit unsigned integer (0-4294967295), got {event_id}") raise ValueError(f"event_id must be a 32-bit unsigned integer (0-4294967295), got {event_id}")
if duration_seconds < 0: if duration_seconds < 0:
raise ValueError(f"duration_seconds must be non-negative, got {duration_seconds}") raise ValueError(f"duration_seconds must be non-negative, got {duration_seconds}")
# Build the splice_insert command
flags = 0x00 flags = 0x00
if out_of_network: if out_of_network:
flags |= 0x80 # out_of_network_indicator (bit 7) flags |= 0x80
flags |= 0x40 # program_splice_flag (bit 6) flags |= 0x40 # program_splice_flag
if duration_seconds > 0: if duration_seconds > 0:
flags |= 0x20 # duration_flag (bit 5) flags |= 0x20 # duration_flag
if splice_immediate: if splice_immediate:
flags |= 0x10 # splice_immediate_flag (bit 4) flags |= 0x10
# Build splice_insert command: event_id (4 bytes) + cancel_indicator (1 byte)
splice_cmd = struct.pack('>I', event_id) splice_cmd = struct.pack('>I', event_id)
splice_cmd += struct.pack('>B', 0x00) # splice_event_cancel_indicator = 0 splice_cmd += struct.pack('>B', 0x00) # cancel_indicator
splice_cmd += struct.pack('>B', flags) splice_cmd += struct.pack('>B', flags)
# If not immediate, encode PTS time
if not splice_immediate: if not splice_immediate:
pts_90k = pts_time * 90000 pts_90k = pts_time * 90000
pts_bytes = _encode_33bit_pts(pts_90k) pts_bytes = _encode_33bit_pts(pts_90k)
splice_cmd += pts_bytes splice_cmd += pts_bytes
# If duration specified, encode duration
if duration_seconds > 0: if duration_seconds > 0:
dur_90k = int(duration_seconds * 90000) dur_90k = int(duration_seconds * 90000)
# Auto return flag (bit 7) = 1, 33-bit duration duration_byte = 0xFE # auto_return=1
duration_byte = 0xFE # 1111 1110 (auto_return=1)
splice_cmd += struct.pack('>B', duration_byte) splice_cmd += struct.pack('>B', duration_byte)
dur_bytes = _encode_33bit_pts(dur_90k) dur_bytes = _encode_33bit_pts(dur_90k)
splice_cmd += dur_bytes splice_cmd += dur_bytes
# Unique program ID, avail num, avails expected
splice_cmd += struct.pack('>H', 0x0001) # unique_program_id splice_cmd += struct.pack('>H', 0x0001) # unique_program_id
splice_cmd += struct.pack('>B', 0x00) # avail_num splice_cmd += struct.pack('>B', 0x00) # avail_num
splice_cmd += struct.pack('>B', 0x00) # avails_expected splice_cmd += struct.pack('>B', 0x00) # avails_expected
# Calculate splice command length
splice_cmd_length = len(splice_cmd) splice_cmd_length = len(splice_cmd)
# Build SCTE35 section header
# Table ID: 0xFC
section = struct.pack('>B', 0xFC) section = struct.pack('>B', 0xFC)
# Section Syntax Indicator (1 bit) + Private Indicator (1 bit) + Reserved (2 bits) = 0x30
section += struct.pack('>B', 0x30) section += struct.pack('>B', 0x30)
# The remainder is: protocol_version (1) + encrypted_packet (1) + pts_adj (5) +
# cw_index (1) + tier (2) + splice_cmd_length (2) + splice_cmd +
# unique_program_id (2) + avail_num (1) + avails_expected (1) +
# descriptor_loop_length (2)
# Build the payload
payload = struct.pack('>B', 0x00) # protocol_version payload = struct.pack('>B', 0x00) # protocol_version
payload += struct.pack('>B', 0x00) # encrypted_packet payload += struct.pack('>B', 0x00) # encrypted_packet
payload += struct.pack('>5s', b'\x00\x00\x00\x00\x00') # PTS adjustment (5 bytes) payload += struct.pack('>5s', b'\x00\x00\x00\x00\x00') # PTS adjustment
payload += struct.pack('>B', 0x00) # CW index payload += struct.pack('>B', 0x00) # CW index
# Tier (reserved=0xFF, reserved=0xF0) payload += struct.pack('>H', 0xFFF0) # tier
payload += struct.pack('>H', 0xFFF0) payload += struct.pack('>H', splice_cmd_length)
payload += struct.pack('>H', splice_cmd_length) # Splice command length
payload += splice_cmd payload += splice_cmd
payload += struct.pack('>H', 0x0000) # descriptor_loop_length payload += struct.pack('>H', 0x0000) # descriptor_loop_length
# Section length (length of everything after the 2-byte section_length field)
section_length = len(payload) section_length = len(payload)
section += struct.pack('>H', section_length) section += struct.pack('>H', section_length)
section += payload section += payload
# Calculate and append CRC32
crc = binascii.crc32(section) & 0xFFFFFFFF crc = binascii.crc32(section) & 0xFFFFFFFF
section += struct.pack('>I', crc) section += struct.pack('>I', crc)
@ -135,7 +104,9 @@ class SCTE35Manager:
duration_seconds: float, duration_seconds: float,
webhook_url: Optional[str] = None, webhook_url: Optional[str] = None,
out_of_network: bool = True, out_of_network: bool = True,
splice_immediate: bool = False splice_immediate: bool = False,
port_index: Optional[int] = None,
srt_destination_url: Optional[str] = None,
) -> SCTE35Marker: ) -> SCTE35Marker:
""" """
Create SCTE35 marker, trigger webhook if configured, add to history. Create SCTE35 marker, trigger webhook if configured, add to history.
@ -146,11 +117,12 @@ class SCTE35Manager:
webhook_url: Optional webhook URL to POST marker data to webhook_url: Optional webhook URL to POST marker data to
out_of_network: Whether this is an out-of-network splice out_of_network: Whether this is an out-of-network splice
splice_immediate: Whether splice should happen immediately splice_immediate: Whether splice should happen immediately
port_index: Which port this marker is for (None = global/all)
srt_destination_url: Target specific SRT destination (None = all)
Returns: Returns:
The created SCTE35Marker object The created SCTE35Marker object
""" """
# Generate binary cue
cue_bytes = self.encode_splice_insert( cue_bytes = self.encode_splice_insert(
event_id=event_id, event_id=event_id,
duration_seconds=duration_seconds, duration_seconds=duration_seconds,
@ -158,37 +130,29 @@ class SCTE35Manager:
splice_immediate=splice_immediate splice_immediate=splice_immediate
) )
# Create marker object - event_id must be a string per model
marker = SCTE35Marker( marker = SCTE35Marker(
event_id=str(event_id), event_id=str(event_id),
duration_seconds=int(duration_seconds), duration_seconds=int(duration_seconds),
out_of_network=out_of_network, out_of_network=out_of_network,
splice_immediate=splice_immediate, splice_immediate=splice_immediate,
timestamp=datetime.utcnow(), timestamp=datetime.utcnow(),
webhook_url=webhook_url webhook_url=webhook_url,
port_index=port_index,
srt_destination_url=srt_destination_url,
) )
# Add to history
self._marker_history.append(marker) self._marker_history.append(marker)
# Trigger webhook if configured
if webhook_url: if webhook_url:
try: try:
await self._trigger_webhook(marker, cue_bytes) await self._trigger_webhook(marker, cue_bytes)
except Exception as e: except Exception as e:
# Log but don't fail - marker is already added to history
logger.error(f"Webhook trigger failed for event_id {event_id}: {e}") logger.error(f"Webhook trigger failed for event_id {event_id}: {e}")
return marker return marker
async def _trigger_webhook(self, marker: SCTE35Marker, cue_bytes: bytes) -> None: async def _trigger_webhook(self, marker: SCTE35Marker, cue_bytes: bytes) -> None:
""" """POST JSON payload to webhook_url with marker metadata."""
POST JSON payload to webhook_url with marker metadata.
Args:
marker: SCTE35Marker object with metadata
cue_bytes: Binary SCTE35 cue bytes
"""
if not marker.webhook_url: if not marker.webhook_url:
return return
@ -198,22 +162,18 @@ class SCTE35Manager:
"out_of_network": marker.out_of_network, "out_of_network": marker.out_of_network,
"splice_immediate": marker.splice_immediate, "splice_immediate": marker.splice_immediate,
"timestamp": marker.timestamp.isoformat(), "timestamp": marker.timestamp.isoformat(),
"cue_hex": cue_bytes.hex() "cue_hex": cue_bytes.hex(),
"port_index": marker.port_index,
"srt_destination_url": marker.srt_destination_url,
} }
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(marker.webhook_url, json=payload) as response: async with session.post(marker.webhook_url, json=payload) as response:
# Just log the status
if response.status != 200: if response.status != 200:
logger.warning(f"Webhook returned status {response.status}") logger.warning(f"Webhook returned status {response.status}")
def get_marker_history(self) -> list[SCTE35Marker]: def get_marker_history(self) -> list[SCTE35Marker]:
""" """Return all markers in history as a copy."""
Return all markers in history as a copy.
Returns:
List of SCTE35Marker objects
"""
return list(self._marker_history) return list(self._marker_history)
def clear_history(self) -> None: def clear_history(self) -> None: