@tmigone/pulseaudio
Version:
A TypeScript based client library for PulseAudio.
368 lines • 14.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const events_1 = require("events");
const fs_1 = require("fs");
const net_1 = require("net");
const packet_1 = require("./packet");
const request_1 = require("./request");
const protocol_1 = require("./protocol");
const error_1 = require("./error");
const client_1 = require("./commands/client");
const sink_1 = require("./commands/sink");
const server_1 = require("./commands/server");
const sinkInput_1 = require("./commands/sinkInput");
const module_1 = require("./commands/module");
const source_1 = require("./commands/source");
const sourceOutput_1 = require("./commands/sourceOutput");
class PulseAudio extends events_1.EventEmitter {
constructor(address, cookie) {
super();
this.cookie = Buffer.allocUnsafe(256);
this.connected = false;
this.protocol = 0;
this.chunks = [];
this.requests = [];
this.lastRequestId = 0;
this.parseAddress(address);
if (cookie !== undefined)
this.parseCookie(cookie);
}
async connect() {
return await new Promise((resolve, reject) => {
this.socket = new net_1.Socket();
if (this.address.type === 'tcp') {
this.socket.connect(this.address.port, this.address.host);
}
else {
this.socket.connect(this.address.path);
}
this.socket.on('connect', async () => {
this.connected = true;
const reply = await this.authenticate();
this.protocol = reply.protocol;
if (this.address.type === 'tcp') {
console.log(`Connected to PulseAudio at tcp://${this.address.host}:${this.address.port} using protocol v${this.protocol}`);
}
else {
console.log(`Connected to PulseAudio at unix://${this.address.path} using protocol v${this.protocol}`);
}
if (reply.protocol < protocol_1.PA_PROTOCOL_MINIMUM_VERSION) {
this.disconnect();
reject(new Error(`Server protocol version is too low, please update to ${protocol_1.PA_PROTOCOL_MINIMUM_VERSION} or higher.`));
}
resolve(reply);
});
this.socket.on('readable', this.onReadable.bind(this));
this.socket.on('error', reject);
});
}
disconnect() {
this.socket.removeAllListeners();
this.socket.end();
}
async setClientName(clientName = 'paclient') {
const query = client_1.SetClientName.query(this.requestId(), clientName);
return await this.sendRequest(query);
}
async authenticate() {
const query = server_1.Authenticate.query(this.requestId(), this.cookie);
return await this.sendRequest(query);
}
async subscribe() {
const query = server_1.Subscribe.query(this.requestId());
return await this.sendRequest(query);
}
async getServerInfo() {
const query = server_1.GetServerInfo.query(this.requestId());
return await this.sendRequest(query);
}
async getSink(sink) {
const query = sink_1.GetSink.query(this.requestId(), sink);
return await this.sendRequest(query);
}
async getSinkList() {
const query = sink_1.GetSinkList.query(this.requestId());
return await this.sendRequest(query);
}
async setSinkVolume(sink, volume) {
const query = sink_1.SetSinkVolume.query(this.requestId(), sink, { channels: 2, volumes: [volume, volume] });
return await this.sendRequest(query);
}
async getSinkInputList() {
const query = sinkInput_1.GetSinkInputList.query(this.requestId());
return await this.sendRequest(query);
}
async getSinkInput(sinkInput) {
const query = sinkInput_1.GetSinkInput.query(this.requestId(), sinkInput);
return await this.sendRequest(query);
}
async moveSinkInput(sinkInput, sink) {
const query = sinkInput_1.MoveSinkInput.query(this.requestId(), sinkInput, sink);
return await this.sendRequest(query);
}
async getSource(source) {
const query = source_1.GetSource.query(this.requestId(), source);
return await this.sendRequest(query);
}
async getSourceList() {
const query = source_1.GetSourceList.query(this.requestId());
return await this.sendRequest(query);
}
async setSourceVolume(source, volume) {
const query = source_1.SetSourceVolume.query(this.requestId(), source, { channels: 2, volumes: [volume, volume] });
return await this.sendRequest(query);
}
async getSourceOutput(sourceOutput) {
const query = sourceOutput_1.GetSourceOutput.query(this.requestId(), sourceOutput);
return await this.sendRequest(query);
}
async getSourceOutputList() {
const query = sourceOutput_1.GetSourceOutputList.query(this.requestId());
return await this.sendRequest(query);
}
async moveSourceOutput(sourceOutput, source) {
const query = sourceOutput_1.MoveSourceOutput.query(this.requestId(), sourceOutput, source);
return await this.sendRequest(query);
}
async getModule(module) {
const query = module_1.GetModule.query(this.requestId(), module);
return await this.sendRequest(query);
}
async getModuleList() {
const query = module_1.GetModuleList.query(this.requestId());
return await this.sendRequest(query);
}
async loadModule(name, argument) {
const query = module_1.LoadModule.query(this.requestId(), name, argument);
return await this.sendRequest(query);
}
async unloadModule(module) {
const query = module_1.UnloadModule.query(this.requestId(), module);
return await this.sendRequest(query);
}
onReadable() {
if (this.socket.readableLength < 10) {
console.log('Malformed packet!');
const malformed = this.socket.read(this.socket.readableLength);
console.log(malformed);
return;
}
const chunk = this.socket.read(this.socket.readableLength);
if (packet_1.default.isChunkHeader(chunk) && this.chunks.length > 0) {
console.log('WARNING: discarding chunks, this might be a bug...');
this.chunks = [];
}
this.chunks.push(chunk);
while (packet_1.default.isValidPacket(this.chunks)) {
const packet = new packet_1.default(Buffer.concat(this.chunks));
this.resolveRequest(packet);
this.chunks = packet_1.default.getChunksSize(this.chunks) === packet.packet.length ? [] : [Buffer.concat(this.chunks).subarray(packet.packet.length)];
}
}
requestId() {
this.lastRequestId = (this.lastRequestId + 1) & protocol_1.PA_MAX_REQUEST_ID;
return this.lastRequestId;
}
async sendRequest(query) {
const request = new request_1.default(this.lastRequestId, query);
this.requests.push(request);
if (!this.connected && query.command.value !== 8..toString().charCodeAt(0)) {
return this.rejectRequest(request, new Error('No connection to PulseAudio.'));
}
this.socket.write(request.query.write());
return await request.promise;
}
resolveRequest(reply) {
let request;
let event;
switch (reply.command.value) {
case 0:
request = this.requests.find(r => r.id === reply.requestId.value);
request === null || request === void 0 ? void 0 : request.resolve({ success: false, error: error_1.PAError[reply.tags[0].value] });
this.requests = this.requests.filter(r => r.id !== reply.requestId.value);
break;
case 2:
request = this.requests.find(r => r.id === reply.requestId.value);
request === null || request === void 0 ? void 0 : request.resolve(this.parseReply(reply, request.query.command));
this.requests = this.requests.filter(r => r.id !== reply.requestId.value);
break;
case 66:
event = this.parseEvent(reply);
this.emit(event.category, event);
this.emit('all', event);
break;
default:
throw new Error(`Reply type ${reply.command.value} not supported. Please report issue.`);
}
}
rejectRequest(request, error) {
request.reject(error);
this.requests = this.requests.filter(r => r.id !== request.id);
}
parseReply(reply, query) {
let retObj = {};
switch (query.value) {
case 8:
retObj = server_1.Authenticate.reply(reply, this.protocol);
break;
case 20:
retObj = server_1.GetServerInfo.reply(reply, this.protocol);
break;
case 35:
retObj = server_1.Subscribe.reply(reply, this.protocol);
break;
case 9:
retObj = client_1.SetClientName.reply(reply, this.protocol);
break;
case 22:
retObj = sink_1.GetSinkList.reply(reply, this.protocol);
break;
case 21:
retObj = sink_1.GetSink.reply(reply, this.protocol);
break;
case 36:
retObj = sink_1.SetSinkVolume.reply(reply, this.protocol);
break;
case 29:
retObj = sinkInput_1.GetSinkInput.reply(reply, this.protocol);
break;
case 30:
retObj = sinkInput_1.GetSinkInputList.reply(reply, this.protocol);
break;
case 67:
retObj = sinkInput_1.MoveSinkInput.reply(reply, this.protocol);
break;
case 23:
retObj = source_1.GetSource.reply(reply, this.protocol);
break;
case 24:
retObj = source_1.GetSourceList.reply(reply, this.protocol);
break;
case 38:
retObj = source_1.SetSourceVolume.reply(reply, this.protocol);
break;
case 31:
retObj = sourceOutput_1.GetSourceOutput.reply(reply, this.protocol);
break;
case 32:
retObj = sourceOutput_1.GetSourceOutputList.reply(reply, this.protocol);
break;
case 68:
retObj = sourceOutput_1.MoveSourceOutput.reply(reply, this.protocol);
break;
case 25:
retObj = module_1.GetModule.reply(reply, this.protocol);
break;
case 26:
retObj = module_1.GetModuleList.reply(reply, this.protocol);
break;
case 51:
retObj = module_1.LoadModule.reply(reply, this.protocol);
break;
case 52:
retObj = module_1.UnloadModule.reply(reply, this.protocol);
break;
default:
throw new Error(`Command ${query.value} not supported. Please report issue.`);
}
return retObj;
}
parseEvent(packet) {
const details = packet.tags[0].value;
const index = packet.tags[1].value;
let category = '';
switch (details & 15) {
case 0:
category = 'sink';
break;
case 1:
category = 'source';
break;
case 2:
category = 'sinkInput';
break;
case 3:
category = 'sourceOutput';
break;
case 4:
category = 'module';
break;
case 5:
category = 'client';
break;
case 6:
category = 'sampleCache';
break;
case 7:
category = 'server';
break;
case 8:
category = 'autoload';
break;
case 9:
category = 'card';
break;
default:
throw new Error(`Details type ${details} not supported. Please report issue.`);
}
let type = '';
switch (details & 48) {
case 0:
type = 'new';
break;
case 16:
type = 'change';
break;
case 32:
type = 'remove';
break;
default:
throw new Error(`Event type ${details} not supported. Please report issue.`);
}
return {
index,
category,
type
};
}
parseAddress(address) {
var _a, _b;
if (address.startsWith('tcp:')) {
const split = address.split(':');
this.address = {
type: 'tcp',
port: parseInt((_a = split[2]) !== null && _a !== void 0 ? _a : '4317'),
host: split[1]
};
}
else if (address.startsWith('unix:')) {
const split = address.split(':');
this.address = {
type: 'unix',
path: split[1]
};
}
else if (address.includes(':')) {
const split = address.split(':');
this.address = {
type: 'tcp',
port: parseInt((_b = split[1]) !== null && _b !== void 0 ? _b : '4317'),
host: split[0]
};
}
else {
throw new Error('Unrecognized server address format. Please use "tcp:host:port", "unix:/path/to/socket" or "host:port".');
}
}
parseCookie(cookiePath) {
try {
this.cookie = Buffer.from((0, fs_1.readFileSync)(cookiePath, 'hex'), 'hex');
}
catch (error) {
console.log('Error reading cookie file, might not be able to authenticate.');
console.log(error);
}
}
}
exports.default = PulseAudio;
//# sourceMappingURL=client.js.map