@@ 0,0 1,177 @@
+import socket
+import time
+
+from pyatem.debuglog import DebugLog
+from pyatem.field import FirmwareVersionField, ProductNameField, MixerEffectConfigField, MediaplayerSlotsField, \
+ VideoModeField, InputPropertiesField, InitCompleteField, ManualField
+from pyatem.protocol import AtemProtocol
+from pyatem.transport import Packet, UdpProtocol
+
+logger_emulator = DebugLog("/workspace/emulator.html")
+
+
+class AtemClient:
+ STATE_CLOSED = 0
+ STATE_HANDSHAKE = 1
+ STATE_CONNECTED = 2
+
+ def __init__(self, emulator, addr, session):
+ self.emulator = emulator
+ self.sock = emulator.sock
+ self.addr = addr
+ self.session = session
+
+ self.state = AtemClient.STATE_CLOSED
+
+ self.local_sequence_number = 0
+ self.local_ack_number = 0
+ self.remote_sequence_number = 0
+ self.remote_ack_number = 0
+
+ def send_packet(self, data, flags=0, session=None, client_packet_id=None):
+ packet = Packet()
+ packet.emulator = True
+ packet.flags = flags
+ packet.data = data
+ if client_packet_id:
+ packet.remote_sequence_number = client_packet_id
+ if session:
+ packet.session = session
+ else:
+ packet.session = self.session
+ if not packet.flags & UdpProtocol.FLAG_SYN:
+ packet.sequence_number = (self.local_sequence_number + 1) % 2 ** 16
+ raw = packet.to_bytes()
+ print('> {}'.format(packet))
+ logger_emulator.add_packet(sending=True, raw=raw)
+ self.sock.sendto(raw, self.addr)
+
+ if packet.flags & (UdpProtocol.FLAG_SYN) == 0:
+ self.local_sequence_number = (self.local_sequence_number + 1) % 2 ** 16
+
+ def send_fields(self, fields):
+ data = b''
+ for field in fields:
+ data += field.make_packet()
+
+ if len(data) > 1300:
+ raise ValueError("Field list too long for UDP packet")
+
+ self.send_packet(data, flags=UdpProtocol.FLAG_RELIABLE)
+
+ def _flatten(self, idict):
+ result = []
+ for key in idict:
+ if isinstance(idict[key], dict):
+ result.extend(self._flatten(idict[key]))
+ elif isinstance(idict[key], list):
+ result.extend(idict[key])
+ else:
+ result.append(idict[key])
+ return result
+
+ def send_initial_state(self):
+ # fields = self._flatten(self.emulator.mixerstate)
+ fields = self.emulator.mixerstate
+ fields.append(InitCompleteField.create())
+ buffer = []
+ size = 0
+ # Flag should be 0x01
+ for field in fields:
+ if isinstance(field, bytes):
+ continue
+ fsize = len(field.raw) + 8
+ if size + fsize > 1300:
+ self.send_fields(buffer)
+ buffer = []
+ size = 0
+ buffer.append(field)
+ size += fsize
+ self.send_fields(buffer)
+
+ # Flag should be 0x11
+ self.send_packet(b'', flags=(UdpProtocol.FLAG_RELIABLE | UdpProtocol.FLAG_ACK))
+
+ def on_packet(self, raw):
+ logger_emulator.add_packet(sending=False, raw=raw)
+ packet = Packet.from_bytes(raw)
+ packet.emulator = True
+ print('< {}'.format(packet))
+ if self.state == AtemClient.STATE_CLOSED:
+ print("Temp session id is {}".format(packet.session))
+ self.state = AtemClient.STATE_HANDSHAKE
+
+ raw = b'\x02\0\0\xc2\0\0\0\0'
+ # Flag should be 0x02
+ self.send_packet(raw, flags=UdpProtocol.FLAG_SYN, session=packet.session, client_packet_id=0xad)
+ elif self.state == AtemClient.STATE_HANDSHAKE:
+ print("Handshake complete, session is now {}".format(self.session))
+ self.state = AtemClient.STATE_CONNECTED
+ # Handshake done, start dumping state
+ self.send_initial_state()
+
+
+class AtemEmulator:
+ def __init__(self, host=None, port=9910):
+ if host is None:
+ host = ''
+ self.host = host
+ self.port = port
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+ self.clients = {}
+ self.mixerstate = {}
+
+ def listen(self):
+ self.sock.bind((self.host, self.port))
+ while True:
+ raw, addr = self.sock.recvfrom(9000)
+ if addr not in self.clients:
+ self.on_connect(addr)
+ self.clients[addr].on_packet(raw)
+
+ def on_connect(self, addr):
+ print("New client on {}".format(addr))
+ self.clients[addr] = AtemClient(self, addr, len(self.clients) + 0x8100)
+
+
+if __name__ == '__main__':
+ def passthrough_done():
+ print("Passthrough initialized")
+ testdev.mixerstate = unknown_stuff
+ testdev.listen()
+
+
+ def passthrough_unknown_field(key, raw):
+ if not isinstance(raw, bytes):
+ unknown_stuff.append(raw)
+ else:
+ if len(key) > 4:
+ print(key)
+ unknown_stuff.append(ManualField(key, raw))
+
+
+ unknown_stuff = []
+ testdev = AtemEmulator()
+ pt = AtemProtocol(ip='192.168.2.84')
+ pt.on('change', passthrough_unknown_field)
+ pt.on('connected', passthrough_done)
+ pt.connect()
+ print("Connecting to passthrough device")
+ while True:
+ pt.loop()
+
+ testdev.mixerstate = {
+ 'firmware-version': FirmwareVersionField.create(1, 0),
+ 'product-name': ProductNameField.create("Emulated mixer"),
+ 'mixer-effect-config': {
+ '0': MixerEffectConfigField.create(0, 4),
+ '1': MixerEffectConfigField.create(1, 4),
+ },
+ 'mediaplayer-slots': MediaplayerSlotsField.create(0, 0),
+ 'video-mode': VideoModeField.create(27),
+ 'input-properties': {
+ '0': InputPropertiesField.create(0, 'Black', 'BLK', 0, 0, 0, 0xff, True, True),
+ }
+ }
+ testdev.listen()
@@ 6,6 6,10 @@ class FieldBase:
def _get_string(self, raw):
return raw.split(b'\x00')[0].decode()
+ def make_packet(self):
+ header = struct.pack('!H2x 4s', len(self.raw) + 8, self.fieldcode.encode())
+ return header + self.raw
+
class FirmwareVersionField(FieldBase):
"""
@@ 24,11 28,17 @@ class FirmwareVersionField(FieldBase):
:ivar minor: Minor firmware version
"""
+ @classmethod
+ def create(cls, major, minor):
+ raw = struct.pack('>HH', major, minor)
+ return cls(raw)
+
def __init__(self, raw):
"""
:param raw:
"""
self.raw = raw
+ self.fieldcode = '_ver'
self.major, self.minor = struct.unpack('>HH', raw)
self.version = "{}.{}".format(self.major, self.minor)
@@ 51,8 61,15 @@ class ProductNameField(FieldBase):
:ivar name: User friendly product name
"""
+ @classmethod
+ def create(cls, name):
+ name = name.encode()
+ name += b'\0' * (44 - len(name))
+ return cls(name)
+
def __init__(self, raw):
self.raw = raw
+ self.fieldcode = '_pin'
self.name = self._get_string(raw)
def __repr__(self):
@@ 79,7 96,13 @@ class MixerEffectConfigField(FieldBase):
:ivar keyers: Number of upstream keyers on this M/E
"""
+ @classmethod
+ def create(cls, index, keyers):
+ raw = struct.pack('>2B2x', index, keyers)
+ return cls(raw)
+
def __init__(self, raw):
+ self.fieldcode = '_MeC'
self.raw = raw
self.index, self.keyers = struct.unpack('>2B2x', raw)
@@ 104,7 127,13 @@ class MediaplayerSlotsField(FieldBase):
:ivar name: User friendly product name
"""
+ @classmethod
+ def create(cls, stills, clips):
+ raw = struct.pack('>2B2x', stills, clips)
+ return cls(raw)
+
def __init__(self, raw):
+ self.fieldcode = '_mpl'
self.raw = raw
self.stills, self.clips = struct.unpack('>2B2x', raw)
@@ 158,7 187,13 @@ class VideoModeField(FieldBase):
:ivar rate: refresh rate of the mode
"""
+ @classmethod
+ def create(cls, mode):
+ raw = struct.pack('>1B3x', mode)
+ return cls(raw)
+
def __init__(self, raw):
+ self.fieldcode = 'VidM'
self.raw = raw
self.mode, = struct.unpack('>1B3x', raw)
@@ 289,7 324,31 @@ class InputPropertiesField(FieldBase):
PORT_ME_OUTPUT = 128
PORT_AUX_OUTPUT = 129
+ @classmethod
+ def create(cls, index, name, short_name, source_category, port_type, source_ports, available, available_me1=True,
+ available_me2=False):
+
+ available_me = 0
+ if available_me1:
+ available_me += 1
+ if available_me2:
+ available_me += 2
+
+ raw = struct.pack('>H 20s 4s 10B', index, name.encode(), short_name.encode(),
+ source_category,
+ 0,
+ source_category,
+ source_ports,
+ source_category,
+ source_ports,
+ port_type,
+ 0,
+ available,
+ available_me)
+ return cls(raw)
+
def __init__(self, raw):
+ self.fieldcode = 'InPr'
self.raw = raw
fields = struct.unpack('>H 20s 4s 10B', raw)
self.index = fields[0]
@@ 333,6 392,7 @@ class ProgramBusInputField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'PrgI'
self.raw = raw
self.index, self.source = struct.unpack('>BxH', raw)
@@ 364,6 424,7 @@ class PreviewBusInputField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'PrvI'
self.raw = raw
self.index, self.source, in_program = struct.unpack('>B x H B 3x', raw)
self.in_program = in_program == 1
@@ 422,6 483,7 @@ class TransitionSettingsField(FieldBase):
STYLE_STING = 4
def __init__(self, raw):
+ self.fieldcode = 'TrSS'
self.raw = raw
self.index, self.style, nt, self.style_next, ntn = struct.unpack('>B 2B 2B 3x', raw)
@@ 462,6 524,7 @@ class TransitionPreviewField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'TsPr'
self.raw = raw
self.index, self.enabled = struct.unpack('>B ? 2x', raw)
@@ 495,6 558,7 @@ class TransitionPositionField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'TrPs'
self.raw = raw
self.index, self.in_transition, self.frames_remaining, position = struct.unpack('>B ? B x H 2x', raw)
self.position = position
@@ 523,6 587,7 @@ class TallyIndexField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'TlIn'
self.raw = raw
offset = 0
self.num, = struct.unpack_from('>H', raw, offset)
@@ 556,6 621,7 @@ class TallySourceField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'TlSr'
self.raw = raw
offset = 0
self.num, = struct.unpack_from('>H', raw, offset)
@@ 591,6 657,7 @@ class KeyOnAirField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'KeOn'
self.raw = raw
self.index, self.keyer, self.enabled = struct.unpack('>BB?x', raw)
@@ 620,6 687,7 @@ class ColorGeneratorField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'ColV'
self.raw = raw
self.index, self.hue, self.saturation, self.luma = struct.unpack('>Bx 3H', raw)
self.hue = self.hue / 10.0
@@ 653,6 721,7 @@ class AuxOutputSourceField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'AuxS'
self.raw = raw
self.index, self.source = struct.unpack('>BxH', raw)
@@ 682,6 751,7 @@ class FadeToBlackStateField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'FtbS'
self.raw = raw
self.index, self.done, self.transitioning, self.frames_remaining = struct.unpack('>B??B', raw)
@@ 718,6 788,7 @@ class MediaplayerFileInfoField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'MPfe'
self.raw = raw
namelen = len(raw) - 23
self.type, self.index, self.is_used, self.hash, self.name = struct.unpack('>Bx H ? 16s 2x {}p'.format(namelen),
@@ 796,6 867,7 @@ class TopologyField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = '_top'
self.raw = raw
field = struct.unpack('>28B', raw)
@@ 847,6 919,7 @@ class DkeyPropertiesField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'DskP'
self.raw = raw
field = struct.unpack('>B?B ?HH? ?4h 2B', raw)
self.index = field[0]
@@ 892,6 965,7 @@ class DkeyStateField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'DskS'
self.raw = raw
field = struct.unpack('>B 3? B 3x', raw)
self.index = field[0]
@@ 927,6 1001,7 @@ class TransitionMixField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'TMxP'
self.raw = raw
self.index, self.rate = struct.unpack('>BBxx', raw)
@@ 953,6 1028,7 @@ class FadeToBlackField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'FtbP'
self.raw = raw
self.index, self.rate = struct.unpack('>BBxx', raw)
@@ 980,6 1056,7 @@ class TransitionDipField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'TDpP'
self.raw = raw
self.index, self.rate, self.source = struct.unpack('>BBH', raw)
@@ 1016,6 1093,7 @@ class TransitionWipeField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'TWpP'
self.raw = raw
field = struct.unpack('>BBBx 6H 2? 2x', raw)
self.index = field[0]
@@ 1073,6 1151,7 @@ class TransitionDveField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'TDvP'
self.raw = raw
field = struct.unpack('>BBx B 2H 2? 2H 3? 3x', raw)
self.index = field[0]
@@ 1119,6 1198,7 @@ class FairlightMasterPropertiesField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'FAMP'
self.raw = raw
field = struct.unpack('>x ? 4x h 2x H i ? 3x', raw)
self.eq_enable = field[0]
@@ 1164,6 1244,7 @@ class FairlightStripPropertiesField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'FASP'
self.raw = raw
field = struct.unpack('>H 12xBBxB 4x h 5x ? 4x h 2x Hh 4x h x B 2x', raw)
self.index = field[0]
@@ 1205,6 1286,7 @@ class FairlightStripDeleteField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'FASD'
self.raw = raw
def __repr__(self):
@@ 1239,6 1321,7 @@ class FairlightAudioInputField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'FAIP'
self.raw = raw
self.index, self.type, self.number, self.split, self.level = struct.unpack('>HB 2x B xxxx B x B 3x', raw)
@@ 1274,6 1357,7 @@ class FairlightTallyField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'FMTl'
self.raw = raw
offset = 0
self.num, = struct.unpack_from('>H', raw, offset)
@@ 1330,6 1414,7 @@ class AtemEqBandPropertiesField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'AEBP'
self.raw = raw
values = struct.unpack('>H 2x 4x 6x BB B ? B B x B 4x H i H 2x', raw)
self.index = values[0]
@@ 1420,6 1505,7 @@ class AudioInputField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'AMIP'
self.raw = raw
self.index, self.type, self.number, self.plug, self.state, self.volume, self.balance = struct.unpack(
'>HB 2x B x BB x Hh 2x', raw)
@@ 1435,6 1521,7 @@ class KeyPropertiesBaseField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'KeBP'
self.raw = raw
field = struct.unpack('>BBB Bx B HH ?x 4h', raw)
self.index = field[0]
@@ 1461,6 1548,7 @@ class KeyPropertiesDveField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'KeDV'
self.raw = raw
field = struct.unpack('>BBxx 5i ??Bx HH BBBBBx 4HB? 4hB 3x', raw)
self.index = field[0]
@@ 1511,6 1599,7 @@ class KeyPropertiesLumaField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'KeLm'
self.raw = raw
field = struct.unpack('>BB?x HH ?3x', raw)
self.index = field[0]
@@ 1552,6 1641,7 @@ class RecordingDiskField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'RTMD'
self.raw = raw
field = struct.unpack('>IIH 64s 2x', raw)
self.index = field[0]
@@ 1589,6 1679,7 @@ class RecordingSettingsField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'RMSu'
self.raw = raw
field = struct.unpack('>128s ii ?3x', raw)
self.filename = self._get_string(field[0])
@@ 1629,6 1720,7 @@ class RecordingStatusField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'RTMS'
self.raw = raw
field = struct.unpack('>H2xi', raw)
self.status = field[0]
@@ 1647,7 1739,7 @@ class RecordingStatusField(FieldBase):
class RecordingDurationField(FieldBase):
"""
- Data from the `RTMS`. The status for the stream recorder.
+ Data from the `RTMR`. The status for the stream recorder.
====== ==== ====== ===========
Offset Size Type Descriptions
@@ 1663,6 1755,7 @@ class RecordingDurationField(FieldBase):
"""
def __init__(self, raw):
+ self.fieldcode = 'RTMR'
self.raw = raw
field = struct.unpack('>4B ?3x', raw)
self.hours = field[0]
@@ 1676,3 1769,39 @@ class RecordingDurationField(FieldBase):
if self.has_dropped_frames:
drop = ' dropped-frames'
return '<recording-duration {}:{}:{}:{}{}>'.format(self.hours, self.minutes, self.seconds, self.frames, drop)
+
+
+class InitCompleteField(FieldBase):
+ """
+ Data from the `InCm` field. This marks the end of the initial configuration packets
+
+ ====== ==== ====== ===========
+ Offset Size Type Description
+ ====== ==== ====== ===========
+ 0 4 ? Unknown
+ ====== ==== ====== ===========
+
+ """
+
+ @classmethod
+ def create(cls):
+ return cls(b'\1\0\0\4')
+
+ def __init__(self, raw):
+ """
+ :param raw:
+ """
+ self.raw = raw
+ self.fieldcode = 'InCm'
+
+ def __repr__(self):
+ return '<init-complete>'
+
+
+class ManualField(FieldBase):
+ def __init__(self, fieldcode, raw):
+ self.fieldcode = fieldcode
+ self.raw = raw
+
+ def __repr__(self):
+ return '<manual {}>'.format(self.fieldcode)
@@ 70,15 70,15 @@ class AtemProtocol:
'TDpP': 'transition-dip',
'TWpP': 'transition-wipe',
'TDvP': 'transition-dve',
- 'TStP': 'transition-stinger',
+ #'TStP': 'transition-stinger',
'KeOn': 'key-on-air',
'KeBP': 'key-properties-base',
'KeLm': 'key-properties-luma',
- 'KePt': 'key-properties-pattern',
+ #'KePt': 'key-properties-pattern',
'KeDV': 'key-properties-dve',
- 'KeFS': 'key-properties-fly',
- 'KKFP': 'key-properties-fly-keyframe',
- 'DskB': 'dkey-properties-base',
+ #'KeFS': 'key-properties-fly',
+ #'KKFP': 'key-properties-fly-keyframe',
+ #'DskB': 'dkey-properties-base',
'DskP': 'dkey-properties',
'DskS': 'dkey-state',
'FtbP': 'fade-to-black',
@@ 86,15 86,15 @@ class AtemProtocol:
'ColV': 'color-generator',
'AuxS': 'aux-output-source',
'MPfe': 'mediaplayer-file-info',
- 'MPCE': 'mediaplayer-selected',
+ #'MPCE': 'mediaplayer-selected',
'FASP': 'fairlight-strip-properties',
'FAMP': 'fairlight-master-properties',
- '_TlC': 'tally-config',
+ #'_TlC': 'tally-config',
'TlIn': 'tally-index',
'TlSr': 'tally-source',
'FMTl': 'fairlight-tally',
- 'MPrp': 'macro-properties',
- 'AiVM': 'auto-input-video-mode',
+ #'MPrp': 'macro-properties',
+ #'AiVM': 'auto-input-video-mode',
'FASD': 'fairlight-strip-delete',
'FAIP': 'fairlight-audio-input',
'AMIP': 'audio-input',
@@ 102,12 102,12 @@ class AtemProtocol:
'RTMD': 'recording-disk',
'RTMS': 'recording-status',
'RMSu': 'recording-settings',
- 'SRSU': 'streaming-services',
- 'STAB': 'streaming-audio-bitrate',
- 'StRS': 'streaming-status',
- 'SRST': 'streaming-time',
- 'SRSS': 'streaming-stats',
- 'SAth': 'streaming-authentication',
+ #'SRSU': 'streaming-services',
+ #'STAB': 'streaming-audio-bitrate',
+ #'StRS': 'streaming-status',
+ #'SRST': 'streaming-time',
+ #'SRSS': 'streaming-stats',
+ #'SAth': 'streaming-authentication',
'AEBP': 'atem-eq-band-properties',
}