~martijnbraam/pyatem

b73a6e96ac04e5d6292304f1693f4283f77c579e — Martijn Braam 5 months ago 7d4a662 emulator
Work in progress atem emulator
5 files changed, 398 insertions(+), 17 deletions(-)

A pyatem/debuglog.py
A pyatem/emulator.py
M pyatem/field.py
M pyatem/protocol.py
M pyatem/transport.py
A pyatem/debuglog.py => pyatem/debuglog.py +69 -0
@@ 0,0 1,69 @@
import struct


class DebugLog:
    def __init__(self, filename):
        self.filename = filename
        with open(filename, 'w') as handle:
            handle.write('<style>\n')
            handle.write('tr.packet {\n')
            handle.write('  font-family: monospace;\n')
            handle.write('}\n')
            handle.write('tr.packet.sending {\n')
            handle.write('  background: #ddd;\n')
            handle.write('}\n')
            handle.write('td.header {\n')
            handle.write('  white-space: nowrap;\n')
            handle.write('}\n')
            handle.write('td {\n')
            handle.write('  vertical-align: top;\n')
            handle.write('  font-size: 12px;\n')
            handle.write('}\n')
            handle.write('span {\n')
            handle.write('  padding-right: 10px;\n')
            handle.write('}\n')
            handle.write('span.sep {\n')
            handle.write('  padding-right: 20px;\n')
            handle.write('}\n')
            handle.write('span.data {\n')
            handle.write('  display: inline-block;\n')
            handle.write('}\n')
            handle.write('</style>\n\n<table style="width: 100%">')

    def add_packet(self, sending, raw):
        dc = 'receiving'
        if sending:
            dc = 'sending'
        row = '<tr class="packet {}"><td class="header">\n'.format(dc)
        row += '<span class="flags">{:02x} {:02x}</span>\n'.format(raw[0], raw[1])
        row += '<span class="session">{:02x} {:02x}</span>\n'.format(raw[2], raw[3])
        row += '<span class="acknr">{:02x} {:02x}</span>\n'.format(raw[4], raw[5])
        row += '<span class="hdrunkwn">{:02x} {:02x}</span>\n'.format(raw[6], raw[7])
        row += '<span class="remseq">{:02x} {:02x}</span>\n'.format(raw[8], raw[9])
        row += '<span class="locseq">{:02x} {:02x}</span>\n'.format(raw[10], raw[11])
        row += '<span class="sep"></span></td><td>\n'
        row += '<span class="data">\n'
        data = raw[12:]
        if len(data) > 8:
            row += '<table class="fields">\n'
            offset = 0
            while offset < len(data):
                l1, l2, l3, l4, cmd = struct.unpack_from('!4B 4s', data, offset)
                datalen, _ = struct.unpack_from('!H2x 4s', data, offset)
                raw = data[offset + 8:offset + datalen]
                offset += datalen
                row += '<tr><td class="header">\n'
                row += '{:02x} {:02x} {:02x} {:02x} {} <span class="sep"></span>'.format(l1, l2, l3, l4, cmd.decode())
                row += '</td><td>\n'
                for b in raw:
                    row += '{:02x} '.format(b)
                row += '</tr>\n'
            row += '</table>\n'

        else:
            for b in data:
                row += '{:02x} '.format(b)
        row += '</span>\n'
        row += '</td></tr>\n'
        with open(self.filename, 'a') as handle:
            handle.write(row)

A pyatem/emulator.py => pyatem/emulator.py +177 -0
@@ 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()

M pyatem/field.py => pyatem/field.py +130 -1
@@ 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)

M pyatem/protocol.py => pyatem/protocol.py +15 -15
@@ 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',
        }


M pyatem/transport.py => pyatem/transport.py +7 -1
@@ 12,6 12,7 @@ from hexdump import hexdump

class Packet:
    def __init__(self):
        self.emulator = False
        self.flags = 0
        self.length = 0
        self.session = 0


@@ 88,7 89,12 @@ class Packet:
        label = ''
        if self.label:
            label = ' ' + self.label
        return '<Packet flags={} data={} sequence={}{}{}>'.format(flags, data_len, self.sequence_number, extra, label)
        if False:
            return '<Packet flags={} data={} sequence={}{}{}>'.format(flags, data_len, self.remote_sequence_number,
                                                                      extra, label)
        else:
            return '<Packet flags={} data={} sequence={}{}{}>'.format(flags, data_len, self.sequence_number, extra,
                                                                      label)


class UdpProtocol: