Merge pull request #2829 from blakeblackshear/release-0.11.0

Release 0.11.0
This commit is contained in:
Blake Blackshear 2022-09-22 18:47:05 -05:00 committed by GitHub
commit 3846a13805
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
215 changed files with 14573 additions and 19654 deletions

View File

@ -9,19 +9,32 @@
"mhutchie.git-graph", "mhutchie.git-graph",
"ms-azuretools.vscode-docker", "ms-azuretools.vscode-docker",
"streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker",
"eamodio.gitlens",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"ms-python.vscode-pylance" "ms-python.vscode-pylance",
"dbaeumer.vscode-eslint",
"mikestead.dotenv",
"csstools.postcss",
"blanu.vscode-styled-jsx",
"bradlc.vscode-tailwindcss"
], ],
"settings": { "settings": {
"python.pythonPath": "/usr/bin/python3",
"python.linting.pylintEnabled": true, "python.linting.pylintEnabled": true,
"python.linting.enabled": true, "python.linting.enabled": true,
"python.formatting.provider": "black", "python.formatting.provider": "black",
"python.languageServer": "Pylance",
"editor.formatOnPaste": false, "editor.formatOnPaste": false,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.formatOnType": true, "editor.formatOnType": true,
"files.trimTrailingWhitespace": true, "files.trimTrailingWhitespace": true,
"terminal.integrated.shell.linux": "/bin/bash" "eslint.workingDirectories": ["./web"],
"[json][jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsx][js][tsx][ts]": {
"editor.codeActionsOnSave": ["source.addMissingImports", "source.fixAll"],
"editor.tabSize": 2
},
"cSpell.ignoreWords": ["rtmp"],
"cSpell.words": ["preact"]
} }
} }

View File

@ -7,5 +7,6 @@ config/
.git .git
core core
*.mp4 *.mp4
*.jpg
*.db *.db
*.ts *.ts

View File

@ -2,6 +2,9 @@ name: On pull request
on: pull_request on: pull_request
env:
DEFAULT_PYTHON: 3.9
jobs: jobs:
web_lint: web_lint:
name: Web - Lint name: Web - Lint
@ -10,25 +13,11 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 14.x node-version: 16.x
- run: npm install - run: npm install
working-directory: ./web working-directory: ./web
- name: Lint - name: Lint
run: npm run lint:cmd run: npm run lint
working-directory: ./web
web_build:
name: Web - Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 14.x
- run: npm install
working-directory: ./web
- name: Build
run: npm run build
working-directory: ./web working-directory: ./web
web_test: web_test:
@ -38,33 +27,54 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- uses: actions/setup-node@master - uses: actions/setup-node@master
with: with:
node-version: 14.x node-version: 16.x
- run: npm install - run: npm install
working-directory: ./web working-directory: ./web
- name: Test - name: Test
run: npm run test run: npm run test
working-directory: ./web working-directory: ./web
docker_tests_on_aarch64: python_checks:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Python checks
steps: steps:
- name: Check out code - name: Check out the repository
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: Set up QEMU - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: docker/setup-qemu-action@v1 uses: actions/setup-python@v2.2.2
- name: Set up Docker Buildx with:
uses: docker/setup-buildx-action@v1 python-version: ${{ env.DEFAULT_PYTHON }}
- name: Build and run tests - name: Install requirements
run: make run_tests PLATFORM="linux/arm64/v8" ARCH="aarch64" run: |
pip install pip
pip install -r requirements-dev.txt
- name: Lint
run: |
python3 -m black frigate --check
docker_tests_on_amd64: python_tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Python Tests
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@master
with:
node-version: 16.x
- run: npm install
working-directory: ./web
- name: Build web
run: npm run build
working-directory: ./web
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Build and run tests - name: Create Version Module
run: make run_tests PLATFORM="linux/amd64" ARCH="amd64" run: make version
- name: Build
run: make
- name: Run mypy
run: docker run --rm --entrypoint=python3 frigate:latest -u -m mypy --config-file frigate/mypy.ini frigate
- name: Run tests
run: docker run --rm --entrypoint=python3 frigate:latest -u -m unittest

1
.gitignore vendored
View File

@ -14,3 +14,4 @@ web/build
web/node_modules web/node_modules
web/coverage web/coverage
core core
!/web/**/*.ts

View File

@ -1,74 +1,39 @@
default_target: amd64_frigate default_target: local
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1) COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
VERSION = 0.11.0
CURRENT_UID := $(shell id -u)
CURRENT_GID := $(shell id -g)
version: version:
echo "VERSION='0.10.1-$(COMMIT_HASH)'" > frigate/version.py echo "VERSION=\"$(VERSION)-$(COMMIT_HASH)\"" > frigate/version.py
web: build_web:
docker build --tag frigate-web --file docker/Dockerfile.web web/ docker run --volume ${PWD}/web:/web -w /web --volume /etc/passwd:/etc/passwd:ro --volume /etc/group:/etc/group:ro -u $(CURRENT_UID):$(CURRENT_GID) node:16 /bin/bash -c "npm install && npm run build"
amd64_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-amd64 --file docker/Dockerfile.wheels .
amd64_ffmpeg:
docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.2.0-amd64 --file docker/Dockerfile.ffmpeg.amd64 .
nginx_frigate: nginx_frigate:
docker buildx build --push --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate-nginx:1.0.2 --file docker/Dockerfile.nginx . docker buildx build --push --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate-nginx:1.0.2 --file docker/Dockerfile.nginx .
amd64_frigate: version web local:
docker build --no-cache --tag frigate-base --build-arg ARCH=amd64 --build-arg FFMPEG_VERSION=1.1.0 --build-arg WHEELS_VERSION=1.0.3 --build-arg NGINX_VERSION=1.0.2 --file docker/Dockerfile.base . DOCKER_BUILDKIT=1 docker build -t frigate -f docker/Dockerfile .
docker build --no-cache --tag frigate --file docker/Dockerfile.amd64 .
amd64_all: amd64_wheels amd64_ffmpeg amd64_frigate amd64:
docker buildx build --platform linux/amd64 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
amd64nvidia_wheels: arm64:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-amd64nvidia --file docker/Dockerfile.wheels . docker buildx build --platform linux/arm64 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
amd64nvidia_ffmpeg: armv7:
docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.2.0-amd64nvidia --file docker/Dockerfile.ffmpeg.amd64nvidia . docker buildx build --platform linux/arm/v7 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
amd64nvidia_frigate: version web build: version amd64 arm64 armv7
docker build --no-cache --tag frigate-base --build-arg ARCH=amd64nvidia --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --build-arg NGINX_VERSION=1.0.2 --file docker/Dockerfile.base . docker buildx build --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
docker build --no-cache --tag frigate --file docker/Dockerfile.amd64nvidia .
amd64nvidia_all: amd64nvidia_wheels amd64nvidia_ffmpeg amd64nvidia_frigate push: build
docker buildx build --push --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
aarch64_wheels: run_tests: frigate
docker build --tag blakeblackshear/frigate-wheels:1.0.3-aarch64 --file docker/Dockerfile.wheels . docker run --rm --entrypoint=python3 frigate:latest -u -m unittest
docker run --rm --entrypoint=python3 frigate:latest -u -m mypy --config-file frigate/mypy.ini frigate
aarch64_ffmpeg: .PHONY: run_tests
docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.3.0-aarch64 --file docker/Dockerfile.ffmpeg.aarch64 .
aarch64_frigate: version web
docker build --no-cache --tag frigate-base --build-arg ARCH=aarch64 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --build-arg NGINX_VERSION=1.0.2 --file docker/Dockerfile.base .
docker build --no-cache --tag frigate --file docker/Dockerfile.aarch64 .
aarch64_all: aarch64_wheels aarch64_ffmpeg aarch64_frigate
armv7_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-armv7 --file docker/Dockerfile.wheels .
armv7_ffmpeg:
docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.2.0-armv7 --file docker/Dockerfile.ffmpeg.armv7 .
armv7_frigate: version web
docker build --no-cache --tag frigate-base --build-arg ARCH=armv7 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --build-arg NGINX_VERSION=1.0.2 --file docker/Dockerfile.base .
docker build --no-cache --tag frigate --file docker/Dockerfile.armv7 .
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
run_tests:
# PLATFORM: linux/arm64/v8 linux/amd64 or linux/arm/v7
# ARCH: aarch64 amd64 or armv7
@cat docker/Dockerfile.base docker/Dockerfile.$(ARCH) > docker/Dockerfile.test
@sed -i "s/FROM frigate-web as web/#/g" docker/Dockerfile.test
@sed -i "s/COPY --from=web \/opt\/frigate\/build web\//#/g" docker/Dockerfile.test
@sed -i "s/FROM frigate-base/#/g" docker/Dockerfile.test
@echo "" >> docker/Dockerfile.test
@echo "RUN python3 -m unittest" >> docker/Dockerfile.test
@docker buildx build --platform=$(PLATFORM) --tag frigate-base --build-arg NGINX_VERSION=1.0.2 --build-arg FFMPEG_VERSION=1.0.0 --build-arg ARCH=$(ARCH) --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.test .
@rm docker/Dockerfile.test
.PHONY: web run_tests

View File

@ -3,20 +3,28 @@ services:
dev: dev:
container_name: frigate-dev container_name: frigate-dev
user: vscode user: vscode
privileged: true # add groups from host for render, plugdev, video
group_add:
- "109" # render
- "110" # render
- "44" # video
- "46" # plugdev
shm_size: "256mb" shm_size: "256mb"
build: build:
context: . context: .
dockerfile: docker/Dockerfile.dev dockerfile: docker/Dockerfile.dev
devices:
- /dev/bus/usb:/dev/bus/usb
- /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware
volumes: volumes:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- .:/lab/frigate:cached - .:/lab/frigate:cached
- ./config/config.yml:/config/config.yml:ro - ./config/config.yml:/config/config.yml:ro
- ./debug:/media/frigate - ./debug:/media/frigate
- /dev/bus/usb:/dev/bus/usb - /dev/bus/usb:/dev/bus/usb
- /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware
ports: ports:
- "1935:1935" - "1935:1935"
- "3000:3000"
- "5000:5000" - "5000:5000"
- "5001:5001" - "5001:5001"
- "8080:8080" - "8080:8080"

148
docker/Dockerfile Normal file
View File

@ -0,0 +1,148 @@
FROM blakeblackshear/frigate-nginx:1.0.2 as nginx
FROM debian:11 as wheels
ARG TARGETARCH
ENV DEBIAN_FRONTEND=noninteractive
# Use a separate container to build wheels to prevent build dependencies in final image
RUN apt-get -qq update \
&& apt-get -qq install -y \
apt-transport-https \
gnupg \
wget \
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9165938D90FDDD2E \
&& echo "deb http://raspbian.raspberrypi.org/raspbian/ bullseye main contrib non-free rpi" | tee /etc/apt/sources.list.d/raspi.list \
&& apt-get -qq update \
&& apt-get -qq install -y \
python3 \
python3-dev \
wget \
# opencv dependencies
build-essential cmake git pkg-config libgtk-3-dev \
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
gfortran openexr libatlas-base-dev libssl-dev\
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \
# scipy dependencies
gcc gfortran libopenblas-dev liblapack-dev
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
&& python3 get-pip.py "pip"
RUN if [ "${TARGETARCH}" = "arm" ]; \
then echo "[global]" > /etc/pip.conf \
&& echo "extra-index-url=https://www.piwheels.org/simple" >> /etc/pip.conf; \
fi
COPY requirements.txt /requirements.txt
RUN pip3 install -r requirements.txt
COPY requirements-wheels.txt /requirements-wheels.txt
RUN pip3 wheel --wheel-dir=/wheels -r requirements-wheels.txt
# Frigate Container
FROM debian:11-slim
ARG TARGETARCH
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
# http://stackoverflow.com/questions/48162574/ddg#49462622
ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
ENV FLASK_ENV=development
COPY --from=wheels /wheels /wheels
# Install ffmpeg
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
apt-transport-https \
gnupg \
wget \
unzip tzdata libxml2 xz-utils \
python3-pip \
# add raspberry pi repo
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9165938D90FDDD2E \
&& echo "deb http://raspbian.raspberrypi.org/raspbian/ bullseye main contrib non-free rpi" | tee /etc/apt/sources.list.d/raspi.list \
# add coral repo
&& apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \
&& echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
&& echo "libedgetpu1-max libedgetpu/accepted-eula select true" | debconf-set-selections \
# enable non-free repo
&& sed -i -e's/ main/ main contrib non-free/g' /etc/apt/sources.list \
&& apt-get -qq update \
&& apt-get -qq install --no-install-recommends --no-install-suggests -y \
# coral drivers
libedgetpu1-max python3-tflite-runtime python3-pycoral \
&& pip3 install -U /wheels/*.whl \
# btbn-ffmpeg -> amd64 / arm64
&& if [ "${TARGETARCH}" = "amd64" ] || [ "${TARGETARCH}" = "arm64" ]; then \
mkdir -p /usr/lib/btbn-ffmpeg \
&& wget -O btbn-ffmpeg.tar.xz "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linux$( [ "$TARGETARCH" = "amd64" ] && echo "64" || echo "arm64" )-gpl-5.1.tar.xz" \
&& tar -xf btbn-ffmpeg.tar.xz -C /usr/lib/btbn-ffmpeg --strip-components 1 \
&& rm btbn-ffmpeg.tar.xz; \
fi \
# ffmpeg -> arm32
&& if [ "${TARGETARCH}" = "arm" ]; then \
apt-get -qq install --no-install-recommends --no-install-suggests -y ffmpeg; \
fi \
# arch specific packages
&& if [ "${TARGETARCH}" = "amd64" ]; then \
apt-get -qq install --no-install-recommends --no-install-suggests -y \
mesa-va-drivers libva-drm2 intel-media-va-driver-non-free i965-va-driver libmfx1; \
fi \
&& if [ "${TARGETARCH}" = "arm64" ]; then \
apt-get -qq install --no-install-recommends --no-install-suggests -y \
libva-drm2 mesa-va-drivers; \
fi \
# not sure why 32bit arm requires all these
&& if [ "${TARGETARCH}" = "arm" ]; then \
apt-get -qq install --no-install-recommends --no-install-suggests -y \
libgtk-3-dev \
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
gfortran openexr libatlas-base-dev libssl-dev\
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev; \
fi \
&& rm -rf /wheels \
&& apt-get remove gnupg apt-transport-https -y \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
ENV PATH=$PATH:/usr/lib/btbn-ffmpeg/bin
COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/
# get model and labels
COPY labelmap.txt /labelmap.txt
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite -O /edgetpu_model.tflite
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite -O /cpu_model.tflite
WORKDIR /opt/frigate/
ADD frigate frigate/
ADD migrations migrations/
COPY web/dist web/
COPY docker/rootfs/ /
# s6-overlay
RUN S6_ARCH="${TARGETARCH}" \
&& if [ "${TARGETARCH}" = "amd64" ]; then S6_ARCH="amd64"; fi \
&& if [ "${TARGETARCH}" = "arm" ]; then S6_ARCH="armhf"; fi \
&& if [ "${TARGETARCH}" = "arm64" ]; then S6_ARCH="aarch64"; fi \
&& wget -O /tmp/s6-overlay-installer "https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-${S6_ARCH}-installer" \
&& chmod +x /tmp/s6-overlay-installer && /tmp/s6-overlay-installer /
EXPOSE 5000
EXPOSE 1935
ENTRYPOINT ["/init"]
CMD ["python3", "-u", "-m", "frigate"]

View File

@ -1,28 +0,0 @@
FROM frigate-base
LABEL maintainer "blakeb@blakeshome.com"
ENV DEBIAN_FRONTEND=noninteractive
# Install packages for apt repo
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
# ffmpeg runtime dependencies
libgomp1 \
# runtime dependencies
libopenexr24 \
libgstreamer1.0-0 \
libgstreamer-plugins-base1.0-0 \
libopenblas-base \
libjpeg-turbo8 \
libpng16-16 \
libtiff5 \
libdc1394-22 \
&& rm -rf /var/lib/apt/lists/* \
&& (apt-get autoremove -y; apt-get autoclean -y)
# s6-overlay
ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-aarch64-installer /tmp/
RUN chmod +x /tmp/s6-overlay-aarch64-installer && /tmp/s6-overlay-aarch64-installer /
ENTRYPOINT ["/init"]
CMD ["python3", "-u", "-m", "frigate"]

View File

@ -1,28 +0,0 @@
FROM frigate-base
LABEL maintainer "blakeb@blakeshome.com"
# By default, use the i965 driver
ENV LIBVA_DRIVER_NAME=i965
# Install packages for apt repo
RUN wget -qO - https://repositories.intel.com/graphics/intel-graphics.key | apt-key add - \
&& echo 'deb [arch=amd64] https://repositories.intel.com/graphics/ubuntu focal main' > /etc/apt/sources.list.d/intel-graphics.list \
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F63F0F2B90935439 \
&& echo 'deb http://ppa.launchpad.net/kisak/kisak-mesa/ubuntu focal main' > /etc/apt/sources.list.d/kisak-mesa-focal.list
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
# ffmpeg dependencies
libgomp1 \
# VAAPI drivers for Intel hardware accel
libva-drm2 libva2 libmfx1 i965-va-driver vainfo intel-media-va-driver-non-free mesa-vdpau-drivers mesa-va-drivers mesa-vdpau-drivers libdrm-radeon1 \
&& rm -rf /var/lib/apt/lists/* \
&& (apt-get autoremove -y; apt-get autoclean -y)
# s6-overlay
ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-amd64-installer /tmp/
RUN chmod +x /tmp/s6-overlay-amd64-installer && /tmp/s6-overlay-amd64-installer /
ENTRYPOINT ["/init"]
CMD ["python3", "-u", "-m", "frigate"]

View File

@ -1,51 +0,0 @@
FROM frigate-base
LABEL maintainer "blakeb@blakeshome.com"
# Install packages for apt repo
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
# ffmpeg dependencies
libgomp1 \
&& rm -rf /var/lib/apt/lists/* \
&& (apt-get autoremove -y; apt-get autoclean -y)
# nvidia layer (see https://gitlab.com/nvidia/container-images/cuda/blob/master/dist/11.1/ubuntu20.04-x86_64/base/Dockerfile)
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
RUN apt-get update && apt-get install -y --no-install-recommends \
gnupg2 curl ca-certificates && \
curl -fsSL https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/7fa2af80.pub | apt-key add - && \
echo "deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64 /" > /etc/apt/sources.list.d/cuda.list && \
echo "deb https://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu2004/x86_64 /" > /etc/apt/sources.list.d/nvidia-ml.list && \
apt-get purge --autoremove -y curl \
&& rm -rf /var/lib/apt/lists/*
ENV CUDA_VERSION 11.1.1
# For libraries in the cuda-compat-* package: https://docs.nvidia.com/cuda/eula/index.html#attachment-a
RUN apt-get update && apt-get install -y --no-install-recommends \
cuda-cudart-11-1=11.1.74-1 \
cuda-compat-11-1 \
&& ln -s cuda-11.1 /usr/local/cuda && \
rm -rf /var/lib/apt/lists/*
# Required for nvidia-docker v1
RUN echo "/usr/local/nvidia/lib" >> /etc/ld.so.conf.d/nvidia.conf && \
echo "/usr/local/nvidia/lib64" >> /etc/ld.so.conf.d/nvidia.conf
ENV PATH /usr/local/nvidia/bin:/usr/local/cuda/bin:${PATH}
ENV LD_LIBRARY_PATH /usr/local/nvidia/lib:/usr/local/nvidia/lib64
# nvidia-container-runtime
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
ENV NVIDIA_REQUIRE_CUDA "cuda>=11.1 brand=tesla,driver>=418,driver<419 brand=tesla,driver>=440,driver<441 brand=tesla,driver>=450,driver<451"
# s6-overlay
ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-amd64-installer /tmp/
RUN chmod +x /tmp/s6-overlay-amd64-installer && /tmp/s6-overlay-amd64-installer /
ENTRYPOINT ["/init"]
CMD ["python3", "-u", "-m", "frigate"]

View File

@ -1,30 +0,0 @@
FROM frigate-base
LABEL maintainer "blakeb@blakeshome.com"
ENV DEBIAN_FRONTEND=noninteractive
# Install packages for apt repo
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
# ffmpeg runtime dependencies
libgomp1 \
# runtime dependencies
libopenexr24 \
libgstreamer1.0-0 \
libgstreamer-plugins-base1.0-0 \
libopenblas-base \
libjpeg-turbo8 \
libpng16-16 \
libtiff5 \
libdc1394-22 \
libaom0 \
libx265-179 \
&& rm -rf /var/lib/apt/lists/* \
&& (apt-get autoremove -y; apt-get autoclean -y)
# s6-overlay
ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-armhf-installer /tmp/
RUN chmod +x /tmp/s6-overlay-armhf-installer && /tmp/s6-overlay-armhf-installer /
ENTRYPOINT ["/init"]
CMD ["python3", "-u", "-m", "frigate"]

View File

@ -1,55 +0,0 @@
ARG ARCH=amd64
ARG WHEELS_VERSION
ARG FFMPEG_VERSION
ARG NGINX_VERSION
FROM blakeblackshear/frigate-wheels:${WHEELS_VERSION}-${ARCH} as wheels
FROM blakeblackshear/frigate-ffmpeg:${FFMPEG_VERSION}-${ARCH} as ffmpeg
FROM blakeblackshear/frigate-nginx:${NGINX_VERSION} as nginx
FROM frigate-web as web
FROM ubuntu:20.04
LABEL maintainer "blakeb@blakeshome.com"
COPY --from=ffmpeg /usr/local /usr/local/
COPY --from=wheels /wheels/. /wheels/
ENV FLASK_ENV=development
# ENV FONTCONFIG_PATH=/etc/fonts
ENV DEBIAN_FRONTEND=noninteractive
# Install packages for apt repo
RUN apt-get -qq update \
&& apt-get upgrade -y \
&& apt-get -qq install --no-install-recommends -y gnupg wget unzip tzdata libxml2 \
&& apt-get -qq install --no-install-recommends -y python3-pip \
&& pip3 install -U /wheels/*.whl \
&& APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \
&& echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
&& echo "libedgetpu1-max libedgetpu/accepted-eula select true" | debconf-set-selections \
&& apt-get -qq update && apt-get -qq install --no-install-recommends -y libedgetpu1-max python3-tflite-runtime python3-pycoral \
&& rm -rf /var/lib/apt/lists/* /wheels \
&& (apt-get autoremove -y; apt-get autoclean -y)
RUN pip3 install \
peewee_migrate \
pydantic \
zeroconf \
ws4py
COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/
# get model and labels
COPY labelmap.txt /labelmap.txt
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite -O /edgetpu_model.tflite
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite -O /cpu_model.tflite
WORKDIR /opt/frigate/
ADD frigate frigate/
ADD migrations migrations/
COPY --from=web /opt/frigate/build web/
COPY docker/rootfs/ /
EXPOSE 5000
EXPOSE 1935

View File

@ -6,7 +6,7 @@ ARG USER_GID=$USER_UID
# Create the user # Create the user
RUN groupadd --gid $USER_GID $USERNAME \ RUN groupadd --gid $USER_GID $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /bin/bash \
# #
# [Optional] Add sudo support. Omit if you don't need to install software after connecting. # [Optional] Add sudo support. Omit if you don't need to install software after connecting.
&& apt-get update \ && apt-get update \
@ -17,10 +17,11 @@ RUN groupadd --gid $USER_GID $USERNAME \
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y git curl vim htop && apt-get install -y git curl vim htop
RUN pip3 install pylint black COPY requirements-dev.txt /opt/frigate/requirements-dev.txt
RUN pip3 install -r requirements-dev.txt
# Install Node 14 # Install Node 16
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \ RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \
&& apt-get install -y nodejs && apt-get install -y nodejs
RUN npm install -g npm@latest RUN npm install -g npm@latest

View File

@ -1,486 +0,0 @@
# inspired by:
# https://github.com/collelog/ffmpeg/blob/master/4.3.1-alpine-rpi4-arm64v8.Dockerfile
# https://github.com/mmastrac/ffmpeg-omx-rpi-docker/blob/master/Dockerfile
# https://github.com/jrottenberg/ffmpeg/pull/158/files
# https://github.com/jrottenberg/ffmpeg/pull/239
FROM ubuntu:20.04 AS base
WORKDIR /tmp/workdir
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 xutils-dev && \
apt-get autoremove -y && \
apt-get clean -y
FROM base as build
ENV FFMPEG_VERSION=4.3.2 \
AOM_VERSION=v1.0.0 \
FDKAAC_VERSION=0.1.5 \
FREETYPE_VERSION=2.11.0 \
FRIBIDI_VERSION=0.19.7 \
KVAZAAR_VERSION=1.2.0 \
LAME_VERSION=3.100 \
LIBPTHREAD_STUBS_VERSION=0.4 \
LIBVIDSTAB_VERSION=1.1.0 \
LIBXCB_VERSION=1.13.1 \
XCBPROTO_VERSION=1.13 \
OGG_VERSION=1.3.2 \
OPENCOREAMR_VERSION=0.1.5 \
OPUS_VERSION=1.2 \
OPENJPEG_VERSION=2.1.2 \
THEORA_VERSION=1.1.1 \
VORBIS_VERSION=1.3.5 \
VPX_VERSION=1.8.0 \
WEBP_VERSION=1.0.2 \
X264_VERSION=20170226-2245-stable \
X265_VERSION=3.1.1 \
XAU_VERSION=1.0.9 \
XORG_MACROS_VERSION=1.19.2 \
XPROTO_VERSION=7.0.31 \
XVID_VERSION=1.3.4 \
LIBZMQ_VERSION=4.3.2 \
SRC=/usr/local
ARG FREETYPE_SHA256SUM="a45c6b403413abd5706f3582f04c8339d26397c4304b78fa552f2215df64101f freetype-2.11.0.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
ARG OPUS_SHA256SUM="77db45a87b51578fbc49555ef1b10926179861d854eb2613207dc79d9ec0a9a9 opus-1.2.tar.gz"
ARG THEORA_SHA256SUM="40952956c47811928d1e7922cda3bc1f427eb75680c3c37249c91e949054916b libtheora-1.1.1.tar.gz"
ARG VORBIS_SHA256SUM="6efbcecdd3e5dfbf090341b485da9d176eb250d893e3eb378c428a2db38301ce libvorbis-1.3.5.tar.gz"
ARG XVID_SHA256SUM="4e9fd62728885855bc5007fe1be58df42e5e274497591fec37249e1052ae316f xvidcore-1.3.4.tar.gz"
ARG LIBZMQ_SHA256SUM="02ecc88466ae38cf2c8d79f09cfd2675ba299a439680b64ade733e26a349edeb v4.3.2.tar.gz"
ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib
ARG MAKEFLAGS="-j2"
ARG PKG_CONFIG_PATH="/opt/ffmpeg/share/pkgconfig:/opt/ffmpeg/lib/pkgconfig:/opt/ffmpeg/lib64/pkgconfig"
ARG PREFIX=/opt/ffmpeg
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64:/usr/lib64:/usr/lib:/lib64:/lib"
RUN buildDeps="autoconf \
automake \
cmake \
curl \
bzip2 \
libexpat1-dev \
g++ \
gcc \
git \
gperf \
libtool \
make \
nasm \
perl \
pkg-config \
python \
libssl-dev \
yasm \
linux-headers-raspi2 \
libomxil-bellagio-dev \
zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
## opencore-amr https://sourceforge.net/projects/opencore-amr/
RUN \
DIR=/tmp/opencore-amr && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-${OPENCOREAMR_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## x264 http://www.videolan.org/developers/x264.html
RUN \
DIR=/tmp/x264 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \
tar -jx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared --enable-pic --disable-cli && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### x265 http://x265.org/
RUN \
DIR=/tmp/x265 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \
tar -zx && \
cd x265_${X265_VERSION}/build/linux && \
sed -i "/-DEXTRA_LIB/ s/$/ -DCMAKE_INSTALL_PREFIX=\${PREFIX}/" multilib.sh && \
sed -i "/^cmake/ s/$/ -DENABLE_CLI=OFF/" multilib.sh && \
export CXXFLAGS="${CXXFLAGS} -fPIC" && \
./multilib.sh && \
make -C 8bit install && \
rm -rf ${DIR}
### libogg https://www.xiph.org/ogg/
RUN \
DIR=/tmp/ogg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz && \
echo ${OGG_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libogg-${OGG_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libopus https://www.opus-codec.org/
RUN \
DIR=/tmp/opus && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://archive.mozilla.org/pub/opus/opus-${OPUS_VERSION}.tar.gz && \
echo ${OPUS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f opus-${OPUS_VERSION}.tar.gz && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libvorbis https://xiph.org/vorbis/
RUN \
DIR=/tmp/vorbis && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz && \
echo ${VORBIS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libvorbis-${VORBIS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libtheora http://www.theora.org/
RUN \
DIR=/tmp/theora && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.gz && \
echo ${THEORA_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libtheora-${THEORA_VERSION}.tar.gz && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libvpx https://www.webmproject.org/code/
RUN \
DIR=/tmp/vpx && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-vp8 --enable-vp9 --enable-vp9-highbitdepth --enable-pic --enable-shared \
--disable-debug --disable-examples --disable-docs --disable-install-bins && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libwebp https://developers.google.com/speed/webp/
RUN \
DIR=/tmp/vebp && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${WEBP_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libmp3lame http://lame.sourceforge.net/
RUN \
DIR=/tmp/lame && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/lame/lame/$(echo ${LAME_VERSION} | sed -e 's/[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)/\1.\2/')/lame-${LAME_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" --enable-shared --enable-nasm --disable-frontend && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### xvid https://www.xvid.com/
RUN \
DIR=/tmp/xvid && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz && \
echo ${XVID_SHA256SUM} | sha256sum --check && \
tar -zx -f xvidcore-${XVID_VERSION}.tar.gz && \
cd xvidcore/build/generic && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### fdk-aac https://github.com/mstorsjo/fdk-aac
RUN \
DIR=/tmp/fdk-aac && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared --datadir="${DIR}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## openjpeg https://github.com/uclouvain/openjpeg
RUN \
DIR=/tmp/openjpeg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
export CFLAGS="${CFLAGS} -DPNG_ARM_NEON_OPT=0" && \
cmake -DBUILD_THIRDPARTY:BOOL=ON -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## freetype https://www.freetype.org/
RUN \
DIR=/tmp/freetype && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz && \
echo ${FREETYPE_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f freetype-${FREETYPE_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## libvstab https://github.com/georgmartius/vid.stab
RUN \
DIR=/tmp/vid.stab && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/georgmartius/vid.stab/archive/v${LIBVIDSTAB_VERSION}.tar.gz && \
echo ${LIBVIDSTAB_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f v${LIBVIDSTAB_VERSION}.tar.gz && \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## fridibi https://www.fribidi.org/
RUN \
DIR=/tmp/fribidi && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/fribidi/fribidi/archive/${FRIBIDI_VERSION}.tar.gz && \
echo ${FRIBIDI_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f ${FRIBIDI_VERSION}.tar.gz && \
sed -i 's/^SUBDIRS =.*/SUBDIRS=gen.tab charset lib bin/' Makefile.am && \
./bootstrap --no-config --auto && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j1 && \
make -j $(nproc) install && \
rm -rf ${DIR}
## kvazaar https://github.com/ultravideo/kvazaar
RUN \
DIR=/tmp/kvazaar && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/ultravideo/kvazaar/archive/v${KVAZAAR_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f v${KVAZAAR_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/aom && \
git clone --branch ${AOM_VERSION} --depth 1 https://aomedia.googlesource.com/aom ${DIR} ; \
cd ${DIR} ; \
rm -rf CMakeCache.txt CMakeFiles ; \
mkdir -p ./aom_build ; \
cd ./aom_build ; \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" -DBUILD_SHARED_LIBS=1 ..; \
make ; \
make install ; \
rm -rf ${DIR}
## libxcb (and supporting libraries) for screen capture https://xcb.freedesktop.org/
RUN \
DIR=/tmp/xorg-macros && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive//individual/util/util-macros-${XORG_MACROS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f util-macros-${XORG_MACROS_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/xproto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/proto/xproto-${XPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xproto-${XPROTO_VERSION}.tar.gz && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libXau && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/lib/libXau-${XAU_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libXau-${XAU_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libpthread-stubs && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb-proto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libxcb-${LIBXCB_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libxcb-${LIBXCB_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## libzmq https://github.com/zeromq/libzmq/
RUN \
DIR=/tmp/libzmq && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/zeromq/libzmq/archive/v${LIBZMQ_VERSION}.tar.gz && \
echo ${LIBZMQ_SHA256SUM} | sha256sum --check && \
tar -xz --strip-components=1 -f v${LIBZMQ_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make -j $(nproc) && \
make check && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/rkmpp && \
mkdir -p ${DIR} && \
cd ${DIR} && \
git clone https://github.com/rockchip-linux/libdrm-rockchip && git clone https://github.com/rockchip-linux/mpp && \
cd libdrm-rockchip && bash autogen.sh && ./configure && make && make install && \
cd ../mpp && cmake -DRKPLATFORM=ON -DHAVE_DRM=ON && make -j6 && make install && \
rm -rf ${DIR}
## ffmpeg https://ffmpeg.org/
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
./configure \
--disable-debug \
--disable-doc \
--disable-ffplay \
--enable-shared \
--enable-avresample \
--enable-libopencore-amrnb \
--enable-libopencore-amrwb \
--enable-gpl \
--enable-libfreetype \
--enable-libvidstab \
--enable-libmp3lame \
--enable-libopus \
--enable-libtheora \
--enable-libvorbis \
--enable-libvpx \
--enable-libwebp \
--enable-libxcb \
--enable-libx265 \
--enable-libxvid \
--enable-libx264 \
--enable-nonfree \
--enable-openssl \
--enable-libfdk_aac \
--enable-postproc \
--enable-small \
--enable-version3 \
--enable-libzmq \
--extra-libs=-ldl \
--prefix="${PREFIX}" \
--enable-libopenjpeg \
--enable-libkvazaar \
--enable-libaom \
--extra-libs=-lpthread \
--enable-rkmpp \
--enable-libdrm \
# --enable-omx \
# --enable-omx-rpi \
# --enable-mmal \
--enable-v4l2_m2m \
--enable-neon \
--extra-cflags="-I${PREFIX}/include" \
--extra-ldflags="-L${PREFIX}/lib" && \
make -j $(nproc) && \
make -j $(nproc) install && \
make tools/zmqsend && cp tools/zmqsend ${PREFIX}/bin/ && \
make distclean && \
hash -r && \
cd tools && \
make qt-faststart && cp qt-faststart ${PREFIX}/bin/
## cleanup
RUN \
ldd ${PREFIX}/bin/ffmpeg | grep opt/ffmpeg | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
for lib in /usr/local/lib/*.so.*; do ln -s "${lib##*/}" "${lib%%.so.*}".so; done && \
cp ${PREFIX}/bin/* /usr/local/bin/ && \
cp -r ${PREFIX}/share/ffmpeg /usr/local/share/ && \
LD_LIBRARY_PATH=/usr/local/lib ffmpeg -buildconf && \
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
mkdir -p /usr/local/lib/pkgconfig && \
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
done
FROM base AS release
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64:/usr/lib:/usr/lib64:/lib:/lib64
CMD ["--help"]
ENTRYPOINT ["ffmpeg"]
COPY --from=build /usr/local /usr/local/
# Run ffmpeg with -c:v h264_v4l2m2m to enable HW accell for decoding on raspberry pi4 64-bit

View File

@ -1,468 +0,0 @@
# inspired by:
# https://github.com/collelog/ffmpeg/blob/master/4.3.1-alpine-rpi4-arm64v8.Dockerfile
# https://github.com/jrottenberg/ffmpeg/pull/158/files
# https://github.com/jrottenberg/ffmpeg/pull/239
FROM ubuntu:20.04 AS base
WORKDIR /tmp/workdir
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
apt-get autoremove -y && \
apt-get clean -y
FROM base as build
ENV FFMPEG_VERSION=4.3.2 \
AOM_VERSION=v1.0.0 \
FDKAAC_VERSION=0.1.5 \
FREETYPE_VERSION=2.5.5 \
FRIBIDI_VERSION=0.19.7 \
KVAZAAR_VERSION=1.2.0 \
LAME_VERSION=3.100 \
LIBPTHREAD_STUBS_VERSION=0.4 \
LIBVIDSTAB_VERSION=1.1.0 \
LIBXCB_VERSION=1.13.1 \
XCBPROTO_VERSION=1.13 \
OGG_VERSION=1.3.2 \
OPENCOREAMR_VERSION=0.1.5 \
OPUS_VERSION=1.2 \
OPENJPEG_VERSION=2.1.2 \
THEORA_VERSION=1.1.1 \
VORBIS_VERSION=1.3.5 \
VPX_VERSION=1.8.0 \
WEBP_VERSION=1.0.2 \
X264_VERSION=20170226-2245-stable \
X265_VERSION=3.1.1 \
XAU_VERSION=1.0.9 \
XORG_MACROS_VERSION=1.19.2 \
XPROTO_VERSION=7.0.31 \
XVID_VERSION=1.3.4 \
LIBZMQ_VERSION=4.3.2 \
SRC=/usr/local
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
ARG OPUS_SHA256SUM="77db45a87b51578fbc49555ef1b10926179861d854eb2613207dc79d9ec0a9a9 opus-1.2.tar.gz"
ARG THEORA_SHA256SUM="40952956c47811928d1e7922cda3bc1f427eb75680c3c37249c91e949054916b libtheora-1.1.1.tar.gz"
ARG VORBIS_SHA256SUM="6efbcecdd3e5dfbf090341b485da9d176eb250d893e3eb378c428a2db38301ce libvorbis-1.3.5.tar.gz"
ARG XVID_SHA256SUM="4e9fd62728885855bc5007fe1be58df42e5e274497591fec37249e1052ae316f xvidcore-1.3.4.tar.gz"
ARG LIBZMQ_SHA256SUM="02ecc88466ae38cf2c8d79f09cfd2675ba299a439680b64ade733e26a349edeb v4.3.2.tar.gz"
ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib
ARG MAKEFLAGS="-j2"
ARG PKG_CONFIG_PATH="/opt/ffmpeg/share/pkgconfig:/opt/ffmpeg/lib/pkgconfig:/opt/ffmpeg/lib64/pkgconfig"
ARG PREFIX=/opt/ffmpeg
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64:/usr/lib64:/usr/lib:/lib64:/lib"
RUN buildDeps="autoconf \
automake \
cmake \
curl \
bzip2 \
libexpat1-dev \
g++ \
gcc \
git \
gperf \
libtool \
make \
nasm \
perl \
pkg-config \
python \
libssl-dev \
yasm \
libva-dev \
libmfx-dev \
zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
## opencore-amr https://sourceforge.net/projects/opencore-amr/
RUN \
DIR=/tmp/opencore-amr && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-${OPENCOREAMR_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
## x264 http://www.videolan.org/developers/x264.html
RUN \
DIR=/tmp/x264 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \
tar -jx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared --enable-pic --disable-cli && \
make && \
make install && \
rm -rf ${DIR}
### x265 http://x265.org/
RUN \
DIR=/tmp/x265 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \
tar -zx && \
cd x265_${X265_VERSION}/build/linux && \
sed -i "/-DEXTRA_LIB/ s/$/ -DCMAKE_INSTALL_PREFIX=\${PREFIX}/" multilib.sh && \
sed -i "/^cmake/ s/$/ -DENABLE_CLI=OFF/" multilib.sh && \
./multilib.sh && \
make -C 8bit install && \
rm -rf ${DIR}
### libogg https://www.xiph.org/ogg/
RUN \
DIR=/tmp/ogg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz && \
echo ${OGG_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libogg-${OGG_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libopus https://www.opus-codec.org/
RUN \
DIR=/tmp/opus && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://archive.mozilla.org/pub/opus/opus-${OPUS_VERSION}.tar.gz && \
echo ${OPUS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f opus-${OPUS_VERSION}.tar.gz && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libvorbis https://xiph.org/vorbis/
RUN \
DIR=/tmp/vorbis && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz && \
echo ${VORBIS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libvorbis-${VORBIS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libtheora http://www.theora.org/
RUN \
DIR=/tmp/theora && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.gz && \
echo ${THEORA_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libtheora-${THEORA_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libvpx https://www.webmproject.org/code/
RUN \
DIR=/tmp/vpx && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-vp8 --enable-vp9 --enable-vp9-highbitdepth --enable-pic --enable-shared \
--disable-debug --disable-examples --disable-docs --disable-install-bins && \
make && \
make install && \
rm -rf ${DIR}
### libwebp https://developers.google.com/speed/webp/
RUN \
DIR=/tmp/vebp && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${WEBP_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libmp3lame http://lame.sourceforge.net/
RUN \
DIR=/tmp/lame && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/lame/lame/$(echo ${LAME_VERSION} | sed -e 's/[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)/\1.\2/')/lame-${LAME_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" --enable-shared --enable-nasm --disable-frontend && \
make && \
make install && \
rm -rf ${DIR}
### xvid https://www.xvid.com/
RUN \
DIR=/tmp/xvid && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz && \
echo ${XVID_SHA256SUM} | sha256sum --check && \
tar -zx -f xvidcore-${XVID_VERSION}.tar.gz && \
cd xvidcore/build/generic && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" && \
make && \
make install && \
rm -rf ${DIR}
### fdk-aac https://github.com/mstorsjo/fdk-aac
RUN \
DIR=/tmp/fdk-aac && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared --datadir="${DIR}" && \
make && \
make install && \
rm -rf ${DIR}
## openjpeg https://github.com/uclouvain/openjpeg
RUN \
DIR=/tmp/openjpeg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
cmake -DBUILD_THIRDPARTY:BOOL=ON -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make && \
make install && \
rm -rf ${DIR}
## freetype https://www.freetype.org/
RUN \
DIR=/tmp/freetype && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz && \
echo ${FREETYPE_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f freetype-${FREETYPE_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
## libvstab https://github.com/georgmartius/vid.stab
RUN \
DIR=/tmp/vid.stab && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/georgmartius/vid.stab/archive/v${LIBVIDSTAB_VERSION}.tar.gz && \
echo ${LIBVIDSTAB_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f v${LIBVIDSTAB_VERSION}.tar.gz && \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make && \
make install && \
rm -rf ${DIR}
## fridibi https://www.fribidi.org/
RUN \
DIR=/tmp/fribidi && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/fribidi/fribidi/archive/${FRIBIDI_VERSION}.tar.gz && \
echo ${FRIBIDI_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f ${FRIBIDI_VERSION}.tar.gz && \
sed -i 's/^SUBDIRS =.*/SUBDIRS=gen.tab charset lib bin/' Makefile.am && \
./bootstrap --no-config --auto && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j1 && \
make install && \
rm -rf ${DIR}
## kvazaar https://github.com/ultravideo/kvazaar
RUN \
DIR=/tmp/kvazaar && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/ultravideo/kvazaar/archive/v${KVAZAAR_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f v${KVAZAAR_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/aom && \
git clone --branch ${AOM_VERSION} --depth 1 https://aomedia.googlesource.com/aom ${DIR} ; \
cd ${DIR} ; \
rm -rf CMakeCache.txt CMakeFiles ; \
mkdir -p ./aom_build ; \
cd ./aom_build ; \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" -DBUILD_SHARED_LIBS=1 ..; \
make ; \
make install ; \
rm -rf ${DIR}
## libxcb (and supporting libraries) for screen capture https://xcb.freedesktop.org/
RUN \
DIR=/tmp/xorg-macros && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive//individual/util/util-macros-${XORG_MACROS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f util-macros-${XORG_MACROS_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/xproto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/proto/xproto-${XPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xproto-${XPROTO_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libXau && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/lib/libXau-${XAU_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libXau-${XAU_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libpthread-stubs && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb-proto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libxcb-${LIBXCB_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libxcb-${LIBXCB_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
## libzmq https://github.com/zeromq/libzmq/
RUN \
DIR=/tmp/libzmq && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/zeromq/libzmq/archive/v${LIBZMQ_VERSION}.tar.gz && \
echo ${LIBZMQ_SHA256SUM} | sha256sum --check && \
tar -xz --strip-components=1 -f v${LIBZMQ_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make && \
make check && \
make install && \
rm -rf ${DIR}
## ffmpeg https://ffmpeg.org/
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
./configure \
--disable-debug \
--disable-doc \
--disable-ffplay \
--enable-shared \
--enable-avresample \
--enable-libopencore-amrnb \
--enable-libopencore-amrwb \
--enable-gpl \
--enable-libfreetype \
--enable-libvidstab \
--enable-libmfx \
--enable-libmp3lame \
--enable-libopus \
--enable-libtheora \
--enable-libvorbis \
--enable-libvpx \
--enable-libwebp \
--enable-libxcb \
--enable-libx265 \
--enable-libxvid \
--enable-libx264 \
--enable-nonfree \
--enable-openssl \
--enable-libfdk_aac \
--enable-postproc \
--enable-small \
--enable-version3 \
--enable-libzmq \
--extra-libs=-ldl \
--prefix="${PREFIX}" \
--enable-libopenjpeg \
--enable-libkvazaar \
--enable-libaom \
--extra-libs=-lpthread \
--enable-vaapi \
--extra-cflags="-I${PREFIX}/include" \
--extra-ldflags="-L${PREFIX}/lib" && \
make && \
make install && \
make tools/zmqsend && cp tools/zmqsend ${PREFIX}/bin/ && \
make distclean && \
hash -r && \
cd tools && \
make qt-faststart && cp qt-faststart ${PREFIX}/bin/
## cleanup
RUN \
ldd ${PREFIX}/bin/ffmpeg | grep opt/ffmpeg | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
for lib in /usr/local/lib/*.so.*; do ln -s "${lib##*/}" "${lib%%.so.*}".so; done && \
cp ${PREFIX}/bin/* /usr/local/bin/ && \
cp -r ${PREFIX}/share/ffmpeg /usr/local/share/ && \
LD_LIBRARY_PATH=/usr/local/lib ffmpeg -buildconf && \
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
mkdir -p /usr/local/lib/pkgconfig && \
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
done
FROM base AS release
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64:/usr/lib:/usr/lib64:/lib:/lib64
CMD ["--help"]
ENTRYPOINT ["ffmpeg"]
COPY --from=build /usr/local /usr/local/
RUN \
apt-get update -y && \
apt-get install -y --no-install-recommends libva-drm2 libva2 i965-va-driver mesa-va-drivers && \
rm -rf /var/lib/apt/lists/*

View File

@ -1,549 +0,0 @@
# inspired by https://github.com/jrottenberg/ffmpeg/blob/master/docker-images/4.3/ubuntu1804/Dockerfile
# ffmpeg - http://ffmpeg.org/download.html
#
# From https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu
#
# https://hub.docker.com/r/jrottenberg/ffmpeg/
#
#
FROM nvidia/cuda:11.1-devel-ubuntu20.04 AS devel-base
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /tmp/workdir
RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
apt-get autoremove -y && \
apt-get clean -y
FROM nvidia/cuda:11.1-runtime-ubuntu20.04 AS runtime-base
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /tmp/workdir
RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 libxcb-shape0-dev && \
apt-get autoremove -y && \
apt-get clean -y
FROM devel-base as build
ENV NVIDIA_HEADERS_VERSION=9.1.23.1
ENV FFMPEG_VERSION=4.3.2 \
AOM_VERSION=v1.0.0 \
FDKAAC_VERSION=0.1.5 \
FREETYPE_VERSION=2.5.5 \
FRIBIDI_VERSION=0.19.7 \
KVAZAAR_VERSION=1.2.0 \
LAME_VERSION=3.100 \
LIBPTHREAD_STUBS_VERSION=0.4 \
LIBVIDSTAB_VERSION=1.1.0 \
LIBXCB_VERSION=1.13.1 \
XCBPROTO_VERSION=1.13 \
OGG_VERSION=1.3.2 \
OPENCOREAMR_VERSION=0.1.5 \
OPUS_VERSION=1.2 \
OPENJPEG_VERSION=2.1.2 \
THEORA_VERSION=1.1.1 \
VORBIS_VERSION=1.3.5 \
VPX_VERSION=1.8.0 \
WEBP_VERSION=1.0.2 \
X264_VERSION=20170226-2245-stable \
X265_VERSION=3.1.1 \
XAU_VERSION=1.0.9 \
XORG_MACROS_VERSION=1.19.2 \
XPROTO_VERSION=7.0.31 \
XVID_VERSION=1.3.4 \
LIBZMQ_VERSION=4.3.2 \
LIBSRT_VERSION=1.4.1 \
LIBARIBB24_VERSION=1.0.3 \
LIBPNG_VERSION=1.6.9 \
SRC=/usr/local
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
ARG OPUS_SHA256SUM="77db45a87b51578fbc49555ef1b10926179861d854eb2613207dc79d9ec0a9a9 opus-1.2.tar.gz"
ARG THEORA_SHA256SUM="40952956c47811928d1e7922cda3bc1f427eb75680c3c37249c91e949054916b libtheora-1.1.1.tar.gz"
ARG VORBIS_SHA256SUM="6efbcecdd3e5dfbf090341b485da9d176eb250d893e3eb378c428a2db38301ce libvorbis-1.3.5.tar.gz"
ARG XVID_SHA256SUM="4e9fd62728885855bc5007fe1be58df42e5e274497591fec37249e1052ae316f xvidcore-1.3.4.tar.gz"
ARG LIBZMQ_SHA256SUM="02ecc88466ae38cf2c8d79f09cfd2675ba299a439680b64ade733e26a349edeb v4.3.2.tar.gz"
ARG LIBARIBB24_SHA256SUM="f61560738926e57f9173510389634d8c06cabedfa857db4b28fb7704707ff128 v1.0.3.tar.gz"
ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib
ARG MAKEFLAGS="-j2"
ARG PKG_CONFIG_PATH="/opt/ffmpeg/share/pkgconfig:/opt/ffmpeg/lib/pkgconfig:/opt/ffmpeg/lib64/pkgconfig"
ARG PREFIX=/opt/ffmpeg
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64"
RUN buildDeps="autoconf \
automake \
cmake \
curl \
bzip2 \
libexpat1-dev \
g++ \
gcc \
git \
gperf \
libtool \
make \
nasm \
perl \
pkg-config \
python \
libssl-dev \
yasm \
zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
RUN \
DIR=/tmp/nv-codec-headers && \
git clone https://github.com/FFmpeg/nv-codec-headers ${DIR} && \
cd ${DIR} && \
git checkout n${NVIDIA_HEADERS_VERSION} && \
make PREFIX="${PREFIX}" && \
make install PREFIX="${PREFIX}" && \
rm -rf ${DIR}
## opencore-amr https://sourceforge.net/projects/opencore-amr/
RUN \
DIR=/tmp/opencore-amr && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-${OPENCOREAMR_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
## x264 http://www.videolan.org/developers/x264.html
RUN \
DIR=/tmp/x264 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \
tar -jx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared --enable-pic --disable-cli && \
make && \
make install && \
rm -rf ${DIR}
### x265 http://x265.org/
RUN \
DIR=/tmp/x265 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \
tar -zx && \
cd x265_${X265_VERSION}/build/linux && \
sed -i "/-DEXTRA_LIB/ s/$/ -DCMAKE_INSTALL_PREFIX=\${PREFIX}/" multilib.sh && \
sed -i "/^cmake/ s/$/ -DENABLE_CLI=OFF/" multilib.sh && \
./multilib.sh && \
make -C 8bit install && \
rm -rf ${DIR}
### libogg https://www.xiph.org/ogg/
RUN \
DIR=/tmp/ogg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz && \
echo ${OGG_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libogg-${OGG_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libopus https://www.opus-codec.org/
RUN \
DIR=/tmp/opus && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://archive.mozilla.org/pub/opus/opus-${OPUS_VERSION}.tar.gz && \
echo ${OPUS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f opus-${OPUS_VERSION}.tar.gz && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libvorbis https://xiph.org/vorbis/
RUN \
DIR=/tmp/vorbis && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz && \
echo ${VORBIS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libvorbis-${VORBIS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libtheora http://www.theora.org/
RUN \
DIR=/tmp/theora && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.gz && \
echo ${THEORA_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libtheora-${THEORA_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libvpx https://www.webmproject.org/code/
RUN \
DIR=/tmp/vpx && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-vp8 --enable-vp9 --enable-vp9-highbitdepth --enable-pic --enable-shared \
--disable-debug --disable-examples --disable-docs --disable-install-bins && \
make && \
make install && \
rm -rf ${DIR}
### libwebp https://developers.google.com/speed/webp/
RUN \
DIR=/tmp/vebp && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${WEBP_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libmp3lame http://lame.sourceforge.net/
RUN \
DIR=/tmp/lame && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/lame/lame/$(echo ${LAME_VERSION} | sed -e 's/[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)/\1.\2/')/lame-${LAME_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" --enable-shared --enable-nasm --disable-frontend && \
make && \
make install && \
rm -rf ${DIR}
### xvid https://www.xvid.com/
RUN \
DIR=/tmp/xvid && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz && \
echo ${XVID_SHA256SUM} | sha256sum --check && \
tar -zx -f xvidcore-${XVID_VERSION}.tar.gz && \
cd xvidcore/build/generic && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" && \
make && \
make install && \
rm -rf ${DIR}
### fdk-aac https://github.com/mstorsjo/fdk-aac
RUN \
DIR=/tmp/fdk-aac && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared --datadir="${DIR}" && \
make && \
make install && \
rm -rf ${DIR}
## openjpeg https://github.com/uclouvain/openjpeg
RUN \
DIR=/tmp/openjpeg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
cmake -DBUILD_THIRDPARTY:BOOL=ON -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make && \
make install && \
rm -rf ${DIR}
## freetype https://www.freetype.org/
RUN \
DIR=/tmp/freetype && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz && \
echo ${FREETYPE_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f freetype-${FREETYPE_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
## libvstab https://github.com/georgmartius/vid.stab
RUN \
DIR=/tmp/vid.stab && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/georgmartius/vid.stab/archive/v${LIBVIDSTAB_VERSION}.tar.gz && \
echo ${LIBVIDSTAB_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f v${LIBVIDSTAB_VERSION}.tar.gz && \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make && \
make install && \
rm -rf ${DIR}
## fridibi https://www.fribidi.org/
RUN \
DIR=/tmp/fribidi && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/fribidi/fribidi/archive/${FRIBIDI_VERSION}.tar.gz && \
echo ${FRIBIDI_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f ${FRIBIDI_VERSION}.tar.gz && \
sed -i 's/^SUBDIRS =.*/SUBDIRS=gen.tab charset lib bin/' Makefile.am && \
./bootstrap --no-config --auto && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j1 && \
make install && \
rm -rf ${DIR}
## kvazaar https://github.com/ultravideo/kvazaar
RUN \
DIR=/tmp/kvazaar && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/ultravideo/kvazaar/archive/v${KVAZAAR_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f v${KVAZAAR_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/aom && \
git clone --branch ${AOM_VERSION} --depth 1 https://aomedia.googlesource.com/aom ${DIR} ; \
cd ${DIR} ; \
rm -rf CMakeCache.txt CMakeFiles ; \
mkdir -p ./aom_build ; \
cd ./aom_build ; \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" -DBUILD_SHARED_LIBS=1 ..; \
make ; \
make install ; \
rm -rf ${DIR}
## libxcb (and supporting libraries) for screen capture https://xcb.freedesktop.org/
RUN \
DIR=/tmp/xorg-macros && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive//individual/util/util-macros-${XORG_MACROS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f util-macros-${XORG_MACROS_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/xproto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/proto/xproto-${XPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xproto-${XPROTO_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libXau && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/lib/libXau-${XAU_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libXau-${XAU_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libpthread-stubs && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb-proto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libxcb-${LIBXCB_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libxcb-${LIBXCB_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
## libzmq https://github.com/zeromq/libzmq/
RUN \
DIR=/tmp/libzmq && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/zeromq/libzmq/archive/v${LIBZMQ_VERSION}.tar.gz && \
echo ${LIBZMQ_SHA256SUM} | sha256sum --check && \
tar -xz --strip-components=1 -f v${LIBZMQ_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make && \
make check && \
make install && \
rm -rf ${DIR}
## libsrt https://github.com/Haivision/srt
RUN \
DIR=/tmp/srt && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/Haivision/srt/archive/v${LIBSRT_VERSION}.tar.gz && \
tar -xz --strip-components=1 -f v${LIBSRT_VERSION}.tar.gz && \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make && \
make install && \
rm -rf ${DIR}
## libpng
RUN \
DIR=/tmp/png && \
mkdir -p ${DIR} && \
cd ${DIR} && \
git clone https://git.code.sf.net/p/libpng/code ${DIR} -b v${LIBPNG_VERSION} --depth 1 && \
./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make check && \
make install && \
rm -rf ${DIR}
## libaribb24
RUN \
DIR=/tmp/b24 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/nkoriyama/aribb24/archive/v${LIBARIBB24_VERSION}.tar.gz && \
echo ${LIBARIBB24_SHA256SUM} | sha256sum --check && \
tar -xz --strip-components=1 -f v${LIBARIBB24_VERSION}.tar.gz && \
autoreconf -fiv && \
./configure CFLAGS="-I${PREFIX}/include -fPIC" --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
## ffmpeg https://ffmpeg.org/
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
./configure \
--disable-debug \
--disable-doc \
--disable-ffplay \
--enable-shared \
--enable-avresample \
--enable-libopencore-amrnb \
--enable-libopencore-amrwb \
--enable-gpl \
--enable-libfreetype \
--enable-libvidstab \
--enable-libmp3lame \
--enable-libopus \
--enable-libtheora \
--enable-libvorbis \
--enable-libvpx \
--enable-libwebp \
--enable-libxcb \
--enable-libx265 \
--enable-libxvid \
--enable-libx264 \
--enable-nonfree \
--enable-openssl \
--enable-libfdk_aac \
--enable-postproc \
--enable-small \
--enable-version3 \
--enable-libzmq \
--extra-libs=-ldl \
--prefix="${PREFIX}" \
--enable-libopenjpeg \
--enable-libkvazaar \
--enable-libaom \
--extra-libs=-lpthread \
--enable-libsrt \
--enable-libaribb24 \
--enable-nvenc \
--enable-cuda \
--enable-cuvid \
--enable-libnpp \
--extra-cflags="-I${PREFIX}/include -I${PREFIX}/include/ffnvcodec -I/usr/local/cuda/include/" \
--extra-ldflags="-L${PREFIX}/lib -L/usr/local/cuda/lib64 -L/usr/local/cuda/lib32/" && \
make && \
make install && \
make tools/zmqsend && cp tools/zmqsend ${PREFIX}/bin/ && \
make distclean && \
hash -r && \
cd tools && \
make qt-faststart && cp qt-faststart ${PREFIX}/bin/
## cleanup
RUN \
LD_LIBRARY_PATH="${PREFIX}/lib:${PREFIX}/lib64:${LD_LIBRARY_PATH}" ldd ${PREFIX}/bin/ffmpeg | grep opt/ffmpeg | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
for lib in /usr/local/lib/*.so.*; do ln -s "${lib##*/}" "${lib%%.so.*}".so; done && \
cp ${PREFIX}/bin/* /usr/local/bin/ && \
cp -r ${PREFIX}/share/* /usr/local/share/ && \
LD_LIBRARY_PATH=/usr/local/lib ffmpeg -buildconf && \
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
mkdir -p /usr/local/lib/pkgconfig && \
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
sed "s:${PREFIX}:/usr/local:g; s:/lib64:/lib:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
done
FROM runtime-base AS release
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64
CMD ["--help"]
ENTRYPOINT ["ffmpeg"]
# copy only needed files, without copying nvidia dev files
COPY --from=build /usr/local/bin /usr/local/bin/
COPY --from=build /usr/local/share /usr/local/share/
COPY --from=build /usr/local/lib /usr/local/lib/
COPY --from=build /usr/local/include /usr/local/include/
# Let's make sure the app built correctly
# Convenient to verify on https://hub.docker.com/r/jrottenberg/ffmpeg/builds/ console output

View File

@ -1,490 +0,0 @@
# inspired by:
# https://github.com/collelog/ffmpeg/blob/master/4.3.1-alpine-rpi4-arm64v8.Dockerfile
# https://github.com/mmastrac/ffmpeg-omx-rpi-docker/blob/master/Dockerfile
# https://github.com/jrottenberg/ffmpeg/pull/158/files
# https://github.com/jrottenberg/ffmpeg/pull/239
FROM ubuntu:20.04 AS base
WORKDIR /tmp/workdir
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
apt-get autoremove -y && \
apt-get clean -y
FROM base as build
ENV FFMPEG_VERSION=4.3.2 \
AOM_VERSION=v1.0.0 \
FDKAAC_VERSION=0.1.5 \
FREETYPE_VERSION=2.5.5 \
FRIBIDI_VERSION=0.19.7 \
KVAZAAR_VERSION=1.2.0 \
LAME_VERSION=3.100 \
LIBPTHREAD_STUBS_VERSION=0.4 \
LIBVIDSTAB_VERSION=1.1.0 \
LIBXCB_VERSION=1.13.1 \
XCBPROTO_VERSION=1.13 \
OGG_VERSION=1.3.2 \
OPENCOREAMR_VERSION=0.1.5 \
OPUS_VERSION=1.2 \
OPENJPEG_VERSION=2.1.2 \
THEORA_VERSION=1.1.1 \
VORBIS_VERSION=1.3.5 \
VPX_VERSION=1.8.0 \
WEBP_VERSION=1.0.2 \
X264_VERSION=20170226-2245-stable \
X265_VERSION=3.1.1 \
XAU_VERSION=1.0.9 \
XORG_MACROS_VERSION=1.19.2 \
XPROTO_VERSION=7.0.31 \
XVID_VERSION=1.3.4 \
LIBZMQ_VERSION=4.3.3 \
SRC=/usr/local
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
ARG OPUS_SHA256SUM="77db45a87b51578fbc49555ef1b10926179861d854eb2613207dc79d9ec0a9a9 opus-1.2.tar.gz"
ARG THEORA_SHA256SUM="40952956c47811928d1e7922cda3bc1f427eb75680c3c37249c91e949054916b libtheora-1.1.1.tar.gz"
ARG VORBIS_SHA256SUM="6efbcecdd3e5dfbf090341b485da9d176eb250d893e3eb378c428a2db38301ce libvorbis-1.3.5.tar.gz"
ARG XVID_SHA256SUM="4e9fd62728885855bc5007fe1be58df42e5e274497591fec37249e1052ae316f xvidcore-1.3.4.tar.gz"
ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib
ARG MAKEFLAGS="-j2"
ARG PKG_CONFIG_PATH="/opt/ffmpeg/share/pkgconfig:/opt/ffmpeg/lib/pkgconfig:/opt/ffmpeg/lib64/pkgconfig:/opt/vc/lib/pkgconfig"
ARG PREFIX=/opt/ffmpeg
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64:/usr/lib64:/usr/lib:/lib64:/lib:/opt/vc/lib"
RUN buildDeps="autoconf \
automake \
cmake \
curl \
bzip2 \
libexpat1-dev \
g++ \
gcc \
git \
gperf \
libtool \
make \
nasm \
perl \
pkg-config \
python \
sudo \
libssl-dev \
yasm \
linux-headers-raspi2 \
libomxil-bellagio-dev \
libx265-dev \
libaom-dev \
zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
## opencore-amr https://sourceforge.net/projects/opencore-amr/
RUN \
DIR=/tmp/opencore-amr && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-${OPENCOREAMR_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## x264 http://www.videolan.org/developers/x264.html
RUN \
DIR=/tmp/x264 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \
tar -jx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared --enable-pic --disable-cli && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
# ### x265 http://x265.org/
# RUN \
# DIR=/tmp/x265 && \
# mkdir -p ${DIR} && \
# cd ${DIR} && \
# curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \
# tar -zx && \
# cd x265_${X265_VERSION}/build/linux && \
# sed -i "/-DEXTRA_LIB/ s/$/ -DCMAKE_INSTALL_PREFIX=\${PREFIX}/" multilib.sh && \
# sed -i "/^cmake/ s/$/ -DENABLE_CLI=OFF/" multilib.sh && \
# # export CXXFLAGS="${CXXFLAGS} -fPIC" && \
# ./multilib.sh && \
# make -C 8bit install && \
# rm -rf ${DIR}
### libogg https://www.xiph.org/ogg/
RUN \
DIR=/tmp/ogg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz && \
echo ${OGG_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libogg-${OGG_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libopus https://www.opus-codec.org/
RUN \
DIR=/tmp/opus && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://archive.mozilla.org/pub/opus/opus-${OPUS_VERSION}.tar.gz && \
echo ${OPUS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f opus-${OPUS_VERSION}.tar.gz && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libvorbis https://xiph.org/vorbis/
RUN \
DIR=/tmp/vorbis && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz && \
echo ${VORBIS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libvorbis-${VORBIS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libtheora http://www.theora.org/
RUN \
DIR=/tmp/theora && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.gz && \
echo ${THEORA_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libtheora-${THEORA_VERSION}.tar.gz && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libvpx https://www.webmproject.org/code/
RUN \
DIR=/tmp/vpx && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-vp8 --enable-vp9 --enable-vp9-highbitdepth --enable-pic --enable-shared \
--disable-debug --disable-examples --disable-docs --disable-install-bins && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libwebp https://developers.google.com/speed/webp/
RUN \
DIR=/tmp/vebp && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${WEBP_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libmp3lame http://lame.sourceforge.net/
RUN \
DIR=/tmp/lame && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/lame/lame/$(echo ${LAME_VERSION} | sed -e 's/[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)/\1.\2/')/lame-${LAME_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" --enable-shared --enable-nasm --disable-frontend && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### xvid https://www.xvid.com/
RUN \
DIR=/tmp/xvid && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz && \
echo ${XVID_SHA256SUM} | sha256sum --check && \
tar -zx -f xvidcore-${XVID_VERSION}.tar.gz && \
cd xvidcore/build/generic && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### fdk-aac https://github.com/mstorsjo/fdk-aac
RUN \
DIR=/tmp/fdk-aac && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared --datadir="${DIR}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## openjpeg https://github.com/uclouvain/openjpeg
RUN \
DIR=/tmp/openjpeg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
export CFLAGS="${CFLAGS} -DPNG_ARM_NEON_OPT=0" && \
cmake -DBUILD_THIRDPARTY:BOOL=ON -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## freetype https://www.freetype.org/
RUN \
DIR=/tmp/freetype && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz && \
echo ${FREETYPE_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f freetype-${FREETYPE_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## libvstab https://github.com/georgmartius/vid.stab
RUN \
DIR=/tmp/vid.stab && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/georgmartius/vid.stab/archive/v${LIBVIDSTAB_VERSION}.tar.gz && \
echo ${LIBVIDSTAB_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f v${LIBVIDSTAB_VERSION}.tar.gz && \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## fridibi https://www.fribidi.org/
RUN \
DIR=/tmp/fribidi && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/fribidi/fribidi/archive/${FRIBIDI_VERSION}.tar.gz && \
echo ${FRIBIDI_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f ${FRIBIDI_VERSION}.tar.gz && \
sed -i 's/^SUBDIRS =.*/SUBDIRS=gen.tab charset lib bin/' Makefile.am && \
./bootstrap --no-config --auto && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j1 && \
make -j $(nproc) install && \
rm -rf ${DIR}
## kvazaar https://github.com/ultravideo/kvazaar
RUN \
DIR=/tmp/kvazaar && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/ultravideo/kvazaar/archive/v${KVAZAAR_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f v${KVAZAAR_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
# RUN \
# DIR=/tmp/aom && \
# git clone --branch ${AOM_VERSION} --depth 1 https://aomedia.googlesource.com/aom ${DIR} ; \
# cd ${DIR} ; \
# rm -rf CMakeCache.txt CMakeFiles ; \
# mkdir -p ./aom_build ; \
# cd ./aom_build ; \
# cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" -DBUILD_SHARED_LIBS=1 ..; \
# make ; \
# make install ; \
# rm -rf ${DIR}
## libxcb (and supporting libraries) for screen capture https://xcb.freedesktop.org/
RUN \
DIR=/tmp/xorg-macros && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive//individual/util/util-macros-${XORG_MACROS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f util-macros-${XORG_MACROS_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/xproto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/proto/xproto-${XPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xproto-${XPROTO_VERSION}.tar.gz && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libXau && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/lib/libXau-${XAU_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libXau-${XAU_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libpthread-stubs && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb-proto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libxcb-${LIBXCB_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libxcb-${LIBXCB_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## libzmq https://github.com/zeromq/libzmq/
RUN \
DIR=/tmp/libzmq && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/zeromq/libzmq/archive/v${LIBZMQ_VERSION}.tar.gz && \
tar -xz --strip-components=1 -f v${LIBZMQ_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make -j $(nproc) && \
# make check && \
make -j $(nproc) install && \
rm -rf ${DIR}
## userland https://github.com/raspberrypi/userland
RUN \
DIR=/tmp/userland && \
mkdir -p ${DIR} && \
cd ${DIR} && \
git clone --depth 1 https://github.com/raspberrypi/userland.git . && \
./buildme && \
rm -rf ${DIR}
## ffmpeg https://ffmpeg.org/
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
./configure \
--disable-debug \
--disable-doc \
--disable-ffplay \
--enable-shared \
--enable-avresample \
--enable-libopencore-amrnb \
--enable-libopencore-amrwb \
--enable-gpl \
--enable-libfreetype \
--enable-libvidstab \
--enable-libmp3lame \
--enable-libopus \
--enable-libtheora \
--enable-libvorbis \
--enable-libvpx \
--enable-libwebp \
--enable-libxcb \
--enable-libx265 \
--enable-libxvid \
--enable-libx264 \
--enable-nonfree \
--enable-openssl \
--enable-libfdk_aac \
--enable-postproc \
--enable-small \
--enable-version3 \
--enable-libzmq \
--extra-libs=-ldl \
--prefix="${PREFIX}" \
--enable-libopenjpeg \
--enable-libkvazaar \
--enable-libaom \
--extra-libs=-lpthread \
--enable-omx \
--enable-omx-rpi \
--enable-mmal \
--enable-v4l2_m2m \
--enable-neon \
--extra-cflags="-I${PREFIX}/include" \
--extra-ldflags="-L${PREFIX}/lib" && \
make -j $(nproc) && \
make -j $(nproc) install && \
make tools/zmqsend && cp tools/zmqsend ${PREFIX}/bin/ && \
make distclean && \
hash -r && \
cd tools && \
make qt-faststart && cp qt-faststart ${PREFIX}/bin/
## cleanup
RUN \
ldd ${PREFIX}/bin/ffmpeg | grep opt/ffmpeg | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
# copy userland lib too
ldd ${PREFIX}/bin/ffmpeg | grep opt/vc | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
for lib in /usr/local/lib/*.so.*; do ln -s "${lib##*/}" "${lib%%.so.*}".so; done && \
cp ${PREFIX}/bin/* /usr/local/bin/ && \
cp -r ${PREFIX}/share/ffmpeg /usr/local/share/ && \
LD_LIBRARY_PATH=/usr/local/lib ffmpeg -buildconf && \
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
mkdir -p /usr/local/lib/pkgconfig && \
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
done
FROM base AS release
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64:/usr/lib:/usr/lib64:/lib:/lib64
RUN \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends libx265-dev libaom-dev && \
apt-get autoremove -y && \
apt-get clean -y
CMD ["--help"]
ENTRYPOINT ["ffmpeg"]
COPY --from=build /usr/local /usr/local/

View File

@ -1,52 +0,0 @@
FROM ubuntu:20.04 AS base
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
apt-get autoremove -y && \
apt-get clean -y
FROM base as build
ARG NGINX_VERSION=1.18.0
ARG VOD_MODULE_VERSION=1.28
ARG SECURE_TOKEN_MODULE_VERSION=1.4
ARG RTMP_MODULE_VERSION=1.2.1
RUN cp /etc/apt/sources.list /etc/apt/sources.list~ \
&& sed -Ei 's/^# deb-src /deb-src /' /etc/apt/sources.list \
&& apt-get update
RUN apt-get -yqq build-dep nginx
RUN apt-get -yqq install --no-install-recommends curl \
&& mkdir /tmp/nginx \
&& curl -sL https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz | tar -C /tmp/nginx -zx --strip-components=1 \
&& mkdir /tmp/nginx-vod-module \
&& curl -sL https://github.com/kaltura/nginx-vod-module/archive/refs/tags/${VOD_MODULE_VERSION}.tar.gz | tar -C /tmp/nginx-vod-module -zx --strip-components=1 \
# Patch MAX_CLIPS to allow more clips to be added than the default 128
&& sed -i 's/MAX_CLIPS (128)/MAX_CLIPS (1080)/g' /tmp/nginx-vod-module/vod/media_set.h \
&& mkdir /tmp/nginx-secure-token-module \
&& curl -sL https://github.com/kaltura/nginx-secure-token-module/archive/refs/tags/${SECURE_TOKEN_MODULE_VERSION}.tar.gz | tar -C /tmp/nginx-secure-token-module -zx --strip-components=1 \
&& mkdir /tmp/nginx-rtmp-module \
&& curl -sL https://github.com/arut/nginx-rtmp-module/archive/refs/tags/v${RTMP_MODULE_VERSION}.tar.gz | tar -C /tmp/nginx-rtmp-module -zx --strip-components=1
WORKDIR /tmp/nginx
RUN ./configure --prefix=/usr/local/nginx \
--with-file-aio \
--with-http_sub_module \
--with-http_ssl_module \
--with-threads \
--add-module=../nginx-vod-module \
--add-module=../nginx-secure-token-module \
--add-module=../nginx-rtmp-module \
--with-cc-opt="-O3 -Wno-error=implicit-fallthrough"
RUN make && make install
RUN rm -rf /usr/local/nginx/html /usr/local/nginx/conf/*.default
FROM base
COPY --from=build /usr/local/nginx /usr/local/nginx
ENTRYPOINT ["/usr/local/nginx/sbin/nginx"]
CMD ["-g", "daemon off;"]

View File

@ -1,9 +0,0 @@
ARG NODE_VERSION=14.0
FROM node:${NODE_VERSION}
WORKDIR /opt/frigate
COPY . .
RUN npm install && npm run build

View File

@ -1,41 +0,0 @@
FROM ubuntu:20.04 as build
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -qq update \
&& apt-get -qq install -y \
python3 \
python3-dev \
wget \
# opencv dependencies
build-essential cmake git pkg-config libgtk-3-dev \
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
gfortran openexr libatlas-base-dev libssl-dev\
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \
# scipy dependencies
gcc gfortran libopenblas-dev liblapack-dev cython
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
&& python3 get-pip.py "pip==20.2.4"
RUN pip3 install scikit-build
RUN pip3 wheel --wheel-dir=/wheels \
opencv-python-headless \
numpy \
imutils \
scipy \
psutil \
Flask \
paho-mqtt \
PyYAML \
matplotlib \
click \
setproctitle \
peewee
FROM scratch
COPY --from=build /wheels /wheels

View File

@ -30,17 +30,17 @@ http {
gzip_vary on; gzip_vary on;
upstream frigate_api { upstream frigate_api {
server localhost:5001; server 127.0.0.1:5001;
keepalive 1024; keepalive 1024;
} }
upstream mqtt_ws { upstream mqtt_ws {
server localhost:5002; server 127.0.0.1:5002;
keepalive 1024; keepalive 1024;
} }
upstream jsmpeg { upstream jsmpeg {
server localhost:8082; server 127.0.0.1:8082;
keepalive 1024; keepalive 1024;
} }
@ -55,6 +55,7 @@ http {
vod_upstream_location /api; vod_upstream_location /api;
vod_align_segments_to_key_frames on; vod_align_segments_to_key_frames on;
vod_manifest_segment_durations_mode accurate; vod_manifest_segment_durations_mode accurate;
vod_ignore_edit_list on;
# vod caches # vod caches
vod_metadata_cache metadata_cache 512m; vod_metadata_cache metadata_cache 512m;
@ -81,11 +82,13 @@ http {
add_header Access-Control-Expose-Headers 'Server,range,Content-Length,Content-Range'; add_header Access-Control-Expose-Headers 'Server,range,Content-Length,Content-Range';
add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS'; add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS';
add_header Access-Control-Allow-Origin '*'; add_header Access-Control-Allow-Origin '*';
expires -1; add_header Cache-Control "no-store";
expires off;
} }
location /stream/ { location /stream/ {
add_header 'Cache-Control' 'no-cache'; add_header Cache-Control "no-store";
expires off;
add_header 'Access-Control-Allow-Origin' "$http_origin" always; add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Expose-Headers' 'Content-Length'; add_header 'Access-Control-Expose-Headers' 'Content-Length';
@ -170,10 +173,23 @@ http {
proxy_set_header Host $host; proxy_set_header Host $host;
} }
location /api/ { location ~* /api/.*\.(jpg|jpeg|png)$ {
add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
rewrite ^/api/(.*)$ $1 break;
proxy_pass http://frigate_api;
proxy_pass_request_headers on;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/ {
add_header Cache-Control "no-store"; add_header Cache-Control "no-store";
expires off;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
proxy_pass http://frigate_api/; proxy_pass http://frigate_api/;
proxy_pass_request_headers on; proxy_pass_request_headers on;
proxy_set_header Host $host; proxy_set_header Host $host;
@ -181,21 +197,23 @@ http {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
location / { location / {
add_header Cache-Control "no-cache"; add_header Cache-Control "no-store";
expires off;
location ~* \.(?:js|css|svg|ico|png)$ { location /assets/ {
access_log off; access_log off;
expires 1y; expires 1y;
add_header Cache-Control "public"; add_header Cache-Control "public";
} }
sub_filter 'href="/' 'href="$http_x_ingress_path/'; sub_filter 'href="/BASE_PATH/' 'href="$http_x_ingress_path/';
sub_filter 'url(/' 'url($http_x_ingress_path/'; sub_filter 'url(/BASE_PATH/' 'url($http_x_ingress_path/';
sub_filter '"/dist/' '"$http_x_ingress_path/dist/'; sub_filter '"/BASE_PATH/dist/' '"$http_x_ingress_path/dist/';
sub_filter '"/js/' '"$http_x_ingress_path/js/'; sub_filter '"/BASE_PATH/js/' '"$http_x_ingress_path/js/';
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path";</script>'; sub_filter '"/BASE_PATH/assets/' '"$http_x_ingress_path/assets/';
sub_filter '="/BASE_PATH/"' '=window.baseUrl';
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path/";</script>';
sub_filter_types text/css application/javascript; sub_filter_types text/css application/javascript;
sub_filter_once off; sub_filter_once off;

View File

@ -67,3 +67,14 @@ model:
``` ```
Note that if you rename objects in the labelmap, you will also need to update your `objects -> track` list as well. Note that if you rename objects in the labelmap, you will also need to update your `objects -> track` list as well.
## Custom ffmpeg build
Included with Frigate is a build of ffmpeg that works for the vast majority of users. However, there exists some hardware setups which have incompatibilities with the included build. In this case, a docker volume mapping can be used to overwrite the included ffmpeg build with an ffmpeg build that works for your specific hardware setup.
To do this:
1. Download your ffmpeg build and uncompress to a folder on the host (let's use `/home/appdata/frigate/custom-ffmpeg` for this example).
2. Update your docker-compose or docker CLI to include `'/home/appdata/frigate/custom-ffmpeg':'/usr/lib/btbn-ffmpeg':'ro'` in the volume mappings.
3. Restart frigate and the custom version will be used if the mapping was done correctly.
NOTE: The folder that is mapped from the host needs to be the folder that contains `/bin`. So if the full structure is `/home/appdata/frigate/custom-ffmpeg/bin/ffmpeg` then `/home/appdata/frigate/custom-ffmpeg` needs to be mapped to `/usr/lib/btbn-ffmpeg`.

View File

@ -0,0 +1,14 @@
# Birdseye
Birdseye allows a heads-up view of your cameras to see what is going on around your property / space without having to watch all cameras that may have nothing happening. Birdseye allows specific modes that intelligently show and disappear based on what you care about.
### Birdseye Modes
Birdseye offers different modes to customize which cameras show under which circumstances.
- **continuous:** All cameras are always included
- **motion:** Cameras that have detected motion within the last 30 seconds are included
- **objects:** Cameras that have tracked an active object within the last 30 seconds are included
### Custom Birdseye Icon
A custom icon can be added to the birdseye background by provided a file `custom.png` inside of the Frigate `media` folder. The file must be a png with the icon as transparent, any non-transparent pixels will be white when displayed in the birdseye view.

View File

@ -58,18 +58,17 @@ ffmpeg:
### Reolink 410/520 (possibly others) ### Reolink 410/520 (possibly others)
According to [this discussion](https://github.com/blakeblackshear/frigate/issues/1713#issuecomment-932976305), the http video streams seem to be the most reliable for Reolink. According to [this discussion](https://github.com/blakeblackshear/frigate/issues/3235#issuecomment-1135876973), the http video streams seem to be the most reliable for Reolink.
```yaml ```yaml
cameras: cameras:
reolink: reolink:
ffmpeg: ffmpeg:
hwaccel_args:
input_args: input_args:
- -avoid_negative_ts - -avoid_negative_ts
- make_zero - make_zero
- -fflags - -fflags
- nobuffer+genpts+discardcorrupt - +genpts+discardcorrupt
- -flags - -flags
- low_delay - low_delay
- -strict - -strict
@ -102,7 +101,7 @@ You will need to remove `nobuffer` flag for Blue Iris RTSP cameras
```yaml ```yaml
ffmpeg: ffmpeg:
input_args: -avoid_negative_ts make_zero -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1 input_args: -avoid_negative_ts make_zero -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rtsp_transport tcp -timeout 5000000 -use_wallclock_as_timestamps 1
``` ```
### UDP Only Cameras ### UDP Only Cameras
@ -111,5 +110,16 @@ If your cameras do not support TCP connections for RTSP, you can use UDP.
```yaml ```yaml
ffmpeg: ffmpeg:
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -stimeout 5000000 -use_wallclock_as_timestamps 1 input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -timeout 5000000 -use_wallclock_as_timestamps 1
```
### Unifi Protect Cameras
In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record and rtmp.
```yaml
ffmpeg:
output_args:
record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v copy -ar 44100 -c:a aac
rtmp: -c:v copy -f flv -ar 44100 -c:a aac
``` ```

View File

@ -5,51 +5,29 @@ title: Hardware Acceleration
It is recommended to update your configuration to enable hardware accelerated decoding in ffmpeg. Depending on your system, these parameters may not be compatible. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro It is recommended to update your configuration to enable hardware accelerated decoding in ffmpeg. Depending on your system, these parameters may not be compatible. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro
### Raspberry Pi 3/4 (32-bit OS) ### Raspberry Pi 3/4
Ensure you increase the allocated RAM for your GPU to at least 128 (raspi-config > Performance Options > GPU Memory). Ensure you increase the allocated RAM for your GPU to at least 128 (raspi-config > Performance Options > GPU Memory).
**NOTICE**: If you are using the addon, you may need to turn off `Protection mode` for hardware acceleration. **NOTICE**: If you are using the addon, you may need to turn off `Protection mode` for hardware acceleration.
```yaml ```yaml
ffmpeg: ffmpeg:
hwaccel_args: hwaccel_args: -c:v h264_v4l2m2m
- -c:v
- h264_mmal
```
### Raspberry Pi 3/4 (64-bit OS)
**NOTICE**: If you are using the addon, you may need to turn off `Protection mode` for hardware acceleration.
```yaml
ffmpeg:
hwaccel_args:
- -c:v
- h264_v4l2m2m
``` ```
### Intel-based CPUs (<10th Generation) via Quicksync ### Intel-based CPUs (<10th Generation) via Quicksync
```yaml ```yaml
ffmpeg: ffmpeg:
hwaccel_args: hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p
- -hwaccel
- vaapi
- -hwaccel_device
- /dev/dri/renderD128
- -hwaccel_output_format
- yuv420p
``` ```
**NOTICE**: With some of the processors, like the J4125, the default driver `iHD` doesn't seem to work correctly for hardware acceleration. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file.
### Intel-based CPUs (>=10th Generation) via Quicksync ### Intel-based CPUs (>=10th Generation) via Quicksync
```yaml ```yaml
ffmpeg: ffmpeg:
hwaccel_args: hwaccel_args: -c:v h264_qsv
- -hwaccel
- qsv
- -qsv_device
- /dev/dri/renderD128
``` ```
### AMD/ATI GPUs (Radeon HD 2000 and newer GPUs) via libva-mesa-driver ### AMD/ATI GPUs (Radeon HD 2000 and newer GPUs) via libva-mesa-driver
@ -58,13 +36,83 @@ ffmpeg:
```yaml ```yaml
ffmpeg: ffmpeg:
hwaccel_args: hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p
- -hwaccel
- vaapi
- -hwaccel_device
- /dev/dri/renderD128
``` ```
### NVIDIA GPU ### NVIDIA GPU
NVIDIA GPU based decoding via NVDEC is supported, but requires special configuration. See the [NVIDIA NVDEC documentation](/configuration/nvdec) for more details. [Supported Nvidia GPUs for Decoding](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new)
These instructions are based on the [jellyfin documentation](https://jellyfin.org/docs/general/administration/hardware-acceleration.html#nvidia-hardware-acceleration-on-docker-linux)
Add `--gpus all` to your docker run command or update your compose file.
```yaml
services:
frigate:
...
image: blakeblackshear/frigate:stable
deploy: # <------------- Add this section
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
```
The decoder you need to pass in the `hwaccel_args` will depend on the input video.
A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the container to get a list)
```shell
V..... h263_cuvid Nvidia CUVID H263 decoder (codec h263)
V..... h264_cuvid Nvidia CUVID H264 decoder (codec h264)
V..... hevc_cuvid Nvidia CUVID HEVC decoder (codec hevc)
V..... mjpeg_cuvid Nvidia CUVID MJPEG decoder (codec mjpeg)
V..... mpeg1_cuvid Nvidia CUVID MPEG1VIDEO decoder (codec mpeg1video)
V..... mpeg2_cuvid Nvidia CUVID MPEG2VIDEO decoder (codec mpeg2video)
V..... mpeg4_cuvid Nvidia CUVID MPEG4 decoder (codec mpeg4)
V..... vc1_cuvid Nvidia CUVID VC1 decoder (codec vc1)
V..... vp8_cuvid Nvidia CUVID VP8 decoder (codec vp8)
V..... vp9_cuvid Nvidia CUVID VP9 decoder (codec vp9)
```
For example, for H264 video, you'll select `h264_cuvid`.
```yaml
ffmpeg:
hwaccel_args: -c:v h264_cuvid
```
If everything is working correctly, you should see a significant improvement in performance.
Verify that hardware decoding is working by running `nvidia-smi`, which should show the ffmpeg
processes:
```
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 455.38 Driver Version: 455.38 CUDA Version: 11.1 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 GeForce GTX 166... Off | 00000000:03:00.0 Off | N/A |
| 38% 41C P2 36W / 125W | 2082MiB / 5942MiB | 5% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| 0 N/A N/A 12737 C ffmpeg 249MiB |
| 0 N/A N/A 12751 C ffmpeg 249MiB |
| 0 N/A N/A 12772 C ffmpeg 249MiB |
| 0 N/A N/A 12775 C ffmpeg 249MiB |
| 0 N/A N/A 12800 C ffmpeg 249MiB |
| 0 N/A N/A 12811 C ffmpeg 417MiB |
| 0 N/A N/A 12827 C ffmpeg 417MiB |
+-----------------------------------------------------------------------------+
```

View File

@ -114,6 +114,7 @@ environment_vars:
EXAMPLE_VAR: value EXAMPLE_VAR: value
# Optional: birdseye configuration # Optional: birdseye configuration
# NOTE: Can (enabled, mode) be overridden at the camera level
birdseye: birdseye:
# Optional: Enable birdseye view (default: shown below) # Optional: Enable birdseye view (default: shown below)
enabled: True enabled: True
@ -138,7 +139,7 @@ ffmpeg:
# NOTE: See hardware acceleration docs for your specific device # NOTE: See hardware acceleration docs for your specific device
hwaccel_args: [] hwaccel_args: []
# Optional: global input args (default: shown below) # Optional: global input args (default: shown below)
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1 input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -timeout 5000000 -use_wallclock_as_timestamps 1
# Optional: global output args # Optional: global output args
output_args: output_args:
# Optional: output args for detect streams (default: shown below) # Optional: output args for detect streams (default: shown below)
@ -202,6 +203,10 @@ objects:
min_area: 5000 min_area: 5000
# Optional: maximum width*height of the bounding box for the detected object (default: 24000000) # Optional: maximum width*height of the bounding box for the detected object (default: 24000000)
max_area: 100000 max_area: 100000
# Optional: minimum width/height of the bounding box for the detected object (default: 0)
min_ratio: 0.5
# Optional: maximum width/height of the bounding box for the detected object (default: 24000000)
max_ratio: 2.0
# Optional: minimum score for the object to initiate tracking (default: shown below) # Optional: minimum score for the object to initiate tracking (default: shown below)
min_score: 0.5 min_score: 0.5
# Optional: minimum decimal percentage for tracked object's computed score to be considered a true positive (default: shown below) # Optional: minimum decimal percentage for tracked object's computed score to be considered a true positive (default: shown below)
@ -245,6 +250,8 @@ motion:
# Enables dynamic contrast improvement. This should help improve night detections at the cost of making motion detection more sensitive # Enables dynamic contrast improvement. This should help improve night detections at the cost of making motion detection more sensitive
# for daytime. # for daytime.
improve_contrast: False improve_contrast: False
# Optional: Delay when updating camera motion through MQTT from ON -> OFF (default: shown below).
mqtt_off_delay: 30
# Optional: Record configuration # Optional: Record configuration
# NOTE: Can be overridden at the camera level # NOTE: Can be overridden at the camera level
@ -275,10 +282,6 @@ record:
mode: all mode: all
# Optional: Event recording settings # Optional: Event recording settings
events: events:
# Optional: Maximum length of time to retain video during long events. (default: shown below)
# NOTE: If an object is being tracked for longer than this amount of time, the retained recordings
# will be the last x seconds of the event unless retain->days under record is > 0.
max_seconds: 300
# Optional: Number of seconds before the event to include (default: shown below) # Optional: Number of seconds before the event to include (default: shown below)
pre_capture: 5 pre_capture: 5
# Optional: Number of seconds after the event to include (default: shown below) # Optional: Number of seconds after the event to include (default: shown below)
@ -447,4 +450,12 @@ cameras:
quality: 70 quality: 70
# Optional: Restrict mqtt messages to objects that entered any of the listed zones (default: no required zones) # Optional: Restrict mqtt messages to objects that entered any of the listed zones (default: no required zones)
required_zones: [] required_zones: []
# Optional: Configuration for how camera is handled in the GUI.
ui:
# Optional: Adjust sort order of cameras in the UI. Larger numbers come later (default: shown below)
# By default the cameras are sorted alphabetically.
order: 0
# Optional: Whether or not to show the camera in the Frigate UI (default: shown below)
dashboard: True
``` ```

View File

@ -1,99 +0,0 @@
---
id: nvdec
title: NVIDIA hardware decoder
---
Certain nvidia cards include a hardware decoder, which can greatly improve the
performance of video decoding. In order to use NVDEC, a special build of
ffmpeg with NVDEC support is required. The special docker architecture 'amd64nvidia'
includes this support for amd64 platforms. An aarch64 for the Jetson, which
also includes NVDEC may be added in the future.
Some more detailed setup instructions are also available in [this issue](https://github.com/blakeblackshear/frigate/issues/1847#issuecomment-932076731).
## Docker setup
### Requirements
[nVidia closed source driver](https://www.nvidia.com/en-us/drivers/unix/) required to access NVDEC.
[nvidia-docker](https://github.com/NVIDIA/nvidia-docker) required to pass NVDEC to docker.
### Setting up docker-compose
In order to pass NVDEC, the docker engine must be set to `nvidia` and the environment variables
`NVIDIA_VISIBLE_DEVICES=all` and `NVIDIA_DRIVER_CAPABILITIES=compute,utility,video` must be set.
In a docker compose file, these lines need to be set:
```yaml
services:
frigate:
...
image: blakeblackshear/frigate:stable-amd64nvidia
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=compute,utility,video
```
### Setting up the configuration file
In your frigate config.yml, you'll need to set ffmpeg to use the hardware decoder.
The decoder you choose will depend on the input video.
A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the container to get a list)
```shell
V..... h263_cuvid Nvidia CUVID H263 decoder (codec h263)
V..... h264_cuvid Nvidia CUVID H264 decoder (codec h264)
V..... hevc_cuvid Nvidia CUVID HEVC decoder (codec hevc)
V..... mjpeg_cuvid Nvidia CUVID MJPEG decoder (codec mjpeg)
V..... mpeg1_cuvid Nvidia CUVID MPEG1VIDEO decoder (codec mpeg1video)
V..... mpeg2_cuvid Nvidia CUVID MPEG2VIDEO decoder (codec mpeg2video)
V..... mpeg4_cuvid Nvidia CUVID MPEG4 decoder (codec mpeg4)
V..... vc1_cuvid Nvidia CUVID VC1 decoder (codec vc1)
V..... vp8_cuvid Nvidia CUVID VP8 decoder (codec vp8)
V..... vp9_cuvid Nvidia CUVID VP9 decoder (codec vp9)
```
For example, for H265 video (hevc), you'll select `hevc_cuvid`. Add
`-c:v hevc_cuvid` to your ffmpeg input arguments:
```yaml
ffmpeg:
input_args: ...
- -c:v
- hevc_cuvid
```
If everything is working correctly, you should see a significant improvement in performance.
Verify that hardware decoding is working by running `nvidia-smi`, which should show the ffmpeg
processes:
```
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 455.38 Driver Version: 455.38 CUDA Version: 11.1 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 GeForce GTX 166... Off | 00000000:03:00.0 Off | N/A |
| 38% 41C P2 36W / 125W | 2082MiB / 5942MiB | 5% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| 0 N/A N/A 12737 C ffmpeg 249MiB |
| 0 N/A N/A 12751 C ffmpeg 249MiB |
| 0 N/A N/A 12772 C ffmpeg 249MiB |
| 0 N/A N/A 12775 C ffmpeg 249MiB |
| 0 N/A N/A 12800 C ffmpeg 249MiB |
| 0 N/A N/A 12811 C ffmpeg 417MiB |
| 0 N/A N/A 12827 C ffmpeg 417MiB |
+-----------------------------------------------------------------------------+
```

View File

@ -0,0 +1,15 @@
---
id: user_interface
title: User Interface Configurations
---
### Experimental UI
While developing and testing new components, users may decide to opt-in to test potential new features on the front-end.
```yaml
ui:
use_experimental: true
```
Note that experimental changes may contain bugs or may be removed at any time in future releases of the software. Use of these features are presented as-is and with no functional guarantee.

View File

@ -40,9 +40,7 @@ Fork [blakeblackshear/frigate-hass-integration](https://github.com/blakeblackshe
### Setup ### Setup
#### 1. Build the docker container locally with the appropriate make command #### 1. Build the version information and docker container locally by running `make`
For x86 machines, use `make amd64_frigate`
#### 2. Create a local config file for testing #### 2. Create a local config file for testing
@ -90,6 +88,38 @@ VSCode will start the docker compose file for you and open a terminal window con
After closing VSCode, you may still have containers running. To close everything down, just run `docker-compose down -v` to cleanup all containers. After closing VSCode, you may still have containers running. To close everything down, just run `docker-compose down -v` to cleanup all containers.
### Testing
#### FFMPEG Hardware Acceleration
The following commands are used inside the container to ensure hardware acceleration is working properly.
**Raspberry Pi (64bit)**
This should show <50% CPU in top, and ~80% CPU without `-c:v h264_v4l2m2m`.
```shell
ffmpeg -c:v h264_v4l2m2m -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
```
**NVIDIA**
```shell
ffmpeg -c:v h264_cuvid -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
```
**VAAPI**
```shell
ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
```
**QSV**
```shell
ffmpeg -c:v h264_qsv -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
```
## Web Interface ## Web Interface
### Prerequisites ### Prerequisites
@ -117,20 +147,16 @@ cd web && npm install
#### 3. Run the development server #### 3. Run the development server
```console ```console
cd web && npm run start cd web && npm run dev
``` ```
#### 3a. Run the development server against a non-local instance #### 3a. Run the development server against a non-local instance
To run the development server against a non-local instance, you will need to provide an environment variable, `SNOWPACK_PUBLIC_API_HOST` that tells the web application how to connect to the Frigate API: To run the development server against a non-local instance, you will need to modify the API_HOST default return in `web/src/env.js`.
```console
cd web && SNOWPACK_PUBLIC_API_HOST=http://<ip-address-to-your-frigate-instance>:5000 npm run start
```
#### 4. Making changes #### 4. Making changes
The Web UI is built using [Snowpack](https://www.snowpack.dev/), [Preact](https://preactjs.com), and [Tailwind CSS](https://tailwindcss.com). The Web UI is built using [Vite](https://vitejs.dev/), [Preact](https://preactjs.com), and [Tailwind CSS](https://tailwindcss.com).
Light guidelines and advice: Light guidelines and advice:
@ -182,3 +208,16 @@ npm run build
``` ```
This command generates static content into the `build` directory and can be served using any static contents hosting service. This command generates static content into the `build` directory and can be served using any static contents hosting service.
## Official builds
Setup buildx for multiarch
```
docker buildx stop builder && docker buildx rm builder # <---- if existing
docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --name builder --driver docker-container --driver-opt network=host --use
docker buildx inspect builder --bootstrap
make build_web
make push
```

View File

@ -3,7 +3,11 @@ id: false_positives
title: Reducing false positives title: Reducing false positives
--- ---
Tune your object filters to adjust false positives: `min_area`, `max_area`, `min_score`, `threshold`. Tune your object filters to adjust false positives: `min_area`, `max_area`, `min_ratio`, `max_ratio`, `min_score`, `threshold`.
The `min_area` and `max_area` values are compared against the area (number of pixels) from a given detected object. If the area is outside this range, the object will be ignored as a false positive. This allows objects that must be too small or too large to be ignored.
Similarly, the `min_ratio` and `max_ratio` values are compared against a given detected object's width/height ratio (in pixels). If the ratio is outside this range, the object will be ignored as a false positive. This allows objects that are proportionally too short-and-wide (higher ratio) or too tall-and-narrow (smaller ratio) to be ignored.
For object filters in your configuration, any single detection below `min_score` will be ignored as a false positive. `threshold` is based on the median of the history of scores (padded to 3 values) for a tracked object. Consider the following frames when `min_score` is set to 0.6 and threshold is set to 0.85: For object filters in your configuration, any single detection below `min_score` will be ignored as a false positive. `threshold` is based on the median of the history of scores (padded to 3 values) for a tracked object. Consider the following frames when `min_score` is set to 0.6 and threshold is set to 0.85:

View File

@ -100,18 +100,7 @@ Additionally, the USB Coral draws a considerable amount of power. If using any o
## Docker ## Docker
Running in Docker directly is the recommended install method. Running in Docker with compose is the recommended install method:
Make sure you choose the right image for your architecture:
| Arch | Image Name |
| ----------- | ------------------------------------------ |
| amd64 | blakeblackshear/frigate:stable-amd64 |
| amd64nvidia | blakeblackshear/frigate:stable-amd64nvidia |
| armv7 | blakeblackshear/frigate:stable-armv7 |
| aarch64 | blakeblackshear/frigate:stable-aarch64 |
It is recommended to run with docker-compose:
```yaml ```yaml
version: "3.9" version: "3.9"
@ -120,7 +109,7 @@ services:
container_name: frigate container_name: frigate
privileged: true # this may not be necessary for all setups privileged: true # this may not be necessary for all setups
restart: unless-stopped restart: unless-stopped
image: blakeblackshear/frigate:<specify_version_tag> image: blakeblackshear/frigate:stable
shm_size: "64mb" # update for your cameras based on calculation above shm_size: "64mb" # update for your cameras based on calculation above
devices: devices:
- /dev/bus/usb:/dev/bus/usb # passes the USB Coral, needs to be modified for other versions - /dev/bus/usb:/dev/bus/usb # passes the USB Coral, needs to be modified for other versions
@ -157,7 +146,7 @@ docker run -d \
-e FRIGATE_RTSP_PASSWORD='password' \ -e FRIGATE_RTSP_PASSWORD='password' \
-p 5000:5000 \ -p 5000:5000 \
-p 1935:1935 \ -p 1935:1935 \
blakeblackshear/frigate:<specify_version_tag> blakeblackshear/frigate:stable
``` ```
## Home Assistant Operating System (HassOS) ## Home Assistant Operating System (HassOS)

View File

@ -24,16 +24,6 @@ Accepts the following query string parameters:
You can access a higher resolution mjpeg stream by appending `h=height-in-pixels` to the endpoint. For example `http://localhost:5000/api/back?h=1080`. You can also increase the FPS by appending `fps=frame-rate` to the URL such as `http://localhost:5000/api/back?fps=10` or both with `?fps=10&h=1000`. You can access a higher resolution mjpeg stream by appending `h=height-in-pixels` to the endpoint. For example `http://localhost:5000/api/back?h=1080`. You can also increase the FPS by appending `fps=frame-rate` to the URL such as `http://localhost:5000/api/back?fps=10` or both with `?fps=10&h=1000`.
### `GET /api/<camera_name>/<object_name>/best.jpg[?h=300&crop=1&quality=70]`
The best snapshot for any object type. It is a full resolution image by default.
Example parameters:
- `h=300`: resizes the image to 300 pixes tall
- `crop=1`: crops the image to the region of the detection rather than returning the entire image
- `quality=70`: sets the jpeg encoding quality (0-100)
### `GET /api/<camera_name>/latest.jpg[?h=300]` ### `GET /api/<camera_name>/latest.jpg[?h=300]`
The most recent frame that frigate has finished processing. It is a full resolution image by default. The most recent frame that frigate has finished processing. It is a full resolution image by default.
@ -120,7 +110,8 @@ Sample response:
"service": { "service": {
/* Uptime in seconds */ /* Uptime in seconds */
"uptime": 10, "uptime": 10,
"version": "0.8.0-8883709", "version": "0.10.1-8883709",
"latest_version": "0.10.1",
/* Storage data in MB for important locations */ /* Storage data in MB for important locations */
"storage": { "storage": {
"/media/frigate/clips": { "/media/frigate/clips": {
@ -188,10 +179,37 @@ Returns data for a single event.
Permanently deletes the event along with any clips/snapshots. Permanently deletes the event along with any clips/snapshots.
### `POST /api/events/<id>/retain`
Sets retain to true for the event id.
### `POST /api/events/<id>/plus`
Submits the snapshot of the event to Frigate+ for labeling.
### `DELETE /api/events/<id>/retain`
Sets retain to false for the event id (event may be deleted quickly after removing).
### `POST /api/events/<id>/sub_label`
Set a sub label for an event. For example to update `person` -> `person's name` if they were recognized with facial recognition.
Sub labels must be 20 characters or shorter.
```json
{
"subLabel": "some_string"
}
```
### `GET /api/events/<id>/thumbnail.jpg` ### `GET /api/events/<id>/thumbnail.jpg`
Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio. Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.
### `GET /api/<camera_name>/<label>/thumbnail.jpg`
Returns the thumbnail from the latest event for the given camera and label combo. Using `any` as the label will return the latest thumbnail regardless of type.
### `GET /api/events/<id>/clip.mp4` ### `GET /api/events/<id>/clip.mp4`
Returns the clip for the event id. Works after the event has ended. Returns the clip for the event id. Works after the event has ended.
@ -210,6 +228,10 @@ Accepts the following query string parameters, but they are only applied when an
| `crop` | int | Crop the snapshot to the (0 or 1) | | `crop` | int | Crop the snapshot to the (0 or 1) |
| `quality` | int | Jpeg encoding quality (0-100). Defaults to 70. | | `quality` | int | Jpeg encoding quality (0-100). Defaults to 70. |
### `GET /api/<camera_name>/<label>/snapshot.jpg`
Returns the snapshot image from the latest event for the given camera and label combo. Using `any` as the label will return the latest thumbnail regardless of type.
### `GET /clips/<camera>-<id>.jpg` ### `GET /clips/<camera>-<id>.jpg`
JPG snapshot for the given camera and event id. JPG snapshot for the given camera and event id.
@ -229,3 +251,16 @@ HTTP Live Streaming Video on Demand URL for the specified event. Can be viewed i
### `GET /vod/<camera>/start/<start-timestamp>/end/<end-timestamp>/index.m3u8` ### `GET /vod/<camera>/start/<start-timestamp>/end/<end-timestamp>/index.m3u8`
HTTP Live Streaming Video on Demand URL for the camera with the specified time range. Can be viewed in an application like VLC. HTTP Live Streaming Video on Demand URL for the camera with the specified time range. Can be viewed in an application like VLC.
### `GET /api/<camera_name>/recordings/summary`
Hourly summary of recordings data for a camera.
### `GET /api/<camera_name>/recordings`
Get recording segment details for the given timestamp range.
| param | Type | Description |
| -------- | ---- | ------------------------------------- |
| `after` | int | Unix timestamp for beginning of range |
| `before` | int | Unix timestamp for end of range |

View File

@ -18,10 +18,12 @@ Causes frigate to exit. Docker should be configured to automatically restart the
### `frigate/<camera_name>/<object_name>` ### `frigate/<camera_name>/<object_name>`
Publishes the count of objects for the camera for use as a sensor in Home Assistant. Publishes the count of objects for the camera for use as a sensor in Home Assistant.
`all` can be used as the object_name for the count of all objects for the camera.
### `frigate/<zone_name>/<object_name>` ### `frigate/<zone_name>/<object_name>`
Publishes the count of objects for the zone for use as a sensor in Home Assistant. Publishes the count of objects for the zone for use as a sensor in Home Assistant.
`all` can be used as the object_name for the count of all objects for the zone.
### `frigate/<camera_name>/<object_name>/snapshot` ### `frigate/<camera_name>/<object_name>/snapshot`
@ -50,6 +52,7 @@ Message published for each changed event. The first message is published when th
"score": 0.7890625, "score": 0.7890625,
"box": [424, 500, 536, 712], "box": [424, 500, 536, 712],
"area": 23744, "area": 23744,
"ratio": 2.113207,
"region": [264, 450, 667, 853], "region": [264, 450, 667, 853],
"current_zones": ["driveway"], "current_zones": ["driveway"],
"entered_zones": ["yard", "driveway"], "entered_zones": ["yard", "driveway"],
@ -73,6 +76,7 @@ Message published for each changed event. The first message is published when th
"score": 0.87890625, "score": 0.87890625,
"box": [432, 496, 544, 854], "box": [432, 496, 544, 854],
"area": 40096, "area": 40096,
"ratio": 1.251397,
"region": [218, 440, 693, 915], "region": [218, 440, 693, 915],
"current_zones": ["yard", "driveway"], "current_zones": ["yard", "driveway"],
"entered_zones": ["yard", "driveway"], "entered_zones": ["yard", "driveway"],
@ -113,3 +117,42 @@ Topic to turn snapshots for a camera on and off. Expected values are `ON` and `O
### `frigate/<camera_name>/snapshots/state` ### `frigate/<camera_name>/snapshots/state`
Topic with current state of snapshots for a camera. Published values are `ON` and `OFF`. Topic with current state of snapshots for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/motion/set`
Topic to turn motion detection for a camera on and off. Expected values are `ON` and `OFF`.
NOTE: Turning off motion detection will fail if detection is not disabled.
### `frigate/<camera_name>/motion`
Whether camera_name is currently detecting motion. Expected values are `ON` and `OFF`.
NOTE: After motion is initially detected, `ON` will be set until no motion has
been detected for `mqtt_off_delay` seconds (30 by default).
### `frigate/<camera_name>/motion/state`
Topic with current state of motion detection for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/improve_contrast/set`
Topic to turn improve_contrast for a camera on and off. Expected values are `ON` and `OFF`.
### `frigate/<camera_name>/improve_contrast/state`
Topic with current state of improve_contrast for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/motion_threshold/set`
Topic to adjust motion threshold for a camera. Expected value is an integer.
### `frigate/<camera_name>/motion_threshold/state`
Topic with current motion threshold for a camera. Published value is an integer.
### `frigate/<camera_name>/motion_contour_area/set`
Topic to adjust motion contour area for a camera. Expected value is an integer.
### `frigate/<camera_name>/motion_contour_area/state`
Topic with current motion contour area for a camera. Published value is an integer.

121
docs/package-lock.json generated
View File

@ -8,8 +8,8 @@
"name": "docs", "name": "docs",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@docusaurus/core": "^2.0.0-beta.15", "@docusaurus/core": "^2.0.0-beta.20",
"@docusaurus/preset-classic": "^2.0.0-beta.15", "@docusaurus/preset-classic": "^2.0.0-beta.20",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "^1.6.22",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
@ -3453,9 +3453,9 @@
} }
}, },
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@ -3563,9 +3563,9 @@
} }
}, },
"node_modules/async": { "node_modules/async": {
"version": "2.6.3", "version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dependencies": { "dependencies": {
"lodash": "^4.17.14" "lodash": "^4.17.14"
} }
@ -3877,6 +3877,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"optional": true,
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bluebird": { "node_modules/bluebird": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -6182,6 +6191,12 @@
"webpack": "^4.0.0 || ^5.0.0" "webpack": "^4.0.0 || ^5.0.0"
} }
}, },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"optional": true
},
"node_modules/filesize": { "node_modules/filesize": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
@ -6272,9 +6287,9 @@
} }
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.14.7", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz",
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -8909,9 +8924,9 @@
} }
}, },
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"node_modules/mixin-deep": { "node_modules/mixin-deep": {
"version": "1.3.2", "version": "1.3.2",
@ -8974,6 +8989,12 @@
"resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE="
}, },
"node_modules/nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"optional": true
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
@ -10396,9 +10417,12 @@
} }
}, },
"node_modules/prismjs": { "node_modules/prismjs": {
"version": "1.25.0", "version": "1.28.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.28.0.tgz",
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==" "integrity": "sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw==",
"engines": {
"node": ">=6"
}
}, },
"node_modules/process-nextick-args": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
@ -13394,9 +13418,9 @@
} }
}, },
"node_modules/url-parse": { "node_modules/url-parse": {
"version": "1.5.3", "version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dependencies": { "dependencies": {
"querystringify": "^2.1.1", "querystringify": "^2.1.1",
"requires-port": "^1.0.0" "requires-port": "^1.0.0"
@ -17139,9 +17163,9 @@
"integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==" "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw=="
}, },
"ansi-regex": { "ansi-regex": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="
}, },
"ansi-styles": { "ansi-styles": {
"version": "3.2.1", "version": "3.2.1",
@ -17219,9 +17243,9 @@
"integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c="
}, },
"async": { "async": {
"version": "2.6.3", "version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"requires": { "requires": {
"lodash": "^4.17.14" "lodash": "^4.17.14"
} }
@ -17456,6 +17480,15 @@
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
}, },
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bluebird": { "bluebird": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -19221,6 +19254,12 @@
"schema-utils": "^3.0.0" "schema-utils": "^3.0.0"
} }
}, },
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"optional": true
},
"filesize": { "filesize": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
@ -19292,9 +19331,9 @@
} }
}, },
"follow-redirects": { "follow-redirects": {
"version": "1.14.7", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz",
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ=="
}, },
"for-in": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
@ -21270,9 +21309,9 @@
} }
}, },
"minimist": { "minimist": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"mixin-deep": { "mixin-deep": {
"version": "1.3.2", "version": "1.3.2",
@ -21325,6 +21364,12 @@
"resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE="
}, },
"nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"optional": true
},
"nanoid": { "nanoid": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
@ -22285,9 +22330,9 @@
"requires": {} "requires": {}
}, },
"prismjs": { "prismjs": {
"version": "1.25.0", "version": "1.28.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.28.0.tgz",
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==" "integrity": "sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw=="
}, },
"process-nextick-args": { "process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
@ -24571,9 +24616,9 @@
} }
}, },
"url-parse": { "url-parse": {
"version": "1.5.3", "version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"requires": { "requires": {
"querystringify": "^2.1.1", "querystringify": "^2.1.1",
"requires-port": "^1.0.0" "requires-port": "^1.0.0"

View File

@ -12,8 +12,8 @@
"clear": "docusaurus clear" "clear": "docusaurus clear"
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "^2.0.0-beta.15", "@docusaurus/core": "^2.0.0-beta.20",
"@docusaurus/preset-classic": "^2.0.0-beta.15", "@docusaurus/preset-classic": "^2.0.0-beta.20",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "^1.6.22",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",

View File

@ -1,36 +1,36 @@
module.exports = { module.exports = {
docs: { docs: {
Frigate: [ Frigate: ["index", "hardware", "installation"],
'index',
'hardware',
'installation',
],
Guides: [ Guides: [
'guides/camera_setup', "guides/camera_setup",
'guides/getting_started', "guides/getting_started",
'guides/events_setup', "guides/events_setup",
'guides/false_positives', "guides/false_positives",
'guides/ha_notifications', "guides/ha_notifications",
'guides/stationary_objects', "guides/stationary_objects",
], ],
Configuration: [ Configuration: [
'configuration/index', "configuration/index",
'configuration/detectors', "configuration/detectors",
'configuration/cameras', "configuration/cameras",
'configuration/masks', "configuration/masks",
'configuration/record', "configuration/record",
'configuration/snapshots', "configuration/snapshots",
'configuration/objects', "configuration/objects",
'configuration/rtmp', "configuration/rtmp",
'configuration/zones', "configuration/zones",
'configuration/stationary_objects', "configuration/birdseye",
'configuration/advanced', "configuration/stationary_objects",
'configuration/hardware_acceleration', "configuration/advanced",
'configuration/nvdec', "configuration/hardware_acceleration",
'configuration/camera_specific', "configuration/camera_specific",
], ],
Integrations: ['integrations/home-assistant', 'integrations/api', 'integrations/mqtt'], Integrations: [
Troubleshooting: ['faqs'], "integrations/home-assistant",
Development: ['contributing'], "integrations/api",
"integrations/mqtt",
],
Troubleshooting: ["faqs"],
Development: ["contributing"],
}, },
}; };

View File

@ -1,14 +1,13 @@
import faulthandler import faulthandler
from flask import cli
faulthandler.enable() faulthandler.enable()
import sys
import threading import threading
threading.current_thread().name = "frigate" threading.current_thread().name = "frigate"
from frigate.app import FrigateApp from frigate.app import FrigateApp
cli = sys.modules["flask.cli"]
cli.show_server_banner = lambda *x: None cli.show_server_banner = lambda *x: None
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,12 +1,16 @@
import json import json
import logging import logging
import multiprocessing as mp import multiprocessing as mp
from multiprocessing.queues import Queue
from multiprocessing.synchronize import Event
from multiprocessing.context import Process
import os import os
import signal import signal
import sys import sys
import threading import threading
from logging.handlers import QueueHandler from logging.handlers import QueueHandler
from typing import Dict, List from typing import Optional
from types import FrameType
import traceback import traceback
import yaml import yaml
@ -25,32 +29,33 @@ from frigate.models import Event, Recordings
from frigate.mqtt import MqttSocketRelay, create_mqtt_client from frigate.mqtt import MqttSocketRelay, create_mqtt_client
from frigate.object_processing import TrackedObjectProcessor from frigate.object_processing import TrackedObjectProcessor
from frigate.output import output_frames from frigate.output import output_frames
from frigate.plus import PlusApi
from frigate.record import RecordingCleanup, RecordingMaintainer from frigate.record import RecordingCleanup, RecordingMaintainer
from frigate.stats import StatsEmitter, stats_init from frigate.stats import StatsEmitter, stats_init
from frigate.version import VERSION from frigate.version import VERSION
from frigate.video import capture_camera, track_camera from frigate.video import capture_camera, track_camera
from frigate.watchdog import FrigateWatchdog from frigate.watchdog import FrigateWatchdog
from frigate.types import CameraMetricsTypes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FrigateApp: class FrigateApp:
def __init__(self): def __init__(self) -> None:
self.stop_event = mp.Event() self.stop_event: Event = mp.Event()
self.base_config: FrigateConfig = None self.detection_queue: Queue = mp.Queue()
self.config: FrigateConfig = None self.detectors: dict[str, EdgeTPUProcess] = {}
self.detection_queue = mp.Queue() self.detection_out_events: dict[str, Event] = {}
self.detectors: Dict[str, EdgeTPUProcess] = {} self.detection_shms: list[mp.shared_memory.SharedMemory] = []
self.detection_out_events: Dict[str, mp.Event] = {} self.log_queue: Queue = mp.Queue()
self.detection_shms: List[mp.shared_memory.SharedMemory] = [] self.plus_api = PlusApi()
self.log_queue = mp.Queue() self.camera_metrics: dict[str, CameraMetricsTypes] = {}
self.camera_metrics = {}
def set_environment_vars(self): def set_environment_vars(self) -> None:
for key, value in self.config.environment_vars.items(): for key, value in self.config.environment_vars.items():
os.environ[key] = value os.environ[key] = value
def ensure_dirs(self): def ensure_dirs(self) -> None:
for d in [RECORD_DIR, CLIPS_DIR, CACHE_DIR]: for d in [RECORD_DIR, CLIPS_DIR, CACHE_DIR]:
if not os.path.exists(d) and not os.path.islink(d): if not os.path.exists(d) and not os.path.islink(d):
logger.info(f"Creating directory: {d}") logger.info(f"Creating directory: {d}")
@ -58,7 +63,7 @@ class FrigateApp:
else: else:
logger.debug(f"Skipping directory: {d}") logger.debug(f"Skipping directory: {d}")
def init_logger(self): def init_logger(self) -> None:
self.log_process = mp.Process( self.log_process = mp.Process(
target=log_process, args=(self.log_queue,), name="log_process" target=log_process, args=(self.log_queue,), name="log_process"
) )
@ -66,7 +71,7 @@ class FrigateApp:
self.log_process.start() self.log_process.start()
root_configurer(self.log_queue) root_configurer(self.log_queue)
def init_config(self): def init_config(self) -> None:
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml") config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
# Check if we can use .yaml instead of .yml # Check if we can use .yaml instead of .yml
@ -86,14 +91,26 @@ class FrigateApp:
"detection_enabled": mp.Value( "detection_enabled": mp.Value(
"i", self.config.cameras[camera_name].detect.enabled "i", self.config.cameras[camera_name].detect.enabled
), ),
"motion_enabled": mp.Value("i", True),
"improve_contrast_enabled": mp.Value(
"i", self.config.cameras[camera_name].motion.improve_contrast
),
"motion_threshold": mp.Value(
"i", self.config.cameras[camera_name].motion.threshold
),
"motion_contour_area": mp.Value(
"i", self.config.cameras[camera_name].motion.contour_area
),
"detection_fps": mp.Value("d", 0.0), "detection_fps": mp.Value("d", 0.0),
"detection_frame": mp.Value("d", 0.0), "detection_frame": mp.Value("d", 0.0),
"read_start": mp.Value("d", 0.0), "read_start": mp.Value("d", 0.0),
"ffmpeg_pid": mp.Value("i", 0), "ffmpeg_pid": mp.Value("i", 0),
"frame_queue": mp.Queue(maxsize=2), "frame_queue": mp.Queue(maxsize=2),
"capture_process": None,
"process": None,
} }
def set_log_levels(self): def set_log_levels(self) -> None:
logging.getLogger().setLevel(self.config.logger.default.value.upper()) logging.getLogger().setLevel(self.config.logger.default.value.upper())
for log, level in self.config.logger.logs.items(): for log, level in self.config.logger.logs.items():
logging.getLogger(log).setLevel(level.value.upper()) logging.getLogger(log).setLevel(level.value.upper())
@ -101,21 +118,23 @@ class FrigateApp:
if not "werkzeug" in self.config.logger.logs: if not "werkzeug" in self.config.logger.logs:
logging.getLogger("werkzeug").setLevel("ERROR") logging.getLogger("werkzeug").setLevel("ERROR")
def init_queues(self): def init_queues(self) -> None:
# Queues for clip processing # Queues for clip processing
self.event_queue = mp.Queue() self.event_queue: Queue = mp.Queue()
self.event_processed_queue = mp.Queue() self.event_processed_queue: Queue = mp.Queue()
self.video_output_queue = mp.Queue(maxsize=len(self.config.cameras.keys()) * 2) self.video_output_queue: Queue = mp.Queue(
maxsize=len(self.config.cameras.keys()) * 2
)
# Queue for cameras to push tracked objects to # Queue for cameras to push tracked objects to
self.detected_frames_queue = mp.Queue( self.detected_frames_queue: Queue = mp.Queue(
maxsize=len(self.config.cameras.keys()) * 2 maxsize=len(self.config.cameras.keys()) * 2
) )
# Queue for recordings info # Queue for recordings info
self.recordings_info_queue = mp.Queue() self.recordings_info_queue: Queue = mp.Queue()
def init_database(self): def init_database(self) -> None:
# Migrate DB location # Migrate DB location
old_db_path = os.path.join(CLIPS_DIR, "frigate.db") old_db_path = os.path.join(CLIPS_DIR, "frigate.db")
if not os.path.isfile(self.config.database.path) and os.path.isfile( if not os.path.isfile(self.config.database.path) and os.path.isfile(
@ -137,27 +156,28 @@ class FrigateApp:
models = [Event, Recordings] models = [Event, Recordings]
self.db.bind(models) self.db.bind(models)
def init_stats(self): def init_stats(self) -> None:
self.stats_tracking = stats_init(self.camera_metrics, self.detectors) self.stats_tracking = stats_init(self.camera_metrics, self.detectors)
def init_web_server(self): def init_web_server(self) -> None:
self.flask_app = create_app( self.flask_app = create_app(
self.config, self.config,
self.db, self.db,
self.stats_tracking, self.stats_tracking,
self.detected_frames_processor, self.detected_frames_processor,
self.plus_api,
) )
def init_mqtt(self): def init_mqtt(self) -> None:
self.mqtt_client = create_mqtt_client(self.config, self.camera_metrics) self.mqtt_client = create_mqtt_client(self.config, self.camera_metrics)
def start_mqtt_relay(self): def start_mqtt_relay(self) -> None:
self.mqtt_relay = MqttSocketRelay( self.mqtt_relay = MqttSocketRelay(
self.mqtt_client, self.config.mqtt.topic_prefix self.mqtt_client, self.config.mqtt.topic_prefix
) )
self.mqtt_relay.start() self.mqtt_relay.start()
def start_detectors(self): def start_detectors(self) -> None:
model_path = self.config.model.path model_path = self.config.model.path
model_shape = (self.config.model.height, self.config.model.width) model_shape = (self.config.model.height, self.config.model.width)
for name in self.config.cameras.keys(): for name in self.config.cameras.keys():
@ -204,7 +224,7 @@ class FrigateApp:
detector.num_threads, detector.num_threads,
) )
def start_detected_frames_processor(self): def start_detected_frames_processor(self) -> None:
self.detected_frames_processor = TrackedObjectProcessor( self.detected_frames_processor = TrackedObjectProcessor(
self.config, self.config,
self.mqtt_client, self.mqtt_client,
@ -218,7 +238,7 @@ class FrigateApp:
) )
self.detected_frames_processor.start() self.detected_frames_processor.start()
def start_video_output_processor(self): def start_video_output_processor(self) -> None:
output_processor = mp.Process( output_processor = mp.Process(
target=output_frames, target=output_frames,
name=f"output_processor", name=f"output_processor",
@ -232,7 +252,7 @@ class FrigateApp:
output_processor.start() output_processor.start()
logger.info(f"Output process started: {output_processor.pid}") logger.info(f"Output process started: {output_processor.pid}")
def start_camera_processors(self): def start_camera_processors(self) -> None:
model_shape = (self.config.model.height, self.config.model.width) model_shape = (self.config.model.height, self.config.model.width)
for name, config in self.config.cameras.items(): for name, config in self.config.cameras.items():
camera_process = mp.Process( camera_process = mp.Process(
@ -254,7 +274,7 @@ class FrigateApp:
camera_process.start() camera_process.start()
logger.info(f"Camera processor started for {name}: {camera_process.pid}") logger.info(f"Camera processor started for {name}: {camera_process.pid}")
def start_camera_capture_processes(self): def start_camera_capture_processes(self) -> None:
for name, config in self.config.cameras.items(): for name, config in self.config.cameras.items():
capture_process = mp.Process( capture_process = mp.Process(
target=capture_camera, target=capture_camera,
@ -266,7 +286,7 @@ class FrigateApp:
capture_process.start() capture_process.start()
logger.info(f"Capture process started for {name}: {capture_process.pid}") logger.info(f"Capture process started for {name}: {capture_process.pid}")
def start_event_processor(self): def start_event_processor(self) -> None:
self.event_processor = EventProcessor( self.event_processor = EventProcessor(
self.config, self.config,
self.camera_metrics, self.camera_metrics,
@ -276,21 +296,21 @@ class FrigateApp:
) )
self.event_processor.start() self.event_processor.start()
def start_event_cleanup(self): def start_event_cleanup(self) -> None:
self.event_cleanup = EventCleanup(self.config, self.stop_event) self.event_cleanup = EventCleanup(self.config, self.stop_event)
self.event_cleanup.start() self.event_cleanup.start()
def start_recording_maintainer(self): def start_recording_maintainer(self) -> None:
self.recording_maintainer = RecordingMaintainer( self.recording_maintainer = RecordingMaintainer(
self.config, self.recordings_info_queue, self.stop_event self.config, self.recordings_info_queue, self.stop_event
) )
self.recording_maintainer.start() self.recording_maintainer.start()
def start_recording_cleanup(self): def start_recording_cleanup(self) -> None:
self.recording_cleanup = RecordingCleanup(self.config, self.stop_event) self.recording_cleanup = RecordingCleanup(self.config, self.stop_event)
self.recording_cleanup.start() self.recording_cleanup.start()
def start_stats_emitter(self): def start_stats_emitter(self) -> None:
self.stats_emitter = StatsEmitter( self.stats_emitter = StatsEmitter(
self.config, self.config,
self.stats_tracking, self.stats_tracking,
@ -300,11 +320,11 @@ class FrigateApp:
) )
self.stats_emitter.start() self.stats_emitter.start()
def start_watchdog(self): def start_watchdog(self) -> None:
self.frigate_watchdog = FrigateWatchdog(self.detectors, self.stop_event) self.frigate_watchdog = FrigateWatchdog(self.detectors, self.stop_event)
self.frigate_watchdog.start() self.frigate_watchdog.start()
def start(self): def start(self) -> None:
self.init_logger() self.init_logger()
logger.info(f"Starting Frigate ({VERSION})") logger.info(f"Starting Frigate ({VERSION})")
try: try:
@ -353,7 +373,7 @@ class FrigateApp:
self.start_watchdog() self.start_watchdog()
# self.zeroconf = broadcast_zeroconf(self.config.mqtt.client_id) # self.zeroconf = broadcast_zeroconf(self.config.mqtt.client_id)
def receiveSignal(signalNumber, frame): def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
self.stop() self.stop()
sys.exit() sys.exit()
@ -366,7 +386,7 @@ class FrigateApp:
self.stop() self.stop()
def stop(self): def stop(self) -> None:
logger.info(f"Stopping...") logger.info(f"Stopping...")
self.stop_event.set() self.stop_event.set()

View File

@ -44,6 +44,10 @@ class DetectorConfig(FrigateBaseModel):
num_threads: int = Field(default=3, title="Number of detection threads") num_threads: int = Field(default=3, title="Number of detection threads")
class UIConfig(FrigateBaseModel):
use_experimental: bool = Field(default=False, title="Experimental UI")
class MqttConfig(FrigateBaseModel): class MqttConfig(FrigateBaseModel):
host: str = Field(title="MQTT Host") host: str = Field(title="MQTT Host")
port: int = Field(default=1883, title="MQTT Port") port: int = Field(default=1883, title="MQTT Port")
@ -79,7 +83,6 @@ class RetainConfig(FrigateBaseModel):
class EventsConfig(FrigateBaseModel): class EventsConfig(FrigateBaseModel):
max_seconds: int = Field(default=300, title="Maximum event duration.")
pre_capture: int = Field(default=5, title="Seconds to retain before event starts.") pre_capture: int = Field(default=5, title="Seconds to retain before event starts.")
post_capture: int = Field(default=5, title="Seconds to retain after event ends.") post_capture: int = Field(default=5, title="Seconds to retain after event ends.")
required_zones: List[str] = Field( required_zones: List[str] = Field(
@ -130,6 +133,10 @@ class MotionConfig(FrigateBaseModel):
mask: Union[str, List[str]] = Field( mask: Union[str, List[str]] = Field(
default="", title="Coordinates polygon for the motion mask." default="", title="Coordinates polygon for the motion mask."
) )
mqtt_off_delay: int = Field(
default=30,
title="Delay for updating MQTT with no motion detected.",
)
class RuntimeMotionConfig(MotionConfig): class RuntimeMotionConfig(MotionConfig):
@ -209,6 +216,14 @@ class FilterConfig(FrigateBaseModel):
max_area: int = Field( max_area: int = Field(
default=24000000, title="Maximum area of bounding box for object to be counted." default=24000000, title="Maximum area of bounding box for object to be counted."
) )
min_ratio: float = Field(
default=0,
title="Minimum ratio of bounding box's width/height for object to be counted.",
)
max_ratio: float = Field(
default=24000000,
title="Maximum ratio of bounding box's width/height for object to be counted.",
)
threshold: float = Field( threshold: float = Field(
default=0.7, default=0.7,
title="Average detection confidence threshold for object to be counted.", title="Average detection confidence threshold for object to be counted.",
@ -315,6 +330,14 @@ class BirdseyeConfig(FrigateBaseModel):
) )
# uses BaseModel because some global attributes are not available at the camera level
class BirdseyeCameraConfig(BaseModel):
enabled: bool = Field(default=True, title="Enable birdseye view for camera.")
mode: BirdseyeModeEnum = Field(
default=BirdseyeModeEnum.objects, title="Tracking mode for camera."
)
FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning"] FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning"]
FFMPEG_INPUT_ARGS_DEFAULT = [ FFMPEG_INPUT_ARGS_DEFAULT = [
"-avoid_negative_ts", "-avoid_negative_ts",
@ -323,7 +346,7 @@ FFMPEG_INPUT_ARGS_DEFAULT = [
"+genpts+discardcorrupt", "+genpts+discardcorrupt",
"-rtsp_transport", "-rtsp_transport",
"tcp", "tcp",
"-stimeout", "-timeout",
"5000000", "5000000",
"-use_wallclock_as_timestamps", "-use_wallclock_as_timestamps",
"1", "1",
@ -498,6 +521,13 @@ class CameraLiveConfig(FrigateBaseModel):
quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality") quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality")
class CameraUiConfig(FrigateBaseModel):
order: int = Field(default=0, title="Order of camera in UI.")
dashboard: bool = Field(
default=True, title="Show this camera in Frigate dashboard UI."
)
class CameraConfig(FrigateBaseModel): class CameraConfig(FrigateBaseModel):
name: Optional[str] = Field(title="Camera name.", regex="^[a-zA-Z0-9_-]+$") name: Optional[str] = Field(title="Camera name.", regex="^[a-zA-Z0-9_-]+$")
ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.") ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
@ -530,6 +560,12 @@ class CameraConfig(FrigateBaseModel):
detect: DetectConfig = Field( detect: DetectConfig = Field(
default_factory=DetectConfig, title="Object detection configuration." default_factory=DetectConfig, title="Object detection configuration."
) )
ui: CameraUiConfig = Field(
default_factory=CameraUiConfig, title="Camera UI Modifications."
)
birdseye: BirdseyeCameraConfig = Field(
default_factory=BirdseyeCameraConfig, title="Birdseye camera configuration."
)
timestamp_style: TimestampStyleConfig = Field( timestamp_style: TimestampStyleConfig = Field(
default_factory=TimestampStyleConfig, title="Timestamp style configuration." default_factory=TimestampStyleConfig, title="Timestamp style configuration."
) )
@ -710,6 +746,7 @@ class FrigateConfig(FrigateBaseModel):
environment_vars: Dict[str, str] = Field( environment_vars: Dict[str, str] = Field(
default_factory=dict, title="Frigate environment variables." default_factory=dict, title="Frigate environment variables."
) )
ui: UIConfig = Field(default_factory=UIConfig, title="UI configuration.")
model: ModelConfig = Field( model: ModelConfig = Field(
default_factory=ModelConfig, title="Detection model configuration." default_factory=ModelConfig, title="Detection model configuration."
) )
@ -765,6 +802,7 @@ class FrigateConfig(FrigateBaseModel):
# Global config to propegate down to camera level # Global config to propegate down to camera level
global_config = config.dict( global_config = config.dict(
include={ include={
"birdseye": ...,
"record": ..., "record": ...,
"snapshots": ..., "snapshots": ...,
"live": ..., "live": ...,

View File

@ -3,3 +3,5 @@ CLIPS_DIR = f"{BASE_DIR}/clips"
RECORD_DIR = f"{BASE_DIR}/recordings" RECORD_DIR = f"{BASE_DIR}/recordings"
CACHE_DIR = "/tmp/cache" CACHE_DIR = "/tmp/cache"
YAML_EXT = (".yaml", ".yml") YAML_EXT = (".yaml", ".yml")
PLUS_ENV_VAR = "PLUS_API_KEY"
PLUS_API_HOST = "https://api.frigate.video"

View File

@ -6,7 +6,6 @@ import queue
import signal import signal
import threading import threading
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict
import numpy as np import numpy as np
import tflite_runtime.interpreter as tflite import tflite_runtime.interpreter as tflite
@ -110,7 +109,7 @@ class LocalObjectDetector(ObjectDetector):
def run_detector( def run_detector(
name: str, name: str,
detection_queue: mp.Queue, detection_queue: mp.Queue,
out_events: Dict[str, mp.Event], out_events: dict[str, mp.Event],
avg_speed, avg_speed,
start, start,
model_path, model_path,

View File

@ -15,8 +15,16 @@ from frigate.models import Event
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def should_insert_db(prev_event, current_event):
"""If current event has new clip or snapshot."""
return (not prev_event["has_clip"] and not prev_event["has_snapshot"]) and (
current_event["has_clip"] or current_event["has_snapshot"]
)
def should_update_db(prev_event, current_event): def should_update_db(prev_event, current_event):
return ( """If current_event has updated fields and (clip or snapshot)."""
return (current_event["has_clip"] or current_event["has_snapshot"]) and (
prev_event["top_score"] != current_event["top_score"] prev_event["top_score"] != current_event["top_score"]
or prev_event["entered_zones"] != current_event["entered_zones"] or prev_event["entered_zones"] != current_event["entered_zones"]
or prev_event["thumbnail"] != current_event["thumbnail"] or prev_event["thumbnail"] != current_event["thumbnail"]
@ -58,13 +66,12 @@ class EventProcessor(threading.Thread):
if event_type == "start": if event_type == "start":
self.events_in_process[event_data["id"]] = event_data self.events_in_process[event_data["id"]] = event_data
elif event_type == "update" and should_update_db( elif event_type == "update" and should_insert_db(
self.events_in_process[event_data["id"]], event_data self.events_in_process[event_data["id"]], event_data
): ):
self.events_in_process[event_data["id"]] = event_data self.events_in_process[event_data["id"]] = event_data
# TODO: this will generate a lot of db activity possibly # TODO: this will generate a lot of db activity possibly
if event_data["has_clip"] or event_data["has_snapshot"]: Event.insert(
Event.replace(
id=event_data["id"], id=event_data["id"],
label=event_data["label"], label=event_data["label"],
camera=camera, camera=camera,
@ -81,10 +88,32 @@ class EventProcessor(threading.Thread):
has_snapshot=event_data["has_snapshot"], has_snapshot=event_data["has_snapshot"],
).execute() ).execute()
elif event_type == "update" and should_update_db(
self.events_in_process[event_data["id"]], event_data
):
self.events_in_process[event_data["id"]] = event_data
# TODO: this will generate a lot of db activity possibly
Event.update(
label=event_data["label"],
camera=camera,
start_time=event_data["start_time"] - event_config.pre_capture,
end_time=None,
top_score=event_data["top_score"],
false_positive=event_data["false_positive"],
zones=list(event_data["entered_zones"]),
thumbnail=event_data["thumbnail"],
region=event_data["region"],
box=event_data["box"],
area=event_data["area"],
ratio=event_data["ratio"],
has_clip=event_data["has_clip"],
has_snapshot=event_data["has_snapshot"],
).where(Event.id == event_data["id"]).execute()
elif event_type == "end": elif event_type == "end":
if event_data["has_clip"] or event_data["has_snapshot"]: if event_data["has_clip"] or event_data["has_snapshot"]:
Event.replace( # Full update for valid end of event
id=event_data["id"], Event.update(
label=event_data["label"], label=event_data["label"],
camera=camera, camera=camera,
start_time=event_data["start_time"] - event_config.pre_capture, start_time=event_data["start_time"] - event_config.pre_capture,
@ -96,9 +125,16 @@ class EventProcessor(threading.Thread):
region=event_data["region"], region=event_data["region"],
box=event_data["box"], box=event_data["box"],
area=event_data["area"], area=event_data["area"],
ratio=event_data["ratio"],
has_clip=event_data["has_clip"], has_clip=event_data["has_clip"],
has_snapshot=event_data["has_snapshot"], has_snapshot=event_data["has_snapshot"],
).execute() ).where(Event.id == event_data["id"]).execute()
else:
# Event ended after clip & snapshot disabled,
# only end time should be updated.
Event.update(
end_time=event_data["end_time"] + event_config.post_capture
).where(Event.id == event_data["id"]).execute()
del self.events_in_process[event_data["id"]] del self.events_in_process[event_data["id"]]
self.event_processed_queue.put((event_data["id"], camera)) self.event_processed_queue.put((event_data["id"], camera))
@ -147,6 +183,7 @@ class EventCleanup(threading.Thread):
Event.camera.not_in(self.camera_keys), Event.camera.not_in(self.camera_keys),
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == l.label, Event.label == l.label,
Event.retain_indefinitely == False,
) )
# delete the media from disk # delete the media from disk
for event in expired_events: for event in expired_events:
@ -166,6 +203,7 @@ class EventCleanup(threading.Thread):
Event.camera.not_in(self.camera_keys), Event.camera.not_in(self.camera_keys),
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == l.label, Event.label == l.label,
Event.retain_indefinitely == False,
) )
update_query.execute() update_query.execute()
@ -192,6 +230,7 @@ class EventCleanup(threading.Thread):
Event.camera == name, Event.camera == name,
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == l.label, Event.label == l.label,
Event.retain_indefinitely == False,
) )
# delete the grabbed clips from disk # delete the grabbed clips from disk
for event in expired_events: for event in expired_events:
@ -210,6 +249,7 @@ class EventCleanup(threading.Thread):
Event.camera == name, Event.camera == name,
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == l.label, Event.label == l.label,
Event.retain_indefinitely == False,
) )
update_query.execute() update_query.execute()

View File

@ -2,18 +2,15 @@ import base64
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime, timedelta from datetime import datetime, timedelta
import copy import copy
import json
import glob
import logging import logging
import os import os
import re
import subprocess as sp import subprocess as sp
import time import time
from functools import reduce from functools import reduce
from pathlib import Path from pathlib import Path
from urllib.parse import unquote
import cv2 import cv2
from flask.helpers import send_file
import numpy as np import numpy as np
from flask import ( from flask import (
@ -26,13 +23,12 @@ from flask import (
request, request,
) )
from peewee import SqliteDatabase, operator, fn, DoesNotExist, Value from peewee import SqliteDatabase, operator, fn, DoesNotExist
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
from frigate.const import CLIPS_DIR, RECORD_DIR from frigate.const import CLIPS_DIR
from frigate.models import Event, Recordings from frigate.models import Event, Recordings
from frigate.stats import stats_snapshot from frigate.stats import stats_snapshot
from frigate.util import calculate_region
from frigate.version import VERSION from frigate.version import VERSION
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -45,6 +41,7 @@ def create_app(
database: SqliteDatabase, database: SqliteDatabase,
stats_tracking, stats_tracking,
detected_frames_processor, detected_frames_processor,
plus_api,
): ):
app = Flask(__name__) app = Flask(__name__)
@ -61,6 +58,7 @@ def create_app(
app.frigate_config = frigate_config app.frigate_config = frigate_config
app.stats_tracking = stats_tracking app.stats_tracking = stats_tracking
app.detected_frames_processor = detected_frames_processor app.detected_frames_processor = detected_frames_processor
app.plus_api = plus_api
app.register_blueprint(bp) app.register_blueprint(bp)
@ -120,6 +118,152 @@ def event(id):
return "Event not found", 404 return "Event not found", 404
@bp.route("/events/<id>/retain", methods=("POST",))
def set_retain(id):
try:
event = Event.get(Event.id == id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
)
event.retain_indefinitely = True
event.save()
return make_response(
jsonify({"success": True, "message": "Event " + id + " retained"}), 200
)
@bp.route("/events/<id>/plus", methods=("POST",))
def send_to_plus(id):
if not current_app.plus_api.is_active():
message = "PLUS_API_KEY environment variable is not set"
logger.error(message)
return make_response(
jsonify(
{
"success": False,
"message": message,
}
),
400,
)
try:
event = Event.get(Event.id == id)
except DoesNotExist:
message = f"Event {id} not found"
logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 404)
if event.plus_id:
message = "Already submitted to plus"
logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 400)
# load clean.png
try:
filename = f"{event.camera}-{event.id}-clean.png"
image = cv2.imread(os.path.join(CLIPS_DIR, filename))
except Exception:
logger.error(f"Unable to load clean png for event: {event.id}")
return make_response(
jsonify(
{"success": False, "message": "Unable to load clean png for event"}
),
400,
)
try:
plus_id = current_app.plus_api.upload_image(image, event.camera)
except Exception as ex:
logger.exception(ex)
return make_response(
jsonify({"success": False, "message": str(ex)}),
400,
)
# store image id in the database
event.plus_id = plus_id
event.save()
return make_response(jsonify({"success": True, "plus_id": plus_id}), 200)
@bp.route("/events/<id>/retain", methods=("DELETE",))
def delete_retain(id):
try:
event = Event.get(Event.id == id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
)
event.retain_indefinitely = False
event.save()
return make_response(
jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200
)
@bp.route("/events/<id>/sub_label", methods=("POST",))
def set_sub_label(id):
try:
event = Event.get(Event.id == id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
)
if request.json:
new_sub_label = request.json.get("subLabel")
else:
new_sub_label = None
if new_sub_label and len(new_sub_label) > 20:
return make_response(
jsonify(
{
"success": False,
"message": new_sub_label
+ " exceeds the 20 character limit for sub_label",
}
),
400,
)
event.sub_label = new_sub_label
event.save()
return make_response(
jsonify(
{
"success": True,
"message": "Event " + id + " sub label set to " + new_sub_label,
}
),
200,
)
@bp.route("/sub_labels")
def get_sub_labels():
try:
events = Event.select(Event.sub_label).distinct()
except Exception as e:
return jsonify(
{"success": False, "message": f"Failed to get sub_labels: {e}"}, "404"
)
sub_labels = [e.sub_label for e in events]
if None in sub_labels:
sub_labels.remove(None)
return jsonify(sub_labels)
@bp.route("/events/<id>", methods=("DELETE",)) @bp.route("/events/<id>", methods=("DELETE",))
def delete_event(id): def delete_event(id):
try: try:
@ -146,11 +290,14 @@ def delete_event(id):
@bp.route("/events/<id>/thumbnail.jpg") @bp.route("/events/<id>/thumbnail.jpg")
def event_thumbnail(id): def event_thumbnail(id, max_cache_age=2592000):
format = request.args.get("format", "ios") format = request.args.get("format", "ios")
thumbnail_bytes = None thumbnail_bytes = None
event_complete = False
try: try:
event = Event.get(Event.id == id) event = Event.get(Event.id == id)
if not event.end_time is None:
event_complete = True
thumbnail_bytes = base64.b64decode(event.thumbnail) thumbnail_bytes = base64.b64decode(event.thumbnail)
except DoesNotExist: except DoesNotExist:
# see if the object is currently being tracked # see if the object is currently being tracked
@ -185,15 +332,55 @@ def event_thumbnail(id):
response = make_response(thumbnail_bytes) response = make_response(thumbnail_bytes)
response.headers["Content-Type"] = "image/jpeg" response.headers["Content-Type"] = "image/jpeg"
if event_complete:
response.headers["Cache-Control"] = f"private, max-age={max_cache_age}"
else:
response.headers["Cache-Control"] = "no-store"
return response
@bp.route("/<camera_name>/<label>/best.jpg")
@bp.route("/<camera_name>/<label>/thumbnail.jpg")
def label_thumbnail(camera_name, label):
label = unquote(label)
if label == "any":
event_query = (
Event.select()
.where(Event.camera == camera_name)
.where(Event.has_snapshot == True)
.order_by(Event.start_time.desc())
)
else:
event_query = (
Event.select()
.where(Event.camera == camera_name)
.where(Event.label == label)
.where(Event.has_snapshot == True)
.order_by(Event.start_time.desc())
)
try:
event = event_query.get()
return event_thumbnail(event.id, 60)
except DoesNotExist:
frame = np.zeros((175, 175, 3), np.uint8)
ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
response = make_response(jpg.tobytes())
response.headers["Content-Type"] = "image/jpeg"
response.headers["Cache-Control"] = "no-store"
return response return response
@bp.route("/events/<id>/snapshot.jpg") @bp.route("/events/<id>/snapshot.jpg")
def event_snapshot(id): def event_snapshot(id):
download = request.args.get("download", type=bool) download = request.args.get("download", type=bool)
event_complete = False
jpg_bytes = None jpg_bytes = None
try: try:
event = Event.get(Event.id == id, Event.end_time != None) event = Event.get(Event.id == id, Event.end_time != None)
event_complete = True
if not event.has_snapshot: if not event.has_snapshot:
return "Snapshot not available", 404 return "Snapshot not available", 404
# read snapshot from disk # read snapshot from disk
@ -226,6 +413,10 @@ def event_snapshot(id):
response = make_response(jpg_bytes) response = make_response(jpg_bytes)
response.headers["Content-Type"] = "image/jpeg" response.headers["Content-Type"] = "image/jpeg"
if event_complete:
response.headers["Cache-Control"] = "private, max-age=31536000"
else:
response.headers["Cache-Control"] = "no-store"
if download: if download:
response.headers[ response.headers[
"Content-Disposition" "Content-Disposition"
@ -233,6 +424,37 @@ def event_snapshot(id):
return response return response
@bp.route("/<camera_name>/<label>/snapshot.jpg")
def label_snapshot(camera_name, label):
label = unquote(label)
if label == "any":
event_query = (
Event.select()
.where(Event.camera == camera_name)
.where(Event.has_snapshot == True)
.order_by(Event.start_time.desc())
)
else:
event_query = (
Event.select()
.where(Event.camera == camera_name)
.where(Event.label == label)
.where(Event.has_snapshot == True)
.order_by(Event.start_time.desc())
)
try:
event = event_query.get()
return event_snapshot(event.id)
except DoesNotExist:
frame = np.zeros((720, 1280, 3), np.uint8)
ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
response = make_response(jpg.tobytes())
response.headers["Content-Type"] = "image/jpeg"
return response
@bp.route("/events/<id>/clip.mp4") @bp.route("/events/<id>/clip.mp4")
def event_clip(id): def event_clip(id):
download = request.args.get("download", type=bool) download = request.args.get("download", type=bool)
@ -271,9 +493,10 @@ def event_clip(id):
@bp.route("/events") @bp.route("/events")
def events(): def events():
limit = request.args.get("limit", 100) limit = request.args.get("limit", 100)
camera = request.args.get("camera") camera = request.args.get("camera", "all")
label = request.args.get("label") label = unquote(request.args.get("label", "all"))
zone = request.args.get("zone") sub_label = request.args.get("sub_label", "all")
zone = request.args.get("zone", "all")
after = request.args.get("after", type=float) after = request.args.get("after", type=float)
before = request.args.get("before", type=float) before = request.args.get("before", type=float)
has_clip = request.args.get("has_clip", type=int) has_clip = request.args.get("has_clip", type=int)
@ -283,20 +506,38 @@ def events():
clauses = [] clauses = []
excluded_fields = [] excluded_fields = []
if camera: selected_columns = [
Event.id,
Event.camera,
Event.label,
Event.zones,
Event.start_time,
Event.end_time,
Event.has_clip,
Event.has_snapshot,
Event.plus_id,
Event.retain_indefinitely,
Event.sub_label,
Event.top_score,
]
if camera != "all":
clauses.append((Event.camera == camera)) clauses.append((Event.camera == camera))
if label: if label != "all":
clauses.append((Event.label == label)) clauses.append((Event.label == label))
if zone: if sub_label != "all":
clauses.append((Event.sub_label == sub_label))
if zone != "all":
clauses.append((Event.zones.cast("text") % f'*"{zone}"*')) clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))
if after: if after:
clauses.append((Event.start_time >= after)) clauses.append((Event.start_time > after))
if before: if before:
clauses.append((Event.start_time <= before)) clauses.append((Event.start_time < before))
if not has_clip is None: if not has_clip is None:
clauses.append((Event.has_clip == has_clip)) clauses.append((Event.has_clip == has_clip))
@ -306,12 +547,14 @@ def events():
if not include_thumbnails: if not include_thumbnails:
excluded_fields.append(Event.thumbnail) excluded_fields.append(Event.thumbnail)
else:
selected_columns.append(Event.thumbnail)
if len(clauses) == 0: if len(clauses) == 0:
clauses.append((True)) clauses.append((True))
events = ( events = (
Event.select() Event.select(*selected_columns)
.where(reduce(operator.and_, clauses)) .where(reduce(operator.and_, clauses))
.order_by(Event.start_time.desc()) .order_by(Event.start_time.desc())
.limit(limit) .limit(limit)
@ -331,6 +574,8 @@ def config():
for cmd in camera_dict["ffmpeg_cmds"]: for cmd in camera_dict["ffmpeg_cmds"]:
cmd["cmd"] = " ".join(cmd["cmd"]) cmd["cmd"] = " ".join(cmd["cmd"])
config["plus"] = {"enabled": current_app.plus_api.is_active()}
return jsonify(config) return jsonify(config)
@ -352,48 +597,6 @@ def stats():
return jsonify(stats) return jsonify(stats)
@bp.route("/<camera_name>/<label>/best.jpg")
def best(camera_name, label):
if camera_name in current_app.frigate_config.cameras:
best_object = current_app.detected_frames_processor.get_best(camera_name, label)
best_frame = best_object.get("frame")
if best_frame is None:
best_frame = np.zeros((720, 1280, 3), np.uint8)
else:
best_frame = cv2.cvtColor(best_frame, cv2.COLOR_YUV2BGR_I420)
crop = bool(request.args.get("crop", 0, type=int))
if crop:
box_size = 300
box = best_object.get("box", (0, 0, box_size, box_size))
region = calculate_region(
best_frame.shape,
box[0],
box[1],
box[2],
box[3],
box_size,
multiplier=1.1,
)
best_frame = best_frame[region[1] : region[3], region[0] : region[2]]
height = int(request.args.get("h", str(best_frame.shape[0])))
width = int(height * best_frame.shape[1] / best_frame.shape[0])
resize_quality = request.args.get("quality", default=70, type=int)
best_frame = cv2.resize(
best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA
)
ret, jpg = cv2.imencode(
".jpg", best_frame, [int(cv2.IMWRITE_JPEG_QUALITY), resize_quality]
)
response = make_response(jpg.tobytes())
response.headers["Content-Type"] = "image/jpeg"
return response
else:
return "Camera named {} not found".format(camera_name), 404
@bp.route("/<camera_name>") @bp.route("/<camera_name>")
def mjpeg_feed(camera_name): def mjpeg_feed(camera_name):
fps = int(request.args.get("fps", "3")) fps = int(request.args.get("fps", "3"))
@ -451,132 +654,111 @@ def latest_frame(camera_name):
) )
response = make_response(jpg.tobytes()) response = make_response(jpg.tobytes())
response.headers["Content-Type"] = "image/jpeg" response.headers["Content-Type"] = "image/jpeg"
response.headers["Cache-Control"] = "no-store"
return response return response
else: else:
return "Camera named {} not found".format(camera_name), 404 return "Camera named {} not found".format(camera_name), 404
# return hourly summary for recordings of camera
@bp.route("/<camera_name>/recordings/summary")
def recordings_summary(camera_name):
recording_groups = (
Recordings.select(
fn.strftime(
"%Y-%m-%d %H",
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
).alias("hour"),
fn.SUM(Recordings.duration).alias("duration"),
fn.SUM(Recordings.motion).alias("motion"),
fn.SUM(Recordings.objects).alias("objects"),
)
.where(Recordings.camera == camera_name)
.group_by(
fn.strftime(
"%Y-%m-%d %H",
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
)
)
.order_by(
fn.strftime(
"%Y-%m-%d H",
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
).desc()
)
)
event_groups = (
Event.select(
fn.strftime(
"%Y-%m-%d %H", fn.datetime(Event.start_time, "unixepoch", "localtime")
).alias("hour"),
fn.COUNT(Event.id).alias("count"),
)
.where(Event.camera == camera_name, Event.has_clip)
.group_by(
fn.strftime(
"%Y-%m-%d %H", fn.datetime(Event.start_time, "unixepoch", "localtime")
),
)
.objects()
)
event_map = {g.hour: g.count for g in event_groups}
days = {}
for recording_group in recording_groups.objects():
parts = recording_group.hour.split()
hour = parts[1]
day = parts[0]
events_count = event_map.get(recording_group.hour, 0)
hour_data = {
"hour": hour,
"events": events_count,
"motion": recording_group.motion,
"objects": recording_group.objects,
"duration": round(recording_group.duration),
}
if day not in days:
days[day] = {"events": events_count, "hours": [hour_data], "day": day}
else:
days[day]["events"] += events_count
days[day]["hours"].append(hour_data)
return jsonify(list(days.values()))
# return hour of recordings data for camera
@bp.route("/<camera_name>/recordings") @bp.route("/<camera_name>/recordings")
def recordings(camera_name): def recordings(camera_name):
dates = OrderedDict() after = request.args.get(
"after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp()
)
before = request.args.get("before", type=float, default=datetime.now().timestamp())
# Retrieve all recordings for this camera
recordings = ( recordings = (
Recordings.select() Recordings.select(
.where(Recordings.camera == camera_name) Recordings.id,
.order_by(Recordings.start_time.asc()) Recordings.start_time,
Recordings.end_time,
Recordings.motion,
Recordings.objects,
)
.where(
Recordings.camera == camera_name,
Recordings.end_time >= after,
Recordings.start_time <= before,
)
.order_by(Recordings.start_time)
) )
last_end = 0 return jsonify([e for e in recordings.dicts()])
recording: Recordings
for recording in recordings:
date = datetime.fromtimestamp(recording.start_time)
key = date.strftime("%Y-%m-%d")
hour = date.strftime("%H")
# Create Day Record
if key not in dates:
dates[key] = OrderedDict()
# Create Hour Record
if hour not in dates[key]:
dates[key][hour] = {"delay": {}, "events": []}
# Check for delay
the_hour = datetime.strptime(f"{key} {hour}", "%Y-%m-%d %H").timestamp()
# diff current recording start time and the greater of the previous end time or top of the hour
diff = recording.start_time - max(last_end, the_hour)
# Determine seconds into recording
seconds = 0
if datetime.fromtimestamp(last_end).strftime("%H") == hour:
seconds = int(last_end - the_hour)
# Determine the delay
delay = min(int(diff), 3600 - seconds)
if delay > 1:
# Add an offset for any delay greater than a second
dates[key][hour]["delay"][seconds] = delay
last_end = recording.end_time
# Packing intervals to return all events with same label and overlapping times as one row.
# See: https://blogs.solidq.com/en/sqlserver/packing-intervals/
events = Event.raw(
"""WITH C1 AS
(
SELECT id, label, camera, top_score, start_time AS ts, +1 AS type, 1 AS sub
FROM event
WHERE camera = ?
UNION ALL
SELECT id, label, camera, top_score, end_time + 15 AS ts, -1 AS type, 0 AS sub
FROM event
WHERE camera = ?
),
C2 AS
(
SELECT C1.*,
SUM(type) OVER(PARTITION BY label ORDER BY ts, type DESC
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW) - sub AS cnt
FROM C1
),
C3 AS
(
SELECT id, label, camera, top_score, ts,
(ROW_NUMBER() OVER(PARTITION BY label ORDER BY ts) - 1) / 2 + 1
AS grpnum
FROM C2
WHERE cnt = 0
)
SELECT id, label, camera, top_score, start_time, end_time
FROM event
WHERE camera = ? AND end_time IS NULL
UNION ALL
SELECT MIN(id) as id, label, camera, MAX(top_score) as top_score, MIN(ts) AS start_time, max(ts) AS end_time
FROM C3
GROUP BY label, grpnum
ORDER BY start_time;""",
camera_name,
camera_name,
camera_name,
)
event: Event
for event in events:
date = datetime.fromtimestamp(event.start_time)
key = date.strftime("%Y-%m-%d")
hour = date.strftime("%H")
if key in dates and hour in dates[key]:
dates[key][hour]["events"].append(
model_to_dict(
event,
exclude=[
Event.false_positive,
Event.zones,
Event.thumbnail,
Event.has_clip,
Event.has_snapshot,
],
)
)
return jsonify(
[
{
"date": date,
"events": sum([len(value["events"]) for value in hours.values()]),
"recordings": [
{"hour": hour, "delay": value["delay"], "events": value["events"]}
for hour, value in hours.items()
],
}
for date, hours in dates.items()
]
)
@bp.route("/<camera>/start/<int:start_ts>/end/<int:end_ts>/clip.mp4") @bp.route("/<camera_name>/start/<int:start_ts>/end/<int:end_ts>/clip.mp4")
@bp.route("/<camera>/start/<float:start_ts>/end/<float:end_ts>/clip.mp4") @bp.route("/<camera_name>/start/<float:start_ts>/end/<float:end_ts>/clip.mp4")
def recording_clip(camera, start_ts, end_ts): def recording_clip(camera_name, start_ts, end_ts):
download = request.args.get("download", type=bool) download = request.args.get("download", type=bool)
recordings = ( recordings = (
@ -586,7 +768,7 @@ def recording_clip(camera, start_ts, end_ts):
| (Recordings.end_time.between(start_ts, end_ts)) | (Recordings.end_time.between(start_ts, end_ts))
| ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time))
) )
.where(Recordings.camera == camera) .where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.asc()) .order_by(Recordings.start_time.asc())
) )
@ -601,9 +783,10 @@ def recording_clip(camera, start_ts, end_ts):
if clip.end_time > end_ts: if clip.end_time > end_ts:
playlist_lines.append(f"outpoint {int(end_ts - clip.start_time)}") playlist_lines.append(f"outpoint {int(end_ts - clip.start_time)}")
file_name = f"clip_{camera}_{start_ts}-{end_ts}.mp4" file_name = f"clip_{camera_name}_{start_ts}-{end_ts}.mp4"
path = f"/tmp/cache/{file_name}" path = f"/tmp/cache/{file_name}"
if not os.path.exists(path):
ffmpeg_cmd = [ ffmpeg_cmd = [
"ffmpeg", "ffmpeg",
"-y", "-y",
@ -614,23 +797,27 @@ def recording_clip(camera, start_ts, end_ts):
"-safe", "-safe",
"0", "0",
"-i", "-i",
"-", "/dev/stdin",
"-c", "-c",
"copy", "copy",
"-movflags", "-movflags",
"+faststart", "+faststart",
path, path,
] ]
p = sp.run( p = sp.run(
ffmpeg_cmd, ffmpeg_cmd,
input="\n".join(playlist_lines), input="\n".join(playlist_lines),
encoding="ascii", encoding="ascii",
capture_output=True, capture_output=True,
) )
if p.returncode != 0: if p.returncode != 0:
logger.error(p.stderr) logger.error(p.stderr)
return f"Could not create clip from recordings for {camera}.", 500 return f"Could not create clip from recordings for {camera_name}.", 500
else:
logger.debug(
f"Ignoring subsequent request for {path} as it already exists in the cache."
)
response = make_response() response = make_response()
response.headers["Content-Description"] = "File Transfer" response.headers["Content-Description"] = "File Transfer"
@ -646,9 +833,9 @@ def recording_clip(camera, start_ts, end_ts):
return response return response
@bp.route("/vod/<camera>/start/<int:start_ts>/end/<int:end_ts>") @bp.route("/vod/<camera_name>/start/<int:start_ts>/end/<int:end_ts>")
@bp.route("/vod/<camera>/start/<float:start_ts>/end/<float:end_ts>") @bp.route("/vod/<camera_name>/start/<float:start_ts>/end/<float:end_ts>")
def vod_ts(camera, start_ts, end_ts): def vod_ts(camera_name, start_ts, end_ts):
recordings = ( recordings = (
Recordings.select() Recordings.select()
.where( .where(
@ -656,7 +843,7 @@ def vod_ts(camera, start_ts, end_ts):
| Recordings.end_time.between(start_ts, end_ts) | Recordings.end_time.between(start_ts, end_ts)
| ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time))
) )
.where(Recordings.camera == camera) .where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.asc()) .order_by(Recordings.start_time.asc())
) )
@ -667,16 +854,13 @@ def vod_ts(camera, start_ts, end_ts):
for recording in recordings: for recording in recordings:
clip = {"type": "source", "path": recording.path} clip = {"type": "source", "path": recording.path}
duration = int(recording.duration * 1000) duration = int(recording.duration * 1000)
# Determine if offset is needed for first clip
if recording.start_time < start_ts:
offset = int((start_ts - recording.start_time) * 1000)
clip["clipFrom"] = offset
duration -= offset
# Determine if we need to end the last clip early # Determine if we need to end the last clip early
if recording.end_time > end_ts: if recording.end_time > end_ts:
duration -= int((recording.end_time - end_ts) * 1000) duration -= int((recording.end_time - end_ts) * 1000)
if duration > 0: if duration > 0:
clip["keyFrameDurations"] = [duration]
clips.append(clip) clips.append(clip)
durations.append(duration) durations.append(duration)
else: else:
@ -697,14 +881,14 @@ def vod_ts(camera, start_ts, end_ts):
) )
@bp.route("/vod/<year_month>/<day>/<hour>/<camera>") @bp.route("/vod/<year_month>/<day>/<hour>/<camera_name>")
def vod_hour(year_month, day, hour, camera): def vod_hour(year_month, day, hour, camera_name):
start_date = datetime.strptime(f"{year_month}-{day} {hour}", "%Y-%m-%d %H") start_date = datetime.strptime(f"{year_month}-{day} {hour}", "%Y-%m-%d %H")
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
start_ts = start_date.timestamp() start_ts = start_date.timestamp()
end_ts = end_date.timestamp() end_ts = end_date.timestamp()
return vod_ts(camera, start_ts, end_ts) return vod_ts(camera_name, start_ts, end_ts)
@bp.route("/vod/event/<id>") @bp.route("/vod/event/<id>")

View File

@ -4,13 +4,14 @@ import threading
import os import os
import signal import signal
import queue import queue
import multiprocessing as mp from multiprocessing.queues import Queue
from logging import handlers from logging import handlers
from setproctitle import setproctitle from setproctitle import setproctitle
from typing import Deque
from collections import deque from collections import deque
def listener_configurer(): def listener_configurer() -> None:
root = logging.getLogger() root = logging.getLogger()
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
formatter = logging.Formatter( formatter = logging.Formatter(
@ -21,14 +22,14 @@ def listener_configurer():
root.setLevel(logging.INFO) root.setLevel(logging.INFO)
def root_configurer(queue): def root_configurer(queue: Queue) -> None:
h = handlers.QueueHandler(queue) h = handlers.QueueHandler(queue)
root = logging.getLogger() root = logging.getLogger()
root.addHandler(h) root.addHandler(h)
root.setLevel(logging.INFO) root.setLevel(logging.INFO)
def log_process(log_queue): def log_process(log_queue: Queue) -> None:
threading.current_thread().name = f"logger" threading.current_thread().name = f"logger"
setproctitle("frigate.logger") setproctitle("frigate.logger")
listener_configurer() listener_configurer()
@ -43,34 +44,32 @@ def log_process(log_queue):
# based on https://codereview.stackexchange.com/a/17959 # based on https://codereview.stackexchange.com/a/17959
class LogPipe(threading.Thread): class LogPipe(threading.Thread):
def __init__(self, log_name, level): def __init__(self, log_name: str):
"""Setup the object with a logger and a loglevel """Setup the object with a logger and start the thread"""
and start the thread
"""
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.daemon = False self.daemon = False
self.logger = logging.getLogger(log_name) self.logger = logging.getLogger(log_name)
self.level = level self.level = logging.ERROR
self.deque = deque(maxlen=100) self.deque: Deque[str] = deque(maxlen=100)
self.fdRead, self.fdWrite = os.pipe() self.fdRead, self.fdWrite = os.pipe()
self.pipeReader = os.fdopen(self.fdRead) self.pipeReader = os.fdopen(self.fdRead)
self.start() self.start()
def fileno(self): def fileno(self) -> int:
"""Return the write file descriptor of the pipe""" """Return the write file descriptor of the pipe"""
return self.fdWrite return self.fdWrite
def run(self): def run(self) -> None:
"""Run the thread, logging everything.""" """Run the thread, logging everything."""
for line in iter(self.pipeReader.readline, ""): for line in iter(self.pipeReader.readline, ""):
self.deque.append(line.strip("\n")) self.deque.append(line.strip("\n"))
self.pipeReader.close() self.pipeReader.close()
def dump(self): def dump(self) -> None:
while len(self.deque) > 0: while len(self.deque) > 0:
self.logger.log(self.level, self.deque.popleft()) self.logger.log(self.level, self.deque.popleft())
def close(self): def close(self) -> None:
"""Close the write end of the pipe.""" """Close the write end of the pipe."""
os.close(self.fdWrite) os.close(self.fdWrite)

View File

@ -1,11 +1,20 @@
from numpy import unique from numpy import unique
from peewee import * from peewee import (
from playhouse.sqlite_ext import * Model,
CharField,
DateTimeField,
FloatField,
BooleanField,
TextField,
IntegerField,
)
from playhouse.sqlite_ext import JSONField
class Event(Model): class Event(Model): # type: ignore[misc]
id = CharField(null=False, primary_key=True, max_length=30) id = CharField(null=False, primary_key=True, max_length=30)
label = CharField(index=True, max_length=20) label = CharField(index=True, max_length=20)
sub_label = CharField(max_length=20, null=True)
camera = CharField(index=True, max_length=20) camera = CharField(index=True, max_length=20)
start_time = DateTimeField() start_time = DateTimeField()
end_time = DateTimeField() end_time = DateTimeField()
@ -18,9 +27,12 @@ class Event(Model):
region = JSONField() region = JSONField()
box = JSONField() box = JSONField()
area = IntegerField() area = IntegerField()
retain_indefinitely = BooleanField(default=False)
ratio = FloatField(default=1.0)
plus_id = CharField(max_length=30)
class Recordings(Model): class Recordings(Model): # type: ignore[misc]
id = CharField(null=False, primary_key=True, max_length=30) id = CharField(null=False, primary_key=True, max_length=30)
camera = CharField(index=True, max_length=20) camera = CharField(index=True, max_length=20)
path = CharField(unique=True) path = CharField(unique=True)

View File

@ -5,7 +5,14 @@ from frigate.config import MotionConfig
class MotionDetector: class MotionDetector:
def __init__(self, frame_shape, config: MotionConfig): def __init__(
self,
frame_shape,
config: MotionConfig,
improve_contrast_enabled,
motion_threshold,
motion_contour_area,
):
self.config = config self.config = config
self.frame_shape = frame_shape self.frame_shape = frame_shape
self.resize_factor = frame_shape[0] / config.frame_height self.resize_factor = frame_shape[0] / config.frame_height
@ -24,6 +31,9 @@ class MotionDetector:
) )
self.mask = np.where(resized_mask == [0]) self.mask = np.where(resized_mask == [0])
self.save_images = False self.save_images = False
self.improve_contrast = improve_contrast_enabled
self.threshold = motion_threshold
self.contour_area = motion_contour_area
def detect(self, frame): def detect(self, frame):
motion_boxes = [] motion_boxes = []
@ -38,7 +48,7 @@ class MotionDetector:
) )
# Improve contrast # Improve contrast
if self.config.improve_contrast: if self.improve_contrast.value:
minval = np.percentile(resized_frame, 4) minval = np.percentile(resized_frame, 4)
maxval = np.percentile(resized_frame, 96) maxval = np.percentile(resized_frame, 96)
# don't adjust if the image is a single color # don't adjust if the image is a single color
@ -68,7 +78,7 @@ class MotionDetector:
# compute the threshold image for the current frame # compute the threshold image for the current frame
current_thresh = cv2.threshold( current_thresh = cv2.threshold(
frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY frameDelta, self.threshold.value, 255, cv2.THRESH_BINARY
)[1] )[1]
# black out everything in the avg_delta where there isnt motion in the current frame # black out everything in the avg_delta where there isnt motion in the current frame
@ -78,7 +88,7 @@ class MotionDetector:
# then look for deltas above the threshold, but only in areas where there is a delta # then look for deltas above the threshold, but only in areas where there is a delta
# in the current frame. this prevents deltas from previous frames from being included # in the current frame. this prevents deltas from previous frames from being included
thresh = cv2.threshold( thresh = cv2.threshold(
avg_delta_image, self.config.threshold, 255, cv2.THRESH_BINARY avg_delta_image, self.threshold.value, 255, cv2.THRESH_BINARY
)[1] )[1]
# dilate the thresholded image to fill in holes, then find contours # dilate the thresholded image to fill in holes, then find contours
@ -93,7 +103,7 @@ class MotionDetector:
for c in cnts: for c in cnts:
# if the contour is big enough, count it as motion # if the contour is big enough, count it as motion
contour_area = cv2.contourArea(c) contour_area = cv2.contourArea(c)
if contour_area > self.config.contour_area: if contour_area > self.contour_area.value:
x, y, w, h = cv2.boundingRect(c) x, y, w, h = cv2.boundingRect(c)
motion_boxes.append( motion_boxes.append(
( (
@ -110,8 +120,7 @@ class MotionDetector:
# print(self.frame_counter) # print(self.frame_counter)
for c in cnts: for c in cnts:
contour_area = cv2.contourArea(c) contour_area = cv2.contourArea(c)
# print(contour_area) if contour_area > self.contour_area.value:
if contour_area > self.config.contour_area:
x, y, w, h = cv2.boundingRect(c) x, y, w, h = cv2.boundingRect(c)
cv2.rectangle( cv2.rectangle(
thresh_dilated, thresh_dilated,

View File

@ -78,6 +78,12 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
logger.info(f"Turning on detection for {camera_name} via mqtt") logger.info(f"Turning on detection for {camera_name} via mqtt")
camera_metrics[camera_name]["detection_enabled"].value = True camera_metrics[camera_name]["detection_enabled"].value = True
detect_settings.enabled = True detect_settings.enabled = True
if not camera_metrics[camera_name]["motion_enabled"].value:
logger.info(
f"Turning on motion for {camera_name} due to detection being enabled."
)
camera_metrics[camera_name]["motion_enabled"].value = True
elif payload == "OFF": elif payload == "OFF":
if camera_metrics[camera_name]["detection_enabled"].value: if camera_metrics[camera_name]["detection_enabled"].value:
logger.info(f"Turning off detection for {camera_name} via mqtt") logger.info(f"Turning off detection for {camera_name} via mqtt")
@ -89,6 +95,102 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
state_topic = f"{message.topic[:-4]}/state" state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True) client.publish(state_topic, payload, retain=True)
def on_motion_command(client, userdata, message):
payload = message.payload.decode()
logger.debug(f"on_motion_toggle: {message.topic} {payload}")
camera_name = message.topic.split("/")[-3]
if payload == "ON":
if not camera_metrics[camera_name]["motion_enabled"].value:
logger.info(f"Turning on motion for {camera_name} via mqtt")
camera_metrics[camera_name]["motion_enabled"].value = True
elif payload == "OFF":
if camera_metrics[camera_name]["detection_enabled"].value:
logger.error(
f"Turning off motion is not allowed when detection is enabled."
)
return
if camera_metrics[camera_name]["motion_enabled"].value:
logger.info(f"Turning off motion for {camera_name} via mqtt")
camera_metrics[camera_name]["motion_enabled"].value = False
else:
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_improve_contrast_command(client, userdata, message):
payload = message.payload.decode()
logger.debug(f"on_improve_contrast_toggle: {message.topic} {payload}")
camera_name = message.topic.split("/")[-3]
motion_settings = config.cameras[camera_name].motion
if payload == "ON":
if not camera_metrics[camera_name]["improve_contrast_enabled"].value:
logger.info(f"Turning on improve contrast for {camera_name} via mqtt")
camera_metrics[camera_name]["improve_contrast_enabled"].value = True
motion_settings.improve_contrast = True
elif payload == "OFF":
if camera_metrics[camera_name]["improve_contrast_enabled"].value:
logger.info(f"Turning off improve contrast for {camera_name} via mqtt")
camera_metrics[camera_name]["improve_contrast_enabled"].value = False
motion_settings.improve_contrast = False
else:
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_motion_threshold_command(client, userdata, message):
try:
payload = int(message.payload.decode())
except ValueError:
logger.warning(
f"Received unsupported value at {message.topic}: {message.payload.decode()}"
)
return
logger.debug(f"on_motion_threshold_toggle: {message.topic} {payload}")
camera_name = message.topic.split("/")[-3]
motion_settings = config.cameras[camera_name].motion
logger.info(f"Setting motion threshold for {camera_name} via mqtt: {payload}")
camera_metrics[camera_name]["motion_threshold"].value = payload
motion_settings.threshold = payload
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_motion_contour_area_command(client, userdata, message):
try:
payload = int(message.payload.decode())
except ValueError:
logger.warning(
f"Received unsupported value at {message.topic}: {message.payload.decode()}"
)
return
logger.debug(f"on_motion_contour_area_toggle: {message.topic} {payload}")
camera_name = message.topic.split("/")[-3]
motion_settings = config.cameras[camera_name].motion
logger.info(
f"Setting motion contour area for {camera_name} via mqtt: {payload}"
)
camera_metrics[camera_name]["motion_contour_area"].value = payload
motion_settings.contour_area = payload
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_restart_command(client, userdata, message): def on_restart_command(client, userdata, message):
restart_frigate() restart_frigate()
@ -96,9 +198,13 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
threading.current_thread().name = "mqtt" threading.current_thread().name = "mqtt"
if rc != 0: if rc != 0:
if rc == 3: if rc == 3:
logger.error("Unable to connect to MQTT server: MQTT Server unavailable") logger.error(
"Unable to connect to MQTT server: MQTT Server unavailable"
)
elif rc == 4: elif rc == 4:
logger.error("Unable to connect to MQTT server: MQTT Bad username or password") logger.error(
"Unable to connect to MQTT server: MQTT Bad username or password"
)
elif rc == 5: elif rc == 5:
logger.error("Unable to connect to MQTT server: MQTT Not authorized") logger.error("Unable to connect to MQTT server: MQTT Not authorized")
else: else:
@ -128,6 +234,21 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
client.message_callback_add( client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/detect/set", on_detect_command f"{mqtt_config.topic_prefix}/{name}/detect/set", on_detect_command
) )
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/motion/set", on_motion_command
)
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/improve_contrast/set",
on_improve_contrast_command,
)
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/motion_threshold/set",
on_motion_threshold_command,
)
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/motion_contour_area/set",
on_motion_contour_area_command,
)
client.message_callback_add( client.message_callback_add(
f"{mqtt_config.topic_prefix}/restart", on_restart_command f"{mqtt_config.topic_prefix}/restart", on_restart_command
@ -173,6 +294,31 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
"ON" if config.cameras[name].detect.enabled else "OFF", "ON" if config.cameras[name].detect.enabled else "OFF",
retain=True, retain=True,
) )
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion/state",
"ON",
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/improve_contrast/state",
"ON" if config.cameras[name].motion.improve_contrast else "OFF",
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion_threshold/state",
config.cameras[name].motion.threshold,
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion_contour_area/state",
config.cameras[name].motion.contour_area,
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion",
"OFF",
retain=False,
)
return client return client

60
frigate/mypy.ini Normal file
View File

@ -0,0 +1,60 @@
[mypy]
python_version = 3.9
show_error_codes = true
follow_imports = normal
ignore_missing_imports = true
strict_equality = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_unused_configs = true
warn_unused_ignores = true
enable_error_code = ignore-without-code
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
no_implicit_reexport = true
[mypy-frigate.*]
ignore_errors = true
[mypy-frigate.__main__]
ignore_errors = false
disallow_untyped_calls = false
[mypy-frigate.app]
ignore_errors = false
disallow_untyped_calls = false
[mypy-frigate.const]
ignore_errors = false
[mypy-frigate.log]
ignore_errors = false
[mypy-frigate.models]
ignore_errors = false
[mypy-frigate.plus]
ignore_errors = false
[mypy-frigate.stats]
ignore_errors = false
[mypy-frigate.types]
ignore_errors = false
[mypy-frigate.version]
ignore_errors = false
[mypy-frigate.watchdog]
ignore_errors = false
disallow_untyped_calls = false
[mypy-frigate.zeroconf]
ignore_errors = false

View File

@ -1,29 +1,24 @@
import base64 import base64
import copy
import datetime import datetime
import hashlib
import itertools
import json import json
import logging import logging
import os import os
import queue import queue
import threading import threading
import time
from collections import Counter, defaultdict from collections import Counter, defaultdict
from statistics import mean, median from statistics import median
from typing import Callable, Dict from typing import Callable
import cv2 import cv2
import numpy as np import numpy as np
from frigate.config import CameraConfig, SnapshotsConfig, RecordConfig, FrigateConfig from frigate.config import CameraConfig, SnapshotsConfig, RecordConfig, FrigateConfig
from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR from frigate.const import CLIPS_DIR
from frigate.util import ( from frigate.util import (
SharedMemoryFrameManager, SharedMemoryFrameManager,
calculate_region, calculate_region,
draw_box_with_label, draw_box_with_label,
draw_timestamp, draw_timestamp,
load_labels,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -192,6 +187,7 @@ class TrackedObject:
"score": self.obj_data["score"], "score": self.obj_data["score"],
"box": self.obj_data["box"], "box": self.obj_data["box"],
"area": self.obj_data["area"], "area": self.obj_data["area"],
"ratio": self.obj_data["ratio"],
"region": self.obj_data["region"], "region": self.obj_data["region"],
"stationary": self.obj_data["motionless_count"] "stationary": self.obj_data["motionless_count"]
> self.camera_config.detect.stationary.threshold, > self.camera_config.detect.stationary.threshold,
@ -341,6 +337,14 @@ def zone_filtered(obj: TrackedObject, object_config):
if obj_settings.threshold > obj.computed_score: if obj_settings.threshold > obj.computed_score:
return True return True
# if the object is not proportionally wide enough
if obj_settings.min_ratio > obj.obj_data["ratio"]:
return True
# if the object is proportionally too wide
if obj_settings.max_ratio < obj.obj_data["ratio"]:
return True
return False return False
@ -353,9 +357,9 @@ class CameraState:
self.config = config self.config = config
self.camera_config = config.cameras[name] self.camera_config = config.cameras[name]
self.frame_manager = frame_manager self.frame_manager = frame_manager
self.best_objects: Dict[str, TrackedObject] = {} self.best_objects: dict[str, TrackedObject] = {}
self.object_counts = defaultdict(int) self.object_counts = defaultdict(int)
self.tracked_objects: Dict[str, TrackedObject] = {} self.tracked_objects: dict[str, TrackedObject] = {}
self.frame_cache = {} self.frame_cache = {}
self.zone_objects = defaultdict(list) self.zone_objects = defaultdict(list)
self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8) self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8)
@ -452,7 +456,7 @@ class CameraState:
def finished(self, obj_id): def finished(self, obj_id):
del self.tracked_objects[obj_id] del self.tracked_objects[obj_id]
def on(self, event_type: str, callback: Callable[[Dict], None]): def on(self, event_type: str, callback: Callable[[dict], None]):
self.callbacks[event_type].append(callback) self.callbacks[event_type].append(callback)
def update(self, frame_time, current_detections, motion_boxes, regions): def update(self, frame_time, current_detections, motion_boxes, regions):
@ -554,13 +558,24 @@ class CameraState:
if not obj.false_positive if not obj.false_positive
) )
# keep track of all labels detected for this camera
total_label_count = 0
# report on detected objects # report on detected objects
for obj_name, count in obj_counter.items(): for obj_name, count in obj_counter.items():
total_label_count += count
if count != self.object_counts[obj_name]: if count != self.object_counts[obj_name]:
self.object_counts[obj_name] = count self.object_counts[obj_name] = count
for c in self.callbacks["object_status"]: for c in self.callbacks["object_status"]:
c(self.name, obj_name, count) c(self.name, obj_name, count)
# publish for all labels detected for this camera
if total_label_count != self.object_counts.get("all"):
self.object_counts["all"] = total_label_count
for c in self.callbacks["object_status"]:
c(self.name, "all", total_label_count)
# expire any objects that are >0 and no longer detected # expire any objects that are >0 and no longer detected
expired_objects = [ expired_objects = [
obj_name obj_name
@ -568,6 +583,10 @@ class CameraState:
if count > 0 and obj_name not in obj_counter if count > 0 and obj_name not in obj_counter
] ]
for obj_name in expired_objects: for obj_name in expired_objects:
# Ignore the artificial all label
if obj_name == "all":
continue
self.object_counts[obj_name] = 0 self.object_counts[obj_name] = 0
for c in self.callbacks["object_status"]: for c in self.callbacks["object_status"]:
c(self.name, obj_name, 0) c(self.name, obj_name, 0)
@ -626,8 +645,9 @@ class TrackedObjectProcessor(threading.Thread):
self.video_output_queue = video_output_queue self.video_output_queue = video_output_queue
self.recordings_info_queue = recordings_info_queue self.recordings_info_queue = recordings_info_queue
self.stop_event = stop_event self.stop_event = stop_event
self.camera_states: Dict[str, CameraState] = {} self.camera_states: dict[str, CameraState] = {}
self.frame_manager = SharedMemoryFrameManager() self.frame_manager = SharedMemoryFrameManager()
self.last_motion_detected: dict[str, float] = {}
def start(camera, obj: TrackedObject, current_frame_time): def start(camera, obj: TrackedObject, current_frame_time):
self.event_queue.put(("start", camera, obj.to_dict())) self.event_queue.put(("start", camera, obj.to_dict()))
@ -820,6 +840,32 @@ class TrackedObjectProcessor(threading.Thread):
return True return True
def update_mqtt_motion(self, camera, frame_time, motion_boxes):
# publish if motion is currently being detected
if motion_boxes:
# only send ON if motion isn't already active
if self.last_motion_detected.get(camera, 0) == 0:
self.client.publish(
f"{self.topic_prefix}/{camera}/motion",
"ON",
retain=False,
)
# always updated latest motion
self.last_motion_detected[camera] = frame_time
elif self.last_motion_detected.get(camera, 0) > 0:
mqtt_delay = self.config.cameras[camera].motion.mqtt_off_delay
# If no motion, make sure the off_delay has passed
if frame_time - self.last_motion_detected.get(camera, 0) >= mqtt_delay:
self.client.publish(
f"{self.topic_prefix}/{camera}/motion",
"OFF",
retain=False,
)
# reset the last_motion so redundant `off` commands aren't sent
self.last_motion_detected[camera] = 0
def get_best(self, camera, label): def get_best(self, camera, label):
# TODO: need a lock here # TODO: need a lock here
camera_state = self.camera_states[camera] camera_state = self.camera_states[camera]
@ -855,6 +901,8 @@ class TrackedObjectProcessor(threading.Thread):
frame_time, current_tracked_objects, motion_boxes, regions frame_time, current_tracked_objects, motion_boxes, regions
) )
self.update_mqtt_motion(camera, frame_time, motion_boxes)
tracked_objects = [ tracked_objects = [
o.to_dict() for o in camera_state.tracked_objects.values() o.to_dict() for o in camera_state.tracked_objects.values()
] ]
@ -889,9 +937,14 @@ class TrackedObjectProcessor(threading.Thread):
for obj in camera_state.tracked_objects.values() for obj in camera_state.tracked_objects.values()
if zone in obj.current_zones and not obj.false_positive if zone in obj.current_zones and not obj.false_positive
) )
total_label_count = 0
# update counts and publish status # update counts and publish status
for label in set(self.zone_data[zone].keys()) | set(obj_counter.keys()): for label in set(self.zone_data[zone].keys()) | set(obj_counter.keys()):
# Ignore the artificial all label
if label == "all":
continue
# if we have previously published a count for this zone/label # if we have previously published a count for this zone/label
zone_label = self.zone_data[zone][label] zone_label = self.zone_data[zone][label]
if camera in zone_label: if camera in zone_label:
@ -906,6 +959,10 @@ class TrackedObjectProcessor(threading.Thread):
new_count, new_count,
retain=False, retain=False,
) )
# Set the count for the /zone/all topic.
total_label_count += new_count
# if this is a new zone/label combo for this camera # if this is a new zone/label combo for this camera
else: else:
if label in obj_counter: if label in obj_counter:
@ -916,6 +973,31 @@ class TrackedObjectProcessor(threading.Thread):
retain=False, retain=False,
) )
# Set the count for the /zone/all topic.
total_label_count += obj_counter[label]
# if we have previously published a count for this zone all labels
zone_label = self.zone_data[zone]["all"]
if camera in zone_label:
current_count = sum(zone_label.values())
zone_label[camera] = total_label_count
new_count = sum(zone_label.values())
if new_count != current_count:
self.client.publish(
f"{self.topic_prefix}/{zone}/all",
new_count,
retain=False,
)
# if this is a new zone all label for this camera
else:
zone_label[camera] = total_label_count
self.client.publish(
f"{self.topic_prefix}/{zone}/all",
total_label_count,
retain=False,
)
# cleanup event finished queue # cleanup event finished queue
while not self.event_processed_queue.empty(): while not self.event_processed_queue.empty():
event_id, camera = self.event_processed_queue.get() event_id, camera = self.event_processed_queue.get()

View File

@ -111,6 +111,8 @@ class ObjectTracker:
): ):
return True return True
return False
def update(self, id, new_obj): def update(self, id, new_obj):
self.disappeared[id] = 0 self.disappeared[id] = 0
# update the motionless count if the object has not moved to a new position # update the motionless count if the object has not moved to a new position
@ -149,7 +151,8 @@ class ObjectTracker:
"score": obj[1], "score": obj[1],
"box": obj[2], "box": obj[2],
"area": obj[3], "area": obj[3],
"region": obj[4], "ratio": obj[4],
"region": obj[5],
"frame_time": frame_time, "frame_time": frame_time,
} }
) )

View File

@ -22,6 +22,7 @@ from ws4py.server.wsgiutils import WebSocketWSGIApplication
from ws4py.websocket import WebSocket from ws4py.websocket import WebSocket
from frigate.config import BirdseyeModeEnum, FrigateConfig from frigate.config import BirdseyeModeEnum, FrigateConfig
from frigate.const import BASE_DIR
from frigate.util import SharedMemoryFrameManager, copy_yuv_to_position, get_yuv_crop from frigate.util import SharedMemoryFrameManager, copy_yuv_to_position, get_yuv_crop
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -104,12 +105,21 @@ class BirdsEyeFrameManager:
self.blank_frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] = 16 self.blank_frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] = 16
# find and copy the logo on the blank frame # find and copy the logo on the blank frame
birdseye_logo = None
custom_logo_files = glob.glob(f"{BASE_DIR}/custom.png")
if len(custom_logo_files) > 0:
birdseye_logo = cv2.imread(custom_logo_files[0], cv2.IMREAD_UNCHANGED)
if birdseye_logo is None:
logo_files = glob.glob("/opt/frigate/frigate/birdseye.png") logo_files = glob.glob("/opt/frigate/frigate/birdseye.png")
frigate_logo = None
if len(logo_files) > 0: if len(logo_files) > 0:
frigate_logo = cv2.imread(logo_files[0], cv2.IMREAD_UNCHANGED) birdseye_logo = cv2.imread(logo_files[0], cv2.IMREAD_UNCHANGED)
if not frigate_logo is None:
transparent_layer = frigate_logo[:, :, 3] if not birdseye_logo is None:
transparent_layer = birdseye_logo[:, :, 3]
y_offset = height // 2 - transparent_layer.shape[0] // 2 y_offset = height // 2 - transparent_layer.shape[0] // 2
x_offset = width // 2 - transparent_layer.shape[1] // 2 x_offset = width // 2 - transparent_layer.shape[1] // 2
self.blank_frame[ self.blank_frame[
@ -180,14 +190,14 @@ class BirdsEyeFrameManager:
channel_dims, channel_dims,
) )
def camera_active(self, object_box_count, motion_box_count): def camera_active(self, mode, object_box_count, motion_box_count):
if self.mode == BirdseyeModeEnum.continuous: if mode == BirdseyeModeEnum.continuous:
return True return True
if self.mode == BirdseyeModeEnum.motion and motion_box_count > 0: if mode == BirdseyeModeEnum.motion and motion_box_count > 0:
return True return True
if self.mode == BirdseyeModeEnum.objects and object_box_count > 0: if mode == BirdseyeModeEnum.objects and object_box_count > 0:
return True return True
def update_frame(self): def update_frame(self):
@ -301,10 +311,14 @@ class BirdsEyeFrameManager:
return True return True
def update(self, camera, object_count, motion_count, frame_time, frame) -> bool: def update(self, camera, object_count, motion_count, frame_time, frame) -> bool:
# don't process if birdseye is disabled for this camera
camera_config = self.config.cameras[camera].birdseye
if not camera_config.enabled:
return False
# update the last active frame for the camera # update the last active frame for the camera
self.cameras[camera]["current_frame"] = frame_time self.cameras[camera]["current_frame"] = frame_time
if self.camera_active(object_count, motion_count): if self.camera_active(camera_config.mode, object_count, motion_count):
self.cameras[camera]["last_active_frame"] = frame_time self.cameras[camera]["last_active_frame"] = frame_time
now = datetime.datetime.now().timestamp() now = datetime.datetime.now().timestamp()

126
frigate/plus.py Normal file
View File

@ -0,0 +1,126 @@
import datetime
import json
import logging
import os
import re
import requests
from frigate.const import PLUS_ENV_VAR, PLUS_API_HOST
from requests.models import Response
import cv2
from numpy import ndarray
logger = logging.getLogger(__name__)
def get_jpg_bytes(image: ndarray, max_dim: int, quality: int) -> bytes:
if image.shape[1] >= image.shape[0]:
width = min(max_dim, image.shape[1])
height = int(width * image.shape[0] / image.shape[1])
else:
height = min(max_dim, image.shape[0])
width = int(height * image.shape[1] / image.shape[0])
original = cv2.resize(image, dsize=(width, height), interpolation=cv2.INTER_AREA)
ret, jpg = cv2.imencode(".jpg", original, [int(cv2.IMWRITE_JPEG_QUALITY), quality])
jpg_bytes = jpg.tobytes()
return jpg_bytes if type(jpg_bytes) is bytes else b""
class PlusApi:
def __init__(self) -> None:
self.host = PLUS_API_HOST
self.key = None
if PLUS_ENV_VAR in os.environ:
self.key = os.environ.get(PLUS_ENV_VAR)
# check for the addon options file
elif os.path.isfile("/data/options.json"):
with open("/data/options.json") as f:
raw_options = f.read()
options = json.loads(raw_options)
self.key = options.get("plus_api_key")
if self.key is not None and not re.match(
r"[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}:[a-z0-9]{40}",
self.key,
):
logger.error("Plus API Key is not formatted correctly.")
self.key = None
self._is_active: bool = self.key is not None
self._token_data: dict = {}
def _refresh_token_if_needed(self) -> None:
if (
self._token_data.get("expires") is None
or self._token_data["expires"] - datetime.datetime.now().timestamp() < 60
):
if self.key is None:
raise Exception("Plus API not activated")
parts = self.key.split(":")
r = requests.get(f"{self.host}/v1/auth/token", auth=(parts[0], parts[1]))
if not r.ok:
raise Exception("Unable to refresh API token")
self._token_data = r.json()
def _get_authorization_header(self) -> dict:
self._refresh_token_if_needed()
return {"authorization": f"Bearer {self._token_data.get('accessToken')}"}
def _get(self, path: str) -> Response:
return requests.get(
f"{self.host}/v1/{path}", headers=self._get_authorization_header()
)
def _post(self, path: str, data: dict) -> Response:
return requests.post(
f"{self.host}/v1/{path}",
headers=self._get_authorization_header(),
json=data,
)
def is_active(self) -> bool:
return self._is_active
def upload_image(self, image: ndarray, camera: str) -> str:
r = self._get("image/signed_urls")
presigned_urls = r.json()
if not r.ok:
raise Exception("Unable to get signed urls")
# resize and submit original
files = {"file": get_jpg_bytes(image, 1920, 85)}
data = presigned_urls["original"]["fields"]
data["content-type"] = "image/jpeg"
r = requests.post(presigned_urls["original"]["url"], files=files, data=data)
if not r.ok:
logger.error(f"Failed to upload original: {r.status_code} {r.text}")
raise Exception(r.text)
# resize and submit annotate
files = {"file": get_jpg_bytes(image, 640, 70)}
data = presigned_urls["annotate"]["fields"]
data["content-type"] = "image/jpeg"
r = requests.post(presigned_urls["annotate"]["url"], files=files, data=data)
if not r.ok:
logger.error(f"Failed to upload annotate: {r.status_code} {r.text}")
raise Exception(r.text)
# resize and submit thumbnail
files = {"file": get_jpg_bytes(image, 200, 70)}
data = presigned_urls["thumbnail"]["fields"]
data["content-type"] = "image/jpeg"
r = requests.post(presigned_urls["thumbnail"]["url"], files=files, data=data)
if not r.ok:
logger.error(f"Failed to upload thumbnail: {r.status_code} {r.text}")
raise Exception(r.text)
# create image
r = self._post(
"image/create", {"id": presigned_urls["imageId"], "camera": camera}
)
if not r.ok:
raise Exception(r.text)
# return image id
return str(presigned_urls.get("imageId"))

View File

@ -9,7 +9,6 @@ import shutil
import string import string
import subprocess as sp import subprocess as sp
import threading import threading
import time
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
@ -100,11 +99,23 @@ class RecordingMaintainer(threading.Thread):
# delete all cached files past the most recent 5 # delete all cached files past the most recent 5
keep_count = 5 keep_count = 5
for camera in grouped_recordings.keys(): for camera in grouped_recordings.keys():
if len(grouped_recordings[camera]) > keep_count: segment_count = len(grouped_recordings[camera])
if segment_count > keep_count:
####
# Need to find a way to tell if these are aging out based on retention settings or if the system is overloaded.
####
# logger.warning(
# f"Too many recording segments in cache for {camera}. Keeping the {keep_count} most recent segments out of {segment_count}, discarding the rest..."
# )
to_remove = grouped_recordings[camera][:-keep_count] to_remove = grouped_recordings[camera][:-keep_count]
for f in to_remove: for f in to_remove:
Path(f["cache_path"]).unlink(missing_ok=True) cache_path = f["cache_path"]
self.end_time_cache.pop(f["cache_path"], None) ####
# Need to find a way to tell if these are aging out based on retention settings or if the system is overloaded.
####
# logger.warning(f"Discarding a recording segment: {cache_path}")
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
grouped_recordings[camera] = grouped_recordings[camera][-keep_count:] grouped_recordings[camera] = grouped_recordings[camera][-keep_count:]
for camera, recordings in grouped_recordings.items(): for camera, recordings in grouped_recordings.items():
@ -378,16 +389,11 @@ class RecordingCleanup(threading.Thread):
logger.debug("Start all cameras.") logger.debug("Start all cameras.")
for camera, config in self.config.cameras.items(): for camera, config in self.config.cameras.items():
logger.debug(f"Start camera: {camera}.") logger.debug(f"Start camera: {camera}.")
# When deleting recordings without events, we have to keep at LEAST the configured max clip duration # Get the timestamp for cutoff of retained days
min_end = (
datetime.datetime.now()
- datetime.timedelta(seconds=config.record.events.max_seconds)
).timestamp()
expire_days = config.record.retain.days expire_days = config.record.retain.days
expire_before = ( expire_date = (
datetime.datetime.now() - datetime.timedelta(days=expire_days) datetime.datetime.now() - datetime.timedelta(days=expire_days)
).timestamp() ).timestamp()
expire_date = min(min_end, expire_before)
# Get recordings to check for expiration # Get recordings to check for expiration
recordings: Recordings = ( recordings: Recordings = (
@ -459,7 +465,13 @@ class RecordingCleanup(threading.Thread):
deleted_recordings.add(recording.id) deleted_recordings.add(recording.id)
logger.debug(f"Expiring {len(deleted_recordings)} recordings") logger.debug(f"Expiring {len(deleted_recordings)} recordings")
Recordings.delete().where(Recordings.id << deleted_recordings).execute() # delete up to 100,000 at a time
max_deletes = 100000
deleted_recordings_list = list(deleted_recordings)
for i in range(0, len(deleted_recordings_list), max_deletes):
Recordings.delete().where(
Recordings.id << deleted_recordings_list[i : i + max_deletes]
).execute()
logger.debug(f"End camera: {camera}.") logger.debug(f"End camera: {camera}.")
@ -534,7 +546,12 @@ class RecordingCleanup(threading.Thread):
logger.debug( logger.debug(
f"Deleting {len(recordings_to_delete)} recordings with missing files" f"Deleting {len(recordings_to_delete)} recordings with missing files"
) )
Recordings.delete().where(Recordings.id << recordings_to_delete).execute() # delete up to 100,000 at a time
max_deletes = 100000
for i in range(0, len(recordings_to_delete), max_deletes):
Recordings.delete().where(
Recordings.id << recordings_to_delete[i : i + max_deletes]
).execute()
logger.debug("End sync recordings.") logger.debug("End sync recordings.")

View File

@ -5,24 +5,49 @@ import time
import psutil import psutil
import shutil import shutil
import os import os
import requests
from typing import Optional, Any
from paho.mqtt.client import Client
from multiprocessing.synchronize import Event
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
from frigate.types import StatsTrackingTypes, CameraMetricsTypes
from frigate.version import VERSION from frigate.version import VERSION
from frigate.edgetpu import EdgeTPUProcess
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def stats_init(camera_metrics, detectors): def get_latest_version() -> str:
stats_tracking = { try:
request = requests.get(
"https://api.github.com/repos/blakeblackshear/frigate/releases/latest"
)
except:
return "unknown"
response = request.json()
if request.ok and response and "tag_name" in response:
return str(response.get("tag_name").replace("v", ""))
else:
return "unknown"
def stats_init(
camera_metrics: dict[str, CameraMetricsTypes], detectors: dict[str, EdgeTPUProcess]
) -> StatsTrackingTypes:
stats_tracking: StatsTrackingTypes = {
"camera_metrics": camera_metrics, "camera_metrics": camera_metrics,
"detectors": detectors, "detectors": detectors,
"started": int(time.time()), "started": int(time.time()),
"latest_frigate_version": get_latest_version(),
} }
return stats_tracking return stats_tracking
def get_fs_type(path): def get_fs_type(path: str) -> str:
bestMatch = "" bestMatch = ""
fsType = "" fsType = ""
for part in psutil.disk_partitions(all=True): for part in psutil.disk_partitions(all=True):
@ -32,7 +57,7 @@ def get_fs_type(path):
return fsType return fsType
def read_temperature(path): def read_temperature(path: str) -> Optional[float]:
if os.path.isfile(path): if os.path.isfile(path):
with open(path) as f: with open(path) as f:
line = f.readline().strip() line = f.readline().strip()
@ -40,7 +65,7 @@ def read_temperature(path):
return None return None
def get_temperatures(): def get_temperatures() -> dict[str, float]:
temps = {} temps = {}
# Get temperatures for all attached Corals # Get temperatures for all attached Corals
@ -54,35 +79,43 @@ def get_temperatures():
return temps return temps
def stats_snapshot(stats_tracking): def stats_snapshot(stats_tracking: StatsTrackingTypes) -> dict[str, Any]:
camera_metrics = stats_tracking["camera_metrics"] camera_metrics = stats_tracking["camera_metrics"]
stats = {} stats: dict[str, Any] = {}
total_detection_fps = 0 total_detection_fps = 0
for name, camera_stats in camera_metrics.items(): for name, camera_stats in camera_metrics.items():
total_detection_fps += camera_stats["detection_fps"].value total_detection_fps += camera_stats["detection_fps"].value
pid = camera_stats["process"].pid if camera_stats["process"] else None
cpid = (
camera_stats["capture_process"].pid
if camera_stats["capture_process"]
else None
)
stats[name] = { stats[name] = {
"camera_fps": round(camera_stats["camera_fps"].value, 2), "camera_fps": round(camera_stats["camera_fps"].value, 2),
"process_fps": round(camera_stats["process_fps"].value, 2), "process_fps": round(camera_stats["process_fps"].value, 2),
"skipped_fps": round(camera_stats["skipped_fps"].value, 2), "skipped_fps": round(camera_stats["skipped_fps"].value, 2),
"detection_fps": round(camera_stats["detection_fps"].value, 2), "detection_fps": round(camera_stats["detection_fps"].value, 2),
"pid": camera_stats["process"].pid, "pid": pid,
"capture_pid": camera_stats["capture_process"].pid, "capture_pid": cpid,
} }
stats["detectors"] = {} stats["detectors"] = {}
for name, detector in stats_tracking["detectors"].items(): for name, detector in stats_tracking["detectors"].items():
pid = detector.detect_process.pid if detector.detect_process else None
stats["detectors"][name] = { stats["detectors"][name] = {
"inference_speed": round(detector.avg_inference_speed.value * 1000, 2), "inference_speed": round(detector.avg_inference_speed.value * 1000, 2),
"detection_start": detector.detection_start.value, "detection_start": detector.detection_start.value,
"pid": detector.detect_process.pid, "pid": pid,
} }
stats["detection_fps"] = round(total_detection_fps, 2) stats["detection_fps"] = round(total_detection_fps, 2)
stats["service"] = { stats["service"] = {
"uptime": (int(time.time()) - stats_tracking["started"]), "uptime": (int(time.time()) - stats_tracking["started"]),
"version": VERSION, "version": VERSION,
"latest_version": stats_tracking["latest_frigate_version"],
"storage": {}, "storage": {},
"temperatures": get_temperatures(), "temperatures": get_temperatures(),
} }
@ -103,10 +136,10 @@ class StatsEmitter(threading.Thread):
def __init__( def __init__(
self, self,
config: FrigateConfig, config: FrigateConfig,
stats_tracking, stats_tracking: StatsTrackingTypes,
mqtt_client, mqtt_client: Client,
topic_prefix, topic_prefix: str,
stop_event, stop_event: Event,
): ):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.name = "frigate_stats_emitter" self.name = "frigate_stats_emitter"
@ -116,7 +149,7 @@ class StatsEmitter(threading.Thread):
self.topic_prefix = topic_prefix self.topic_prefix = topic_prefix
self.stop_event = stop_event self.stop_event = stop_event
def run(self): def run(self) -> None:
time.sleep(10) time.sleep(10)
while not self.stop_event.wait(self.config.mqtt.stats_interval): while not self.stop_event.wait(self.config.mqtt.stats_interval):
stats = stats_snapshot(self.stats_tracking) stats = stats_snapshot(self.stats_tracking)

4
frigate/test/const.py Normal file
View File

@ -0,0 +1,4 @@
"""Consts for testing."""
TEST_DB = "test.db"
TEST_DB_CLEANUPS = ["test.db", "test.db-shm", "test.db-wal"]

View File

@ -2,6 +2,7 @@ import unittest
import numpy as np import numpy as np
from pydantic import ValidationError from pydantic import ValidationError
from frigate.config import ( from frigate.config import (
BirdseyeModeEnum,
FrigateConfig, FrigateConfig,
DetectorTypeEnum, DetectorTypeEnum,
) )
@ -80,6 +81,86 @@ class TestConfig(unittest.TestCase):
runtime_config = frigate_config.runtime_config runtime_config = frigate_config.runtime_config
assert "dog" in runtime_config.cameras["back"].objects.track assert "dog" in runtime_config.cameras["back"].objects.track
def test_override_birdseye(self):
config = {
"mqtt": {"host": "mqtt"},
"birdseye": {"enabled": True, "mode": "continuous"},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
"birdseye": {"enabled": False, "mode": "motion"},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert not runtime_config.cameras["back"].birdseye.enabled
assert runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.motion
def test_override_birdseye_non_inheritable(self):
config = {
"mqtt": {"host": "mqtt"},
"birdseye": {"enabled": True, "mode": "continuous", "height": 1920},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].birdseye.enabled
def test_inherit_birdseye(self):
config = {
"mqtt": {"host": "mqtt"},
"birdseye": {"enabled": True, "mode": "continuous"},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].birdseye.enabled
assert (
runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.continuous
)
def test_override_tracked_objects(self): def test_override_tracked_objects(self):
config = { config = {
"mqtt": {"host": "mqtt"}, "mqtt": {"host": "mqtt"},
@ -1268,6 +1349,36 @@ class TestConfig(unittest.TestCase):
ValidationError, lambda: frigate_config.runtime_config.cameras ValidationError, lambda: frigate_config.runtime_config.cameras
) )
def test_object_filter_ratios_work(self):
config = {
"mqtt": {"host": "mqtt"},
"objects": {
"track": ["person", "dog"],
"filters": {"dog": {"min_ratio": 0.2, "max_ratio": 10.1}},
},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert "dog" in runtime_config.cameras["back"].objects.filters
assert runtime_config.cameras["back"].objects.filters["dog"].min_ratio == 0.2
assert runtime_config.cameras["back"].objects.filters["dog"].max_ratio == 10.1
if __name__ == "__main__": if __name__ == "__main__":
unittest.main(verbosity=2) unittest.main(verbosity=2)

328
frigate/test/test_http.py Normal file
View File

@ -0,0 +1,328 @@
import datetime
import json
import logging
import os
import unittest
from unittest.mock import patch
from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase
from playhouse.shortcuts import model_to_dict
from frigate.config import FrigateConfig
from frigate.http import create_app
from frigate.models import Event, Recordings
from frigate.plus import PlusApi
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
class TestHttp(unittest.TestCase):
def setUp(self):
# setup clean database for each test run
migrate_db = SqliteExtDatabase("test.db")
del logging.getLogger("peewee_migrate").handlers[:]
router = Router(migrate_db)
router.run()
migrate_db.close()
self.db = SqliteQueueDatabase(TEST_DB)
models = [Event, Recordings]
self.db.bind(models)
self.minimal_config = {
"mqtt": {"host": "mqtt"},
"cameras": {
"front_door": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
}
},
}
self.test_stats = {
"detection_fps": 13.7,
"detectors": {
"cpu1": {
"detection_start": 0.0,
"inference_speed": 91.43,
"pid": 42,
},
"cpu2": {
"detection_start": 0.0,
"inference_speed": 84.99,
"pid": 44,
},
},
"front_door": {
"camera_fps": 0.0,
"capture_pid": 53,
"detection_fps": 0.0,
"pid": 52,
"process_fps": 0.0,
"skipped_fps": 0.0,
},
"service": {
"storage": {
"/dev/shm": {
"free": 50.5,
"mount_type": "tmpfs",
"total": 67.1,
"used": 16.6,
},
"/media/frigate/clips": {
"free": 42429.9,
"mount_type": "ext4",
"total": 244529.7,
"used": 189607.0,
},
"/media/frigate/recordings": {
"free": 0.2,
"mount_type": "ext4",
"total": 8.0,
"used": 7.8,
},
"/tmp/cache": {
"free": 976.8,
"mount_type": "tmpfs",
"total": 1000.0,
"used": 23.2,
},
},
"uptime": 101113,
"version": "0.10.1",
"latest_version": "0.11",
},
}
def tearDown(self):
if not self.db.is_closed():
self.db.close()
try:
for file in TEST_DB_CLEANUPS:
os.remove(file)
except OSError:
pass
def test_get_event_list(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
id2 = "7890.random"
with app.test_client() as client:
_insert_mock_event(id)
events = client.get(f"/events").json
assert events
assert len(events) == 1
assert events[0]["id"] == id
_insert_mock_event(id2)
events = client.get(f"/events").json
assert events
assert len(events) == 2
events = client.get(
f"/events",
query_string={"limit": 1},
).json
assert events
assert len(events) == 1
events = client.get(
f"/events",
query_string={"has_clip": 0},
).json
assert not events
def test_get_good_event(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
with app.test_client() as client:
_insert_mock_event(id)
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event == model_to_dict(Event.get(Event.id == id))
def test_get_bad_event(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
bad_id = "654321.other"
with app.test_client() as client:
_insert_mock_event(id)
event = client.get(f"/events/{bad_id}").json
assert not event
def test_delete_event(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
with app.test_client() as client:
_insert_mock_event(id)
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
client.delete(f"/events/{id}")
event = client.get(f"/events/{id}").json
assert not event
def test_event_retention(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
with app.test_client() as client:
_insert_mock_event(id)
client.post(f"/events/{id}/retain")
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event["retain_indefinitely"] == True
client.delete(f"/events/{id}/retain")
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event["retain_indefinitely"] == False
def test_set_delete_sub_label(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
sub_label = "sub"
with app.test_client() as client:
_insert_mock_event(id)
client.post(
f"/events/{id}/sub_label",
data=json.dumps({"subLabel": sub_label}),
content_type="application/json",
)
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event["sub_label"] == sub_label
client.post(
f"/events/{id}/sub_label",
data=json.dumps({"subLabel": ""}),
content_type="application/json",
)
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event["sub_label"] == ""
def test_sub_label_list(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
sub_label = "sub"
with app.test_client() as client:
_insert_mock_event(id)
client.post(
f"/events/{id}/sub_label",
data=json.dumps({"subLabel": sub_label}),
content_type="application/json",
)
sub_labels = client.get("/sub_labels").json
assert sub_labels
assert sub_labels == [sub_label]
def test_config(self):
app = create_app(
FrigateConfig(**self.minimal_config).runtime_config,
self.db,
None,
None,
PlusApi(),
)
with app.test_client() as client:
config = client.get("/config").json
assert config
assert config["cameras"]["front_door"]
def test_recordings(self):
app = create_app(
FrigateConfig(**self.minimal_config).runtime_config,
self.db,
None,
None,
PlusApi(),
)
id = "123456.random"
with app.test_client() as client:
_insert_mock_recording(id)
recording = client.get("/front_door/recordings").json
assert recording
assert recording[0]["id"] == id
@patch("frigate.http.stats_snapshot")
def test_stats(self, mock_stats):
app = create_app(
FrigateConfig(**self.minimal_config).runtime_config,
self.db,
None,
None,
PlusApi(),
)
mock_stats.return_value = self.test_stats
with app.test_client() as client:
stats = client.get("/stats").json
assert stats == self.test_stats
def _insert_mock_event(id: str) -> Event:
"""Inserts a basic event model with a given id."""
return Event.insert(
id=id,
label="Mock",
camera="front_door",
start_time=datetime.datetime.now().timestamp(),
end_time=datetime.datetime.now().timestamp() + 20,
top_score=100,
false_positive=False,
zones=list(),
thumbnail="",
region=[],
box=[],
area=0,
has_clip=True,
has_snapshot=True,
).execute()
def _insert_mock_recording(id: str) -> Event:
"""Inserts a basic recording model with a given id."""
return Recordings.insert(
id=id,
camera="front_door",
path=f"/recordings/{id}",
start_time=datetime.datetime.now().timestamp() - 50,
end_time=datetime.datetime.now().timestamp() - 60,
duration=10,
motion=True,
objects=True,
).execute()

31
frigate/types.py Normal file
View File

@ -0,0 +1,31 @@
from typing import Optional, TypedDict
from multiprocessing.queues import Queue
from multiprocessing.sharedctypes import Synchronized
from multiprocessing.context import Process
from frigate.edgetpu import EdgeTPUProcess
class CameraMetricsTypes(TypedDict):
camera_fps: Synchronized
capture_process: Optional[Process]
detection_enabled: Synchronized
detection_fps: Synchronized
detection_frame: Synchronized
ffmpeg_pid: Synchronized
frame_queue: Queue
motion_enabled: Synchronized
improve_contrast_enabled: Synchronized
motion_threshold: Synchronized
motion_contour_area: Synchronized
process: Optional[Process]
process_fps: Synchronized
read_start: Synchronized
skipped_fps: Synchronized
class StatsTrackingTypes(TypedDict):
camera_metrics: dict[str, CameraMetricsTypes]
detectors: dict[str, EdgeTPUProcess]
started: int
latest_frigate_version: str

View File

@ -1,4 +1,3 @@
import collections
import copy import copy
import datetime import datetime
import hashlib import hashlib
@ -11,6 +10,7 @@ import threading
import time import time
import traceback import traceback
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Mapping
from multiprocessing import shared_memory from multiprocessing import shared_memory
from typing import AnyStr from typing import AnyStr
@ -34,7 +34,7 @@ def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dic
for k, v2 in dct2.items(): for k, v2 in dct2.items():
if k in merged: if k in merged:
v1 = merged[k] v1 = merged[k]
if isinstance(v1, dict) and isinstance(v2, collections.Mapping): if isinstance(v1, dict) and isinstance(v2, Mapping):
merged[k] = deep_merge(v1, v2, override) merged[k] = deep_merge(v1, v2, override)
elif isinstance(v1, list) and isinstance(v2, list): elif isinstance(v1, list) and isinstance(v2, list):
if merge_lists: if merge_lists:
@ -522,7 +522,7 @@ def clipped(obj, frame_shape):
# if the object is within 5 pixels of the region border, and the region is not on the edge # if the object is within 5 pixels of the region border, and the region is not on the edge
# consider the object to be clipped # consider the object to be clipped
box = obj[2] box = obj[2]
region = obj[4] region = obj[5]
if ( if (
(region[0] > 5 and box[0] - region[0] <= 5) (region[0] > 5 and box[0] - region[0] <= 5)
or (region[1] > 5 and box[1] - region[1] <= 5) or (region[1] > 5 and box[1] - region[1] <= 5)

View File

@ -9,7 +9,6 @@ import subprocess as sp
import threading import threading
import time import time
from collections import defaultdict from collections import defaultdict
from typing import Dict, List
import numpy as np import numpy as np
from cv2 import cv2, reduce from cv2 import cv2, reduce
@ -38,6 +37,10 @@ logger = logging.getLogger(__name__)
def filtered(obj, objects_to_track, object_filters): def filtered(obj, objects_to_track, object_filters):
object_name = obj[0] object_name = obj[0]
object_score = obj[1]
object_box = obj[2]
object_area = obj[3]
object_ratio = obj[4]
if not object_name in objects_to_track: if not object_name in objects_to_track:
return True return True
@ -47,24 +50,35 @@ def filtered(obj, objects_to_track, object_filters):
# if the min area is larger than the # if the min area is larger than the
# detected object, don't add it to detected objects # detected object, don't add it to detected objects
if obj_settings.min_area > obj[3]: if obj_settings.min_area > object_area:
return True return True
# if the detected object is larger than the # if the detected object is larger than the
# max area, don't add it to detected objects # max area, don't add it to detected objects
if obj_settings.max_area < obj[3]: if obj_settings.max_area < object_area:
return True return True
# if the score is lower than the min_score, skip # if the score is lower than the min_score, skip
if obj_settings.min_score > obj[1]: if obj_settings.min_score > object_score:
return True
# if the object is not proportionally wide enough
if obj_settings.min_ratio > object_ratio:
return True
# if the object is proportionally too wide
if obj_settings.max_ratio < object_ratio:
return True return True
if not obj_settings.mask is None: if not obj_settings.mask is None:
# compute the coordinates of the object and make sure # compute the coordinates of the object and make sure
# the location isnt outside the bounds of the image (can happen from rounding) # the location isn't outside the bounds of the image (can happen from rounding)
y_location = min(int(obj[2][3]), len(obj_settings.mask) - 1) object_xmin = object_box[0]
object_xmax = object_box[2]
object_ymax = object_box[3]
y_location = min(int(object_ymax), len(obj_settings.mask) - 1)
x_location = min( x_location = min(
int((obj[2][2] - obj[2][0]) / 2.0) + obj[2][0], int((object_xmax + object_xmin) / 2.0),
len(obj_settings.mask[0]) - 1, len(obj_settings.mask[0]) - 1,
) )
@ -188,7 +202,7 @@ class CameraWatchdog(threading.Thread):
self.config = config self.config = config
self.capture_thread = None self.capture_thread = None
self.ffmpeg_detect_process = None self.ffmpeg_detect_process = None
self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect", logging.ERROR) self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect")
self.ffmpeg_other_processes = [] self.ffmpeg_other_processes = []
self.camera_fps = camera_fps self.camera_fps = camera_fps
self.ffmpeg_pid = ffmpeg_pid self.ffmpeg_pid = ffmpeg_pid
@ -204,8 +218,7 @@ class CameraWatchdog(threading.Thread):
if "detect" in c["roles"]: if "detect" in c["roles"]:
continue continue
logpipe = LogPipe( logpipe = LogPipe(
f"ffmpeg.{self.camera_name}.{'_'.join(sorted(c['roles']))}", f"ffmpeg.{self.camera_name}.{'_'.join(sorted(c['roles']))}"
logging.ERROR,
) )
self.ffmpeg_other_processes.append( self.ffmpeg_other_processes.append(
{ {
@ -348,12 +361,22 @@ def track_camera(
frame_queue = process_info["frame_queue"] frame_queue = process_info["frame_queue"]
detection_enabled = process_info["detection_enabled"] detection_enabled = process_info["detection_enabled"]
motion_enabled = process_info["motion_enabled"]
improve_contrast_enabled = process_info["improve_contrast_enabled"]
motion_threshold = process_info["motion_threshold"]
motion_contour_area = process_info["motion_contour_area"]
frame_shape = config.frame_shape frame_shape = config.frame_shape
objects_to_track = config.objects.track objects_to_track = config.objects.track
object_filters = config.objects.filters object_filters = config.objects.filters
motion_detector = MotionDetector(frame_shape, config.motion) motion_detector = MotionDetector(
frame_shape,
config.motion,
improve_contrast_enabled,
motion_threshold,
motion_contour_area,
)
object_detector = RemoteObjectDetector( object_detector = RemoteObjectDetector(
name, labelmap, detection_queue, result_connection, model_shape name, labelmap, detection_queue, result_connection, model_shape
) )
@ -377,6 +400,7 @@ def track_camera(
objects_to_track, objects_to_track,
object_filters, object_filters,
detection_enabled, detection_enabled,
motion_enabled,
stop_event, stop_event,
) )
@ -416,7 +440,13 @@ def intersects_any(box_a, boxes):
def detect( def detect(
object_detector, frame, model_shape, region, objects_to_track, object_filters detect_config: DetectConfig,
object_detector,
frame,
model_shape,
region,
objects_to_track,
object_filters,
): ):
tensor_input = create_tensor_input(frame, model_shape, region) tensor_input = create_tensor_input(frame, model_shape, region)
@ -425,15 +455,25 @@ def detect(
for d in region_detections: for d in region_detections:
box = d[2] box = d[2]
size = region[2] - region[0] size = region[2] - region[0]
x_min = int((box[1] * size) + region[0]) x_min = int(max(0, (box[1] * size) + region[0]))
y_min = int((box[0] * size) + region[1]) y_min = int(max(0, (box[0] * size) + region[1]))
x_max = int((box[3] * size) + region[0]) x_max = int(min(detect_config.width - 1, (box[3] * size) + region[0]))
y_max = int((box[2] * size) + region[1]) y_max = int(min(detect_config.height - 1, (box[2] * size) + region[1]))
# ignore objects that were detected outside the frame
if (x_min >= detect_config.width - 1) or (y_min >= detect_config.height - 1):
continue
width = x_max - x_min
height = y_max - y_min
area = width * height
ratio = width / height
det = ( det = (
d[0], d[0],
d[1], d[1],
(x_min, y_min, x_max, y_max), (x_min, y_min, x_max, y_max),
(x_max - x_min) * (y_max - y_min), area,
ratio,
region, region,
) )
# apply object filters # apply object filters
@ -454,10 +494,11 @@ def process_frames(
object_detector: RemoteObjectDetector, object_detector: RemoteObjectDetector,
object_tracker: ObjectTracker, object_tracker: ObjectTracker,
detected_objects_queue: mp.Queue, detected_objects_queue: mp.Queue,
process_info: Dict, process_info: dict,
objects_to_track: List[str], objects_to_track: list[str],
object_filters, object_filters,
detection_enabled: mp.Value, detection_enabled: mp.Value,
motion_enabled: mp.Value,
stop_event, stop_event,
exit_on_empty: bool = False, exit_on_empty: bool = False,
): ):
@ -491,8 +532,8 @@ def process_frames(
logger.info(f"{camera_name}: frame {frame_time} is not in memory store.") logger.info(f"{camera_name}: frame {frame_time} is not in memory store.")
continue continue
# look for motion # look for motion if enabled
motion_boxes = motion_detector.detect(frame) motion_boxes = motion_detector.detect(frame) if motion_enabled.value else []
regions = [] regions = []
@ -580,6 +621,7 @@ def process_frames(
obj["score"], obj["score"],
obj["box"], obj["box"],
obj["area"], obj["area"],
obj["ratio"],
obj["region"], obj["region"],
) )
for obj in object_tracker.tracked_objects.values() for obj in object_tracker.tracked_objects.values()
@ -589,6 +631,7 @@ def process_frames(
for region in regions: for region in regions:
detections.extend( detections.extend(
detect( detect(
detect_config,
object_detector, object_detector,
frame, frame,
model_shape, model_shape,
@ -615,15 +658,23 @@ def process_frames(
for group in detected_object_groups.values(): for group in detected_object_groups.values():
# apply non-maxima suppression to suppress weak, overlapping bounding boxes # apply non-maxima suppression to suppress weak, overlapping bounding boxes
# o[2] is the box of the object: xmin, ymin, xmax, ymax
# apply max/min to ensure values do not exceed the known frame size
boxes = [ boxes = [
(o[2][0], o[2][1], o[2][2] - o[2][0], o[2][3] - o[2][1]) (
o[2][0],
o[2][1],
o[2][2] - o[2][0],
o[2][3] - o[2][1],
)
for o in group for o in group
] ]
confidences = [o[1] for o in group] confidences = [o[1] for o in group]
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4) idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
for index in idxs: for index in idxs:
obj = group[index[0]] index = index if isinstance(index, np.int32) else index[0]
obj = group[index]
if clipped(obj, frame_shape): if clipped(obj, frame_shape):
box = obj[2] box = obj[2]
# calculate a new region that will hopefully get the entire object # calculate a new region that will hopefully get the entire object
@ -640,6 +691,7 @@ def process_frames(
selected_objects.extend( selected_objects.extend(
detect( detect(
detect_config,
object_detector, object_detector,
frame, frame,
model_shape, model_shape,

View File

@ -5,21 +5,21 @@ import time
import os import os
import signal import signal
from frigate.util import ( from frigate.edgetpu import EdgeTPUProcess
restart_frigate, from frigate.util import restart_frigate
) from multiprocessing.synchronize import Event
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FrigateWatchdog(threading.Thread): class FrigateWatchdog(threading.Thread):
def __init__(self, detectors, stop_event): def __init__(self, detectors: dict[str, EdgeTPUProcess], stop_event: Event):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.name = "frigate_watchdog" self.name = "frigate_watchdog"
self.detectors = detectors self.detectors = detectors
self.stop_event = stop_event self.stop_event = stop_event
def run(self): def run(self) -> None:
time.sleep(10) time.sleep(10)
while not self.stop_event.wait(10): while not self.stop_event.wait(10):
now = datetime.datetime.now().timestamp() now = datetime.datetime.now().timestamp()
@ -32,7 +32,10 @@ class FrigateWatchdog(threading.Thread):
"Detection appears to be stuck. Restarting detection process..." "Detection appears to be stuck. Restarting detection process..."
) )
detector.start_or_restart() detector.start_or_restart()
elif not detector.detect_process.is_alive(): elif (
detector.detect_process is not None
and not detector.detect_process.is_alive()
):
logger.info("Detection appears to have stopped. Exiting frigate...") logger.info("Detection appears to have stopped. Exiting frigate...")
restart_frigate() restart_frigate()

View File

@ -14,38 +14,41 @@ logger = logging.getLogger(__name__)
ZEROCONF_TYPE = "_frigate._tcp.local." ZEROCONF_TYPE = "_frigate._tcp.local."
# Taken from: http://stackoverflow.com/a/11735897 # Taken from: http://stackoverflow.com/a/11735897
def get_local_ip() -> str: def get_local_ip() -> bytes:
"""Try to determine the local IP address of the machine.""" """Try to determine the local IP address of the machine."""
host_ip_str = ""
try: try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Use Google Public DNS server to determine own IP # Use Google Public DNS server to determine own IP
sock.connect(("8.8.8.8", 80)) sock.connect(("8.8.8.8", 80))
return sock.getsockname()[0] # type: ignore host_ip_str = sock.getsockname()[0]
except OSError: except OSError:
try: try:
return socket.gethostbyname(socket.gethostname()) host_ip_str = socket.gethostbyname(socket.gethostname())
except socket.gaierror: except socket.gaierror:
return "127.0.0.1" host_ip_str = "127.0.0.1"
finally: finally:
sock.close() sock.close()
try:
host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip_str)
except OSError:
host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip_str)
def broadcast_zeroconf(frigate_id): return host_ip_pton
def broadcast_zeroconf(frigate_id: str) -> Zeroconf:
zeroconf = Zeroconf(interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only) zeroconf = Zeroconf(interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only)
host_ip = get_local_ip() host_ip = get_local_ip()
try:
host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip)
except OSError:
host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip)
info = ServiceInfo( info = ServiceInfo(
ZEROCONF_TYPE, ZEROCONF_TYPE,
name=f"{frigate_id}.{ZEROCONF_TYPE}", name=f"{frigate_id}.{ZEROCONF_TYPE}",
addresses=[host_ip_pton], addresses=[host_ip],
port=5000, port=5000,
) )

View File

@ -0,0 +1,46 @@
"""Peewee migrations -- 007_add_retain_indefinitely.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import datetime as dt
import peewee as pw
from playhouse.sqlite_ext import *
from decimal import ROUND_HALF_EVEN
from frigate.models import Event
try:
import playhouse.postgres_ext as pw_pext
except ImportError:
pass
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.add_fields(
Event,
retain_indefinitely=pw.BooleanField(default=False),
)
def rollback(migrator, database, fake=False, **kwargs):
migrator.remove_fields(Event, ["retain_indefinitely"])

View File

@ -0,0 +1,46 @@
"""Peewee migrations -- 008_add_sub_label.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import datetime as dt
import peewee as pw
from playhouse.sqlite_ext import *
from decimal import ROUND_HALF_EVEN
from frigate.models import Event
try:
import playhouse.postgres_ext as pw_pext
except ImportError:
pass
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.add_fields(
Event,
sub_label=pw.CharField(max_length=20, null=True),
)
def rollback(migrator, database, fake=False, **kwargs):
migrator.remove_fields(Event, ["sub_label"])

View File

@ -0,0 +1,38 @@
"""Peewee migrations -- 009_add_object_filter_ratio.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import peewee as pw
from frigate.models import Event
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.add_fields(
Event,
ratio=pw.FloatField(default=1.0), # Assume that existing detections are square
)
def rollback(migrator, database, fake=False, **kwargs):
migrator.remove_fields(Event, ["ratio"])

View File

@ -0,0 +1,46 @@
"""Peewee migrations -- 010_add_plus_image_id.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import datetime as dt
import peewee as pw
from playhouse.sqlite_ext import *
from decimal import ROUND_HALF_EVEN
from frigate.models import Event
try:
import playhouse.postgres_ext as pw_pext
except ImportError:
pass
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.add_fields(
Event,
plus_id=pw.CharField(max_length=30, null=True),
)
def rollback(migrator, database, fake=False, **kwargs):
migrator.remove_fields(Event, ["plus_id"])

View File

@ -0,0 +1,39 @@
"""Peewee migrations -- 011_update_indexes.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import peewee as pw
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.sql(
'CREATE INDEX "event_start_time_end_time" ON "event" ("start_time" DESC, "end_time" DESC)'
)
migrator.sql("DROP INDEX recordings_start_time_end_time")
migrator.sql(
'CREATE INDEX "recordings_end_time_start_time" ON "recordings" ("end_time" DESC, "start_time" DESC)'
)
def rollback(migrator, database, fake=False, **kwargs):
pass

View File

@ -115,6 +115,7 @@ class ProcessClip:
} }
detection_enabled = mp.Value("d", 1) detection_enabled = mp.Value("d", 1)
motion_enabled = mp.Value("d", True)
stop_event = mp.Event() stop_event = mp.Event()
model_shape = (self.config.model.height, self.config.model.width) model_shape = (self.config.model.height, self.config.model.width)
@ -133,6 +134,7 @@ class ProcessClip:
objects_to_track, objects_to_track,
object_filters, object_filters,
detection_enabled, detection_enabled,
motion_enabled,
stop_event, stop_event,
exit_on_empty=True, exit_on_empty=True,
) )

2
requirements-dev.txt Normal file
View File

@ -0,0 +1,2 @@
pylint == 2.13.*
black == 22.3.*

20
requirements-wheels.txt Normal file
View File

@ -0,0 +1,20 @@
click == 8.1.*
Flask == 2.1.*
imutils == 0.5.*
matplotlib == 3.5.*
mypy == 0.942
numpy == 1.22.*
opencv-python-headless == 4.5.5.*
paho-mqtt == 1.6.*
peewee == 3.14.*
peewee_migrate == 1.4.*
psutil == 5.9.*
pydantic == 1.9.*
PyYAML == 6.0.*
types-PyYAML == 6.0.*
requests == 2.27.*
types-requests == 2.27.*
scipy == 1.8.*
setproctitle == 1.2.*
ws4py == 0.5.*
zeroconf == 0.38.4

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
scikit-build == 0.14.1

View File

@ -1 +0,0 @@
node_modules

View File

@ -1,2 +1,2 @@
build/* dist/*
node_modules/* node_modules/*

32
web/.eslintrc Normal file
View File

@ -0,0 +1,32 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended", "preact", "prettier"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {
"indent": ["error", 2, { "SwitchCase": 1 }],
"comma-dangle": [
"error",
{ "objects": "always-multiline", "arrays": "always-multiline", "imports": "always-multiline" }
],
"no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
"no-console": "error"
},
"overrides": [
{
"files": ["**/*.{ts,tsx}"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"]
}
]
}

View File

@ -1,140 +0,0 @@
module.exports = {
parser: '@babel/eslint-parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
},
extends: [
'prettier',
'preact',
'plugin:import/react',
'plugin:testing-library/recommended',
'plugin:jest/recommended',
],
plugins: ['import', 'testing-library', 'jest'],
env: {
es6: true,
node: true,
browser: true,
},
rules: {
'constructor-super': 'error',
'default-case': ['error', { commentPattern: '^no default$' }],
'handle-callback-err': ['error', '^(err|error)$'],
'new-cap': ['error', { newIsCap: true, capIsNew: false }],
'no-alert': 'error',
'no-array-constructor': 'error',
'no-caller': 'error',
'no-case-declarations': 'error',
'no-class-assign': 'error',
'no-cond-assign': 'error',
'no-console': 'error',
'no-const-assign': 'error',
'no-control-regex': 'error',
'no-debugger': 'error',
'no-delete-var': 'error',
'no-dupe-args': 'error',
'no-dupe-class-members': 'error',
'no-dupe-keys': 'error',
'no-duplicate-case': 'error',
'no-duplicate-imports': 'error',
'no-empty-character-class': 'error',
'no-empty-pattern': 'error',
'no-eval': 'error',
'no-ex-assign': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'error',
'no-extra-boolean-cast': 'error',
'no-fallthrough': 'error',
'no-floating-decimal': 'error',
'no-func-assign': 'error',
'no-implied-eval': 'error',
'no-inner-declarations': ['error', 'functions'],
'no-invalid-regexp': 'error',
'no-irregular-whitespace': 'error',
'no-iterator': 'error',
'no-label-var': 'error',
'no-labels': ['error', { allowLoop: false, allowSwitch: false }],
'no-lone-blocks': 'error',
'no-loop-func': 'error',
'no-multi-str': 'error',
'no-native-reassign': 'error',
'no-negated-in-lhs': 'error',
'no-new': 'error',
'no-new-func': 'error',
'no-new-object': 'error',
'no-new-require': 'error',
'no-new-symbol': 'error',
'no-new-wrappers': 'error',
'no-obj-calls': 'error',
'no-octal': 'error',
'no-octal-escape': 'error',
'no-path-concat': 'error',
'no-proto': 'error',
'no-redeclare': 'error',
'no-regex-spaces': 'error',
'no-return-assign': ['error', 'except-parens'],
'no-script-url': 'error',
'no-self-assign': 'error',
'no-self-compare': 'error',
'no-sequences': 'error',
'no-shadow-restricted-names': 'error',
'no-sparse-arrays': 'error',
'no-this-before-super': 'error',
'no-throw-literal': 'error',
'no-trailing-spaces': 'error',
'no-undef': 'error',
'no-undef-init': 'error',
'no-unexpected-multiline': 'error',
'no-unmodified-loop-condition': 'error',
'no-unneeded-ternary': ['error', { defaultAssignment: false }],
'no-unreachable': 'error',
'no-unsafe-finally': 'error',
'no-unused-vars': ['error', { vars: 'all', args: 'none', ignoreRestSiblings: true }],
'no-useless-call': 'error',
'no-useless-computed-key': 'error',
'no-useless-concat': 'error',
'no-useless-constructor': 'error',
'no-useless-escape': 'error',
'no-var': 'error',
'no-with': 'error',
'prefer-const': 'error',
'prefer-rest-params': 'error',
'use-isnan': 'error',
'valid-typeof': 'error',
camelcase: 'off',
eqeqeq: ['error', 'allow-null'],
indent: ['error', 2, { SwitchCase: 1 }],
quotes: ['error', 'single', 'avoid-escape'],
radix: 'error',
yoda: ['error', 'never'],
'import/no-unresolved': 'error',
// 'react-hooks/exhaustive-deps': 'error',
'jest/consistent-test-it': ['error', { fn: 'test' }],
'jest/no-test-prefixes': 'error',
'jest/no-restricted-matchers': [
'error',
{ toMatchSnapshot: 'Use `toMatchInlineSnapshot()` and ensure you only snapshot very small elements' },
],
'jest/valid-describe': 'error',
'jest/valid-expect-in-promise': 'error',
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx'],
},
},
},
};

24
web/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

4
web/.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"printWidth": 120,
"singleQuote": true
}

View File

@ -1,3 +0,0 @@
# Frigate Web UI
For installation and contributing instructions, please follow the [Contributing Docs](https://blakeblackshear.github.io/frigate/contributing).

View File

@ -1,4 +1,4 @@
module.exports = { module.exports = {
presets: ['@babel/preset-env'], presets: ['@babel/preset-env', ['@babel/typescript', { jsxPragma: 'h' }]],
plugins: [['@babel/plugin-transform-react-jsx', { pragma: 'h' }]], plugins: [['@babel/plugin-transform-react-jsx', { pragma: 'h' }]],
}; };

85
web/config/handlers.js Normal file
View File

@ -0,0 +1,85 @@
import { rest } from 'msw';
import { API_HOST } from '../src/env';
export const handlers = [
rest.get(`${API_HOST}api/config`, (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
mqtt: {
stats_interval: 60,
},
service: {
version: '0.8.3',
},
cameras: {
front: {
name: 'front',
objects: { track: ['taco', 'cat', 'dog'] },
record: { enabled: true },
detect: { width: 1280, height: 720 },
snapshots: {},
live: { height: 720 },
ui: { dashboard: true, order: 0 },
},
side: {
name: 'side',
objects: { track: ['taco', 'cat', 'dog'] },
record: { enabled: false },
detect: { width: 1280, height: 720 },
snapshots: {},
live: { height: 720 },
ui: { dashboard: true, order: 1 },
},
},
})
);
}),
rest.get(`${API_HOST}api/stats`, (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
detection_fps: 0.0,
detectors: { coral: { detection_start: 0.0, inference_speed: 8.94, pid: 52 } },
front: { camera_fps: 5.0, capture_pid: 64, detection_fps: 0.0, pid: 54, process_fps: 0.0, skipped_fps: 0.0 },
side: {
camera_fps: 6.9,
capture_pid: 71,
detection_fps: 0.0,
pid: 60,
process_fps: 0.0,
skipped_fps: 0.0,
},
service: { uptime: 34812, version: '0.8.1-d376f6b' },
})
);
}),
rest.get(`${API_HOST}api/events`, (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json(
new Array(12).fill(null).map((v, i) => ({
end_time: 1613257337 + i,
has_clip: true,
has_snapshot: true,
id: i,
label: 'person',
start_time: 1613257326 + i,
top_score: Math.random(),
zones: ['front_patio'],
thumbnail: '/9j/4aa...',
camera: 'camera_name',
}))
)
);
}),
rest.get(`${API_HOST}api/sub_labels`, (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
'one',
'two',
])
);
}),
];

6
web/config/server.js Normal file
View File

@ -0,0 +1,6 @@
// src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
// This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers);

View File

@ -1,5 +1,6 @@
import 'regenerator-runtime/runtime'; import 'regenerator-runtime/runtime';
import '@testing-library/jest-dom/extend-expect'; import '@testing-library/jest-dom/extend-expect';
import { server } from './server.js';
Object.defineProperty(window, 'matchMedia', { Object.defineProperty(window, 'matchMedia', {
writable: true, writable: true,
@ -13,6 +14,16 @@ Object.defineProperty(window, 'matchMedia', {
}), }),
}); });
window.fetch = () => Promise.resolve();
jest.mock('../src/env'); jest.mock('../src/env');
// Establish API mocking before all tests.
beforeAll(() => server.listen());
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => {
server.resetHandlers();
});
// Clean up after the tests are finished.
afterAll(() => server.close());

View File

@ -0,0 +1,24 @@
import { h } from 'preact';
import { render } from '@testing-library/preact';
import { ApiProvider } from '../src/api';
const Wrapper = ({ children }) => {
return (
<ApiProvider
options={{
dedupingInterval: 0,
provider: () => new Map(),
}}
>
{children}
</ApiProvider>
);
};
const customRender = (ui, options) => render(ui, { wrapper: Wrapper, ...options });
// re-export everything
export * from '@testing-library/preact';
// override render method
export { customRender as render };

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 558 B

After

Width:  |  Height:  |  Size: 558 B

View File

Before

Width:  |  Height:  |  Size: 800 B

After

Width:  |  Height:  |  Size: 800 B

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

BIN
web/images/marker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,25 +1,25 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/images/favicon.ico" />
<link rel="icon" href="/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Frigate</title> <title>Frigate</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3b82f7" /> <link rel="mask-icon" href="/images/safari-pinned-tab.svg" color="#3b82f7" />
<meta name="msapplication-TileColor" content="#3b82f7" /> <meta name="msapplication-TileColor" content="#3b82f7" />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" /> <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#111827" media="(prefers-color-scheme: dark)" /> <meta name="theme-color" content="#111827" media="(prefers-color-scheme: dark)" />
</head> </head>
<body> <body>
<div id="root" class="z-0"></div> <div id="app" class="z-0"></div>
<div id="dialogs" class="z-0"></div> <div id="dialogs" class="z-0"></div>
<div id="menus" class="z-0"></div> <div id="menus" class="z-0"></div>
<div id="tooltips" class="z-0"></div> <div id="tooltips" class="z-0"></div>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="/dist/index.js"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@ -1,12 +1,198 @@
/*
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
module.exports = { module.exports = {
moduleFileExtensions: ['js', 'jsx'], // All imported modules in your tests should be mocked automatically
name: 'react-component-benchmark', // automock: false,
resetMocks: true,
roots: ['<rootDir>'], // Stop running tests after `n` failures
setupFilesAfterEnv: ['<rootDir>/config/setupTests.js'], // bail: 0,
testEnvironment: 'jsdom',
timers: 'fake', // The directory where Jest should store its cached dependency information
// cacheDirectory: "/tmp/jest_rs",
// Automatically clear mock calls, instances and results before every test
// clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
// coverageDirectory: 'coverage',
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
// coverageProvider: "babel",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: { moduleNameMapper: {
'\\.(scss|sass|css)$': '<rootDir>/src/__mocks__/styleMock.js' '\\.(scss|sass|css)$': '<rootDir>/src/__mocks__/styleMock.js',
} '^testing-library$': '<rootDir>/config/testing-library',
'^react$': 'preact/compat',
'^react-dom$': 'preact/compat',
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
resetMocks: true,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
roots: ['<rootDir>'],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
setupFilesAfterEnv: ['<rootDir>/config/setupTests.js'],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: 'jsdom',
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: 'fake',
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
}; };

23687
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +1,51 @@
{ {
"name": "frigate", "name": "frigate",
"private": true, "private": true,
"version": "0.0.0",
"scripts": { "scripts": {
"start": "cross-env SNOWPACK_PUBLIC_API_HOST=http://localhost:5000 snowpack dev", "dev": "vite --host",
"start:custom": "snowpack dev", "lint": "eslint ./ --ext .jsx,.js,.tsx,.ts",
"prebuild": "rimraf build", "build": "tsc && vite build --base=/BASE_PATH/",
"build": "cross-env NODE_ENV=production SNOWPACK_MODE=production SNOWPACK_PUBLIC_API_HOST='' snowpack build", "preview": "vite preview",
"lint": "npm run lint:cmd -- --fix",
"lint:cmd": "eslint ./ --ext .jsx,.js",
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@cycjimmy/jsmpeg-player": "^5.0.1", "@cycjimmy/jsmpeg-player": "^5.0.1",
"date-fns": "^2.21.3", "axios": "^0.26.0",
"idb-keyval": "^5.0.2", "date-fns": "^2.28.0",
"immer": "^9.0.6", "idb-keyval": "^6.1.0",
"preact": "^10.5.9", "immer": "^9.0.12",
"preact": "^10.6.6",
"preact-async-route": "^2.2.1", "preact-async-route": "^2.2.1",
"preact-router": "^3.2.1", "preact-router": "^4.0.1",
"video.js": "^7.15.4", "swr": "^1.2.2",
"videojs-playlist": "^4.3.1", "video.js": "^7.20.2",
"videojs-seek-buttons": "^2.0.1" "videojs-playlist": "^5.0.0",
"videojs-seek-buttons": "^2.2.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.12.13", "@babel/preset-env": "^7.16.11",
"@babel/plugin-transform-react-jsx": "^7.12.13", "@babel/preset-typescript": "^7.16.7",
"@babel/preset-env": "^7.12.13", "@preact/preset-vite": "^2.1.5",
"@prefresh/snowpack": "^3.0.1", "@tailwindcss/forms": "^0.5.0",
"@snowpack/plugin-postcss": "^1.1.0", "@testing-library/jest-dom": "^5.16.2",
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/preact": "^2.0.1", "@testing-library/preact": "^2.0.1",
"@testing-library/user-event": "^12.7.1", "@testing-library/preact-hooks": "^1.1.0",
"autoprefixer": "^10.2.1", "@testing-library/user-event": "^13.5.0",
"cross-env": "^7.0.3", "@types/video.js": "^7.3.44",
"eslint": "^7.19.0", "@typescript-eslint/eslint-plugin": "^5.18.0",
"eslint-config-preact": "^1.1.3", "@typescript-eslint/parser": "^5.18.0",
"eslint-config-prettier": "^7.2.0", "autoprefixer": "^10.4.2",
"eslint-plugin-import": "^2.22.1", "eslint": "^8.13.0",
"eslint-plugin-jest": "^24.1.3", "eslint-config-preact": "^1.3.0",
"eslint-plugin-testing-library": "^3.10.1", "eslint-config-prettier": "^8.5.0",
"jest": "^26.6.3", "eslint-plugin-jest": "^26.1.4",
"postcss": "^8.2.10", "jest": "^27.5.1",
"postcss-cli": "^8.3.1", "msw": "^0.38.2",
"prettier": "^2.2.1", "postcss": "^8.4.7",
"rimraf": "^3.0.2", "prettier": "^2.5.1",
"snowpack": "^3.0.11", "tailwindcss": "^3.0.23",
"snowpack-plugin-hash": "^0.14.2", "typescript": "^4.5.4",
"tailwindcss": "^2.0.2" "vite": "^2.8.0"
} }
} }

View File

@ -1,3 +1,6 @@
module.exports = { module.exports = {
plugins: [require('tailwindcss'), require('autoprefixer')], plugins: {
}; tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,5 +0,0 @@
module.exports = {
printWidth: 120,
singleQuote: true,
useTabs: false,
};

View File

@ -1,19 +0,0 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Some files were not shown because too many files have changed in this diff Show More