From a171e1faad5f5bcda65228c78103193a3e1109d9 Mon Sep 17 00:00:00 2001 From: Minh Duc Ta Date: Fri, 26 May 2023 01:06:07 +0300 Subject: [PATCH 01/11] add Docker + testing --- .gitignore | 1 + Dockerfile | 27 +++++++++++++++++++++++++++ entrypoint.sh | 5 +++++ read_and_feed.py | 3 ++- src/__init__.py | 6 ++++-- src/api.py | 3 ++- src/encoder.py | 22 ++++++++++++++-------- 7 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 Dockerfile create mode 100755 entrypoint.sh diff --git a/.gitignore b/.gitignore index e04fa95..368f2da 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__/ /media/ /videos/ /src/app.db +/src/data diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..271cb40 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM ultravideo/kvazaar + +RUN echo 'tzdata tzdata/Areas select Europe' | debconf-set-selections +RUN echo 'tzdata tzdata/Zones/Europe select Paris' | debconf-set-selections +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata && \ + apt-get install -y software-properties-common && \ + rm -rf /var/lib/apt/lists/* +RUN add-apt-repository ppa:deadsnakes/ppa + +RUN apt-get update \ + && apt-get install curl ffmpeg gpac python3.9 python3-pip python3.9-venv -y + +WORKDIR /app + +RUN python3.9 -m venv /venv +ENV PATH=/venv/bin:$PATH + +COPY requirements.txt /app/ +RUN pip install --upgrade pip +RUN python --version +RUN pip --version +RUN pip install -r requirements.txt + +COPY . . + +ENTRYPOINT [ "/app/entrypoint.sh" ] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..7742c3f --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +flask --app src db upgrade +python read_and_feed.py & rq worker & flask --app src run -h 0.0.0.0 -p 7655 + diff --git a/read_and_feed.py b/read_and_feed.py index c56d123..5e00cb1 100644 --- a/read_and_feed.py +++ b/read_and_feed.py @@ -76,7 +76,8 @@ def main(camera_name="output"): segment_to_remove.unlink() segment_end_time = datetime.utcnow() # This is not needed if the input is from actual camera - sleep(10 - (segment_end_time - segment_start_time).total_seconds()) + if (10 - (segment_end_time - segment_start_time).total_seconds()) > 0: + sleep(10 - (segment_end_time - segment_start_time).total_seconds()) a.stdout.close() diff --git a/src/__init__.py b/src/__init__.py index 024661c..ab30970 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,6 +1,7 @@ import rq from flask import Flask import pathlib +import os from redis.client import Redis from flask_sqlalchemy import SQLAlchemy @@ -14,8 +15,9 @@ video_storage.mkdir() app = Flask(__name__) -app.redis = Redis.from_url("redis://") -app.config["SQLALCHEMY_DATABASE_URI"] = f'sqlite:///{(pathlib.Path(__file__) / ".." / "app.db").resolve()}' +redis_url = os.environ["REDIS_URL"] or "redis://" +app.redis = Redis.from_url(redis_url) +app.config["SQLALCHEMY_DATABASE_URI"] = f'sqlite:///{(pathlib.Path(__file__) / ".." / ".." / "data" / "app.db").resolve()}' app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.task_queue = rq.Queue(connection=app.redis) db = SQLAlchemy(app) diff --git a/src/api.py b/src/api.py index 30d187d..3fa6520 100644 --- a/src/api.py +++ b/src/api.py @@ -107,8 +107,10 @@ def delete_roi_region(id_): @app.route("/encode", methods=["POST"]) def start_encoding(): + print('start encoding') data = request.get_json() roi_id = data.get("roi_id") + print(data) f = None if roi_id is not None: f = roi_storage / roi_id @@ -144,7 +146,6 @@ def start_encoding(): "src.encoder.encode", f, start_point, duration, mkstemp()[1], camera, out_path ) - print(a.get_id()) return {"id": a.get_id()} diff --git a/src/encoder.py b/src/encoder.py index 18139be..8cad93d 100644 --- a/src/encoder.py +++ b/src/encoder.py @@ -18,19 +18,22 @@ def ffmpeg_concat_and_pipe_partial_videos(time, duration, camera): i = 0 current_segment = None segment_start = None + start_time = time - timedelta(seconds=duration) for i, v in enumerate(segments): r = datetime.fromisoformat(v.split("_")[1][:-4]) - if r >= (time - timedelta(seconds=10)): + if r >= start_time: + break + else: current_segment = v segment_start = r - break - seek = time - segment_start + seek = start_time - segment_start inputs = ["ffmpeg", f"-ss", f"{seek.seconds}.{seek.microseconds}", "-i", f"media/{camera}/{current_segment}"] concat = [f"[0:v]"] total_time = 10 - seek.seconds - seek.microseconds / 1e7 + print(f"segments: {segments}") - while total_time < duration: + while total_time < duration and i < len(segments): i += 1 concat.append(f"[{len(concat)}:v]") inputs.extend(["-i", f"media/{camera}/{segments[i]}"]) @@ -68,6 +71,7 @@ def encode(roi_file, start_time, duration, out_file, camera, out_path): if roi_file is not None: roi_file = preprocess_roi(roi_file) + print(f"ffmpeg cmd: {ffmpeg_cmd}") ffmpeg_handle = Popen( ffmpeg_cmd, stdout=PIPE, @@ -79,9 +83,9 @@ def encode(roi_file, start_time, duration, out_file, camera, out_path): "--input-fps", "30", "-i", "-", "--input-res", resolution, - "--preset", "medium", + "--preset", "ultrafast", "--qp", "37" if roi_file is not None else "27", - "-o", out_file, + "-o", str(out_file), ] if roi_file is not None: encode_command.extend([ @@ -107,13 +111,15 @@ def encode(roi_file, start_time, duration, out_file, camera, out_path): kvazaar_handle.wait() + print(f"Kvazaar done {str(out_file)}") + if out_path is None: out_path = (video_storage / job_get_id).with_suffix(".mp4") check_call( [ "MP4Box", - "-add", out_file, - "-new", out_path + "-add", str(out_file), + "-new", str(out_path) ] ) From 605695fea9d2ef9a868aff63d5706c71b71ef6c0 Mon Sep 17 00:00:00 2001 From: Minh Duc Ta Date: Thu, 1 Jun 2023 00:33:30 +0300 Subject: [PATCH 02/11] update docker file --- Dockerfile | 12 ++++++++- encode_test.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/encoder.py | 11 ++++---- 3 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 encode_test.py diff --git a/Dockerfile b/Dockerfile index 271cb40..9532e4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,17 @@ RUN apt-get update && \ RUN add-apt-repository ppa:deadsnakes/ppa RUN apt-get update \ - && apt-get install curl ffmpeg gpac python3.9 python3-pip python3.9-venv -y + && apt-get install build-essential git curl ffmpeg python3.9 python3-pip python3.9-venv \ + zlib1g-dev libfreetype6-dev libjpeg62-dev libpng-dev libmad0-dev libfaad-dev libogg-dev libvorbis-dev libtheora-dev liba52-0.7.4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavdevice-dev libxv-dev x11proto-video-dev libgl1-mesa-dev x11proto-gl-dev libxvidcore-dev libssl-dev libjack-dev libasound2-dev libpulse-dev libsdl2-dev dvb-apps mesa-utils -y + +# RUN ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts + +WORKDIR /gpac-build +RUN git clone https://github.com/gpac/gpac.git +WORKDIR /gpac-build/gpac +RUN ./configure +RUN make +RUN make install WORKDIR /app diff --git a/encode_test.py b/encode_test.py new file mode 100644 index 0000000..437607c --- /dev/null +++ b/encode_test.py @@ -0,0 +1,72 @@ +import os +from datetime import datetime, timedelta +from pathlib import Path +from subprocess import Popen, PIPE, DEVNULL, check_call +from tempfile import mkstemp + +import numpy as np + +duration = 10.0 + +ffmpeg_cmd = [ + 'ffmpeg', '-ss', '3.19912', + '-i', '/event_medias/output/output_2023-05-31T20:13:20.036643.mp4', + '-i', '/event_medias/output/output_2023-05-31T20:13:30.041467.mp4', + '-filter_complex', '[0:v][1:v]concat=n=2[outv]', + '-map', '[outv]', '-t', f'{duration}', + '-f', 'rawvideo', + '-pix_fmt', 'yuv420p', '-' +] + +ffmpeg_handle = Popen( + ffmpeg_cmd, + stdout=PIPE, + # stderr=DEVNULL +) + +resolution = "480x360" +roi_file = None +out_file = "/event_medias/kavazaar_test" +out_path = "/event_medias/kavazaar_test.mp4" + + +encode_command = [ + "kvazaar", + "--input-fps", "30", + "-i", "-", + "--input-res", resolution, + "--preset", "ultrafast", + "--qp", "27" if roi_file is not None else "27", + "-o", str(out_file), +] + +if roi_file is not None: + encode_command.extend([ + "--roi", roi_file, + ]) + +kvazaar_handle = Popen( + encode_command, + stdin=ffmpeg_handle.stdout, + stderr=PIPE, +) + +frames_encoded = 0 +total_frames = duration * 30 +for line in kvazaar_handle.stderr: + a = line.decode() + if a.startswith("POC"): + frames_encoded += 1 + + print(a, end="") + +kvazaar_handle.wait() +print(f"Kvazaar done {str(out_file)}") + +check_call( + [ + "MP4Box", + "-add", str(out_file), + "-new", str(out_path) + ] +) diff --git a/src/encoder.py b/src/encoder.py index 8cad93d..9a76428 100644 --- a/src/encoder.py +++ b/src/encoder.py @@ -75,7 +75,7 @@ def encode(roi_file, start_time, duration, out_file, camera, out_path): ffmpeg_handle = Popen( ffmpeg_cmd, stdout=PIPE, - stderr=DEVNULL + # stderr=DEVNULL ) resolution = os.environ["RESOLUTION"] or "1920x1080" encode_command = [ @@ -87,10 +87,10 @@ def encode(roi_file, start_time, duration, out_file, camera, out_path): "--qp", "37" if roi_file is not None else "27", "-o", str(out_file), ] - if roi_file is not None: - encode_command.extend([ - "--roi", roi_file, - ]) + # if roi_file is not None: + # encode_command.extend([ + # "--roi", roi_file, + # ]) kvazaar_handle = Popen( encode_command, @@ -104,6 +104,7 @@ def encode(roi_file, start_time, duration, out_file, camera, out_path): if a.startswith("POC"): frames_encoded += 1 job.meta["progress"] = 100 * frames_encoded / total_frames + print(a, end="") job.save_meta() job.meta["progress"] = 100 From 332c7f879f73f784cc22cef534707e09b69cb09d Mon Sep 17 00:00:00 2001 From: Minh Duc Ta Date: Mon, 5 Jun 2023 10:15:05 +0300 Subject: [PATCH 03/11] support native webcam stream --- Dockerfile | 7 ++- entrypoint.sh | 8 ++- read_and_feed.py | 146 +++++++++++++++++++++++++++++++++++------------ requirements.txt | 1 + src/encoder.py | 10 ++-- 5 files changed, 127 insertions(+), 45 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9532e4d..b7aedce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,14 +10,15 @@ RUN add-apt-repository ppa:deadsnakes/ppa RUN apt-get update \ && apt-get install build-essential git curl ffmpeg python3.9 python3-pip python3.9-venv \ - zlib1g-dev libfreetype6-dev libjpeg62-dev libpng-dev libmad0-dev libfaad-dev libogg-dev libvorbis-dev libtheora-dev liba52-0.7.4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavdevice-dev libxv-dev x11proto-video-dev libgl1-mesa-dev x11proto-gl-dev libxvidcore-dev libssl-dev libjack-dev libasound2-dev libpulse-dev libsdl2-dev dvb-apps mesa-utils -y + zlib1g-dev libfreetype6-dev libjpeg62-dev libpng-dev libmad0-dev libfaad-dev libogg-dev libvorbis-dev libtheora-dev liba52-0.7.4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavdevice-dev libxv-dev x11proto-video-dev libgl1-mesa-dev x11proto-gl-dev libxvidcore-dev libssl-dev libjack-dev libasound2-dev libpulse-dev libsdl2-dev dvb-apps mesa-utils \ + libpq-dev libsm6 libgl1 libxext6 -y # RUN ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts WORKDIR /gpac-build RUN git clone https://github.com/gpac/gpac.git WORKDIR /gpac-build/gpac -RUN ./configure +RUN ./configure --static-bin RUN make RUN make install @@ -27,7 +28,7 @@ RUN python3.9 -m venv /venv ENV PATH=/venv/bin:$PATH COPY requirements.txt /app/ -RUN pip install --upgrade pip +RUN pip install --upgrade pip cmake RUN python --version RUN pip --version RUN pip install -r requirements.txt diff --git a/entrypoint.sh b/entrypoint.sh index 7742c3f..e0bc0b2 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,11 @@ #!/bin/bash flask --app src db upgrade -python read_and_feed.py & rq worker & flask --app src run -h 0.0.0.0 -p 7655 + +if [[ "$DISABLE_READ_AND_FEED" == "1" ]]; then + rq worker & flask --app src run -h 0.0.0.0 -p 7655 +else + python read_and_feed.py "$CAMERA_NAME" "$STREAM" & rq worker & flask --app src run -h 0.0.0.0 -p 7655 +fi + diff --git a/read_and_feed.py b/read_and_feed.py index 5e00cb1..83f088c 100644 --- a/read_and_feed.py +++ b/read_and_feed.py @@ -1,5 +1,8 @@ +from sys import platform import os import sys +import cv2 +import subprocess from collections import deque from datetime import datetime, timedelta from pathlib import Path @@ -9,9 +12,70 @@ load_dotenv() +def save_10_seconds_webcam(output_path, device): + # Open the webcam + cap = cv2.VideoCapture(0) # Use 0 for the default camera -def main(camera_name="output"): + # Check if the camera is opened correctly + if not cap.isOpened(): + print("Failed to open the webcam") + return + + # Get the frames per second (FPS) of the camera + resolution = os.environ["RESOLUTION"] or "1920x1080" + width, height = [int(x) for x in resolution.split("x")] + cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) + cap.set(cv2.CAP_PROP_FPS, 30) + fps = cap.get(cv2.CAP_PROP_FPS) + + + # Calculate the number of frames to capture for 10 seconds + num_frames = int(fps * 10) + + # Initialize variables + frame_counter = 0 + video_frames = [] + + while cap.isOpened() and frame_counter < num_frames: + # Read a frame from the camera + ret, frame = cap.read() + + if not ret: + break + + # Add the frame to the list + video_frames.append(frame) + + # Increment the frame counter + frame_counter += 1 + + # Release the camera + cap.release() + + # Check if enough frames were captured + if frame_counter < num_frames: + print("Not enough frames available from the webcam") + return + + # Get the width and height of the frames + height, width, _ = video_frames[0].shape + + # Create a VideoWriter object to save the frames as an MP4 file + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) + + # Write the frames to the MP4 file + for frame in video_frames: + out.write(frame) + + # Release the VideoWriter + out.release() + + +def main(camera_name="output", stream=""): media_dir = (Path(__file__) / ".." / "media" / camera_name).resolve() + media_dir = Path(os.environ.get("MEDIA_DIR", media_dir)) media_dir.mkdir(exist_ok=True, parents=True) for file in media_dir.glob("*"): file.unlink() @@ -29,20 +93,23 @@ def main(camera_name="output"): # '-pix_fmt', "yuv420p", # '-' # ] - a = Popen( - [ - 'ffmpeg', - '-f', 'lavfi', - '-i', f'color=c=black:size={resolution}', - '-vf', rf"drawtext=fontsize=100:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2:text=%{{n}}", - '-f', "rawvideo", - '-pix_fmt', "yuv420p", - '-' - - ], - stdout=PIPE, - stderr=DEVNULL - ) + print(f"Stream {camera_name}: {stream}") + if stream: + a = None + else: + a = Popen( + [ + 'ffmpeg', + '-f', 'lavfi', + '-i', f'color=c=black:size={resolution}', + '-vf', rf"drawtext=fontsize=100:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2:text=%{{n}}", + '-f', "rawvideo", + '-pix_fmt', "yuv420p", + '-' + ], + stdout=PIPE, + stderr=DEVNULL + ) i = 0 width, height = [int(x) for x in resolution.split("x")] @@ -50,25 +117,29 @@ def main(camera_name="output"): while True: segment_start_time = datetime.utcnow() output_file = media_dir / f'output_{segment_start_time.isoformat()}.mp4' - b = Popen( - [ - 'ffmpeg', - '-s:v', resolution, - '-f', 'rawvideo', - '-pix_fmt', 'yuv420p', - '-r', '30', - '-i', 'pipe:.yuv', - "-c:v", "libx264", - str(output_file), - "-y" - ], - stdin=PIPE, - stderr=DEVNULL - ) - for _ in range(30 * 10): - f = a.stdout.read(int(width * height * 1.5)) - b.stdin.write(f) - b.stdin.close() + if a: + b = Popen( + [ + 'ffmpeg', + '-s:v', resolution, + '-f', 'rawvideo', + '-pix_fmt', 'yuv420p', + '-r', '30', + '-i', 'pipe:.yuv', + "-c:v", "libx264", + str(output_file), + "-y" + ], + stdin=PIPE, + stderr=DEVNULL + ) + for _ in range(30 * 10): + f = a.stdout.read(int(width * height * 1.5)) + b.stdin.write(f) + b.stdin.close() + else: + save_10_seconds_webcam(str(output_file), stream if platform != "darwin" else 0) + i += 1 segments.append(output_file) if len(segments) > 60: @@ -78,9 +149,12 @@ def main(camera_name="output"): # This is not needed if the input is from actual camera if (10 - (segment_end_time - segment_start_time).total_seconds()) > 0: sleep(10 - (segment_end_time - segment_start_time).total_seconds()) - a.stdout.close() + + if a: + a.stdout.close() if __name__ == '__main__': camera = sys.argv[1] if len(sys.argv) > 1 else "output" - main(camera) + stream = sys.argv[2] if len(sys.argv) > 2 else "" + main(camera, stream) diff --git a/requirements.txt b/requirements.txt index 8eb2554..e040e48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,4 @@ toml==0.10.2 typing_extensions==4.5.0 urllib3==1.26.14 Werkzeug==2.2.2 +opencv-python-headless \ No newline at end of file diff --git a/src/encoder.py b/src/encoder.py index 9a76428..5bbcd0e 100644 --- a/src/encoder.py +++ b/src/encoder.py @@ -75,7 +75,7 @@ def encode(roi_file, start_time, duration, out_file, camera, out_path): ffmpeg_handle = Popen( ffmpeg_cmd, stdout=PIPE, - # stderr=DEVNULL + stderr=DEVNULL ) resolution = os.environ["RESOLUTION"] or "1920x1080" encode_command = [ @@ -87,10 +87,10 @@ def encode(roi_file, start_time, duration, out_file, camera, out_path): "--qp", "37" if roi_file is not None else "27", "-o", str(out_file), ] - # if roi_file is not None: - # encode_command.extend([ - # "--roi", roi_file, - # ]) + if roi_file is not None: + encode_command.extend([ + "--roi", roi_file, + ]) kvazaar_handle = Popen( encode_command, From b466abc41e2e529a1d3fc0a1542ac18b471cd6a6 Mon Sep 17 00:00:00 2001 From: Minh Duc Ta Date: Tue, 6 Jun 2023 08:43:38 +0300 Subject: [PATCH 04/11] fix encoder --- src/encoder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/encoder.py b/src/encoder.py index 5bbcd0e..afdf0d4 100644 --- a/src/encoder.py +++ b/src/encoder.py @@ -31,13 +31,12 @@ def ffmpeg_concat_and_pipe_partial_videos(time, duration, camera): inputs = ["ffmpeg", f"-ss", f"{seek.seconds}.{seek.microseconds}", "-i", f"media/{camera}/{current_segment}"] concat = [f"[0:v]"] total_time = 10 - seek.seconds - seek.microseconds / 1e7 - print(f"segments: {segments}") while total_time < duration and i < len(segments): - i += 1 concat.append(f"[{len(concat)}:v]") inputs.extend(["-i", f"media/{camera}/{segments[i]}"]) total_time += 10 + i += 1 concat.append(f"concat=n={len(concat)}[outv]") inputs.extend( From 5d1f6e3afb9c2cce68ec9021e89cda4d095c9ec7 Mon Sep 17 00:00:00 2001 From: Minh Duc Ta Date: Tue, 6 Jun 2023 11:26:12 +0300 Subject: [PATCH 05/11] udpate dockerfile --- Dockerfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index b7aedce..5f7d8aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,12 +4,14 @@ RUN echo 'tzdata tzdata/Areas select Europe' | debconf-set-selections RUN echo 'tzdata tzdata/Zones/Europe select Paris' | debconf-set-selections RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata && \ - apt-get install -y software-properties-common && \ + apt-get -y install build-essential zlib1g-dev \ + libncurses5-dev libgdbm-dev libnss3-dev \ + libssl-dev libreadline-dev libffi-dev curl software-properties-common && \ rm -rf /var/lib/apt/lists/* RUN add-apt-repository ppa:deadsnakes/ppa RUN apt-get update \ - && apt-get install build-essential git curl ffmpeg python3.9 python3-pip python3.9-venv \ + && apt-get install build-essential git curl ffmpeg python3.8 python3-pip python3.8-venv \ zlib1g-dev libfreetype6-dev libjpeg62-dev libpng-dev libmad0-dev libfaad-dev libogg-dev libvorbis-dev libtheora-dev liba52-0.7.4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavdevice-dev libxv-dev x11proto-video-dev libgl1-mesa-dev x11proto-gl-dev libxvidcore-dev libssl-dev libjack-dev libasound2-dev libpulse-dev libsdl2-dev dvb-apps mesa-utils \ libpq-dev libsm6 libgl1 libxext6 -y @@ -24,7 +26,7 @@ RUN make install WORKDIR /app -RUN python3.9 -m venv /venv +RUN python3.8 -m venv /venv ENV PATH=/venv/bin:$PATH COPY requirements.txt /app/ From b1c02941747780afca3a96bc87236c35777a1470 Mon Sep 17 00:00:00 2001 From: Minh Duc Ta Date: Tue, 6 Jun 2023 12:08:29 +0300 Subject: [PATCH 06/11] update dockerfile --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5f7d8aa..5ddc6e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,9 @@ RUN python3.8 -m venv /venv ENV PATH=/venv/bin:$PATH COPY requirements.txt /app/ -RUN pip install --upgrade pip cmake +RUN pip install --upgrade setuptools pip +RUN pip install scikit-build +RUN pip install --upgrade cmake RUN python --version RUN pip --version RUN pip install -r requirements.txt From 46c73db9e1d0eac98158f11ee53fead9bdc49fa2 Mon Sep 17 00:00:00 2001 From: Minh Duc Ta Date: Wed, 7 Jun 2023 01:12:49 +0300 Subject: [PATCH 07/11] update dockerfile --- Dockerfile | 20 ++++++++++++++++---- read_and_feed.py | 3 ++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5ddc6e8..c1639fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,13 +5,25 @@ RUN echo 'tzdata tzdata/Zones/Europe select Paris' | debconf-set-selections RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata && \ apt-get -y install build-essential zlib1g-dev \ - libncurses5-dev libgdbm-dev libnss3-dev \ + libncurses5-dev libgdbm-dev libnss3-dev sqlite3 libsqlite3-dev \ libssl-dev libreadline-dev libffi-dev curl software-properties-common && \ rm -rf /var/lib/apt/lists/* -RUN add-apt-repository ppa:deadsnakes/ppa + +# RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - +# RUN add-apt-repository ppa:deadsnakes/ppa +RUN mkdir /tmp/python-build && \ + cd /tmp/python-build && \ + curl -fsSL https://www.python.org/ftp/python/3.9.7/Python-3.9.7.tgz | tar zx --strip-components=1 +RUN cd /tmp/python-build && \ + ./configure --enable-loadable-sqlite-extensions --enable-optimizations && \ + make -j "$(nproc)" +RUN cd /tmp/python-build && \ + make altinstall +RUN ln -s /usr/local/bin/python3.9 /usr/bin/python3.9 +RUN python3.9 --version RUN apt-get update \ - && apt-get install build-essential git curl ffmpeg python3.8 python3-pip python3.8-venv \ + && apt-get install build-essential git curl ffmpeg python3-pip \ zlib1g-dev libfreetype6-dev libjpeg62-dev libpng-dev libmad0-dev libfaad-dev libogg-dev libvorbis-dev libtheora-dev liba52-0.7.4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavdevice-dev libxv-dev x11proto-video-dev libgl1-mesa-dev x11proto-gl-dev libxvidcore-dev libssl-dev libjack-dev libasound2-dev libpulse-dev libsdl2-dev dvb-apps mesa-utils \ libpq-dev libsm6 libgl1 libxext6 -y @@ -26,7 +38,7 @@ RUN make install WORKDIR /app -RUN python3.8 -m venv /venv +RUN python3.9 -m venv /venv ENV PATH=/venv/bin:$PATH COPY requirements.txt /app/ diff --git a/read_and_feed.py b/read_and_feed.py index 83f088c..6cb0d4f 100644 --- a/read_and_feed.py +++ b/read_and_feed.py @@ -26,8 +26,9 @@ def save_10_seconds_webcam(output_path, device): width, height = [int(x) for x in resolution.split("x")] cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) - cap.set(cv2.CAP_PROP_FPS, 30) fps = cap.get(cv2.CAP_PROP_FPS) + # print(f'fps: {fps}') + # cap.set(cv2.CAP_PROP_FPS, 30) # Calculate the number of frames to capture for 10 seconds From ba6a20a76b5d91909cad1f27e8b134de5908d821 Mon Sep 17 00:00:00 2001 From: Kseniia Khak Date: Fri, 9 Jun 2023 11:46:57 +0300 Subject: [PATCH 08/11] check file extention when combining segments --- src/encoder.py | 87 +++++++++++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/src/encoder.py b/src/encoder.py index afdf0d4..95a4828 100644 --- a/src/encoder.py +++ b/src/encoder.py @@ -14,21 +14,30 @@ def ffmpeg_concat_and_pipe_partial_videos(time, duration, camera): - segments = sorted(os.listdir((Path(__file__) / ".." / ".." / "media" / camera).resolve())) + segments = sorted( + os.listdir((Path(__file__) / ".." / ".." / "media" / camera).resolve()) + ) i = 0 current_segment = None segment_start = None start_time = time - timedelta(seconds=duration) - for i, v in enumerate(segments): - r = datetime.fromisoformat(v.split("_")[1][:-4]) - if r >= start_time: - break - else: - current_segment = v - segment_start = r + for file in segments: + if file.endswith(".mp4"): + r = datetime.fromisoformat(file.split("_")[1][:-4]) + if r >= start_time: + break + else: + current_segment = file + segment_start = r seek = start_time - segment_start - inputs = ["ffmpeg", f"-ss", f"{seek.seconds}.{seek.microseconds}", "-i", f"media/{camera}/{current_segment}"] + inputs = [ + "ffmpeg", + f"-ss", + f"{seek.seconds}.{seek.microseconds}", + "-i", + f"media/{camera}/{current_segment}", + ] concat = [f"[0:v]"] total_time = 10 - seek.seconds - seek.microseconds / 1e7 @@ -40,12 +49,19 @@ def ffmpeg_concat_and_pipe_partial_videos(time, duration, camera): concat.append(f"concat=n={len(concat)}[outv]") inputs.extend( - ["-filter_complex", "".join(concat), - "-map", "[outv]", - "-t", str(duration), - "-f", "rawvideo", - "-pix_fmt", "yuv420p", - "-"] + [ + "-filter_complex", + "".join(concat), + "-map", + "[outv]", + "-t", + str(duration), + "-f", + "rawvideo", + "-pix_fmt", + "yuv420p", + "-", + ] ) return inputs @@ -71,25 +87,30 @@ def encode(roi_file, start_time, duration, out_file, camera, out_path): roi_file = preprocess_roi(roi_file) print(f"ffmpeg cmd: {ffmpeg_cmd}") - ffmpeg_handle = Popen( - ffmpeg_cmd, - stdout=PIPE, - stderr=DEVNULL - ) + ffmpeg_handle = Popen(ffmpeg_cmd, stdout=PIPE, stderr=DEVNULL) resolution = os.environ["RESOLUTION"] or "1920x1080" encode_command = [ "kvazaar", - "--input-fps", "30", - "-i", "-", - "--input-res", resolution, - "--preset", "ultrafast", - "--qp", "37" if roi_file is not None else "27", - "-o", str(out_file), + "--input-fps", + "30", + "-i", + "-", + "--input-res", + resolution, + "--preset", + "ultrafast", + "--qp", + "37" if roi_file is not None else "27", + "-o", + str(out_file), ] if roi_file is not None: - encode_command.extend([ - "--roi", roi_file, - ]) + encode_command.extend( + [ + "--roi", + roi_file, + ] + ) kvazaar_handle = Popen( encode_command, @@ -115,13 +136,7 @@ def encode(roi_file, start_time, duration, out_file, camera, out_path): if out_path is None: out_path = (video_storage / job_get_id).with_suffix(".mp4") - check_call( - [ - "MP4Box", - "-add", str(out_file), - "-new", str(out_path) - ] - ) + check_call(["MP4Box", "-add", str(out_file), "-new", str(out_path)]) with app.app_context(): r = models.Encoding(id=job_get_id, out_path=str(out_path)) From c035715bca29a0cc8ee6ee2b3e2699a3a2def909 Mon Sep 17 00:00:00 2001 From: Kseniia Khak Date: Fri, 9 Jun 2023 15:55:39 +0300 Subject: [PATCH 09/11] comment out data *= -10 --- src/encoder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/encoder.py b/src/encoder.py index 95a4828..347ecd6 100644 --- a/src/encoder.py +++ b/src/encoder.py @@ -68,7 +68,7 @@ def ffmpeg_concat_and_pipe_partial_videos(time, duration, camera): def preprocess_roi(f): data = np.load(roi_storage / f, allow_pickle=True) - data *= -10 + # data *= -10 handle, name = mkstemp() file = os.fdopen(handle, "w") file.write(f"{data.shape[1]} {data.shape[0]}\n") @@ -111,7 +111,6 @@ def encode(roi_file, start_time, duration, out_file, camera, out_path): roi_file, ] ) - kvazaar_handle = Popen( encode_command, stdin=ffmpeg_handle.stdout, From acc9698576e50726295415fb6b859f5f4d3a1b0c Mon Sep 17 00:00:00 2001 From: Kseniia Khak Date: Sun, 11 Jun 2023 01:44:11 +0300 Subject: [PATCH 10/11] add sleep(10) --- read_and_feed.py | 61 +++++++++++++++++++++++++++++------------------- src/encoder.py | 8 ++++--- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/read_and_feed.py b/read_and_feed.py index 6cb0d4f..e48e3bb 100644 --- a/read_and_feed.py +++ b/read_and_feed.py @@ -12,6 +12,7 @@ load_dotenv() + def save_10_seconds_webcam(output_path, device): # Open the webcam cap = cv2.VideoCapture(0) # Use 0 for the default camera @@ -30,7 +31,6 @@ def save_10_seconds_webcam(output_path, device): # print(f'fps: {fps}') # cap.set(cv2.CAP_PROP_FPS, 30) - # Calculate the number of frames to capture for 10 seconds num_frames = int(fps * 10) @@ -100,16 +100,21 @@ def main(camera_name="output", stream=""): else: a = Popen( [ - 'ffmpeg', - '-f', 'lavfi', - '-i', f'color=c=black:size={resolution}', - '-vf', rf"drawtext=fontsize=100:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2:text=%{{n}}", - '-f', "rawvideo", - '-pix_fmt', "yuv420p", - '-' + "ffmpeg", + "-f", + "lavfi", + "-i", + f"color=c=black:size={resolution}", + "-vf", + rf"drawtext=fontsize=100:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2:text=%{{n}}", + "-f", + "rawvideo", + "-pix_fmt", + "yuv420p", + "-", ], stdout=PIPE, - stderr=DEVNULL + stderr=DEVNULL, ) i = 0 @@ -117,29 +122,37 @@ def main(camera_name="output", stream=""): segments = deque() while True: segment_start_time = datetime.utcnow() - output_file = media_dir / f'output_{segment_start_time.isoformat()}.mp4' + output_file = media_dir / f"output_{segment_start_time.isoformat()}.mp4" if a: b = Popen( [ - 'ffmpeg', - '-s:v', resolution, - '-f', 'rawvideo', - '-pix_fmt', 'yuv420p', - '-r', '30', - '-i', 'pipe:.yuv', - "-c:v", "libx264", + "ffmpeg", + "-s:v", + resolution, + "-f", + "rawvideo", + "-pix_fmt", + "yuv420p", + "-r", + "30", + "-i", + "pipe:.yuv", + "-c:v", + "libx264", str(output_file), - "-y" + "-y", ], stdin=PIPE, - stderr=DEVNULL + stderr=DEVNULL, ) for _ in range(30 * 10): f = a.stdout.read(int(width * height * 1.5)) b.stdin.write(f) b.stdin.close() else: - save_10_seconds_webcam(str(output_file), stream if platform != "darwin" else 0) + save_10_seconds_webcam( + str(output_file), stream if platform != "darwin" else 0 + ) i += 1 segments.append(output_file) @@ -148,14 +161,14 @@ def main(camera_name="output", stream=""): segment_to_remove.unlink() segment_end_time = datetime.utcnow() # This is not needed if the input is from actual camera - if (10 - (segment_end_time - segment_start_time).total_seconds()) > 0: - sleep(10 - (segment_end_time - segment_start_time).total_seconds()) + # if (10 - (segment_end_time - segment_start_time).total_seconds()) > 0: + # sleep(10 - (segment_end_time - segment_start_time).total_seconds()) if a: a.stdout.close() -if __name__ == '__main__': +if __name__ == "__main__": camera = sys.argv[1] if len(sys.argv) > 1 else "output" - stream = sys.argv[2] if len(sys.argv) > 2 else "" + stream = sys.argv[2] if len(sys.argv) > 2 else "" main(camera, stream) diff --git a/src/encoder.py b/src/encoder.py index 347ecd6..84b986f 100644 --- a/src/encoder.py +++ b/src/encoder.py @@ -7,21 +7,23 @@ import numpy as np from rq import get_current_job from dotenv import load_dotenv +import time from src import video_storage, roi_storage, models, db, app, api load_dotenv() -def ffmpeg_concat_and_pipe_partial_videos(time, duration, camera): +def ffmpeg_concat_and_pipe_partial_videos(event_time, duration, camera): + time.sleep(10) segments = sorted( os.listdir((Path(__file__) / ".." / ".." / "media" / camera).resolve()) ) i = 0 current_segment = None segment_start = None - start_time = time - timedelta(seconds=duration) - for file in segments: + start_time = event_time - timedelta(seconds=duration) + for i, file in enumerate(segments): if file.endswith(".mp4"): r = datetime.fromisoformat(file.split("_")[1][:-4]) if r >= start_time: From a1dce7bdef2e15b157c22621f0c60d013ffad6b9 Mon Sep 17 00:00:00 2001 From: Kseniia Khak Date: Sun, 11 Jun 2023 21:13:49 +0300 Subject: [PATCH 11/11] flip video in read_and_feed --- read_and_feed.py | 1 + 1 file changed, 1 insertion(+) diff --git a/read_and_feed.py b/read_and_feed.py index e48e3bb..a3d2627 100644 --- a/read_and_feed.py +++ b/read_and_feed.py @@ -44,6 +44,7 @@ def save_10_seconds_webcam(output_path, device): if not ret: break + frame = cv2.flip(frame, 1) # Add the frame to the list video_frames.append(frame)