Compare commits

..

No commits in common. "main" and "dev" have entirely different histories.
main ... dev

10 changed files with 86 additions and 150 deletions

View File

@ -1,8 +1,7 @@
FOX 12:207028615:8VSB:49:52:3
ION:521028615:8VSB:49:52:3 ION:521028615:8VSB:49:52:3
KATU:533028615:8VSB:49:52:3 KATU:533028615:8VSB:49:52:3
KOIN-HD:539028615:8VSB:49:52:3 KOIN-HD:539028615:8VSB:49:52:3
KGW:545028615:8VSB:49:52:3 KGW:545028615:8VSB:49:52:3
KJYY-LD:563028615:8VSB:49:52:1
KBLN-DT:575028615:8VSB:49:52:1 KBLN-DT:575028615:8VSB:49:52:1
TBN HD:581028615:8VSB:49:52:3 TBN HD:581028615:8VSB:49:52:3
KUNP-LD:593028615:8VSB:49:52:3

View File

@ -6,7 +6,7 @@
}, },
"scripts": { "scripts": {
"build:scss": "npx sass src/static/css:dist/static/css", "build:scss": "npx sass src/static/css:dist/static/css",
"copy:html": "cp src/static/*.* dist/static", "copy:html": "cp src/static/index.html dist/static",
"build:js:fe": "npx tsc src/static/js/*.ts --outDir dist/static/js", "build:js:fe": "npx tsc src/static/js/*.ts --outDir dist/static/js",
"build:js:be": "npx tsc --skipLibCheck src/*.ts --outDir dist", "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", "build": "npm run build:js:fe && npm run build:js:be && npm run build:scss && npm run copy:html",
@ -17,6 +17,6 @@
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/ws": "^8.5.13", "@types/ws": "^8.5.13",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.8.2"
} }
} }

View File

@ -8,7 +8,7 @@ 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[]) { public constructor(port: number, root: string, tune: (ch: string, adp?: number) => void, getChannels: ()=>string[], getSignal: (adapter:number)=>object) {
this.port = port; this.port = port;
this.root = root; this.root = root;
this.httpServer = http.createServer((req, res) => { this.httpServer = http.createServer((req, res) => {
@ -27,6 +27,11 @@ export default class HttpServer {
body = JSON.stringify(getChannels()); body = JSON.stringify(getChannels());
status = 200; status = 200;
break; break;
case "signal":
const adapter = parseInt(url.searchParams.get('adapter'));
body = JSON.stringify(getSignal(adapter));
status = 200;
break;
} }
break; break;

View File

@ -20,17 +20,16 @@ const tune = (reqChannel: string, reqAdapter?: number) => {
}); });
} }
const getChannels = zap.getChannels.bind(zap) const getChannels = () =>
const getMetaData0 = zap.getMetaData.bind(zap,0) zap.getChannels();
const getMetaData1 = zap.getMetaData.bind(zap,1)
const getSignal = (adapter: number) =>
zap.getSignal(adapter)
const httpServer = new HttpServer(HTTP_PORT, STATIC_ROOT, tune, getChannels); const httpServer = new HttpServer(HTTP_PORT, STATIC_ROOT, tune, getChannels, getSignal);
const tvWebSocket0 = new TVWebSocket(WS_PORT, TV_DEV_0, getMetaData0); const tvWebSocket0 = new TVWebSocket(WS_PORT, TV_DEV_0);
const tvWebSocket1 = new TVWebSocket(WS_PORT + 1, TV_DEV_1, getMetaData1); const tvWebSocket1 = new TVWebSocket(WS_PORT + 1, TV_DEV_1);
httpServer.start(); httpServer.start();
tune('ION',0);
tune("KGW",1);
process.stdin.setEncoding("utf8"); process.stdin.setEncoding("utf8");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<title>PDX Airwave TV</title> <title>WebRTC Stream</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="css/index.css" /> <link rel="stylesheet" href="css/index.css" />
@ -11,23 +11,24 @@
<body> <body>
<header></header> <header></header>
<main> <main>
<h1>Portland Airwave Television</h1> <h1>Video streams</h1>
<h2>WebRTC</h2>
<div class="content"> <div class="content">
<div class="player"> <div class="player">
<h2 id="station-0">N/A</h2> <video id="video0" autoplay playsinline controls></video>
<video id="video-0" autoplay playsinline controls></video>
<div id="channel-container-0" class="channel-group"></div> <div id="channel-container-0" class="channel-group"></div>
<button onClick="tune(0)">Tune</button> <button onClick="tune(0)">Tune</button>
<h3 id="signal-0"></h3> <p id="signal-0">N/A</p>
<button onClick="getSignal(0)">Get Signal</button>
</div> </div>
<div class="player"> <div class="player">
<h2 id="station-1">N/A</h2> <video id="video1" autoplay playsinline controls></video>
<video id="video-1" autoplay playsinline controls></video>
<div id="channel-container-1" class="channel-group"></div> <div id="channel-container-1" class="channel-group"></div>
<button onClick="tune(1)">Tune</button> <button onClick="tune(1)">Tune</button>
<h3 id="signal-1"></h3> <p id="signal-1">N/A</p>
<button onClick="getSignal(1)">Get Signal</button>
</div> </div>
</main> </main>
</div> </div>

View File

@ -46,4 +46,16 @@ const tune = (adapter = 0) => {
} }
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); populateChannels(PLAYERS);

View File

@ -13,40 +13,10 @@ const config = {
} }
] ]
}; };
const pc0 = new RTCPeerConnection(config);
const pc1 = new RTCPeerConnection(config);
interface IData { const video0 = document.getElementById('video0') as HTMLVideoElement;
channel: string, const video1 = document.getElementById('video1') as HTMLVideoElement;
signal: number,
cn: number
}
const pc0 = new RTCPeerConnection(isSecure ? config : {});
const pc1 = new RTCPeerConnection(isSecure ? config : {});
const video0 = document.getElementById('video-0') as HTMLVideoElement;
const video1 = document.getElementById('video-1') as HTMLVideoElement;
const station0 = document.getElementById('station-0') as HTMLHeadElement;
const station1 = document.getElementById('station-1') as HTMLHeadElement;
const signal0 = document.getElementById('signal-0') as HTMLHeadElement;
const signal1 = document.getElementById('signal-1') as HTMLHeadElement;
// 0
const dataChannel0 = pc0.createDataChannel('meta');
console.log("📡 Data channel created by client");
dataChannel0.onopen = () => {
console.log('📬 Client: Data channel opened');
};
dataChannel0.onmessage = (event) => {
const json = JSON.parse(event.data) as IData;
station0.textContent = json.channel;
signal0.textContent = `Signal: ${json.signal}\tC/N: ${json.cn}`
};
dataChannel0.onclose = () => {
console.log("📴 Client: Data channel closed");
};
// 0 // 0
pc0.ontrack = (event) => { pc0.ontrack = (event) => {
@ -60,23 +30,12 @@ pc0.onicecandidate = ({ candidate }) => {
} }
}; };
const restartIce = (pc, ws) => {
setInterval(async () => {
const offer = await pc.createOffer({ iceRestart: true });
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({ type: 'offer', data: offer }));
}, 50000)
}
ws0.onopen = async () => { ws0.onopen = async () => {
pc0.addTransceiver('video', { direction: 'recvonly' }); pc0.addTransceiver('video', { direction: 'recvonly' });
pc0.addTransceiver('audio', { direction: 'recvonly' }) pc0.addTransceiver('audio', { direction: 'recvonly' })
const offer = await pc0.createOffer(); const offer = await pc0.createOffer();
await pc0.setLocalDescription(offer); await pc0.setLocalDescription(offer);
ws0.send(JSON.stringify({ type: 'offer', data: offer })); ws0.send(JSON.stringify({ type: 'offer', data: offer }));
if (isSecure) {
restartIce(pc0,ws0);
}
} }
ws0.onmessage = async (message) => { ws0.onmessage = async (message) => {
@ -90,24 +49,6 @@ ws0.onmessage = async (message) => {
}; };
// 1 // 1
const dataChannel1 = pc1.createDataChannel('meta');
console.log("📡 Data channel created by client");
dataChannel1.onopen = () => {
console.log('📬 Client: Data channel opened');
};
dataChannel1.onmessage = (event) => {
const json = JSON.parse(event.data) as IData;
station1.textContent = json.channel;
signal1.textContent = `Signal: ${json.signal}\tC/N: ${json.cn}`
};
dataChannel1.onclose = () => {
console.log("📴 Client: Data channel closed");
};
pc1.ontrack = (event) => { pc1.ontrack = (event) => {
console.log("Received track event", event.streams); console.log("Received track event", event.streams);
video1.srcObject = event.streams[0]; video1.srcObject = event.streams[0];
@ -125,9 +66,6 @@ ws1.onopen = async () => {
const offer = await pc1.createOffer(); const offer = await pc1.createOffer();
await pc1.setLocalDescription(offer); await pc1.setLocalDescription(offer);
ws1.send(JSON.stringify({ type: 'offer', data: offer })); ws1.send(JSON.stringify({ type: 'offer', data: offer }));
if (isSecure) {
restartIce(pc1,ws1);
}
} }
ws1.onmessage = async (message) => { ws1.onmessage = async (message) => {

View File

@ -4,12 +4,12 @@ import * as ws from 'ws';
// Constants // Constants
const WIDTH = 640; // Video width const WIDTH = 640; // Video width
const HEIGHT = 360; // 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; videoDevice: string;
public constructor(port: number, videoDevice, getMetaData: ()=>any) { public constructor(port: number, videoDevice) {
this.videoDevice = videoDevice this.videoDevice = videoDevice
const ffmpegProcess = this.startFFmpeg(); const ffmpegProcess = this.startFFmpeg();
const videoTrack = this.createVideoTrack(ffmpegProcess); const videoTrack = this.createVideoTrack(ffmpegProcess);
@ -25,29 +25,7 @@ export default class TVWebSocket {
wss.on('connection', async (ws: ws.WebSocket) => { wss.on('connection', async (ws: ws.WebSocket) => {
const peerConnection: RTCPeerConnection = this.createPeerConnection(videoTrack, audioTrack); const peerConnection: RTCPeerConnection = this.createPeerConnection(videoTrack, audioTrack);
peerConnection.ondatachannel = (event) => {
const dataChannel = event.channel; // This is the data channel created by the client
dataChannel.onopen = () => {
console.log('📬 Server: Data channel opened');
// Now you can send data through the channel
setInterval(() => {
if (dataChannel.readyState === 'open') {
const metaData = getMetaData(); // Example function to fetch data
dataChannel.send(JSON.stringify(metaData)); // Send data to the client
}
}, 1000);
};
dataChannel.onmessage = (event) => {
console.log("📦 Server received message:", event.data);
};
dataChannel.onclose = () => {
console.log("📴 Server: Data channel closed");
};
}
ws.on('message', async (message: Buffer) => { ws.on('message', async (message: Buffer) => {
const { type, data } = JSON.parse(message.toString()); const { type, data } = JSON.parse(message.toString());
@ -88,26 +66,21 @@ export default class TVWebSocket {
// Function to start FFmpeg and capture raw video // Function to start FFmpeg and capture raw video
startFFmpeg = (): ChildProcessWithoutNullStreams => { startFFmpeg = (): ChildProcessWithoutNullStreams => {
const p = spawn('ffmpeg', [ const p = spawn('ffmpeg', [
// '-loglevel', 'debug', '-loglevel', 'debug',
'-i', this.videoDevice, '-i', this.videoDevice,
'-framerate' ,'59.94',
// Video // Video
'-map', '0:v:0', '-map', '0:v:0',
'-r', '30', '-vf', `scale=${WIDTH}:${HEIGHT}`,
'-vf', `scale=${WIDTH}:${HEIGHT}:flags=fast_bilinear`,
'-vcodec', 'rawvideo', '-vcodec', 'rawvideo',
'-preset', 'ultrafast',
'-b:v' ,'1M',
'-pix_fmt', 'yuv420p', '-pix_fmt', 'yuv420p',
'-f', 'rawvideo', '-f', 'rawvideo',
// '-threads', '1',
//quality //quality
'-fflags', '+discardcorrupt', '-fflags', '+discardcorrupt',
'-err_detect', 'ignore_err', '-err_detect', 'ignore_err',
'-analyzeduration', '500k', '-analyzeduration', '100M',
'-probesize', '500k', '-probesize', '100M',
'pipe:3', 'pipe:3',

View File

@ -5,10 +5,10 @@ export interface IZap {
process: ChildProcessWithoutNullStreams | null, process: ChildProcessWithoutNullStreams | null,
channel: string, channel: string,
adapter: 0 | 1 adapter: 0 | 1
strength: {
signal: string; signal: string;
cn: string; cn: string;
},
} }
export default class Zap { export default class Zap {
@ -23,21 +23,21 @@ export default class Zap {
public constructor(fileName = "dvb_channel.conf", channel = "ION") { public constructor(fileName = "dvb_channel.conf", channel = "ION") {
const zap0: IZap = { const zap0: IZap = {
process: null, process: null,
channel: "None", channel,
adapter: 0, adapter: 0,
strength: {
signal: "None", signal: "None",
cn: "None" cn: "None"
}
} }
const zap1: IZap = { const zap1: IZap = {
process: null, process: null,
channel: "None", channel,
adapter: 1, adapter: 1,
strength: {
signal: "None", signal: "None",
cn: "None" cn: "None"
}
} }
this.zap0 = zap0; this.zap0 = zap0;
this.zap1 = zap1; this.zap1 = zap1;
@ -53,9 +53,16 @@ export default class Zap {
return this.channelNameList; return this.channelNameList;
} }
public getMetaData(adapter: 0 | 1) { public getSignal(adapter: number) {
const { channel, signal, cn } = adapter === 0 ? this.zap0 : this.zap1 if(adapter == 0){
return { channel, signal, cn } 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 { private nextChannel(channel: string) :string {
@ -125,14 +132,16 @@ export default class Zap {
zap.process.stderr.on("data", (data) => { zap.process.stderr.on("data", (data) => {
const output: string = data.toString(); const output = data.toString();
if (/Lock/.test(output)) { if (/Lock/.test(output)) {
clearTimeout(lockTimer); clearTimeout(lockTimer);
const match = output.match(this.regex); const match = output.match(this.regex);
zap.channel = verifiedChannel; zap.channel = verifiedChannel;
zap.signal = match?.[1], zap.strength = {
zap.cn = match?.[3] signal: match[1],
cn: match[3]
}
resolve(zap); resolve(zap);
} }