Adjust for incoming changes

This commit is contained in:
Nick Mowen 2022-03-11 07:10:39 -07:00
commit da559cde46
149 changed files with 9856 additions and 19606 deletions

View File

@ -9,19 +9,33 @@
"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",
"esbenp.prettier-vscode",
"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

@ -10,11 +10,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 working-directory: ./web
web_build: web_build:
@ -24,7 +24,7 @@ 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: Build - name: Build
@ -38,26 +38,14 @@ 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:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and run tests
run: make run_tests PLATFORM="linux/arm64/v8" ARCH="aarch64"
docker_tests_on_amd64: python_tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code - name: Check out code
@ -66,5 +54,7 @@ jobs:
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: Build
run: make run_tests PLATFORM="linux/amd64" ARCH="amd64" run: make
- name: Run tests
run: docker run --rm --entrypoint=python3 frigate:latest -u -m unittest

View File

@ -1,63 +1,18 @@
default_target: amd64_frigate default_target: frigate
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1) COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
version: version:
echo "VERSION='0.11.0-$(COMMIT_HASH)'" > frigate/version.py echo "VERSION='0.11.0-$(COMMIT_HASH)'" > frigate/version.py
web:
docker build --tag frigate-web --file docker/Dockerfile.web web/
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 frigate: version
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 frigate_push: version
docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate:0.11.0-$(COMMIT_HASH) --file docker/Dockerfile .
amd64nvidia_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-amd64nvidia --file docker/Dockerfile.wheels .
amd64nvidia_ffmpeg:
docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.2.0-amd64nvidia --file docker/Dockerfile.ffmpeg.amd64nvidia .
amd64nvidia_frigate: version web
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 build --no-cache --tag frigate --file docker/Dockerfile.amd64nvidia .
amd64nvidia_all: amd64nvidia_wheels amd64nvidia_ffmpeg amd64nvidia_frigate
aarch64_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-aarch64 --file docker/Dockerfile.wheels .
aarch64_ffmpeg:
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: run_tests:
# PLATFORM: linux/arm64/v8 linux/amd64 or linux/arm/v7 # PLATFORM: linux/arm64/v8 linux/amd64 or linux/arm/v7
@ -71,4 +26,4 @@ run_tests:
@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 . @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 @rm docker/Dockerfile.test
.PHONY: web run_tests .PHONY: run_tests

View File

@ -3,18 +3,24 @@ 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
- 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"
- "5000:5000" - "5000:5000"

View File

@ -1,6 +1,6 @@
FROM blakeblackshear/frigate-nginx:1.0.2 as nginx FROM blakeblackshear/frigate-nginx:1.0.2 as nginx
FROM node:14 as web FROM node:16 as web
WORKDIR /opt/frigate WORKDIR /opt/frigate
@ -62,7 +62,6 @@ RUN pip3 wheel --wheel-dir=/wheels \
# Frigate Container # Frigate Container
FROM debian:11-slim FROM debian:11-slim
ARG TARGETARCH ARG TARGETARCH
ARG S6_OVERLAY_VERSION=3.0.0.2
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
ENV FLASK_ENV=development ENV FLASK_ENV=development
@ -93,6 +92,19 @@ RUN apt-get -qq update \
&& rm -rf /var/lib/apt/lists/* /wheels \ && rm -rf /var/lib/apt/lists/* /wheels \
&& (apt-get autoremove -y; apt-get autoclean -y) && (apt-get autoremove -y; apt-get autoclean -y)
# AMD64 specific packages
RUN if [ "${TARGETARCH}" = "amd64" ]; \
then \
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-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
# 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) \
fi
COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/ COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/
# get model and labels # get model and labels
@ -104,7 +116,7 @@ WORKDIR /opt/frigate/
ADD frigate frigate/ ADD frigate frigate/
ADD migrations migrations/ ADD migrations migrations/
COPY --from=web /opt/frigate/build web/ COPY --from=web /opt/frigate/dist web/
COPY docker/rootfs/ / COPY docker/rootfs/ /
@ -114,10 +126,6 @@ RUN S6_ARCH="${TARGETARCH}" \
&& if [ "${TARGETARCH}" = "arm64" ]; then S6_ARCH="aarch64"; 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" \ && 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 / && chmod +x /tmp/s6-overlay-installer && /tmp/s6-overlay-installer /
# && wget -O - "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch-${S6_OVERLAY_VERSION}.tar.xz" \
# | tar -C / -Jxpf - \
# && wget -O - "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}-${S6_OVERLAY_VERSION}.tar.xz" \
# | tar -C / -Jxpf -
EXPOSE 5000 EXPOSE 5000
EXPOSE 1935 EXPOSE 1935

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 \
@ -19,8 +19,8 @@ RUN apt-get update \
RUN pip3 install pylint black RUN pip3 install pylint black
# 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

@ -173,7 +173,6 @@ http {
location /api/ { location /api/ {
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';
add_header Cache-Control "no-store";
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;

View File

@ -131,20 +131,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:

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.
@ -200,6 +190,10 @@ Sets retain to false for the event id (event may be deleted quickly after removi
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.
@ -218,6 +212,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.

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`

View File

@ -183,8 +183,11 @@ def delete_event(id):
def event_thumbnail(id): def event_thumbnail(id):
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
@ -219,8 +222,43 @@ 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"] = "private, max-age=31536000"
return response return response
@bp.route("/<camera_name>/<label>/best.jpg")
@bp.route("/<camera_name>/<label>/thumbnail.jpg")
def label_thumbnail(camera_name, 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)
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"
return response
@bp.route("/events/<id>/snapshot.jpg") @bp.route("/events/<id>/snapshot.jpg")
def event_snapshot(id): def event_snapshot(id):
@ -266,6 +304,37 @@ def event_snapshot(id):
] = f"attachment; filename=snapshot-{id}.jpg" ] = f"attachment; filename=snapshot-{id}.jpg"
return response return response
@bp.route("/<camera_name>/<label>/snapshot.jpg")
def label_snapshot(camera_name, 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):
@ -305,9 +374,9 @@ 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 = request.args.get("label", "all")
zone = request.args.get("zone") 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)
@ -317,20 +386,20 @@ def events():
clauses = [] clauses = []
excluded_fields = [] excluded_fields = []
if camera: 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 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))
@ -386,48 +455,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"))
@ -648,7 +675,7 @@ def recording_clip(camera, start_ts, end_ts):
"-safe", "-safe",
"0", "0",
"-i", "-i",
"-", "/dev/stdin",
"-c", "-c",
"copy", "copy",
"-movflags", "-movflags",

View File

@ -554,13 +554,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 +579,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)
@ -889,9 +904,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 +926,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 +940,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

@ -1 +0,0 @@
node_modules

View File

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

29
web/.eslintrc Normal file
View File

@ -0,0 +1,29 @@
{
"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" }],
"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,157 +0,0 @@
module.exports = {
parser: '@babel/eslint-parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
},
extends: [
'prettier',
'preact',
'plugin:import/react',
'plugin:import/typescript',
'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'],
},
},
},
overrides: [
{
files: ['*.{ts,tsx}'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended'],
settings: {
'import/resolver': {
node: {
extensions: ['.ts', '.tsx'],
},
},
},
},
],
};

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).

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

@ -0,0 +1,73 @@
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 },
},
side: {
name: 'side',
objects: { track: ['taco', 'cat', 'dog'] },
record: { enabled: false },
detect: { width: 1280, height: 720 },
snapshots: {},
live: { height: 720 },
},
},
})
);
}),
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...',
}))
)
);
}),
];

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

View File

Before

Width:  |  Height:  |  Size: 534 B

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,
}; };

View File

@ -1,8 +0,0 @@
{
"compilerOptions": {
"target": "ES2019",
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
}
}

23695
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,54 +1,50 @@
{ {
"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",
"start:custom": "snowpack dev", "lint": "eslint ./ --ext .jsx,.js,.tsx,.ts",
"prebuild": "rimraf build", "build": "tsc && vite build",
"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,.tsx,.ts",
"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.17.0",
"videojs-seek-buttons": "^2.0.1" "videojs-playlist": "^5.0.0",
"videojs-seek-buttons": "^2.2.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.12.13", "@babel/preset-env": "^7.16.11",
"@babel/plugin-transform-react-jsx": "^7.12.13",
"@babel/preset-env": "^7.12.13",
"@babel/preset-typescript": "^7.16.7", "@babel/preset-typescript": "^7.16.7",
"@prefresh/snowpack": "^3.0.1", "@preact/preset-vite": "^2.1.5",
"@snowpack/plugin-postcss": "^1.1.0", "@tailwindcss/forms": "^0.5.0",
"@testing-library/jest-dom": "^5.11.9", "@testing-library/jest-dom": "^5.16.2",
"@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",
"@typescript-eslint/eslint-plugin": "^5.12.0", "@testing-library/user-event": "^13.5.0",
"@typescript-eslint/parser": "^5.12.0", "@typescript-eslint/eslint-plugin": "^5.13.0",
"autoprefixer": "^10.2.1", "@typescript-eslint/parser": "^5.13.0",
"cross-env": "^7.0.3", "autoprefixer": "^10.4.2",
"eslint": "^7.19.0", "eslint": "^8.10.0",
"eslint-config-preact": "^1.1.3", "eslint-config-preact": "^1.3.0",
"eslint-config-prettier": "^7.2.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^26.1.1",
"eslint-plugin-jest": "^24.1.3", "jest": "^27.5.1",
"eslint-plugin-testing-library": "^3.10.1", "msw": "^0.38.2",
"jest": "^26.6.3", "postcss": "^8.4.7",
"postcss": "^8.2.10", "prettier": "^2.5.1",
"postcss-cli": "^8.3.1", "tailwindcss": "^3.0.23",
"prettier": "^2.2.1", "typescript": "^4.5.4",
"rimraf": "^3.0.2", "vite": "^2.8.0"
"snowpack": "^3.0.11",
"snowpack-plugin-hash": "^0.14.2",
"tailwindcss": "^2.0.2"
} }
} }

View File

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

View File

@ -1,6 +0,0 @@
module.exports = {
printWidth: 120,
singleQuote: true,
jsxSingleQuote: 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"
}

19
web/site.webmanifest Normal file
View File

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

View File

@ -1,19 +0,0 @@
module.exports = {
mount: {
public: { url: '/', static: true },
src: { url: '/dist' },
},
plugins: ['@snowpack/plugin-postcss', '@prefresh/snowpack', 'snowpack-plugin-hash'],
routes: [{ match: 'all', src: '(?!.*(.svg|.gif|.json|.jpg|.jpeg|.png|.js)).*', dest: '/index.html' }],
optimize: {
bundle: false,
minify: true,
treeshake: true,
},
packageOptions: {
sourcemap: false,
},
buildOptions: {
sourcemap: false,
},
};

View File

@ -7,17 +7,18 @@ import Cameras from './routes/Cameras';
import { Router } from 'preact-router'; import { Router } from 'preact-router';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
import { DarkModeProvider, DrawerProvider } from './context'; import { DarkModeProvider, DrawerProvider } from './context';
import { FetchStatus, useConfig } from './api'; import useSWR from 'swr';
export default function App() { export default function App() {
const { status, data: config } = useConfig(); const { data: config } = useSWR('config');
const cameraComponent = config && config.ui.use_experimental ? Routes.getCameraV2 : Routes.getCamera; const cameraComponent = config && config.ui.use_experimental ? Routes.getCameraV2 : Routes.getCamera;
return ( return (
<DarkModeProvider> <DarkModeProvider>
<DrawerProvider> <DrawerProvider>
<div data-testid="app" className="w-full"> <div data-testid="app" className="w-full">
<AppBar /> <AppBar />
{status !== FetchStatus.LOADED ? ( {!config ? (
<div className="flex flex-grow-1 min-h-screen justify-center items-center"> <div className="flex flex-grow-1 min-h-screen justify-center items-center">
<ActivityIndicator /> <ActivityIndicator />
</div> </div>

View File

@ -19,7 +19,7 @@ export default function AppBar() {
const { send: sendRestart } = useRestart(); const { send: sendRestart } = useRestart();
const handleSelectDarkMode = useCallback( const handleSelectDarkMode = useCallback(
(value, label) => { (value) => {
setDarkMode(value); setDarkMode(value);
setShowMoreMenu(false); setShowMoreMenu(false);
}, },

View File

@ -3,14 +3,15 @@ import LinkedLogo from './components/LinkedLogo';
import { Match } from 'preact-router/match'; import { Match } from 'preact-router/match';
import { memo } from 'preact/compat'; import { memo } from 'preact/compat';
import { ENV } from './env'; import { ENV } from './env';
import { useConfig } from './api'; import useSWR from 'swr';
import { useMemo } from 'preact/hooks';
import NavigationDrawer, { Destination, Separator } from './components/NavigationDrawer'; import NavigationDrawer, { Destination, Separator } from './components/NavigationDrawer';
export default function Sidebar() { export default function Sidebar() {
const { data: config } = useConfig(); const { data: config } = useSWR('config');
const cameras = useMemo(() => Object.entries(config.cameras), [config]); if (!config) {
const { birdseye } = config; return null;
}
const { cameras, birdseye } = config;
return ( return (
<NavigationDrawer header={<Header />}> <NavigationDrawer header={<Header />}>
@ -20,7 +21,10 @@ export default function Sidebar() {
matches ? ( matches ? (
<Fragment> <Fragment>
<Separator /> <Separator />
{cameras.filter(([cam, conf]) => conf.gui.show).sort(([aCam, aConf], [bCam, bConf]) => aConf.gui.order === bConf.gui.order ? 0 : (aConf.gui.order > bConf.gui.order ? 1 : -1)).map(([camera]) => ( {Object.entries(cameras)
.filter(([cam, conf]) => conf.gui.show)
.sort(([aCam, aConf], [bCam, bConf]) => aConf.gui.order === bConf.gui.order ? 0 : (aConf.gui.order > bConf.gui.order ? 1 : -1))
.map(([camera]) => (
<Destination key={camera} href={`/cameras/${camera}`} text={camera} /> <Destination key={camera} href={`/cameras/${camera}`} text={camera} />
))} ))}
<Separator /> <Separator />
@ -33,7 +37,10 @@ export default function Sidebar() {
matches ? ( matches ? (
<Fragment> <Fragment>
<Separator /> <Separator />
{cameras.filter(([cam, conf]) => conf.gui.show).sort(([aCam, aConf], [bCam, bConf]) => aConf.gui.order === bConf.gui.order ? 0 : (aConf.gui.order > bConf.gui.order ? 1 : -1)).map(([camera, conf]) => { {Object.entries(cameras)
.filter(([cam, conf]) => conf.gui.show)
.sort(([aCam, aConf], [bCam, bConf]) => aConf.gui.order === bConf.gui.order ? 0 : (aConf.gui.order > bConf.gui.order ? 1 : -1))
.map(([camera, conf]) => {
if (conf.record.enabled) { if (conf.record.enabled) {
return ( return (
<Destination <Destination

View File

@ -1,25 +1,17 @@
import { h } from 'preact'; import { h } from 'preact';
import * as Api from '../api';
import * as IDB from 'idb-keyval'; import * as IDB from 'idb-keyval';
import * as PreactRouter from 'preact-router'; import * as PreactRouter from 'preact-router';
import App from '../App'; import App from '../App';
import { render, screen } from '@testing-library/preact'; import { render, screen } from 'testing-library';
describe('App', () => { describe('App', () => {
let mockUseConfig;
beforeEach(() => { beforeEach(() => {
jest.spyOn(IDB, 'get').mockImplementation(() => Promise.resolve(undefined)); jest.spyOn(IDB, 'get').mockImplementation(() => Promise.resolve(undefined));
jest.spyOn(IDB, 'set').mockImplementation(() => Promise.resolve(true)); jest.spyOn(IDB, 'set').mockImplementation(() => Promise.resolve(true));
mockUseConfig = jest.spyOn(Api, 'useConfig').mockImplementation(() => ({
data: { cameras: { front: { name: 'front', objects: { track: ['taco', 'cat', 'dog'] } } } },
}));
jest.spyOn(Api, 'useApiHost').mockImplementation(() => 'http://base-url.local:5000');
jest.spyOn(PreactRouter, 'Router').mockImplementation(() => <div data-testid="router" />); jest.spyOn(PreactRouter, 'Router').mockImplementation(() => <div data-testid="router" />);
}); });
test('shows a loading indicator while loading', async () => { test('shows a loading indicator while loading', async () => {
mockUseConfig.mockReturnValue({ status: 'loading' });
render(<App />); render(<App />);
await screen.findByTestId('app'); await screen.findByTestId('app');
expect(screen.queryByLabelText('Loading…')).toBeInTheDocument(); expect(screen.queryByLabelText('Loading…')).toBeInTheDocument();

View File

@ -1,7 +1,7 @@
import { h } from 'preact'; import { h } from 'preact';
import * as Context from '../context'; import * as Context from '../context';
import AppBar from '../AppBar'; import AppBar from '../AppBar';
import { fireEvent, render, screen } from '@testing-library/preact'; import { fireEvent, render, screen } from 'testing-library';
describe('AppBar', () => { describe('AppBar', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -1,40 +1,17 @@
import { h } from 'preact'; import { h } from 'preact';
import * as Api from '../api';
import * as Context from '../context'; import * as Context from '../context';
import Sidebar from '../Sidebar'; import Sidebar from '../Sidebar';
import { render, screen } from '@testing-library/preact'; import { render, screen } from 'testing-library';
describe('Sidebar', () => { describe('Sidebar', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(Api, 'useConfig').mockImplementation(() => ({
data: {
cameras: {
front: { name: 'front', objects: { track: ['taco', 'cat', 'dog'] }, record: { enabled: true }, gui: { order: 0, show: true } },
side: { name: 'side', objects: { track: ['taco', 'cat', 'dog'] }, record: { enabled: false }, gui: { order: 0, show: true } },
},
},
}));
jest.spyOn(Context, 'useDrawer').mockImplementation(() => ({ showDrawer: true, setShowDrawer: () => {} })); jest.spyOn(Context, 'useDrawer').mockImplementation(() => ({ showDrawer: true, setShowDrawer: () => {} }));
}); });
test('does not render cameras by default', async () => { test('does not render cameras by default', async () => {
render(<Sidebar />); const { findByText } = render(<Sidebar />);
await findByText('Cameras');
expect(screen.queryByRole('link', { name: 'front' })).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'front' })).not.toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'side' })).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'side' })).not.toBeInTheDocument();
}); });
test('render cameras if in camera route', async () => {
window.history.replaceState({}, 'Cameras', '/cameras/front');
render(<Sidebar />);
expect(screen.queryByRole('link', { name: 'front' })).toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'side' })).toBeInTheDocument();
});
test('render cameras if in record route', async () => {
window.history.replaceState({}, 'Front Recordings', '/recording/front');
render(<Sidebar />);
expect(screen.queryByRole('link', { name: 'front' })).toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'side' })).not.toBeInTheDocument();
});
}); });

View File

@ -1,7 +1,7 @@
import { h } from 'preact'; import { h } from 'preact';
import * as Mqtt from '../mqtt'; import * as Mqtt from '../mqtt';
import { ApiProvider, useFetch, useApiHost } from '..'; import { ApiProvider, useApiHost } from '..';
import { render, screen } from '@testing-library/preact'; import { render, screen } from 'testing-library';
describe('useApiHost', () => { describe('useApiHost', () => {
beforeEach(() => { beforeEach(() => {
@ -21,101 +21,3 @@ describe('useApiHost', () => {
expect(screen.queryByText('http://base-url.local:5000')).toBeInTheDocument(); expect(screen.queryByText('http://base-url.local:5000')).toBeInTheDocument();
}); });
}); });
function Test() {
const { data, status } = useFetch('/api/tacos');
return (
<div>
<span>{data ? data.returnData : ''}</span>
<span>{status}</span>
</div>
);
}
describe('useFetch', () => {
let fetchSpy;
beforeEach(() => {
jest.spyOn(Mqtt, 'MqttProvider').mockImplementation(({ children }) => children);
fetchSpy = jest.spyOn(window, 'fetch').mockImplementation(async (url, options) => {
if (url.endsWith('/api/config')) {
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
}
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ok: true, json: () => Promise.resolve({ returnData: 'yep' }) });
}, 1);
});
});
});
test('loads data', async () => {
render(
<ApiProvider>
<Test />
</ApiProvider>
);
expect(screen.queryByText('loading')).toBeInTheDocument();
expect(screen.queryByText('yep')).not.toBeInTheDocument();
jest.runAllTimers();
await screen.findByText('loaded');
expect(fetchSpy).toHaveBeenCalledWith('http://base-url.local:5000/api/tacos');
expect(screen.queryByText('loaded')).toBeInTheDocument();
expect(screen.queryByText('yep')).toBeInTheDocument();
});
test('sets error if response is not okay', async () => {
jest.spyOn(window, 'fetch').mockImplementation((url) => {
if (url.includes('/config')) {
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
}
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ok: false });
}, 1);
});
});
render(
<ApiProvider>
<Test />
</ApiProvider>
);
expect(screen.queryByText('loading')).toBeInTheDocument();
jest.runAllTimers();
await screen.findByText('error');
});
test('does not re-fetch if the query has already been made', async () => {
const { rerender } = render(
<ApiProvider>
<Test key={0} />
</ApiProvider>
);
expect(screen.queryByText('loading')).toBeInTheDocument();
expect(screen.queryByText('yep')).not.toBeInTheDocument();
jest.runAllTimers();
await screen.findByText('loaded');
expect(fetchSpy).toHaveBeenCalledWith('http://base-url.local:5000/api/tacos');
rerender(
<ApiProvider>
<Test key={1} />
</ApiProvider>
);
expect(screen.queryByText('loaded')).toBeInTheDocument();
expect(screen.queryByText('yep')).toBeInTheDocument();
jest.runAllTimers();
// once for /api/config, once for /api/tacos
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
});

View File

@ -1,14 +1,16 @@
import { h } from 'preact'; import { h } from 'preact';
import { Mqtt, MqttProvider, useMqtt } from '../mqtt'; import { Mqtt, MqttProvider, useMqtt } from '../mqtt';
import { useCallback, useContext } from 'preact/hooks'; import { useCallback, useContext } from 'preact/hooks';
import { fireEvent, render, screen } from '@testing-library/preact'; import { fireEvent, render, screen } from 'testing-library';
function Test() { function Test() {
const { state } = useContext(Mqtt); const { state } = useContext(Mqtt);
return state.__connected ? ( return state.__connected ? (
<div data-testid="data"> <div data-testid="data">
{Object.keys(state).map((key) => ( {Object.keys(state).map((key) => (
<div data-testid={key}>{JSON.stringify(state[key])}</div> <div key={key} data-testid={key}>
{JSON.stringify(state[key])}
</div>
))} ))}
</div> </div>
) : null; ) : null;
@ -28,10 +30,10 @@ describe('MqttProvider', () => {
return new Proxy( return new Proxy(
{}, {},
{ {
get(target, prop, receiver) { get(_target, prop, _receiver) {
return wsClient[prop]; return wsClient[prop];
}, },
set(target, prop, value) { set(_target, prop, value) {
wsClient[prop] = typeof value === 'function' ? jest.fn(value) : value; wsClient[prop] = typeof value === 'function' ? jest.fn(value) : value;
if (prop === 'onopen') { if (prop === 'onopen') {
wsClient[prop](); wsClient[prop]();
@ -121,12 +123,24 @@ describe('MqttProvider', () => {
</MqttProvider> </MqttProvider>
); );
await screen.findByTestId('data'); await screen.findByTestId('data');
expect(screen.getByTestId('front/detect/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"ON","retain":true}'); expect(screen.getByTestId('front/detect/state')).toHaveTextContent(
expect(screen.getByTestId('front/recordings/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF","retain":true}'); '{"lastUpdate":123456,"payload":"ON","retain":true}'
expect(screen.getByTestId('front/snapshots/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"ON","retain":true}'); );
expect(screen.getByTestId('side/detect/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF","retain":true}'); expect(screen.getByTestId('front/recordings/state')).toHaveTextContent(
expect(screen.getByTestId('side/recordings/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF","retain":true}'); '{"lastUpdate":123456,"payload":"OFF","retain":true}'
expect(screen.getByTestId('side/snapshots/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF","retain":true}'); );
expect(screen.getByTestId('front/snapshots/state')).toHaveTextContent(
'{"lastUpdate":123456,"payload":"ON","retain":true}'
);
expect(screen.getByTestId('side/detect/state')).toHaveTextContent(
'{"lastUpdate":123456,"payload":"OFF","retain":true}'
);
expect(screen.getByTestId('side/recordings/state')).toHaveTextContent(
'{"lastUpdate":123456,"payload":"OFF","retain":true}'
);
expect(screen.getByTestId('side/snapshots/state')).toHaveTextContent(
'{"lastUpdate":123456,"payload":"OFF","retain":true}'
);
}); });
}); });

View File

@ -1,166 +1,29 @@
import { h } from 'preact';
import { baseUrl } from './baseUrl'; import { baseUrl } from './baseUrl';
import { h, createContext } from 'preact'; import useSWR, { SWRConfig } from 'swr';
import { MqttProvider } from './mqtt'; import { MqttProvider } from './mqtt';
import produce from 'immer'; import axios from 'axios';
import { useContext, useEffect, useReducer } from 'preact/hooks';
export const FetchStatus = { axios.defaults.baseURL = `${baseUrl}/api/`;
NONE: 'none',
LOADING: 'loading',
LOADED: 'loaded',
ERROR: 'error',
};
const initialState = Object.freeze({ export function ApiProvider({ children, options }) {
host: baseUrl,
queries: {},
});
const Api = createContext(initialState);
function reducer(state, { type, payload }) {
switch (type) {
case 'REQUEST': {
const { url, fetchId } = payload;
const data = state.queries[url]?.data || null;
return produce(state, (draftState) => {
draftState.queries[url] = { status: FetchStatus.LOADING, data, fetchId };
});
}
case 'RESPONSE': {
const { url, ok, data, fetchId } = payload;
return produce(state, (draftState) => {
draftState.queries[url] = { status: ok ? FetchStatus.LOADED : FetchStatus.ERROR, data, fetchId };
});
}
case 'DELETE': {
const { eventId } = payload;
return produce(state, (draftState) => {
Object.keys(draftState.queries).map((url) => {
draftState.queries[url].deletedId = eventId;
});
});
}
default:
return state;
}
}
export function ApiProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return ( return (
<Api.Provider value={{ state, dispatch }}> <SWRConfig
value={{
fetcher: (path) => axios.get(path).then((res) => res.data),
...options,
}}
>
<MqttWithConfig>{children}</MqttWithConfig> <MqttWithConfig>{children}</MqttWithConfig>
</Api.Provider> </SWRConfig>
); );
} }
function MqttWithConfig({ children }) { function MqttWithConfig({ children }) {
const { data, status } = useConfig(); const { data } = useSWR('config');
return status === FetchStatus.LOADED ? <MqttProvider config={data}>{children}</MqttProvider> : children; return data ? <MqttProvider config={data}>{children}</MqttProvider> : children;
}
function shouldFetch(state, url, fetchId = null) {
if ((fetchId && url in state.queries && state.queries[url].fetchId !== fetchId) || !(url in state.queries)) {
return true;
}
const { status } = state.queries[url];
return status !== FetchStatus.LOADING && status !== FetchStatus.LOADED;
}
export function useFetch(url, fetchId) {
const { state, dispatch } = useContext(Api);
useEffect(() => {
if (!shouldFetch(state, url, fetchId)) {
return;
}
async function fetchData() {
await dispatch({ type: 'REQUEST', payload: { url, fetchId } });
const response = await fetch(`${state.host}${url}`);
try {
const data = await response.json();
await dispatch({ type: 'RESPONSE', payload: { url, ok: response.ok, data, fetchId } });
} catch (e) {
await dispatch({ type: 'RESPONSE', payload: { url, ok: false, data: null, fetchId } });
}
}
fetchData();
}, [url, fetchId, state, dispatch]);
if (!(url in state.queries)) {
return { data: null, status: FetchStatus.NONE };
}
const data = state.queries[url].data || null;
const status = state.queries[url].status;
const deletedId = state.queries[url].deletedId || 0;
return { data, status, deletedId };
}
export function useDelete() {
const { dispatch, state } = useContext(Api);
async function deleteEvent(eventId) {
if (!eventId) return null;
const response = await fetch(`${state.host}/api/events/${eventId}`, { method: 'DELETE' });
await dispatch({ type: 'DELETE', payload: { eventId } });
return await (response.status < 300 ? response.json() : { success: true });
}
return deleteEvent;
}
export function useRetain() {
const { state } = useContext(Api);
async function retainEvent(eventId, shouldRetain) {
if (!eventId) return null;
if (shouldRetain) {
const response = await fetch(`${state.host}/api/events/${eventId}/retain`, { method: 'POST' });
return await (response.status < 300 ? response.json() : { success: true });
} else {
const response = await fetch(`${state.host}/api/events/${eventId}/retain`, { method: 'DELETE' });
return await (response.status < 300 ? response.json() : { success: true });
}
}
return retainEvent;
} }
export function useApiHost() { export function useApiHost() {
const { state } = useContext(Api); return baseUrl;
return state.host;
}
export function useEvents(searchParams, fetchId) {
const url = `/api/events${searchParams ? `?${searchParams.toString()}` : ''}`;
return useFetch(url, fetchId);
}
export function useEvent(eventId, fetchId) {
const url = `/api/events/${eventId}`;
return useFetch(url, fetchId);
}
export function useRecording(camera, fetchId) {
const url = `/api/${camera}/recordings`;
return useFetch(url, fetchId);
}
export function useConfig(searchParams, fetchId) {
const url = `/api/config${searchParams ? `?${searchParams.toString()}` : ''}`;
return useFetch(url, fetchId);
}
export function useStats(searchParams, fetchId) {
const url = `/api/stats${searchParams ? `?${searchParams.toString()}` : ''}`;
return useFetch(url, fetchId);
} }

View File

@ -1,8 +1,8 @@
import { h } from 'preact'; import { h, JSX } from 'preact';
interface BubbleButtonProps { interface BubbleButtonProps {
variant?: 'primary' | 'secondary'; variant?: 'primary' | 'secondary';
children?: preact.JSX.Element; children?: JSX.Element;
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;

View File

@ -64,7 +64,6 @@ export default function Button({
color = 'blue', color = 'blue',
disabled = false, disabled = false,
href, href,
size,
type = 'contained', type = 'contained',
...attrs ...attrs
}) { }) {
@ -81,11 +80,11 @@ export default function Button({
classes = classes.replace(/(?:focus|active|hover):[^ ]+/g, ''); classes = classes.replace(/(?:focus|active|hover):[^ ]+/g, '');
} }
const handleMousenter = useCallback((event) => { const handleMousenter = useCallback(() => {
setHovered(true); setHovered(true);
}, []); }, []);
const handleMouseleave = useCallback((event) => { const handleMouseleave = useCallback(() => {
setHovered(false); setHovered(false);
}, []); }, []);

View File

@ -7,27 +7,35 @@ export default function ButtonsTabbed({
setHeader = null, setHeader = null,
headers = [''], headers = [''],
className = 'text-gray-600 py-0 px-4 block hover:text-gray-500', className = 'text-gray-600 py-0 px-4 block hover:text-gray-500',
selectedClassName = `${className} focus:outline-none border-b-2 font-medium border-gray-500` selectedClassName = `${className} focus:outline-none border-b-2 font-medium border-gray-500`,
}) { }) {
const [selected, setSelected] = useState(0); const [selected, setSelected] = useState(0);
const captitalize = (str) => { return (`${str.charAt(0).toUpperCase()}${str.slice(1)}`); }; const captitalize = (str) => {
return `${str.charAt(0).toUpperCase()}${str.slice(1)}`;
};
const getHeader = useCallback((i) => { const getHeader = useCallback(
return (headers.length === viewModes.length ? headers[i] : captitalize(viewModes[i])); (i) => {
}, [headers, viewModes]); return headers.length === viewModes.length ? headers[i] : captitalize(viewModes[i]);
},
[headers, viewModes]
);
const handleClick = useCallback((i) => { const handleClick = useCallback(
setSelected(i); (i) => {
setViewMode && setViewMode(viewModes[i]); setSelected(i);
setHeader && setHeader(getHeader(i)); setViewMode && setViewMode(viewModes[i]);
}, [setViewMode, setHeader, setSelected, viewModes, getHeader]); setHeader && setHeader(getHeader(i));
},
[setViewMode, setHeader, setSelected, viewModes, getHeader]
);
setHeader && setHeader(getHeader(selected)); setHeader && setHeader(getHeader(selected));
return ( return (
<nav className="flex justify-end"> <nav className="flex justify-end">
{viewModes.map((item, i) => { {viewModes.map((item, i) => {
return ( return (
<button onClick={() => handleClick(i)} className={i === selected ? selectedClassName : className}> <button key={i} onClick={() => handleClick(i)} className={i === selected ? selectedClassName : className}>
{captitalize(item)} {captitalize(item)}
</button> </button>
); );

View File

@ -5,7 +5,7 @@ import ArrowRightDouble from '../icons/ArrowRightDouble';
const todayTimestamp = new Date().setHours(0, 0, 0, 0).valueOf(); const todayTimestamp = new Date().setHours(0, 0, 0, 0).valueOf();
const Calender = ({ onChange, calenderRef, close }) => { const Calendar = ({ onChange, calendarRef, close, dateRange }) => {
const keyRef = useRef([]); const keyRef = useRef([]);
const date = new Date(); const date = new Date();
@ -36,7 +36,7 @@ const Calender = ({ onChange, calenderRef, close }) => {
year, year,
month, month,
selectedDay: null, selectedDay: null,
timeRange: { before: null, after: null }, timeRange: dateRange,
monthDetails: null, monthDetails: null,
}); });
@ -98,7 +98,11 @@ const Calender = ({ onChange, calenderRef, close }) => {
); );
useEffect(() => { useEffect(() => {
setState((prev) => ({ ...prev, selectedDay: todayTimestamp, monthDetails: getMonthDetails(year, month) })); setState((prev) => ({
...prev,
selectedDay: todayTimestamp,
monthDetails: getMonthDetails(year, month),
}));
}, [year, month, getMonthDetails]); }, [year, month, getMonthDetails]);
useEffect(() => { useEffect(() => {
@ -150,7 +154,10 @@ const Calender = ({ onChange, calenderRef, close }) => {
// user has selected a date < after, reset values // user has selected a date < after, reset values
if (after === null || day.timestamp < after) { if (after === null || day.timestamp < after) {
timeRange = { before: new Date(day.timestamp).setHours(24, 0, 0, 0), after: day.timestamp }; timeRange = {
before: new Date(day.timestamp).setHours(24, 0, 0, 0),
after: day.timestamp,
};
} }
// user has selected a date > after // user has selected a date > after
@ -280,7 +287,7 @@ const Calender = ({ onChange, calenderRef, close }) => {
}; };
return ( return (
<div className="select-none w-96 flex flex-shrink" ref={calenderRef}> <div className="select-none w-96 flex flex-shrink" ref={calendarRef}>
<div className="py-4 px-6"> <div className="py-4 px-6">
<div className="flex items-center"> <div className="flex items-center">
<div className="w-1/6 relative flex justify-around"> <div className="w-1/6 relative flex justify-around">
@ -314,7 +321,7 @@ const Calender = ({ onChange, calenderRef, close }) => {
<ArrowRight className="h-2/6" /> <ArrowRight className="h-2/6" />
</div> </div>
</div> </div>
<div className="w-1/6 relative flex justify-around " tabIndex={104} onClick={() => setYear(1)}> <div className="w-1/6 relative flex justify-around" tabIndex={104} onClick={() => setYear(1)}>
<div className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800"> <div className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800">
<ArrowRightDouble className="h-2/6" /> <ArrowRightDouble className="h-2/6" />
</div> </div>
@ -326,4 +333,4 @@ const Calender = ({ onChange, calenderRef, close }) => {
); );
}; };
export default Calender; export default Calendar;

View File

@ -1,19 +1,20 @@
import { h } from 'preact'; import { h } from 'preact';
import ActivityIndicator from './ActivityIndicator'; import ActivityIndicator from './ActivityIndicator';
import { useApiHost, useConfig } from '../api'; import { useApiHost } from '../api';
import useSWR from 'swr';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useResizeObserver } from '../hooks'; import { useResizeObserver } from '../hooks';
export default function CameraImage({ camera, onload, searchParams = '', stretch = false }) { export default function CameraImage({ camera, onload, searchParams = '', stretch = false }) {
const { data: config } = useConfig(); const { data: config } = useSWR('config');
const apiHost = useApiHost(); const apiHost = useApiHost();
const [hasLoaded, setHasLoaded] = useState(false); const [hasLoaded, setHasLoaded] = useState(false);
const containerRef = useRef(null); const containerRef = useRef(null);
const canvasRef = useRef(null); const canvasRef = useRef(null);
const [{ width: availableWidth }] = useResizeObserver(containerRef); const [{ width: availableWidth }] = useResizeObserver(containerRef);
const { name } = config.cameras[camera]; const { name } = config ? config.cameras[camera] : '';
const { width, height } = config.cameras[camera].detect; const { width, height } = config ? config.cameras[camera].detect : { width: 1, height: 1 };
const aspectRatio = width / height; const aspectRatio = width / height;
const scaledHeight = useMemo(() => { const scaledHeight = useMemo(() => {
@ -36,11 +37,11 @@ export default function CameraImage({ camera, onload, searchParams = '', stretch
); );
useEffect(() => { useEffect(() => {
if (scaledHeight === 0 || !canvasRef.current) { if (!config || scaledHeight === 0 || !canvasRef.current) {
return; return;
} }
img.src = `${apiHost}/api/${name}/latest.jpg?h=${scaledHeight}${searchParams ? `&${searchParams}` : ''}`; img.src = `${apiHost}/api/${name}/latest.jpg?h=${scaledHeight}${searchParams ? `&${searchParams}` : ''}`;
}, [apiHost, canvasRef, name, img, searchParams, scaledHeight]); }, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]);
return ( return (
<div className="relative w-full" ref={containerRef}> <div className="relative w-full" ref={containerRef}>

View File

@ -66,7 +66,6 @@ export default function DatePicker({
onBlur, onBlur,
onChangeText, onChangeText,
onFocus, onFocus,
readonly,
trailingIcon: TrailingIcon, trailingIcon: TrailingIcon,
value: propValue = '', value: propValue = '',
...props ...props

View File

@ -1,9 +1,9 @@
import { h } from 'preact'; import { h } from 'preact';
import Heading from '../Heading'; import Heading from '../Heading';
import { TimelineEvent } from '../Timeline/Timeline'; import type { TimelineEvent } from '../Timeline/TimelineEvent';
interface HistoryHeaderProps { interface HistoryHeaderProps {
event: TimelineEvent; event?: TimelineEvent;
className?: string; className?: string;
} }
export const HistoryHeader = ({ event, className = '' }: HistoryHeaderProps) => { export const HistoryHeader = ({ event, className = '' }: HistoryHeaderProps) => {

View File

@ -15,7 +15,7 @@ interface VideoProperties {
} }
interface HistoryVideoProps { interface HistoryVideoProps {
id: string; id?: string;
isPlaying: boolean; isPlaying: boolean;
currentTime: number; currentTime: number;
onTimeUpdate?: (event: OnTimeUpdateEvent) => void; onTimeUpdate?: (event: OnTimeUpdateEvent) => void;
@ -32,9 +32,13 @@ export const HistoryVideo = ({
onPlay, onPlay,
}: HistoryVideoProps) => { }: HistoryVideoProps) => {
const apiHost = useApiHost(); const apiHost = useApiHost();
const videoRef = useRef<HTMLVideoElement>(); const videoRef = useRef<HTMLVideoElement|null>(null);
const [videoHeight, setVideoHeight] = useState<number>(undefined); const [videoHeight, setVideoHeight] = useState<number>(0);
const [videoProperties, setVideoProperties] = useState<VideoProperties>(undefined); const [videoProperties, setVideoProperties] = useState<VideoProperties>({
posterUrl: '',
videoUrl: '',
height: 0,
});
const currentVideo = videoRef.current; const currentVideo = videoRef.current;
if (currentVideo && !videoHeight) { if (currentVideo && !videoHeight) {
@ -48,7 +52,7 @@ export const HistoryVideo = ({
const idExists = !isNullOrUndefined(id); const idExists = !isNullOrUndefined(id);
if (idExists) { if (idExists) {
if (videoRef.current && !videoRef.current.paused) { if (videoRef.current && !videoRef.current.paused) {
videoRef.current = undefined; videoRef.current = null;
} }
setVideoProperties({ setVideoProperties({
@ -57,7 +61,11 @@ export const HistoryVideo = ({
height: videoHeight, height: videoHeight,
}); });
} else { } else {
setVideoProperties(undefined); setVideoProperties({
posterUrl: '',
videoUrl: '',
height: 0,
});
} }
}, [id, videoHeight, videoRef, apiHost]); }, [id, videoHeight, videoRef, apiHost]);
@ -78,7 +86,7 @@ export const HistoryVideo = ({
const video = videoRef.current; const video = videoRef.current;
const videoExists = !isNullOrUndefined(video); const videoExists = !isNullOrUndefined(video);
if (videoExists) { if (video && videoExists) {
if (videoIsPlaying) { if (videoIsPlaying) {
attemptPlayVideo(video); attemptPlayVideo(video);
} else { } else {
@ -91,7 +99,7 @@ export const HistoryVideo = ({
const video = videoRef.current; const video = videoRef.current;
const videoExists = !isNullOrUndefined(video); const videoExists = !isNullOrUndefined(video);
const hasSeeked = currentTime >= 0; const hasSeeked = currentTime >= 0;
if (videoExists && hasSeeked) { if (video && videoExists && hasSeeked) {
video.currentTime = currentTime; video.currentTime = currentTime;
} }
}, [currentTime, videoRef]); }, [currentTime, videoRef]);

View File

@ -1,22 +1,34 @@
import { Fragment, h } from 'preact'; import { Fragment, h } from 'preact';
import { useCallback, useEffect, useState } from 'preact/hooks'; import { useCallback, useEffect, useState } from 'preact/hooks';
import { useEvents } from '../../api'; import useSWR from 'swr';
import { useSearchString } from '../../hooks/useSearchString'; import axios from 'axios';
import { getNowYesterdayInLong } from '../../utils/dateUtil';
import Timeline from '../Timeline/Timeline'; import Timeline from '../Timeline/Timeline';
import { TimelineChangeEvent } from '../Timeline/TimelineChangeEvent'; import type { TimelineChangeEvent } from '../Timeline/TimelineChangeEvent';
import { TimelineEvent } from '../Timeline/TimelineEvent'; import type { TimelineEvent } from '../Timeline/TimelineEvent';
import { HistoryHeader } from './HistoryHeader'; import { HistoryHeader } from './HistoryHeader';
import { HistoryVideo } from './HistoryVideo'; import { HistoryVideo } from './HistoryVideo';
export default function HistoryViewer({ camera }) { export default function HistoryViewer({ camera }: {camera: string}) {
const { searchString } = useSearchString(500, `camera=${camera}&after=${getNowYesterdayInLong()}`); const searchParams = {
const { data: events } = useEvents(searchString); before: null,
after: null,
camera,
label: 'all',
zone: 'all',
};
const [timelineEvents, setTimelineEvents] = useState<TimelineEvent[]>(undefined); // TODO: refactor
const [currentEvent, setCurrentEvent] = useState<TimelineEvent>(undefined); const eventsFetcher = (path: string, params: {[name:string]: string|number}) => {
const [isPlaying, setIsPlaying] = useState(undefined); params = { ...params, include_thumbnails: 0, limit: 500 };
const [currentTime, setCurrentTime] = useState<number>(undefined); return axios.get<TimelineEvent[]>(path, { params }).then((res) => res.data);
};
const { data: events } = useSWR(['events', searchParams], eventsFetcher);
const [timelineEvents, setTimelineEvents] = useState<TimelineEvent[]>([]);
const [currentEvent, setCurrentEvent] = useState<TimelineEvent>();
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [currentTime, setCurrentTime] = useState<number>(new Date().getTime());
useEffect(() => { useEffect(() => {
if (events) { if (events) {

View File

@ -5,7 +5,7 @@ import JSMpeg from '@cycjimmy/jsmpeg-player';
export default function JSMpegPlayer({ camera, width, height }) { export default function JSMpegPlayer({ camera, width, height }) {
const playerRef = useRef(); const playerRef = useRef();
const url = `${baseUrl.replace(/^http/, 'ws')}/live/${camera}` const url = `${baseUrl.replace(/^http/, 'ws')}/live/${camera}`;
useEffect(() => { useEffect(() => {
const video = new JSMpeg.VideoElement( const video = new JSMpeg.VideoElement(
@ -16,15 +16,15 @@ export default function JSMpegPlayer({ camera, width, height }) {
); );
const fullscreen = () => { const fullscreen = () => {
if(video.els.canvas.webkitRequestFullScreen) { if (video.els.canvas.webkitRequestFullScreen) {
video.els.canvas.webkitRequestFullScreen(); video.els.canvas.webkitRequestFullScreen();
} }
else { else {
video.els.canvas.mozRequestFullScreen(); video.els.canvas.mozRequestFullScreen();
} }
} };
video.els.canvas.addEventListener('click',fullscreen) video.els.canvas.addEventListener('click',fullscreen);
return () => { return () => {
video.destroy(); video.destroy();

View File

@ -16,18 +16,22 @@ export default function Menu({ className, children, onDismiss, relativeTo, width
) : null; ) : null;
} }
export function MenuItem({ focus, icon: Icon, label, onSelect, value }) { export function MenuItem({ focus, icon: Icon, label, href, onSelect, value, ...attrs }) {
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
onSelect && onSelect(value, label); onSelect && onSelect(value, label);
}, [onSelect, value, label]); }, [onSelect, value, label]);
const Element = href ? 'a' : 'div';
return ( return (
<div <Element
className={`flex space-x-2 p-2 px-5 hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer ${ className={`flex space-x-2 p-2 px-5 hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer ${
focus ? 'bg-gray-200 dark:bg-gray-800 dark:text-white' : '' focus ? 'bg-gray-200 dark:bg-gray-800 dark:text-white' : ''
}`} }`}
href={href}
onClick={handleClick} onClick={handleClick}
role="option" role="option"
{...attrs}
> >
{Icon ? ( {Icon ? (
<div className="w-6 h-6 self-center mr-4 text-gray-500 flex-shrink-0"> <div className="w-6 h-6 self-center mr-4 text-gray-500 flex-shrink-0">
@ -35,7 +39,7 @@ export function MenuItem({ focus, icon: Icon, label, onSelect, value }) {
</div> </div>
) : null} ) : null}
<div className="whitespace-nowrap">{label}</div> <div className="whitespace-nowrap">{label}</div>
</div> </Element>
); );
} }

View File

@ -47,7 +47,7 @@ export function Destination({ className = '', href, text, ...other }) {
const styleProps = { const styleProps = {
[external [external
? 'className' ? className
: 'class']: 'block p-2 text-sm font-semibold text-gray-900 rounded hover:bg-blue-500 dark:text-gray-200 hover:text-white dark:hover:text-white focus:outline-none ring-opacity-50 focus:ring-2 ring-blue-300', : 'class']: 'block p-2 text-sm font-semibold text-gray-900 rounded hover:bg-blue-500 dark:text-gray-200 hover:text-white dark:hover:text-white focus:outline-none ring-opacity-50 focus:ring-2 ring-blue-300',
}; };

View File

@ -7,7 +7,7 @@ import {
parseISO, parseISO,
startOfHour, startOfHour,
differenceInMinutes, differenceInMinutes,
differenceInHours, differenceInHours
} from 'date-fns'; } from 'date-fns';
import ArrowDropdown from '../icons/ArrowDropdown'; import ArrowDropdown from '../icons/ArrowDropdown';
import ArrowDropup from '../icons/ArrowDropup'; import ArrowDropup from '../icons/ArrowDropup';
@ -16,7 +16,7 @@ import Menu from '../icons/Menu';
import MenuOpen from '../icons/MenuOpen'; import MenuOpen from '../icons/MenuOpen';
import { useApiHost } from '../api'; import { useApiHost } from '../api';
export default function RecordingPlaylist({ camera, recordings, selectedDate, selectedHour }) { export default function RecordingPlaylist({ camera, recordings, selectedDate }) {
const [active, setActive] = useState(true); const [active, setActive] = useState(true);
const toggle = () => setActive(!active); const toggle = () => setActive(!active);
@ -33,7 +33,7 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate, se
.slice() .slice()
.reverse() .reverse()
.map((item, i) => ( .map((item, i) => (
<div className="mb-2 w-full"> <div key={i} className="mb-2 w-full">
<div <div
className={`flex w-full text-md text-white px-8 py-2 mb-2 ${ className={`flex w-full text-md text-white px-8 py-2 mb-2 ${
i === 0 ? 'border-t border-white border-opacity-50' : '' i === 0 ? 'border-t border-white border-opacity-50' : ''
@ -50,7 +50,7 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate, se
.slice() .slice()
.reverse() .reverse()
.map((event) => ( .map((event) => (
<EventCard camera={camera} event={event} delay={item.delay} /> <EventCard key={event.id} camera={camera} event={event} delay={item.delay} />
))} ))}
</div> </div>
))} ))}

View File

@ -110,7 +110,7 @@ export default function RelativeModal({
const menu = ( const menu = (
<Fragment> <Fragment>
<div data-testid="scrim" key="scrim" className="absolute inset-0 z-10" onClick={handleDismiss} /> <div data-testid="scrim" key="scrim" className="fixed inset-0 z-10" onClick={handleDismiss} />
<div <div
key="menu" key="menu"
className={`z-10 bg-white dark:bg-gray-700 dark:text-white absolute shadow-lg rounded w-auto h-auto transition-transform transition-opacity duration-75 transform scale-90 opacity-0 overflow-x-hidden overflow-y-auto ${ className={`z-10 bg-white dark:bg-gray-700 dark:text-white absolute shadow-lg rounded w-auto h-auto transition-transform transition-opacity duration-75 transform scale-90 opacity-0 overflow-x-hidden overflow-y-auto ${

View File

@ -4,7 +4,7 @@ import ArrowDropup from '../icons/ArrowDropup';
import Menu, { MenuItem } from './Menu'; import Menu, { MenuItem } from './Menu';
import TextField from './TextField'; import TextField from './TextField';
import DatePicker from './DatePicker'; import DatePicker from './DatePicker';
import Calender from './Calender'; import Calendar from './Calendar';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
export default function Select({ export default function Select({
@ -71,8 +71,8 @@ export default function Select({
}, [type, options, inputOptions, propSelected, setSelected]); }, [type, options, inputOptions, propSelected, setSelected]);
const [focused, setFocused] = useState(null); const [focused, setFocused] = useState(null);
const [showCalender, setShowCalender] = useState(false); const [showCalendar, setShowCalendar] = useState(false);
const calenderRef = useRef(null); const calendarRef = useRef(null);
const ref = useRef(null); const ref = useRef(null);
const handleSelect = useCallback( const handleSelect = useCallback(
@ -80,8 +80,8 @@ export default function Select({
setSelected(options.findIndex(({ value }) => Object.values(propSelected).includes(value))); setSelected(options.findIndex(({ value }) => Object.values(propSelected).includes(value)));
setShowMenu(false); setShowMenu(false);
//show calender date range picker //show calendar date range picker
if (value === 'custom_range') return setShowCalender(true); if (value === 'custom_range') return setShowCalendar(true);
onChange && onChange(value); onChange && onChange(value);
}, },
[onChange, options, propSelected, setSelected] [onChange, options, propSelected, setSelected]
@ -110,7 +110,7 @@ export default function Select({
setSelected(focused); setSelected(focused);
if (options[focused].value === 'custom_range') { if (options[focused].value === 'custom_range') {
setShowMenu(false); setShowMenu(false);
return setShowCalender(true); return setShowCalendar(true);
} }
onChange && onChange(options[focused].value); onChange && onChange(options[focused].value);
@ -184,8 +184,8 @@ export default function Select({
useEffect(() => { useEffect(() => {
const addBackDrop = (e) => { const addBackDrop = (e) => {
if (showCalender && !findDOMNodes(calenderRef.current).contains(e.target)) { if (showCalendar && !findDOMNodes(calendarRef.current).contains(e.target)) {
setShowCalender(false); setShowCalendar(false);
} }
}; };
window.addEventListener('click', addBackDrop); window.addEventListener('click', addBackDrop);
@ -193,7 +193,7 @@ export default function Select({
return function cleanup() { return function cleanup() {
window.removeEventListener('click', addBackDrop); window.removeEventListener('click', addBackDrop);
}; };
}, [showCalender]); }, [showCalendar]);
switch (type) { switch (type) {
case 'datepicker': case 'datepicker':
@ -208,9 +208,9 @@ export default function Select({
trailingIcon={showMenu ? ArrowDropup : ArrowDropdown} trailingIcon={showMenu ? ArrowDropup : ArrowDropdown}
value={datePickerValue} value={datePickerValue}
/> />
{showCalender && ( {showCalendar && (
<Menu className="rounded-t-none" onDismiss={handleDismiss} relativeTo={ref}> <Menu className="rounded-t-none" onDismiss={handleDismiss} relativeTo={ref}>
<Calender onChange={handleDateRange} calenderRef={calenderRef} close={() => setShowCalender(false)} /> <Calendar onChange={handleDateRange} calendarRef={calendarRef} close={() => setShowCalendar(false)} />
</Menu> </Menu>
)} )}
{showMenu ? ( {showMenu ? (
@ -223,7 +223,7 @@ export default function Select({
</Fragment> </Fragment>
); );
// case 'dropdown': // case 'dropdown':
default: default:
return ( return (
<Fragment> <Fragment>

View File

@ -4,14 +4,11 @@ import { useCallback, useState } from 'preact/hooks';
export default function Switch({ checked, id, onChange, label, labelPosition = 'before' }) { export default function Switch({ checked, id, onChange, label, labelPosition = 'before' }) {
const [isFocused, setFocused] = useState(false); const [isFocused, setFocused] = useState(false);
const handleChange = useCallback( const handleChange = useCallback(() => {
(event) => { if (onChange) {
if (onChange) { onChange(id, !checked);
onChange(id, !checked); }
} }, [id, onChange, checked]);
},
[id, onChange, checked]
);
const handleFocus = useCallback(() => { const handleFocus = useCallback(() => {
onChange && setFocused(true); onChange && setFocused(true);

View File

@ -1,12 +1,12 @@
import { Fragment, h } from 'preact'; import { Fragment, h } from 'preact';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { getTimelineEventBlocksFromTimelineEvents } from '../../utils/Timeline/timelineEventUtils'; import { getTimelineEventBlocksFromTimelineEvents } from '../../utils/Timeline/timelineEventUtils';
import { ScrollPermission } from './ScrollPermission'; import type { ScrollPermission } from './ScrollPermission';
import { TimelineBlocks } from './TimelineBlocks'; import { TimelineBlocks } from './TimelineBlocks';
import { TimelineChangeEvent } from './TimelineChangeEvent'; import type { TimelineChangeEvent } from './TimelineChangeEvent';
import { DisabledControls, TimelineControls } from './TimelineControls'; import { DisabledControls, TimelineControls } from './TimelineControls';
import { TimelineEvent } from './TimelineEvent'; import type { TimelineEvent } from './TimelineEvent';
import { TimelineEventBlock } from './TimelineEventBlock'; import type { TimelineEventBlock } from './TimelineEventBlock';
interface TimelineProps { interface TimelineProps {
events: TimelineEvent[]; events: TimelineEvent[];
@ -16,7 +16,7 @@ interface TimelineProps {
} }
export default function Timeline({ events, isPlaying, onChange, onPlayPause }: TimelineProps) { export default function Timeline({ events, isPlaying, onChange, onPlayPause }: TimelineProps) {
const timelineContainerRef = useRef<HTMLDivElement>(undefined); const timelineContainerRef = useRef<HTMLDivElement>(null);
const [timeline, setTimeline] = useState<TimelineEventBlock[]>([]); const [timeline, setTimeline] = useState<TimelineEventBlock[]>([]);
const [disabledControls, setDisabledControls] = useState<DisabledControls>({ const [disabledControls, setDisabledControls] = useState<DisabledControls>({
@ -24,10 +24,10 @@ export default function Timeline({ events, isPlaying, onChange, onPlayPause }: T
next: true, next: true,
previous: false, previous: false,
}); });
const [timelineOffset, setTimelineOffset] = useState<number | undefined>(undefined); const [timelineOffset, setTimelineOffset] = useState<number>(0);
const [markerTime, setMarkerTime] = useState<Date | undefined>(undefined); const [markerTime, setMarkerTime] = useState<Date>(new Date());
const [currentEvent, setCurrentEvent] = useState<TimelineEventBlock | undefined>(undefined); const [currentEvent, setCurrentEvent] = useState<TimelineEventBlock | undefined>(undefined);
const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | undefined>(undefined); const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout>();
const [scrollPermission, setScrollPermission] = useState<ScrollPermission>({ const [scrollPermission, setScrollPermission] = useState<ScrollPermission>({
allowed: true, allowed: true,
resetAfterSeeked: false, resetAfterSeeked: false,
@ -51,7 +51,7 @@ export default function Timeline({ events, isPlaying, onChange, onPlayPause }: T
); );
const scrollToEvent = useCallback( const scrollToEvent = useCallback(
(event, offset = 0) => { (event: TimelineEventBlock, offset = 0) => {
scrollToPosition(event.positionX + offset - timelineOffset); scrollToPosition(event.positionX + offset - timelineOffset);
}, },
[timelineOffset, scrollToPosition] [timelineOffset, scrollToPosition]
@ -137,7 +137,9 @@ export default function Timeline({ events, isPlaying, onChange, onPlayPause }: T
}; };
const waitForSeekComplete = (markerTime: Date) => { const waitForSeekComplete = (markerTime: Date) => {
clearTimeout(scrollTimeout); if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
setScrollTimeout(setTimeout(() => seekCompleteHandler(markerTime), 150)); setScrollTimeout(setTimeout(() => seekCompleteHandler(markerTime), 150));
}; };
@ -161,11 +163,12 @@ export default function Timeline({ events, isPlaying, onChange, onPlayPause }: T
const firstTimelineEventStartTime = firstTimelineEvent.startTime.getTime(); const firstTimelineEventStartTime = firstTimelineEvent.startTime.getTime();
return new Date(firstTimelineEventStartTime + scrollPosition * 1000); return new Date(firstTimelineEventStartTime + scrollPosition * 1000);
} }
return new Date();
}, [timeline, timelineContainerRef]); }, [timeline, timelineContainerRef]);
useEffect(() => { useEffect(() => {
if (timelineContainerRef) { if (timelineContainerRef) {
const timelineContainerWidth = timelineContainerRef.current.offsetWidth; const timelineContainerWidth = timelineContainerRef.current?.offsetWidth || 0;
const offset = Math.round(timelineContainerWidth / 2); const offset = Math.round(timelineContainerWidth / 2);
setTimelineOffset(offset); setTimelineOffset(offset);
} }
@ -180,7 +183,7 @@ export default function Timeline({ events, isPlaying, onChange, onPlayPause }: T
); );
const onPlayPauseHandler = (isPlaying: boolean) => { const onPlayPauseHandler = (isPlaying: boolean) => {
onPlayPause(isPlaying); onPlayPause && onPlayPause(isPlaying);
}; };
const onPreviousHandler = () => { const onPreviousHandler = () => {

View File

@ -1,7 +1,7 @@
import { h } from 'preact'; import { h } from 'preact';
import { useCallback } from 'preact/hooks'; import { useCallback } from 'preact/hooks';
import { getColorFromTimelineEvent } from '../../utils/tailwind/twTimelineEventUtil'; import { getColorFromTimelineEvent } from '../../utils/tailwind/twTimelineEventUtil';
import { TimelineEventBlock } from './TimelineEventBlock'; import type { TimelineEventBlock } from './TimelineEventBlock';
interface TimelineBlockViewProps { interface TimelineBlockViewProps {
block: TimelineEventBlock; block: TimelineEventBlock;

View File

@ -3,7 +3,7 @@ import { useMemo } from 'preact/hooks';
import { findLargestYOffsetInBlocks, getTimelineWidthFromBlocks } from '../../utils/Timeline/timelineEventUtils'; import { findLargestYOffsetInBlocks, getTimelineWidthFromBlocks } from '../../utils/Timeline/timelineEventUtils';
import { convertRemToPixels } from '../../utils/windowUtils'; import { convertRemToPixels } from '../../utils/windowUtils';
import { TimelineBlockView } from './TimelineBlockView'; import { TimelineBlockView } from './TimelineBlockView';
import { TimelineEventBlock } from './TimelineEventBlock'; import type { TimelineEventBlock } from './TimelineEventBlock';
interface TimelineBlocksProps { interface TimelineBlocksProps {
timeline: TimelineEventBlock[]; timeline: TimelineEventBlock[];
@ -36,11 +36,12 @@ export const TimelineBlocks = ({ timeline, firstBlockOffset, onEventClick }: Tim
...block, ...block,
yOffset: block.yOffset + timelineBlockOffset, yOffset: block.yOffset + timelineBlockOffset,
}; };
return <TimelineBlockView block={updatedBlock} onClick={onClickHandler} />; return <TimelineBlockView key={block.id} block={updatedBlock} onClick={onClickHandler} />;
})} })}
</div> </div>
); );
} }
return <div />
}, [timeline, onEventClick, firstBlockOffset]); }, [timeline, onEventClick, firstBlockOffset]);
return timelineEventBlocks; return timelineEventBlocks;

View File

@ -1,7 +1,7 @@
import { TimelineEvent } from './TimelineEvent'; import type { TimelineEvent } from './TimelineEvent';
export interface TimelineChangeEvent { export interface TimelineChangeEvent {
timelineEvent: TimelineEvent; timelineEvent?: TimelineEvent;
markerTime: Date; markerTime: Date;
seekComplete: boolean; seekComplete: boolean;
} }

View File

@ -1,4 +1,4 @@
import { TimelineEvent } from './TimelineEvent'; import type { TimelineEvent } from './TimelineEvent';
export interface TimelineEventBlock extends TimelineEvent { export interface TimelineEventBlock extends TimelineEvent {
index: number; index: number;

View File

@ -1,6 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
import { render, screen } from '@testing-library/preact'; import { render, screen } from 'testing-library';
describe('ActivityIndicator', () => { describe('ActivityIndicator', () => {
test('renders an ActivityIndicator with default size md', async () => { test('renders an ActivityIndicator with default size md', async () => {

View File

@ -1,7 +1,7 @@
import { h } from 'preact'; import { h } from 'preact';
import { DrawerProvider } from '../../context'; import { DrawerProvider } from '../../context';
import AppBar from '../AppBar'; import AppBar from '../AppBar';
import { fireEvent, render, screen } from '@testing-library/preact'; import { fireEvent, render, screen } from 'testing-library';
import { useRef } from 'preact/hooks'; import { useRef } from 'preact/hooks';
function Title() { function Title() {

View File

@ -1,6 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import AutoUpdatingCameraImage from '../AutoUpdatingCameraImage'; import AutoUpdatingCameraImage from '../AutoUpdatingCameraImage';
import { screen, render } from '@testing-library/preact'; import { screen, render } from 'testing-library';
let mockOnload; let mockOnload;
jest.mock('../CameraImage', () => { jest.mock('../CameraImage', () => {
@ -34,9 +34,9 @@ describe('AutoUpdatingCameraImage', () => {
test('on load, sets a new cache key to search params', async () => { test('on load, sets a new cache key to search params', async () => {
dateNowSpy.mockReturnValueOnce(100).mockReturnValueOnce(200).mockReturnValueOnce(300); dateNowSpy.mockReturnValueOnce(100).mockReturnValueOnce(200).mockReturnValueOnce(300);
render(<AutoUpdatingCameraImage camera="tacos" searchParams="foo" />); render(<AutoUpdatingCameraImage camera="front" searchParams="foo" />);
mockOnload(); mockOnload();
jest.runAllTimers(); await screen.findByText('cache=100&foo');
expect(screen.queryByText('cache=100&foo')).toBeInTheDocument(); expect(screen.getByText('cache=100&foo')).toBeInTheDocument();
}); });
}); });

View File

@ -1,6 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import Button from '../Button'; import Button from '../Button';
import { render, screen } from '@testing-library/preact'; import { render, screen } from 'testing-library';
describe('Button', () => { describe('Button', () => {
test('renders children', async () => { test('renders children', async () => {

View File

@ -1,15 +1,10 @@
import { h } from 'preact'; import { h } from 'preact';
import * as Api from '../../api';
import * as Hooks from '../../hooks'; import * as Hooks from '../../hooks';
import CameraImage from '../CameraImage'; import CameraImage from '../CameraImage';
import { render, screen } from '@testing-library/preact'; import { render, screen } from 'testing-library';
describe('CameraImage', () => { describe('CameraImage', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(Api, 'useConfig').mockImplementation(() => {
return { data: { cameras: { front: { name: 'front', detect: { width: 1280, height: 720 } } } } };
});
jest.spyOn(Api, 'useApiHost').mockReturnValue('http://base-url.local:5000');
jest.spyOn(Hooks, 'useResizeObserver').mockImplementation(() => [{ width: 0 }]); jest.spyOn(Hooks, 'useResizeObserver').mockImplementation(() => [{ width: 0 }]);
}); });
@ -17,24 +12,4 @@ describe('CameraImage', () => {
render(<CameraImage camera="front" />); render(<CameraImage camera="front" />);
expect(screen.queryByLabelText('Loading…')).toBeInTheDocument(); expect(screen.queryByLabelText('Loading…')).toBeInTheDocument();
}); });
test('creates a scaled canvas using the available width & height, preserving camera aspect ratio', async () => {
jest.spyOn(Hooks, 'useResizeObserver').mockReturnValueOnce([{ width: 720 }]);
render(<CameraImage camera="front" />);
expect(screen.queryByLabelText('Loading…')).toBeInTheDocument();
const canvas = screen.queryByTestId('cameraimage-canvas');
expect(canvas).toHaveAttribute('height', '405');
expect(canvas).toHaveAttribute('width', '720');
});
test('allows camera image to stretch to available space', async () => {
jest.spyOn(Hooks, 'useResizeObserver').mockReturnValueOnce([{ width: 1400 }]);
render(<CameraImage camera="front" stretch />);
expect(screen.queryByLabelText('Loading…')).toBeInTheDocument();
const canvas = screen.queryByTestId('cameraimage-canvas');
expect(canvas).toHaveAttribute('height', '787');
expect(canvas).toHaveAttribute('width', '1400');
});
}); });

View File

@ -1,6 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import Card from '../Card'; import Card from '../Card';
import { render, screen } from '@testing-library/preact'; import { render, screen } from 'testing-library';
describe('Card', () => { describe('Card', () => {
test('renders a Card with media', async () => { test('renders a Card with media', async () => {

View File

@ -1,6 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import Dialog from '../Dialog'; import Dialog from '../Dialog';
import { render, screen } from '@testing-library/preact'; import { render, screen } from 'testing-library';
describe('Dialog', () => { describe('Dialog', () => {
let portal; let portal;

View File

@ -1,6 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import Heading from '../Heading'; import Heading from '../Heading';
import { render, screen } from '@testing-library/preact'; import { render, screen } from 'testing-library';
describe('Heading', () => { describe('Heading', () => {
test('renders content with default size', async () => { test('renders content with default size', async () => {

View File

@ -1,6 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import Link from '../Link'; import Link from '../Link';
import { render, screen } from '@testing-library/preact'; import { render, screen } from 'testing-library';
describe('Link', () => { describe('Link', () => {
test('renders a link', async () => { test('renders a link', async () => {

View File

@ -1,6 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import Menu, { MenuItem } from '../Menu'; import Menu, { MenuItem } from '../Menu';
import { fireEvent, render, screen } from '@testing-library/preact'; import { fireEvent, render, screen } from 'testing-library';
import { useRef } from 'preact/hooks'; import { useRef } from 'preact/hooks';
describe('Menu', () => { describe('Menu', () => {

View File

@ -1,7 +1,7 @@
import { h } from 'preact'; import { h } from 'preact';
import * as Context from '../../context'; import * as Context from '../../context';
import NavigationDrawer, { Destination } from '../NavigationDrawer'; import NavigationDrawer, { Destination } from '../NavigationDrawer';
import { fireEvent, render, screen } from '@testing-library/preact'; import { fireEvent, render, screen } from 'testing-library';
describe('NavigationDrawer', () => { describe('NavigationDrawer', () => {
let useDrawer, setShowDrawer; let useDrawer, setShowDrawer;
@ -49,6 +49,7 @@ describe('Destination', () => {
}); });
test('dismisses the drawer moments after being clicked', async () => { test('dismisses the drawer moments after being clicked', async () => {
jest.useFakeTimers();
render( render(
<NavigationDrawer> <NavigationDrawer>
<Destination href="/tacos" text="Tacos" /> <Destination href="/tacos" text="Tacos" />

View File

@ -1,6 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import Prompt from '../Prompt'; import Prompt from '../Prompt';
import { fireEvent, render, screen } from '@testing-library/preact'; import { fireEvent, render, screen } from 'testing-library';
describe('Prompt', () => { describe('Prompt', () => {
let portal; let portal;
@ -16,7 +16,7 @@ describe('Prompt', () => {
}); });
test('renders to a portal', async () => { test('renders to a portal', async () => {
render(<Prompt title='Tacos' text='This is the dialog' />); render(<Prompt title="Tacos" text="This is the dialog" />);
expect(screen.getByText('Tacos')).toBeInTheDocument(); expect(screen.getByText('Tacos')).toBeInTheDocument();
expect(screen.getByRole('modal').closest('#dialogs')).not.toBeNull(); expect(screen.getByRole('modal').closest('#dialogs')).not.toBeNull();
}); });
@ -29,7 +29,7 @@ describe('Prompt', () => {
{ color: 'red', text: 'Delete' }, { color: 'red', text: 'Delete' },
{ text: 'Okay', onClick: handleClick }, { text: 'Okay', onClick: handleClick },
]} ]}
title='Tacos' title="Tacos"
/> />
); );
fireEvent.click(screen.getByRole('button', { name: 'Okay' })); fireEvent.click(screen.getByRole('button', { name: 'Okay' }));

View File

@ -1,7 +1,7 @@
import { h, createRef } from 'preact'; import { h, createRef } from 'preact';
import RelativeModal from '../RelativeModal'; import RelativeModal from '../RelativeModal';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { fireEvent, render, screen } from '@testing-library/preact'; import { fireEvent, render, screen } from 'testing-library';
describe('RelativeModal', () => { describe('RelativeModal', () => {
test('keeps tab focus', async () => { test('keeps tab focus', async () => {

View File

@ -1,6 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import Select from '../Select'; import Select from '../Select';
import { fireEvent, render, screen } from '@testing-library/preact'; import { fireEvent, render, screen } from 'testing-library';
describe('Select', () => { describe('Select', () => {
test('on focus, shows a menu', async () => { test('on focus, shows a menu', async () => {

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