├── .editorconfig ├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── dist ├── obs-remote.js └── obs-remote.min.js ├── docs └── OBSRemote.md ├── index.js ├── package.json └── src ├── .jshintrc ├── obs-remote.js ├── obs-scene.js └── obs-source.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Enforce Unix line-endings 4 | # 4 spaces instead of tabs 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | indent_style = space 11 | indent_size = 4 12 | 13 | charset = utf-8 14 | 15 | # NPM forces an indent size of 2 on package.json. 16 | # If you can't beat em, join em. 17 | [*.json] 18 | indent_size = 2 19 | 20 | # Don't remove trailing whitespace from Markdown 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/ 3 | 4 | # OS specific 5 | .DS_Store 6 | 7 | # JetBrains IDEs (IntelliJ/WebStorm) 8 | .idea/ 9 | *.iml -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 'use strict'; 3 | 4 | // Force use of Unix newlines 5 | grunt.util.linefeed = '\n'; 6 | 7 | // Project configuration. 8 | grunt.initConfig({ 9 | 10 | // Metadata. 11 | pkg: grunt.file.readJSON('package.json'), 12 | banner: '/*!\n' + 13 | ' * OBS Remote JS API v<%= pkg.version %> (<%= pkg.homepage %>)\n' + 14 | ' * Copyright 2014 <%= pkg.author %>\n' + 15 | ' * Licensed under <%= pkg.license.type %> (<%= pkg.license.url %>)\n' + 16 | ' */\n', 17 | 18 | // Task configuration. 19 | clean: { 20 | dist: 'dist' 21 | }, 22 | 23 | jshint: { 24 | options: { 25 | jshintrc: 'src/.jshintrc' 26 | }, 27 | core: { 28 | src: 'src/*.js' 29 | } 30 | }, 31 | 32 | concat: { 33 | options: { 34 | banner: '<%= banner %>', 35 | stripBanners: false 36 | }, 37 | obsremote: { 38 | src: [ 39 | 'src/obs-source.js', 40 | 'src/obs-scene.js', 41 | 'src/obs-remote.js' 42 | ], 43 | dest: 'dist/<%= pkg.name %>.js' 44 | } 45 | }, 46 | 47 | uglify: { 48 | options: { 49 | preserveComments: 'some' 50 | }, 51 | core: { 52 | src: '<%= concat.obsremote.dest %>', 53 | dest: 'dist/<%= pkg.name %>.min.js' 54 | } 55 | } 56 | }); 57 | 58 | // These plugins provide necessary tasks. 59 | require('load-grunt-tasks')(grunt, { scope: 'devDependencies' }); 60 | require('time-grunt')(grunt); 61 | 62 | // JS distribution task. 63 | grunt.registerTask('build', ['clean:dist', 'concat', 'uglify:core']); 64 | 65 | // Default task. 66 | grunt.registerTask('default', ['build']); 67 | }; 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Matthew McNamara 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OBS Remote JS 2 | 3 | ## ❗ DEPRECATION NOTICE ❗ 4 | This library is for the very old OBS Remote plugin for the very old OBS Classic. It DOES NOT work with the newer OBS Studio or obs-websocket. You probably want to use [obs-websocket-js](https://github.com/haganbmj/obs-websocket-js) instead. 5 | 6 | ----- 7 | 8 | OBS Remote JS is a Javascript API for [OBS Remote](http://www.obsremote.com/), a plugin [for Open Broadcaster Software](https://obsproject.com/), which can be used in browsers and NodeJS. 9 | It largely follows the API laid out in this plugin, but some callbacks have been changed for ease of use. 10 | Documentation is provided in the `docs` folder. 11 | 12 | ### Installation and Usage 13 | #####Node 14 | `npm install obs-remote` 15 | 16 | ```var OBSRemote = require('obs-remote'); 17 | var obs = new OBSRemote(); 18 | obs.connect('localhost', 'myPassword');``` 19 | 20 | #####Browser (via Bower) 21 | `bower install obs-remote` 22 | 23 | ```var obs = new OBSRemote(); 24 | obs.connect('localhost', 'myPassword');``` 25 | 26 | ### License 27 | OBS Remote JS is provided under the MIT license, which is available to read in the [LICENSE][] file. 28 | [license]: LICENSE 29 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obs-remote", 3 | "version": "1.0.1", 4 | "homepage": "https://github.com/NodeCG/obs-remote-js", 5 | "authors": [ 6 | "Matthew McNamara " 7 | ], 8 | "description": "OBS Remote API for Node.JS and browsers", 9 | "main": [ 10 | "dist/obs-remote.js" 11 | ], 12 | "keywords": [ 13 | "obs", 14 | "remote" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /dist/obs-remote.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OBS Remote JS API v1.0.1 (https://github.com/nodecg/obs-remote-js) 3 | * Copyright 2014 Matthew McNamara 4 | * Licensed under MIT (https://github.com/nodecg/obs-remote-js/blob/master/LICENSE) 5 | */ 6 | (function () { 7 | 'use strict'; 8 | 9 | function OBSSource(width, height, x, y, name, rendered) { 10 | this.width = width || 0; 11 | this.height = height || 0; 12 | this.x = x || 0; 13 | this.y = y || 0; 14 | this.name = name || ''; 15 | this.rendered = rendered || false; 16 | } 17 | 18 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 19 | module.exports.OBSSource = OBSSource; 20 | } else { 21 | window.OBSSource = OBSSource; 22 | } 23 | })(); 24 | 25 | (function () { 26 | 'use strict'; 27 | 28 | function OBSScene(name, sources) { 29 | this.name = name || ''; 30 | this.sources = sources || []; 31 | } 32 | 33 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 34 | module.exports.OBSScene = OBSScene; 35 | } else { 36 | window.OBSScene = OBSScene; 37 | } 38 | })(); 39 | 40 | (function () { 41 | 'use strict'; 42 | 43 | var OBSSource = {}; 44 | var OBSScene = {}; 45 | 46 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 47 | OBSScene = module.exports.OBSScene; 48 | OBSSource = module.exports.OBSSource; 49 | } else { 50 | OBSScene = window.OBSScene; 51 | OBSSource = window.OBSSource; 52 | } 53 | 54 | function OBSRemote() { 55 | OBSRemote.API_VERSION = 1.1; 56 | OBSRemote.DEFAULT_PORT = 4444; 57 | OBSRemote.WS_PROTOCOL = 'obsapi'; 58 | 59 | this._connected = false; 60 | this._socket = undefined; 61 | this._messageCounter = 0; 62 | this._responseCallbacks = {}; 63 | 64 | this._auth = {salt: '', challenge: ''}; 65 | } 66 | 67 | // IE11 crypto object is prefixed 68 | var crypto = {}; 69 | var WebSocket = {}; 70 | 71 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 72 | crypto = require('crypto'); 73 | OBSRemote.prototype._authHash = OBSRemote.prototype._nodeCryptoHash; 74 | 75 | WebSocket = require('ws'); 76 | } else { 77 | crypto = window.crypto || window.msCrypto || {}; 78 | OBSRemote.prototype._authHash = OBSRemote.prototype._webCryptoHash; 79 | 80 | if (typeof crypto.subtle === 'undefined') { 81 | // Safari crypto.subtle is prefixed, all other browsers use subtle or don't implement 82 | if (typeof crypto.webkitSubtle === 'undefined') { 83 | // Native crypto not available, fall back to CryptoJS 84 | if (typeof CryptoJS === 'undefined') { 85 | throw new Error('OBS Remote requires CryptoJS when native crypto is not available!'); 86 | } 87 | 88 | OBSRemote.prototype._authHash = OBSRemote.prototype._cryptoJSHash; 89 | } else { 90 | crypto.subtle = crypto.webkitSubtle; 91 | } 92 | } 93 | 94 | WebSocket = window.WebSocket; 95 | } 96 | 97 | /** 98 | * Try to connect to OBS, with optional password 99 | * @param address "ipAddress" or "ipAddress:port" 100 | * defaults to "localhost" 101 | * @param password Optional authentication password 102 | */ 103 | OBSRemote.prototype.connect = function(address, password) { 104 | // Password is optional, set to empty string if undefined 105 | password = (typeof password === 'undefined') ? 106 | '' : 107 | password; 108 | 109 | // Check for address 110 | address = (typeof address === 'undefined' || address === '') ? 111 | 'localhost' : 112 | address; 113 | 114 | // Check for port number, if missing use 4444 115 | var colonIndex = address.indexOf(':'); 116 | if (colonIndex < 0 || colonIndex === address.length - 1) { 117 | address += ':' + OBSRemote.DEFAULT_PORT; 118 | } 119 | 120 | // Check if we already have a connection 121 | if (this._connected) { 122 | this._socket.close(); 123 | this._connected = false; 124 | } 125 | 126 | // Connect and setup WebSocket callbacks 127 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 128 | this._socket = new WebSocket('ws://' + address, {protocol: OBSRemote.WS_PROTOCOL}); 129 | } else { 130 | this._socket = new WebSocket('ws://' + address, OBSRemote.WS_PROTOCOL); 131 | } 132 | 133 | var self = this; 134 | 135 | this._socket.onopen = function() { 136 | self._connected = true; 137 | self.onConnectionOpened(); 138 | 139 | self.isAuthRequired(function(required) { 140 | if (!required) return; 141 | 142 | self.authenticate(password); 143 | }); 144 | }; 145 | 146 | this._socket.onclose = function() { 147 | self.onConnectionClosed(); 148 | self._connected = false; 149 | }; 150 | 151 | this._socket.onerror = function(event) { 152 | self.onConnectionFailed(event); 153 | self._connected = false; 154 | }; 155 | 156 | this._socket.onmessage = function(message) { 157 | self._messageReceived(message); 158 | }; 159 | }; 160 | 161 | /** 162 | * Attempts to authenticate with OBS 163 | * Will cause either onAuthenticationFailed or onAuthenticationSucceeded to be called 164 | * @param password the password to try authenticating with 165 | */ 166 | OBSRemote.prototype.authenticate = function(password) { 167 | var self = this; 168 | this._authHash(password, function(authResp) { 169 | 170 | function cb(message) { 171 | var successful = (message.status === 'ok'); 172 | var remainingAttempts = 0; 173 | 174 | if (!successful) { 175 | // eugh 176 | remainingAttempts = message.error.substr(43); 177 | 178 | self.onAuthenticationFailed(remainingAttempts); 179 | } else { 180 | self.onAuthenticationSucceeded(); 181 | } 182 | } 183 | 184 | self._sendMessage('Authenticate', { 185 | 'auth': authResp 186 | }, cb); 187 | }); 188 | }; 189 | 190 | /** 191 | * Starts or stops OBS from streaming, recording, or previewing. 192 | * Result of this will be either the onStreamStarted or onStreamStopped callback. 193 | * @param previewOnly Only toggle the preview 194 | */ 195 | OBSRemote.prototype.toggleStream = function(previewOnly) { 196 | // previewOnly is optional, default to false 197 | previewOnly = (typeof previewOnly === 'undefined') ? 198 | false : 199 | previewOnly; 200 | 201 | this._sendMessage('StartStopStreaming', { 202 | 'preview-only': previewOnly 203 | }); 204 | }; 205 | 206 | /** 207 | * Requests OBS Remote version 208 | * @param callback function(Number version) 209 | */ 210 | OBSRemote.prototype.getVersion = function(callback) { 211 | function cb (message) { 212 | callback(message.version); 213 | } 214 | 215 | this._sendMessage('GetVersion', cb); 216 | }; 217 | 218 | /** 219 | * Checks if authentication is required 220 | * @param callback function(Boolean isRequired) 221 | */ 222 | OBSRemote.prototype.isAuthRequired = function(callback) { 223 | var self = this; 224 | function cb (message) { 225 | var authRequired = message.authRequired; 226 | 227 | if (authRequired) { 228 | self._auth.salt = message.salt; 229 | self._auth.challenge = message.challenge; 230 | } 231 | 232 | callback(authRequired); 233 | } 234 | 235 | this._sendMessage('GetAuthRequired', cb); 236 | }; 237 | 238 | /** 239 | * Gets name of current scene and full list of all other scenes 240 | * @param callback function(String currentScene, Array scenes) 241 | */ 242 | OBSRemote.prototype.getSceneList = function(callback) { 243 | function cb (message) { 244 | var currentScene = message['current-scene']; 245 | var scenes = []; 246 | 247 | message.scenes.forEach(function(scene) { 248 | scenes.push(_convertToOBSScene(scene)); 249 | }); 250 | 251 | callback(currentScene, scenes); 252 | } 253 | 254 | this._sendMessage('GetSceneList', cb); 255 | }; 256 | 257 | /** 258 | * Gets the current scene and full list of sources 259 | * @param callback function(OBSScene scene) 260 | */ 261 | OBSRemote.prototype.getCurrentScene = function(callback) { 262 | function cb (message) { 263 | var obsScene = _convertToOBSScene(message); 264 | 265 | callback(obsScene); 266 | } 267 | 268 | this._sendMessage('GetCurrentScene', cb); 269 | }; 270 | 271 | /** 272 | * Tells OBS to switch to the given scene name 273 | * If successful onSceneSwitched will be called 274 | * @param sceneName name of scene to switch to 275 | */ 276 | OBSRemote.prototype.setCurrentScene = function(sceneName) { 277 | this._sendMessage('SetCurrentScene', { 278 | 'scene-name': sceneName 279 | }); 280 | }; 281 | 282 | /** 283 | * Reorders sources in the current scene 284 | * @param sources Array of Strings, or OBSSources 285 | */ 286 | OBSRemote.prototype.setSourcesOrder = function(sources) { 287 | var sourceNames = sources; 288 | 289 | // Support Array[OBSSource] for convenience 290 | if (sources[1] instanceof 'OBSSource') { 291 | sourceNames = []; 292 | sources.forEach(function (source) { 293 | sourceNames.push(source.name); 294 | }); 295 | } 296 | 297 | this._sendMessage('SetSourcesOrder', { 298 | 'scene-names': sourceNames 299 | }); 300 | }; 301 | 302 | /** 303 | * Sets a source's render state in the current scene 304 | * @param sourceName 305 | * @param shouldRender 306 | */ 307 | OBSRemote.prototype.setSourceRender = function(sourceName, shouldRender) { 308 | this._sendMessage('SetSourceRender', { 309 | source: sourceName, 310 | render: shouldRender 311 | }); 312 | }; 313 | 314 | /** 315 | * Gets current streaming status, and if we're previewing or not 316 | * @param callback function(Boolean streaming, Boolean previewOnly) 317 | */ 318 | OBSRemote.prototype.getStreamingStatus = function(callback) { 319 | function cb(message) { 320 | callback(message.streaming, message['preview-only']); 321 | } 322 | 323 | this._sendMessage('GetStreamingStatus', cb); 324 | }; 325 | 326 | /** 327 | * Gets current volume levels and mute statuses 328 | * @param callback function(Number microphoneVolume, Boolean microphoneMuted, Number desktopVolume, Boolean desktop) 329 | */ 330 | OBSRemote.prototype.getVolumes = function(callback) { 331 | function cb(message) { 332 | callback(message['mic-volume'], message['mic-muted'], message['desktop-volume'], message['desktop-muted']); 333 | } 334 | 335 | this._sendMessage('GetVolumes', cb); 336 | }; 337 | 338 | /** 339 | * Sets microphone volume, and whether we're still adjusting it 340 | * @param volume 341 | * @param adjusting Optional, defaults to false 342 | */ 343 | OBSRemote.prototype.setMicrophoneVolume = function(volume, adjusting) { 344 | this._sendMessage('SetVolume', { 345 | channel: 'microphone', 346 | volume: volume, 347 | final: !adjusting 348 | }); 349 | }; 350 | 351 | /** 352 | * Toggles microphone mute state 353 | */ 354 | OBSRemote.prototype.toggleMicrophoneMute = function() { 355 | this._sendMessage('ToggleMute', { 356 | channel: 'microphone' 357 | }); 358 | }; 359 | 360 | /** 361 | * Sets desktop volume, and whether we're still adjusting it 362 | * @param volume 363 | * @param adjusting Optional, defaults to false 364 | */ 365 | OBSRemote.prototype.setDesktopVolume = function(volume, adjusting) { 366 | this._sendMessage('SetVolume', { 367 | channel: 'desktop', 368 | volume: volume, 369 | final: !adjusting 370 | }); 371 | }; 372 | 373 | /** 374 | * Toggles desktop mute state 375 | */ 376 | OBSRemote.prototype.toggleDesktopMute = function() { 377 | this._sendMessage('ToggleMute', { 378 | channel: 'desktop' 379 | }); 380 | }; 381 | 382 | /** 383 | * OBSRemote API callbacks 384 | */ 385 | 386 | /* jshint ignore:start */ 387 | 388 | /** 389 | * Called when the connection to OBS is made 390 | * You may still need to authenticate! 391 | */ 392 | OBSRemote.prototype.onConnectionOpened = function() {}; 393 | 394 | /** 395 | * Called when the connection to OBS is closed 396 | */ 397 | OBSRemote.prototype.onConnectionClosed = function() {}; 398 | 399 | /** 400 | * Called when the connection to OBS fails 401 | */ 402 | OBSRemote.prototype.onConnectionFailed = function() {}; 403 | 404 | /** 405 | * Called when authentication is successful 406 | */ 407 | OBSRemote.prototype.onAuthenticationSucceeded = function() {}; 408 | 409 | /** 410 | * Called when authentication fails 411 | * @param remainingAttempts how many more attempts can be made 412 | */ 413 | OBSRemote.prototype.onAuthenticationFailed = function(remainingAttempts) {}; 414 | 415 | /** 416 | * OBS standard callbacks 417 | */ 418 | 419 | /** 420 | * Called when OBS starts streaming, recording or previewing 421 | * @param previewing are we previewing or 'LIVE' 422 | */ 423 | OBSRemote.prototype.onStreamStarted = function(previewing) {}; 424 | 425 | /** 426 | * Called when OBS stops streaming, recording or previewing 427 | * @param previewing were we previewing, or 'LIVE' 428 | */ 429 | OBSRemote.prototype.onStreamStopped = function(previewing) {}; 430 | 431 | /** 432 | * Called frequently by OBS while live or previewing 433 | * @param streaming are we streaming (or recording) 434 | * @param previewing are we previewing or live 435 | * @param bytesPerSecond 436 | * @param strain 437 | * @param streamDurationInMS 438 | * @param totalFrames 439 | * @param droppedFrames 440 | * @param framesPerSecond 441 | */ 442 | OBSRemote.prototype.onStatusUpdate = function(streaming, previewing, bytesPerSecond, strain, streamDurationInMS, 443 | totalFrames, droppedFrames, framesPerSecond) {}; 444 | 445 | /** 446 | * Called when OBS switches scene 447 | * @param sceneName scene OBS has switched to 448 | */ 449 | OBSRemote.prototype.onSceneSwitched = function(sceneName) {}; 450 | 451 | /** 452 | * Called when the scene list changes (new order, addition, removal or renaming) 453 | * @param scenes new list of scenes 454 | */ 455 | OBSRemote.prototype.onScenesChanged = function(scenes) {}; 456 | 457 | /** 458 | * Called when source oder changes in the current scene 459 | * @param sources 460 | */ 461 | OBSRemote.prototype.onSourceOrderChanged = function(sources) {}; 462 | 463 | /** 464 | * Called when a source is added or removed from the current scene 465 | * @param sources 466 | */ 467 | OBSRemote.prototype.onSourceAddedOrRemoved = function(sources) {}; 468 | 469 | /** 470 | * Called when a source in the current scene changes 471 | * @param originalName if the name changed, this is what it was originally 472 | * @param source 473 | */ 474 | OBSRemote.prototype.onSourceChanged = function(originalName, source) {}; 475 | 476 | /** 477 | * Called when the microphone volume changes, or is muted 478 | * @param volume 479 | * @param muted 480 | * @param adjusting 481 | */ 482 | OBSRemote.prototype.onMicrophoneVolumeChanged = function(volume, muted, adjusting) {}; 483 | 484 | /** 485 | * Called when the desktop volume changes, or is muted 486 | * @param volume 487 | * @param muted 488 | * @param adjusting 489 | */ 490 | OBSRemote.prototype.onDesktopVolumeChanged = function(volume, muted, adjusting) {}; 491 | 492 | /* jshint ignore:end */ 493 | 494 | OBSRemote.prototype._sendMessage = function(requestType, args, callback) { 495 | if (this._connected) { 496 | var msgId = this._getNextMsgId(); 497 | 498 | // Callback but no args 499 | if (typeof args === 'function') { 500 | callback = args; 501 | args = {}; 502 | } 503 | 504 | // Ensure message isn't undefined, use empty object 505 | args = args || {}; 506 | 507 | // Ensure callback isn't undefined, use empty function 508 | callback = callback || function () {}; 509 | 510 | // Store the callback with the message ID 511 | this._responseCallbacks[msgId] = callback; 512 | 513 | args['message-id'] = msgId; 514 | args['request-type'] = requestType; 515 | 516 | var serialisedMsg = JSON.stringify(args); 517 | this._socket.send(serialisedMsg); 518 | } 519 | }; 520 | 521 | OBSRemote.prototype._getNextMsgId = function() { 522 | this._messageCounter += 1; 523 | return this._messageCounter + ''; 524 | }; 525 | 526 | OBSRemote.prototype._messageReceived = function(msg) { 527 | var message = JSON.parse(msg.data); 528 | if (!message) { 529 | return; 530 | } 531 | 532 | var self = this; 533 | // Check if this is an update event 534 | var updateType = message['update-type']; 535 | if (updateType) { 536 | switch (updateType) { 537 | case 'StreamStarting': 538 | this.onStreamStarted(message['preview-only']); 539 | break; 540 | case 'StreamStopping': 541 | this.onStreamStopped(message['preview-only']); 542 | break; 543 | case 'SwitchScenes': 544 | this.onSceneSwitched(message['scene-name']); 545 | break; 546 | case 'StreamStatus': 547 | this.onStatusUpdate(message.streaming, message['preview-only'], message['bytes-per-sec'], 548 | message.strain, message['total-stream-time'], message['num-total-frames'], 549 | message['num-dropped-frames'], message.fps); 550 | break; 551 | case 'ScenesChanged': 552 | // Get new scene list before we call onScenesChanged 553 | // Why this isn't default behaviour is beyond me 554 | this.getSceneList(function(currentScene, scenes) { 555 | self.onScenesChanged(scenes); 556 | }); 557 | break; 558 | case 'SourceOrderChanged': 559 | // Call getCurrentScene to get full source details 560 | this.getCurrentScene(function(scene) { 561 | self.onSourceOrderChanged(scene.sources); 562 | }); 563 | break; 564 | case 'RepopulateSources': 565 | var sources = []; 566 | message.sources.forEach(function(source) { 567 | sources.push(_convertToOBSSource(source)); 568 | }); 569 | this.onSourceAddedOrRemoved(sources); 570 | break; 571 | case 'SourceChanged': 572 | this.onSourceChanged(message['source-name'], _convertToOBSSource(message.source)); 573 | break; 574 | case 'VolumeChanged': 575 | // Which callback do we use 576 | var volumeCallback = (message.channel === 'desktop') ? 577 | this.onDesktopVolumeChanged : 578 | this.onMicrophoneVolumeChanged; 579 | 580 | volumeCallback(message.volume, message.muted, !message.finalValue); 581 | break; 582 | default: 583 | console.warn('[OBSRemote] Unknown OBS update type:', updateType, ', full message:', message); 584 | } 585 | } else { 586 | var msgId = message['message-id']; 587 | 588 | if (message.status === 'error') { 589 | console.error('[OBSRemote] Error:', message.error); 590 | } 591 | 592 | var callback = this._responseCallbacks[msgId]; 593 | callback(message); 594 | delete this._responseCallbacks[msgId]; 595 | } 596 | }; 597 | 598 | OBSRemote.prototype._webCryptoHash = function(pass, callback) { 599 | var utf8Pass = _encodeStringAsUTF8(pass); 600 | var utf8Salt = _encodeStringAsUTF8(this._auth.salt); 601 | 602 | var ab1 = _stringToArrayBuffer(utf8Pass + utf8Salt); 603 | 604 | var self = this; 605 | crypto.subtle.digest('SHA-256', ab1) 606 | .then(function(authHash) { 607 | var utf8AuthHash = _encodeStringAsUTF8(_arrayBufferToBase64(authHash)); 608 | var utf8Challenge = _encodeStringAsUTF8(self._auth.challenge); 609 | 610 | var ab2 = _stringToArrayBuffer(utf8AuthHash + utf8Challenge); 611 | 612 | crypto.subtle.digest('SHA-256', ab2) 613 | .then(function(authResp) { 614 | var authRespB64 = _arrayBufferToBase64(authResp); 615 | callback(authRespB64); 616 | }); 617 | }); 618 | }; 619 | 620 | OBSRemote.prototype._cryptoJSHash = function(pass, callback) { 621 | var utf8Pass = _encodeStringAsUTF8(pass); 622 | var utf8Salt = _encodeStringAsUTF8(this._auth.salt); 623 | 624 | var authHash = CryptoJS.SHA256(utf8Pass + utf8Salt).toString(CryptoJS.enc.Base64); 625 | 626 | var utf8AuthHash = _encodeStringAsUTF8(authHash); 627 | var utf8Challenge = _encodeStringAsUTF8(this._auth.challenge); 628 | 629 | var authResp = CryptoJS.SHA256(utf8AuthHash + utf8Challenge).toString(CryptoJS.enc.Base64); 630 | 631 | callback(authResp); 632 | }; 633 | 634 | OBSRemote.prototype._nodeCryptoHash = function(pass, callback) { 635 | var authHasher = crypto.createHash('sha256'); 636 | 637 | var utf8Pass = _encodeStringAsUTF8(pass); 638 | var utf8Salt = _encodeStringAsUTF8(this._auth.salt); 639 | 640 | authHasher.update(utf8Pass + utf8Salt); 641 | var authHash = authHasher.digest('base64'); 642 | 643 | var respHasher = crypto.createHash('sha256'); 644 | 645 | var utf8AuthHash = _encodeStringAsUTF8(authHash); 646 | var utf8Challenge = _encodeStringAsUTF8(this._auth.challenge); 647 | 648 | respHasher.update(utf8AuthHash + utf8Challenge); 649 | var respHash = respHasher.digest('base64'); 650 | 651 | callback(respHash); 652 | }; 653 | 654 | function _encodeStringAsUTF8(string) { 655 | return unescape(encodeURIComponent(string)); //jshint ignore:line 656 | } 657 | 658 | function _stringToArrayBuffer(string) { 659 | var ret = new Uint8Array(string.length); 660 | for (var i = 0; i < string.length; i++) { 661 | ret[i] = string.charCodeAt(i); 662 | } 663 | return ret.buffer; 664 | } 665 | 666 | function _arrayBufferToBase64(arrayBuffer) { 667 | var binary = ''; 668 | var bytes = new Uint8Array(arrayBuffer); 669 | 670 | var length = bytes.byteLength; 671 | for (var i = 0; i < length; i++) { 672 | binary += String.fromCharCode(bytes[i]); 673 | } 674 | 675 | return btoa(binary); 676 | } 677 | 678 | function _convertToOBSScene(scene) { 679 | var name = scene.name; 680 | var sources = []; 681 | 682 | scene.sources.forEach(function(source) { 683 | sources.push(_convertToOBSSource(source)); 684 | }); 685 | 686 | return new OBSScene(name, sources); 687 | } 688 | 689 | function _convertToOBSSource(source) { 690 | return new OBSSource(source.cx, source.cy, source.x, source.y, source.name, source.render); 691 | } 692 | 693 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 694 | module.exports = OBSRemote; 695 | } else { 696 | window.OBSRemote = OBSRemote; 697 | } 698 | })(); 699 | -------------------------------------------------------------------------------- /dist/obs-remote.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OBS Remote JS API v1.0.1 (https://github.com/nodecg/obs-remote-js) 3 | * Copyright 2014 Matthew McNamara 4 | * Licensed under MIT (https://github.com/nodecg/obs-remote-js/blob/master/LICENSE) 5 | */ 6 | !function(){"use strict";function a(a,b,c,d,e,f){this.width=a||0,this.height=b||0,this.x=c||0,this.y=d||0,this.name=e||"",this.rendered=f||!1}"undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports.OBSSource=a:window.OBSSource=a}(),function(){"use strict";function a(a,b){this.name=a||"",this.sources=b||[]}"undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports.OBSScene=a:window.OBSScene=a}(),function(){"use strict";function a(){a.API_VERSION=1.1,a.DEFAULT_PORT=4444,a.WS_PROTOCOL="obsapi",this._connected=!1,this._socket=void 0,this._messageCounter=0,this._responseCallbacks={},this._auth={salt:"",challenge:""}}function b(a){return unescape(encodeURIComponent(a))}function c(a){for(var b=new Uint8Array(a.length),c=0;ce;e++)b+=String.fromCharCode(c[e]);return btoa(b)}function e(a){var b=a.name,c=[];return a.sources.forEach(function(a){c.push(f(a))}),new h(b,c)}function f(a){return new g(a.cx,a.cy,a.x,a.y,a.name,a.render)}var g={},h={};"undefined"!=typeof module&&"undefined"!=typeof module.exports?(h=module.exports.OBSScene,g=module.exports.OBSSource):(h=window.OBSScene,g=window.OBSSource);var i={},j={};if("undefined"!=typeof module&&"undefined"!=typeof module.exports)i=require("crypto"),a.prototype._authHash=a.prototype._nodeCryptoHash,j=require("ws");else{if(i=window.crypto||window.msCrypto||{},a.prototype._authHash=a.prototype._webCryptoHash,"undefined"==typeof i.subtle)if("undefined"==typeof i.webkitSubtle){if("undefined"==typeof CryptoJS)throw new Error("OBS Remote requires CryptoJS when native crypto is not available!");a.prototype._authHash=a.prototype._cryptoJSHash}else i.subtle=i.webkitSubtle;j=window.WebSocket}a.prototype.connect=function(b,c){c="undefined"==typeof c?"":c,b="undefined"==typeof b||""===b?"localhost":b;var d=b.indexOf(":");(0>d||d===b.length-1)&&(b+=":"+a.DEFAULT_PORT),this._connected&&(this._socket.close(),this._connected=!1),this._socket="undefined"!=typeof module&&"undefined"!=typeof module.exports?new j("ws://"+b,{protocol:a.WS_PROTOCOL}):new j("ws://"+b,a.WS_PROTOCOL);var e=this;this._socket.onopen=function(){e._connected=!0,e.onConnectionOpened(),e.isAuthRequired(function(a){a&&e.authenticate(c)})},this._socket.onclose=function(){e.onConnectionClosed(),e._connected=!1},this._socket.onerror=function(a){e.onConnectionFailed(a),e._connected=!1},this._socket.onmessage=function(a){e._messageReceived(a)}},a.prototype.authenticate=function(a){var b=this;this._authHash(a,function(a){function c(a){var c="ok"===a.status,d=0;c?b.onAuthenticationSucceeded():(d=a.error.substr(43),b.onAuthenticationFailed(d))}b._sendMessage("Authenticate",{auth:a},c)})},a.prototype.toggleStream=function(a){a="undefined"==typeof a?!1:a,this._sendMessage("StartStopStreaming",{"preview-only":a})},a.prototype.getVersion=function(a){function b(b){a(b.version)}this._sendMessage("GetVersion",b)},a.prototype.isAuthRequired=function(a){function b(b){var d=b.authRequired;d&&(c._auth.salt=b.salt,c._auth.challenge=b.challenge),a(d)}var c=this;this._sendMessage("GetAuthRequired",b)},a.prototype.getSceneList=function(a){function b(b){var c=b["current-scene"],d=[];b.scenes.forEach(function(a){d.push(e(a))}),a(c,d)}this._sendMessage("GetSceneList",b)},a.prototype.getCurrentScene=function(a){function b(b){var c=e(b);a(c)}this._sendMessage("GetCurrentScene",b)},a.prototype.setCurrentScene=function(a){this._sendMessage("SetCurrentScene",{"scene-name":a})},a.prototype.setSourcesOrder=function(a){var b=a;a[1]instanceof"OBSSource"&&(b=[],a.forEach(function(a){b.push(a.name)})),this._sendMessage("SetSourcesOrder",{"scene-names":b})},a.prototype.setSourceRender=function(a,b){this._sendMessage("SetSourceRender",{source:a,render:b})},a.prototype.getStreamingStatus=function(a){function b(b){a(b.streaming,b["preview-only"])}this._sendMessage("GetStreamingStatus",b)},a.prototype.getVolumes=function(a){function b(b){a(b["mic-volume"],b["mic-muted"],b["desktop-volume"],b["desktop-muted"])}this._sendMessage("GetVolumes",b)},a.prototype.setMicrophoneVolume=function(a,b){this._sendMessage("SetVolume",{channel:"microphone",volume:a,"final":!b})},a.prototype.toggleMicrophoneMute=function(){this._sendMessage("ToggleMute",{channel:"microphone"})},a.prototype.setDesktopVolume=function(a,b){this._sendMessage("SetVolume",{channel:"desktop",volume:a,"final":!b})},a.prototype.toggleDesktopMute=function(){this._sendMessage("ToggleMute",{channel:"desktop"})},a.prototype.onConnectionOpened=function(){},a.prototype.onConnectionClosed=function(){},a.prototype.onConnectionFailed=function(){},a.prototype.onAuthenticationSucceeded=function(){},a.prototype.onAuthenticationFailed=function(){},a.prototype.onStreamStarted=function(){},a.prototype.onStreamStopped=function(){},a.prototype.onStatusUpdate=function(){},a.prototype.onSceneSwitched=function(){},a.prototype.onScenesChanged=function(){},a.prototype.onSourceOrderChanged=function(){},a.prototype.onSourceAddedOrRemoved=function(){},a.prototype.onSourceChanged=function(){},a.prototype.onMicrophoneVolumeChanged=function(){},a.prototype.onDesktopVolumeChanged=function(){},a.prototype._sendMessage=function(a,b,c){if(this._connected){var d=this._getNextMsgId();"function"==typeof b&&(c=b,b={}),b=b||{},c=c||function(){},this._responseCallbacks[d]=c,b["message-id"]=d,b["request-type"]=a;var e=JSON.stringify(b);this._socket.send(e)}},a.prototype._getNextMsgId=function(){return this._messageCounter+=1,this._messageCounter+""},a.prototype._messageReceived=function(a){var b=JSON.parse(a.data);if(b){var c=this,d=b["update-type"];if(d)switch(d){case"StreamStarting":this.onStreamStarted(b["preview-only"]);break;case"StreamStopping":this.onStreamStopped(b["preview-only"]);break;case"SwitchScenes":this.onSceneSwitched(b["scene-name"]);break;case"StreamStatus":this.onStatusUpdate(b.streaming,b["preview-only"],b["bytes-per-sec"],b.strain,b["total-stream-time"],b["num-total-frames"],b["num-dropped-frames"],b.fps);break;case"ScenesChanged":this.getSceneList(function(a,b){c.onScenesChanged(b)});break;case"SourceOrderChanged":this.getCurrentScene(function(a){c.onSourceOrderChanged(a.sources)});break;case"RepopulateSources":var e=[];b.sources.forEach(function(a){e.push(f(a))}),this.onSourceAddedOrRemoved(e);break;case"SourceChanged":this.onSourceChanged(b["source-name"],f(b.source));break;case"VolumeChanged":var g="desktop"===b.channel?this.onDesktopVolumeChanged:this.onMicrophoneVolumeChanged;g(b.volume,b.muted,!b.finalValue);break;default:console.warn("[OBSRemote] Unknown OBS update type:",d,", full message:",b)}else{var h=b["message-id"];"error"===b.status&&console.error("[OBSRemote] Error:",b.error);var i=this._responseCallbacks[h];i(b),delete this._responseCallbacks[h]}}},a.prototype._webCryptoHash=function(a,e){var f=b(a),g=b(this._auth.salt),h=c(f+g),j=this;i.subtle.digest("SHA-256",h).then(function(a){var f=b(d(a)),g=b(j._auth.challenge),h=c(f+g);i.subtle.digest("SHA-256",h).then(function(a){var b=d(a);e(b)})})},a.prototype._cryptoJSHash=function(a,c){var d=b(a),e=b(this._auth.salt),f=CryptoJS.SHA256(d+e).toString(CryptoJS.enc.Base64),g=b(f),h=b(this._auth.challenge),i=CryptoJS.SHA256(g+h).toString(CryptoJS.enc.Base64);c(i)},a.prototype._nodeCryptoHash=function(a,c){var d=i.createHash("sha256"),e=b(a),f=b(this._auth.salt);d.update(e+f);var g=d.digest("base64"),h=i.createHash("sha256"),j=b(g),k=b(this._auth.challenge);h.update(j+k);var l=h.digest("base64");c(l)},"undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports=a:window.OBSRemote=a}(); -------------------------------------------------------------------------------- /docs/OBSRemote.md: -------------------------------------------------------------------------------- 1 | # OBSRemote 2 | 3 | ## Class: OBSSource 4 | 5 | This represents a 'source' in OBS. It only holds data. 6 | 7 | ### new OBSSource(width, height, x, y, name, rendered) 8 | 9 | Constructs a new OBSSource object. 10 | 11 | * `width` Number 12 | * `height` Number 13 | * `x` Number 14 | * `y` Number 15 | * `name` String 16 | * `rendered` Boolean 17 | 18 | ## Class: OBSScene 19 | 20 | This represents a 'scene' in OBS. It only holds data. 21 | 22 | ### new OBSScene(name, sources) 23 | 24 | Constructs a new OBSSource object. 25 | 26 | * `name` String 27 | * `sources` Array of OBSSource 28 | 29 | ## Class OBSRemote 30 | 31 | This class is responsible for communication with OBS. 32 | 33 | ### new OBSRemote() 34 | 35 | Constructs a new OBSRemote object. 36 | 37 | ### remote.connect([address [, password]]) 38 | 39 | Connects to OBS. If address is not given, `localhost:4444` is used. 40 | If the password is given, OBSRemote will automatically attempt authentication. 41 | 42 | ### remote.authenticate(password) 43 | 44 | Attempts to authenticate with OBS. 45 | Will cause either `onAuthenticationFailed` or `onAuthenticationSucceeded` to be called 46 | 47 | ### remote.isAuthRequired(function (Boolean isRequired) {} ) 48 | 49 | Checks if authentication is required 50 | 51 | ### remote.toggleStream(previewMode) 52 | 53 | Starts or stops OBS from streaming, recording, or previewing. 54 | Result of this will be either the `onStreamStarted` or `onStreamStopped` callback. 55 | 56 | ### remote.getVersion(function (Number version) {} ) 57 | 58 | Requests the OBS Remote plugin version 59 | 60 | ### remote.getSceneList(function (String currentScene, Array scenes) {} ) 61 | 62 | Gets name of current scene and full list of all other scenes 63 | 64 | ### remote.getCurrentScene(function (OBSScene scene) {} ) 65 | 66 | Gets the current scene and full list of sources 67 | 68 | ### remote.setCurrentScene(sceneName) 69 | 70 | Tells OBS to switch to the given scene name 71 | If successful the `onSceneSwitched` will be called 72 | 73 | ### remote.setSourcesOrder(sources) 74 | 75 | Reorders sources in the current scene 76 | `sources` can be an Array of either Strings or OBSSources 77 | 78 | ### remote.setSourceRender(sourceName, shouldRender) 79 | 80 | Sets a source's render state in the current scene 81 | 82 | ### remote.getStreamingStatus(function (Boolean streaming, Boolean previewOnly) {} ) 83 | 84 | Gets current streaming status, and if we're previewing or not 85 | 86 | ### remote.getVolumes(function (Number microphoneVolume, Boolean microphoneMuted, Number desktopVolume, Boolean desktop) {} ) 87 | 88 | Gets current volume levels and mute statuses 89 | 90 | ### remote.setMicrophoneVolume(volume, adjusting) 91 | 92 | Sets microphone volume, and whether we're still adjusting it 93 | 94 | ### remote.toggleMicrophoneMute() 95 | 96 | Toggles microphone mute state 97 | 98 | ### remote.setDesktopVolume(volume, adjusting) 99 | 100 | Sets desktop volume, and whether we're still adjusting it 101 | 102 | ### remote.toggleDesktopMute() 103 | 104 | Toggles desktop mute state 105 | 106 | ### remote.onConnectionOpened() 107 | 108 | Called when the connection to OBS is made 109 | You may still need to authenticate! 110 | 111 | ### remote.onConnectionClosed() 112 | 113 | Called when the connection to OBS is closed 114 | 115 | ### remote.onConnectionFailed() 116 | 117 | Called when the connection to OBS fails 118 | 119 | ### remote.onAuthenticationSucceeded() 120 | 121 | Called when authentication is successful 122 | 123 | ### remote.onAuthenticationFailed(Number remainingAttempts) 124 | 125 | Called when authentication fails 126 | 127 | ### remote.onStreamStarted(previewing) 128 | 129 | Called when OBS starts streaming, recording or previewing 130 | 131 | ### remote.onStreamStopped(previewing) 132 | 133 | Called when OBS stops streaming, recording or previewing 134 | 135 | ### remote.onStatusUpdate(streaming, previewing, bytesPerSecond, strain, streamDurationInMS, totalFrames, droppedFrames, framesPerSecond) 136 | 137 | Called frequently by OBS while live or previewing 138 | 139 | ### remote.onSceneSwitched(sceneName) 140 | 141 | Called when OBS switches scene 142 | 143 | ### remote.onScenesChanged(scenes) 144 | 145 | Called when the scene list changes (new order, addition, removal or renaming) 146 | 147 | ### remote.onSourceOrderChanged(sources) 148 | 149 | Called when source oder changes in the current scene 150 | 151 | ### remote.onSourceAddedOrRemoved(sources) 152 | 153 | Called when a source is added or removed from the current scene 154 | 155 | ### remote.onSourceChanged(originalName, source) 156 | 157 | Called when a source in the current scene changes 158 | 159 | ### remote.onMicrophoneVolumeChanged(volume, muted, adjusting) 160 | 161 | Called when the microphone volume changes, or is muted 162 | 163 | ### remote.onDesktopVolumeChanged(volume, muted, adjusting) 164 | 165 | Called when the desktop volume changes, or is muted 166 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/obs-remote.js'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obs-remote", 3 | "version": "1.0.1", 4 | "description": "OBS Remote API for Node.JS and browsers", 5 | "main": "index.js", 6 | "dependencies": { 7 | "ws": "^1.0.1" 8 | }, 9 | "devDependencies": { 10 | "grunt": "^0.4.5", 11 | "grunt-contrib-clean": "^0.6.0", 12 | "grunt-contrib-concat": "^0.5.0", 13 | "grunt-contrib-jshint": "^0.10.0", 14 | "grunt-contrib-uglify": "^0.6.0", 15 | "load-grunt-tasks": "^1.0.0", 16 | "time-grunt": "^1.0.0" 17 | }, 18 | "files": [ 19 | "LICENSE", 20 | "dist/obs-remote.js", 21 | "index.js", 22 | "README.md" 23 | ], 24 | "scripts": { 25 | "test": "echo \"Error: no test specified\" && exit 1" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/nodecg/obs-remote-js.git" 30 | }, 31 | "author": "Matthew McNamara ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/nodecg/obs-remote-js/issues" 35 | }, 36 | "homepage": "https://github.com/nodecg/obs-remote-js" 37 | } 38 | -------------------------------------------------------------------------------- /src/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 50, 3 | "bitwise" : true, 4 | "camelcase" : true, 5 | "curly" : false, 6 | "eqeqeq" : true, 7 | "forin" : true, 8 | "freeze" : true, 9 | "immed" : true, 10 | "indent" : 4, 11 | "latedef" : false, 12 | "newcap" : true, 13 | "noarg" : true, 14 | "noempty" : true, 15 | "nonbsp" : true, 16 | "nonew" : false, 17 | "plusplus" : false, 18 | "quotmark" : "single", 19 | "undef" : true, 20 | "unused" : true, 21 | "strict" : true, 22 | "maxdepth" : 6, 23 | "maxstatements" : false, 24 | "maxcomplexity" : false, 25 | "maxlen" : 120, 26 | "asi" : false, 27 | "boss" : false, 28 | "debug" : false, 29 | "eqnull" : false, 30 | "esnext" : false, 31 | "moz" : false, 32 | "evil" : false, 33 | "expr" : false, 34 | "funcscope" : false, 35 | "globalstrict" : true, 36 | "iterator" : false, 37 | "lastsemic" : false, 38 | "laxbreak" : true, 39 | "laxcomma" : true, 40 | "loopfunc" : false, 41 | "multistr" : false, 42 | "noyield" : false, 43 | "notypeof" : false, 44 | "proto" : false, 45 | "scripturl" : false, 46 | "shadow" : false, 47 | "sub" : false, 48 | "supernew" : false, 49 | "validthis" : false, 50 | "browser" : true, 51 | "browserify" : false, 52 | "couch" : false, 53 | "devel" : true, 54 | "dojo" : false, 55 | "jasmine" : false, 56 | "jquery" : true, 57 | "mocha" : false, 58 | "mootools" : false, 59 | "node" : true, 60 | "nonstandard" : false, 61 | "prototypejs" : false, 62 | "qunit" : false, 63 | "rhino" : false, 64 | "shelljs" : false, 65 | "worker" : false, 66 | "wsh" : false, 67 | "yui" : false, 68 | 69 | 70 | "globals": { 71 | "CryptoJS": true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/obs-remote.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var OBSSource = {}; 5 | var OBSScene = {}; 6 | 7 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 8 | OBSScene = module.exports.OBSScene; 9 | OBSSource = module.exports.OBSSource; 10 | } else { 11 | OBSScene = window.OBSScene; 12 | OBSSource = window.OBSSource; 13 | } 14 | 15 | function OBSRemote() { 16 | OBSRemote.API_VERSION = 1.1; 17 | OBSRemote.DEFAULT_PORT = 4444; 18 | OBSRemote.WS_PROTOCOL = 'obsapi'; 19 | 20 | this._connected = false; 21 | this._socket = undefined; 22 | this._messageCounter = 0; 23 | this._responseCallbacks = {}; 24 | 25 | this._auth = {salt: '', challenge: ''}; 26 | } 27 | 28 | // IE11 crypto object is prefixed 29 | var crypto = {}; 30 | var WebSocket = {}; 31 | 32 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 33 | crypto = require('crypto'); 34 | OBSRemote.prototype._authHash = OBSRemote.prototype._nodeCryptoHash; 35 | 36 | WebSocket = require('ws'); 37 | } else { 38 | crypto = window.crypto || window.msCrypto || {}; 39 | OBSRemote.prototype._authHash = OBSRemote.prototype._webCryptoHash; 40 | 41 | if (typeof crypto.subtle === 'undefined') { 42 | // Safari crypto.subtle is prefixed, all other browsers use subtle or don't implement 43 | if (typeof crypto.webkitSubtle === 'undefined') { 44 | // Native crypto not available, fall back to CryptoJS 45 | if (typeof CryptoJS === 'undefined') { 46 | throw new Error('OBS Remote requires CryptoJS when native crypto is not available!'); 47 | } 48 | 49 | OBSRemote.prototype._authHash = OBSRemote.prototype._cryptoJSHash; 50 | } else { 51 | crypto.subtle = crypto.webkitSubtle; 52 | } 53 | } 54 | 55 | WebSocket = window.WebSocket; 56 | } 57 | 58 | /** 59 | * Try to connect to OBS, with optional password 60 | * @param address "ipAddress" or "ipAddress:port" 61 | * defaults to "localhost" 62 | * @param password Optional authentication password 63 | */ 64 | OBSRemote.prototype.connect = function(address, password) { 65 | // Password is optional, set to empty string if undefined 66 | password = (typeof password === 'undefined') ? 67 | '' : 68 | password; 69 | 70 | // Check for address 71 | address = (typeof address === 'undefined' || address === '') ? 72 | 'localhost' : 73 | address; 74 | 75 | // Check for port number, if missing use 4444 76 | var colonIndex = address.indexOf(':'); 77 | if (colonIndex < 0 || colonIndex === address.length - 1) { 78 | address += ':' + OBSRemote.DEFAULT_PORT; 79 | } 80 | 81 | // Check if we already have a connection 82 | if (this._connected) { 83 | this._socket.close(); 84 | this._connected = false; 85 | } 86 | 87 | // Connect and setup WebSocket callbacks 88 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 89 | this._socket = new WebSocket('ws://' + address, {protocol: OBSRemote.WS_PROTOCOL}); 90 | } else { 91 | this._socket = new WebSocket('ws://' + address, OBSRemote.WS_PROTOCOL); 92 | } 93 | 94 | var self = this; 95 | 96 | this._socket.onopen = function() { 97 | self._connected = true; 98 | self.onConnectionOpened(); 99 | 100 | self.isAuthRequired(function(required) { 101 | if (!required) return; 102 | 103 | self.authenticate(password); 104 | }); 105 | }; 106 | 107 | this._socket.onclose = function() { 108 | if (self._connected) { 109 | self.onConnectionClosed(); 110 | } 111 | self._connected = false; 112 | }; 113 | 114 | this._socket.onerror = function(event) { 115 | self.onConnectionFailed(event); 116 | self._connected = false; 117 | }; 118 | 119 | this._socket.onmessage = function(message) { 120 | self._messageReceived(message); 121 | }; 122 | }; 123 | 124 | /** 125 | * Attempts to authenticate with OBS 126 | * Will cause either onAuthenticationFailed or onAuthenticationSucceeded to be called 127 | * @param password the password to try authenticating with 128 | */ 129 | OBSRemote.prototype.authenticate = function(password) { 130 | var self = this; 131 | this._authHash(password, function(authResp) { 132 | 133 | function cb(message) { 134 | var successful = (message.status === 'ok'); 135 | var remainingAttempts = 0; 136 | 137 | if (!successful) { 138 | // eugh 139 | remainingAttempts = message.error.substr(43); 140 | 141 | self.onAuthenticationFailed(remainingAttempts); 142 | } else { 143 | self.onAuthenticationSucceeded(); 144 | } 145 | } 146 | 147 | self._sendMessage('Authenticate', { 148 | 'auth': authResp 149 | }, cb); 150 | }); 151 | }; 152 | 153 | /** 154 | * Starts or stops OBS from streaming, recording, or previewing. 155 | * Result of this will be either the onStreamStarted or onStreamStopped callback. 156 | * @param previewOnly Only toggle the preview 157 | */ 158 | OBSRemote.prototype.toggleStream = function(previewOnly) { 159 | // previewOnly is optional, default to false 160 | previewOnly = (typeof previewOnly === 'undefined') ? 161 | false : 162 | previewOnly; 163 | 164 | this._sendMessage('StartStopStreaming', { 165 | 'preview-only': previewOnly 166 | }); 167 | }; 168 | 169 | /** 170 | * Requests OBS Remote version 171 | * @param callback function(Number version) 172 | */ 173 | OBSRemote.prototype.getVersion = function(callback) { 174 | function cb(message) { 175 | callback(message.version); 176 | } 177 | 178 | this._sendMessage('GetVersion', cb); 179 | }; 180 | 181 | /** 182 | * Checks if authentication is required 183 | * @param callback function(Boolean isRequired) 184 | */ 185 | OBSRemote.prototype.isAuthRequired = function(callback) { 186 | var self = this; 187 | 188 | function cb(message) { 189 | var authRequired = message.authRequired; 190 | 191 | if (authRequired) { 192 | self._auth.salt = message.salt; 193 | self._auth.challenge = message.challenge; 194 | } 195 | 196 | callback(authRequired); 197 | } 198 | 199 | this._sendMessage('GetAuthRequired', cb); 200 | }; 201 | 202 | /** 203 | * Gets name of current scene and full list of all other scenes 204 | * @param callback function(String currentScene, Array scenes) 205 | */ 206 | OBSRemote.prototype.getSceneList = function(callback) { 207 | function cb(message) { 208 | var currentScene = message['current-scene']; 209 | var scenes = []; 210 | 211 | message.scenes.forEach(function(scene) { 212 | scenes.push(_convertToOBSScene(scene)); 213 | }); 214 | 215 | callback(currentScene, scenes); 216 | } 217 | 218 | this._sendMessage('GetSceneList', cb); 219 | }; 220 | 221 | /** 222 | * Gets the current scene and full list of sources 223 | * @param callback function(OBSScene scene) 224 | */ 225 | OBSRemote.prototype.getCurrentScene = function(callback) { 226 | function cb(message) { 227 | var obsScene = _convertToOBSScene(message); 228 | 229 | callback(obsScene); 230 | } 231 | 232 | this._sendMessage('GetCurrentScene', cb); 233 | }; 234 | 235 | /** 236 | * Tells OBS to switch to the given scene name 237 | * If successful onSceneSwitched will be called 238 | * @param sceneName name of scene to switch to 239 | */ 240 | OBSRemote.prototype.setCurrentScene = function(sceneName) { 241 | this._sendMessage('SetCurrentScene', { 242 | 'scene-name': sceneName 243 | }); 244 | }; 245 | 246 | /** 247 | * Reorders sources in the current scene 248 | * @param sources Array of Strings, or OBSSources 249 | */ 250 | OBSRemote.prototype.setSourcesOrder = function(sources) { 251 | var sourceNames = sources; 252 | 253 | // Support Array[OBSSource] for convenience 254 | if (sources[1] instanceof 'OBSSource') { 255 | sourceNames = []; 256 | sources.forEach(function(source) { 257 | sourceNames.push(source.name); 258 | }); 259 | } 260 | 261 | this._sendMessage('SetSourcesOrder', { 262 | 'scene-names': sourceNames 263 | }); 264 | }; 265 | 266 | /** 267 | * Sets a source's render state in the current scene 268 | * @param sourceName 269 | * @param shouldRender 270 | */ 271 | OBSRemote.prototype.setSourceRender = function(sourceName, shouldRender) { 272 | this._sendMessage('SetSourceRender', { 273 | source: sourceName, 274 | render: shouldRender 275 | }); 276 | }; 277 | 278 | /** 279 | * Gets current streaming status, and if we're previewing or not 280 | * @param callback function(Boolean streaming, Boolean previewOnly) 281 | */ 282 | OBSRemote.prototype.getStreamingStatus = function(callback) { 283 | function cb(message) { 284 | callback(message.streaming, message['preview-only']); 285 | } 286 | 287 | this._sendMessage('GetStreamingStatus', cb); 288 | }; 289 | 290 | /** 291 | * Gets current volume levels and mute statuses 292 | * @param callback function(Number microphoneVolume, Boolean microphoneMuted, Number desktopVolume, Boolean desktop) 293 | */ 294 | OBSRemote.prototype.getVolumes = function(callback) { 295 | function cb(message) { 296 | callback(message['mic-volume'], message['mic-muted'], message['desktop-volume'], message['desktop-muted']); 297 | } 298 | 299 | this._sendMessage('GetVolumes', cb); 300 | }; 301 | 302 | /** 303 | * Sets microphone volume, and whether we're still adjusting it 304 | * @param volume 305 | * @param adjusting Optional, defaults to false 306 | */ 307 | OBSRemote.prototype.setMicrophoneVolume = function(volume, adjusting) { 308 | this._sendMessage('SetVolume', { 309 | channel: 'microphone', 310 | volume: volume, 311 | final: !adjusting 312 | }); 313 | }; 314 | 315 | /** 316 | * Toggles microphone mute state 317 | */ 318 | OBSRemote.prototype.toggleMicrophoneMute = function() { 319 | this._sendMessage('ToggleMute', { 320 | channel: 'microphone' 321 | }); 322 | }; 323 | 324 | /** 325 | * Sets desktop volume, and whether we're still adjusting it 326 | * @param volume 327 | * @param adjusting Optional, defaults to false 328 | */ 329 | OBSRemote.prototype.setDesktopVolume = function(volume, adjusting) { 330 | this._sendMessage('SetVolume', { 331 | channel: 'desktop', 332 | volume: volume, 333 | final: !adjusting 334 | }); 335 | }; 336 | 337 | /** 338 | * Toggles desktop mute state 339 | */ 340 | OBSRemote.prototype.toggleDesktopMute = function() { 341 | this._sendMessage('ToggleMute', { 342 | channel: 'desktop' 343 | }); 344 | }; 345 | 346 | /** 347 | * OBSRemote API callbacks 348 | */ 349 | 350 | /* jshint ignore:start */ 351 | 352 | /** 353 | * Called when the connection to OBS is made 354 | * You may still need to authenticate! 355 | */ 356 | OBSRemote.prototype.onConnectionOpened = function() {}; 357 | 358 | /** 359 | * Called when the connection to OBS is closed 360 | */ 361 | OBSRemote.prototype.onConnectionClosed = function() {}; 362 | 363 | /** 364 | * Called when the connection to OBS fails 365 | */ 366 | OBSRemote.prototype.onConnectionFailed = function() {}; 367 | 368 | /** 369 | * Called when authentication is successful 370 | */ 371 | OBSRemote.prototype.onAuthenticationSucceeded = function() {}; 372 | 373 | /** 374 | * Called when authentication fails 375 | * @param remainingAttempts how many more attempts can be made 376 | */ 377 | OBSRemote.prototype.onAuthenticationFailed = function(remainingAttempts) {}; 378 | 379 | /** 380 | * OBS standard callbacks 381 | */ 382 | 383 | /** 384 | * Called when OBS starts streaming, recording or previewing 385 | * @param previewing are we previewing or 'LIVE' 386 | */ 387 | OBSRemote.prototype.onStreamStarted = function(previewing) {}; 388 | 389 | /** 390 | * Called when OBS stops streaming, recording or previewing 391 | * @param previewing were we previewing, or 'LIVE' 392 | */ 393 | OBSRemote.prototype.onStreamStopped = function(previewing) {}; 394 | 395 | /** 396 | * Called frequently by OBS while live or previewing 397 | * @param streaming are we streaming (or recording) 398 | * @param previewing are we previewing or live 399 | * @param bytesPerSecond 400 | * @param strain 401 | * @param streamDurationInMS 402 | * @param totalFrames 403 | * @param droppedFrames 404 | * @param framesPerSecond 405 | */ 406 | OBSRemote.prototype.onStatusUpdate = function(streaming, previewing, bytesPerSecond, strain, streamDurationInMS, 407 | totalFrames, droppedFrames, framesPerSecond) {}; 408 | 409 | /** 410 | * Called when OBS switches scene 411 | * @param sceneName scene OBS has switched to 412 | */ 413 | OBSRemote.prototype.onSceneSwitched = function(sceneName) {}; 414 | 415 | /** 416 | * Called when the scene list changes (new order, addition, removal or renaming) 417 | * @param scenes new list of scenes 418 | */ 419 | OBSRemote.prototype.onScenesChanged = function(scenes) {}; 420 | 421 | /** 422 | * Called when source oder changes in the current scene 423 | * @param sources 424 | */ 425 | OBSRemote.prototype.onSourceOrderChanged = function(sources) {}; 426 | 427 | /** 428 | * Called when a source is added or removed from the current scene 429 | * @param sources 430 | */ 431 | OBSRemote.prototype.onSourceAddedOrRemoved = function(sources) {}; 432 | 433 | /** 434 | * Called when a source in the current scene changes 435 | * @param originalName if the name changed, this is what it was originally 436 | * @param source 437 | */ 438 | OBSRemote.prototype.onSourceChanged = function(originalName, source) {}; 439 | 440 | /** 441 | * Called when the microphone volume changes, or is muted 442 | * @param volume 443 | * @param muted 444 | * @param adjusting 445 | */ 446 | OBSRemote.prototype.onMicrophoneVolumeChanged = function(volume, muted, adjusting) {}; 447 | 448 | /** 449 | * Called when the desktop volume changes, or is muted 450 | * @param volume 451 | * @param muted 452 | * @param adjusting 453 | */ 454 | OBSRemote.prototype.onDesktopVolumeChanged = function(volume, muted, adjusting) {}; 455 | 456 | /* jshint ignore:end */ 457 | 458 | OBSRemote.prototype._sendMessage = function(requestType, args, callback) { 459 | if (this._connected) { 460 | var msgId = this._getNextMsgId(); 461 | 462 | // Callback but no args 463 | if (typeof args === 'function') { 464 | callback = args; 465 | args = {}; 466 | } 467 | 468 | // Ensure message isn't undefined, use empty object 469 | args = args || {}; 470 | 471 | // Ensure callback isn't undefined, use empty function 472 | callback = callback || function() {}; 473 | 474 | // Store the callback with the message ID 475 | this._responseCallbacks[msgId] = callback; 476 | 477 | args['message-id'] = msgId; 478 | args['request-type'] = requestType; 479 | 480 | var serialisedMsg = JSON.stringify(args); 481 | this._socket.send(serialisedMsg); 482 | } 483 | }; 484 | 485 | OBSRemote.prototype._getNextMsgId = function() { 486 | this._messageCounter += 1; 487 | return this._messageCounter + ''; 488 | }; 489 | 490 | OBSRemote.prototype._messageReceived = function(msg) { 491 | var message = JSON.parse(msg.data); 492 | if (!message) { 493 | return; 494 | } 495 | 496 | var self = this; 497 | // Check if this is an update event 498 | var updateType = message['update-type']; 499 | if (updateType) { 500 | switch (updateType) { 501 | case 'StreamStarting': 502 | this.onStreamStarted(message['preview-only']); 503 | break; 504 | case 'StreamStopping': 505 | this.onStreamStopped(message['preview-only']); 506 | break; 507 | case 'SwitchScenes': 508 | this.onSceneSwitched(message['scene-name']); 509 | break; 510 | case 'StreamStatus': 511 | this.onStatusUpdate(message.streaming, message['preview-only'], message['bytes-per-sec'], 512 | message.strain, message['total-stream-time'], message['num-total-frames'], 513 | message['num-dropped-frames'], message.fps); 514 | break; 515 | case 'ScenesChanged': 516 | // Get new scene list before we call onScenesChanged 517 | // Why this isn't default behaviour is beyond me 518 | this.getSceneList(function(currentScene, scenes) { 519 | self.onScenesChanged(scenes); 520 | }); 521 | break; 522 | case 'SourceOrderChanged': 523 | // Call getCurrentScene to get full source details 524 | this.getCurrentScene(function(scene) { 525 | self.onSourceOrderChanged(scene.sources); 526 | }); 527 | break; 528 | case 'RepopulateSources': 529 | var sources = []; 530 | message.sources.forEach(function(source) { 531 | sources.push(_convertToOBSSource(source)); 532 | }); 533 | this.onSourceAddedOrRemoved(sources); 534 | break; 535 | case 'SourceChanged': 536 | this.onSourceChanged(message['source-name'], _convertToOBSSource(message.source)); 537 | break; 538 | case 'VolumeChanged': 539 | // Which callback do we use 540 | var volumeCallback = (message.channel === 'desktop') ? 541 | this.onDesktopVolumeChanged : 542 | this.onMicrophoneVolumeChanged; 543 | 544 | volumeCallback(message.volume, message.muted, !message.finalValue); 545 | break; 546 | default: 547 | console.warn('[OBSRemote] Unknown OBS update type:', updateType, ', full message:', message); 548 | } 549 | } else { 550 | var msgId = message['message-id']; 551 | 552 | if (message.status === 'error') { 553 | console.error('[OBSRemote] Error:', message.error); 554 | } 555 | 556 | var callback = this._responseCallbacks[msgId]; 557 | callback(message); 558 | delete this._responseCallbacks[msgId]; 559 | } 560 | }; 561 | 562 | OBSRemote.prototype._webCryptoHash = function(pass, callback) { 563 | var utf8Pass = _encodeStringAsUTF8(pass); 564 | var utf8Salt = _encodeStringAsUTF8(this._auth.salt); 565 | 566 | var ab1 = _stringToArrayBuffer(utf8Pass + utf8Salt); 567 | 568 | var self = this; 569 | crypto.subtle.digest('SHA-256', ab1) 570 | .then(function(authHash) { 571 | var utf8AuthHash = _encodeStringAsUTF8(_arrayBufferToBase64(authHash)); 572 | var utf8Challenge = _encodeStringAsUTF8(self._auth.challenge); 573 | 574 | var ab2 = _stringToArrayBuffer(utf8AuthHash + utf8Challenge); 575 | 576 | crypto.subtle.digest('SHA-256', ab2) 577 | .then(function(authResp) { 578 | var authRespB64 = _arrayBufferToBase64(authResp); 579 | callback(authRespB64); 580 | }); 581 | }); 582 | }; 583 | 584 | OBSRemote.prototype._cryptoJSHash = function(pass, callback) { 585 | var utf8Pass = _encodeStringAsUTF8(pass); 586 | var utf8Salt = _encodeStringAsUTF8(this._auth.salt); 587 | 588 | var authHash = CryptoJS.SHA256(utf8Pass + utf8Salt).toString(CryptoJS.enc.Base64); 589 | 590 | var utf8AuthHash = _encodeStringAsUTF8(authHash); 591 | var utf8Challenge = _encodeStringAsUTF8(this._auth.challenge); 592 | 593 | var authResp = CryptoJS.SHA256(utf8AuthHash + utf8Challenge).toString(CryptoJS.enc.Base64); 594 | 595 | callback(authResp); 596 | }; 597 | 598 | OBSRemote.prototype._nodeCryptoHash = function(pass, callback) { 599 | var authHasher = crypto.createHash('sha256'); 600 | 601 | var utf8Pass = _encodeStringAsUTF8(pass); 602 | var utf8Salt = _encodeStringAsUTF8(this._auth.salt); 603 | 604 | authHasher.update(utf8Pass + utf8Salt); 605 | var authHash = authHasher.digest('base64'); 606 | 607 | var respHasher = crypto.createHash('sha256'); 608 | 609 | var utf8AuthHash = _encodeStringAsUTF8(authHash); 610 | var utf8Challenge = _encodeStringAsUTF8(this._auth.challenge); 611 | 612 | respHasher.update(utf8AuthHash + utf8Challenge); 613 | var respHash = respHasher.digest('base64'); 614 | 615 | callback(respHash); 616 | }; 617 | 618 | function _encodeStringAsUTF8(string) { 619 | return unescape(encodeURIComponent(string)); //jshint ignore:line 620 | } 621 | 622 | function _stringToArrayBuffer(string) { 623 | var ret = new Uint8Array(string.length); 624 | for (var i = 0; i < string.length; i++) { 625 | ret[i] = string.charCodeAt(i); 626 | } 627 | return ret.buffer; 628 | } 629 | 630 | function _arrayBufferToBase64(arrayBuffer) { 631 | var binary = ''; 632 | var bytes = new Uint8Array(arrayBuffer); 633 | 634 | var length = bytes.byteLength; 635 | for (var i = 0; i < length; i++) { 636 | binary += String.fromCharCode(bytes[i]); 637 | } 638 | 639 | return btoa(binary); 640 | } 641 | 642 | function _convertToOBSScene(scene) { 643 | var name = scene.name; 644 | var sources = []; 645 | 646 | scene.sources.forEach(function(source) { 647 | sources.push(_convertToOBSSource(source)); 648 | }); 649 | 650 | return new OBSScene(name, sources); 651 | } 652 | 653 | function _convertToOBSSource(source) { 654 | return new OBSSource(source.cx, source.cy, source.x, source.y, source.name, source.render); 655 | } 656 | 657 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 658 | module.exports = OBSRemote; 659 | } else { 660 | window.OBSRemote = OBSRemote; 661 | } 662 | })(); 663 | -------------------------------------------------------------------------------- /src/obs-scene.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | function OBSScene(name, sources) { 5 | this.name = name || ''; 6 | this.sources = sources || []; 7 | } 8 | 9 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 10 | module.exports.OBSScene = OBSScene; 11 | } else { 12 | window.OBSScene = OBSScene; 13 | } 14 | })(); 15 | -------------------------------------------------------------------------------- /src/obs-source.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | function OBSSource(width, height, x, y, name, rendered) { 5 | this.width = width || 0; 6 | this.height = height || 0; 7 | this.x = x || 0; 8 | this.y = y || 0; 9 | this.name = name || ''; 10 | this.rendered = rendered || false; 11 | } 12 | 13 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 14 | module.exports.OBSSource = OBSSource; 15 | } else { 16 | window.OBSSource = OBSSource; 17 | } 18 | })(); 19 | --------------------------------------------------------------------------------