Source: index.js

const SockJS = require('sockjs-client');
const Util = require('util');
const EventEmitter = require('events');
const { connect } = require('http2');

const ID_CONNECT = 1;
const ID_STREAMSTATUS = 2;
const ID_STREAMSTATUS_CHANGED = 3;
const ID_SCENES = 4;
const ID_TOGGLE = 5;

//Map of all the scenes in the connected Slobs. Filled with getScenes();
const scenesMap = new Map();
const slobsStatus = new Map();
var connected = false;

/**
 * @private
 * 
 * This function handles sending our authorization token to Slobs
 * 
 * @param {object} slobs object containing slobs information.The slobs object created with slobs(message).
 *
 *
 */
function auth(slobs)
{
	const connectMessage = JSON.stringify({
		  jsonrpc: '2.0',
		  id: ID_CONNECT,
		  method: 'auth',
		  params: {
			resource: 'TcpServerService',
			args: [slobs.token],
		  },
		});
		slobs.socket.send(connectMessage);
}

/**
 * Handler for Messages based off ID's
 * @param {object} slobs object containing slobs information.The slobs object created with slobs(message).
 * @fires streamStarted
 * @fires streamEnded
 * @fires recordStarted
 * @fires recordEnded
 */
function messageHandler(slobs, emitter)
{
	const data = JSON.parse(slobs.message.data);
	if (data.result)
	{
		
		if (data.id === ID_STREAMSTATUS)
		{
			slobsStatus.set('streamStatus', data.result.streamingStatus);
			slobsStatus.set('recordingStatus', data.result.recordingStatus);
			if (data.result.streamingStatus == 'live')
			{
				slobsStatus.set('StreamStartTime', data.result.streamingStatusTime);
			} else slobsStatus.set('startTime', null);
			if (data.result.recordingStatus == 'recording')
			{
				slobsStatus.set('recordingStartTime', data.result.recordingStatusTime);
			}else slobsStatus.set('recordingStartTime', null);
		}
        if (data.result._type !== undefined && data.result._type === 'EVENT') {
          if (data.result.emitter === 'STREAM' && data.result.resourceId === 'StreamingService.streamingStatusChange') {
			slobsStatus.set('streamStatus', data.result.data);
			if (data.result.data == 'live')
			{
				/**
				 * Triggers when Slobs starts Streaming.
				 * @event streamStarted 
				 */
				emitter.emit('streamStarted');
				slobsStatus.set('startTime', new Date());
			} else if (data.result.data == 'offline')
			{
				/**
				 * Triggers when Slobs Stops streaming.
				 * @event streamEnded 
				 */
				emitter.emit('streamEnded');
				 slobsStatus.set('startTime', null);
			}
		  }
		  
		  else if (data.result.emitter === 'STREAM' && data.result.resourceId === 'StreamingService.recordingStatusChange') {
			slobsStatus.set('recordingStatus', data.result.data);
			if (data.result.data == 'recording')
			{
				/**
				 * Triggers when Slobs starts recording.
				 * @event recordStarted 
				 */
				emitter.emit('recordStarted');
				slobsStatus.set('recordingStartTime', new Date());
			} else if (data.result.data == 'offline') {
				/**
				 * Triggers when Slobs stops recording.
				 * @event recordEnded 
				 */
				emitter.emit('recordEnded');
				slobsStatus.set('recordingStartTime', null);
			}
		  }
        }
	}
	if (data.id == ID_CONNECT && data.result == false)
	{
		auth();
	}
	else if (data.id == ID_CONNECT)
	{
		connected = true;
		subscribeStreaming(slobs.socket);
		streamingStatus(slobs.socket);
		getScenes(slobs.socket);
	}
	if (data.id === ID_SCENES) {
		for (let i = 0; i < data.result.length; i++) {
		  const sources = new Map();
		  for (let j = 0; j < data.result[i].nodes.length; j++) {
			sources.set(data.result[i].nodes[j].name, data.result[i].nodes[j]);
		  }
		  const sceneName = data.result[i].name;
		  scenesMap.set(sceneName, sources);
		}
	}
}
/**@private
 * /OUTPUTS ERROR OF WHY SLOBS DISCONNECTED
 * @param {object} slobs object containing slobs information.
 */
function disconnectHandler(slobs, emmiter)
{
	var data = JSON.parse(slobs.message.data);
	emmiter.emit('close', data);
	connected = false;
}

/**@private
 * Used to get events of streaming status changes.
 * @param {object} slobs object containing slobs information.
 */
function subscribeStreaming(slobs) {
  let message = JSON.stringify({
    id: ID_STREAMSTATUS_CHANGED,
    jsonrpc: '2.0',
    method: 'streamingStatusChange',
    params: { resource: 'StreamingService' },
  });
  slobs.send(message);
  
  message = JSON.stringify({
    id: ID_STREAMSTATUS_CHANGED,
    jsonrpc: '2.0',
    method: 'recordingStatusChange',
    params: { resource: 'StreamingService' },
  });
  slobs.send(message);
  
}
/**@private
 * Gets the starting streaming status
 * @param {object} slobs object containing slobs information.
 */
function streamingStatus(slobs) {
  const message = JSON.stringify({
    id: ID_STREAMSTATUS,
    jsonrpc: '2.0',
    method: 'getModel',
    params: { resource: 'StreamingService' },
  });
  slobs.send(message);
}

/**@private
 * Toggles slobs streaming between offline/live
 * @param {object} slobs object containing slobs information.
 */
function toggleStreaming(slobs)
{
	const message = JSON.stringify({
    id: ID_TOGGLE,
    jsonrpc: '2.0',
    method: 'toggleStreaming',
    params: { resource: 'StreamingService' },
  });
  slobs.send(message);
}
/**@private
 * Toggles slobs recording between offline/recording
 * @param {object} slobs object containing slobs information.
 */
function toggleRecording(slobs)
{
	const message = JSON.stringify({
    id: ID_TOGGLE,
    jsonrpc: '2.0',
    method: 'toggleRecording',
    params: { resource: 'StreamingService' },
  });
  slobs.send(message);
}
/**@private
 * Requests a list of all the scenes in slobs.
 * @param {object} slobs object containing slobs information.
 */
function getScenes(slobs) {
  const message = JSON.stringify({
    id: ID_SCENES,
    jsonrpc: '2.0',
    method: 'getScenes',
    params: { resource: 'ScenesService' },
  });
  slobs.send(message);
}

/**@private
 * Allows us to turn on/off a source.
 * @param {object} slobs object containing slobs information.
 * @param {string} sceneName case sensitive name of scene.
 * @param {string} sourceName case sensitive name of source.
 */
function toggleSourceVisible(slobs,sceneName, sourceName) {
  if (sceneName !== null){	
	  let source = scenesMap.get(sceneName).get(sourceName);
	  let scene = scenesMap.get(sceneName)
	  if (source.sceneNodeType === 'folder')
	  {
		  for (let value of scene.values())
		  {
			  for (let j = 0; j < source.childrenIds.length; j++)
			  {
				  if (value.id === source.childrenIds[j])
				  {
					  const message = JSON.stringify({
						id: ID_TOGGLE,
						jsonrpc: '2.0',
						method: 'setVisibility',
						params: { resource: value.resourceId, args: [!value.visible] },
					  });
					  slobs.send(message);
					  value.visible = !value.visible;
				  }
			  }
		  }
	  }
	  else{
		  const message = JSON.stringify({
			id: ID_TOGGLE,
			jsonrpc: '2.0',
			method: 'setVisibility',
			params: { resource: source.resourceId, args: [!source.visible] },
		  });
		  slobs.send(message);
		  source.visible = !source.visible;
	  }
  }
  else{
	  for (let key of scenesMap.keys())
	  {
		  let source = scenesMap.get(key).get(sourceName);
		  let scene = scenesMap.get(key)
		  if (source !== undefined){
			  if (source.sceneNodeType === 'folder')
			  {
				  for (let value of scene.values())
				  {
					  for (let j = 0; j < source.childrenIds.length; j++)
					  {
						  if (value.id === source.childrenIds[j])
						  {  
							  const message = JSON.stringify({
								id: ID_TOGGLE,
								jsonrpc: '2.0',
								method: 'setVisibility',
								params: { resource: value.resourceId, args: [!value.visible] },
							  });
							  slobs.send(message);
							  value.visible = !value.visible;
						  }
					  }
				  }
			  }
			  else{
				  const message = JSON.stringify({
					id: ID_TOGGLE,
					jsonrpc: '2.0',
					method: 'setVisibility',
					params: { resource: source.resourceId, args: [!source.visible] },
				  });
				  slobs.send(message);
				  source.visible = !source.visible;
			  }
			}
	  }
  }
}
/**@private
 * Allows us to set active scene.
 * @param {object} slobs 
 * @param {string} sceneName case sensitive name of scene.
 */
function activeScene(slobs, sceneName) {
  const scene = scenesMap.get(sceneName).values().next().value;
  const message = JSON.stringify({
    id: 10,
    jsonrpc: '2.0',
    method: 'makeSceneActive',
    params: { resource: 'ScenesService', args: [scene.sceneId] },
  });
  slobs.send(message);
}

/**@private
 * @param {int} s milliseconds to be converted to time.
 * @returns {string} Time formatted hh:mm:ss.
 * @example msToTime(144324234);
 *
 * 
 */
function msToTime(s) {
  var ms = s % 1000;
  s = (s - ms) / 1000;
  var secs = s % 60;
  s = (s - secs) / 60;
  var mins = s % 60;
  var hrs = (s - mins) / 60;

  return hrs + ':' + mins + ':' + secs;
}

class Slobs extends EventEmitter
{
	/**
	 * @class Class that represents Slobs
	 * @author Krammy <krammy_ie@outlook.com>
	 * @constructor
	 * @param {string} ip IP Address where slobs is running. Local: http://127.0.0.1:59650/api
	 * @param {string} token Settings->Remote Control and click on the QR-Code and then on show details.
	 * @example slobs = new SlobsJS("127.0.0.1:59650/api", "token2143u323i4oy");
	 *
	 * 
	 */
	constructor(){
		super();
        this.socket = null;
        this.tk = null;
    }

	login(ip, token) {
        this.socket = new SockJS(ip);
        this.tk = token;
        
        if (this.socket !== null)
        {
            //AUTHORIZE WITH SLOBS
            this.socket.onopen = () => {
                auth(this.slobs());
            };
            
            // HANDLE MESSAGES RECEIVED
            this.socket.onmessage = (message) => {
                messageHandler(this.slobs(message), this);
            }

            //Output error message if socket closes
            this.socket.onclose = (message) => {
                disconnectHandler(this.slobs(message));
            };
        }
	}
	disconnect()
	{
		this.socket.close(1000, "disconnecting");
		this.tk = null;
		this.socket = null;
		scenesMap.clear();
		slobsStatus.clear();
	}
	/**@public
	 * 
	 * Gets the map of all the scenes from Slobs
	 * @example slobs.getScenes();
	 * @returns {Map} Map of all scenes & sources.
	 * 
	 *
	 * 
	 */
	getScenes()
	{
		return scenesMap;
	}
	
	/**@public
	 * Gets whether stream is live / offline
	 * @example slobs.getStreamingStatus();
	 * @returns {string} live / offline
	 *
	 * 
	 */
	getStreamingStatus() {
		return slobsStatus.get('streamStatus');
	}
	
	/**@public
	 *Gets time since stream started if online (hh:mm:ss), otherwise returns 'offline'.
	* @example slobs.getStreamUptime();
	 * @returns {string} Time formatted (hh:mm:ss) or 'offline'
	 *
	 * 
	 */
	getStreamUptime()
	{
		let time = slobsStatus.get('startTime');
		if(time !== null){
			let date = new Date();
			time = new Date(time);
			time = msToTime(date - time)
		}else time = "offline";
		return time;
	}
	/**@public
	 *Gets time since stream started if recording (hh:mm:ss), otherwise returns 'offline'.
	 * @example slobs.getRecordingUptime();
	 * @returns {string} Time formatted (hh:mm:ss) or 'offline'
	 *
	 * 
	 */
	getRecordingUptime()
	{
		let time = slobsStatus.get('recordingStartTime');
		if(time !== null){
			let date = new Date();
			time = new Date(time);
			time = msToTime(date - time)
		}else time = "offline";
		return time;
	}
	
	/**@public
	 * Allows us to toggle on/off a Source in SLOBS.
	 * @param {string} sceneName Case sensitive name of the Scene the source belongs to.
	 * @param {string} sourceName Case sensitive name of the Source we wish to toggle.
	 *
	 * @example slobs.toggleSource("Game Scene", "Webcam")
	 *
	 * 
	 */
	toggleSource(sceneName, sourceName) {
		toggleSourceVisible(this.socket,sceneName, sourceName);
	}
	/**@public
	 * Set's the defined scene as the active scene.
	 * @param {string} sceneName Case sensitive name of the Scene we wish to set as active.
	 * @example slobs.setActiveScene("Game Scene");
	 *
	 * 
	 */
	setActiveScene(sceneName) {
		activeScene(this.socket,sceneName);
	}
	
	
	/**@public
	 * Toggles recording on/off
	 * @example slobs.toggleRecording();
	 *
	 * 
	 */
	toggleRecording()
	{
		toggleRecording(this.socket);
	}
	
	/**@public
	 * Toggles streaming on/off
	 * @example slobs.toggleStreaming();
	 *
	 * 
	 */
	toggleStreaming()
	{
		toggleStreaming(this.socket);
	}
	/**@public
	 * returns true/false if connected to Streamlabs OBS
	 * @example var connected = slobs.getConnection();
	 *
	 * 
	 */
	getConnected()
	{
		return connected;
	}
	
	
	
	/**@private
	 * Returns an object of all the slobs information for our messages.
	 * @param {string} message the message received from Slobs.
	 * @returns {*} Object containing information for messageHandlers.
	 *
	 * 
	 */
	slobs(message)
	{
		return {"socket": this.socket,
				"token": this.tk, 
				"message":message
		}
	}
}

module.exports = Slobs;