├── .gitignore ├── README.md ├── __init__.py ├── atem.py ├── config.py.dist └── roadmap.md /.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | *.pyc 3 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a python port of the QT library at https://github.com/petersimonsson/libqatemcontrol 2 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sxpert/PyATEM/0d61bdd4a8217ff3dc65be1e1f07aa395ac0a8b5/__init__.py -------------------------------------------------------------------------------- /atem.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | # -*- coding: utf-8 -*- 3 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 4 | 5 | import socket 6 | import struct 7 | import collections 8 | import ctypes 9 | import time 10 | from pprint import pprint 11 | 12 | 13 | def dumpHex (buffer): 14 | s = '' 15 | for c in buffer: 16 | s += hex(c) + ' ' 17 | print(s) 18 | 19 | 20 | def dumpAscii (buffer): 21 | s = '' 22 | for c in buffer: 23 | if (ord(c)>=0x20)and(ord(c)<=0x7F): 24 | s+=c 25 | else: 26 | s+='.' 27 | print(s) 28 | 29 | 30 | # implements communication with atem switcher 31 | class Atem: 32 | 33 | # size of header data 34 | SIZE_OF_HEADER = 0x0c 35 | 36 | # packet types 37 | CMD_NOCOMMAND = 0x00 38 | CMD_ACKREQUEST = 0x01 39 | CMD_HELLOPACKET = 0x02 40 | CMD_RESEND = 0x04 41 | CMD_UNDEFINED = 0x08 42 | CMD_ACK = 0x10 43 | 44 | # labels 45 | LABELS_VIDEOMODES = ['525i59.94NTSC', '625i50PAL', '525i59.94NTSC16:9', '625i50PAL16:9', 46 | '720p50', '720p59.94', '1080i50', '1080i59.94', 47 | '1080p23.98', '1080p24', '1080p25', '1080p29.97', '1080p50', '1080p59.94', 48 | '2160p23.98', '2160p24', '2160p25', '2160p29.97'] 49 | LABELS_PORTS_EXTERNAL = {0: 'SDI', 1: 'HDMI', 2: 'Component', 3: 'Composite', 4: 'SVideo'} 50 | LABELS_PORTS_INTERNAL = {0: 'External', 1: 'Black', 2: 'Color Bars', 3: 'Color Generator', 4: 'Media Player Fill', 51 | 5: 'Media Player Key', 6: 'SuperSource', 128: 'ME Output', 129: 'Auxilary', 130: 'Mask'} 52 | LABELS_MULTIVIEWER_LAYOUT = ['top', 'bottom', 'left', 'right'] 53 | LABELS_AUDIO_PLUG = ['Internal', 'SDI', 'HDMI', 'Component', 'Composite', 'SVideo', 'XLR', 'AES/EBU', 'RCA'] 54 | LABELS_VIDEOSRC = { 0: 'Black', 1: 'Input 1', 2: 'Input 2', 3: 'Input 3', 4: 'Input 4', 5: 'Input 5', 6: 'Input 6', 7: 'Input 7', 8: 'Input 8', 9: 'Input 9', 10: 'Input 10', 11: 'Input 11', 12: 'Input 12', 13: 'Input 13', 14: 'Input 14', 15: 'Input 15', 16: 'Input 16', 17: 'Input 17', 18: 'Input 18', 19: 'Input 19', 20: 'Input 20', 1000: 'Color Bars', 2001: 'Color 1', 2002: 'Color 2', 3010: 'Media Player 1', 3011: 'Media Player 1 Key', 3020: 'Media Player 2', 3021: 'Media Player 2 Key', 4010: 'Key 1 Mask', 4020: 'Key 2 Mask', 4030: 'Key 3 Mask', 4040: 'Key 4 Mask', 5010: 'DSK 1 Mask', 5020: 'DSK 2 Mask', 6000: 'Super Source', 7001: 'Clean Feed 1', 7002: 'Clean Feed 2', 8001: 'Auxilary 1', 8002: 'Auxilary 2', 8003: 'Auxilary 3', 8004: 'Auxilary 4', 8005: 'Auxilary 5', 8006: 'Auxilary 6', 10010: 'ME 1 Prog', 10011: 'ME 1 Prev', 10020: 'ME 2 Prog', 10021: 'ME 2 Prev' } 55 | LABELS_AUDIOSRC = { 1: 'Input 1', 2: 'Input 2', 3: 'Input 3', 4: 'Input 4', 5: 'Input 5', 6: 'Input 6', 7: 'Input 7', 8: 'Input 8', 9: 'Input 9', 10: 'Input 10', 11: 'Input 11', 12: 'Input 12', 13: 'Input 13', 14: 'Input 14', 15: 'Input 15', 16: 'Input 16', 17: 'Input 17', 18: 'Input 18', 19: 'Input 19', 20: 'Input 20', 1001: 'XLR', 1101: 'AES/EBU', 1201: 'RCA', 2001: 'MP1', 2002: 'MP2' } 56 | # cc 57 | LABELS_CC_DOMAIN = {0: 'lens', 1: 'camera', 8: 'chip'} 58 | LABELS_CC_LENS_FEATURE = {0: 'focus', 1: 'auto_focused', 3: 'iris', 9: 'zoom'} 59 | LABELS_CC_CAM_FEATURE = {1: 'gain', 2: 'white_balance', 5: 'shutter'} 60 | LABELS_CC_CHIP_FEATURE = {0: 'lift', 1: 'gamma', 2: 'gain', 3: 'aperture', 4: 'contrast', 5: 'luminance', 6: 'hue-saturation'} 61 | 62 | # value options 63 | VALUES_CC_GAIN = {512: '0db', 1024: '6db', 2048: '12db', 4096: '18db'} 64 | VALUES_CC_WB = {3200: '3200K', 4500: '4500K', 5000: '5000K', 5600: '5600K', 6500: '6500K', 7500: '7500K'} 65 | VALUES_CC_SHUTTER = {20000: '1/50', 16667: '1/60', 13333: '1/75', 11111: '1/90', 10000: '1/100', 8333: '1/120', 6667: '1/150', 5556: '1/180', 4000: '1/250', 2778: '1/360', 2000: '1/500', 1379: '1/725', 1000: '1/1000', 690: '1/1450', 500: '1/2000'} 66 | VALUES_AUDIO_MIX = { 0: 'off', 1: 'on', 2: 'AFV' } 67 | 68 | # initializes the class 69 | def __init__(self, address): 70 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 71 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 72 | self.socket.setblocking(0) 73 | self.socket.bind(('0.0.0.0', 9910)) 74 | 75 | self.address = (address, 9910) 76 | self.packetCounter = 0 77 | self.isInitialized = False 78 | self.currentUid = 0x1337 79 | 80 | self.system_config = { 'inputs': {}, 'audio': {} } 81 | self.status = {} 82 | self.config = { 'multiviewers': {}, 'mediapool': {} } 83 | self.state = { 84 | 'program': {}, 85 | 'preview': {}, 86 | 'keyers': {}, 87 | 'dskeyers': {}, 88 | 'aux': {}, 89 | 'mediaplayer': {}, 90 | 'mediapool': {}, 91 | 'audio': {}, 92 | 'tally_by_index': {}, 93 | 'tally': {} 94 | } 95 | self.cameracontrol = {} 96 | 97 | self.state['booted'] = True 98 | 99 | self.tallyHandler = None 100 | self.pgmInputHandler = None 101 | 102 | # hello packet 103 | def connectToSwitcher(self): 104 | datagram = self.createCommandHeader(self.CMD_HELLOPACKET, 8, self.currentUid, 0x0) 105 | datagram += struct.pack('!I', 0x01000000) 106 | datagram += struct.pack('!I', 0x00) 107 | self.sendDatagram(datagram) 108 | 109 | # reads packets sent by the switcher 110 | def handleSocketData(self): 111 | # network is 100Mbit/s max, MTU is thus at most 1500 112 | try: 113 | d = self.socket.recvfrom(2048) 114 | except socket.error: 115 | return False 116 | datagram, server = d 117 | #print('received datagram') 118 | header = self.parseCommandHeader(datagram) 119 | if header: 120 | self.currentUid = header['uid'] 121 | 122 | if header['bitmask'] & self.CMD_HELLOPACKET : 123 | #print('not initialized, received HELLOPACKET, sending ACK packet') 124 | self.isInitialized = False 125 | ackDatagram = self.createCommandHeader (self.CMD_ACK, 0, header['uid'], 0x0) 126 | self.sendDatagram (ackDatagram) 127 | elif (header['bitmask'] & self.CMD_ACKREQUEST) and\ 128 | (self.isInitialized or len(datagram) == self.SIZE_OF_HEADER): 129 | #print('initialized, received ACKREQUEST, sending ACK packet') 130 | #print("Sending ACK for packageId %d" % header['packageId']) 131 | ackDatagram = self.createCommandHeader(self.CMD_ACK, 0, header['uid'], header['packageId']) 132 | self.sendDatagram(ackDatagram) 133 | if not self.isInitialized: 134 | self.isInitialized = True 135 | 136 | if len(datagram) > self.SIZE_OF_HEADER + 2 and not (header['bitmask'] & self.CMD_HELLOPACKET): 137 | self.parsePayload(datagram) 138 | 139 | return True 140 | 141 | def waitForPacket(self): 142 | #print(">>> waiting for packet") 143 | while not self.handleSocketData(): 144 | time.sleep(0.01) 145 | #print(">>> packet obtained") 146 | 147 | # generates packet header data 148 | def createCommandHeader (self, bitmask, payloadSize, uid, ackId): 149 | buffer = b'' 150 | packageId = 0 151 | 152 | if not (bitmask & (self.CMD_HELLOPACKET | self.CMD_ACK)): 153 | self.packetCounter += 1 154 | packageId = self.packetCounter 155 | 156 | val = bitmask << 11 157 | val |= (payloadSize + self.SIZE_OF_HEADER) 158 | buffer += struct.pack('!H',val) 159 | buffer += struct.pack('!H',uid) 160 | buffer += struct.pack('!H',ackId) 161 | buffer += struct.pack('!I',0) 162 | buffer += struct.pack('!H',packageId) 163 | return buffer 164 | 165 | # parses the packet header 166 | def parseCommandHeader (self, datagram): 167 | header = {} 168 | 169 | if len(datagram)>=self.SIZE_OF_HEADER : 170 | header['bitmask'] = struct.unpack('B',datagram[0:1])[0] >> 3 171 | header['size'] = struct.unpack('!H',datagram[0:2])[0] & 0x07FF 172 | header['uid'] = struct.unpack('!H',datagram[2:4])[0] 173 | header['ackId'] = struct.unpack('!H',datagram[4:6])[0] 174 | header['packageId']=struct.unpack('!H',datagram[10:12])[0] 175 | #print(header) 176 | return header 177 | return False 178 | 179 | def parsePayload(self, datagram): 180 | print('parsing payload') 181 | # eat up header 182 | datagram = datagram[self.SIZE_OF_HEADER:] 183 | # handle data 184 | while len(datagram) > 0 : 185 | size = struct.unpack('!H',datagram[0:2])[0] 186 | packet = datagram[0:size] 187 | datagram = datagram[size:] 188 | 189 | # skip size and 2 unknown bytes 190 | packet = packet[4:] 191 | ptype = packet[:4] 192 | payload = packet[4:] 193 | 194 | # find the approporiate function in the class 195 | method = 'recv' + ptype.decode("utf-8") 196 | if method in dir(self) : 197 | func = getattr(self, method) 198 | if callable(func) : 199 | print('> calling '+method) 200 | func(payload) 201 | else: 202 | print('problem, member '+method+' not callable') 203 | else: 204 | print('unknown type '+ptype.decode("utf-8")) 205 | #dumpAscii(payload) 206 | 207 | #sys.exit() 208 | 209 | def sendCommand (self, command, payload) : 210 | print('sending command') 211 | size = len(command) + len(payload) + 4 212 | dg = self.createCommandHeader(self.CMD_ACKREQUEST, size, self.currentUid, 0) 213 | dg += struct.pack('!H', size) 214 | dg += "\x00\x00" 215 | dg += command 216 | dg += payload 217 | self.sendDatagram(dg) 218 | 219 | # sends a datagram to the switcher 220 | def sendDatagram (self, datagram) : 221 | #print('sending packet') 222 | #dumpHex(datagram) 223 | self.socket.sendto(datagram, self.address) 224 | 225 | def parseBitmask(self, num, labels): 226 | states = {} 227 | for i, label in enumerate(labels): 228 | states[label] = bool(num & (1 << len(labels) - i - 1)) 229 | return states 230 | 231 | def convert_cstring(self, bytes): 232 | return ctypes.create_string_buffer(bytes).value.decode('utf-8') 233 | 234 | # handling of subpackets 235 | # ---------------------- 236 | 237 | def recv_ver(self, data): 238 | major, minor = struct.unpack('!HH', data[0:4]) 239 | self.system_config['version'] = str(major)+'.'+str(minor) 240 | 241 | def recv_pin (self, data): 242 | self.system_config['name'] = data 243 | 244 | def recvWarn(self, text): 245 | print('Warning: '+text) 246 | 247 | def recv_top(self, data): 248 | self.system_config['topology'] = {} 249 | datalabels = ['mes', 'sources', 'color_generators', 'aux_busses', 'dsks', 'stingers', 'dves', 250 | 'supersources'] 251 | for i, label in enumerate(datalabels): 252 | self.system_config['topology'][label] = data[i] 253 | 254 | self.system_config['topology']['hasSD'] = (data[9] > 0) 255 | 256 | def recv_MeC(self, data): 257 | index = data[0] 258 | self.system_config.setdefault('keyers', {})[index] = data[1] 259 | 260 | def recv_mpl(self, data): 261 | self.system_config['media_players'] = {} 262 | self.system_config['media_players']['still'] = data[0] 263 | self.system_config['media_players']['clip'] = data[1] 264 | 265 | def recv_MvC(self, data): 266 | self.system_config['multiviewers'] = data[0] 267 | 268 | def recv_SSC(self, data): 269 | self.system_config['super_source_boxes'] = data[0] 270 | 271 | def recv_TlC(self, data): 272 | self.system_config['tally_channels'] = data[4] 273 | 274 | def recv_AMC(self, data): 275 | self.system_config['audio_channels'] = data[0] 276 | self.system_config['has_monitor'] = (data[1] > 0) 277 | 278 | def recv_VMC(self, data): 279 | size = 18 280 | for i in range(size): 281 | self.system_config.setdefault('video_modes', {})[i] = bool(data[0] & (1 << size - i - 1)) 282 | 283 | def recv_MAC(self, data): 284 | self.system_config['macro_banks'] = data[0] 285 | 286 | def recvPowr(self, data): 287 | self.status['power'] = self.parseBitmask(data[0], ['main', 'backup']) 288 | 289 | def recvDcOt(self, data): 290 | self.config['down_converter'] = data[0] 291 | 292 | def recvVidM(self, data): 293 | self.config['video_mode'] = data[0] 294 | 295 | def recvInPr(self, data): 296 | index = struct.unpack('!H', data[0:2])[0] 297 | self.system_config['inputs'][index] = {} 298 | input_setting = self.system_config['inputs'][index] 299 | input_setting['name_long'] = self.convert_cstring(data[2:22]) 300 | input_setting['name_short'] = self.convert_cstring(data[22:26]) 301 | input_setting['types_available'] = self.parseBitmask(data[27], self.LABELS_PORTS_EXTERNAL) 302 | input_setting['port_type_external'] = data[29] 303 | input_setting['port_type_internal'] = data[30] 304 | input_setting['availability'] = self.parseBitmask(data[32], ['Auxilary', 'Multiviewer', 'SuperSourceArt', 305 | 'SuperSourceBox', 'KeySource']) 306 | input_setting['me_availability'] = self.parseBitmask(data[33], ['ME1', 'ME2']) 307 | 308 | def recvMvPr(self, data): 309 | index = data[0] 310 | self.config['multiviewers'].setdefault(index, {})['layout'] = data[1] 311 | 312 | def recvMvIn(self, data): 313 | index = data[0] 314 | window = data[1] 315 | self.config['multiviewers'].setdefault(index, {}).setdefault('windows', {})[window] = struct.unpack('!H', data[2:4])[0] 316 | 317 | def recvPrgI(self, data): 318 | meIndex = data[0] 319 | self.state['program'][meIndex] = struct.unpack('!H', data[2:4])[0] 320 | self.pgmInputHandler(self) 321 | 322 | def recvPrvI(self, data): 323 | meIndex = data[0] 324 | self.state['preview'][meIndex] = struct.unpack('!H', data[2:4])[0] 325 | 326 | def recvKeOn(self, data): 327 | meIndex = data[0] 328 | keyer = data[1] 329 | self.state['keyers'].setdefault(meIndex, {})[keyer] = (data[2] != 0) 330 | 331 | def recvDskB(self, data): 332 | keyer = data[0] 333 | keyer_setting = self.state['dskeyers'].setdefault(keyer, {}) 334 | keyer_setting['fill'] = struct.unpack('!H', data[2:4])[0] 335 | keyer_setting['key'] = struct.unpack('!H', data[4:6])[0] 336 | 337 | def recvDskS(self, data): 338 | keyer = data[0] 339 | dsk_setting = self.state['dskeyers'].setdefault(keyer, {}) 340 | dsk_setting['onAir'] = (data[1] != 0) 341 | dsk_setting['inTransition'] = (data[2] != 0) 342 | dsk_setting['autoTransitioning'] = (data[3] != 0) 343 | dsk_setting['framesRemaining'] = data[4] 344 | 345 | def recvAuxS(self, data): 346 | auxIndex = data[0] 347 | self.state[auxIndex] = struct.unpack('!H', data[2:4])[0] 348 | 349 | def recvCCdo(self, data): 350 | input_num = data[1] 351 | domain = data[2] 352 | feature = data[3] 353 | feature_label = feature 354 | try: 355 | if domain == 0: 356 | feature_label = self.LABELS_CC_LENS_FEATURE[feature] 357 | elif domain == 1: 358 | feature_label = self.LABELS_CC_CAM_FEATURE[feature] 359 | elif domain == 8: 360 | feature_label = self.LABELS_CC_CHIP_FEATURE[feature] 361 | self.cameracontrol.setdefault(input_num, {}).setdefault('features', {}).setdefault(self.LABELS_CC_DOMAIN[domain], {})[feature_label] = bool(data[4]) 362 | except KeyError: 363 | print("Warning: CC Feature not recognized (no label)") 364 | 365 | def recvCCdP(self, data): 366 | input_num = data[1] 367 | domain = data[2] 368 | feature = data[3] 369 | feature_label = feature 370 | val = None 371 | val_translated = None 372 | if domain == 0: #lens 373 | if feature == 0: #focus 374 | val = val_translated = struct.unpack('!h', data[16:18])[0] 375 | elif feature == 1: #auto focused 376 | pass 377 | elif feature == 3: #iris 378 | val = val_translated = struct.unpack('!h', data[16:18])[0] 379 | elif feature == 9: #zoom 380 | val = val_translated = struct.unpack('!h', data[16:18])[0] 381 | elif domain == 1: #camera 382 | if feature == 1: #gain 383 | val = struct.unpack('!h', data[16:18])[0] 384 | val_translated = self.VALUES_CC_GAIN.get(val, 'unknown') 385 | elif feature == 2: #white balance 386 | val = struct.unpack('!h', data[16:18])[0] 387 | val_translated = self.VALUES_CC_WB.get(val, val + 'K') 388 | elif feature == 5: #shutter 389 | val = struct.unpack('!h', data[18:20])[0] 390 | val_translated = self.VALUES_CC_SHUTTER.get(val, 'off') 391 | elif domain == 8: #chip 392 | val_keys_color = ['R','G','B','Y'] 393 | if feature == 0: #lift 394 | val = dict(zip(val_keys_color, struct.unpack('!hhhh', data[16:24]))) 395 | val_translated = {k: float(v)/4096 for k, v in val.items()} 396 | elif feature == 1: #gamma 397 | val = dict(zip(val_keys_color, struct.unpack('!hhhh', data[16:24]))) 398 | val_translated = {k: float(v)/8192 for k, v in val.items()} 399 | elif feature == 2: #gain 400 | val = dict(zip(val_keys_color, struct.unpack('!hhhh', data[16:24]))) 401 | val_translated = {k: float(v)*16/32767 for k, v in val.items()} 402 | elif feature == 3: #aperture 403 | pass # no idea - todo 404 | elif feature == 4: #contrast 405 | val = struct.unpack('!h', data[18:20])[0] 406 | val_translated = float(val) / 4096 407 | elif feature == 5: #luminance 408 | val = struct.unpack('!h', data[16:18])[0] 409 | val_translated = float(val) / 2048 410 | elif feature == 6: #hue-saturation 411 | val_keys = ['hue', 'saturation'] 412 | val = dict(zip(val_keys, struct.unpack('!hh', data[16:20]))) 413 | val_translated = {} 414 | val_translated['hue'] = float(val['hue']) * 360 / 2048 + 180 415 | val_translated['saturation'] = float(val['saturation']) / 4096 416 | try: 417 | if domain == 0: 418 | feature_label = self.LABELS_CC_LENS_FEATURE[feature] 419 | elif domain == 1: 420 | feature_label = self.LABELS_CC_CAM_FEATURE[feature] 421 | elif domain == 8: 422 | feature_label = self.LABELS_CC_CHIP_FEATURE[feature] 423 | self.cameracontrol.setdefault(input_num, {}).setdefault('state_raw', {}).setdefault(self.LABELS_CC_DOMAIN[domain], {})[feature_label] = val 424 | self.cameracontrol.setdefault(input_num, {}).setdefault('state', {}).setdefault(self.LABELS_CC_DOMAIN[domain], {})[feature_label] = val_translated 425 | except KeyError: 426 | print("Warning: CC Feature not recognized (no label)") 427 | 428 | def recvRCPS(self, data): 429 | player_num = data[0] 430 | player = self.state['mediaplayer'].setdefault(player_num, {}) 431 | player['playing'] = bool(data[1]) 432 | player['loop'] = bool(data[2]) 433 | player['beginning'] = bool(data[3]) 434 | player['clip_frame'] = struct.unpack('!H', data[4:6])[0] 435 | 436 | def recvMPCE(self, data): 437 | player_num = data[0] 438 | player = self.state['mediaplayer'].setdefault(player_num, {}) 439 | player['type'] = {1: 'still', 2: 'clip'}.get(data[1]) 440 | player['still_index'] = data[2] 441 | player['clip_index'] = data[3] 442 | 443 | def recvMPSp(self, data): 444 | self.config['mediapool'].setdefault(0, {})['maxlength'] = struct.unpack('!H', data[0:2])[0] 445 | self.config['mediapool'].setdefault(1, {})['maxlength'] = struct.unpack('!H', data[2:4])[0] 446 | 447 | def recvMPCS(self, data): 448 | bank = data[0] 449 | clip_bank = self.state['mediapool'].setdefault('clips', {}).setdefault(bank, {}) 450 | clip_bank['used'] = bool(data[1]) 451 | clip_bank['filename'] = self.convert_cstring(data[2:18]) 452 | clip_bank['length'] = struct.unpack('!H', data[66:68])[0] 453 | 454 | def recvMPAS(self, data): 455 | bank = data[0] 456 | audio_bank = self.state['mediapool'].setdefault('audio', {}).setdefault(bank, {}) 457 | audio_bank['used'] = bool(data[1]) 458 | audio_bank['filename'] = self.convert_cstring(data[18:34]) 459 | 460 | def recvMPfe(self, data): 461 | if data[0] != 0: 462 | return 463 | bank = data[3] 464 | still_bank = self.state['mediapool'].setdefault('stills', {}).setdefault(bank, {}) 465 | still_bank['used'] = bool(data[4]) 466 | still_bank['hash'] = data[5:21].decode("utf-8") 467 | filename_length = data[23] 468 | still_bank['filename'] = data[24:(24+filename_length)].decode("utf-8") 469 | 470 | def recvAMIP(self, data): 471 | channel = struct.unpack('!H', data[0:2])[0] 472 | channel_config = self.system_config['audio'].setdefault(channel, {}) 473 | channel_config['fromMediaPlayer'] = bool(data[6]) 474 | channel_config['plug'] = data[7] 475 | 476 | channel_state = self.state['audio'].setdefault(channel, {}) 477 | channel_state['mix_option'] = data[8] 478 | channel_state['volume'] = struct.unpack('!H', data[10:12])[0] 479 | channel_state['balance'] = struct.unpack('!h', data[12:14])[0] 480 | 481 | def recvAMMO(self, data): 482 | self.state['audio']['master_volume'] = struct.unpack('!H', data[0:2])[0] 483 | 484 | def recvAMmO(self, data): 485 | monitor = self.state['audio'].setdefault('monitor', {}) 486 | monitor['enabled'] = bool(data[0]) 487 | monitor['volume'] = struct.unpack('!H', data[2:4])[0] 488 | monitor['mute'] = bool(data[4]) 489 | monitor['solo'] = bool(data[5]) 490 | monitor['solo_input'] = struct.unpack('!H', data[6:8])[0] 491 | monitor['dim'] = bool(data[8]) 492 | 493 | def recvAMTl(self, data): 494 | src_count = struct.unpack('!H', data[0:2])[0] 495 | for i in range(src_count): 496 | num = 2+i*3 497 | channel = struct.unpack('!H', data[num:num+2])[0] 498 | self.state['audio'].setdefault('tally', {})[channel] = bool(data[num+2]) 499 | 500 | def recvTlIn(self, data): 501 | src_count = struct.unpack('!H', data[0:2])[0] 502 | for i in range(2, src_count+2): 503 | self.state['tally_by_index'][str(i-1)] = self.parseBitmask(data[i], ['prv', 'pgm']) 504 | self.tallyHandler(self) 505 | 506 | def recvTlSr(self, data): 507 | src_count = struct.unpack('!H', data[0:2])[0] 508 | for i in range(2, src_count*3+2): 509 | source = struct.unpack('!H', data[i:i+2])[0] 510 | self.state['tally'][source] = self.parseBitmask(data[i+2], ['prv', 'pgm']) 511 | self.tallyHandler(self) 512 | 513 | def recvTime(self, data): 514 | self.state['last_state_change'] = struct.unpack('!BBBB', data[0:4]) 515 | 516 | ## user functions 517 | # used to register a function that should be called when a change is received from the atem 518 | def handleAtemChange(self, func, method=''): 519 | pass # todo: if given, filter by method parameter, then call func with atem method 520 | 521 | # used to register a function that should be called when a change is set in an attribute 522 | def handleStateChange(self, func, name=[]): 523 | pass # todo: if given, filer by name path, then call func with name path and val 524 | 525 | # used to get ranges and options lists for attributes 526 | def getOption(self, name): 527 | return # todo: return range/list of options for the given attribute name path 528 | 529 | def dump(self): 530 | pprint(self.system_config) 531 | pprint(self.status) 532 | pprint(self.state) 533 | pprint(self.cameracontrol) 534 | 535 | 536 | def init(a): 537 | a.connectToSwitcher() 538 | while True: 539 | a.waitForPacket() 540 | 541 | if __name__ == '__main__': 542 | import config 543 | a = Atem(config.address) 544 | a.connectToSwitcher() 545 | def tallyWatch(atem): 546 | print("Tally changed") 547 | pprint(atem.state['tally']) 548 | pprint(atem.state['tally_by_index']) 549 | def inputWatch(atem): 550 | print("PGM input changed") 551 | pprint(atem.state['program']) 552 | a.tallyHandler = tallyWatch 553 | a.pgmInputHandler = inputWatch 554 | while True: 555 | a.waitForPacket() -------------------------------------------------------------------------------- /config.py.dist: -------------------------------------------------------------------------------- 1 | address='127.0.0.1' 2 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | # Ideas for re-structuring this lib 2 | 3 | ## OOP based model 4 | 5 | - AtemConnection Class as master 6 | - Several classes for handling of subaspects: "feature classes" 7 | - Feature classes register function for handling messages from the switcher 8 | - Feature classes provide functions for: 9 | - retrieving values 10 | - listen on value change 11 | - manipulating features 12 | - getting option lists for enum values 13 | 14 | ## Feature classes 15 | 16 | - AtemFacts 17 | - version 18 | - topologies 19 | - power 20 | - etc. 21 | - AtemMacros 22 | - controlling macros 23 | - AtemConfig 24 | - down conversion 25 | - video standard 26 | - input properties 27 | - multiviewers 28 | - AtemMixing 29 | - program and preview 30 | - cut/auto 31 | - FTB 32 | - transitions 33 | - ... 34 | - AtemKeyerBase 35 | - AtemKeyer 36 | - AtemDownstreamKeyer 37 | - AtemVirtualSource 38 | - AtemColorGen 39 | - AtemMediaplayer 40 | - AtemMediaPool 41 | - AtemAux 42 | - AtemCameraControl 43 | - AtemSuperSource 44 | - AtemAudioMixer 45 | - AtemTally 46 | - video tallies 47 | - audio tallies --------------------------------------------------------------------------------