Add python ASCII video (and audio) socket server and client

This commit is contained in:
iAmInActions 2024-11-05 23:46:55 +01:00
parent 1b758cb627
commit ebc593ffc9
2 changed files with 294 additions and 0 deletions

View File

@ -0,0 +1,140 @@
import socket
import sys
import os
import pyaudio
import threading
import argparse
import struct
import termios
import tty
# Global variable for volume control
volume = 1.0
def play_audio(audio_socket):
""" Function to continuously play audio data received from the server """
global volume
p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paUInt8, # 8-bit unsigned format
channels=1, # Mono audio
rate=8000, # 8000 Hz sample rate
output=True)
try:
while True:
audio_chunk = audio_socket.recv(1024)
if not audio_chunk:
break
# Adjust the volume of the audio chunk
adjusted_chunk = bytes(min(int(sample * volume), 255) for sample in audio_chunk)
stream.write(adjusted_chunk)
except Exception as e:
print(f"Error playing audio: {e}")
finally:
stream.stop_stream()
stream.close()
p.terminate()
def handle_key_input(video_socket, audio_socket):
""" Function to handle key input for quitting and volume control """
global volume
# Save the terminal settings
original_settings = termios.tcgetattr(sys.stdin)
tty.setcbreak(sys.stdin.fileno()) # Set the terminal to cbreak mode for unbuffered input
try:
while True:
key = sys.stdin.read(1) # Read a single character
if key.lower() == 'q': # Quit on 'q' or 'Q'
print("Quitting...")
video_socket.close()
audio_socket.close()
break
elif key == '+': # Increase volume on '+'
volume = min(volume + 0.1, 2.0) # Cap volume at 2.0
print(f"Volume increased to {volume}")
elif key == '-': # Decrease volume on '-'
volume = max(volume - 0.1, 0.0) # Floor volume at 0.0
print(f"Volume decreased to {volume}")
finally:
# Restore the terminal settings
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, original_settings)
def main():
parser = argparse.ArgumentParser(description="ASCII Video Client")
parser.add_argument("video_url", help="URL of the video to stream")
parser.add_argument("server_ip", help="IP address of the server")
parser.add_argument("port", type=int, help="Port number of the server")
parser.add_argument("contrast", type=float, nargs="?", default=1.0, help="Contrast adjustment (default: 1.0)")
parser.add_argument("brightness", type=float, nargs="?", default=0.0, help="Brightness adjustment (default: 0.0)")
parser.add_argument("-c", "--color", action="store_true", help="Enable color mode using ANSI color codes")
args = parser.parse_args()
# Get the terminal size
terminal_size = os.get_terminal_size()
width = terminal_size.columns
height = terminal_size.lines
# Connect to the server for video
try:
video_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
video_socket.connect((args.server_ip, args.port))
print("Connected to video server successfully.")
except Exception as e:
print(f"Failed to connect to video server: {e}")
return
# Send the video URL, contrast, brightness, color flag, and terminal size
color_mode = "1" if args.color else "0"
video_socket.send(f"{args.video_url} {args.contrast} {args.brightness} {color_mode} {width} {height}".encode())
# Connect to the server for audio
try:
audio_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
audio_socket.connect((args.server_ip, args.port + 1))
print("Connected to audio server successfully.")
except Exception as e:
print(f"Failed to connect to audio server: {e}")
return
# Start a separate thread for playing audio
audio_thread = threading.Thread(target=play_audio, args=(audio_socket,))
audio_thread.start()
# Handle key input in the main thread
key_thread = threading.Thread(target=handle_key_input, args=(video_socket, audio_socket))
key_thread.start()
try:
while True:
# Receive the frame size
frame_size_data = video_socket.recv(4)
if not frame_size_data:
break
frame_size = struct.unpack("I", frame_size_data)[0]
# Receive the full frame data
frame_data = b""
while len(frame_data) < frame_size:
chunk = video_socket.recv(frame_size - len(frame_data))
if not chunk:
print("Failed to receive full frame data. Connection may be closed.")
return
frame_data += chunk
# Clear the terminal and display the frame
os.system('clear' if os.name == 'posix' else 'cls')
print(frame_data.decode(), end="\r")
except Exception as e:
print(f"Error during video reception: {e}")
finally:
video_socket.close()
audio_socket.close()
audio_thread.join()
key_thread.join()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,154 @@
import socket
import subprocess
import numpy as np
from PIL import Image
import threading
import argparse
import struct
# Constants
FPS = 8
# ASCII_CHARS = '$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,"^. ' # For inverted terminals
ASCII_CHARS = ' .^",:;Il!i><~+_-?][}{1)(|\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$'
# ANSI color codes
ANSI_COLORS = [
"\033[38;5;16m", # Black
"\033[38;5;88m", # Dark Red
"\033[38;5;22m", # Dark Green
"\033[38;5;130m", # Brown
"\033[38;5;19m", # Dark Blue
"\033[38;5;54m", # Dark Magenta
"\033[38;5;58m", # Dark Cyan
"\033[38;5;244m", # Gray
"\033[38;5;250m", # Light Gray
"\033[38;5;196m", # Red
"\033[38;5;46m", # Green
"\033[38;5;226m", # Yellow
"\033[38;5;21m", # Blue
"\033[38;5;201m", # Magenta
"\033[38;5;51m", # Cyan
"\033[38;5;231m" # White
]
RESET_COLOR = "\033[0m"
def frame_to_ascii(frame, color_mode):
img = Image.fromarray(frame).convert("RGB").resize((frame.shape[1], frame.shape[0]))
ascii_frame = ""
for y in range(img.height):
for x in range(img.width):
r, g, b = img.getpixel((x, y))
gray = int(0.3 * r + 0.59 * g + 0.11 * b)
index = min(gray // 25, len(ASCII_CHARS) - 1) # Ensure the index is within bounds
char = ASCII_CHARS[index]
if color_mode:
color_index = (r // 51) * 36 + (g // 51) * 6 + (b // 51)
color_code = f"\033[38;5;{16 + color_index}m"
ascii_frame += f"{color_code}{char}{RESET_COLOR}"
else:
ascii_frame += char
ascii_frame += "\n"
return ascii_frame
def process_video(url, contrast, brightness, color_mode, width, height, video_conn, sync_event):
ffmpeg_command = [
"ffmpeg",
"-re",
"-i", url,
"-vf", f"scale={width}:{height},eq=contrast={contrast}:brightness={brightness}",
"-r", str(FPS),
"-f", "rawvideo",
"-pix_fmt", "rgb24",
"pipe:1"
]
video_process = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
frame_size = width * height * 3
try:
while True:
frame_bytes = video_process.stdout.read(frame_size)
if not frame_bytes:
break
frame = np.frombuffer(frame_bytes, np.uint8).reshape((height, width, 3))
ascii_frame = frame_to_ascii(frame, color_mode)
frame_data = ascii_frame.encode()
# Send the frame size followed by the frame data
video_conn.sendall(struct.pack("I", len(frame_data)) + frame_data)
sync_event.wait(1 / FPS)
finally:
video_process.stdout.close()
video_conn.close()
def process_audio(url, audio_conn, sync_event):
ffmpeg_audio_command = [
"ffmpeg",
"-re",
"-i", url,
"-f", "wav",
"-ar", "8000",
"-ac", "1",
"-acodec", "pcm_u8",
"pipe:1"
]
audio_process = subprocess.Popen(ffmpeg_audio_command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
try:
while True:
audio_chunk = audio_process.stdout.read(1024)
if not audio_chunk:
break
try:
audio_conn.sendall(audio_chunk)
except BrokenPipeError:
print("Audio connection closed by client.")
break
sync_event.set()
sync_event.clear()
finally:
audio_process.stdout.close()
audio_conn.close()
def handle_client(video_conn, audio_conn):
data = video_conn.recv(1024).decode().split()
url = data[0]
contrast = float(data[1])
brightness = float(data[2])
color_mode = bool(int(data[3]))
width = int(data[4])
height = int(data[5])
print(f"Received URL: {url} with contrast={contrast}, brightness={brightness}, color_mode={color_mode}, width={width}, height={height}")
sync_event = threading.Event()
video_thread = threading.Thread(target=process_video, args=(url, contrast, brightness, color_mode, width, height, video_conn, sync_event))
audio_thread = threading.Thread(target=process_audio, args=(url, audio_conn, sync_event))
video_thread.start()
audio_thread.start()
video_thread.join()
audio_thread.join()
def main():
parser = argparse.ArgumentParser(description="ASCII Video Streaming Server")
args = parser.parse_args()
server_socket_video = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket_audio = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket_video.bind(("0.0.0.0", 12345))
server_socket_audio.bind(("0.0.0.0", 12346))
server_socket_video.listen(5)
server_socket_audio.listen(5)
print("Server listening on ports 12345 (video) and 12346 (audio)")
while True:
video_conn, video_addr = server_socket_video.accept()
audio_conn, audio_addr = server_socket_audio.accept()
print(f"Connected by {video_addr} for video and {audio_addr} for audio")
handle_client(video_conn, audio_conn)
if __name__ == "__main__":
main()