├── .gitignore ├── .prettierrc ├── examples ├── config.json.example ├── pro6-cli └── ProRemote.html ├── yaml2md ├── README.md ├── yaml2asyncapi ├── Pro6 └── api.yaml ├── Pro6.md └── Pro7.md /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /examples/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "HOST" : "localhost", 3 | "PORT" : "60157", 4 | "CONTROL_PASSWORD" : "control", 5 | "OBSERVE_PASSWORD" : "observe", 6 | "STAGEDISPLAY_PASSWORD" : "stagedisplay" 7 | } -------------------------------------------------------------------------------- /yaml2md: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import yaml 5 | 6 | output = '' 7 | 8 | 9 | def e(s='', end='\n'): 10 | global output 11 | output += s + end 12 | 13 | 14 | def format_type(ttype): 15 | if ttype not in ['bool', 'int', 'float', 'string', 'object', 'list']: 16 | if type(ttype) is list: 17 | retval = [] 18 | for t in ttype: 19 | retval.append(format_type(t)) 20 | return '|'.join(retval) 21 | 22 | if ttype in types: 23 | link_to = ttype.lower() 24 | ttype = f'[`{ttype}`](#{link_to})' 25 | if 'list[' in ttype: 26 | inner_type = ttype.replace('list[', '')[:-1] 27 | link_to = inner_type.lower() 28 | ttype = f'list[[`{inner_type}`](#{link_to})]' 29 | else: 30 | ttype = f'`{ttype}`' 31 | return ttype 32 | 33 | 34 | if len(sys.argv) == 1: 35 | print(f''' 36 | USAGE: 37 | 38 | {sys.argv[0]} yaml_file output_name 39 | 40 | Will parse an API yaml file to Markdown. 41 | ''') 42 | exit() 43 | 44 | source = sys.argv[1] 45 | target = source + '.md' 46 | target = target.replace('.yaml.md', '.md') 47 | target = target.replace('.yml.md', '.md') 48 | 49 | if len(sys.argv) == 3: 50 | target = sys.argv[2] 51 | 52 | # read input yaml file 53 | with open(source, 'r') as f: 54 | t = f.read() 55 | data = yaml.load(t, Loader=yaml.CLoader) 56 | 57 | 58 | # handle metadata 59 | e(f'# {data["application"]}') 60 | e() 61 | e(f'{data["description"]}') 62 | 63 | # handle channels 64 | for channelkey in data['channels']: 65 | channel = data['channels'][channelkey] 66 | name = channel.get('name', channelkey).strip() 67 | desc = channel['description'].strip() 68 | 69 | e(f'\n## {name}') 70 | e() 71 | e(desc) 72 | 73 | types = channel.get('types', {}) 74 | messages = channel.get('messages', {}) 75 | actions = channel.get('actions', {}) 76 | undocumented = channel.get('undocumented', {}) 77 | 78 | # output the typed data definitions 79 | e(f'\n### {name} Types') 80 | for typekey in types: 81 | t = types[typekey] 82 | tname = t.get('name', typekey).strip() 83 | ttype = t['type'] 84 | tdesc = t.get('description', '').strip() 85 | texample = t.get('example', '').strip() 86 | formatted_type = format_type(ttype) 87 | 88 | e(f'\n#### {tname}') 89 | e(f'Base Type: {formatted_type}') 90 | e() 91 | 92 | if tdesc != '': 93 | e(tdesc) 94 | e() 95 | 96 | if ttype == 'object': 97 | # print(t) 98 | e('##### PARAMETERS:\n') 99 | 100 | for paramname in t['parameters']: 101 | param = t['parameters'][paramname] 102 | pdesc = f'always `{param}`' 103 | pexample = '' 104 | poptions = None 105 | if type(param) is str: 106 | ptype = 'string' 107 | elif type(param) is int: 108 | ptype = 'int' 109 | elif type(param) is float: 110 | ptype = 'float' 111 | elif type(param) is bool: 112 | ptype = 'bool' 113 | elif type(param) is list: 114 | ptype = 'list' 115 | poptions = param 116 | else: 117 | ptype = param.get('type', 'string') 118 | pdesc = param.get('description', '') 119 | poptions = param.get('options', None) 120 | pexample = param.get('example', '') 121 | 122 | if ptype == 'union': 123 | ptype = param.get('types', 'union') 124 | 125 | pformatted_type = format_type(ptype) 126 | e(f'- **{paramname}** ({pformatted_type})') 127 | extras = [] 128 | if (poptions is not None): 129 | poptions = [f'`{x}`' for x in poptions] 130 | extras.append(f' Must be one of these: {poptions}') 131 | if (pdesc != ''): 132 | # need to indent every description line 133 | for line in pdesc.split('\n'): 134 | extras.append(f' {line}') 135 | if (pexample != ''): 136 | if ptype == 'string': 137 | pexample = f'"{pexample}"' 138 | if '`' not in pexample: 139 | pexample = f'```{pexample}```' 140 | extras.append(f' {pexample}') 141 | if len(extras) > 0: 142 | e() 143 | e('\n'.join(extras)) 144 | e() 145 | if texample != '': 146 | if '`' not in texample: 147 | texample = f'```{texample}```' 148 | 149 | e(f'\n##### EXAMPLE:\n') 150 | e(texample) 151 | e() 152 | e('********************************\n') 153 | 154 | # output the list of pushy messages (with anchor links) 155 | # output the action messages with their responses 156 | # output the response-only messages 157 | 158 | print(output) 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProPresenter-API 2 | 3 | ## 7.9 UPDATE & PUBLIC API 4 | 5 | The upcoming 7.9 release of ProPresenter will include an officially supported public API. 6 | 7 | The official documentation page is here: 8 | 9 | https://renewedvision.com/propresenter/api/ 10 | 11 | The full auto-generated Swagger HTML is a bit easier to navigate, and it is here: 12 | 13 | https://renewedvision.com/api_spec/index.html 14 | 15 | And, the actual source OPENAPI spec document (JS) is here: 16 | 17 | https://renewedvision.com/api_spec/swagger.json 18 | 19 | But I'm mirroring it here as a formatted JSON document too. 20 | 21 | [pro7.9.openapi-spec.json](https://github.com/jeffmikels/ProPresenter-API/blob/master/pro7.9.openapi-spec.json) 22 | 23 | ## NETWORK LINK 24 | 25 | For information on the ProPresenter Network Link protocol, see my other repo here: 26 | 27 | https://github.com/jeffmikels/pro_presenter_network_link_tools 28 | 29 | ## REVERSE-ENGINEERED STAGE DISPLAY & REMOTE DOCUMENTATION 30 | 31 | Documenting RenewedVision's undocumented network protocols with examples 32 | 33 | See below for changes to this repository: 34 | 35 | For ProPresenter 6, go here: 36 | https://jeffmikels.github.io/ProPresenter-API/Pro6/ 37 | 38 | For ProPresenter 7, go here: 39 | https://jeffmikels.github.io/ProPresenter-API/Pro7/ 40 | 41 | For a Node Library implementing this protocol, go here: 42 | https://github.com/utopiantools/node-propresenter.git 43 | 44 | or 45 | 46 | ``` 47 | npm install https://github.com/utopiantools/node-propresenter.git 48 | ``` 49 | 50 | For an example of a real app built with node.js that uses this API to turn ProPresenter into a universal remote control: 51 | https://github.com/jeffmikels/propresenter-watcher 52 | 53 | For documentation regarding the new Pro7 file format: 54 | https://github.com/greyshirtguy/ProPresenter7-Proto 55 | 56 | ## CHANGES 57 | 58 | Recently, I have begun to migrate all the documentation into machine-readable formats 59 | so that as the Pro7 API improves, we will be able to more readily track the changes. 60 | 61 | It is also my hope that this effort will inspire RenewedVision to join the documentation 62 | effort directly. 63 | 64 | ## API METHODOLOGY 65 | 66 | AsyncAPI is a great system for defining APIs and for generating code and documentation, but editing it by hand 67 | seemed cumbersome to me, so I built my own solution. 68 | 69 | 1. I converted our former Markdown documentation files into a `api.yaml` format that is inspired by AsyncAPI, but is more easily edited. 70 | 2. I wrote a Python script to automatically convert my `api.yaml` files into AsyncAPI 2.1.0 files. 71 | 3. I use the AsyncAPI command line generator commands to render out html files. 72 | 73 | ## API.YAML SPECIFICATIONS 74 | 75 | The `api.yaml` file begins with a section called `asyncapi`. That section may contain anything an AsyncAPI `2.1.0` file may contain. 76 | 77 | Then, there is a `types` section that holds defined data types similar to the way AsyncAPI does schemas. These will be translated into AsyncAPI schema nodes. 78 | 79 | Types are structured as follows: 80 | 81 | ```yaml 82 | typename: 83 | type: |- 84 | can be a built-in type like 85 | 86 | `integer`, `string`, `number`, `list`, `list[othertype]`, or `object` 87 | 88 | or it can be another defined type or list[othertype] 89 | parameters: 90 | field_1: 91 | type: typename 92 | field_2: constantvalue 93 | field_3: 94 | type: typename 95 | options: [option1, option2, option3] 96 | description: "each field can have a description" 97 | examples: ["each field can have an example"] 98 | example: 99 | field_1: an arbitrary value 100 | field_2: constantvalue 101 | field_3: option1 102 | ``` 103 | 104 | Then, there is the `channels` section which defines the main application message endpoints. ProPresenter exposes two channels, `/remote` and `/stagedisplay`. 105 | 106 | Each `channel` is structured as follows: 107 | 108 | ```yaml 109 | channels: 110 | channelname: 111 | summary: Simple name for this channel 112 | description: | 113 | Block markdown content describing this channel in detail 114 | actions: "list of actions, see below" 115 | ``` 116 | 117 | Each `channel` has a list of `actions` that can be sent or received. 118 | 119 | Actions may contain a `message` and or `response` and are structured as follows (NOTE: `payload` and `example` follow the same structure as a type, and the `example` must match the payload structure): 120 | 121 | ```yaml 122 | actionName: 123 | summary: simple name for this action 124 | description: markdown text describing this action 125 | message: 126 | payload: 127 | action: actionName 128 | field_2: someConstantValue 129 | field_3: 130 | type: string 131 | options: ["1", "2", "3"] 132 | example: "2" 133 | description: textual description 134 | example: 135 | response: 136 | payload: 137 | example: 138 | ``` 139 | 140 | # CONTRIBUTING 141 | 142 | To contribute, please just edit the `api.yaml` file. I will regenerate the other documentation myself until I get Github actions working. 143 | -------------------------------------------------------------------------------- /examples/pro6-cli: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | 3 | import json 4 | import sys 5 | import time 6 | import click # pip install click 7 | import websocket # pip install websocket-client 8 | 9 | 10 | class Pro6: 11 | 12 | def __init__(self, host='', port='', password='', type='remote'): 13 | 14 | self.password = password 15 | self.connected = False 16 | self.controlling = False 17 | 18 | if type == 'remote': 19 | self.wsurl = "ws://{}:{}/{}".format(host, port, 'remote') 20 | self.ws = websocket.create_connection(self.wsurl, timeout=2) 21 | # self.ws.on_message = on_message 22 | # self.ws.on_error = on_error 23 | # self.ws.on_close = on_close 24 | # self.ws.on_open = on_open 25 | self.connected = True 26 | self.authenticate() 27 | 28 | elif type == 'stagedisplay': 29 | self.wsurl = "ws://{}:{}/{}".format(host, port, 'stagedisplay') 30 | self.ws = websocket.WebSocketApp(self.wsurl) 31 | self.ws.on_message = self.on_message 32 | self.ws.on_error = self.on_error 33 | self.ws.on_open = self.auth_stagedisplay 34 | self.ws.run_forever() 35 | 36 | def on_message(self, ws, message): 37 | print(message) 38 | 39 | def on_error(self, ws, error): 40 | print(error) 41 | 42 | def auth_stagedisplay(self, ws): 43 | cmd = {"pwd":self.password,"ptl":"610","acn":"ath"} 44 | ws.send(json.dumps(cmd)) 45 | 46 | def send(self, obj, awaitAnswer=True): 47 | if self.connected: 48 | s = json.dumps(obj) 49 | # print('SENDING ', s) 50 | try: 51 | self.ws.send(s) 52 | if awaitAnswer: 53 | return json.loads(self.ws.recv()) 54 | except websocket._exceptions.WebSocketTimeoutException: 55 | return {"error":"Websocket Timeout. We expected a response, but got nothing. This happens whenever ProPresenter received an invalid command and ignored it."} 56 | else: 57 | return {"error":"Websocket Not Connected"} 58 | 59 | def authenticate(self): 60 | cmd = { 61 | 'action': 'authenticate', 62 | 'protocol': '600', 63 | 'password': self.password 64 | } 65 | return self.send(cmd) 66 | 67 | def get_library(self): 68 | cmd = {"action":"libraryRequest"} 69 | return self.send(cmd) 70 | 71 | def get_playlists(self): 72 | cmd = {"action":"playlistRequestAll"} 73 | return self.send(cmd) 74 | 75 | def get_presentation(self, path): 76 | cmd = { 77 | "action": "presentationRequest", 78 | "presentationPath": path, 79 | "presentationSlideQuality": 0, # disable slide images 80 | } 81 | return self.send(cmd) 82 | 83 | def get_current_presentation(self): 84 | cmd = { 85 | "action": "presentationCurrent", 86 | "presentationSlideQuality": 0, #disable slide images 87 | } 88 | return self.send(cmd) 89 | 90 | res = self.get_slide_index() 91 | if 'slideIndex' in res: 92 | res = self.trigger_slide(res['slideIndex']) 93 | if 'presentationPath' in res: 94 | return self.get_presentation(res['presentationPath']) 95 | 96 | def get_slide_index(self): 97 | cmd = {"action":"presentationSlideIndex"} 98 | return self.send(cmd) 99 | 100 | def trigger_slide(self, n): 101 | cmd = {"action":"presentationTriggerIndex","slideIndex":n} 102 | return self.send(cmd) 103 | 104 | def next_slide(self): 105 | curSlide = 0 106 | numSlides = 0 107 | res = self.get_current_presentation() 108 | for group in res['presentation']['presentationSlideGroups']: 109 | numSlides += len(group['groupSlides']); 110 | 111 | res = self.get_slide_index() 112 | curSlide = int(res['slideIndex']) 113 | if curSlide < numSlides-1: 114 | return self.trigger_slide(curSlide + 1) 115 | else: 116 | return self.trigger_slide(0) 117 | 118 | def prev_slide(self): 119 | curSlide = 0 120 | numSlides = 0 121 | res = self.get_current_presentation() 122 | for group in res['presentation']['presentationSlideGroups']: 123 | numSlides += len(group['groupSlides']); 124 | 125 | res = self.get_slide_index() 126 | curSlide = int(res['slideIndex']) 127 | if curSlide > 0: 128 | return self.trigger_slide(curSlide - 1) 129 | 130 | 131 | @click.command() 132 | @click.option('--action', default='', help='send a specific action') 133 | @click.option('--send', default=None, help='send a raw JSON command') 134 | @click.option('--next', is_flag=True, help='Go to the next slide.') 135 | @click.option('--prev', is_flag=True, help='Go to the previous slide.') 136 | @click.option('--trace', is_flag=True, help='Enable websocket tracing.') 137 | @click.option('--stagedisplay', is_flag=True, help='Act as stage display.') 138 | def command(action, send, next, prev, trace, stagedisplay): 139 | # load configuration 140 | config = json.load(open('config.json','r')) 141 | 142 | if trace: 143 | websocket.enableTrace(True) 144 | 145 | if stagedisplay: 146 | p6 = Pro6( 147 | host=config['HOST'], 148 | port=config['PORT'], 149 | password=config['STAGEDISPLAY_PASSWORD'], 150 | type='stagedisplay' 151 | ) 152 | 153 | else: 154 | p6 = Pro6(host=config['HOST'], port=config['PORT'], password=config['CONTROL_PASSWORD']) 155 | 156 | if next: 157 | print(json.dumps(p6.next_slide())) 158 | exit() 159 | 160 | if prev: 161 | print(json.dumps(p6.prev_slide())) 162 | exit() 163 | 164 | if action: 165 | print(json.dumps(p6.send({'action': action}))) 166 | exit() 167 | 168 | if send: 169 | print(json.dumps(p6.send(json.loads(send)))) 170 | exit() 171 | 172 | 173 | if __name__ == "__main__": 174 | command() 175 | exit() 176 | 177 | # ws = websocket.WebSocket() 178 | # ws.settimeout(2) 179 | # ws.connect(wsurl) 180 | # 181 | # # request control 182 | # result = authenticate(ws) 183 | # print (result) 184 | # 185 | # # get current slide index 186 | # ws.send('{"action":"presentationSlideIndex"}') 187 | # result = json.loads(ws.recv()) 188 | # if 'slideIndex' in result: 189 | # slideIndex = result['slideIndex'] 190 | # 191 | # # go to next slide 192 | # newSlideIndex = int(slideIndex) + 1 193 | # ws.send('{"action":"presentationTriggerIndex","slideIndex":'+str(newSlideIndex)+'}') 194 | # result = json.loads(ws.recv()) 195 | # print(result) 196 | # ws.close() 197 | -------------------------------------------------------------------------------- /yaml2asyncapi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import json 5 | from typing import Literal 6 | import yaml 7 | try: 8 | from yaml import CLoader as Loader, CDumper as Dumper 9 | except ImportError: 10 | from yaml import Loader, Dumper 11 | 12 | from yaml.representer import SafeRepresenter 13 | 14 | # for dumping literal strings as scalar literal blocks 15 | 16 | 17 | class LiteralString(str): 18 | pass 19 | 20 | 21 | def literal_presenter(dumper, data): 22 | if type(data) is tuple: 23 | data = ''.join(data) 24 | return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|') 25 | 26 | 27 | yaml.add_representer(LiteralString, literal_presenter) 28 | 29 | 30 | def type_to_schema(t): 31 | schema = {} 32 | description = t.get('description', '') 33 | 34 | if description != '': 35 | schema['description'] = LiteralString(description) 36 | 37 | # type 38 | typetype = t['type'] 39 | 40 | # if we are given a list of types, handle first 41 | if type(typetype) is list: 42 | oneof = [] 43 | for typeitem in typetype: 44 | if typeitem in refs: 45 | oneof.append({'$ref': refs[typeitem]}) 46 | else: 47 | oneof.append({'type': typeitem}) 48 | schema['oneOf'] = oneof 49 | elif typetype == 'int': 50 | schema['type'] = 'integer' 51 | elif typetype == 'bool': 52 | schema['type'] = 'boolean' 53 | elif typetype == 'object': 54 | schema['type'] = 'object' 55 | elif typetype == 'list': 56 | schema['type'] = 'array' 57 | elif 'list[' in typetype: 58 | schema['type'] = 'array' 59 | subtype = typetype.replace('list[', '')[:-1] 60 | if subtype in refs: 61 | schema['items'] = [{'$ref': refs[subtype]}] 62 | else: 63 | schema['items'] = [{'type': subtype}] 64 | elif typetype in refs: 65 | schema['$ref'] = refs[typetype] 66 | else: 67 | schema['type'] = typetype 68 | 69 | # parameters if there are some 70 | params = t.get('parameters', None) 71 | if params is not None: 72 | schema['properties'] = {} 73 | schema['required'] = [] 74 | optional = False 75 | for paramname in params: 76 | p = params[paramname] 77 | if type(p) is dict: 78 | if 'optional' in p and p['optional'] == True: 79 | optional = True 80 | pschema = type_to_schema(p) 81 | elif type(p) is list: 82 | oneof = [] 83 | for typeitem in p: 84 | if typeitem in refs: 85 | oneof.append({'$ref': refs[typeitem]}) 86 | else: 87 | oneof.append({'type': typeitem}) 88 | pschema = {'oneOf': oneof} 89 | 90 | else: 91 | pschema = {'const': p} 92 | 93 | if optional == False: 94 | schema['required'].append(paramname) 95 | 96 | schema['properties'][paramname] = pschema 97 | schema['additionalProperties'] = False 98 | 99 | # check for 'options' 100 | options = t.get('options', None) 101 | if options is not None: 102 | schema['enum'] = options 103 | 104 | # example 105 | example = t.get('example', '') 106 | if example != '': 107 | exampleObject = example 108 | if type(example) is str: 109 | try: 110 | exampleObject = json.loads(example.replace('`', '')) 111 | except: 112 | exampleObject = LiteralString(example) 113 | schema['examples'] = [exampleObject] 114 | 115 | return schema 116 | 117 | 118 | # ============================= 119 | # BEGIN MAIN 120 | # ============================= 121 | if len(sys.argv) == 1: 122 | print(f''' 123 | USAGE: 124 | 125 | {sys.argv[0]} yaml_file output_name 126 | 127 | Will parse my simple API yaml file to AsyncAPI. 128 | ''') 129 | exit() 130 | 131 | source = sys.argv[1] 132 | target = source + '-asyncapi.yaml' 133 | target = target.replace('.yaml-asyncapi.yaml', '-asyncapi.yaml') 134 | target = target.replace('.yml-asyncapi.yaml', '-asyncapi.yaml') 135 | 136 | if len(sys.argv) == 3: 137 | target = sys.argv[2] 138 | if target == source: 139 | print('ERROR: source file and target file have the same name') 140 | exit(1) 141 | 142 | # read input yaml file 143 | print(f'READING: {source}') 144 | with open(source, 'r') as f: 145 | t = f.read() 146 | data = yaml.load(t, Loader=Loader) 147 | 148 | print('PROCESSING...') 149 | 150 | # include root level asyncapi data 151 | output = data['asyncapi'] 152 | output.update({'components': {'schemas': {}, 'messages': {}}, 'channels': {}}) 153 | 154 | refs = {} 155 | 156 | # create schemas from the types in two passes. 157 | # pass 1 creates the type refs 158 | for typename in data['types']: 159 | if typename not in refs: 160 | refs[typename] = f'#/components/schemas/{typename}' 161 | 162 | # pass 2 actually creates the types 163 | for typename in data['types']: 164 | t = data['types'][typename] 165 | # print(t) 166 | schema = type_to_schema(t) 167 | output['components']['schemas'][typename] = schema 168 | 169 | # create pub/sub messages from the message / response payloads 170 | for channel in ['remote', 'stagedisplay']: 171 | channelPath = '/'+channel 172 | summary = data['channels'][channel]['summary'] 173 | description = data['channels'][channel]['description'] 174 | output['channels'][channelPath] = { 175 | 'publish': { 176 | 'summary': summary, 177 | 'description': f'Send messages to the API\n\n{description}', 178 | 'message': { 179 | 'oneOf': [] 180 | } 181 | }, 182 | 'subscribe': { 183 | 'summary': summary, 184 | 'description': f'Receive messages from the API\n\n{description}', 185 | 'message': { 186 | 'oneOf': [] 187 | } 188 | }, 189 | } 190 | 191 | for actionName in data['channels'][channel]['actions']: 192 | action = data['channels'][channel]['actions'][actionName] 193 | # print(action) 194 | pubName = f'{actionName}__pub' 195 | subName = f'{actionName}__sub' 196 | messageNames = {'publish': pubName, 'subscribe': subName} 197 | messageTitles = { 198 | 'publish': f'{actionName} (publish)', 199 | 'subscribe': f'{actionName} (subscribe)', 200 | } 201 | 202 | for pubsub in ['publish', 'subscribe']: 203 | messageName = messageNames[pubsub] 204 | message = {} 205 | message['name'] = messageName # machine readable 206 | message['title'] = messageTitles[pubsub] # human readable 207 | 208 | if 'summary' in action: 209 | message['summary'] = action['summary'] 210 | 211 | # generate descriptive text for this message 212 | desc = action.get('description', '') 213 | if desc is None: 214 | desc = '' 215 | 216 | if pubsub == 'subscribe' and 'message' in action: 217 | foreign = messageNames['publish'] 218 | linkref = f'#message-{foreign}' 219 | linktext = messageTitles["publish"] 220 | desc += f'\n\n**RELATED COMMAND**: [{linktext}]({linkref})\n' 221 | if pubsub == 'subscribe' and 'message' not in action: 222 | desc = desc.strip() + '\n\nTHIS IS A SUBSCRIBE-ONLY MESSAGE' 223 | 224 | if pubsub == 'publish' and 'response' in action: 225 | foreign = messageNames['subscribe'] 226 | linkref = f'#message-{foreign}' 227 | linktext = messageTitles["subscribe"] 228 | desc += f'\n\n**RELATED RESPONSE**: [{linktext}]({linkref})\n' 229 | # message['x-response'] = {'$ref': f'#/components/messages/{foreign}'} 230 | 231 | if pubsub == 'publish' and 'response' not in action: 232 | desc = desc.strip() + '\n\nNO RESPONSE' 233 | message['description'] = LiteralString(desc) 234 | 235 | key = 'response' 236 | if pubsub == 'publish': 237 | key = 'message' 238 | if key in action and action[key] != 'none': 239 | refstring = f'#/components/messages/{messageName}' 240 | refs[messageName] = refstring 241 | 242 | # handle payload ... could be a 'type' or a 'payload' 243 | payload = action[key].get('payload', None) 244 | if payload is not None: 245 | forschema = {'type': 'object', 'parameters': payload} 246 | message['payload'] = type_to_schema(forschema) 247 | else: 248 | payloadtype = action[key].get('type', None) 249 | if payloadtype in refs: 250 | message['payload'] = {'$ref': refs[payloadtype]} 251 | else: 252 | print('ERROR: message payload was empty and no type was specified') 253 | print('here is the offending action') 254 | print(json.dumps(action, indent=2)) 255 | message['payload'] = {} 256 | 257 | # handle example 258 | if 'example' in action[key]: 259 | example = action[key]['example'] 260 | if type(example) is dict: 261 | exampleObject = example 262 | elif type(example) is str: 263 | jsonexample = action[key]['example'].replace('`', '') 264 | try: 265 | exampleObject = json.loads(jsonexample) 266 | except: 267 | exampleObject = LiteralString(jsonexample) 268 | message['examples'] = [{'payload': exampleObject}] 269 | 270 | # add to messages component 271 | output['components']['messages'][messageName] = message 272 | 273 | # add ref to the channel/pubsub/messages 274 | output['channels'][channelPath][pubsub]['message']['oneOf'].append( 275 | {'$ref': refstring, } 276 | ) 277 | 278 | # print('== DEBUG ==') 279 | # print(json.dumps(action, indent=2)) 280 | # print(yaml.dump(message, sort_keys=False, indent=2, width=60, default_flow_style=False)) 281 | # input('Hit enter to continue ... ') 282 | 283 | print(f'WRITING: {target}') 284 | o = yaml.dump(output, sort_keys=False, indent=2, width=120, default_flow_style=False) 285 | with open(target, 'w') as of: 286 | of.write(o) 287 | print('DONE') 288 | -------------------------------------------------------------------------------- /examples/ProRemote.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ProRemote 4 | 319 | 320 | 321 |
322 |
323 |
324 |
325 |
326 | 327 | -------------------------------------------------------------------------------- /Pro6/api.yaml: -------------------------------------------------------------------------------- 1 | # This is a machine-readable format for the ProPresenter (6) Remote API 2 | # it was inspired by the asyncapi, but is written to keep commands 3 | # and responses together 4 | # 5 | # The advantage of asyncapi is that it is a standardized specification 6 | # structure for APIs, but the disadvantage is that it has no real 7 | # notion of the request-response message paradigm that ProPresenter uses. 8 | # 9 | # As a result, I have written this document by hand, and have written 10 | # my own translator to convert this document into asyncapi format 11 | # to use its tools for documentation generation 12 | asyncapi: 13 | asyncapi: '2.1.0' 14 | info: 15 | title: ProPresenter API 16 | version: '6' 17 | description: | 18 | Documenting RenewedVision's undocumented network protocols with examples 19 | 20 | This document refers to **ProPresenter 6.** 21 | 22 | Warning: Be careful! It's easy to CRASH ProPresenter when sending invalid messages! 23 | 24 | 25 | Both the Remote Control and the Stage Display protocols are unencrypted text-based websocket connections from the client to the ProPresenter instance. 26 | 27 | NOTE: if both the Remote Control and the Stage Display interface are enabled in ProPresenter, they both operate over the Remote Control network port. 28 | 29 | "Channels" refers to the communication channels opened up by ProPresenter. 30 | Both versions 6 and 7 expose two channels. One is the "remote" channel, and the 31 | other is the "stagedisplay" channel. They have two separate websocket endpoints 32 | that take the following form: 33 | 34 | ``` 35 | ws://[hostname]:[port]/[channel] 36 | ``` 37 | defaultContentType: application/json 38 | servers: 39 | pro6: 40 | protocol: ws 41 | url: "ws://{host}:{port}/{channel_name}" 42 | description: | 43 | Connecting to ProPresenter involves making a Websocket connection using 44 | this address and then immediately following the connection with an 45 | authentication message (as described below) for the relevant "channel." 46 | variables: 47 | host: 48 | description: The hostname or ip address of the computer running ProPresenter 6 49 | port: 50 | description: The port specified in the ProPresenter Network settings 51 | channel_name: 52 | enum: 53 | - remote 54 | - stagedisplay 55 | description: | 56 | *remote* for the remote control channel, 57 | or *stagedisplay* for the stage display channel 58 | 59 | types: 60 | # built-in types bool, int, float, string, object, list 61 | # typed lists may be described as list[othertype] 62 | 63 | # general types 64 | boolint: 65 | type: integer 66 | description: 0 for false, 1 for true, (in Pro6, these sometimes show up as strings) 67 | 68 | color: 69 | type: string 70 | description: | 71 | four space-delimited doubles describing the r,g,b,a values of a color from 0-1 72 | 73 | example: | 74 | "0.5 0.8 1 0.5" 75 | 76 | itemLocation: 77 | type: string 78 | description: | 79 | A slash-escaped full pathname, file basename, or playlist location. 80 | 81 | Pathname locations need to escape the slashes BEFORE they are encoded 82 | to JSON. 83 | 84 | ``` 85 | \/Path\/To\/ProPresenter\/Library\/Song 1 Title.pro6 86 | ``` 87 | 88 | Playlist locations are specified by a zero-based index of the playlist 89 | and playlist group in which the item may be found. 90 | 91 | The first item in the first playlist would be `"0:0"`. 92 | 93 | If the first playlist item is a playlist group, then the first item 94 | in the first playlist in the first playlist group would be `"0.0:0"`. 95 | 96 | That is, the generalized location is this: 97 | 98 | `playlist_index[.child_playlist_index[.deeper_child_index[...]]]:item` 99 | 100 | NOTE: Pro6 supports full pathnames or basenames. 101 | example: "\\/Path\\/To\\/ProPresenter\\/Library\\/Song 1 Title.pro6" 102 | 103 | # presentation types 104 | presentationSlide: 105 | type: object 106 | parameters: 107 | slideEnabled: 108 | type: bool 109 | slideNotes: 110 | type: string 111 | slideAttachmentMask: 112 | type: integer 113 | description: unknown 114 | slideText: 115 | type: string 116 | slideImage: 117 | type: string 118 | description: base64 encoded jpeg image 119 | slideIndex: 120 | type: integer 121 | description: | 122 | it is the zero-based index of the slide in a presentation 123 | 124 | NOTE: it is sometimes transmitted as a JSON string and sometimes a JSON number 125 | slideTransitionType: 126 | type: integer 127 | description: | 128 | `-1` for default transition or another number for a different transition 129 | slideLabel: 130 | type: string 131 | slideColor: 132 | type: color 133 | presentationSlideGroup: 134 | type: object 135 | parameters: 136 | groupsummary: 137 | type: string 138 | groupColor: 139 | type: color 140 | groupSlides: 141 | type: list[presentationSlide] 142 | presentation: 143 | type: object 144 | parameters: 145 | presentationSlideGroups: 146 | type: list[presentationSlideGroup] 147 | presentationsummary: 148 | type: string 149 | presentationHasTimeline: 150 | type: boolint 151 | presentationCurrentLocation: 152 | type: itemLocation 153 | description: | 154 | This field contains the location of the *currently active* presentation if there is one. 155 | 156 | It only refers to the location of *this* presentation if we asked for `presentationCurrent` 157 | 158 | presentationTriggerMessage: 159 | pushy: true 160 | type: object 161 | parameters: 162 | action: presentationTriggerIndex 163 | slideIndex: 164 | type: integer 165 | presentationPath: 166 | type: itemLocation 167 | description: | 168 | If the current presentation was selected from a playlist, it will report the 169 | playlist location (i.e. `0:0`), but if it was selected from the Library directly, 170 | the path value will always be the *basename* only. 171 | 172 | # playlist types for audio or presentation playlists 173 | playlistItemType: 174 | type: object 175 | parameters: 176 | playlistItemType: 177 | type: string 178 | options: 179 | - playlistItemTypeAudio 180 | - playlistItemTypePresentation 181 | playlistItemLocation: 182 | type: itemLocation 183 | playlistItemsummary: 184 | type: string 185 | playlistItemArtist: 186 | type: string 187 | description: only present for playlistItemTypeAudio 188 | playlistTypePlaylist: 189 | type: object 190 | parameters: 191 | playlistLocation: 192 | type: itemLocation 193 | playlistType: playlistTypePlaylist 194 | playlistsummary: 195 | type: string 196 | playlist: 197 | type: list[playlistItemType] 198 | playlistTypeGroup: 199 | type: object 200 | parameters: 201 | playlistType: playlistTypeGroup 202 | playlistsummary: 203 | type: string 204 | playlistLocation: 205 | type: itemLocation 206 | playlist: 207 | type: list[playlistUnionType] 208 | playlistUnionType: 209 | type: 210 | - playlistTypeGroup 211 | - playlistTypePlaylist 212 | 213 | 214 | # audio types 215 | audioPlayPause: 216 | type: string 217 | options: 218 | - Play 219 | - Pause 220 | description: in ProPresenter 7, the "Play" option becomes "Playing" 221 | 222 | # clock types 223 | clockTimeString: 224 | type: string 225 | description: | 226 | - real times are formatted as `"H:MM:SS"` (no padding on the leading H) 227 | - negative times are possible with a leading `-`. 228 | - empty times are `"--:--:--"` 229 | 230 | clockType: 231 | type: integer 232 | options: 233 | - 0 234 | - 1 235 | - 2 236 | description: | 237 | `0` is Countdown, `1` is Countdown to Time, `2` is Elapsed Time 238 | 239 | # in the response from clockRequest 240 | clockDetail: 241 | type: object 242 | parameters: 243 | clockType: 244 | type: clockType 245 | clockState: 246 | type: bool 247 | description: running is `true`, not running is `false` 248 | clocksummary: 249 | type: string 250 | clockIsPM: 251 | type: boolint 252 | clockTime: 253 | type: clockTimeString 254 | clockEndTime: 255 | type: clockTimeString 256 | clockDuration: 257 | type: clockTimeString 258 | clockOverrun: 259 | type: bool 260 | description: indicates if clock should keep running after it hits the endtime. 261 | 262 | clockQuickInfo: 263 | type: list 264 | items: 265 | - 266 | type: clockType 267 | - 268 | type: integer 269 | description: most recently changed value as indicated by the `action` 270 | - 271 | type: clockTimeString 272 | 273 | # in the response from clockStart or clockStop 274 | clockStartStopMessage: 275 | pushy: true 276 | type: object 277 | parameters: 278 | action: clockStartStop 279 | clockTime: 280 | type: clockTimeString 281 | clockState: 282 | type: boolint 283 | description: same as clockState in clockDetail, but given as a boolint 284 | clockIndex: 285 | type: integer 286 | description: zero-based clock index, clock lists are in a stable order 287 | clockInfo: 288 | type: clockQuickInfo 289 | 290 | clockCurrentTimesMessage: 291 | pushy: true 292 | type: object 293 | parameters: 294 | action: clockCurrentTimes 295 | clockTimes: 296 | type: list[clockTimeString] 297 | example: | 298 | `{"action":"clockCurrentTimes","clockTimes":["0:10:00","--:--:--","13:52:23"]}` 299 | 300 | # message types 301 | screenMessage: 302 | type: object 303 | parameters: 304 | messageComponents: 305 | type: list[string] 306 | description: | 307 | Each string is either a literal string or a variable expansion, which will be 308 | concatenated together to make the final on screen message. 309 | 310 | The content inside a `${}` is referred to as a `messageKey` in later commands 311 | messageTitle: 312 | type: string 313 | example: 314 | { 315 | "messageComponents": [ 316 | "Session will begin in: ", 317 | "${Countdown 1: H:MM:SS}", 318 | "." 319 | ], 320 | "messageTitle": "Countdown" 321 | } 322 | 323 | # stage display types 324 | stageDisplayFrameGeometry: 325 | type: string 326 | description: | 327 | Frame Geometry is delivered in x,y coordinates based on screen percentages in the 328 | following format: 329 | 330 | `{{upper_left_x_pct, upper_left_y_pct}, {lower_right_x_pct, lower_right_y_pct}}` 331 | example: '{{0.025000000000000001, 0.37418655097613884}, {0.40000000000000002, 332 | 0.50108459869848154}}' 333 | 334 | stageDisplayColor: 335 | type: string 336 | description: | 337 | Stage Display colors are strings of three or four space-delimited float values for RGB(A) from 0-1 338 | example: 1.000000 1.000000 1.000000 1.000000 339 | 340 | stageDisplayFrame: 341 | description: 342 | A Stage Display Frame is a box of stage display information. 343 | type: object 344 | parameters: 345 | ufr: 346 | type: stageDisplayFrameGeometry 347 | mde: 348 | type: integer 349 | options: [0, 1, 2] 350 | description: | 351 | * frame mode is indicated by `mde` 352 | * mode 0: static image 353 | * mode 1: text 354 | * mode 2: live slide 355 | tAl: 356 | type: integer 357 | description: Always seems to be `2` 358 | tCl: 359 | type: stageDisplayColor 360 | description: frame text color 361 | tSz: 362 | type: integer 363 | description: Frame font size 364 | nme: 365 | type: string 366 | description: Name of this frame 367 | typ: 368 | type: integer 369 | options: [1,2,3,4,5,6,7,8,9] 370 | description: | 371 | * frame type is indicated by `typ` and determines what content goes in this frame 372 | * type 1: current slide 373 | * type 2: next slide 374 | * type 3: current slide notes 375 | * type 4: next slide notes 376 | * type 5: Stage Message (uses message flash values) 377 | * type 6: Clock 378 | * type 7: Timer Display (uses `uid` to specify timer) 379 | * type 8: Video Countdown 380 | * type 9: Chord Chart 381 | fCl: 382 | type: stageDisplayColor 383 | description: (message frames only) frame message flash color 384 | fCh: 385 | type: bool 386 | description: (message frames only) frame use message flash 387 | uid: 388 | type: string 389 | description: (timer frames only) frame timer uid 390 | 391 | stageDisplayLayout: 392 | type: object 393 | description: | 394 | * `nme` indicates layout name 395 | * `ovr` indicates if overrun color should be used 396 | * `oCl` indicates color for timer overruns 397 | * `brd` indicates if borders and labels should be used 398 | * `uid` indicates layout uid 399 | * `zro` indicates if zeroes should be removed from times 400 | * `fme` indicates array of frame layout specifications 401 | parameters: 402 | nme: 403 | type: string 404 | description: indicates layout name 405 | brd: 406 | type: bool 407 | description: indicates if borders and labels should be used 408 | uid: 409 | type: string 410 | description: indicates layout uid 411 | zro: 412 | type: boolint 413 | description: indicates if leading zeroes should be removed from times 414 | ovr: 415 | type: bool 416 | description: indicates if overrun color should be used 417 | oCl: 418 | type: stageDisplayColor 419 | description: indicates color for timer overruns 420 | fme: 421 | type: list[stageDisplayFrame] 422 | acn: sl 423 | 424 | stageDisplayFrameValue: 425 | type: object 426 | parameters: 427 | uid: 428 | optional: true 429 | type: string 430 | description: (only for slides and timers) uid of the object in this frame 431 | txt: 432 | type: string 433 | description: the text content of this frame, if any 434 | acn: 435 | type: string 436 | options: 437 | - cs 438 | - csn 439 | - ns 440 | - nsn 441 | - msg 442 | - sys 443 | - vid 444 | - tmr 445 | - cc 446 | description: | 447 | * `cs` = current slide text 448 | * `csn` = current slide notes 449 | * `ns` = next slide text 450 | * `nsn` = next slide text 451 | * `msg` = stage display message text 452 | * `sys` = system clock time 453 | * `vid` = video countdown value 454 | * `tmr` = timer value 455 | * `cc` = chord chart 456 | example: 457 | { 458 | "acn": "cs", 459 | "uid": "FAFCA1CB-8CB8-4E53-8B7C-8D61154516D0", 460 | "txt": "" 461 | } 462 | 463 | 464 | stageDisplayLiveSlideFrame: 465 | type: object 466 | parameters: 467 | RVLiveStream_action: RVLiveStream_frameData 468 | RVLiveStream_frameDataLength: 469 | type: integer 470 | description: byte count of this frame data 471 | RVLiveStream_frameData: 472 | type: string 473 | description: base64 encoded jpeg image 474 | example: 475 | { 476 | "RVLiveStream_action": "RVLiveStream_frameData", 477 | "RVLiveStream_frameDataLength": 14625, 478 | "RVLiveStream_frameData": "..." 479 | } 480 | 481 | 482 | 483 | channels: 484 | remote: 485 | summary: Remote Control 486 | description: | 487 | The `remote` channel carries all the 488 | information from the "ProPresenter Remote" app 489 | 490 | CONNECT WITH: 491 | 492 | ``` 493 | ws://[hostname]:[port]/remote 494 | ``` 495 | 496 | # "pushy" message types 497 | messages: 498 | - presentationTriggerMessage 499 | - clockCurrentTimesMessage 500 | - clockStartStopMessage 501 | 502 | # actions refer to commands that can be sent to ProPresenter 503 | actions: 504 | authenticate: 505 | summary: RemoteAuthentication 506 | description: | 507 | This is the first command ProPresenter expects to receive over the websocket. 508 | This command requests authentication. 509 | 510 | For ProPresenter 6, the protocol must be `600` 511 | message: 512 | payload: 513 | action: authenticate 514 | protocol: 600 515 | password: 516 | type: string 517 | description: as defined in the ProPresenter network settings 518 | example: 519 | action: authenticate 520 | protocol: 600 521 | password: myGreatPassword 522 | response: 523 | payload: 524 | action: authenticate 525 | error: 526 | type: string 527 | description: empty if there is no error, otherwise error message 528 | authenticated: 529 | type: boolint 530 | description: 1 for true, 0 for false 531 | controller: 532 | type: boolint 533 | description: Does user have permission to send control messages? 1 for true, 0 for false 534 | example: 535 | {"controller":1,"authenticated":1,"error":"","action":"authenticate"} 536 | 537 | 538 | libraryRequest: 539 | summary: Get Library (all presentations) 540 | description: | 541 | Will return all items in the ProPresenter library. 542 | 543 | - Note that the response will include escaped slashes in path names. 544 | message: 545 | payload: 546 | action: libraryRequest 547 | response: 548 | payload: 549 | action: libraryRequest 550 | library: 551 | type: list[itemLocation] 552 | description: | 553 | This will be a list of full pathnames for all items in the Presentation library. 554 | 555 | Note the use of slashes in the response. ProPresenter expects library 556 | requests to follow this pattern exactly. 557 | example: 558 | { 559 | "library": [ 560 | "\/Path\/To\/ProPresenter\/Library\/Come Alive (Dry Bones).pro6", 561 | "\/Path\/To\/ProPresenter\/Library\/Pour Out My Heart.pro6", 562 | "\/Path\/To\/ProPresenter\/Library\/Away in a manger.pro6", 563 | "... ALL PRESENTATIONS IN THE LIBRARY ..." 564 | ], 565 | "action": "libraryRequest" 566 | } 567 | 568 | 569 | playlistRequestAll: 570 | summary: Get Playlists 571 | description: | 572 | Will return all presentation playlists. 573 | 574 | - Presentation playlists and Audio playlists have a similar format 575 | - A playlist may be a `playlistTypePlaylist` or a `playlistTypeGroup` 576 | - `playlistTypePlaylist` will contain a list of `playlistItemTypePresentation` (or `playlistItemTypeAudio` for audio playlists) 577 | - `playlistTypeGroup` may contain both `playlistTypePlaylist` and `playlistTypeGroup` 578 | message: 579 | payload: 580 | action: playlistRequestAll 581 | response: 582 | payload: 583 | action: playlistRequestAll 584 | playlistAll: 585 | type: list[playlistUnionType] 586 | example: 587 | { 588 | "playlistAll": [ 589 | { 590 | "playlistLocation": "0", 591 | "playlistType": "playlistTypePlaylist", 592 | "playlistName": "Default", 593 | "playlist": [ 594 | { 595 | "playlistItemName": "!~ PRE-SERVICE", 596 | "playlistItemLocation": "0:0", 597 | "playlistItemType": "playlistItemTypePresentation" 598 | } 599 | ] 600 | }, 601 | { 602 | "playlistLocation": "1", 603 | "playlistType": "playlistTypeGroup", 604 | "playlistName": "2017", 605 | "playlist": [ 606 | { 607 | "playlistLocation": "1.0", 608 | "playlistType": "playlistTypePlaylist", 609 | "playlistName": "2017-01-28-Vision Dinner", 610 | "playlist": [ 611 | { 612 | "playlistItemName": "!MISC2", 613 | "playlistItemLocation": "1.0:0", 614 | "playlistItemType": "playlistItemTypePresentation" 615 | }, 616 | { 617 | "playlistItemName": "!MISC1", 618 | "playlistItemLocation": "1.0:1", 619 | "playlistItemType": "playlistItemTypePresentation" 620 | } 621 | ] 622 | } 623 | ] 624 | } 625 | ], 626 | "action": "playlistRequestAll" 627 | } 628 | 629 | presentationRequest: 630 | summary: Request a Playlist by Location 631 | description: | 632 | NOTE: even though the requested action is `presentationRequest` Pro6 responds with 633 | a `presentationCurrent` but it will be different. See below. 634 | message: 635 | description: | 636 | `presentationSlideQuality` is optional. It refers to the pixel width of the 637 | slide previews sent from ProPresenter. If left blank, 100 pixel wide 638 | previews will be sent. If set to 0 previews will not be generated at all. 639 | The remote app asks for quality 25 first and then follows it up with a second 640 | request for quality 100. 641 | payload: 642 | action: presentationRequest 643 | presentationPath: 644 | type: itemLocation 645 | presentationSlideQuality: 646 | type: integer 647 | optional: true 648 | example: 649 | { 650 | "action": "presentationRequest", 651 | "presentationPath": "\\/Path\\/To\\/ProPresenter\\/Library\\/Song 1 Title.pro6", 652 | "presentationSlideQuality": 25 653 | } 654 | 655 | response: 656 | payload: 657 | action: presentationCurrent 658 | presentation: 659 | type: presentation 660 | example: 661 | { 662 | "action": "presentationCurrent", 663 | "presentation": { 664 | "presentationSlideGroups": [ 665 | { 666 | "groupName": "[SLIDE GROUP NAME]", 667 | "groupColor": "0 0 0 1", 668 | "groupSlides": [ 669 | { 670 | "slideEnabled": true, 671 | "slideNotes": "", 672 | "slideAttachmentMask": 0, 673 | "slideText": "[SLIDE TEXT HERE]", 674 | "slideImage": "[BASE64 ENCODED IMAGE]", 675 | "slideIndex": "0", 676 | "slideTransitionType": -1, 677 | "slideLabel": "[SLIDE LABEL]", 678 | "slideColor": "0 0 0 1" 679 | } 680 | ] 681 | } 682 | ], 683 | "presentationName": "[PRESENTATION TITLE]", 684 | "presentationHasTimeline": 0, 685 | "presentationCurrentLocation": "[PRESENTATION PATH OF CURRENTLY ACTIVE SLIDE]" 686 | } 687 | } 688 | 689 | 690 | presentationCurrent: 691 | summary: Request Current Presentation 692 | description: | 693 | Requests the currently active presentation if there is one. If no presentation has a slide 694 | active, then nothing will be returned. 695 | 696 | NOTE: You can distinguish this response from a presentationRequest request 697 | because this response will include presentationPath as a field at the root level 698 | of the response. 699 | message: 700 | payload: 701 | action: presentationCurrent 702 | presentationSlideQuality: 703 | type: integer 704 | optional: true 705 | response: 706 | payload: 707 | action: presentationCurrent 708 | presentationPath: 709 | type: itemLocation 710 | presentation: 711 | type: presentation 712 | 713 | presentationSlideIndex: 714 | summary: Get the Index of the Current Slide 715 | description: | 716 | - Slides are indexed from 0 717 | - This response always sends the slideIndex as a `string` value 718 | - The ProPresenter remote issues this action every time it does a `presentationRequest` 719 | message: 720 | payload: 721 | action: presentationSlideIndex 722 | response: 723 | payload: 724 | action: presentationSlideIndex 725 | slideIndex: 726 | type: string 727 | example: "0" 728 | 729 | presentationTriggerIndex: 730 | summary: Activate a Slide 731 | description: Works just like clicking on a slide 732 | message: 733 | payload: 734 | action: presentationTriggerIndex 735 | slideIndex: 736 | type: integer 737 | presentationPath: 738 | type: itemLocation 739 | example: "0:0" 740 | response: 741 | pushy: true 742 | type: presentationTriggerMessage 743 | 744 | presentationTriggerNext: 745 | summary: Activate the Next Slide 746 | description: Works just like hitting the right arrow or spacebar. 747 | message: 748 | payload: 749 | action: presentationTriggerNext 750 | response: 751 | type: presentationTriggerMessage 752 | 753 | presentationTriggerPrevious: 754 | summary: Activate the Previous Slide 755 | description: Works just like hitting the left arrow. 756 | message: 757 | payload: 758 | action: presentationTriggerPrevious 759 | response: 760 | type: presentationTriggerMessage 761 | 762 | # audio commands 763 | audioRequest: 764 | summary: Get Audio Library 765 | description: Will return all the items in the audio library 766 | message: 767 | payload: 768 | action: audioRequest 769 | response: 770 | payload: 771 | action: audioRequest 772 | audioPlaylist: 773 | type: list[playlistUnionType] 774 | 775 | audioCurrentSong: 776 | summary: Get the Current Song 777 | description: Will return the data for the currently playing song, if there is one. 778 | message: 779 | payload: 780 | action: audioCurrentSong 781 | response: 782 | payload: 783 | action: audioCurrentSong 784 | audioArtist: 785 | type: string 786 | audiosummary: 787 | type: string 788 | example: 789 | { 790 | "audioArtist": "", 791 | "action": "audioCurrentSong", 792 | "audioName": "Peaceful Instrumental - C" 793 | } 794 | 795 | 796 | audioIsPlaying: 797 | summary: Is Audio Playing (BROKEN) 798 | description: This api request exists, but it always reports `false` 799 | message: 800 | payload: 801 | action: audioIsPlaying 802 | response: 803 | payload: 804 | action: audioIsPlaying 805 | audioIsPlaying: false 806 | 807 | audioStartCue: 808 | summary: Start Audio Cue 809 | description: | 810 | This command will result in an `audioTriggered` message, but since the 811 | play/pause status has changed, ProPresenter will also send an `audioPlayPause` message. 812 | message: 813 | payload: 814 | action: audioStartCue 815 | audioChildPath: 816 | type: itemLocation 817 | response: 818 | pushy: true 819 | payload: 820 | action: audioTriggered 821 | audioArtist: 822 | type: string 823 | audiosummary: 824 | type: string 825 | example: 826 | { 827 | "audioArtist": "", 828 | "action": "audioTriggered", 829 | "audioName": "Peaceful Instrumental - C" 830 | } 831 | 832 | 833 | audioPlayPause: 834 | summary: Toggle the Audio Play State 835 | message: 836 | payload: 837 | action: audioPlayPause 838 | response: 839 | pushy: true 840 | payload: 841 | action: audioPlayPause 842 | audioPlayPause: 843 | type: string 844 | options: 845 | - Play 846 | - Pause 847 | 848 | # timeline commands 849 | timelinePlayPause: 850 | summary: Toggle the Timeline Play State 851 | description: If `presentationPath` is missing, Pro6 will crash. 852 | message: 853 | payload: 854 | action: timelinePlayPause 855 | presentationPath: 856 | type: itemLocation 857 | 858 | timelineRewind: 859 | summary: Reset the Timeline for a Presentation 860 | description: If `presentationPath` is missing, Pro6 will crash. 861 | message: 862 | payload: 863 | action: timelineRewind 864 | presentationPath: 865 | type: itemLocation 866 | 867 | # clock / timer commands 868 | clockRequest: 869 | summary: Request all Clock data 870 | description: 871 | message: 872 | payload: 873 | action: clockRequest 874 | response: 875 | payload: 876 | action: clockRequest 877 | clockInfo: 878 | type: list[clockDetail] 879 | 880 | clockCurrentTimes: 881 | summary: Get Current Times for All Clocks 882 | description: 883 | message: 884 | payload: 885 | action: clockCurrentTimes 886 | response: 887 | type: clockCurrentTimesMessage 888 | 889 | clockStartSendingCurrentTime: 890 | summary: Subscribe to Clock Updates 891 | description: | 892 | ProPresenter will send one clock update every second even if no clock values have changed. 893 | 894 | The response payload is exactly the same as `clockCurrentTimes` 895 | message: 896 | payload: 897 | action: clockStartSendingCurrentTime 898 | response: 899 | type: clockCurrentTimesMessage 900 | 901 | clockStopSendingCurrentTime: 902 | summary: Unsubscribe from Clock Updates 903 | description: 904 | message: 905 | payload: 906 | action: clockStopSendingCurrentTime 907 | 908 | clockStart: 909 | summary: Start a Specific Clock 910 | description: 911 | message: 912 | payload: 913 | action: clockStart 914 | clockIndex: 915 | type: integer 916 | response: 917 | type: clockStartStopMessage 918 | 919 | clockStop: 920 | summary: Stop a Specific Clock 921 | description: 922 | message: 923 | payload: 924 | action: clockStop 925 | clockIndex: 926 | type: integer 927 | response: 928 | type: clockStartStopMessage 929 | 930 | clockReset: 931 | summary: Reset a clock back to initial settings 932 | description: 933 | message: 934 | payload: 935 | action: clockReset 936 | clockIndex: 937 | type: integer 938 | response: 939 | payload: 940 | action: clockResetIndex 941 | 942 | clockUpdate: 943 | summary: Update a Clock (Timer) (eg edit time) 944 | description: |- 945 | * Clocks are referenced by index. See reply from "clockRequest" action above to learn indexes. 946 | * Not all parameters are required for each clock type. 947 | * Countdown clocks only need "clockTime". 948 | * Elapsed Time Clocks need "clockTime" and optionally will use "clockElapsedTime" if you send it (to set the End Time). 949 | * You can rename a clock by optionally including the clockName. 950 | * Type 0 is Countdown 951 | * Type 1 is CountDown to Time 952 | * Type 2 is Elapsed Time. 953 | * Overrun can be modified if you choose to include that as well. 954 | message: 955 | payload: 956 | action: clockUpdate 957 | clockIndex: 958 | type: integer 959 | example: 1 960 | clockType: 961 | type: integer 962 | example: 0 963 | clockTime: 964 | type: string 965 | example: 09:04:00 966 | clockOverrun: 967 | type: integer 968 | example: 0 969 | clockIsPM: 970 | type: integer 971 | example: 1 972 | clocksummary: 973 | type: string 974 | example: Countdown 2 975 | clockElapsedTime: 976 | type: string 977 | example: 0:02:00 978 | example: 979 | { 980 | "action":"clockUpdate", 981 | "clockIndex":1, 982 | "clockType":0, 983 | "clockTime":"09:04:00", 984 | "clockOverrun":false, 985 | "clockIsPM":1, 986 | "clockName":"Countdown 2", 987 | "clockElapsedTime":"0:02:00" 988 | } 989 | 990 | 991 | clockResetAll: 992 | message: 993 | payload: 994 | action: clockResetAll 995 | example: 996 | {"action":"clockResetAll"} 997 | 998 | clockStopAll: 999 | message: 1000 | payload: 1001 | action: clockStopAll 1002 | example: 1003 | {"action":"clockStopAll"} 1004 | 1005 | clockStartAll: 1006 | message: 1007 | payload: 1008 | action: clockStartAll 1009 | example: 1010 | {"action":"clockStartAll"} 1011 | 1012 | 1013 | messageRequest: 1014 | summary: Get all Messages 1015 | description: |- 1016 | * The key is everything inside the curly braces `${}` so that the key for a countdown looks like this `Countdown 1: H:MM:SS`. 1017 | * If the key refers to a countdown, the value is used to update the `duration` field of the countdown timer, but will not perform a "reset". 1018 | * If the key refers to a countdown and the countdown is not running, this will resume it from its current value. 1019 | message: 1020 | payload: 1021 | action: messageRequest 1022 | example: 1023 | {"action":"messageRequest"} 1024 | 1025 | response: 1026 | payload: 1027 | action: messageRequest 1028 | messages: 1029 | type: list[screenMessage] 1030 | example: 1031 | { 1032 | "action": "messageRequest", 1033 | "messages": [ 1034 | { 1035 | "messageComponents": [ 1036 | "message:", 1037 | "${Message}" 1038 | ], 1039 | "messageTitle": "Message" 1040 | }, 1041 | { 1042 | "messageComponents": [ 1043 | "Session will begin in: ", 1044 | "${Countdown 1: H:MM:SS}" 1045 | ], 1046 | "messageTitle": "Countdown" 1047 | }, 1048 | { 1049 | "messageComponents": [ 1050 | "${Message}" 1051 | ], 1052 | "messageTitle": "Message" 1053 | }, 1054 | { 1055 | "messageComponents": [ 1056 | "Service starts in ", 1057 | "${countDownTimerName_1: H:MM:SS}" 1058 | ], 1059 | "messageTitle": "Countdown" 1060 | } 1061 | ] 1062 | } 1063 | 1064 | 1065 | messageSend: 1066 | summary: Display a Message 1067 | description: | 1068 | Display a message identified by its index. Add as many key, value 1069 | pairs as you like. 1070 | 1071 | See `messageRequest` for more information about keys. 1072 | message: 1073 | payload: 1074 | action: messageSend 1075 | messageIndex: 1076 | type: integer 1077 | messageKeys: 1078 | type: list[string] 1079 | messageValues: 1080 | type: list[string] 1081 | example: 1082 | {"action":"messageSend","messageIndex":0,"messageKeys":["key1","key2","..."],"messageValues":["Value1","Value2","..."]} 1083 | 1084 | 1085 | messageHide: 1086 | summary: Hide a Message 1087 | description: Hide a message identified by its index 1088 | message: 1089 | payload: 1090 | action: messageHide 1091 | messageIndex: 1092 | type: integer 1093 | 1094 | # stage display control messages 1095 | stageDisplaySendMessage: 1096 | summary: Show Stage Display Message 1097 | description: 1098 | message: 1099 | payload: 1100 | action: stageDisplaySendMessage 1101 | stageDisplayMessage: 1102 | type: string 1103 | example: Type a Message Here 1104 | example: 1105 | {"action":"stageDisplaySendMessage","stageDisplayMessage":"Type a Message Here"} 1106 | 1107 | stageDisplayHideMessage: 1108 | summary: Hide Stage Display Message 1109 | description: 1110 | message: 1111 | payload: 1112 | action: stageDisplayHideMessage 1113 | example: 1114 | {"action":"stageDisplayHideMessage"} 1115 | 1116 | stageDisplaySetIndex: 1117 | summary: Select Stage Display Layout 1118 | description: | 1119 | EXPECTED RESPONSE IS THE SAME AS THE SENT COMMAND 1120 | message: 1121 | payload: 1122 | action: stageDisplaySetIndex 1123 | stageDisplayIndex: 1124 | type: integer 1125 | example: 0 1126 | example: 1127 | {"action":"stageDisplaySetIndex","stageDisplayIndex":0} 1128 | 1129 | response: 1130 | payload: 1131 | action: stageDisplaySetIndex 1132 | stageDisplayIndex: 1133 | type: integer 1134 | example: 0 1135 | example: 1136 | {"action":"stageDisplaySetIndex","stageDisplayIndex":0} 1137 | 1138 | 1139 | stageDisplaySets: 1140 | summary: Get Stage Display Layouts 1141 | description: 1142 | message: 1143 | payload: 1144 | action: stageDisplaySets 1145 | example: 1146 | { "action": "stageDisplaySets" } 1147 | 1148 | response: 1149 | payload: 1150 | action: stageDisplaySets 1151 | stageDisplayIndex: 1152 | type: integer 1153 | example: 4 1154 | stageDisplaySets: 1155 | type: list[string] 1156 | example: 1157 | { 1158 | "stageDisplayIndex": 4, 1159 | "action": "stageDisplaySets", 1160 | "stageDisplaySets": [ 1161 | "Default", 1162 | "Easter Closer", 1163 | "Live Current - Static Next - no borders", 1164 | "Static Current - Static Next", 1165 | "Songs", 1166 | "Slides" 1167 | ] 1168 | } 1169 | 1170 | 1171 | # general control messages 1172 | clearAll: 1173 | summary: Clear All 1174 | description: '' 1175 | message: 1176 | payload: 1177 | action: clearAll 1178 | example: 1179 | {"action":"clearAll"} 1180 | 1181 | 1182 | clearText: 1183 | summary: Clear Slide 1184 | description: '' 1185 | message: 1186 | payload: 1187 | action: clearText 1188 | example: 1189 | {"action":"clearText"} 1190 | 1191 | 1192 | clearProps: 1193 | summary: Clear Props 1194 | description: '' 1195 | message: 1196 | payload: 1197 | action: clearProps 1198 | example: 1199 | {"action":"clearProps"} 1200 | 1201 | 1202 | clearAudio: 1203 | summary: Clear Audio 1204 | description: '' 1205 | message: 1206 | payload: 1207 | action: clearAudio 1208 | example: 1209 | {"action":"clearAudio"} 1210 | 1211 | 1212 | clearVideo: 1213 | summary: Clear Video 1214 | description: '' 1215 | message: 1216 | payload: 1217 | action: clearVideo 1218 | example: 1219 | {"action":"clearVideo"} 1220 | 1221 | 1222 | clearTelestrator: 1223 | summary: Clear Telestrator 1224 | description: '' 1225 | message: 1226 | payload: 1227 | action: clearTelestrator 1228 | example: 1229 | {"action":"clearTelestrator"} 1230 | 1231 | 1232 | clearToLogo: 1233 | summary: Clear To Logo 1234 | description: '' 1235 | message: 1236 | payload: 1237 | action: clearToLogo 1238 | example: 1239 | {"action":"clearToLogo"} 1240 | 1241 | 1242 | undocumented: 1243 | - socialSendTweet 1244 | - telestratorSettings 1245 | - telestratorEndEditing 1246 | - telestratorSet 1247 | - telestratorUndo 1248 | - telestratorNew 1249 | 1250 | stagedisplay: 1251 | summary: Stage Display API 1252 | protocol: 610 1253 | description: | 1254 | The `stagedisplay` channel carries all the 1255 | information from the "ProPresenter Stage" app 1256 | 1257 | CONNECT WITH: 1258 | 1259 | ``` 1260 | ws://[hostname]:[port]/stagedisplay 1261 | ``` 1262 | 1263 | ProPresenter also provides a mechanism for getting a high quality thumbnail image 1264 | for any slide or chord chart in the current presentation: 1265 | 1266 | ``` 1267 | http://PROPRESENTER_IP:PROPRESENTER_PORT/stage/image/SLIDE_UID 1268 | ``` 1269 | 1270 | In ProPresenter 6, the response will be the raw bytes of a jpeg image but the 1271 | response will *NOT* contain the proper Content-Type of `image/jpeg`. 1272 | 1273 | 1274 | # any one of the stagedisplay frame values can be sent independently 1275 | # however, cs, csn, ns, nsn are always sent together with an fv message 1276 | messages: 1277 | - fv 1278 | - sl 1279 | - msg 1280 | - sys 1281 | - tmr 1282 | - cc 1283 | - vid 1284 | 1285 | 1286 | # commands to send 1287 | actions: 1288 | ath: 1289 | summary: StageDisplayAuthentication 1290 | description: '' 1291 | message: 1292 | payload: 1293 | acn: ath 1294 | ptl: 610 1295 | pwd: 1296 | type: string 1297 | example: 1298 | {"pwd":"PASSWORD","ptl":610,"acn":"ath"} 1299 | 1300 | response: 1301 | payload: 1302 | acn: ath 1303 | ath: 1304 | type: bool 1305 | err: 1306 | type: string 1307 | example: 1308 | {"acn":"ath","ath":true,"err":""} 1309 | 1310 | 1311 | asl: 1312 | summary: Get All Stage Display Layouts 1313 | description: |- 1314 | * `acn` of `asl` means "all stage layouts" 1315 | * `ary` indicates array of stage layouts 1316 | message: 1317 | payload: 1318 | acn: asl 1319 | example: 1320 | {"acn":"asl"} 1321 | 1322 | response: 1323 | payload: 1324 | acn: asl 1325 | ary: 1326 | type: list[stageDisplayLayout] 1327 | example: 1328 | { 1329 | "acn": "asl", 1330 | "ary": [ 1331 | { 1332 | "brd": true, 1333 | "uid": "753B184F-CCCD-42F9-A883-D1DF86E1FFB8", 1334 | "zro": 0, 1335 | "oCl": "1.000000 0.000000 0.000000", 1336 | "fme": [ 1337 | { 1338 | "ufr": "{{0.025000000000000001, 0.37418655097613884}, {0.40000000000000002, 0.50108459869848154}}", 1339 | "mde": 1, 1340 | "tCl": "1.000000 1.000000 1.000000", 1341 | "tAl": 2, 1342 | "tSz": 60, 1343 | "nme": "Current Slide", 1344 | "typ": 1 1345 | } 1346 | ], 1347 | "ovr": true, 1348 | "acn": "sl", 1349 | "nme": "Default" 1350 | } 1351 | ] 1352 | } 1353 | 1354 | 1355 | psl: 1356 | summary: Request Current Stage Display Layout 1357 | description: '' 1358 | message: 1359 | payload: 1360 | acn: psl 1361 | example: 1362 | {"acn":"psl"} 1363 | 1364 | response: 1365 | payload: 1366 | acn: psl 1367 | uid: 1368 | type: string 1369 | example: 1370 | {"acn":"psl","uid":"F8260B13-9C5B-4D2C-80F1-C72346759F11"} 1371 | 1372 | 1373 | fv: 1374 | summary: Request Frame Values for Stage Display 1375 | description: | 1376 | When the frame value is pushed as a result of a slide trigger event, 1377 | only `cs`, `ns`, `csn`, `nsn` will be sent. 1378 | 1379 | Applications should listen for all the other `acn` values separately. 1380 | message: 1381 | payload: 1382 | acn: fv 1383 | uid: 1384 | type: string 1385 | example: 1386 | {"acn":"fv","uid":"F8260B13-9C5B-4D2C-80F1-C72346759F11"} 1387 | 1388 | response: 1389 | pushy: true 1390 | payload: 1391 | acn: fv 1392 | ary: 1393 | type: list[stageDisplayFrameValue] 1394 | example: 1395 | { 1396 | "acn": "fv", 1397 | "ary": [ 1398 | { 1399 | "acn": "cs", 1400 | "uid": "FAFCA1CB-8CB8-4E53-8B7C-8D61154516D0", 1401 | "txt": "" 1402 | }, 1403 | { 1404 | "acn": "ns", 1405 | "uid": "95D16968-589A-11EB-8D62-FCAA147AEF2F", 1406 | "txt": "" 1407 | }, 1408 | { 1409 | "acn": "csn", 1410 | "txt": "[sermon_start]\nevent[2888]\nvmixoverlay[1,Out]\nlive[1]" 1411 | }, 1412 | { 1413 | "acn": "nsn", 1414 | "txt": "vmixoverlay[2,Out]" 1415 | }, 1416 | { 1417 | "acn": "msg", 1418 | "txt": "" 1419 | }, 1420 | { 1421 | "acn": "sys", 1422 | "txt": " 9:57 AM" 1423 | }, 1424 | { 1425 | "acn": "tmr", 1426 | "uid": "47E8B48C-0D61-4EFC-9517-BF9FB894C8E2", 1427 | "txt": "0:09:59" 1428 | }, 1429 | { 1430 | "acn": "vid", 1431 | "txt": "" 1432 | }, 1433 | { 1434 | "acn": "cc", 1435 | "uid": "" 1436 | } 1437 | ] 1438 | } 1439 | 1440 | # push only messages 1441 | sl: 1442 | summary: On New Stage Display Selected 1443 | description: |- 1444 | * `acn` of `sl` indicates this is a single stage layout 1445 | response: 1446 | type: stageDisplayLayout 1447 | 1448 | sys: 1449 | summary: On New System Time 1450 | description: '' 1451 | response: 1452 | pushy: true 1453 | payload: 1454 | acn: sys 1455 | txt: 1456 | type: string 1457 | example: ' 11:17 AM' 1458 | example: 1459 | {"acn":"sys","txt":" 11:17 AM"} 1460 | 1461 | 1462 | vid: 1463 | summary: On New Video Countdown Time 1464 | description: Time follows the same H:MM:SS format as the remote protocol. 1465 | response: 1466 | pushy: true 1467 | payload: 1468 | acn: vid 1469 | txt: 1470 | type: string 1471 | example: '--:--:--' 1472 | example: 1473 | {"acn":"vid","txt":"0:00:18"} 1474 | 1475 | cc: 1476 | summary: On New Chord Chart 1477 | description: | 1478 | Chord chart images can be downloaded in the same way as slide preview images 1479 | 1480 | http://PROPRESENTER_IP:PROPRESENTER_PORT/stage/image/UID 1481 | response: 1482 | pushy: true 1483 | payload: 1484 | acn: cc 1485 | uid: 1486 | type: string 1487 | example: E65DF4DF-960D-4CF5-ADF5-60F200F6AFF7 1488 | example: 1489 | {"acn":"cc","uid":"E65DF4DF-960D-4CF5-ADF5-60F200F6AFF7"} 1490 | 1491 | tmr: 1492 | summary: On Timer Update 1493 | description: '' 1494 | response: 1495 | pushy: true 1496 | payload: 1497 | acn: tmr 1498 | uid: 1499 | type: string 1500 | description: timer uid 1501 | txt: 1502 | type: string 1503 | description: timer text 1504 | example: 1505 | { 1506 | "acn": "tmr", 1507 | "uid": "[TIMER UID]", 1508 | "txt": "--:--:--" 1509 | } 1510 | -------------------------------------------------------------------------------- /Pro6.md: -------------------------------------------------------------------------------- 1 | # ProPresenter-API 2 | Documenting RenewedVision's undocumented network protocols with examples 3 | 4 | This document refers to *ProPresenter 6*. 5 | 6 | Both the Remote Control and the Stage Display protocols are unencrypted text-based websocket connections from the client to the ProPresenter instance. 7 | 8 | Note, if both the Remote Control and the Stage Display interface are enabled in ProPresenter, they both operate over the *Remote Control* network port. 9 | 10 | ## Remote Control 11 | 12 | 13 | ### Connecting 14 | 15 | ```javascript 16 | ws://[host]:[port]/remote 17 | ``` 18 | 19 | ### Authenticate 20 | 21 | COMMAND TO SEND: 22 | 23 | ```javascript 24 | {"action":"authenticate","protocol":"600","password":"control"} 25 | ``` 26 | * protocol is used to perform a version check. ProPresenter 6 seems to check for a value here of at least 600 - otherwise it denies authentication and returns "Protocol out of date. Update application" 27 | 28 | EXPECTED RESPONSE: 29 | 30 | ```javascript 31 | {"controller":1,"authenticated":1,"error":"","action":"authenticate"} 32 | ``` 33 | 34 | ### Get Library (all presentations) 35 | 36 | COMMAND TO SEND: 37 | 38 | ```javascript 39 | {"action":"libraryRequest"} 40 | ``` 41 | 42 | EXPECTED RESPONSE: 43 | 44 | ```javascript 45 | { 46 | "library": [ 47 | "\/Path\/To\/ProPresenter\/Library\/Come Alive (Dry Bones).pro6", 48 | "\/Path\/To\/ProPresenter\/Library\/Pour Out My Heart.pro6", 49 | "\/Path\/To\/ProPresenter\/Library\/Away in a manger.pro6", 50 | "... ALL PRESENTATIONS IN THE LIBRARY ..." 51 | ], 52 | "action": "libraryRequest" 53 | } 54 | ``` 55 | 56 | * Note the use of slashes in the response. ProPresenter expects library requests to follow this pattern exactly. 57 | 58 | ### Get All Playlists 59 | 60 | COMMAND TO SEND: 61 | 62 | ```javascript 63 | {"action":"playlistRequestAll"} 64 | ``` 65 | 66 | EXPECTED RESPONSE: 67 | 68 | This request returns all playlists according to the following format. 69 | 70 | ```javascript 71 | { 72 | "playlistAll": [ 73 | { 74 | "playlistLocation": "0", 75 | "playlistType": "playlistTypePlaylist", 76 | "playlistName": "Default", 77 | "playlist": [ 78 | { 79 | "playlistItemName": "!~ PRE-SERVICE", 80 | "playlistItemLocation": "0:0", 81 | "playlistItemType": "playlistItemTypePresentation" 82 | }, 83 | ] 84 | }, 85 | { 86 | "playlistLocation": "1", 87 | "playlistType": "playlistTypeGroup", 88 | "playlistName": "2017", 89 | "playlist": [ 90 | { 91 | "playlistLocation": "1.0", 92 | "playlistType": "playlistTypePlaylist", 93 | "playlistName": "2017-01-28-Vision Dinner", 94 | "playlist": [ 95 | { 96 | "playlistItemName": "!MISC2", 97 | "playlistItemLocation": "1.0:0", 98 | "playlistItemType": "playlistItemTypePresentation" 99 | }, 100 | { 101 | "playlistItemName": "!MISC1", 102 | "playlistItemLocation": "1.0:1", 103 | "playlistItemType": "playlistItemTypePresentation" 104 | }, 105 | ] 106 | }, 107 | ] 108 | } 109 | ], 110 | "action": "playlistRequestAll" 111 | } 112 | ``` 113 | 114 | ### Request Presentation (set of slides) 115 | 116 | COMMAND TO SEND: 117 | 118 | ```javascript 119 | { 120 | "action": "presentationRequest", 121 | "presentationPath": "\/Path\/To\/ProPresenter\/Library\/Song 1 Title.pro6", 122 | "presentationSlideQuality": 25 123 | } 124 | ``` 125 | 126 | * `presentationPath` is required and it can be structured in one of three ways 127 | * It can be a full path to a pro6 file but note that all slashes need to be preceeded by a backslash in the request. 128 | * It can be the basename of a presentation that exists in the library (eg. `Song 1 Title.pro6`) is (sometimes?) good enough. 129 | * It can be the "playlist location" of the presentation. The playlist location is determined according to the order of items in the playlist window, the items are indexed from 0, and groups are sub-indexed with a dot, then presentations inside the playlist are indexed with a colon and a numeral. That is, the first presentation of the first playlist is `0:0` and if the first playlist item is a group, the first item of the first playlist of that group is `0.0:0` 130 | * A presentationPath specified with a playlist address and not a filename seems to be the most reliable. 131 | * `presentationSlideQuality` is optional. It determines the resolution / size of the slide previews sent from ProPresenter. If left blank, high quality previews will be sent. If set to `0` previews will not be generated at all. The remote app asks for quality `25` first and then follows it up with a second request for quality `100`. 132 | 133 | EXPECTED RESPONSE: 134 | 135 | ```javascript 136 | { 137 | "action": "presentationCurrent", 138 | "presentation": { 139 | "presentationSlideGroups": [ 140 | { 141 | "groupName": "[SLIDE GROUP NAME]", 142 | "groupColor": "0 0 0 1", // RGBA scale is from 0-1 143 | "groupSlides": [ 144 | { 145 | "slideEnabled": true, 146 | "slideNotes": "", 147 | "slideAttachmentMask": 0, 148 | "slideText": "[SLIDE TEXT HERE]", 149 | "slideImage": "[BASE64 ENCODED IMAGE]", 150 | "slideIndex": "0", 151 | "slideTransitionType": -1, 152 | "slideLabel": "[SLIDE LABEL]", 153 | "slideColor": "0 0 0 1" 154 | } 155 | ] 156 | }, 157 | ], 158 | "presentationName": "[PRESENTATION TITLE]", 159 | "presentationHasTimeline": 0, 160 | "presentationCurrentLocation": "[PRESENTATION PATH OF CURRENTLY ACTIVE SLIDE]" 161 | } 162 | } 163 | ``` 164 | 165 | * The response contains `presentationCurrent` as the action instead of `presentationRequest`. This seems to be a bug in the ProPresenter response. 166 | * The `presentationCurrentLocation` is not the location of the presentation you requested. It is the path of the presentation whose slide is currently active. 167 | * You can distinguish this response from the real `presentationCurrent` request because that response will include `presentationPath` as a field at the root level of the response. 168 | 169 | ### Request Current Presentation 170 | 171 | COMMAND TO SEND: 172 | 173 | ```javascript 174 | { "action":"presentationCurrent", "presentationSlideQuality": 25} 175 | ``` 176 | 177 | EXPECTED RESPONSE: 178 | 179 | Same response as `requestPresentation` except this response will include `presentationPath` as a field at the root level of the response. 180 | 181 | * NOTE: This action only seems to work if there is an *active slide*. When ProPresenter starts, no slide is marked active, so this action *returns nothing until a slide has been triggered*. 182 | 183 | 184 | ### Get Index of Current Slide 185 | 186 | COMMAND TO SEND: 187 | 188 | ```javascript 189 | {"action":"presentationSlideIndex"} 190 | ``` 191 | EXPECTED RESPONSE: 192 | 193 | ```javascript 194 | {"action":"presentationSlideIndex","slideIndex":"0"} 195 | ``` 196 | 197 | * NOTE: The ProPresenter remote issues this action every time it issues a `presentationRequest` action. 198 | 199 | ### Trigger Slide 200 | 201 | COMMAND TO SEND: 202 | 203 | ```javascript 204 | {"action":"presentationTriggerIndex","slideIndex":3,"presentationPath":"[PRESENTATION PATH]"} 205 | ``` 206 | 207 | EXPECTED RESPONSE: 208 | 209 | ```javascript 210 | {"slideIndex":3,"action":"presentationTriggerIndex","presentationPath":"[PRESENTATION PATH]"} 211 | ``` 212 | 213 | ### Trigger Next Slide 214 | 215 | COMMAND TO SEND: 216 | 217 | ```javascript 218 | {"action":"presentationTriggerNext"} 219 | ``` 220 | 221 | EXPECTED RESPONSE: 222 | 223 | ```javascript 224 | {"slideIndex":3,"action":"presentationTriggerIndex","presentationPath":"[PRESENTATION PATH]"} 225 | ``` 226 | 227 | ### Trigger Previous Slide 228 | 229 | COMMAND TO SEND: 230 | 231 | ```javascript 232 | {"action":"presentationTriggerPrevious"} 233 | ``` 234 | 235 | EXPECTED RESPONSE: 236 | 237 | ```javascript 238 | {"slideIndex":3,"action":"presentationTriggerIndex","presentationPath":"[PRESENTATION PATH]"} 239 | ``` 240 | 241 | ### Get Audio Library 242 | 243 | COMMAND TO SEND: 244 | 245 | ```javascript 246 | { "action": "audioRequest" } 247 | ``` 248 | 249 | EXPECTED RESPONSE: 250 | 251 | ```javascript 252 | { 253 | "action": "audioRequest", 254 | "audioPlaylist": [ 255 | { 256 | "playlistLocation": "0", 257 | "playlistType": "playlistTypePlaylist", 258 | "playlistName": "Library", 259 | "playlist": [ 260 | { 261 | "playlistItemName": "1-11 Have Yourself A Merry Little Christmas.mp3", 262 | "playlistItemArtist": "Chinua Hawk", 263 | "playlistItemType": "playlistItemTypeAudio", 264 | "playlistItemLocation": "0:0" 265 | } 266 | ] 267 | }, 268 | { 269 | "playlistLocation": "1", 270 | "playlistType": "playlistTypeGroup", 271 | "playlistName": "Service End", 272 | "playlist": [ 273 | { 274 | "playlistLocation": "1.0", 275 | "playlistType": "playlistTypePlaylist", 276 | "playlistName": "random", 277 | "playlist": [ 278 | { 279 | "playlistItemName": "03 Black Coal.mp3", 280 | "playlistItemArtist": "Sanctus Real", 281 | "playlistItemType": "playlistItemTypeAudio", 282 | "playlistItemLocation": "1.0:0" 283 | } 284 | ] 285 | } 286 | ] 287 | }, 288 | { 289 | "playlistLocation": "2", 290 | "playlistType": "playlistTypeGroup", 291 | "playlistName": "Christmas", 292 | "playlist": [] 293 | } 294 | ] 295 | } 296 | ``` 297 | 298 | ### Get Current Song 299 | 300 | COMMAND TO SEND: 301 | 302 | ```javascript 303 | { "action": "audioCurrentSong" } 304 | ``` 305 | 306 | EXPECTED RESPONSE: 307 | 308 | ```javascript 309 | { 310 | "audioArtist": "", 311 | "action": "audioCurrentSong", 312 | "audioName": "Peaceful Instrumental - C" 313 | } 314 | ``` 315 | 316 | ### Check if Audio is Playing (BROKEN) 317 | 318 | ProPresenter 6 always replies "false" to this request. 319 | 320 | COMMAND TO SEND: 321 | 322 | ```javascript 323 | { "action": "audioIsPlaying" } 324 | ``` 325 | 326 | EXPECTED RESPONSE: 327 | 328 | ```javascript 329 | {"audioIsPlaying":false,"action":"audioIsPlaying"} 330 | ``` 331 | 332 | ### Start Audio Cue 333 | 334 | COMMAND TO SEND: 335 | 336 | ```javascript 337 | {"action":"audioStartCue", "audioChildPath","[Same as Presentation Path Format]"} 338 | ``` 339 | 340 | EXPECTED RESPONSE: 341 | 342 | There are multiple responses for an audio cue trigger. 343 | 344 | ```javascript 345 | {"action":"audioPlayPause","audioPlayPause":"Play"} 346 | ``` 347 | 348 | ```javascript 349 | { 350 | "audioArtist": "", 351 | "action": "audioTriggered", 352 | "audioName": "Peaceful Instrumental - C" 353 | } 354 | ``` 355 | 356 | ### Audio Play/Pause Toggle 357 | 358 | COMMAND TO SEND: 359 | 360 | ```javascript 361 | {"action":"audioPlayPause"} 362 | ``` 363 | 364 | EXPECTED RESPONSE: 365 | 366 | ```javascript 367 | {"action":"audioPlayPause","audioPlayPause":"Play"} 368 | ``` 369 | 370 | 371 | ### Timeline Play/Pause Toggle 372 | 373 | Note: If Timeline commands are sent without a `presentationPath`, ProPresenter will crash. 374 | 375 | COMMAND TO SEND: 376 | 377 | ```javascript 378 | {"action":"timelinePlayPause","presentationPath":"[PRESENTATION PATH]"} 379 | ``` 380 | 381 | NO RESPONSE 382 | 383 | ### Timeline Rewind 384 | 385 | COMMAND TO SEND: 386 | 387 | ```javascript 388 | {"action":"timelineRewind":,"presentationPath":"[PRESENTATION PATH]"} 389 | ``` 390 | 391 | NO RESPONSE 392 | 393 | ## Clocks Data 394 | 395 | ### Request all Clocks (timers) 396 | 397 | COMMAND TO SEND: 398 | 399 | ```javascript 400 | {"action":"clockRequest"} 401 | ``` 402 | 403 | EXPECTED RESPONSE: 404 | 405 | ```javascript 406 | { 407 | "clockInfo": [ 408 | { 409 | "clockType": 0, 410 | "clockState": false, 411 | "clockName": "Countdown 1", 412 | "clockIsPM": 0, 413 | "clockDuration": "0:10:00", 414 | "clockOverrun": false, 415 | "clockEndTime": "--:--:--", 416 | "clockTime": "--:--:--" 417 | }, 418 | { 419 | "clockType": 1, 420 | "clockState": false, 421 | "clockName": "Countdown 2", 422 | "clockIsPM": 1, 423 | "clockDuration": "7:00:00", 424 | "clockOverrun": false, 425 | "clockEndTime": "--:--:--", 426 | "clockTime": "--:--:--" 427 | }, 428 | { 429 | "clockType": 2, 430 | "clockState": false, 431 | "clockName": "Elapsed Time", 432 | "clockIsPM": 0, 433 | "clockDuration": "0:00:00", 434 | "clockOverrun": false, 435 | "clockEndTime": "--:--:--", 436 | "clockTime": "13:52:23" 437 | } 438 | ], 439 | "action": "clockRequest" 440 | } 441 | ``` 442 | 443 | ### Get Clock Current Times 444 | 445 | COMMAND TO SEND: 446 | 447 | ```javascript 448 | {"action":"clockCurrentTimes"} 449 | ``` 450 | 451 | EXPECTED RESPONSE: 452 | 453 | ```javascript 454 | {"action":"clockCurrentTimes","clockTimes":["0:10:00","--:--:--","13:52:23"]} 455 | ``` 456 | 457 | ### Start Receiving Updates for Clocks (Timers) 458 | 459 | COMMAND TO SEND: 460 | 461 | ```javascript 462 | {"action":"clockStartSendingCurrentTime"} 463 | ``` 464 | 465 | EXPECTED RESPONSE IS SAME AS `clockCurrentTimes` (every second): 466 | 467 | ### Stop Receiving Updates for Clocks (Timers) 468 | 469 | COMMAND TO SEND: 470 | 471 | ```javascript 472 | {"action":"clockStopSendingCurrentTime"} 473 | ``` 474 | 475 | NO EXPECTED RESPONSE 476 | 477 | ### Start a Clock (Timer) 478 | 479 | COMMAND TO SEND: 480 | 481 | ```javascript 482 | {"action":"clockStart","clockIndex":0} 483 | ``` 484 | 485 | EXPECTED RESPONSE: 486 | 487 | ```javascript 488 | {"clockTime":"0:00:00","clockState":1,"clockIndex":0,"clockInfo":[1,1,"0:00:00"],"action":"clockStartStop"} 489 | ``` 490 | * `clockState` indicates if the clock is running or not 491 | * Clocks are referenced by index. See reply from "clockRequest" action above to learn indices. 492 | 493 | ### Stop a Clock (Timer) 494 | 495 | COMMAND TO SEND: 496 | 497 | ```javascript 498 | {"action":"clockStop","clockIndex":0} 499 | ``` 500 | 501 | EXPECTED RESPONSE: 502 | 503 | ```javascript 504 | { 505 | "clockTime": "0:09:59", 506 | "clockState": 0, 507 | "clockIndex": 0, 508 | "clockInfo": [ 509 | 1, 510 | 0, 511 | "0:09:59" 512 | ], 513 | "action": "clockStartStop" 514 | } 515 | ``` 516 | 517 | * `clockState` indicates if the clock is running or not 518 | * `clockInfo` is a quick list of information: 519 | * The first item is the clockType 520 | * The second item is the value that was recently changed indicated by the `action` 521 | * The third item is the current clock timestring. 522 | * Clocks are referenced by index. See reply from "clockRequest" action above to learn indices. 523 | 524 | ### Reset a Clock (Timer) 525 | 526 | COMMAND TO SEND: 527 | 528 | ```javascript 529 | {"action":"clockReset","clockIndex":"0"} 530 | ``` 531 | 532 | EXPECTED RESPONSE: 533 | 534 | ```javascript 535 | { 536 | "action": "clockResetIndex", 537 | "clockIndex": 1 538 | } 539 | ``` 540 | 541 | * Clocks are referenced by index. See reply from "clockRequest" action above to learn indices. 542 | 543 | 544 | 545 | ### Update a Clock (Timer) (eg edit time) 546 | 547 | COMMAND TO SEND: 548 | 549 | ```javascript 550 | { 551 | "action":"clockUpdate", 552 | "clockIndex":1, 553 | "clockType":0, 554 | "clockTime":"09:04:00", 555 | "clockOverrun":false, 556 | "clockIsPM":1, 557 | "clockName":"Countdown 2", 558 | "clockElapsedTime":"0:02:00" 559 | } 560 | ``` 561 | 562 | * Clocks are referenced by index. See reply from "clockRequest" action above to learn indexes. 563 | * Not all parameters are required for each clock type. 564 | * Countdown clocks only need "clockTime". 565 | * Elapsed Time Clocks need "clockTime" and optionally will use "clockElapsedTime" if you send it (to set the End Time). 566 | * You can rename a clock by optionally including the clockName. 567 | * Type 0 is Countdown 568 | * Type 1 is CountDown to Time 569 | * Type 2 is Elapsed Time. 570 | * Overrun can be modified if you choose to include that as well. 571 | 572 | 573 | ### Additional Clock Actions 574 | 575 | `clockResetAll`, `clockStopAll`, `clockStartAll` 576 | 577 | 578 | ### Get all Messages 579 | 580 | COMMAND TO SEND: 581 | 582 | ```javascript 583 | {"action":"messageRequest"} 584 | ``` 585 | 586 | EXPECTED RESPONSE: 587 | 588 | ```javascript 589 | { 590 | "action": "messageRequest", 591 | "messages": [ 592 | { 593 | "messageComponents": [ 594 | "message:", 595 | "${Message}" 596 | ], 597 | "messageTitle": "Message" 598 | }, 599 | { 600 | "messageComponents": [ 601 | "Session will begin in: ", 602 | "${Countdown 1: H:MM:SS}" 603 | ], 604 | "messageTitle": "Countdown" 605 | }, 606 | { 607 | "messageComponents": [ 608 | "${Message}" 609 | ], 610 | "messageTitle": "Message" 611 | }, 612 | { 613 | "messageComponents": [ 614 | "Service starts in ", 615 | "${countDownTimerName_1: H:MM:SS}" 616 | ], 617 | "messageTitle": "Countdown" 618 | } 619 | ] 620 | } 621 | ``` 622 | 623 | * The key is everything inside the curly braces `${}` so that the key for a countdown looks like this `Countdown 1: H:MM:SS`. 624 | * If the key refers to a countdown, the value is used to update the `duration` field of the countdown timer, but will not perform a "reset". 625 | * If the key refers to a countdown and the countdown is not running, this will resume it from its current value. 626 | 627 | ### Display a Message 628 | 629 | Display a message identified by its index. Add as many key, value pairs as you like. Keys can be name of timers. 630 | 631 | COMMAND TO SEND: 632 | 633 | ```javascript 634 | {"action":"messageSend","messageIndex":0,"messageKeys":"["key1","key2"....]","messageValues":"["Value1","Value2"...]"} 635 | ``` 636 | 637 | AN EXAMPLE USING THE DATA ABOVE: 638 | 639 | ```javascript 640 | {"action":"messageSend","messageIndex":0,"messageKeys":["Message"],"messageValues":["Test"]} 641 | ``` 642 | 643 | ### Hide a Message 644 | 645 | COMMAND TO SEND: 646 | Hide a message identified by its index 647 | 648 | ```javascript 649 | {"action":"messageHide","messageIndex","0"} 650 | ``` 651 | 652 | ### Clear All 653 | 654 | COMMAND TO SEND: 655 | 656 | ```javascript 657 | {"action":"clearAll"} 658 | ``` 659 | 660 | ### Clear Slide 661 | 662 | COMMAND TO SEND: 663 | 664 | ```javascript 665 | {"action":"clearText"} 666 | ``` 667 | 668 | ### Clear Props 669 | 670 | COMMAND TO SEND: 671 | 672 | ```javascript 673 | {"action":"clearProps"} 674 | ``` 675 | 676 | ### Clear Audio 677 | 678 | COMMAND TO SEND: 679 | 680 | ```javascript 681 | {"action":"clearAudio"} 682 | ``` 683 | 684 | ### Clear Video 685 | 686 | COMMAND TO SEND: 687 | 688 | ```javascript 689 | {"action":"clearVideo"} 690 | ``` 691 | 692 | ### Clear Telestrator 693 | 694 | COMMAND TO SEND: 695 | 696 | ```javascript 697 | {"action":"clearTelestrator"} 698 | ``` 699 | 700 | ### Clear To Logo 701 | 702 | COMMAND TO SEND: 703 | 704 | ```javascript 705 | {"action":"clearToLogo"} 706 | ``` 707 | 708 | ### Show Stage Display Message 709 | 710 | COMMAND TO SEND: 711 | 712 | ```javascript 713 | {"action":"stageDisplaySendMessage","stageDisplayMessage":"Type a Message Here"} 714 | ``` 715 | 716 | THERE IS NO EXPECTED RESPONSE 717 | 718 | ### Hide Stage Display Message 719 | 720 | COMMAND TO SEND: 721 | 722 | ```javascript 723 | {"action":"stageDisplayHideMessage"} 724 | ``` 725 | 726 | THERE IS NO EXPECTED RESPONSE 727 | 728 | ### Select Stage Display Layout 729 | 730 | COMMAND TO SEND: 731 | 732 | ```javascript 733 | {"action":"stageDisplaySetIndex","stageDisplayIndex":"[STAGE DISPLAY INDEX]"} 734 | ``` 735 | 736 | EXPECTED RESPONSE IS THE SAME AS THE SENT COMMAND 737 | 738 | ### Get Stage Display Layouts 739 | 740 | COMMAND TO SEND: 741 | 742 | ```javascript 743 | { "action": "stageDisplaySets" } 744 | ``` 745 | 746 | EXPECTED RESPONSE: 747 | 748 | ```javascript 749 | { 750 | "stageDisplayIndex": 4, 751 | "action": "stageDisplaySets", 752 | "stageDisplaySets": [ 753 | "Default", 754 | "Easter Closer", 755 | "Live Current - Static Next - no borders", 756 | "Static Current - Static Next", 757 | "Songs", 758 | "Slides" 759 | ] 760 | } 761 | ``` 762 | 763 | ## TODO: Complete documentation for remaining remote commands... 764 | socialSendTweet 765 | telestratorSettings 766 | telestratorEndEditing 767 | telestratorSet 768 | telestratorUndo 769 | telestratorNew 770 | 771 | ## Stage Display API 772 | 773 | ### Connecting 774 | 775 | ```javascript 776 | ws://[host]:[port]/stagedisplay 777 | ``` 778 | 779 | ### Authenticate 780 | 781 | COMMAND TO SEND: 782 | 783 | ```javascript 784 | {"pwd":PASSWORD,"ptl":610,"acn":"ath"} 785 | ``` 786 | 787 | EXPECTED RESPONSE: 788 | 789 | ```javascript 790 | {"acn":"ath","ath":true,"err":""} 791 | ``` 792 | 793 | ### Get All Stage Display Layouts 794 | 795 | COMMAND TO SEND: 796 | 797 | ```javascript 798 | {"acn":"asl"} 799 | ``` 800 | 801 | EXPECTED RESPONSE: 802 | 803 | ```javascript 804 | { 805 | "acn": "asl", 806 | "ary": [ 807 | { 808 | "brd": true, 809 | "uid": "753B184F-CCCD-42F9-A883-D1DF86E1FFB8", 810 | "zro": 0, 811 | "oCl": "1.000000 0.000000 0.000000", 812 | "fme": [ 813 | { 814 | "ufr": "{{0.025000000000000001, 0.37418655097613884}, {0.40000000000000002, 0.50108459869848154}}", 815 | "mde": 1, 816 | "tCl": "1.000000 1.000000 1.000000", 817 | "tAl": 2, 818 | "tSz": 60, 819 | "nme": "Current Slide", 820 | "typ": 1 821 | }, 822 | { 823 | "ufr": "{{0.024390243902439025, 0.27223427331887201}, {0.40182926829268295, 0.10412147505422993}}", 824 | "mde": 1, 825 | "tCl": "1.000000 1.000000 1.000000", 826 | "tAl": 2, 827 | "tSz": 60, 828 | "nme": "Current Slide Notes", 829 | "typ": 3 830 | }, 831 | { 832 | "ufr": "{{0.45000000000000001, 0.47396963123644253}, {0.29999999999999999, 0.40021691973969631}}", 833 | "mde": 1, 834 | "tCl": "1.000000 1.000000 1.000000", 835 | "tAl": 2, 836 | "tSz": 60, 837 | "nme": "Next Slide", 838 | "typ": 2 839 | }, 840 | { 841 | "ufr": "{{0.45000000000000001, 0.37310195227765725}, {0.29999999999999999, 0.1019522776572668}}", 842 | "mde": 1, 843 | "tCl": "1.000000 1.000000 1.000000", 844 | "tAl": 2, 845 | "tSz": 60, 846 | "nme": "Next Slide Notes", 847 | "typ": 4 848 | }, 849 | { 850 | "ufr": "{{0.77500000000000002, 0.37418655097613884}, {0.20000000000000001, 0.40130151843817785}}", 851 | "nme": "Chord Chart", 852 | "mde": 1, 853 | "typ": 9 854 | }, 855 | { 856 | "ufr": "{{0.050000000000000003, 0.89913232104121477}, {0.20000000000000001, 0.1019522776572668}}", 857 | "mde": 1, 858 | "tCl": "1.000000 1.000000 1.000000", 859 | "tAl": 2, 860 | "tSz": 200, 861 | "nme": "Clock", 862 | "typ": 6 863 | }, 864 | { 865 | "ufr": "{{0.40000000000000002, 0.89913232104121477}, {0.20000000000000001, 0.1019522776572668}}", 866 | "mde": 1, 867 | "tCl": "1.000000 1.000000 1.000000", 868 | "tAl": 2, 869 | "tSz": 200, 870 | "nme": "Video Countdown", 871 | "typ": 8 872 | }, 873 | { 874 | "ufr": "{{0.050000000000000003, 0.024945770065075923}, {0.90000000000000002, 0.10086767895878525}}", 875 | "fCl": "0.000000 1.000000 0.000000", 876 | "mde": 1, 877 | "tCl": "1.000000 1.000000 1.000000", 878 | "tAl": 2, 879 | "fCh": true, 880 | "tSz": 60, 881 | "nme": "Message", 882 | "typ": 5 883 | }, 884 | { 885 | "ufr": "{{0.68978420350609759, 0.89488713394793928}, {0.20000000000000001, 0.1019522776572668}}", 886 | "uid": "47E8B48C-0D61-4EFC-9517-BF9FB894C8E2", 887 | "mde": 1, 888 | "tCl": "1.000000 1.000000 1.000000", 889 | "tAl": 2, 890 | "tSz": 200, 891 | "nme": "Countdown 1", 892 | "typ": 7 893 | } 894 | ], 895 | "ovr": true, 896 | "acn": "sl", 897 | "nme": "Default" 898 | }, 899 | { 900 | "brd": false, 901 | "uid": "50AF3434-4328-40AC-846F-CC9583381311", 902 | "zro": 0, 903 | "oCl": "0.985948 0.000000 0.026951", 904 | "fme": [ 905 | { 906 | "ufr": "{{0.60304878048780486, 0.1963123644251627}, {0.39695121951219514, 0.80043383947939262}}", 907 | "mde": 1, 908 | "tCl": "0.990463 1.000000 0.041173", 909 | "tAl": 1, 910 | "tSz": 80, 911 | "nme": "Current Slide", 912 | "typ": 1 913 | }, 914 | { 915 | "ufr": "{{0.0024390243902439024, 0.0021691973969631237}, {0.599390243902439, 0.99457700650759218}}", 916 | "mde": 1, 917 | "tCl": "0.679783 1.000000 0.885215", 918 | "tAl": 0, 919 | "tSz": 120, 920 | "nme": "Current Slide Notes", 921 | "typ": 3 922 | }, 923 | { 924 | "ufr": "{{0.60304878048780486, 0.0021691973969631237}, {0.39512195121951221, 0.19305856832971802}}", 925 | "uid": "D1096B85-CF31-4365-A6E6-ED94264E7DCA", 926 | "mde": 1, 927 | "tCl": "1.000000 1.000000 1.000000", 928 | "tAl": 2, 929 | "tSz": 200, 930 | "nme": "Elapsed Time", 931 | "typ": 7 932 | } 933 | ], 934 | "ovr": false, 935 | "acn": "sl", 936 | "nme": "Easter Closer" 937 | }, 938 | { 939 | "brd": false, 940 | "uid": "F8260B13-9C5B-4D2C-80F1-C72346759F11", 941 | "zro": 0, 942 | "oCl": "0.985948 0.000000 0.026951", 943 | "fme": [ 944 | { 945 | "ufr": "{{0.025000000000000001, 0.37418655097613884}, {0.40000000000000002, 0.50108459869848154}}", 946 | "mde": 2, 947 | "tCl": "1.000000 1.000000 1.000000", 948 | "tAl": 2, 949 | "tSz": 60, 950 | "nme": "Current Slide", 951 | "typ": 1 952 | }, 953 | { 954 | "ufr": "{{0.025000000000000001, 0.27440347071583515}, {0.40000000000000002, 0.10086767895878525}}", 955 | "mde": 1, 956 | "tCl": "1.000000 1.000000 1.000000", 957 | "tAl": 2, 958 | "tSz": 60, 959 | "nme": "Current Slide Notes", 960 | "typ": 3 961 | }, 962 | { 963 | "ufr": "{{0.45000000000000001, 0.47396963123644253}, {0.29999999999999999, 0.40021691973969631}}", 964 | "mde": 0, 965 | "tCl": "1.000000 1.000000 1.000000", 966 | "tAl": 2, 967 | "tSz": 60, 968 | "nme": "Next Slide", 969 | "typ": 2 970 | }, 971 | { 972 | "ufr": "{{0.45000000000000001, 0.37310195227765725}, {0.29999999999999999, 0.1019522776572668}}", 973 | "mde": 1, 974 | "tCl": "1.000000 1.000000 1.000000", 975 | "tAl": 2, 976 | "tSz": 60, 977 | "nme": "Next Slide Notes", 978 | "typ": 4 979 | }, 980 | { 981 | "ufr": "{{0.77500000000000002, 0.37418655097613884}, {0.20000000000000001, 0.40130151843817785}}", 982 | "nme": "Chord Chart", 983 | "mde": 1, 984 | "typ": 9 985 | }, 986 | { 987 | "ufr": "{{0, 0.89804772234273322}, {0.20060975609756099, 0.10303687635574837}}", 988 | "mde": 1, 989 | "tCl": "1.000000 1.000000 1.000000", 990 | "tAl": 2, 991 | "tSz": 200, 992 | "nme": "Clock", 993 | "typ": 6 994 | }, 995 | { 996 | "ufr": "{{0.79878048780487809, 0.89696312364425168}, {0.20060975609756099, 0.10303687635574837}}", 997 | "mde": 1, 998 | "tCl": "1.000000 1.000000 1.000000", 999 | "tAl": 2, 1000 | "tSz": 200, 1001 | "nme": "Video Countdown", 1002 | "typ": 8 1003 | }, 1004 | { 1005 | "ufr": "{{0.050000000000000003, 0.024945770065075923}, {0.90000000000000002, 0.10086767895878525}}", 1006 | "fCl": "0.135296 1.000000 0.024919", 1007 | "mde": 1, 1008 | "tCl": "1.000000 1.000000 1.000000", 1009 | "tAl": 2, 1010 | "fCh": true, 1011 | "tSz": 60, 1012 | "nme": "Message", 1013 | "typ": 5 1014 | } 1015 | ], 1016 | "ovr": false, 1017 | "acn": "sl", 1018 | "nme": "Live Current - Static Next - no borders" 1019 | }, 1020 | { 1021 | "brd": true, 1022 | "uid": "12CB7383-FA02-47BB-B501-747ADCA860D3", 1023 | "zro": 0, 1024 | "oCl": "0.985948 0.000000 0.026951", 1025 | "fme": [ 1026 | { 1027 | "ufr": "{{0.025000000000000001, 0.37418655097613884}, {0.40000000000000002, 0.50108459869848154}}", 1028 | "mde": 0, 1029 | "tCl": "1.000000 1.000000 1.000000", 1030 | "tAl": 2, 1031 | "tSz": 60, 1032 | "nme": "Current Slide", 1033 | "typ": 1 1034 | }, 1035 | { 1036 | "ufr": "{{0.025000000000000001, 0.27440347071583515}, {0.40000000000000002, 0.10086767895878525}}", 1037 | "mde": 1, 1038 | "tCl": "1.000000 1.000000 1.000000", 1039 | "tAl": 2, 1040 | "tSz": 60, 1041 | "nme": "Current Slide Notes", 1042 | "typ": 3 1043 | }, 1044 | { 1045 | "ufr": "{{0.45000000000000001, 0.47396963123644253}, {0.29999999999999999, 0.40021691973969631}}", 1046 | "mde": 0, 1047 | "tCl": "1.000000 1.000000 1.000000", 1048 | "tAl": 2, 1049 | "tSz": 60, 1050 | "nme": "Next Slide", 1051 | "typ": 2 1052 | }, 1053 | { 1054 | "ufr": "{{0.45000000000000001, 0.37310195227765725}, {0.29999999999999999, 0.1019522776572668}}", 1055 | "mde": 1, 1056 | "tCl": "1.000000 1.000000 1.000000", 1057 | "tAl": 2, 1058 | "tSz": 60, 1059 | "nme": "Next Slide Notes", 1060 | "typ": 4 1061 | }, 1062 | { 1063 | "ufr": "{{0.77500000000000002, 0.37418655097613884}, {0.20000000000000001, 0.40130151843817785}}", 1064 | "nme": "Chord Chart", 1065 | "mde": 1, 1066 | "typ": 9 1067 | }, 1068 | { 1069 | "ufr": "{{0, 0.89804772234273322}, {0.20060975609756099, 0.10303687635574837}}", 1070 | "mde": 1, 1071 | "tCl": "1.000000 1.000000 1.000000", 1072 | "tAl": 2, 1073 | "tSz": 200, 1074 | "nme": "Clock", 1075 | "typ": 6 1076 | }, 1077 | { 1078 | "ufr": "{{0.79878048780487809, 0.89696312364425168}, {0.20060975609756099, 0.10303687635574837}}", 1079 | "mde": 1, 1080 | "tCl": "1.000000 1.000000 1.000000", 1081 | "tAl": 2, 1082 | "tSz": 200, 1083 | "nme": "Video Countdown", 1084 | "typ": 8 1085 | }, 1086 | { 1087 | "ufr": "{{0.050000000000000003, 0.024945770065075923}, {0.90000000000000002, 0.10086767895878525}}", 1088 | "fCl": "0.135296 1.000000 0.024919", 1089 | "mde": 1, 1090 | "tCl": "1.000000 1.000000 1.000000", 1091 | "tAl": 2, 1092 | "fCh": true, 1093 | "tSz": 60, 1094 | "nme": "Message", 1095 | "typ": 5 1096 | } 1097 | ], 1098 | "ovr": false, 1099 | "acn": "sl", 1100 | "nme": "Static Current - Static Next" 1101 | } 1102 | ] 1103 | } 1104 | ``` 1105 | 1106 | * `acn` of `asl` means "all stage layouts" 1107 | * `ary` indicates array of stage layouts 1108 | * `nme` indicates layout name 1109 | * `ovr` indicates if overrun color should be used 1110 | * `oCl` indicates color for timer overruns 1111 | * `brd` indicates if borders and labels should be used 1112 | * `uid` indicates layout uid 1113 | * `zro` indicates if zeroes should be removed from times 1114 | * `fme` indicates array of frame layout specifications 1115 | * frame positions are indicated by `ufr` and specified in terms of screen percentages 1116 | * frame name is indicated by `nme` 1117 | * frame text color is indicated by `tCl` 1118 | * frame font size is indicated by `tSz` 1119 | * frame message flash color is indicated by `fCl` 1120 | * frame use message flash indicated by `fCh` 1121 | * frame timer uid is indicated by `uid` 1122 | * frame mode is indicated by `mde` 1123 | * mode 0: static image 1124 | * mode 1: text 1125 | * mode 2: live slide 1126 | * frame type is indicated by `typ` and determines what content goes in this frame 1127 | * type 1: current slide 1128 | * type 2: next slide 1129 | * type 3: current slide notes 1130 | * type 4: next slide notes 1131 | * type 5: Stage Message (uses message flash values) 1132 | * type 6: Clock 1133 | * type 7: Timer Display (uses `uid` to specify timer) 1134 | * type 8: Video Countdown 1135 | * type 9: Chord Chart 1136 | 1137 | ### On New Stage Display Selected 1138 | 1139 | EXPECTED RESPONSE: 1140 | 1141 | ```javascript 1142 | { 1143 | "brd": true, 1144 | "uid": "12CB7383-FA02-47BB-B501-747ADCA860D3", 1145 | "zro": 0, 1146 | "oCl": "0.985948 0.000000 0.026951", 1147 | "fme": [ 1148 | { 1149 | "ufr": "{{0.025000000000000001, 0.37418655097613884}, {0.40000000000000002, 0.50108459869848154}}", 1150 | "mde": 0, 1151 | "tCl": "1.000000 1.000000 1.000000", 1152 | "tAl": 2, 1153 | "tSz": 60, 1154 | "nme": "Current Slide", 1155 | "typ": 1 1156 | }, 1157 | { 1158 | "ufr": "{{0.025000000000000001, 0.27440347071583515}, {0.40000000000000002, 0.10086767895878525}}", 1159 | "mde": 1, 1160 | "tCl": "1.000000 1.000000 1.000000", 1161 | "tAl": 2, 1162 | "tSz": 60, 1163 | "nme": "Current Slide Notes", 1164 | "typ": 3 1165 | }, 1166 | { 1167 | "ufr": "{{0.45000000000000001, 0.47396963123644253}, {0.29999999999999999, 0.40021691973969631}}", 1168 | "mde": 0, 1169 | "tCl": "1.000000 1.000000 1.000000", 1170 | "tAl": 2, 1171 | "tSz": 60, 1172 | "nme": "Next Slide", 1173 | "typ": 2 1174 | }, 1175 | { 1176 | "ufr": "{{0.45000000000000001, 0.37310195227765725}, {0.29999999999999999, 0.1019522776572668}}", 1177 | "mde": 1, 1178 | "tCl": "1.000000 1.000000 1.000000", 1179 | "tAl": 2, 1180 | "tSz": 60, 1181 | "nme": "Next Slide Notes", 1182 | "typ": 4 1183 | }, 1184 | { 1185 | "ufr": "{{0.77500000000000002, 0.37418655097613884}, {0.20000000000000001, 0.40130151843817785}}", 1186 | "nme": "Chord Chart", 1187 | "mde": 1, 1188 | "typ": 9 1189 | }, 1190 | { 1191 | "ufr": "{{0, 0.89804772234273322}, {0.20060975609756099, 0.10303687635574837}}", 1192 | "mde": 1, 1193 | "tCl": "1.000000 1.000000 1.000000", 1194 | "tAl": 2, 1195 | "tSz": 200, 1196 | "nme": "Clock", 1197 | "typ": 6 1198 | }, 1199 | { 1200 | "ufr": "{{0.79878048780487809, 0.89696312364425168}, {0.20060975609756099, 0.10303687635574837}}", 1201 | "mde": 1, 1202 | "tCl": "1.000000 1.000000 1.000000", 1203 | "tAl": 2, 1204 | "tSz": 200, 1205 | "nme": "Video Countdown", 1206 | "typ": 8 1207 | }, 1208 | { 1209 | "ufr": "{{0.050000000000000003, 0.024945770065075923}, {0.90000000000000002, 0.10086767895878525}}", 1210 | "fCl": "0.135296 1.000000 0.024919", 1211 | "mde": 1, 1212 | "tCl": "1.000000 1.000000 1.000000", 1213 | "tAl": 2, 1214 | "fCh": true, 1215 | "tSz": 60, 1216 | "nme": "Message", 1217 | "typ": 5 1218 | } 1219 | ], 1220 | "ovr": false, 1221 | "acn": "sl", 1222 | "nme": "Static Current - Static Next" 1223 | } 1224 | ``` 1225 | 1226 | * `acn` of `sl` indicates this is a single stage layout 1227 | 1228 | ### Request Current Stage Display Layout 1229 | 1230 | COMMAND TO SEND: 1231 | 1232 | ```javascript 1233 | {"acn":"psl"} 1234 | ``` 1235 | 1236 | EXPECTED RESPONSE (also used when stage display is updated): 1237 | 1238 | ```javascript 1239 | {"acn":"psl","uid":"[STAGE DISPLAY UID]"} 1240 | ``` 1241 | 1242 | ### Request Frame Values for Stage Display 1243 | 1244 | COMMAND TO SEND: 1245 | 1246 | ```javascript 1247 | {"acn":"fv","uid":"[STAGE DISPLAY UID"} 1248 | ``` 1249 | 1250 | EXPECTED RESPONSE: 1251 | 1252 | ```javascript 1253 | { 1254 | "acn": "fv", 1255 | "ary": [ 1256 | { 1257 | "acn": "cs", 1258 | "uid": "FAFCA1CB-8CB8-4E53-8B7C-8D61154516D0", 1259 | "txt": "" 1260 | }, 1261 | { 1262 | "acn": "ns", 1263 | "uid": "95D16968-589A-11EB-8D62-FCAA147AEF2F", 1264 | "txt": "" 1265 | }, 1266 | { 1267 | "acn": "msg", 1268 | "txt": "" 1269 | }, 1270 | { 1271 | "acn": "sys", 1272 | "txt": " 9:51 AM" 1273 | }, 1274 | { 1275 | "acn": "vid", 1276 | "txt": "" 1277 | } 1278 | ] 1279 | } 1280 | ``` 1281 | 1282 | ### On New Live Slide Frame 1283 | 1284 | EXPECTED RESPONSE: 1285 | 1286 | ```javascript 1287 | { 1288 | "RVLiveStream_action": "RVLiveStream_frameData", 1289 | "RVLiveStream_frameDataLength": 14625, 1290 | "RVLiveStream_frameData": "/9j//gAQTGF2YzU3LjUzLjEwMAD/2wBDAAgEBAQEBAUFBQUFBQYGBgYGBgYGBgYGBgYHBwcICAgHBwcGBgcHCAgICAkJCQgICAgJCQoKCgwMCwsODg4RERT/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsBAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKCxAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/wAARCAEOAeADASIAAhIAAxIA/9oADAMBAAIRAxEAPwDwqiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKOtAAAUUux/7p/I0bH/ALp/I0AACUUux/7p/I0bH/un8jQAAJRS7H/un8jRsf8Aun8jQAAJRS7H/un8jRsf+6fyNAAAlFLsf+6fyNGx/wC6fyNAAAlFLsf+6fyNGx/7p/I0AACUUux/7p/I0bH/ALp/I0AACUUux/7p/I0bH/un8jQAAJRS7H/un8jRsf8Aun8jQAAJRS7H/un8jRsf+6fyNAAAlFLsf+6fyNGx/wC6fyNAAAlFLsf+6fyNGx/7p/I0AACUUux/7p/I0bH/ALp/I0AACUUux/7p/I0bH/un8jQAAJRS7H/un8jRsf8Aun8jQAAJRS7H/un8jRsf+6fyNAAAlFLsf+6fyNGx/wC6fyNAAAlFLsf+6fyNGx/7p/I0AACUUux/7p/I0bH/ALp/I0AACUUux/7p/I0bH/un8jQAAJRS7H/un8jRsf8Aun8jQAAJRS7H/un8jRsf+6fyNAAAlFLsf+6fyNGx/wC6fyNAAAlFLsf+6fyNGx/7p/I0AACUUux/7p/I0bH/ALp/I0AACUUux/7p/I0bH/un8jQAAJRRRQAAFFFFAAAUUUUAABXafs9wQXPxg8LRTxRzRtcT7o5EV0b/AEWY8qwIPPqK4uu2/Z0/5LL4U/6+bj/0kmoAAPrL/hH9B/6BOm/+Adv/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" 1291 | } 1292 | ``` 1293 | 1294 | * Base64 Encoded Image Bytes 1295 | * Only the Current Slide can be "Live" 1296 | * Live slide images are pushed to the client over the websocket. 1297 | 1298 | ### To Get Static Slide Images 1299 | 1300 | TO RETRIEVE STATIC SLIDE IMAGES: 1301 | 1302 | Issue a normal GET request to the following URL: 1303 | 1304 | http://PROPRESENTER_IP:PROPRESENTER_PORT/stage/image/SLIDE_UID 1305 | 1306 | EXPECTED RESPONSE: 1307 | 1308 | normal jpeg image 1309 | 1310 | ### On New Time 1311 | 1312 | EXPECTED RESPONSE: 1313 | 1314 | ```javascript 1315 | {"acn":"sys","txt":" 11:17 AM"} 1316 | ``` 1317 | 1318 | ### On Timer Update 1319 | 1320 | EXPECTED RESPONSE: 1321 | 1322 | ```javascript 1323 | { acn: 'tmr', 1324 | uid: '[TIMER UID]', 1325 | txt: '--:--:--' } 1326 | ``` 1327 | 1328 | ### On New Slide 1329 | 1330 | EXPECTED RESPONSE: 1331 | 1332 | ```javascript 1333 | { 1334 | "acn": "fv", 1335 | "ary": [ 1336 | { 1337 | "acn": "cs", # CURRENT SLIDE 1338 | "uid": "[SLIDE UID]", 1339 | "txt": "[SLIDE TEXT]" 1340 | }, 1341 | { 1342 | "acn": "ns", # NEXT SLIDE 1343 | "uid": "[SLIDE UID]", 1344 | "txt": "[SLIDE TEXT]" 1345 | }, 1346 | { 1347 | "acn": "csn", # CURRENT SLIDE NOTES 1348 | "txt": "[SLIDE NOTES]" 1349 | }, 1350 | { 1351 | "acn": "nsn", # NEXT SLIDE NOTES 1352 | "txt": "[SLIDE NOTES]" 1353 | } 1354 | ] 1355 | } 1356 | ``` 1357 | -------------------------------------------------------------------------------- /Pro7.md: -------------------------------------------------------------------------------- 1 | # ProPresenter7-API 2 | 3 | Documenting RenewedVision's undocumented network protocols with examples. 4 | 5 | This document refers to _ProPresenter 7_. 6 | 7 | **Warning:** Be careful! It's easy to CRASH ProPresenter when sending invalid messages! 8 | 9 | Both the Remote Control and the Stage Display protocols are unencrypted text-based WebSocket connections from the client App to the ProPresenter instance. 10 | 11 | ## IMPORTANT DIFFERENCE FROM PRO 6 12 | Pro 7 implements a slightly different WebSocket protocol that most websocket clients should handle transparently; however, in rare cases this implementation can cause problems for developers. Of note is that the protocol requires two http request headers to be included in addition to the standard WebSocket upgrade headers. 13 | 14 | `Sec-WebSocket-Key` and `Sec-WebSocket-Version` 15 | 16 | The key must be a Base64 encoded key similar to this: `a8STpzZ6qXqaQvnHNehseA==` 17 | 18 | The Version must be at least 13. 19 | 20 | Note also that Pro7 incorrectly expects the HTTP header keys to appear in CamelCase which violates the HTTP spec, but nonetheless is what it is. 21 | 22 | Therefore, a correctly formed connection to the Pro7 server must have headers looking like this: 23 | 24 | ``` 25 | GET /stagedisplay HTTP/1.1 26 | Upgrade: websocket 27 | Connection: Upgrade 28 | Sec-WebSocket-Key: /8STpzZ6qXqaQvnHNehseA== 29 | Sec-WebSocket-Version: 13 30 | ``` 31 | 32 | ## Remote Control 33 | 34 | ### Connecting 35 | 36 | ```javascript 37 | ws://[host]:[port]/remote 38 | ``` 39 | 40 | ### Authenticate 41 | 42 | COMMAND TO SEND: 43 | 44 | ```javascript 45 | {"action":"authenticate","protocol":"701","password":"control"} 46 | ``` 47 | 48 | - Once connected, you must authenticate first before sending any other messages. 49 | - The value of the protocol parameter is used to perform a version check on the remote client. 50 | - ProPresenter 7.0 - 7.4.1 checks for a value here of at least 700 - otherwise it denies authentication and returns "Protocol out of date. Update application" 51 | - ProPresenter 7.4.2 and later checks for a value here of at least 701 - otherwise it denies authentication and returns "Protocol out of date. Update application" 52 | 53 | EXPECTED RESPONSE: 54 | 55 | ```javascript 56 | {"controller":1,"authenticated":1,"error":"","majorVersion":7,"minorVersion":6,"action":"authenticate"} 57 | ``` 58 | 59 | - authenticated should be 1 when sucessful and 0 when failed. The application version numbers are included in this response. 60 | 61 | ### Trigger a Pro7 Macro 62 | ```javascript 63 | {"action":"macrosTrigger","macroName":""} 64 | or 65 | {"action":"macrosTrigger","macroID":""} 66 | or 67 | {"action":"macrosTrigger","macroIndex":} 68 | ``` 69 | - You can optionally send a macrosTrigger action that includes multiple identifier parameters, they are handled in the following order of precedence: 70 | 1. macroID 71 | 2. macroName 72 | 3. macroIndex 73 | 74 | ### Request list of all Pro7 Macros 75 | ```javascript 76 | {"action":"macrosRequest"} 77 | ``` 78 | EXPECTED RESPONSE: 79 | ```javascript 80 | { 81 | "action": "macrosRequest", 82 | "macros": [ 83 | { 84 | "macroName": "", 85 | "macroID": "", 86 | "macroColor": " ", 87 | "macroIndex": 88 | } 89 | ] 90 | } 91 | ``` 92 | - Return a list of all Macros. 93 | - Color is provided as three double values separated by spaces: "r g b" 94 | 95 | ### Trigger a Pro7 Look 96 | ```javascript 97 | {"action":"looksTrigger","lookName":""} 98 | or 99 | {"action":"looksTrigger","lookID":""} 100 | or 101 | {"action":"looksTrigger","lookIndex":} 102 | ``` 103 | - Triggering a Pro7 Look makes that Look "Live" 104 | - You can optionally send a looksTrigger action that includes multiple identifier parameters, they are handled in the following order of precedence: 105 | 1. lookID 106 | 2. lookName 107 | 3. lookIndex 108 | 109 | ### Request list of all Pro7 Looks 110 | ```javascript 111 | {"action": "looksRequest"} 112 | ``` 113 | EXPECTED RESPONSE: 114 | ```javascript 115 | { 116 | "action": "looksRequest", 117 | "looks": [ 118 | { 119 | "lookName": "", 120 | "lookID": "", 121 | "lookIndex": 122 | } 123 | ], 124 | "activeLook": { 125 | "lookName": "", 126 | "lookID": "" 127 | } 128 | } 129 | ``` 130 | - Return a list of all Looks. 131 | - Includes the currently active Look (activeLook) 132 | 133 | ### Get Library (all presentations in all libraries) 134 | 135 | COMMAND TO SEND: 136 | 137 | ```javascript 138 | {"action":"libraryRequest"} 139 | ``` 140 | 141 | EXPECTED RESPONSE (MacOS): 142 | 143 | ```javascript 144 | { 145 | "library": [ 146 | "\/Path\/To\/ProPresenter\/Library\/Come Alive (Dry Bones).pro", 147 | "\/Path\/To\/ProPresenter\/Library\/Pour Out My Heart.pro", 148 | "\/Path\/To\/ProPresenter\/Library\/Away in a manger.pro", 149 | "... ALL PRESENTATIONS IN ALL LIBRARIES AS A SINGLE LIST ..." 150 | ], 151 | "action": "libraryRequest" 152 | } 153 | ``` 154 | 155 | - Note the use of slashes in the response. ProPresenter expects library requests to follow this pattern exactly. 156 | 157 | EXPECTED RESPONSE (Windows): 158 | 159 | ```javascript 160 | { 161 | "library": [ 162 | "C:/Users/media/Documents/ProPresenter/Libraries/Default/Alive.pro", 163 | "C:/Users/media/Documents/ProPresenter/Libraries/Default/All Because of Jesus.pro", 164 | "C:/Users/media/Documents/ProPresenter/Libraries/Sample/Announcements.pro", 165 | "C:/Users/media/Documents/ProPresenter/Libraries/Sample/Another In The Fire.pro", 166 | "... ALL PRESENTATIONS IN ALL LIBRARIES AS A SINGLE LIST ..." 167 | ], 168 | "action": "libraryRequest" 169 | } 170 | ``` 171 | 172 | ### Get All Playlists 173 | 174 | COMMAND TO SEND: 175 | 176 | ```javascript 177 | {"action":"playlistRequestAll"} 178 | ``` 179 | 180 | EXPECTED RESPONSE: 181 | 182 | This request returns all playlists according to the following format. 183 | 184 | ```javascript 185 | { 186 | "playlistAll": [ 187 | { 188 | "playlistLocation": "0", 189 | "playlistType": "playlistTypePlaylist", 190 | "playlistName": "Default", 191 | "playlist": [ 192 | { 193 | "playlistItemName": "!~ PRE-SERVICE", 194 | "playlistItemLocation": "0:0", 195 | "playlistItemType": "playlistItemTypePresentation" 196 | }, 197 | ] 198 | }, 199 | { 200 | "playlistLocation": "1", 201 | "playlistType": "playlistTypeGroup", 202 | "playlistName": "2017", 203 | "playlist": [ 204 | { 205 | "playlistLocation": "1.0", 206 | "playlistType": "playlistTypePlaylist", 207 | "playlistName": "2017-01-28-Vision Dinner", 208 | "playlist": [ 209 | { 210 | "playlistItemName": "!MISC2", 211 | "playlistItemLocation": "1.0:0", 212 | "playlistItemType": "playlistItemTypePresentation" 213 | }, 214 | { 215 | "playlistItemName": "!MISC1", 216 | "playlistItemLocation": "1.0:1", 217 | "playlistItemType": "playlistItemTypePresentation" 218 | }, 219 | ] 220 | }, 221 | ] 222 | } 223 | ], 224 | "action": "playlistRequestAll" 225 | } 226 | ``` 227 | 228 | playlistItemThumbnail will be included for some items - these will be Base64 encoded Jpegs 229 | 230 | ### Request Presentation (set of slides) 231 | 232 | COMMAND TO SEND: 233 | 234 | ```javascript 235 | { 236 | "action": "presentationRequest", 237 | "presentationPath": "\/Path\/To\/ProPresenter\/Library\/Song 1 Title.pro", 238 | "presentationSlideQuality": 25 239 | } 240 | ``` 241 | 242 | - `presentationPath` is required and it can be structured in one of three ways 243 | - It can be a full path to a pro file but note that all slashes need to be preceeded by a backslash in the request. 244 | - It can be the basename of a presentation that exists in the library (eg. `Song 1 Title.pro`) is (sometimes?) good enough. 245 | - It can be the "playlist location" of the presentation. The playlist location is determined according to the order of items in the playlist window, the items are indexed from 0, and groups are sub-indexed with a dot, then presentations inside the playlist are indexed with a colon and a numeral. That is, the first presentation of the first playlist is `0:0` and if the first playlist item is a group, the first item of the first playlist of that group is `0.0:0` 246 | - A presentationPath specified with a playlist address and not a filename seems to be the most reliable. 247 | - `presentationSlideQuality` is optional. It determines the resolution / size of the slide previews sent from ProPresenter. If left blank, high quality previews will be sent. If set to `0` previews will not be generated at all. The remote app asks for quality `25` first and then follows it up with a second request for quality `100`. 248 | 249 | EXPECTED RESPONSE: 250 | 251 | ```javascript 252 | { 253 | "action": "presentationCurrent", 254 | "presentation": { 255 | "presentationSlideGroups": [ 256 | { 257 | "groupName": "[SLIDE GROUP NAME]", 258 | "groupColor": "0 0 0 1", // RGBA scale is from 0-1 259 | "groupSlides": [ 260 | { 261 | "slideEnabled": true, 262 | "slideNotes": "", 263 | "slideAttachmentMask": 0, 264 | "slideText": "[SLIDE TEXT HERE]", 265 | "slideImage": "[BASE64 ENCODED IMAGE]", 266 | "slideIndex": "0", 267 | "slideTransitionType": -1, 268 | "slideLabel": "[SLIDE LABEL]", 269 | "slideColor": "0 0 0 1" 270 | } 271 | ] 272 | }, 273 | ], 274 | "presentationName": "[PRESENTATION TITLE]", 275 | "presentationHasTimeline": 0, 276 | "presentationCurrentLocation": "[PRESENTATION PATH OF CURRENTLY ACTIVE SLIDE]" 277 | } 278 | } 279 | ``` 280 | 281 | - The response contains `presentationCurrent` as the action instead of `presentationRequest`. This seems to be a bug in the ProPresenter response. 282 | - The `presentationCurrentLocation` is not the location of the presentation you requested. It is the path of the presentation whose slide is currently active. 283 | - You can distinguish this response from the real `presentationCurrent` request because that response will include `presentationPath` as a field at the root level of the response. 284 | 285 | ### Request Current Presentation 286 | 287 | COMMAND TO SEND: 288 | 289 | ```javascript 290 | { "action":"presentationCurrent", "presentationSlideQuality": 25} 291 | ``` 292 | 293 | EXPECTED RESPONSE: 294 | 295 | Same response as `requestPresentation` except this response will include `presentationPath` as a field at the root level of the response. 296 | 297 | - NOTE: This action only seems to work if there is an _active slide_. When ProPresenter starts, no slide is marked active, so this action _returns nothing until a slide has been triggered_. 298 | 299 | TODO: Investigate impact of presentationSlideQuality values for Pro7 on both MacOS and Win (0 will return slides without any slide image in Pro6, but Pro7 on MacOS seems to always include them) 300 | 301 | ### Get Index of Current Slide 302 | 303 | COMMAND TO SEND: 304 | 305 | ```javascript 306 | {"action":"presentationSlideIndex"} 307 | ``` 308 | 309 | EXPECTED RESPONSE: 310 | 311 | ```javascript 312 | {"action":"presentationSlideIndex","slideIndex":"0"} 313 | ``` 314 | 315 | - NOTE: The ProPresenter remote issues this action every time it issues a `presentationRequest` action. 316 | In Pro7, when a presentation is automatically advancing on the annoucements layer while a user is triggering slides in another presentation on the normal output layer - the index returns can sometimes _vary_ between the two different presentations! 317 | 318 | ### Trigger Slide 319 | 320 | COMMAND TO SEND: 321 | 322 | ```javascript 323 | {"action":"presentationTriggerIndex","slideIndex":"3","presentationPath":"[PRESENTATION PATH]"} 324 | ``` 325 | 326 | EXPECTED RESPONSE: 327 | 328 | ```javascript 329 | {"slideIndex":3,"action":"presentationTriggerIndex","presentationPath":"[PRESENTATION PATH]"} 330 | ``` 331 | 332 | NOTES: 333 | 334 | - Currently, the protocol requires SENDING the slideIndex as a string value even though the response delivers it as an integer value. 335 | - Also, the presentation path needs to be either a full pathname to the `.pro` file on the computer or the `playlistIndex:playlistItemIndex` style 336 | 337 | presentationTriggerIndex messages are sent to all connected clients to inform them all when a slide is triggered (if a user triggers a slide, you will get this message sent to you) 338 | 339 | ### Trigger Next Slide 340 | 341 | COMMAND TO SEND: 342 | 343 | ```javascript 344 | {"action":"presentationTriggerNext"} 345 | ``` 346 | 347 | ### Trigger Previous Slide 348 | 349 | COMMAND TO SEND: 350 | 351 | ```javascript 352 | {"action":"presentationTriggerPrevious"} 353 | ``` 354 | 355 | ### Start Audio Cue 356 | 357 | COMMAND TO SEND: 358 | 359 | ```javascript 360 | {"action":"audioStartCue", "audioChildPath":"[Same as Presentation Path Format]", "audioPath":"0"} 361 | ``` 362 | 363 | TODO: describe audioPath paramter required for Pro7 on MacOS 364 | 365 | ### Audio Play/Pause Toggle 366 | 367 | COMMAND TO SEND: 368 | 369 | ```javascript 370 | {"action":"audioPlayPause"} 371 | ``` 372 | 373 | ### TimeLine Play/Pause Toggle 374 | 375 | COMMAND TO SEND: 376 | 377 | ```javascript 378 | {"action":"timelinePlayPause","presentationPath":"[PRESENTATION PATH]"} 379 | ``` 380 | 381 | ### TimeLine Rewind 382 | 383 | COMMAND TO SEND: 384 | 385 | ```javascript 386 | {"action":"timelineRewind":,"presentationPath":"[PRESENTATION PATH]"} 387 | ``` 388 | 389 | ### Get Clock (Timers) Info 390 | 391 | COMMAND TO SEND: 392 | 393 | ```javascript 394 | {"action":"clockRequest"} 395 | ``` 396 | 397 | ### Start Recieving Updates for Clocks (Timers) 398 | 399 | COMMAND TO SEND: 400 | 401 | ```javascript 402 | {"action":"clockStartSendingCurrentTime"} 403 | ``` 404 | 405 | ### Stop Recieving Updates for Clocks (Timers) 406 | 407 | COMMAND TO SEND: 408 | 409 | ```javascript 410 | {"action":"clockStopSendingCurrentTime"} 411 | ``` 412 | 413 | ### Request all Clocks 414 | 415 | COMMAND TO SEND: 416 | 417 | ```javascript 418 | {"action":"clockRequest"} 419 | ``` 420 | 421 | EXPECTED RESPONSE: 422 | 423 | ```javascript 424 | { 425 | "clockInfo": [ 426 | { 427 | "clockType": 0, 428 | "clockState": false, 429 | "clockName": "Countdown 1", 430 | "clockIsPM": 0, 431 | "clockDuration": "0:10:00", 432 | "clockOverrun": false, 433 | "clockEndTime": "--:--:--", 434 | "clockTime": "--:--:--" 435 | }, 436 | { 437 | "clockType": 1, 438 | "clockState": false, 439 | "clockName": "Countdown 2", 440 | "clockIsPM": 1, 441 | "clockDuration": "7:00:00", 442 | "clockOverrun": false, 443 | "clockEndTime": "--:--:--", 444 | "clockTime": "--:--:--" 445 | }, 446 | { 447 | "clockType": 2, 448 | "clockState": false, 449 | "clockName": "Elapsed Time", 450 | "clockIsPM": 0, 451 | "clockDuration": "0:00:00", 452 | "clockOverrun": false, 453 | "clockEndTime": "--:--:--", 454 | "clockTime": "13:52:23" 455 | } 456 | ], 457 | "action": "clockRequest" 458 | } 459 | ``` 460 | 461 | ### Get Clock Current Times 462 | 463 | COMMAND TO SEND: 464 | 465 | ```javascript 466 | {"action":"clockCurrentTimes"} 467 | ``` 468 | 469 | EXPECTED RESPONSE: 470 | 471 | ```javascript 472 | {"action":"clockCurrentTimes","clockTimes":["0:10:00","--:--:--","13:52:23"]} 473 | ``` 474 | 475 | ### Start a Clock (Timer) 476 | 477 | COMMAND TO SEND: 478 | 479 | ```javascript 480 | {"action":"clockStart","clockIndex":"0"} 481 | ``` 482 | 483 | EXPECTED RESPONSE: 484 | 485 | ```javascript 486 | {"clockTime":"0:00:00","clockState":1,"clockIndex":0,"clockInfo":[1,1,"0:00:00"],"action":"clockStartStop"} 487 | ``` 488 | 489 | - `clockState` indicates if the clock is running or not 490 | - Clocks are referenced by index. See reply from "clockRequest" action above to learn indices. 491 | 492 | ### Stop a Clock (Timer) 493 | 494 | COMMAND TO SEND: 495 | 496 | ```javascript 497 | {"action":"clockStop","clockIndex":"0"} 498 | ``` 499 | 500 | EXPECTED RESPONSE: 501 | 502 | ```javascript 503 | {"clockTime":"0:00:00","clockState":0,"clockIndex":0,"clockInfo":[1,1,"0:00:00"],"action":"clockStartStop"} 504 | ``` 505 | 506 | - `clockState` indicates if the clock is running or not 507 | - Clocks are referenced by index. See reply from "clockRequest" action above to learn indices. 508 | 509 | ### Reset a Clock (Timer) 510 | 511 | COMMAND TO SEND: 512 | 513 | ```javascript 514 | {"action":"clockReset","clockIndex":"0"} 515 | ``` 516 | 517 | - Clocks are referenced by index. See reply from "clockRequest" action above to learn indices. 518 | 519 | ### Update a Clock (Timer) (eg edit time) 520 | 521 | COMMAND TO SEND: 522 | 523 | ```javascript 524 | { 525 | "action":"clockUpdate", 526 | "clockIndex":"1", 527 | "clockType":"0", 528 | "clockTime":"09:04:00", 529 | "clockOverrun":"false", 530 | "clockIsPM":"1", 531 | "clockName":"Countdown 2", 532 | "clockElapsedTime":"0:02:00" 533 | } 534 | ``` 535 | 536 | - Clocks are referenced by index. See reply from "clockRequest" action above to learn indexes. 537 | - Not all parameters are required for each clock type. 538 | - Countdown clocks only need "clockTime". 539 | - Elapsed Time Clocks need "clockTime" and optionally will use "clockElapsedTime" if you send it (to set the End Time). 540 | - You can rename a clock by optionally including the clockName. 541 | - Type 0 is Countdown 542 | - Type 1 is CountDown to Time 543 | - Type 2 is Elapsed Time. 544 | - Overrun can be modified if you choose to include that as well. 545 | 546 | ### Start Getting Clock Updates 547 | 548 | COMMAND TO SEND: 549 | 550 | ```javascript 551 | {"action":"clockStartSendingCurrentTime"} 552 | ``` 553 | 554 | EXPECTED RESPONSE (every second): 555 | 556 | ```javascript 557 | {"action":"clockCurrentTimes","clockTimes":["0:10:00","--:--:--","13:52:23"]} 558 | ``` 559 | 560 | ### Additional Clock Actions 561 | 562 | `clockResetAll`, `clockStopAll`, `clockStartAll` 563 | 564 | ### Get all Messages 565 | 566 | COMMAND TO SEND: 567 | 568 | ```javascript 569 | {"action":"messageRequest"} 570 | ``` 571 | 572 | EXPECTED RESPONSE: 573 | 574 | ```javascript 575 | { 576 | "action": "messageRequest", 577 | "messages": [ 578 | { 579 | "messageComponents": [ 580 | "message:", 581 | "${Message}" 582 | ], 583 | "messageTitle": "Message" 584 | }, 585 | { 586 | "messageComponents": [ 587 | "Session will begin in: ", 588 | "${Countdown 1: H:MM:SS}" 589 | ], 590 | "messageTitle": "Countdown" 591 | }, 592 | { 593 | "messageComponents": [ 594 | "${Message}" 595 | ], 596 | "messageTitle": "Message" 597 | }, 598 | { 599 | "messageComponents": [ 600 | "Service starts in ", 601 | "${countDownTimerName_1: H:MM:SS}" 602 | ], 603 | "messageTitle": "Countdown" 604 | } 605 | ] 606 | } 607 | ``` 608 | 609 | - The key is everything inside the curly braces `${}` so that the key for a countdown looks like this `Countdown 1: H:MM:SS`. 610 | - If the key refers to a countdown, the value is used to update the `duration` field of the countdown timer, but will not perform a "reset". 611 | - If the key refers to a countdown and the countdown is not running, this will resume it from its current value. 612 | 613 | ### Display a Message 614 | 615 | Display a message identified by its index. Add as many key, value pairs as you like. Keys can be name of timers. 616 | 617 | COMMAND TO SEND: 618 | 619 | ```javascript 620 | {"action":"messageSend","messageIndex":0,"messageKeys":"["key1","key2"....]","messageValues":"["Value1","Value2"...]"} 621 | ``` 622 | 623 | AN EXAMPLE USING THE DATA ABOVE: 624 | 625 | ```javascript 626 | {"action":"messageSend","messageIndex":0,"messageKeys":["Message"],"messageValues":["Test"]} 627 | ``` 628 | 629 | ### Hide a Message 630 | 631 | COMMAND TO SEND: 632 | Hide a message identified by its index 633 | 634 | ```javascript 635 | {"action":"messageHide","messageIndex","0"} 636 | ``` 637 | 638 | ### Clear All 639 | 640 | COMMAND TO SEND: 641 | 642 | ```javascript 643 | {"action":"clearAll"} 644 | ``` 645 | 646 | ### Clear Slide 647 | 648 | COMMAND TO SEND: 649 | 650 | ```javascript 651 | {"action":"clearText"} 652 | ``` 653 | 654 | ### Clear Props 655 | 656 | COMMAND TO SEND: 657 | 658 | ```javascript 659 | {"action":"clearProps"} 660 | ``` 661 | 662 | ### Clear Audio 663 | 664 | COMMAND TO SEND: 665 | 666 | ```javascript 667 | {"action":"clearAudio"} 668 | ``` 669 | 670 | ### Clear Video 671 | 672 | COMMAND TO SEND: 673 | 674 | ```javascript 675 | {"action":"clearVideo"} 676 | ``` 677 | 678 | ### Clear Telestrator 679 | 680 | COMMAND TO SEND: 681 | 682 | ```javascript 683 | {"action":"clearTelestrator"} 684 | ``` 685 | 686 | ### Clear To Logo 687 | 688 | COMMAND TO SEND: 689 | 690 | ```javascript 691 | {"action":"clearToLogo"} 692 | ``` 693 | 694 | ### Show Stage Display Message 695 | 696 | COMMAND TO SEND: 697 | 698 | ```javascript 699 | {"action":"stageDisplaySendMessage","stageDisplayMessage":"Type a Message Here"} 700 | ``` 701 | 702 | ### Hide Stage Display Message 703 | 704 | COMMAND TO SEND: 705 | 706 | ```javascript 707 | {"action":"stageDisplayHideMessage"} 708 | ``` 709 | 710 | ### Get Stage Display Layouts 711 | 712 | COMMAND TO SEND: 713 | 714 | ```javascript 715 | {"action":"stageDisplaySets"} 716 | ``` 717 | 718 | EXPECTED RESPONSE: 719 | 720 | ```javascript 721 | { 722 | "stageScreens": [ 723 | { 724 | "stageScreenName": "Stage Screen", 725 | "stageScreenUUID": "8e383232-2a89-4802-a507-93c09642fa68", 726 | "stageLayoutSelectedLayoutUUID": "54cc1c01-606c-434f-ad02-91b090d8ea41" 727 | } 728 | ], 729 | "stageLayouts": [ 730 | { 731 | "stageLayoutName": "Current/Next Stacked - Music", 732 | "stageLayoutUUID": "687e061b-c3fc-48a7-be8c-40837b622fed" 733 | }, 734 | { 735 | "stageLayoutName": "Music - text only", 736 | "stageLayoutUUID": "54cc1c01-606c-434f-ad02-91b090d8ea41" 737 | } 738 | ], 739 | "action": "stageDisplaySets" 740 | } 741 | ``` 742 | 743 | ### Select Stage Display Layout 744 | 745 | COMMAND TO SEND: 746 | 747 | ```javascript 748 | {"action":"stageDisplayChangeLayout","stageLayoutUUID":"687e061b-c3fc-48a7-be8c-40837b622fed","stageScreenUUID":"8e383232-2a89-4802-a507-93c09642fa68"} 749 | ``` 750 | 751 | EXPECTED RESPONSE: 752 | 753 | ```javascript 754 | { 755 | "stageScreens": [ 756 | { 757 | "stageScreenName": "Stage Screen", 758 | "stageScreenUUID": "8e383232-2a89-4802-a507-93c09642fa68", 759 | "stageLayoutSelectedLayoutUUID": "687e061b-c3fc-48a7-be8c-40837b622fed" 760 | } 761 | ], 762 | "stageLayouts": [ 763 | { 764 | "stageLayoutName": "Current/Next Stacked - Music", 765 | "stageLayoutUUID": "687e061b-c3fc-48a7-be8c-40837b622fed" 766 | }, 767 | { 768 | "stageLayoutName": "Music - text only", 769 | "stageLayoutUUID": "54cc1c01-606c-434f-ad02-91b090d8ea41" 770 | } 771 | ], 772 | "action": "stageDisplaySets" 773 | } 774 | ``` 775 | 776 | ## TODO: Complete documentation for remaining remote commands... 777 | 778 | audioRequest (Get audio Playlist) 779 | audioCurrentSong (Get current audio playing) 780 | audioIsPlaying (Seems to always return true from MacOS when an image background is triggered) 781 | 782 | ## Stage Display API 783 | 784 | ### Connecting 785 | 786 | ```javascript 787 | ws://[host]:[port]/stagedisplay 788 | ``` 789 | 790 | ### Authenticate 791 | 792 | COMMAND TO SEND: 793 | 794 | ```javascript 795 | {"pwd":PASSWORD,"ptl":610,"acn":"ath"} 796 | ``` 797 | 798 | EXPECTED RESPONSE: 799 | 800 | ```javascript 801 | {"acn":"ath","ath":true,"err":""} 802 | ``` 803 | 804 | ### Get All Stage Display Layouts 805 | 806 | COMMAND TO SEND: 807 | 808 | ```javascript 809 | {"acn":"asl"} 810 | ``` 811 | 812 | EXPECTED RESPONSE: 813 | 814 | ```javascript 815 | { 816 | "acn": "asl", 817 | "ary": [ 818 | { 819 | "brd": true, 820 | "uid": "753B184F-CCCD-42F9-A883-D1DF86E1FFB8", 821 | "zro": 0, 822 | "oCl": "1.000000 0.000000 0.000000", 823 | "fme": [ 824 | { 825 | "ufr": "{{0.025000000000000001, 0.37418655097613884}, {0.40000000000000002, 0.50108459869848154}}", 826 | "mde": 1, 827 | "tCl": "1.000000 1.000000 1.000000", 828 | "tAl": 2, 829 | "tSz": 60, 830 | "nme": "Current Slide", 831 | "typ": 1 832 | }, 833 | { 834 | "ufr": "{{0.024390243902439025, 0.27223427331887201}, {0.40182926829268295, 0.10412147505422993}}", 835 | "mde": 1, 836 | "tCl": "1.000000 1.000000 1.000000", 837 | "tAl": 2, 838 | "tSz": 60, 839 | "nme": "Current Slide Notes", 840 | "typ": 3 841 | }, 842 | { 843 | "ufr": "{{0.45000000000000001, 0.47396963123644253}, {0.29999999999999999, 0.40021691973969631}}", 844 | "mde": 1, 845 | "tCl": "1.000000 1.000000 1.000000", 846 | "tAl": 2, 847 | "tSz": 60, 848 | "nme": "Next Slide", 849 | "typ": 2 850 | }, 851 | { 852 | "ufr": "{{0.45000000000000001, 0.37310195227765725}, {0.29999999999999999, 0.1019522776572668}}", 853 | "mde": 1, 854 | "tCl": "1.000000 1.000000 1.000000", 855 | "tAl": 2, 856 | "tSz": 60, 857 | "nme": "Next Slide Notes", 858 | "typ": 4 859 | }, 860 | { 861 | "ufr": "{{0.77500000000000002, 0.37418655097613884}, {0.20000000000000001, 0.40130151843817785}}", 862 | "nme": "Chord Chart", 863 | "mde": 1, 864 | "typ": 9 865 | }, 866 | { 867 | "ufr": "{{0.050000000000000003, 0.89913232104121477}, {0.20000000000000001, 0.1019522776572668}}", 868 | "mde": 1, 869 | "tCl": "1.000000 1.000000 1.000000", 870 | "tAl": 2, 871 | "tSz": 200, 872 | "nme": "Clock", 873 | "typ": 6 874 | }, 875 | { 876 | "ufr": "{{0.40000000000000002, 0.89913232104121477}, {0.20000000000000001, 0.1019522776572668}}", 877 | "mde": 1, 878 | "tCl": "1.000000 1.000000 1.000000", 879 | "tAl": 2, 880 | "tSz": 200, 881 | "nme": "Video Countdown", 882 | "typ": 8 883 | }, 884 | { 885 | "ufr": "{{0.050000000000000003, 0.024945770065075923}, {0.90000000000000002, 0.10086767895878525}}", 886 | "fCl": "0.000000 1.000000 0.000000", 887 | "mde": 1, 888 | "tCl": "1.000000 1.000000 1.000000", 889 | "tAl": 2, 890 | "fCh": true, 891 | "tSz": 60, 892 | "nme": "Message", 893 | "typ": 5 894 | }, 895 | { 896 | "ufr": "{{0.68978420350609759, 0.89488713394793928}, {0.20000000000000001, 0.1019522776572668}}", 897 | "uid": "47E8B48C-0D61-4EFC-9517-BF9FB894C8E2", 898 | "mde": 1, 899 | "tCl": "1.000000 1.000000 1.000000", 900 | "tAl": 2, 901 | "tSz": 200, 902 | "nme": "Countdown 1", 903 | "typ": 7 904 | } 905 | ], 906 | "ovr": true, 907 | "acn": "sl", 908 | "nme": "Default" 909 | }, 910 | { 911 | "brd": false, 912 | "uid": "50AF3434-4328-40AC-846F-CC9583381311", 913 | "zro": 0, 914 | "oCl": "0.985948 0.000000 0.026951", 915 | "fme": [ 916 | { 917 | "ufr": "{{0.60304878048780486, 0.1963123644251627}, {0.39695121951219514, 0.80043383947939262}}", 918 | "mde": 1, 919 | "tCl": "0.990463 1.000000 0.041173", 920 | "tAl": 1, 921 | "tSz": 80, 922 | "nme": "Current Slide", 923 | "typ": 1 924 | }, 925 | { 926 | "ufr": "{{0.0024390243902439024, 0.0021691973969631237}, {0.599390243902439, 0.99457700650759218}}", 927 | "mde": 1, 928 | "tCl": "0.679783 1.000000 0.885215", 929 | "tAl": 0, 930 | "tSz": 120, 931 | "nme": "Current Slide Notes", 932 | "typ": 3 933 | }, 934 | { 935 | "ufr": "{{0.60304878048780486, 0.0021691973969631237}, {0.39512195121951221, 0.19305856832971802}}", 936 | "uid": "D1096B85-CF31-4365-A6E6-ED94264E7DCA", 937 | "mde": 1, 938 | "tCl": "1.000000 1.000000 1.000000", 939 | "tAl": 2, 940 | "tSz": 200, 941 | "nme": "Elapsed Time", 942 | "typ": 7 943 | } 944 | ], 945 | "ovr": false, 946 | "acn": "sl", 947 | "nme": "Easter Closer" 948 | }, 949 | { 950 | "brd": false, 951 | "uid": "F8260B13-9C5B-4D2C-80F1-C72346759F11", 952 | "zro": 0, 953 | "oCl": "0.985948 0.000000 0.026951", 954 | "fme": [ 955 | { 956 | "ufr": "{{0.025000000000000001, 0.37418655097613884}, {0.40000000000000002, 0.50108459869848154}}", 957 | "mde": 2, 958 | "tCl": "1.000000 1.000000 1.000000", 959 | "tAl": 2, 960 | "tSz": 60, 961 | "nme": "Current Slide", 962 | "typ": 1 963 | }, 964 | { 965 | "ufr": "{{0.025000000000000001, 0.27440347071583515}, {0.40000000000000002, 0.10086767895878525}}", 966 | "mde": 1, 967 | "tCl": "1.000000 1.000000 1.000000", 968 | "tAl": 2, 969 | "tSz": 60, 970 | "nme": "Current Slide Notes", 971 | "typ": 3 972 | }, 973 | { 974 | "ufr": "{{0.45000000000000001, 0.47396963123644253}, {0.29999999999999999, 0.40021691973969631}}", 975 | "mde": 0, 976 | "tCl": "1.000000 1.000000 1.000000", 977 | "tAl": 2, 978 | "tSz": 60, 979 | "nme": "Next Slide", 980 | "typ": 2 981 | }, 982 | { 983 | "ufr": "{{0.45000000000000001, 0.37310195227765725}, {0.29999999999999999, 0.1019522776572668}}", 984 | "mde": 1, 985 | "tCl": "1.000000 1.000000 1.000000", 986 | "tAl": 2, 987 | "tSz": 60, 988 | "nme": "Next Slide Notes", 989 | "typ": 4 990 | }, 991 | { 992 | "ufr": "{{0.77500000000000002, 0.37418655097613884}, {0.20000000000000001, 0.40130151843817785}}", 993 | "nme": "Chord Chart", 994 | "mde": 1, 995 | "typ": 9 996 | }, 997 | { 998 | "ufr": "{{0, 0.89804772234273322}, {0.20060975609756099, 0.10303687635574837}}", 999 | "mde": 1, 1000 | "tCl": "1.000000 1.000000 1.000000", 1001 | "tAl": 2, 1002 | "tSz": 200, 1003 | "nme": "Clock", 1004 | "typ": 6 1005 | }, 1006 | { 1007 | "ufr": "{{0.79878048780487809, 0.89696312364425168}, {0.20060975609756099, 0.10303687635574837}}", 1008 | "mde": 1, 1009 | "tCl": "1.000000 1.000000 1.000000", 1010 | "tAl": 2, 1011 | "tSz": 200, 1012 | "nme": "Video Countdown", 1013 | "typ": 8 1014 | }, 1015 | { 1016 | "ufr": "{{0.050000000000000003, 0.024945770065075923}, {0.90000000000000002, 0.10086767895878525}}", 1017 | "fCl": "0.135296 1.000000 0.024919", 1018 | "mde": 1, 1019 | "tCl": "1.000000 1.000000 1.000000", 1020 | "tAl": 2, 1021 | "fCh": true, 1022 | "tSz": 60, 1023 | "nme": "Message", 1024 | "typ": 5 1025 | } 1026 | ], 1027 | "ovr": false, 1028 | "acn": "sl", 1029 | "nme": "Live Current - Static Next - no borders" 1030 | }, 1031 | { 1032 | "brd": true, 1033 | "uid": "12CB7383-FA02-47BB-B501-747ADCA860D3", 1034 | "zro": 0, 1035 | "oCl": "0.985948 0.000000 0.026951", 1036 | "fme": [ 1037 | { 1038 | "ufr": "{{0.025000000000000001, 0.37418655097613884}, {0.40000000000000002, 0.50108459869848154}}", 1039 | "mde": 0, 1040 | "tCl": "1.000000 1.000000 1.000000", 1041 | "tAl": 2, 1042 | "tSz": 60, 1043 | "nme": "Current Slide", 1044 | "typ": 1 1045 | }, 1046 | { 1047 | "ufr": "{{0.025000000000000001, 0.27440347071583515}, {0.40000000000000002, 0.10086767895878525}}", 1048 | "mde": 1, 1049 | "tCl": "1.000000 1.000000 1.000000", 1050 | "tAl": 2, 1051 | "tSz": 60, 1052 | "nme": "Current Slide Notes", 1053 | "typ": 3 1054 | }, 1055 | { 1056 | "ufr": "{{0.45000000000000001, 0.47396963123644253}, {0.29999999999999999, 0.40021691973969631}}", 1057 | "mde": 0, 1058 | "tCl": "1.000000 1.000000 1.000000", 1059 | "tAl": 2, 1060 | "tSz": 60, 1061 | "nme": "Next Slide", 1062 | "typ": 2 1063 | }, 1064 | { 1065 | "ufr": "{{0.45000000000000001, 0.37310195227765725}, {0.29999999999999999, 0.1019522776572668}}", 1066 | "mde": 1, 1067 | "tCl": "1.000000 1.000000 1.000000", 1068 | "tAl": 2, 1069 | "tSz": 60, 1070 | "nme": "Next Slide Notes", 1071 | "typ": 4 1072 | }, 1073 | { 1074 | "ufr": "{{0.77500000000000002, 0.37418655097613884}, {0.20000000000000001, 0.40130151843817785}}", 1075 | "nme": "Chord Chart", 1076 | "mde": 1, 1077 | "typ": 9 1078 | }, 1079 | { 1080 | "ufr": "{{0, 0.89804772234273322}, {0.20060975609756099, 0.10303687635574837}}", 1081 | "mde": 1, 1082 | "tCl": "1.000000 1.000000 1.000000", 1083 | "tAl": 2, 1084 | "tSz": 200, 1085 | "nme": "Clock", 1086 | "typ": 6 1087 | }, 1088 | { 1089 | "ufr": "{{0.79878048780487809, 0.89696312364425168}, {0.20060975609756099, 0.10303687635574837}}", 1090 | "mde": 1, 1091 | "tCl": "1.000000 1.000000 1.000000", 1092 | "tAl": 2, 1093 | "tSz": 200, 1094 | "nme": "Video Countdown", 1095 | "typ": 8 1096 | }, 1097 | { 1098 | "ufr": "{{0.050000000000000003, 0.024945770065075923}, {0.90000000000000002, 0.10086767895878525}}", 1099 | "fCl": "0.135296 1.000000 0.024919", 1100 | "mde": 1, 1101 | "tCl": "1.000000 1.000000 1.000000", 1102 | "tAl": 2, 1103 | "fCh": true, 1104 | "tSz": 60, 1105 | "nme": "Message", 1106 | "typ": 5 1107 | } 1108 | ], 1109 | "ovr": false, 1110 | "acn": "sl", 1111 | "nme": "Static Current - Static Next" 1112 | } 1113 | ] 1114 | } 1115 | ``` 1116 | 1117 | - `acn` of `asl` means "all stage layouts" 1118 | - `ary` indicates array of stage layouts 1119 | - `nme` indicates layout name 1120 | - `ovr` indicates if overrun color should be used 1121 | - `oCl` indicates color for timer overruns 1122 | - `brd` indicates if borders and labels should be used 1123 | - `uid` indicates layout uid 1124 | - `zro` indicates if zeroes should be removed from times 1125 | - `fme` indicates array of frame layout specifications 1126 | - frame positions are indicated by `ufr` and specified in terms of screen percentages 1127 | - frame name is indicated by `nme` 1128 | - frame text color is indicated by `tCl` 1129 | - frame font size is indicated by `tSz` 1130 | - frame message flash color is indicated by `fCl` 1131 | - frame use message flash indicated by `fCh` 1132 | - frame timer uid is indicated by `uid` 1133 | - frame mode is indicated by `mde` 1134 | - mode 0: static image 1135 | - mode 1: text 1136 | - mode 2: live slide 1137 | - frame type is indicated by `typ` and determines what content goes in this frame 1138 | - type 1: current slide 1139 | - type 2: next slide 1140 | - type 3: current slide notes 1141 | - type 4: next slide notes 1142 | - type 5: Stage Message (uses message flash values) 1143 | - type 6: Clock 1144 | - type 7: Timer Display (uses `uid` to specify timer) 1145 | - type 8: Video Countdown 1146 | - type 9: Chord Chart 1147 | 1148 | ### On New Stage Display Selected 1149 | 1150 | ```javascript 1151 | { 1152 | "brd": true, 1153 | "uid": "12CB7383-FA02-47BB-B501-747ADCA860D3", 1154 | "zro": 0, 1155 | "oCl": "0.985948 0.000000 0.026951", 1156 | "fme": [ 1157 | { 1158 | "ufr": "{{0.025000000000000001, 0.37418655097613884}, {0.40000000000000002, 0.50108459869848154}}", 1159 | "mde": 0, 1160 | "tCl": "1.000000 1.000000 1.000000", 1161 | "tAl": 2, 1162 | "tSz": 60, 1163 | "nme": "Current Slide", 1164 | "typ": 1 1165 | }, 1166 | { 1167 | "ufr": "{{0.025000000000000001, 0.27440347071583515}, {0.40000000000000002, 0.10086767895878525}}", 1168 | "mde": 1, 1169 | "tCl": "1.000000 1.000000 1.000000", 1170 | "tAl": 2, 1171 | "tSz": 60, 1172 | "nme": "Current Slide Notes", 1173 | "typ": 3 1174 | }, 1175 | { 1176 | "ufr": "{{0.45000000000000001, 0.47396963123644253}, {0.29999999999999999, 0.40021691973969631}}", 1177 | "mde": 0, 1178 | "tCl": "1.000000 1.000000 1.000000", 1179 | "tAl": 2, 1180 | "tSz": 60, 1181 | "nme": "Next Slide", 1182 | "typ": 2 1183 | }, 1184 | { 1185 | "ufr": "{{0.45000000000000001, 0.37310195227765725}, {0.29999999999999999, 0.1019522776572668}}", 1186 | "mde": 1, 1187 | "tCl": "1.000000 1.000000 1.000000", 1188 | "tAl": 2, 1189 | "tSz": 60, 1190 | "nme": "Next Slide Notes", 1191 | "typ": 4 1192 | }, 1193 | { 1194 | "ufr": "{{0.77500000000000002, 0.37418655097613884}, {0.20000000000000001, 0.40130151843817785}}", 1195 | "nme": "Chord Chart", 1196 | "mde": 1, 1197 | "typ": 9 1198 | }, 1199 | { 1200 | "ufr": "{{0, 0.89804772234273322}, {0.20060975609756099, 0.10303687635574837}}", 1201 | "mde": 1, 1202 | "tCl": "1.000000 1.000000 1.000000", 1203 | "tAl": 2, 1204 | "tSz": 200, 1205 | "nme": "Clock", 1206 | "typ": 6 1207 | }, 1208 | { 1209 | "ufr": "{{0.79878048780487809, 0.89696312364425168}, {0.20060975609756099, 0.10303687635574837}}", 1210 | "mde": 1, 1211 | "tCl": "1.000000 1.000000 1.000000", 1212 | "tAl": 2, 1213 | "tSz": 200, 1214 | "nme": "Video Countdown", 1215 | "typ": 8 1216 | }, 1217 | { 1218 | "ufr": "{{0.050000000000000003, 0.024945770065075923}, {0.90000000000000002, 0.10086767895878525}}", 1219 | "fCl": "0.135296 1.000000 0.024919", 1220 | "mde": 1, 1221 | "tCl": "1.000000 1.000000 1.000000", 1222 | "tAl": 2, 1223 | "fCh": true, 1224 | "tSz": 60, 1225 | "nme": "Message", 1226 | "typ": 5 1227 | } 1228 | ], 1229 | "ovr": false, 1230 | "acn": "sl", 1231 | "nme": "Static Current - Static Next" 1232 | } 1233 | ``` 1234 | 1235 | - `acn` of `sl` indicates this is a single stage layout 1236 | 1237 | ### Request Current Stage Display Layout 1238 | 1239 | COMMAND TO SEND: 1240 | 1241 | ```javascript 1242 | {"acn":"psl"} 1243 | ``` 1244 | 1245 | EXPECTED RESPONSE (also used when stage display is updated): 1246 | 1247 | ```javascript 1248 | {"acn":"psl","uid":"[STAGE DISPLAY UID]"} 1249 | ``` 1250 | 1251 | ### Request Frame Values for Stage Display 1252 | 1253 | COMMAND TO SEND: 1254 | 1255 | ```javascript 1256 | {"acn":"fv","uid":"[STAGE DISPLAY UID"} 1257 | ``` 1258 | 1259 | ### On New Live Slide Frame 1260 | 1261 | ```javascript 1262 | { 1263 | "RVLiveStream_action": "RVLiveStream_frameData", 1264 | "RVLiveStream_frameDataLength": 14625, 1265 | "RVLiveStream_frameData": "/9j//gAQTGF2YzU3LjUzLjEwMAD/2wBDAAgEBAQEBAUFBQUFBQYGBgYGBgYGBgYGBgYHBwcICAgHBwcGBgcHCAgICAkJCQgICAgJCQoKCgwMCwsODg4RERT/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsBAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKCxAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/wAARCAEOAeADASIAAhIAAxIA/9oADAMBAAIRAxEAPwDwqiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKKAAAooooAACiiigAAKKKOtAAAUUux/7p/I0bH/ALp/I0AACUUux/7p/I0bH/un8jQAAJRS7H/un8jRsf8Aun8jQAAJRS7H/un8jRsf+6fyNAAAlFLsf+6fyNGx/wC6fyNAAAlFLsf+6fyNGx/7p/I0AACUUux/7p/I0bH/ALp/I0AACUUux/7p/I0bH/un8jQAAJRS7H/un8jRsf8Aun8jQAAJRS7H/un8jRsf+6fyNAAAlFLsf+6fyNGx/wC6fyNAAAlFLsf+6fyNGx/7p/I0AACUUux/7p/I0bH/ALp/I0AACUUux/7p/I0bH/un8jQAAJRS7H/un8jRsf8Aun8jQAAJRS7H/un8jRsf+6fyNAAAlFLsf+6fyNGx/wC6fyNAAAlFLsf+6fyNGx/7p/I0AACUUux/7p/I0bH/ALp/I0AACUUux/7p/I0bH/un8jQAAJRS7H/un8jRsf8Aun8jQAAJRS7H/un8jRsf+6fyNAAAlFLsf+6fyNGx/wC6fyNAAAlFLsf+6fyNGx/7p/I0AACUUux/7p/I0bH/ALp/I0AACUUux/7p/I0bH/un8jQAAJRRRQAAFFFFAAAUUUUAABXafs9wQXPxg8LRTxRzRtcT7o5EV0b/AEWY8qwIPPqK4uu2/Z0/5LL4U/6+bj/0kmoAAPrL/hH9B/6BOm/+Adv/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" 1266 | } 1267 | ``` 1268 | 1269 | - Base64 Encoded Image Bytes 1270 | - Only the Current Slide can be "Live" 1271 | - Live slide images are pushed to the client over the websocket. 1272 | 1273 | ### To Get Static Slide Images 1274 | 1275 | TO RETRIEVE STATIC SLIDE IMAGES: 1276 | 1277 | Issue a normal GET request to the following URL: 1278 | 1279 | http://PROPRESENTER_IP:PROPRESENTER_PORT/stage/image/SLIDE_UID 1280 | 1281 | EXPECTED RESPONSE: 1282 | 1283 | rgba tiff image 1284 | 1285 | ( DIFFERENCE FROM 6: Pro6 would send images as standard jpeg, but Pro7 transmits them as rgba tiff images with no content-type set in the header ) 1286 | 1287 | ### On New Time 1288 | 1289 | EXPECTED RESPONSE: 1290 | 1291 | ```javascript 1292 | {"acn":"sys","txt":" 11:17 AM"} 1293 | ``` 1294 | 1295 | ### On Timer Update 1296 | 1297 | EXPECTED RESPONSE: 1298 | 1299 | ```javascript 1300 | { acn: 'tmr', 1301 | uid: '[TIMER UID]', 1302 | txt: '--:--:--' } 1303 | ``` 1304 | 1305 | ### On New Slide 1306 | 1307 | EXPECTED RESPONSE: 1308 | 1309 | ```javascript 1310 | { 1311 | "acn": "fv", 1312 | "ary": [ 1313 | { 1314 | "acn": "cs", # CURRENT SLIDE 1315 | "uid": "[SLIDE UID]", 1316 | "txt": "[SLIDE TEXT]" 1317 | }, 1318 | { 1319 | "acn": "ns", # NEXT SLIDE 1320 | "uid": "[SLIDE UID]", 1321 | "txt": "[SLIDE TEXT]" 1322 | }, 1323 | { 1324 | "acn": "csn", # CURRENT SLIDE NOTES 1325 | "txt": "[SLIDE NOTES]" 1326 | }, 1327 | { 1328 | "acn": "nsn", # NEXT SLIDE NOTES 1329 | "txt": "[SLIDE NOTES]" 1330 | } 1331 | ] 1332 | } 1333 | ``` 1334 | --------------------------------------------------------------------------------