Compare commits
No commits in common. "f0d5af522001ce0abdece4e93a9320608b2f1608" and "a83a5fa605a432a2d8f3c186db915006b3700531" have entirely different histories.
f0d5af5220
...
a83a5fa605
@ -1,33 +0,0 @@
|
|||||||
name: Personal Website - Run Python HTTP Server
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: self-hosted
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
run: |
|
|
||||||
cd ~/ota-tv-web
|
|
||||||
git fetch
|
|
||||||
git checkout main
|
|
||||||
git pull origin main
|
|
||||||
|
|
||||||
- name: Stop existing screen session, if running
|
|
||||||
run: |
|
|
||||||
if screen -list | grep -q "ota_tv_web_server"; then
|
|
||||||
echo "Stopping existing screen session..."
|
|
||||||
screen -S ota_tv_web -X quit
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Start server in screen session
|
|
||||||
run: |
|
|
||||||
cd ~/ota-tv-web
|
|
||||||
chmod +x ./start.sh
|
|
||||||
sassc css/style.scss css/style.css
|
|
||||||
setsid screen -dmS ota_tv_web bash -c 'HTTP_PORT=8081 WS_PORT=3001 npm start > server.log 2>&1'
|
|
||||||
echo "Server started in detached screen session"
|
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
FOX 12:207028615:8VSB:49:52:3
|
|
||||||
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,13 +5,9 @@
|
|||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:scss": "npx sass src/static/css:dist/static/css",
|
"build": "npx tsc --skipLibCheck --outDir dist src/server.ts",
|
||||||
"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",
|
||||||
"dev": "npm run build && npx ts-node src/server.ts"
|
"debug": "npx ts-node src/server.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
|
64
src/http.ts
64
src/http.ts
@ -8,62 +8,20 @@ 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, tune: (ch: string, adp?: number) => void, getChannels: ()=>string[], getSignal: (adapter:number)=>object) {
|
public constructor(port: number, root : string) {
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.root = root;
|
this.root = root;
|
||||||
this.httpServer = http.createServer((req, res) => {
|
this.httpServer = http.createServer((req, res) => {
|
||||||
let status: number = 404;
|
const filePath = path.join(root, req.url === "/" ? "index.html" : req.url || "");
|
||||||
let body: any = "";
|
fs.readFile(filePath, (err, data) => {
|
||||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
if (err) {
|
||||||
const pathname = path.normalize(url.pathname);
|
res.writeHead(404);
|
||||||
|
res.end("Not Found");
|
||||||
if (pathname.startsWith('/api/')) {
|
} else {
|
||||||
const query = pathname.split('/');
|
res.writeHead(200);
|
||||||
const api = query[2];
|
res.end(data);
|
||||||
switch (req.method) {
|
|
||||||
case "GET":
|
|
||||||
switch (api) {
|
|
||||||
case "list":
|
|
||||||
body = JSON.stringify(getChannels());
|
|
||||||
status = 200;
|
|
||||||
break;
|
|
||||||
case "signal":
|
|
||||||
const adapter = parseInt(url.searchParams.get('adapter'));
|
|
||||||
body = JSON.stringify(getSignal(adapter));
|
|
||||||
status = 200;
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "PUT":
|
|
||||||
switch (api) {
|
|
||||||
case "tune":
|
|
||||||
const channel = decodeURIComponent(query[3]);
|
|
||||||
const adapter = parseInt(url.searchParams.get('adapter'));
|
|
||||||
tune(channel, adapter);
|
|
||||||
status = 202;
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
else 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 {
|
|
||||||
body = "Invalid Request"
|
|
||||||
}
|
|
||||||
res.writeHead(status);
|
|
||||||
res.end(body);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public start() {
|
public start() {
|
||||||
@ -76,7 +34,5 @@ export default class HttpServer {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,56 +1,15 @@
|
|||||||
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;
|
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/adapter1/dvr0';
|
|
||||||
|
|
||||||
const zap = new Zap();
|
const httpServer = new HttpServer(HTTP_PORT, STATIC_ROOT);
|
||||||
|
const tvWebSocket = new TVWebSocket(WS_PORT);
|
||||||
|
|
||||||
|
|
||||||
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 getChannels = () =>
|
|
||||||
zap.getChannels();
|
|
||||||
|
|
||||||
const getSignal = (adapter: number) =>
|
|
||||||
zap.getSignal(adapter)
|
|
||||||
|
|
||||||
const httpServer = new HttpServer(HTTP_PORT, STATIC_ROOT, tune, getChannels, getSignal);
|
|
||||||
const tvWebSocket0 = new TVWebSocket(WS_PORT, TV_DEV_0);
|
|
||||||
const tvWebSocket1 = new TVWebSocket(WS_PORT + 1, TV_DEV_1);
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,8 +26,6 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
h1 {}
|
h1 {}
|
||||||
|
|
||||||
p {}
|
p {}
|
||||||
@ -35,20 +33,6 @@ body {
|
|||||||
width: 20em;
|
width: 20em;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
.content{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 1em;
|
|
||||||
.player{
|
|
||||||
|
|
||||||
.channel-group{
|
|
||||||
display: flex;
|
|
||||||
align-self: center;
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,32 +9,11 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header></header>
|
<h1>Video streams</h1>
|
||||||
<main>
|
<h2>WebRTC</h2>
|
||||||
<h1>Video streams</h1>
|
<video id="video" autoplay playsinline controls></video>
|
||||||
<h2>WebRTC</h2>
|
|
||||||
<div class="content">
|
|
||||||
<div class="player">
|
|
||||||
<video id="video0" autoplay playsinline controls></video>
|
|
||||||
<div id="channel-container-0" class="channel-group"></div>
|
|
||||||
<button onClick="tune(0)">Tune</button>
|
|
||||||
<p id="signal-0">N/A</p>
|
|
||||||
<button onClick="getSignal(0)">Get Signal</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="player">
|
<script src="js/index.js"></script>
|
||||||
<video id="video1" autoplay playsinline controls></video>
|
|
||||||
<div id="channel-container-1" class="channel-group"></div>
|
|
||||||
<button onClick="tune(1)">Tune</button>
|
|
||||||
<p id="signal-1">N/A</p>
|
|
||||||
<button onClick="getSignal(1)">Get Signal</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="js/video.js"></script>
|
|
||||||
<script src="js/doc.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -1,61 +0,0 @@
|
|||||||
const PLAYERS = 2;
|
|
||||||
|
|
||||||
const populateChannels = (players: number) => {
|
|
||||||
fetch('/api/list').then(async (res) => {
|
|
||||||
const channelNames: string[] = await res.json()
|
|
||||||
|
|
||||||
|
|
||||||
for (let i = 0; i < players; ++i) {
|
|
||||||
const radioGroup = document.getElementById(`channel-container-${i}`);
|
|
||||||
if (!radioGroup) {
|
|
||||||
throw new Error("Radio group not found")
|
|
||||||
}
|
|
||||||
radioGroup.innerHTML = ''
|
|
||||||
channelNames.forEach((channelName, _) => {
|
|
||||||
const id = `radio-${i}-${channelName}`;
|
|
||||||
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = "radio"
|
|
||||||
input.name = `channel-radio-${i}`;
|
|
||||||
input.value = `${channelName}`;
|
|
||||||
input.id = id
|
|
||||||
|
|
||||||
const lbl = document.createElement("label");
|
|
||||||
lbl.htmlFor = id;
|
|
||||||
lbl.textContent = channelName;
|
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
|
||||||
wrapper.appendChild(input);
|
|
||||||
wrapper.appendChild(lbl);
|
|
||||||
|
|
||||||
radioGroup.appendChild(wrapper)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}).catch(err => {
|
|
||||||
console.log("nope ", err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const tune = (adapter = 0) => {
|
|
||||||
const choice = document.querySelector<HTMLInputElement>(`input[name="channel-radio-${adapter}"]:checked`)
|
|
||||||
const channel = choice?.value;
|
|
||||||
if (channel) {
|
|
||||||
fetch(`/api/tune/${channel}?adapter=${adapter}`, { method: 'PUT' })
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSignal = (adapter = 0) => {
|
|
||||||
const signalElement = document.getElementById(`signal-${adapter}`);
|
|
||||||
if (!signalElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetch(`/api/signal?adapter=${adapter}`).then(res =>
|
|
||||||
res.json()
|
|
||||||
).then((strength: any) => {
|
|
||||||
signalElement.innerHTML = `Signal: ${strength.signal} C/N: ${strength.cn}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
populateChannels(PLAYERS);
|
|
48
src/static/js/index.ts
Normal file
48
src/static/js/index.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
const host = window.location.hostname
|
||||||
|
const ws = new WebSocket(`ws://${host}:3001`);
|
||||||
|
const pc = new RTCPeerConnection({ iceServers: [] });
|
||||||
|
const video = document.getElementById('video') as HTMLVideoElement;
|
||||||
|
|
||||||
|
pc.onconnectionstatechange = (event) => {
|
||||||
|
console.log("onconnectionstatechange ", event)
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.ondatachannel = (event) => {
|
||||||
|
console.log("ondatachannel ", event)
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.ontrack = (event) => {
|
||||||
|
console.log("Received track event", event.streams);
|
||||||
|
video.srcObject = event.streams[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
pc.onicecandidate = ({ candidate }) => {
|
||||||
|
if (candidate) {
|
||||||
|
ws.send(JSON.stringify({ type: 'ice-candidate', data: candidate })); // Use 'candidate' instead of 'ice-candidate'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pc.onicegatheringstatechange = () => {
|
||||||
|
// console.log('ICE state:', pc.iceGatheringState);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onopen = async () => {
|
||||||
|
pc.addTransceiver('video', { direction: 'recvonly' });
|
||||||
|
pc.addTransceiver('audio', { direction: 'recvonly' })
|
||||||
|
const offer = await pc.createOffer();
|
||||||
|
await pc.setLocalDescription(offer);
|
||||||
|
ws.send(JSON.stringify({ type: 'offer', data: offer }));
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = async (message) => {
|
||||||
|
const msg = JSON.parse(message.data);
|
||||||
|
|
||||||
|
if (msg.type === 'answer') {
|
||||||
|
await pc.setRemoteDescription(msg.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (msg.type === 'ice-candidate') {
|
||||||
|
await pc.addIceCandidate(msg.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
;
|
@ -1,67 +0,0 @@
|
|||||||
const host = window.location.hostname
|
|
||||||
const ws0 = new WebSocket(`ws://${host}:3001`);
|
|
||||||
const ws1 = new WebSocket(`ws://${host}:3002`);
|
|
||||||
const pc0 = new RTCPeerConnection({ iceServers: [] });
|
|
||||||
const pc1 = new RTCPeerConnection({ iceServers: [] });
|
|
||||||
const video0 = document.getElementById('video0') as HTMLVideoElement;
|
|
||||||
const video1 = document.getElementById('video1') as HTMLVideoElement;
|
|
||||||
|
|
||||||
// 0
|
|
||||||
pc0.ontrack = (event) => {
|
|
||||||
console.log("Received track event", event.streams);
|
|
||||||
video0.srcObject = event.streams[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
pc0.onicecandidate = ({ candidate }) => {
|
|
||||||
if (candidate) {
|
|
||||||
ws0.send(JSON.stringify({ type: 'ice-candidate', data: candidate }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws0.onopen = async () => {
|
|
||||||
pc0.addTransceiver('video', { direction: 'recvonly' });
|
|
||||||
pc0.addTransceiver('audio', { direction: 'recvonly' })
|
|
||||||
const offer = await pc0.createOffer();
|
|
||||||
await pc0.setLocalDescription(offer);
|
|
||||||
ws0.send(JSON.stringify({ type: 'offer', data: offer }));
|
|
||||||
}
|
|
||||||
|
|
||||||
ws0.onmessage = async (message) => {
|
|
||||||
const msg = JSON.parse(message.data);
|
|
||||||
if (msg.type === 'answer') {
|
|
||||||
await pc0.setRemoteDescription(msg.data);
|
|
||||||
}
|
|
||||||
else if (msg.type === 'ice-candidate') {
|
|
||||||
await pc0.addIceCandidate(msg.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1
|
|
||||||
pc1.ontrack = (event) => {
|
|
||||||
console.log("Received track event", event.streams);
|
|
||||||
video1.srcObject = event.streams[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
pc1.onicecandidate = ({ candidate }) => {
|
|
||||||
if (candidate) {
|
|
||||||
ws1.send(JSON.stringify({ type: 'ice-candidate', data: candidate }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws1.onopen = async () => {
|
|
||||||
pc1.addTransceiver('video', { direction: 'recvonly' });
|
|
||||||
pc1.addTransceiver('audio', { direction: 'recvonly' })
|
|
||||||
const offer = await pc1.createOffer();
|
|
||||||
await pc1.setLocalDescription(offer);
|
|
||||||
ws1.send(JSON.stringify({ type: 'offer', data: offer }));
|
|
||||||
}
|
|
||||||
|
|
||||||
ws1.onmessage = async (message) => {
|
|
||||||
const msg = JSON.parse(message.data);
|
|
||||||
if (msg.type === 'answer') {
|
|
||||||
await pc1.setRemoteDescription(msg.data);
|
|
||||||
}
|
|
||||||
else if (msg.type === 'ice-candidate') {
|
|
||||||
await pc1.addIceCandidate(msg.data);
|
|
||||||
}
|
|
||||||
};
|
|
23
src/ws.ts
23
src/ws.ts
@ -3,23 +3,18 @@ import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
|
|||||||
import * as ws from 'ws';
|
import * as ws from 'ws';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
|
const VIDEO_DEVICE = '/dev/dvb/adapter0/dvr0'; // Video source device
|
||||||
const WIDTH = 640; // Video width
|
const WIDTH = 640; // Video width
|
||||||
const HEIGHT = 480; // Video height
|
const HEIGHT = 480; // Video height
|
||||||
const FRAME_SIZE = WIDTH * HEIGHT * 1.5; // YUV420p frame size (460800 bytes)
|
const FRAME_SIZE = WIDTH * HEIGHT * 1.5; // YUV420p frame size (460800 bytes)
|
||||||
|
|
||||||
export default class TVWebSocket {
|
export default class TVWebSocket {
|
||||||
videoDevice: string;
|
|
||||||
public constructor(port: number, videoDevice) {
|
public constructor(port: number) {
|
||||||
this.videoDevice = videoDevice
|
|
||||||
const ffmpegProcess = this.startFFmpeg();
|
const ffmpegProcess = this.startFFmpeg();
|
||||||
const videoTrack = this.createVideoTrack(ffmpegProcess);
|
const videoTrack = this.createVideoTrack(ffmpegProcess);
|
||||||
const audioTrack = this.createAudioTrack(ffmpegProcess);
|
const audioTrack = this.createAudioTrack(ffmpegProcess);
|
||||||
|
|
||||||
|
|
||||||
ffmpegProcess.stdio[2].on('data',data=>{
|
|
||||||
// console.log("stdio[2] ",data.toString())
|
|
||||||
})
|
|
||||||
|
|
||||||
// WebSocket signaling server
|
// WebSocket signaling server
|
||||||
const wss = new ws.WebSocketServer({ port });
|
const wss = new ws.WebSocketServer({ port });
|
||||||
|
|
||||||
@ -67,7 +62,7 @@ export default class TVWebSocket {
|
|||||||
startFFmpeg = (): ChildProcessWithoutNullStreams => {
|
startFFmpeg = (): ChildProcessWithoutNullStreams => {
|
||||||
const p = spawn('ffmpeg', [
|
const p = spawn('ffmpeg', [
|
||||||
'-loglevel', 'debug',
|
'-loglevel', 'debug',
|
||||||
'-i', this.videoDevice,
|
'-i', VIDEO_DEVICE,
|
||||||
|
|
||||||
// Video
|
// Video
|
||||||
'-map', '0:v:0',
|
'-map', '0:v:0',
|
||||||
@ -75,13 +70,6 @@ export default class TVWebSocket {
|
|||||||
'-vcodec', 'rawvideo',
|
'-vcodec', 'rawvideo',
|
||||||
'-pix_fmt', 'yuv420p',
|
'-pix_fmt', 'yuv420p',
|
||||||
'-f', 'rawvideo',
|
'-f', 'rawvideo',
|
||||||
|
|
||||||
//quality
|
|
||||||
'-fflags', '+discardcorrupt',
|
|
||||||
'-err_detect', 'ignore_err',
|
|
||||||
'-analyzeduration', '100M',
|
|
||||||
'-probesize', '100M',
|
|
||||||
|
|
||||||
'pipe:3',
|
'pipe:3',
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
@ -121,9 +109,6 @@ export default class TVWebSocket {
|
|||||||
// Start FFmpeg and pipe video frames to the source
|
// Start FFmpeg and pipe video frames to the source
|
||||||
videoStream.on('data', (chunk: Buffer) => {
|
videoStream.on('data', (chunk: Buffer) => {
|
||||||
videoBuffer = Buffer.concat([videoBuffer, chunk]);
|
videoBuffer = Buffer.concat([videoBuffer, chunk]);
|
||||||
if (videoBuffer.length > FRAME_SIZE * 2) {
|
|
||||||
console.warn('Video buffer overrun — possible freeze trigger');
|
|
||||||
}
|
|
||||||
while (videoBuffer.length >= FRAME_SIZE) {
|
while (videoBuffer.length >= FRAME_SIZE) {
|
||||||
const frameData = videoBuffer.slice(0, FRAME_SIZE);
|
const frameData = videoBuffer.slice(0, FRAME_SIZE);
|
||||||
videoBuffer = videoBuffer.slice(FRAME_SIZE);
|
videoBuffer = videoBuffer.slice(FRAME_SIZE);
|
||||||
|
164
src/zap.ts
164
src/zap.ts
@ -1,164 +0,0 @@
|
|||||||
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
export interface IZap {
|
|
||||||
process: ChildProcessWithoutNullStreams | null,
|
|
||||||
channel: string,
|
|
||||||
adapter: 0 | 1
|
|
||||||
strength: {
|
|
||||||
signal: string;
|
|
||||||
cn: string;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Zap {
|
|
||||||
private zap0: IZap;
|
|
||||||
private zap1: IZap;
|
|
||||||
private channelNameList: string[];
|
|
||||||
private fileName: string;
|
|
||||||
|
|
||||||
private regex = /Signal=\s*(-?\d+(\.\d+)?dBm)\s+C\/N=\s*(\d+(\.\d+)?dB)/;
|
|
||||||
|
|
||||||
|
|
||||||
public constructor(fileName = "dvb_channel.conf", channel = "ION") {
|
|
||||||
const zap0: IZap = {
|
|
||||||
process: null,
|
|
||||||
channel,
|
|
||||||
adapter: 0,
|
|
||||||
strength: {
|
|
||||||
signal: "None",
|
|
||||||
cn: "None"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const zap1: IZap = {
|
|
||||||
process: null,
|
|
||||||
channel,
|
|
||||||
adapter: 1,
|
|
||||||
strength: {
|
|
||||||
signal: "None",
|
|
||||||
cn: "None"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public getChannels(): string[] {
|
|
||||||
return this.channelNameList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getSignal(adapter: number) {
|
|
||||||
if(adapter == 0){
|
|
||||||
return this.zap0.strength;
|
|
||||||
}
|
|
||||||
else if (adapter == 1){
|
|
||||||
return this.zap1.strength;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return {signal: 'N/A', cn: 'N/A'}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private nextChannel(channel: string) :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) :number{
|
|
||||||
return ((n % m) + m) % m;
|
|
||||||
}
|
|
||||||
|
|
||||||
private previousChannel(channel: string):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);
|
|
||||||
const match = output.match(this.regex);
|
|
||||||
zap.channel = verifiedChannel;
|
|
||||||
zap.strength = {
|
|
||||||
signal: match[1],
|
|
||||||
cn: match[3]
|
|
||||||
}
|
|
||||||
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