From ebc593ffc9a90889ccdd0c888951fabdd82349bb Mon Sep 17 00:00:00 2001 From: iAmInActions Date: Tue, 5 Nov 2024 23:46:55 +0100 Subject: [PATCH] Add python ASCII video (and audio) socket server and client --- py-ascii-video-socket/client.py | 140 +++++++++++++++++++++++++++++ py-ascii-video-socket/server.py | 154 ++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 py-ascii-video-socket/client.py create mode 100644 py-ascii-video-socket/server.py diff --git a/py-ascii-video-socket/client.py b/py-ascii-video-socket/client.py new file mode 100644 index 0000000..f2ece2f --- /dev/null +++ b/py-ascii-video-socket/client.py @@ -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() + diff --git a/py-ascii-video-socket/server.py b/py-ascii-video-socket/server.py new file mode 100644 index 0000000..05f538f --- /dev/null +++ b/py-ascii-video-socket/server.py @@ -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() +