Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
# ProPresenter-OBS

Uses the ProPresenter Stage Display websocket to feed current slide text into a OBS browser source lower third.
Uses the ProPresenter Stage Display websocket **or** OpenAPI to feed current slide text into a OBS browser source lower third.

The original code was from https://github.com/cgarwood/ProPresenter-vMix and made for vMix.

In ProPresenter you must Enable Network and Stage Display App under preferences. Set a password under Stage Display App.
## Configuration

Rename config.js.example to config.js and update the values to match your environment. Use the port listed under Enable Network not the port under the Stage Display App. Use the password under Stage Display App.
Rename config.js.example to config.js and update the values to match your environment.

### API Type Selection

Choose which ProPresenter API to use:

**Option 1: Stage Display API (WebSocket)** - Default, Most Compatible
- Set `api_type: 'stagedisplay'` in config.js
- **Works with ProPresenter 6, ProPresenter 7 (all versions), and ProPresenter 7.9.1+**
- Most widely compatible option - works with all ProPresenter versions
- In ProPresenter: Enable Network and Stage Display App under preferences/settings
- Set a password under Stage Display App
- Use the port listed under Enable Network (NOT the Stage Display App port)
- Use the password from Stage Display App in config.js

**Option 2: OpenAPI (HTTP REST)** - Modern Alternative for ProPresenter 7.9.1+
- Set `api_type: 'openapi'` in config.js
- **Requires ProPresenter 7.9.1 or newer** - will not work on older versions
- In ProPresenter: Enable Network under settings
- No password required - simpler setup
- Uses HTTP streaming for real-time updates
- Uses the official REST API provided by ProPresenter
- Best choice if you're running ProPresenter 7.9.1+ and want simpler configuration

**Which should I use?**
- Use **Stage Display** (default) if you're on ProPresenter 6 or any ProPresenter 7 version before 7.9.1
- Use **Stage Display** if you want maximum compatibility
- Use **OpenAPI** if you're on ProPresenter 7.9.1+ and prefer no password setup

### OBS Setup

In OBS, add a new browser source, check "Local File", navigate to index.html, then set width, height, and FPS to match your stream settings. Edit stylesheet.css to customize your lower thirds setup.

### Filtering Slides

Set the slide notes to `no-obs` on a slide to prevent it from being sent to OBS (useful for slides with more text than you would want in a lower third).
4 changes: 3 additions & 1 deletion config.js.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
//ProPresenter-OBS Configuration
var config = {
//API Type: 'stagedisplay' (default, WebSocket, PP6+) or 'openapi' (HTTP REST, PP 7.9.1+ only)
'api_type' : 'stagedisplay', // 'stagedisplay' or 'openapi'
//IP and Port for ProPresenter Network Connection
//Enable under ProPresenter Preferences -> Network tab
//Enable Network and Stage Display App
'propresenter_ip' : '192.168.1.4',
'propresenter_port' : 60157, //port from Network NOT Stage Display App
'propresenter_password' : 'av', //password from Stage Display App
'propresenter_password' : 'av', //password from Stage Display App (only used for stagedisplay API)
'fade' : true,
'fade_speed' : 200, // milliseconds 600 slow, 400 normal, 200 fast
'fade_repeated_text' : true, // set true to fade in/out repeated text
Expand Down
96 changes: 92 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,21 @@


function connectToProPresenter() {
// Choose connection method based on API type
if (config['api_type'] === 'openapi') {
connectToProPresenterOpenAPI();
} else {
connectToProPresenterStageDisplay();
}
}

function connectToProPresenterStageDisplay() {
var proPresenterStageDisplayURL = 'ws://' + config['propresenter_ip'] + ':' + config['propresenter_port'] + '/stagedisplay';

var ws = new WebSocket(proPresenterStageDisplayURL);

ws.onopen = function() {
console.log('ProPresenter connection opened');
console.log('ProPresenter Stage Display connection opened');
ws.send(JSON.stringify({
"pwd": config['propresenter_password'],
"ptl": 610,
Expand All @@ -75,18 +84,97 @@
};

ws.onclose = function(e) {
console.log('ProPresenter socket was closed. Attempting to reconnect in 1 second...', e.reason);
console.log('ProPresenter Stage Display socket was closed. Attempting to reconnect in 1 second...', e.reason);
setTimeout(function() {
connectToProPresenter();
connectToProPresenterStageDisplay();
}, 1000);
};

ws.onerror = function(err) {
console.error('ProPresenter socket has error: ', err.message, 'Closing socket');
console.error('ProPresenter Stage Display socket has error: ', err.message, 'Closing socket');
ws.close();
};
}

async function connectToProPresenterOpenAPI() {
var openAPIURL = 'http://' + config['propresenter_ip'] + ':' + config['propresenter_port'] + '/v1/status/updates';

console.log('Connecting to ProPresenter OpenAPI:', openAPIURL);

try {
const response = await fetch(openAPIURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(['status/slide'])
});

if (!response.ok) {
throw new Error('HTTP ' + response.status);
}

console.log('ProPresenter OpenAPI connection established');

// Read the streaming response
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
const {done, value} = await reader.read();

if (done) {
console.log('ProPresenter OpenAPI stream ended. Reconnecting...');
setTimeout(connectToProPresenterOpenAPI, 1000);
break;
}

// Decode the chunk and add to buffer
buffer += decoder.decode(value, {stream: true});

// Process complete JSON lines
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep incomplete line in buffer

for (const line of lines) {
if (line.trim()) {
try {
const data = JSON.parse(line);

// Convert OpenAPI format to Stage Display format
if (data.url === 'status/slide' && data.data && data.data.current) {
const slideData = data.data.current;

// Create message in Stage Display format
const stageDisplayMessage = {
'acn': 'fv',
'ary': [
{
'acn': 'cs',
'txt': slideData.text || ''
},
{
'txt': slideData.notes || ''
}
]
};

processProPresenterMessage(stageDisplayMessage);
}
} catch (err) {
// Skip non-JSON lines
console.debug('OpenAPI: Skipped line:', line.substring(0, 50));
}
}
}
}
} catch (err) {
console.error('ProPresenter OpenAPI connection error:', err.message, 'Retrying in 1 second...');
setTimeout(connectToProPresenterOpenAPI, 1000);
}
}



connectToProPresenter();
Expand Down