From 858dcc55073583541513bced1fd7795ce47ea893 Mon Sep 17 00:00:00 2001 From: gsd Date: Fri, 16 Aug 2024 01:02:14 +0300 Subject: [PATCH] go2rtc support --- .dockerignore | 1 + .gitignore | 1 + Dockerfile | 23 +++--- backend/config_parser.py | 70 ++++++++++++++++++- backend/server.py | 28 +++++--- frontend/ang_dvrip/proxy.config.json | 2 +- frontend/ang_dvrip/src/app/app.component.ts | 4 +- .../app/components/about/about.component.html | 2 +- .../components/history/history.component.html | 2 +- .../components/history/history.component.ts | 2 +- .../transcode-modal.component.html | 2 +- .../transcode-modal.component.ts | 4 +- nginx.conf | 28 +++++++- 13 files changed, 136 insertions(+), 33 deletions(-) diff --git a/.dockerignore b/.dockerignore index 60e20b7..ea0ac08 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,7 @@ backend/transcode backend/__pycache__ backend/MiskaRisa264 +backend/go2rtc frontend/ang_dvrip/.angular frontend/ang_dvrip/dist frontend/ang_dvrip/node_modules \ No newline at end of file diff --git a/.gitignore b/.gitignore index d8367d2..b4b35e2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ config.json __pycache__ backend/MiskaRisa264 backend/transcode +backend/go2rtc # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. diff --git a/Dockerfile b/Dockerfile index c3488d8..2071ee9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,42 +1,41 @@ FROM ubuntu:22.04 RUN dpkg --add-architecture i386 \ + #base image && apt-get update \ && apt-get install -y ffmpeg wget unzip python3 python3-pip git curl \ + #wine install && mkdir -pm755 /etc/apt/keyrings \ && wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key \ && wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/jammy/winehq-jammy.sources \ && apt-get update && apt-get install -y winehq-stable \ - && rm -rf /var/lib/apt/lists/* - -RUN curl -s https://deb.nodesource.com/setup_18.x | bash \ - && apt-get update && apt-get install -y nodejs \ - && rm -rf /var/lib/apt/lists/* - -RUN apt-get update && apt-get install -y nginx \ + #frontend install + && curl -s https://deb.nodesource.com/setup_18.x | bash \ + && apt-get update && apt-get install -y nodejs nginx \ && rm -rf /var/lib/apt/lists/* RUN mkdir /opt/win32-python && cd /opt/win32-python \ && wget https://www.python.org/ftp/python/3.12.3/python-3.12.3-embed-win32.zip \ && unzip python-3.12.3-embed-win32.zip \ - && rm python-3.12.3-embed-win32.zip - -RUN echo "wine test open python" \ + && rm python-3.12.3-embed-win32.zip \ + && echo "wine test open python" \ && cd /opt/win32-python \ && wine python.exe --version +RUN cd /tmp && wget -O go2rtc https://github.com/AlexxIT/go2rtc/releases/download/v1.9.4/go2rtc_linux_amd64 && chmod +x go2rtc + #install backend dependes RUN python3 -m pip config --user set global.index https://nexus.pblr-nyk.pro/repository/pypi-all/pypi && \ python3 -m pip config --user set global.index-url https://nexus.pblr-nyk.pro/repository/pypi-all/simple && \ python3 -m pip config --user set global.trusted-host nexus.pblr-nyk.pro -RUN python3 -m pip install fastapi aiofiles uvicorn git+https://github.com/OpenIPC/python-dvr +RUN python3 -m pip install fastapi aiofiles uvicorn git+https://git.pblr-nyk.pro/mirror/python-dvr RUN mkdir /app WORKDIR /app #setup backend COPY backend /app/backend -RUN cd backend && git clone https://git.pblr-nyk.pro/gsd/MiskaRisa264 && mv /opt/win32-python /app/backend/MiskaRisa264/python-win32 && echo "{}" >> config.json +RUN cd backend && git clone https://git.pblr-nyk.pro/gsd/MiskaRisa264 && mv /opt/win32-python /app/backend/MiskaRisa264/python-win32 && echo "{}" >> config.json && mkdir /app/backend/go2rtc && mv /tmp/go2rtc /app/backend/go2rtc/ RUN cd backend && python3 config_parser.py --no-hide-check --err-check #setup frontend diff --git a/backend/config_parser.py b/backend/config_parser.py index 1d634c6..d31ec16 100644 --- a/backend/config_parser.py +++ b/backend/config_parser.py @@ -1,3 +1,4 @@ +from genericpath import exists import os, sys from asyncio_dvrip import DVRIPCam import asyncio @@ -10,12 +11,14 @@ from global_funcs import * class Recorder: loop = asyncio.get_event_loop() - def __init__(self, address, port, username, password, name = ""): + def __init__(self, address, port, username, password, name = "", index = 0): self.address = address self.port = int(port) self.username = username self.password = password self.name = name + self.index = index + self.channels = 0 @property def nvr(self): @@ -58,6 +61,67 @@ class TranscodeStatus: break yield b"" +class Go2RtcChannel: + #vhod_hd: dvrip://bfwc:JQWwM146@192.168.5.2:34567?channel=3&subtype=0 + def __init__(self, recorder: Recorder, count_of_streams = 2) -> None: + self.proto = "dvrip" + self.login = recorder.username + self.password = recorder.password + self.host = f"{recorder.address}:{recorder.port}" + self.recorder_index = recorder.index + self.count_of_channels = recorder.channels + self.count_of_streams = count_of_streams + + def generate_lines(self): + lines = "" + for i in range(0, self.count_of_channels): + for j in range(0, self.count_of_streams): + lines += f" {self.recorder_index}_{i}_{j}: {self.proto}://{self.login}:{self.password}@{self.host}?channel={i}&subtype={j}\n" + return lines + +class Go2Rtc: + WIN = "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.4/go2rtc_win64.zip" + LNX = "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.4/go2rtc_linux_amd64" + + def __init__(self) -> None: + self.enabled = False + try: + self.check_exists() + except: + print("go2rtc is disabled") + pass + + def check_exists(self): + go2rtc_directory = os.path.join(app_dir(), "go2rtc") + if os.path.exists(go2rtc_directory): + print("Go2Rtc directory exists") + if platform.system() == "Windows" and os.path.exists(os.path.join(go2rtc_directory, "go2rtc.exe")): + print("[WIN] Go2Rtc is exists, continue create config") + self.exec = os.path.join(go2rtc_directory, "go2rtc.exe") + self.enabled = True + elif platform.system() == "Linux" and os.path.exists(os.path.join(go2rtc_directory, "go2rtc")): + print("[LNX] Go2Rtc is exists, continue create config") + self.exec = os.path.join(go2rtc_directory, "go2rtc") + self.enabled = True + else: + raise Exception(f"go2rtc not downloaded, windows: {self.WIN} linux: {self.LNX}") + else: + raise Exception("Go2Rtc not found, he is disabled") + + async def start_go2rtc(self, recorders): + lines = "streams:\n" + for recorder in recorders: + lines += Go2RtcChannel(recorder, 2).generate_lines() + + cfg_file = os.path.join(app_dir(), "go2rtc", "go2rtc.yaml") + print(f"go2rtc config: {cfg_file}") + + async with aiofiles.open(cfg_file, "w", encoding="utf8") as cfg: + await cfg.write(lines) + + await asyncio.create_subprocess_exec(self.exec, *["-c", cfg_file]) + + class TranscodeTools: statuses:dict[str, TranscodeStatus] = {} WIN32PYTHON = "python-win32" @@ -237,8 +301,10 @@ class Config: self.listen_address = raw.get("backend", {}).get("address", "0.0.0.0") self.listen_port = int(raw.get("backend", {}).get("port", "8080")) self.recorders = [] + i = 0 for raw_server in raw.get("recorders", []): - self.recorders.append(Recorder(raw_server.get("ip"), raw_server.get("port"), raw_server.get("user"), raw_server.get("password"), raw_server.get("name", ""))) + self.recorders.append(Recorder(raw_server.get("ip"), raw_server.get("port"), raw_server.get("user"), raw_server.get("password"), raw_server.get("name", ""), index=i)) + i += 1 if (self.recorders.__len__() == 0): print("Recorders not find") else: diff --git a/backend/server.py b/backend/server.py index e1876b2..f75a596 100644 --- a/backend/server.py +++ b/backend/server.py @@ -4,13 +4,16 @@ import uvicorn import traceback from config_parser import Config as ConfigParser -from config_parser import TranscodeStatus, TranscodeTools +from config_parser import TranscodeStatus, TranscodeTools, Go2Rtc from nvr_core import NVR from nvr_types import File class Server: app: FastAPI = FastAPI() config: ConfigParser = ConfigParser() + go2rtc: Go2Rtc = Go2Rtc() + + API_BASE_REF = "/api/dvrip" def __init__(self): self.setup_events() @@ -19,10 +22,17 @@ class Server: def setup_events(self): @self.app.on_event('startup') async def on_startup(): - print("i am alive") + for i in range(0, len(self.config.recorders)): + nvr:NVR = self.config.getRecorder(i).nvr + await nvr.login() + channels = await nvr.channels() + nvr.logout() + self.config.recorders[i].channels = len(channels) + print(f"{self.config.recorders[i]} channels count: {self.config.recorders[i].channels}") + await self.go2rtc.start_go2rtc(self.config.recorders) def setup_routers(self): - @self.app.get("/api", status_code=200) + @self.app.get(self.API_BASE_REF, status_code=200) async def getRecorders(response: Response): try: return {"ok":True, "data":self.config.getRecorders()} @@ -31,7 +41,7 @@ class Server: response.status_code = 400 return {"ok":False, "error":e} - @self.app.get("/api/channels/{recorder_index}", status_code=200) + @self.app.get(self.API_BASE_REF + "/channels/{recorder_index}", status_code=200) async def getRecorder(response: Response, recorder_index:int): try: nvr:NVR = self.config.getRecorder(recorder_index).nvr @@ -45,7 +55,7 @@ class Server: finally: nvr.logout() - @self.app.get("/api/history/{recorder_index}/{channel}/{stream}") + @self.app.get(self.API_BASE_REF + "/history/{recorder_index}/{channel}/{stream}") async def getHistory(response: Response, recorder_index:int, channel: int, stream: int, start_date:str = None, end_date:str = None): try: nvr:NVR = self.config.getRecorder(recorder_index).nvr @@ -58,7 +68,7 @@ class Server: finally: nvr.logout() - @self.app.get("/api/snapshot/{recorder_index}/{channel}") + @self.app.get(self.API_BASE_REF + "/snapshot/{recorder_index}/{channel}") async def getSnapshot(response: Response, recorder_index:int, channel: int): try: nvr:NVR = self.config.getRecorder(recorder_index).nvr @@ -73,7 +83,7 @@ class Server: finally: nvr.logout() - @self.app.get("/api/file/{recorder_index}") + @self.app.get(self.API_BASE_REF + "/file/{recorder_index}") async def getFile(response: Response, recorder_index:int, b64:str, background_tasks: BackgroundTasks): try: if len(b64) == 0: @@ -102,7 +112,7 @@ class Server: response.status_code = 400 return {"ok":False, "error":e} - @self.app.get("/api/transcode/status/{recorder_index}") + @self.app.get(self.API_BASE_REF + "/transcode/status/{recorder_index}") async def getTranscodeStatus(response: Response, recorder_index:int, b64:str, background_tasks: BackgroundTasks): try: if len(b64) == 0: @@ -125,7 +135,7 @@ class Server: response.status_code = 400 return {"ok":False, "error":e} - @self.app.get("/api/transcode/download") + @self.app.get(self.API_BASE_REF + "/transcode/download") async def getTranscodeDownload(response: Response, b64:str): try: if len(b64) == 0: diff --git a/frontend/ang_dvrip/proxy.config.json b/frontend/ang_dvrip/proxy.config.json index b07c182..d743693 100644 --- a/frontend/ang_dvrip/proxy.config.json +++ b/frontend/ang_dvrip/proxy.config.json @@ -1,5 +1,5 @@ { - "/api/*": { + "/api/dvrip/*": { "target": "http://localhost:8080", "secure": false, "changeOrigin": true, diff --git a/frontend/ang_dvrip/src/app/app.component.ts b/frontend/ang_dvrip/src/app/app.component.ts index b2a9d5e..512e823 100644 --- a/frontend/ang_dvrip/src/app/app.component.ts +++ b/frontend/ang_dvrip/src/app/app.component.ts @@ -26,7 +26,7 @@ export class AppComponent implements OnInit { getRecorders() { this.loading = true; - this.http.get("/api", {}).subscribe((a:any) => { + this.http.get("/api/dvrip", {}).subscribe((a:any) => { this.availble_recorders = a["data"]; if (this.availble_recorders.length > 0) { this.getChannels(0); @@ -38,7 +38,7 @@ export class AppComponent implements OnInit { getChannels(recorder:number) { this.loading = true; - this.http.get(`/api/channels/${recorder}`).subscribe((a:any) => { + this.http.get(`/api/dvrip/channels/${recorder}`).subscribe((a:any) => { this.availble_channels = a["data"]; this.loading = false; }) diff --git a/frontend/ang_dvrip/src/app/components/about/about.component.html b/frontend/ang_dvrip/src/app/components/about/about.component.html index 473e3de..cb8c7d0 100644 --- a/frontend/ang_dvrip/src/app/components/about/about.component.html +++ b/frontend/ang_dvrip/src/app/components/about/about.component.html @@ -1 +1 @@ - + diff --git a/frontend/ang_dvrip/src/app/components/history/history.component.html b/frontend/ang_dvrip/src/app/components/history/history.component.html index 538c690..0198a59 100644 --- a/frontend/ang_dvrip/src/app/components/history/history.component.html +++ b/frontend/ang_dvrip/src/app/components/history/history.component.html @@ -39,7 +39,7 @@ Действия {{element.converted?'cloud_done':'cloud_download'}} - attachment + attachment diff --git a/frontend/ang_dvrip/src/app/components/history/history.component.ts b/frontend/ang_dvrip/src/app/components/history/history.component.ts index 32f555e..e918255 100644 --- a/frontend/ang_dvrip/src/app/components/history/history.component.ts +++ b/frontend/ang_dvrip/src/app/components/history/history.component.ts @@ -42,7 +42,7 @@ export class HistoryComponent implements OnInit { getHistory() { const params = this.route.snapshot.paramMap; this.dataSource = new MatTableDataSource(); - this.http.get(`api/history/${params.get('recorderId')}/${params.get('channelId')}/${this.selected_stream}?start_date=${this.baseUtils.prepareDate(this.start_date, true)}&end_date=${this.baseUtils.prepareDate(this.end_date, false)}`) + this.http.get(`api/dvrip/history/${params.get('recorderId')}/${params.get('channelId')}/${this.selected_stream}?start_date=${this.baseUtils.prepareDate(this.start_date, true)}&end_date=${this.baseUtils.prepareDate(this.end_date, false)}`) .subscribe((a:any) => { this.dataSource = new MatTableDataSource(a['data']); }) diff --git a/frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.html b/frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.html index c538931..5fa5366 100644 --- a/frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.html +++ b/frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.html @@ -11,7 +11,7 @@ diff --git a/frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.ts b/frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.ts index ac1ef94..b878766 100644 --- a/frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.ts +++ b/frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.ts @@ -36,7 +36,7 @@ export class TranscodeModalComponent implements OnInit { } getStatus() { - this.client.get(`api/transcode/status/${this.data.recorder_index}?b64=${this.data.b64}`) + this.client.get(`api/dvrip/transcode/status/${this.data.recorder_index}?b64=${this.data.b64}`) .subscribe((data:any) => { this.status = new TranscodeStatus().fromData(data["data"]); this.loading = false; @@ -47,7 +47,7 @@ export class TranscodeModalComponent implements OnInit { } getMP4(b64:string) { - window.open(`api/transcode/download?b64=${b64}`) + window.open(`api/dvrip/transcode/download?b64=${b64}`) } } diff --git a/nginx.conf b/nginx.conf index 54df594..edef9c9 100644 --- a/nginx.conf +++ b/nginx.conf @@ -12,7 +12,7 @@ server { try_files $uri /index.html; } - location ^~ /api { + location ^~ /api/dvrip { proxy_pass http://localhost:8080; proxy_redirect off; proxy_set_header Host $host; @@ -25,4 +25,30 @@ server { expires off; etag off; } + + location ^~ /api { + proxy_pass http://localhost:1984; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 86400; + } + + #http://localhost:4444/stream.html?src=0_0_0&mode=mse + location ^~ /stream.html { + proxy_pass http://localhost:1984; + #proxy_redirect off; + #proxy_set_header Host $host; + #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location ^~ /video-stream.js { + proxy_pass http://localhost:1984/video-stream.js; + } + + location ^~ /video-rtc.js { + proxy_pass http://localhost:1984/video-rtc.js; + } + + } \ No newline at end of file