Skip to content

Instantly share code, notes, and snippets.

@Dimitreee
Forked from jlai/webos-ss-client.js
Created July 13, 2020 11:24
Show Gist options
  • Save Dimitreee/1316fbfbf9c9e03c4400bebeb4116090 to your computer and use it in GitHub Desktop.
Save Dimitreee/1316fbfbf9c9e03c4400bebeb4116090 to your computer and use it in GitHub Desktop.
Example nodejs code for connecting to a webOS Smart TV
/*
* Copyright (c) 2014 LG Electronics.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var events = require('events');
var WebSocket = require('ws');
var util = require('util');
if (process.version === 'v0.4.12') {
// Hacks to make ws library run
Buffer.prototype.readUInt32LE = function (offset, noAssert) {
return this[offset] | (this[offset+1] << 8) | (this[offset+2] << 16) | (this[offset+3] << 24);
};
Buffer.prototype.writeUInt32LE = function (value, offset, noAssert) {
this[offset] = value & 0xFF;
this[offset+1] = (value >>> 8) & 0xFF;
this[offset+2] = (value >>> 16) & 0xFF;
this[offset+3] = (value >>> 24) & 0xFF;
};
function FakeBuffer () {
throw new Error("This buffer type doesn't exist on this version of node");
};
global.ArrayBuffer = FakeBuffer;
global.Uint8Array = FakeBuffer;
global.Uint16Array = FakeBuffer;
global.Uint32Array = FakeBuffer;
global.Int8Array = FakeBuffer;
global.Int16Array = FakeBuffer;
global.Int32Array = FakeBuffer;
global.Float32Array = FakeBuffer;
global.Float64Array = FakeBuffer;
}
var Client = function () {
events.EventEmitter.call(this);
this.requestId = 1;
this.requests = {};
this.manifest = {
permissions: ["LAUNCH", "CONTROL_AUDIO"]
};
};
events.EventEmitter.call(Client);
util.inherits(Client, events.EventEmitter);
Client.prototype.connect = function (ip, cb) {
if (cb) {
var handler = function () {
this.removeListener('connected', handler);
this.removeListener('error', handler);
this.removeListener('close', handler);
cb();
};
this.on('connected', handler);
this.on('error', handler);
this.on('close', handler);
}
this.ws = new WebSocket("ws://" + ip + ":3000", {origin: "null"});
this.ws.on('open', function () {
console.log("opened");
this.send({
type: 'register',
payload: {
manifest: this.manifest
}
});
}.bind(this));
this.ws.on('message', function (data) {
console.log("Received message: " + data);
var message = JSON.parse(data);
var request = message.id ? this.requests[message.id] : null;
if (message.type === "response" || message.type === "error") {
if (request) {
if (request.callback) {
request.callback(message.payload);
}
if (!request.isSubscription) {
delete this.requests[request];
}
}
} else if (message.type === "registered") {
this.emit('connected');
}
}.bind(this));
this.ws.on('error', function (err) {
this.emit('error', err);
}.bind(this));
this.ws.on('close', function () {
this.emit('close', 'connection closed');
}.bind(this));
};
Client.prototype.send = function (obj) {
console.log("Sending: " + JSON.stringify(obj));
this.ws.send(JSON.stringify(obj));
};
Client.prototype.sendRequest = function (uri, payload, cb) {
var requestId = this.requestId++;
this.send({
type: 'request',
id: requestId,
uri: uri,
payload: payload || {}
});
this.requests[requestId] = {callback: cb};
};
function DeviceFinder () {
events.EventEmitter.call(this);
this.devices = {};
this.startDiscovery();
};
util.inherits(DeviceFinder, events.EventEmitter);
DeviceFinder.prototype.getDeviceList = function () {
return this.devices;
};
DeviceFinder.prototype.startDiscovery = function () {
if (this.socket) {
return;
}
var dgram = require('dgram');
var socket = this.socket = dgram.createSocket('udp4');
var sendSocket = dgram.createSocket('udp4');
var sendSearch = this.sendSearch.bind(this);
var cleanOld = function () {
Object.keys(this.devices).forEach(function (ip) {
var record = this.devices[ip];
if (record && --record.ttl <= 0) {
delete this.devices[ip];
this.emit("lost", rinfo.address);
}
}, this);
}.bind(this);
socket.on('listening', function () {
socket.setBroadcast(true);
socket.addMembership('239.255.255.250');
for (var i = 0; i < 3; i++) {
setTimeout(function () {
sendSearch();
}, Math.random() * 1000);
}
this.rescanId = setInterval(function () {
cleanOld();
sendSearch();
}, 10 * 1000);
});
socket.on('message', function (data, rinfo) {
if (/^HTTP\/1.1 200 OK\r\n/.test(data) || /^NOTIFY \* HTTP\/1.1\r\n/.test(data)) {
console.log("got response\n" + data);
if (/USN:.*service:webos-second-screen/.test(data)) {
if (/ssdp:byebye/.test(data)) {
delete this.devices[rinfo.address];
this.emit("lost", rinfo.address);
} else {
var existing = this.devices[rinfo.address];
this.devices[rinfo.address] = {ttl: 2};
if (!existing) {
this.emit("found", rinfo.address);
}
}
this.emit("update");
}
}
}.bind(this));
socket.bind(0, '0.0.0.0');
};
DeviceFinder.prototype.sendSearch = function () {
var data = "M-SEARCH * HTTP/1.1\r\n" +
"Host: 239.255.255.250:1900\r\n" +
"ST: urn:lge-com:service:webos-second-screen:1\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"MX: 5\r\n" +
"\r\n";
console.log("sending: " + data);
var buffer = new Buffer(data, 'ascii');
this.socket.send(buffer, 0, buffer.length, 1900, "239.255.255.250");
};
DeviceFinder.prototype.stopDiscovery = function () {
if (this.socket) {
this.socket.end();
this.socket.destroy();
this.socket = null;
}
if (this.rescanId) {
clearInterval(this.rescanId);
this.rescanId = null;
}
};
function openBrowserOnTV(ip, url, cb) {
var client = new Client();
client.connect(ip, function (err) {
if (err) {
console.log("error connecting");
cb(err);
return;
} else {
console.log("connected");
}
client.sendRequest('ssap://system.launcher/open', {"target": url}, cb);
});
}
function toggleMuteOnTV(ip, cb) {
var client = new Client();
client.connect(ip, function (err) {
if (err) {
console.log("error connecting");
cb(err);
return;
} else {
console.log("connected");
}
client.sendRequest('ssap://audio/getMute', {}, function (response) {
client.sendRequest('ssap://audio/setMute', {"mute": !response.mute}, cb);
});
});
}
function openBrowserOnFirstWebOSTVFound() {
var df = new DeviceFinder();
df.startDiscovery();
df.on('found', function (ip) {
console.log("found TV ip: " + ip);
openBrowserOnTV(ip, "http://www.connectsdk.com", function done (err) {
if (err) {
console.log("launch browser failed: " + JSON.stringify(err));
} else {
console.log("launch browser successful");
}
process.exit(0);
});
});
}
/*
toggleMuteOnTV("192.168.0.145", function (err) {
if (err) {
console.log("err: " + JSON.stringify(err));
} else {
console.log("success");
}
process.exit(0);
});
*/
openBrowserOnFirstWebOSTVFound();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment