moved server to C
This commit is contained in:
parent
6256b4ecd6
commit
f6b53c3955
3 changed files with 332 additions and 150 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
app.png
|
assets/
|
||||||
logo.png
|
build/
|
||||||
|
airlink.py
|
||||||
|
|
|
||||||
329
airlink.c
Normal file
329
airlink.c
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
#define PORT_VIDEO 5000
|
||||||
|
#define PORT_AUDIO 5001
|
||||||
|
#define DEVICE_VIDEO "/dev/video20"
|
||||||
|
#define SINK_AUDIO "AirLink_Sink"
|
||||||
|
#define SOURCE_MIC "AirLink_Mic"
|
||||||
|
#define BUFFER_SIZE 65535
|
||||||
|
|
||||||
|
// --- Globals ---
|
||||||
|
pid_t pid_video_process = 0;
|
||||||
|
pid_t pid_audio_process = 0;
|
||||||
|
int pipe_video_fd = -1;
|
||||||
|
int pipe_audio_fd = -1;
|
||||||
|
int socket_video = -1;
|
||||||
|
int socket_audio = -1;
|
||||||
|
int is_running = 1;
|
||||||
|
|
||||||
|
// Stats tracking
|
||||||
|
typedef struct {
|
||||||
|
long long bytes_video;
|
||||||
|
long long bytes_audio;
|
||||||
|
int packets_video;
|
||||||
|
int packets_audio;
|
||||||
|
time_t last_log_time;
|
||||||
|
} TrafficStats;
|
||||||
|
|
||||||
|
TrafficStats stats = {0, 0, 0, 0, 0};
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
void kill_process(pid_t *pid) {
|
||||||
|
if (*pid > 0) {
|
||||||
|
kill(*pid, SIGTERM);
|
||||||
|
waitpid(*pid, NULL, 0); // Wait to prevent zombies
|
||||||
|
*pid = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a black screen on the virtual camera (used for Pause/Stop)
|
||||||
|
void render_black_screen() {
|
||||||
|
kill_process(&pid_video_process);
|
||||||
|
if (pipe_video_fd != -1) { close(pipe_video_fd); pipe_video_fd = -1; }
|
||||||
|
|
||||||
|
printf("[STATE] Rendering black screen (Privacy Mode)\n");
|
||||||
|
// We run this as a blocking system call because it's a short "state change" operation
|
||||||
|
char cmd[512];
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"ffmpeg -hide_banner -loglevel error -f lavfi -i color=black:s=1280x720 "
|
||||||
|
"-pix_fmt yuyv422 -frames:v 5 -f v4l2 %s > /dev/null 2>&1",
|
||||||
|
DEVICE_VIDEO);
|
||||||
|
system(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanup(int sig) {
|
||||||
|
is_running = 0;
|
||||||
|
printf("\n[INFO] Shutting down services...\n");
|
||||||
|
|
||||||
|
kill_process(&pid_video_process);
|
||||||
|
kill_process(&pid_audio_process);
|
||||||
|
|
||||||
|
// Clean up PulseAudio modules
|
||||||
|
char cmd[256];
|
||||||
|
snprintf(cmd, sizeof(cmd), "pactl unload-module %s > /dev/null 2>&1", SINK_AUDIO);
|
||||||
|
system(cmd);
|
||||||
|
system("pactl unload-module AirLink_Mic > /dev/null 2>&1");
|
||||||
|
|
||||||
|
// Stop Avahi
|
||||||
|
system("pkill -f 'avahi-publish-service -s AirLink'");
|
||||||
|
|
||||||
|
// Final Black Screen
|
||||||
|
render_black_screen();
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawns a process and returns the Write FD for its STDIN
|
||||||
|
pid_t spawn_worker(char *const cmd[], int *fd_in) {
|
||||||
|
int pipefd[2];
|
||||||
|
if (pipe(pipefd) == -1) return -1;
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
// Child
|
||||||
|
close(pipefd[1]); // Close write end
|
||||||
|
dup2(pipefd[0], STDIN_FILENO); // Redirect pipe read to STDIN
|
||||||
|
close(pipefd[0]);
|
||||||
|
|
||||||
|
// Close other file descriptors to be safe
|
||||||
|
for (int i = 3; i < 256; i++) close(i);
|
||||||
|
|
||||||
|
execvp(cmd[0], cmd);
|
||||||
|
exit(1); // Should not reach here
|
||||||
|
} else if (pid > 0) {
|
||||||
|
// Parent
|
||||||
|
close(pipefd[0]); // Close read end
|
||||||
|
*fd_in = pipefd[1]; // Keep write end
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void initialize_system() {
|
||||||
|
printf("[INIT] Configuring Kernel Modules and Audio Subsystem...\n");
|
||||||
|
|
||||||
|
// Video: Reload v4l2loopback
|
||||||
|
system("sudo modprobe -r v4l2loopback 2>/dev/null");
|
||||||
|
char v4l2_cmd[256];
|
||||||
|
snprintf(v4l2_cmd, sizeof(v4l2_cmd),
|
||||||
|
"sudo modprobe v4l2loopback video_nr=20 card_label=AirLink_Cam exclusive_caps=1");
|
||||||
|
system(v4l2_cmd);
|
||||||
|
|
||||||
|
// Audio: Null Sink
|
||||||
|
char pa_cmd[512];
|
||||||
|
snprintf(pa_cmd, sizeof(pa_cmd),
|
||||||
|
"pactl load-module module-null-sink sink_name=%s "
|
||||||
|
"sink_properties=device.description='AirLink_Audio' rate=48000 channels=1 >/dev/null 2>&1",
|
||||||
|
SINK_AUDIO);
|
||||||
|
system(pa_cmd);
|
||||||
|
|
||||||
|
// Audio: Remap Source (Virtual Mic)
|
||||||
|
snprintf(pa_cmd, sizeof(pa_cmd),
|
||||||
|
"pactl load-module module-remap-source source_name=%s "
|
||||||
|
"master=%s.monitor source_properties=device.description='AirLink_Mic' channels=1 >/dev/null 2>&1",
|
||||||
|
SOURCE_MIC, SINK_AUDIO);
|
||||||
|
system(pa_cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_video_stream() {
|
||||||
|
// Check if already running
|
||||||
|
if (pid_video_process > 0 && waitpid(pid_video_process, NULL, WNOHANG) == 0) return;
|
||||||
|
|
||||||
|
// FFmpeg command to read raw H264 from stdin and write to V4L2
|
||||||
|
char *cmd[] = {
|
||||||
|
"ffmpeg",
|
||||||
|
"-fflags", "nobuffer",
|
||||||
|
"-flags", "low_delay",
|
||||||
|
"-analyzeduration", "0",
|
||||||
|
"-probesize", "32",
|
||||||
|
"-hide_banner",
|
||||||
|
"-loglevel", "quiet",
|
||||||
|
"-f", "h264",
|
||||||
|
"-i", "pipe:0",
|
||||||
|
"-vf", "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2",
|
||||||
|
"-vsync", "0",
|
||||||
|
"-copyts",
|
||||||
|
"-pix_fmt", "yuyv422",
|
||||||
|
"-f", "v4l2", DEVICE_VIDEO,
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
pid_video_process = spawn_worker(cmd, &pipe_video_fd);
|
||||||
|
signal(SIGPIPE, SIG_IGN); // Prevent crash if ffmpeg dies
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_audio_stream() {
|
||||||
|
if (pid_audio_process > 0 && waitpid(pid_audio_process, NULL, WNOHANG) == 0) return;
|
||||||
|
|
||||||
|
// Use a shell to pipe ffmpeg decoder directly to pacat
|
||||||
|
// Input: Raw PCM or Encoded Audio -> FFmpeg -> Raw PCM -> Pacat
|
||||||
|
// Based on Python: Raw s16le incoming -> Pacat
|
||||||
|
// If incoming is already raw PCM (s16le, 48k, mono), we can skip ffmpeg or just use it as pass-through.
|
||||||
|
// We will use the shell wrapper for simplicity of piping.
|
||||||
|
|
||||||
|
int pipefd[2];
|
||||||
|
if (pipe(pipefd) == -1) return;
|
||||||
|
|
||||||
|
pid_audio_process = fork();
|
||||||
|
if (pid_audio_process == 0) {
|
||||||
|
close(pipefd[1]);
|
||||||
|
dup2(pipefd[0], STDIN_FILENO);
|
||||||
|
|
||||||
|
// Command: Decode (ensure format) -> Play
|
||||||
|
// We assume the Android app sends raw PCM (s16le) as per your Python script audio logic
|
||||||
|
execl("/bin/sh", "sh", "-c",
|
||||||
|
"ffmpeg -hide_banner -loglevel quiet -fflags nobuffer -f s16le -ar 48000 -ac 1 -i pipe:0 "
|
||||||
|
"-f s16le -ar 48000 -ac 1 pipe:1 | "
|
||||||
|
"pacat --playback --device=AirLink_Sink --rate=48000 --channels=1 --format=s16le --latency-msec=20",
|
||||||
|
NULL);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
close(pipefd[0]);
|
||||||
|
pipe_audio_fd = pipefd[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_stats() {
|
||||||
|
time_t now = time(NULL);
|
||||||
|
if (stats.last_log_time == 0) stats.last_log_time = now;
|
||||||
|
|
||||||
|
double delta = difftime(now, stats.last_log_time);
|
||||||
|
if (delta >= 1.0) {
|
||||||
|
double video_mbps = (stats.bytes_video * 8.0) / (1000.0 * 1000.0) / delta;
|
||||||
|
double audio_kbps = (stats.bytes_audio * 8.0) / 1000.0 / delta;
|
||||||
|
|
||||||
|
printf("[STATS] Video: %6.2f Mbps (%d pps) | Audio: %6.2f Kbps (%d pps)\n",
|
||||||
|
video_mbps, stats.packets_video,
|
||||||
|
audio_kbps, stats.packets_audio);
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
stats.bytes_video = 0;
|
||||||
|
stats.bytes_audio = 0;
|
||||||
|
stats.packets_video = 0;
|
||||||
|
stats.packets_audio = 0;
|
||||||
|
stats.last_log_time = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
signal(SIGINT, cleanup);
|
||||||
|
|
||||||
|
initialize_system();
|
||||||
|
|
||||||
|
// Start Avahi Advertising (Background)
|
||||||
|
system("avahi-publish-service -s AirLink _procam._udp 5000 &");
|
||||||
|
|
||||||
|
// Create UDP Sockets
|
||||||
|
socket_video = socket(AF_INET, SOCK_DGRAM, 0);
|
||||||
|
socket_audio = socket(AF_INET, SOCK_DGRAM, 0);
|
||||||
|
|
||||||
|
// Set large kernel buffers to prevent drops
|
||||||
|
int rcvbuf_video = 4 * 1024 * 1024; // 4MB
|
||||||
|
int rcvbuf_audio = 512 * 1024; // 512KB
|
||||||
|
setsockopt(socket_video, SOL_SOCKET, SO_RCVBUF, &rcvbuf_video, sizeof(rcvbuf_video));
|
||||||
|
setsockopt(socket_audio, SOL_SOCKET, SO_RCVBUF, &rcvbuf_audio, sizeof(rcvbuf_audio));
|
||||||
|
|
||||||
|
struct sockaddr_in addr;
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||||
|
|
||||||
|
addr.sin_port = htons(PORT_VIDEO);
|
||||||
|
if (bind(socket_video, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||||
|
perror("Bind Video Failed"); return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
addr.sin_port = htons(PORT_AUDIO);
|
||||||
|
if (bind(socket_audio, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||||
|
perror("Bind Audio Failed"); return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("[READY] Server listening on Ports %d (Video) & %d (Audio)\n", PORT_VIDEO, PORT_AUDIO);
|
||||||
|
|
||||||
|
start_video_stream();
|
||||||
|
start_audio_stream();
|
||||||
|
|
||||||
|
unsigned char buffer[BUFFER_SIZE];
|
||||||
|
fd_set readfds;
|
||||||
|
int max_fd = (socket_video > socket_audio ? socket_video : socket_audio) + 1;
|
||||||
|
|
||||||
|
struct sockaddr_in src_addr;
|
||||||
|
socklen_t src_len;
|
||||||
|
|
||||||
|
while (is_running) {
|
||||||
|
print_stats();
|
||||||
|
|
||||||
|
// Check process health and restart if needed
|
||||||
|
if (waitpid(pid_video_process, NULL, WNOHANG) > 0) start_video_stream();
|
||||||
|
if (waitpid(pid_audio_process, NULL, WNOHANG) > 0) start_audio_stream();
|
||||||
|
|
||||||
|
FD_ZERO(&readfds);
|
||||||
|
FD_SET(socket_video, &readfds);
|
||||||
|
FD_SET(socket_audio, &readfds);
|
||||||
|
|
||||||
|
struct timeval tv = {1, 0}; // 1 second timeout (allows stats to print even if idle)
|
||||||
|
int activity = select(max_fd, &readfds, NULL, NULL, &tv);
|
||||||
|
|
||||||
|
if (activity < 0 && errno != EINTR) break;
|
||||||
|
|
||||||
|
// --- Handle Video & Control ---
|
||||||
|
if (FD_ISSET(socket_video, &readfds)) {
|
||||||
|
src_len = sizeof(src_addr);
|
||||||
|
int n = recvfrom(socket_video, buffer, sizeof(buffer), 0, (struct sockaddr*)&src_addr, &src_len);
|
||||||
|
|
||||||
|
if (n > 0) {
|
||||||
|
// Control Packet Handling
|
||||||
|
if (n < 16) {
|
||||||
|
if (memcmp(buffer, "PING", 4) == 0) {
|
||||||
|
sendto(socket_video, "PONG", 4, 0, (struct sockaddr*)&src_addr, src_len);
|
||||||
|
}
|
||||||
|
else if (memcmp(buffer, "HEARTBEAT", 9) == 0) {
|
||||||
|
sendto(socket_video, "ACK", 3, 0, (struct sockaddr*)&src_addr, src_len);
|
||||||
|
}
|
||||||
|
else if (memcmp(buffer, "PAUSE", 5) == 0 || memcmp(buffer, "BYE", 3) == 0) {
|
||||||
|
render_black_screen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Video Data
|
||||||
|
if (pid_video_process == 0) start_video_stream();
|
||||||
|
write(pipe_video_fd, buffer, n);
|
||||||
|
|
||||||
|
// Update Stats
|
||||||
|
stats.bytes_video += n;
|
||||||
|
stats.packets_video++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Handle Audio ---
|
||||||
|
if (FD_ISSET(socket_audio, &readfds)) {
|
||||||
|
int n = recvfrom(socket_audio, buffer, sizeof(buffer), 0, NULL, NULL);
|
||||||
|
if (n > 0) {
|
||||||
|
// Audio Data (ignore control packets on this port usually)
|
||||||
|
if (n > 16) {
|
||||||
|
write(pipe_audio_fd, buffer, n);
|
||||||
|
|
||||||
|
// Update Stats
|
||||||
|
stats.bytes_audio += n;
|
||||||
|
stats.packets_audio++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
148
server.py
148
server.py
|
|
@ -1,148 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
import socket, subprocess, sys, time, signal, threading, select, math
|
|
||||||
from zeroconf import ServiceInfo, Zeroconf
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
CONF = {
|
|
||||||
'video_port': 5000, 'audio_port': 5001,
|
|
||||||
'video_dev': '/dev/video20', 'audio_sink': 'AirLink_Sink', 'audio_src': 'AirLink_Mic'
|
|
||||||
}
|
|
||||||
|
|
||||||
class AirLinkServer:
|
|
||||||
def __init__(self):
|
|
||||||
self.ip = self.get_lan_ip()
|
|
||||||
self.running = False
|
|
||||||
self.procs = {'video': None, 'audio': None, 'ffmpeg_audio': None}
|
|
||||||
self.sockets = {'video': None, 'audio': None}
|
|
||||||
self.stats = {'v_bytes': 0, 'a_bytes': 0, 'v_pkts': 0, 'a_pkts': 0, 'last_time': time.time()}
|
|
||||||
self.zc = None
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
def get_lan_ip(self):
|
|
||||||
"""Finds LAN IP by prioritizing physical interfaces over VPN/Tun interfaces."""
|
|
||||||
try:
|
|
||||||
# List global IPv4 addresses, prioritize eth/wlan
|
|
||||||
cmd = "ip -4 -o addr show scope global | awk '{print $2,$4}'"
|
|
||||||
out = subprocess.check_output(cmd, shell=True).decode().strip().split('\n')
|
|
||||||
candidates = []
|
|
||||||
for line in out:
|
|
||||||
if not line: continue
|
|
||||||
iface, ip = line.split()[0], line.split()[1].split('/')[0]
|
|
||||||
if iface.startswith(('e', 'w')): return ip # Found physical interface
|
|
||||||
candidates.append(ip)
|
|
||||||
return candidates[0] if candidates else '127.0.0.1'
|
|
||||||
except:
|
|
||||||
# Fallback to standard socket connect
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
try: s.connect(("8.8.8.8", 80)); return s.getsockname()[0]
|
|
||||||
except: return '127.0.0.1'
|
|
||||||
finally: s.close()
|
|
||||||
|
|
||||||
def setup_system(self):
|
|
||||||
print(f"[INIT] Configuring system on {self.ip}...")
|
|
||||||
try:
|
|
||||||
subprocess.run("sudo modprobe -r v4l2loopback; sudo modprobe v4l2loopback video_nr=20 card_label=AirLink_Cam exclusive_caps=1", shell=True, check=True, stderr=subprocess.DEVNULL)
|
|
||||||
print(f"[INFO] Video Device: {CONF['video_dev']}")
|
|
||||||
|
|
||||||
subprocess.run(f"pactl load-module module-null-sink sink_name={CONF['audio_sink']} sink_properties=device.description='AirLink_Audio' rate=48000 channels=1", shell=True, stderr=subprocess.DEVNULL)
|
|
||||||
subprocess.run(f"pactl load-module module-remap-source source_name={CONF['audio_src']} master={CONF['audio_sink']}.monitor source_properties=device.description='AirLink_Mic' channels=1", shell=True, stderr=subprocess.DEVNULL)
|
|
||||||
print(f"[INFO] Audio Source: {CONF['audio_src']}")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[ERR] Setup failed: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def paint_black(self):
|
|
||||||
with self.lock:
|
|
||||||
if self.procs['video']:
|
|
||||||
self.procs['video'].terminate()
|
|
||||||
self.procs['video'] = None
|
|
||||||
try:
|
|
||||||
subprocess.run(['ffmpeg', '-hide_banner', '-loglevel', 'error', '-f', 'lavfi', '-i', 'color=black:s=1280x720', '-pix_fmt', 'yuyv422', '-frames:v', '5', '-f', 'v4l2', CONF['video_dev']], check=False)
|
|
||||||
print("[INFO] Video stream stopped (Black Screen)")
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
def start_pipeline(self, p_type):
|
|
||||||
with self.lock:
|
|
||||||
if p_type == 'video' and (not self.procs['video'] or self.procs['video'].poll() is not None):
|
|
||||||
self.procs['video'] = subprocess.Popen(
|
|
||||||
['ffmpeg', '-fflags', 'nobuffer', '-flags', 'low_delay', '-analyzeduration', '0', '-probesize', '32',
|
|
||||||
'-hide_banner', '-loglevel', 'fatal', '-f', 'h264', '-i', 'pipe:0',
|
|
||||||
'-vf', 'scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2',
|
|
||||||
'-vsync', '0', '-copyts', '-pix_fmt', 'yuyv422', '-f', 'v4l2', CONF['video_dev']],
|
|
||||||
stdin=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=0)
|
|
||||||
|
|
||||||
elif p_type == 'audio' and (not self.procs['audio'] or self.procs['audio'].poll() is not None):
|
|
||||||
self.procs['ffmpeg_audio'] = subprocess.Popen(
|
|
||||||
['ffmpeg', '-hide_banner', '-loglevel', 'fatal', '-fflags', 'nobuffer', '-f', 's16le', '-ar', '48000', '-ac', '1', '-i', 'pipe:0', '-f', 's16le', '-ar', '48000', '-ac', '1', 'pipe:1'],
|
|
||||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=0)
|
|
||||||
self.procs['audio'] = subprocess.Popen(
|
|
||||||
['pacat', '--playback', f"--device={CONF['audio_sink']}", '--rate=48000', '--channels=1', '--format=s16le', '--latency-msec=20'],
|
|
||||||
stdin=self.procs['ffmpeg_audio'].stdout, stderr=subprocess.DEVNULL, bufsize=0)
|
|
||||||
|
|
||||||
def log_stats(self):
|
|
||||||
now = time.time()
|
|
||||||
delta = now - self.stats['last_time']
|
|
||||||
if delta >= 1.0:
|
|
||||||
v_mbps = (self.stats['v_bytes'] * 8) / (1000 * 1000) / delta
|
|
||||||
a_kbps = (self.stats['a_bytes'] * 8) / 1000 / delta
|
|
||||||
print(f"[STATS] Video: {v_mbps:6.2f} Mbps ({self.stats['v_pkts']} pps) | Audio: {a_kbps:6.2f} kbps ({self.stats['a_pkts']} pps)")
|
|
||||||
self.stats = {'v_bytes': 0, 'a_bytes': 0, 'v_pkts': 0, 'a_pkts': 0, 'last_time': now}
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
if not self.setup_system(): return
|
|
||||||
self.running = True
|
|
||||||
|
|
||||||
# Discovery
|
|
||||||
info = ServiceInfo("_procam._udp.local.", "AirLink._procam._udp.local.", addresses=[socket.inet_aton(self.ip)], port=CONF['video_port'], properties={'version': '2.0'})
|
|
||||||
self.zc = Zeroconf(); self.zc.register_service(info)
|
|
||||||
|
|
||||||
# Sockets
|
|
||||||
for k, port, buf in [('video', CONF['video_port'], 4*1024*1024), ('audio', CONF['audio_port'], 512*1024)]:
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, buf)
|
|
||||||
s.bind(("0.0.0.0", port))
|
|
||||||
s.setblocking(0)
|
|
||||||
self.sockets[k] = s
|
|
||||||
|
|
||||||
self.start_pipeline('video'); self.start_pipeline('audio')
|
|
||||||
print(f"\n[READY] Server listening on {self.ip}:{CONF['video_port']}\n")
|
|
||||||
|
|
||||||
while self.running:
|
|
||||||
self.log_stats()
|
|
||||||
# Ensure audio is alive
|
|
||||||
if self.procs['audio'] and self.procs['audio'].poll() is not None: self.start_pipeline('audio')
|
|
||||||
|
|
||||||
ready, _, _ = select.select(list(self.sockets.values()), [], [], 0.1)
|
|
||||||
for sock in ready:
|
|
||||||
try:
|
|
||||||
data, addr = sock.recvfrom(65535)
|
|
||||||
|
|
||||||
# Control Signals
|
|
||||||
if len(data) < 16:
|
|
||||||
if data == b'PING': sock.sendto(b'PONG', addr); continue
|
|
||||||
if data == b'HEARTBEAT': sock.sendto(b'ACK', addr); continue
|
|
||||||
if data in [b'PAUSE', b'BYE']: self.paint_black(); continue
|
|
||||||
|
|
||||||
# Data Routing
|
|
||||||
if sock == self.sockets['video']:
|
|
||||||
self.stats['v_bytes'] += len(data); self.stats['v_pkts'] += 1
|
|
||||||
if not self.procs['video'] or self.procs['video'].poll() is not None: self.start_pipeline('video')
|
|
||||||
if self.procs['video']: self.procs['video'].stdin.write(data); self.procs['video'].stdin.flush()
|
|
||||||
|
|
||||||
elif sock == self.sockets['audio']:
|
|
||||||
self.stats['a_bytes'] += len(data); self.stats['a_pkts'] += 1
|
|
||||||
if self.procs['ffmpeg_audio']: self.procs['ffmpeg_audio'].stdin.write(data); self.procs['ffmpeg_audio'].stdin.flush()
|
|
||||||
except Exception: pass
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
self.running = False
|
|
||||||
self.paint_black()
|
|
||||||
if self.zc: self.zc.close()
|
|
||||||
for mod in [CONF['audio_sink'], CONF['audio_src']]: subprocess.run(f"pactl unload-module {mod}", shell=True, stderr=subprocess.DEVNULL)
|
|
||||||
print("\n[INFO] Cleanup complete. Exiting.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
server = AirLinkServer()
|
|
||||||
signal.signal(signal.SIGINT, lambda s,f: server.cleanup() or sys.exit(0))
|
|
||||||
server.run()
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue