API support for channel change
This commit is contained in:
parent
a83a5fa605
commit
358f9c8784
6
dvb_channel.conf
Normal file
6
dvb_channel.conf
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
ION:521028615:8VSB:49:52:3
|
||||||
|
KATU:533028615:8VSB:49:52:3
|
||||||
|
KOIN-HD:539028615:8VSB:49:52:3
|
||||||
|
KGW:545028615:8VSB:49:52:3
|
||||||
|
KBLN-DT:575028615:8VSB:49:52:1
|
||||||
|
TBN HD:581028615:8VSB:49:52:3
|
@ -5,9 +5,13 @@
|
|||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npx tsc --skipLibCheck --outDir dist src/server.ts",
|
"build:scss": "npx sass src/static/css:dist/static/css",
|
||||||
|
"copy:html": "cp src/static/index.html dist/static",
|
||||||
|
"build:js:fe": "npx tsc src/static/js/*.ts --outDir dist/static/js",
|
||||||
|
"build:js:be": "npx tsc --skipLibCheck src/*.ts --outDir dist",
|
||||||
|
"build": "npm run build:js:fe && npm run build:js:be && npm run build:scss && npm run copy:html",
|
||||||
"start": "npm run build && node dist/server.js",
|
"start": "npm run build && node dist/server.js",
|
||||||
"debug": "npx ts-node src/server.ts"
|
"dev": "npm run build && npx ts-node src/server.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
|
47
src/http.ts
47
src/http.ts
@ -8,20 +8,45 @@ export default class HttpServer {
|
|||||||
private httpServer: http.Server;
|
private httpServer: http.Server;
|
||||||
private port: number;
|
private port: number;
|
||||||
private root: string;
|
private root: string;
|
||||||
public constructor(port: number, root : string) {
|
public constructor(port: number, root: string, tune: (ch: string, adp?: number) => void) {
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.root = root;
|
this.root = root;
|
||||||
this.httpServer = http.createServer((req, res) => {
|
this.httpServer = http.createServer((req, res) => {
|
||||||
const filePath = path.join(root, req.url === "/" ? "index.html" : req.url || "");
|
let status: number = 404;
|
||||||
fs.readFile(filePath, (err, data) => {
|
let body: any;
|
||||||
if (err) {
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||||
res.writeHead(404);
|
const pathname = path.normalize(url.pathname);
|
||||||
res.end("Not Found");
|
if (req.method === 'GET') {
|
||||||
} else {
|
const filePath = path.join(root, path.extname(pathname) === '' ? pathname + "/index.html" : pathname);
|
||||||
res.writeHead(200);
|
try {
|
||||||
res.end(data);
|
body = fs.readFileSync(filePath);
|
||||||
|
status = 200;
|
||||||
}
|
}
|
||||||
});
|
catch (err) {
|
||||||
|
body = "Invalid File"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (req.method === 'PUT' && pathname.startsWith('/api/')) {
|
||||||
|
const query = pathname.split('/');
|
||||||
|
const api = query[2];
|
||||||
|
switch (api) {
|
||||||
|
case "channel":
|
||||||
|
const channel = query[3];
|
||||||
|
const adapter = parseInt(url.searchParams.get('adapter'));
|
||||||
|
tune(channel, adapter);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
body = "Invalid API Endpoint"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
body = "Invalid Request"
|
||||||
|
}
|
||||||
|
res.writeHead(status);
|
||||||
|
res.end(body);
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public start() {
|
public start() {
|
||||||
@ -34,5 +59,7 @@ export default class HttpServer {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,51 @@
|
|||||||
import HttpServer from './http';
|
import HttpServer from './http';
|
||||||
import TVWebSocket from './ws';
|
import TVWebSocket from './ws';
|
||||||
|
import Zap, { IZap } from './zap';
|
||||||
|
|
||||||
const HTTP_PORT = process.env.HTTP_PORT ? parseInt(process.env.HTTP_PORT,10) : 8080;
|
import * as readline from 'readline';
|
||||||
|
|
||||||
|
const HTTP_PORT = process.env.HTTP_PORT ? parseInt(process.env.HTTP_PORT, 10) : 8080;
|
||||||
const WS_PORT = process.env.WS_PORT ? parseInt(process.env.WS_PORT, 10) : 3001;
|
const WS_PORT = process.env.WS_PORT ? parseInt(process.env.WS_PORT, 10) : 3001;
|
||||||
const STATIC_ROOT = process.cwd() + "/dist/static";;
|
const STATIC_ROOT = process.cwd() + "/dist/static";
|
||||||
|
const TV_DEV_0 = process.env.TV_DEV_0 ?? '/dev/dvb/adapter0/dvr0'
|
||||||
|
const TV_DEV_1 = process.env.TV_DEV_1 ? '/dev/dvb/adapter0/dvr1' : null;
|
||||||
|
|
||||||
const httpServer = new HttpServer(HTTP_PORT, STATIC_ROOT);
|
const zap = new Zap();
|
||||||
|
|
||||||
|
|
||||||
|
const tune = (reqChannel: string, reqAdapter?: number) => {
|
||||||
|
const adapter = reqAdapter === 0 || reqAdapter === 1 ? reqAdapter : 0;
|
||||||
|
zap.zapTo(reqChannel, adapter).then((zap: IZap) => {
|
||||||
|
console.log(`Tuned ${zap.adapter} to ${zap.channel}`)
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpServer = new HttpServer(HTTP_PORT, STATIC_ROOT, tune);
|
||||||
const tvWebSocket = new TVWebSocket(WS_PORT);
|
const tvWebSocket = new TVWebSocket(WS_PORT);
|
||||||
|
|
||||||
httpServer.start();
|
httpServer.start();
|
||||||
|
|
||||||
|
|
||||||
|
process.stdin.setEncoding("utf8");
|
||||||
|
process.stdin.resume();
|
||||||
|
|
||||||
|
console.log("Enter Channel Name:");
|
||||||
|
|
||||||
|
process.stdin.on("data", async (data: string) => {
|
||||||
|
const input = data.trim();
|
||||||
|
console.log(`Received: "${input}"`);
|
||||||
|
await zap.zapTo(input).then((zap: IZap) => {
|
||||||
|
console.log(`Tuned ${zap.adapter} to ${zap.channel}`)
|
||||||
|
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
127
src/zap.ts
Normal file
127
src/zap.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
export interface IZap {
|
||||||
|
process: ChildProcessWithoutNullStreams | null,
|
||||||
|
channel: string,
|
||||||
|
adapter: 0 | 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Zap {
|
||||||
|
private zap0: IZap;
|
||||||
|
private zap1: IZap;
|
||||||
|
private channelNameList: string[];
|
||||||
|
private fileName: string;
|
||||||
|
public constructor(fileName = "dvb_channel.conf", channel = "ION") {
|
||||||
|
const zap0: IZap = {
|
||||||
|
process: null,
|
||||||
|
channel,
|
||||||
|
adapter: 0
|
||||||
|
}
|
||||||
|
const zap1: IZap = {
|
||||||
|
process: null,
|
||||||
|
channel,
|
||||||
|
adapter: 1
|
||||||
|
}
|
||||||
|
this.zap0 = zap0;
|
||||||
|
this.zap1 = zap1;
|
||||||
|
|
||||||
|
const file = fs.readFileSync('./' + fileName, "utf-8");
|
||||||
|
const lines = file.split("\n").map(line => line.trim()).filter(line => line && !line.startsWith("#")).map(line => line.split(":")[0]);
|
||||||
|
this.channelNameList = lines;
|
||||||
|
this.fileName = fileName;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextChannel(channel: string) {
|
||||||
|
const size = this.channelNameList.length;
|
||||||
|
const currentIndex = this.channelNameList.indexOf(channel);
|
||||||
|
if (currentIndex >= 0) {
|
||||||
|
return this.channelNameList[this.mod(currentIndex + 1, size)];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mod(n: number, m: number) {
|
||||||
|
return ((n % m) + m) % m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private previousChannel(channel: string) {
|
||||||
|
const size = this.channelNameList.length;
|
||||||
|
const currentIndex = this.channelNameList.indexOf(channel);
|
||||||
|
if (currentIndex >= 0) {
|
||||||
|
return this.channelNameList[this.mod(currentIndex - 1, size)];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanup(proc: ChildProcessWithoutNullStreams): Promise<void> {
|
||||||
|
proc.kill('SIGHUP');
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
proc.once('exit', () =>
|
||||||
|
resolve()
|
||||||
|
)
|
||||||
|
proc.on("error", (err:Error) =>
|
||||||
|
reject(err)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async zapTo(channel: string, adapter: 0 | 1 = 0): Promise<IZap> {
|
||||||
|
let zap = adapter == 0 ? this.zap0 : this.zap1;
|
||||||
|
let verifiedChannel: string;
|
||||||
|
if (channel == '+') {
|
||||||
|
verifiedChannel = this.nextChannel(zap.channel);
|
||||||
|
}
|
||||||
|
else if (channel == '-') {
|
||||||
|
verifiedChannel = this.previousChannel(zap.channel);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!this.channelNameList.includes(channel)) {
|
||||||
|
return Promise.reject(new Error("Invalid Channel name"));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
verifiedChannel = channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cmd = 'dvbv5-zap'
|
||||||
|
const args: string[] = ["-r", "-C", "US", "-I", "ZAP", "-c", this.fileName, "-a", adapter.toString(), verifiedChannel];
|
||||||
|
if (zap.process != null) {
|
||||||
|
await this.cleanup(zap.process).catch(err => Promise.reject(err));
|
||||||
|
|
||||||
|
}
|
||||||
|
zap.process = spawn(cmd, args);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let lockTimer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
|
||||||
|
zap.process.stderr.on("data", (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
|
||||||
|
if (/Lock/.test(output)) {
|
||||||
|
clearTimeout(lockTimer);
|
||||||
|
zap.channel = verifiedChannel;
|
||||||
|
resolve(zap);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/Not locked/.test(output)) {
|
||||||
|
clearTimeout(lockTimer);
|
||||||
|
zap.process.kill("SIGHUP");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
zap.process.on("exit", (code) => {
|
||||||
|
reject(new Error("Unexpected exit of Zap"));
|
||||||
|
});
|
||||||
|
|
||||||
|
lockTimer = setTimeout(() => {
|
||||||
|
reject(new Error("Failed to Zap in time"));
|
||||||
|
}, 5000);
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user