# HOST/main-host.py
import asyncio
import sys
from HOST.aplication_host import Application
from load_config.loading_config_general import load_config_file_general
from load_config.loading_config_host import load_config_file_host
from core.logging_config import setup_logging


async def main_init():

    logger = setup_logging()

    Config_g = load_config_file_general()

    Config_h = load_config_file_host()

    application = Application(Config_g, Config_h)

    try:
        logger.info("iniciando application pyremote")
        
        await application.start()

        logger.info("pyremote iniciado correctamente")

        await application.wait_until_shutdown()    
        
    except KeyboardInterrupt:
        logger.info("interrupción recibida")

    except Exception:
        logger.exception("Error durante la ejecución de PyRemote.")
        raise

    finally:
        logger.info("cerrando pyremote...")
        await application.stop()


if __name__ == "__main__":
    try:
        asyncio.run(main_init())
    except Exception:
        logger.info("error inesperado al ejecutar pyremote")
        sys.exit(1)

#___________________________________
# HOST/aplication_host.py
import asyncio
import logging
from core.logging_config import setup_logging
from server.server import Server

class Application:
    def __init__(self, Config_g, Config_h):
        self.shutdown_event=asyncio.Event()
        self.conf_general=Config_g
        self.conf_host=Config_h
        self.server=Server(self.conf_general, self.conf_host)
        self.logger=setup_logging()
        
    async def start(self):
        await self.server.start_server()
        self.logger.info("servidor iniciado correctamente")

    async def wait_until_shutdown(self):
        #ligar esto al cierre del pyremote
        await self.shutdown_event.wait()

    async def request_shutdown(self):
        self.shutdown_event.set()

    async def stop(self):
        await self.server.start_server()





#___________________________________
#server/server.py
import asyncio
import uvicorn
from fastapi import FastAPI
from server.routes.signaling import router

class Server:
    def __init__(self, config_g, config_h):
        self.app=FastAPI(title="PyRemote Signaling Server")
        self.app.include_router(router)
        self.config_general=config_g
        self.config_host=config_h
        self.server=None
        self.task=None
        

    async def start_server(self):
        uvicorn_config = uvicorn.config(
            app=self.app,
            port=self.config_host.host.server_host,
            host=self.config_host.host.server_port,
            log_level="info",
            access_log=True,
            ws_ping_interval=20,
            ws_ping_timeout=20,
            worker=1
        )

        self.server = uvicorn.Server(uvicorn_config)

        self.task = asyncio.create_task(
            self.server.serve()
        )

        while not self.server.started:
            if self.task.done():
                await self.task
                raise RuntimeError("No se pudo iniciar el servidor")
                
            await asyncio.sleep(0.5)

    
    async def stop_server(self):

        if self.server:
            self.server.should_exit = True

        if self.task:
            await self.task

#___________________________________
#server/routes/signaling.py
from fastapi import APIRouter, WebSocket, WebSocketDisconnect



#___________________________________
#load_config/loading_config_general.py
import json
from dataclasses import dataclass
from core.config import CONFIG_GENERAL

@dataclass
class Application:
    name: str
    version: str
    environment: str

@dataclass
class Discovery:
    protocol: str
    broadcast_address: str
    port: int
    broadcast_interval: int
    response_timeout: int
    device_expiration: int

@dataclass
class Signaling:
    protocol: str
    port: int
    path: str
    connection_timeout: int
    reconnect_attempts: int
    reconnect_interval: int

@dataclass
class Webrtc:
    video_codec: str
    max_fps: int
    max_bitrate: int
    ice_servers: list

@dataclass
class Security:
    require_authorization: bool
    authorization_timeout: int

@dataclass
class Logging:
    level: str
    file: str


@dataclass
class App_config:
    application: Application
    discovery: Discovery
    signaling: Signaling
    webrtc: Webrtc
    security: Security
    logging: Logging


def load_config_file_general(path=CONFIG_GENERAL) -> App_config:
    with open(path, "r", encoding="utf-8") as file:
        data = json.load(file)

    return App_config(
        application=Application(**data["application"]),
        discovery=Discovery(**data["discovery"]),
        signaling=Signaling(**data["signaling"]),
        webrtc=Webrtc(**data["webrtc"]),
        security=Security(**data["security"])
        logging=Logging(**data["logging"])
    )
    
            

#___________________________________
#load_config/loading_config_host.py
import json
from dataclasses import dataclass
from core.config import CONFIG_HOST

@dataclass
class Device:
    device_id: str
    device_name: str
    mode: str

@dataclass
class Host:
    enabled: bool
    server_host: str
    server_port: int
    signaling_server: bool

@dataclass
class Client:
    enabled: boom
    signaling_server_url: null

@dataclass
class Services:
    discovery: bool
    device_manager: bool
    connection_manager: bool
    screen_capture: bool
    screen_receiver: bool

@dataclass
class Preferences:
    auto_start: bool
    start_minimized: bool

@dataclass
class Config_host:
    device: Device
    host: Host
    client: Client
    services: Services
    preferences: Preferences

def load_config_file_host(path=CONFIG_HOST) -> Config_host:
    with open(path, "r", encoding="utf-8") as file:
        data = json.load(file)

    return Config_host(
        device=Device(**data["device"]),
        host=Host(**data["host"]),
        client=Client(**data["client"]),
        services=Services(**data["services"]),
        preferences=Preferences(**data["preferences"])
    )

#___________________________________
#core/config.py
from pathlib import Path
import os
from dotenv import load_dotenv

load_dotenv(Path(__file__).resolve().parent.parent / ".env")   # raíz del repo, no core/


def get_env(name: str, default: str | None = None) -> str:
    value = os.getenv(name, default)

    if value is None or value = "":
        raise RuntimeError(f"Falta la variable de entorno requerida: {name}")

    return value

    

CONFIG_GENERAL=get_env("CONFIG_GENERAL")

CONFIG_HOST=get_env("CONFIG_HOST")

#___________________________________
#.env

CONFIG_GENERAL="config_general.json"

CONFIG_HOST="config_host.json"

#___________________________________
#conf/config_general.json
{
    "application": {
        "name": "PyRemote",
        "version": "1.0.0",
        "environment": "production"
    },

    "discovery": {
        "protocol": "UDP",
        "broadcast_address": "255.255.255.255",
        "port": 50000,
        "broadcast_interval": 3,
        "response_timeout": 2,
        "device_expiration": 10
    },

    "signaling": {
        "protocol": "WebSocket",
        "port": 8000,
        "path": "/ws",
        "connection_timeout": 15,
        "reconnect_attempts": 5,
        "reconnect_interval": 3
    },

    "webrtc": {
        "video_codec": "VP8",
        "max_fps": 30,
        "max_bitrate": 3000000,
        "ice_servers": []
    },

    "security": {
        "require_authorization": true,
        "authorization_timeout": 60
    },

    "logging": {
        "level": "INFO",
        "file": "logs/pyremote.log"
    }
}

#___________________________________
#conf/config_host.json
{
    "device": {
        "device_id": "GENERATED-UUID",
        "device_name": "PC-A",
        "mode": "HOST"
    },

    "host": {
        "enabled": true,
        "server_host": "0.0.0.0",
        "server_port": 8000,
        "signaling_server": true
    },

    "client": {
        "enabled": false,
        "signaling_server_url": null
    },

    "services": {
        "discovery": true,
        "device_manager": true,
        "connection_manager": true,
        "screen_capture": true,
        "screen_receiver": true
    },

    "preferences": {
        "auto_start": true,
        "start_minimized": false
    }
}


#___________________________________
#core/logging_config.py

import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path


# Directorio de logs
BASE_DIR = Path(__file__).resolve().parent
LOG_DIR = BASE_DIR / "logs"

# Crear carpeta si no existe
LOG_DIR.mkdir(parents=True, exist_ok=True)

# Archivo de registros
LOG_FILE = LOG_DIR / "pyremote.log"


def setup_logging():

    logger = logging.getLogger("PyRemote")

    # Evitar configurar varias veces el mismo logger
    if logger.handlers:
        return logger

    logger.setLevel(logging.DEBUG)

    # Formato de los registros
    formatter = logging.Formatter(
        "%(asctime)s | %(levelname)s | %(name)s | %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S"
    )

    # Registro en archivo con rotación
    file_handler = RotatingFileHandler(
        LOG_FILE,
        maxBytes=5 * 1024 * 1024,
        backupCount=5,
        encoding="utf-8"
    )

    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(formatter)

    # Registro en consola
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_handler.setFormatter(formatter)

    # Registrar manejadores
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)

    return logger
    

#___________________________________
#pyremote.log
2026-11-13 02:00:00 | INFO | HOST | Aplicación iniciada.
2026-11-13 02:00:01 | INFO | CONFIG | Configuración general cargada.
2026-11-13 02:00:01 | INFO | CONFIG | Configuración local HOST cargada.
2026-11-13 02:00:02 | INFO | UDP | Servicio de descubrimiento iniciado.
2026-11-13 02:00:03 | INFO | FASTAPI | Servidor iniciado en 0.0.0.0:8000.
2026-11-13 02:00:05 | INFO | UDP | Anuncio HOST enviado.
2026-11-13 02:00:10 | INFO | UDP | Solicitud de descubrimiento recibida.
2026-11-13 02:00:10 | INFO | UDP | Respuesta enviada a 192.168.1.21.
2026-11-13 02:00:11 | INFO | WEBSOCKET | Cliente conectado: PC-B.
2026-11-13 02:00:12 | INFO | CONNECTION | Solicitud de conexión recibida.
2026-11-13 02:00:15 | INFO | CONNECTION | Solicitud aceptada.
2026-11-13 02:00:16 | INFO | WEBRTC | Negociación iniciada.
2026-11-13 02:00:18 | INFO | WEBRTC | Conexión establecida.
2026-11-13 02:00:19 | INFO | SCREEN | Captura de pantalla iniciada.
2026-11-13 02:00:20 | INFO | WEBRTC | Transmisión de vídeo iniciada.
2026-11-13 02:10:00 | INFO | CONNECTION | Sesión finalizada.
2026-11-13 02:10:01 | INFO | HOST | Aplicación cerrada.




#___________________________________
# CLIENT/main_client.py




#___________________________________
#conf_client/config_general.json
{
    "application": {
        "name": "PyRemote",
        "version": "1.0.0",
        "environment": "production"
    },

    "discovery": {
        "protocol": "UDP",
        "broadcast_address": "255.255.255.255",
        "port": 50000,
        "broadcast_interval": 3,
        "response_timeout": 2,
        "device_expiration": 10
    },

    "signaling": {
        "protocol": "WebSocket",
        "port": 8000,
        "path": "/ws",
        "connection_timeout": 15,
        "reconnect_attempts": 5,
        "reconnect_interval": 3
    },

    "webrtc": {
        "video_codec": "VP8",
        "max_fps": 30,
        "max_bitrate": 3000000,
        "ice_servers": []
    },

    "security": {
        "require_authorization": true,
        "authorization_timeout": 60
    },

    "logging": {
        "level": "INFO",
        "file": "logs/pyremote.log"
    }
}

#___________________________________
#conf_client/config_client.json
{
    "device": {
        "device_id": "GENERATED-UUID",
        "device_name": "PC-B",
        "mode": "CLIENT"
    },

    "host": {
        "enabled": false,
        "server_host": null,
        "server_port": null,
        "signaling_server": false
    },

    "client": {
        "enabled": true,
        "signaling_server_url": null,
        "auto_discovery": true
    },

    "services": {
        "discovery": true,
        "device_manager": true,
        "connection_manager": true,
        "screen_capture": true,
        "screen_receiver": true
    },

    "preferences": {
        "auto_start": true,
        "start_minimized": false
    }
}

Embed on website

To embed this project on your website, copy the following code and paste it into your website's HTML: