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
|
10
package.json
10
package.json
@ -5,9 +5,13 @@
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"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",
|
||||
"debug": "npx ts-node src/server.ts"
|
||||
"dev": "npm run build && npx ts-node src/server.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
@ -15,4 +19,4 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
}
|
47
src/http.ts
47
src/http.ts
@ -8,20 +8,45 @@ export default class HttpServer {
|
||||
private httpServer: http.Server;
|
||||
private port: number;
|
||||
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.root = root;
|
||||
this.httpServer = http.createServer((req, res) => {
|
||||
const filePath = path.join(root, req.url === "/" ? "index.html" : req.url || "");
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(404);
|
||||
res.end("Not Found");
|
||||
} else {
|
||||
res.writeHead(200);
|
||||
res.end(data);
|
||||
let status: number = 404;
|
||||
let body: any;
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const pathname = path.normalize(url.pathname);
|
||||
if (req.method === 'GET') {
|
||||
const filePath = path.join(root, path.extname(pathname) === '' ? pathname + "/index.html" : pathname);
|
||||
try {
|
||||
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() {
|
||||
@ -34,5 +59,7 @@ export default class HttpServer {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,51 @@
|
||||
import HttpServer from './http';
|
||||
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 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);
|
||||
|
||||
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