diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73d49a3a..6127955a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,26 +9,34 @@ on: required: true jobs: - update-master-branch: + update-default-branch: runs-on: ubuntu-latest steps: - - uses: everlytic/branch-merge@1.1.5 + - name: Checkout master + uses: actions/checkout@v6 with: - github_token: ${{ secrets.PAT }} - source_ref: 'develop' - target_branch: 'master' - commit_message_template: '[Automated] Merged {source_ref} into target {target_branch}' + ref: 'v5' + token: ${{ secrets.PAT }} + fetch-depth: 0 + + - name: Fast-forward merge from develop + shell: bash + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git merge --ff-only origin/develop + git push origin v5 release: - needs: [ 'update-master-branch' ] + needs: [ 'update-default-branch' ] runs-on: ubuntu-latest permissions: contents: write steps: - name: Fetch repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: - ref: 'master' + ref: 'v5' fetch-depth: 0 - name: Get latest tag @@ -58,9 +66,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: - ref: 'master' + ref: 'v5' token: ${{ secrets.PAT }} fetch-depth: 0 @@ -78,14 +86,26 @@ jobs: env: OUTPUT: ${{ github.workspace }}/CHANGELOG.md - - uses: stefanzweifel/git-auto-commit-action@v4 + - uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: 'docs(changelog): update changelog' file_pattern: CHANGELOG.md - - uses: everlytic/branch-merge@1.1.5 + update-develop-branch: + needs: ['update-changelog'] + runs-on: ubuntu-latest + steps: + - name: Checkout master + uses: actions/checkout@v6 with: - github_token: ${{ secrets.PAT }} - source_ref: 'master' - target_branch: 'develop' - commit_message_template: '[Automated] Merged {source_ref} into target {target_branch}' + ref: 'develop' + token: ${{ secrets.PAT }} + fetch-depth: 0 + + - name: Fast-forward merge from v5 + shell: bash + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git merge --ff-only origin/v5 + git push origin develop diff --git a/.gitignore b/.gitignore index e4c2fd4f..a3aca102 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ leftover* # tmp file workaround lost* -# ignore bin paths +# Ignore bin paths bin/ustreamer bin/camera-streamer @@ -28,5 +28,5 @@ bin/rtsp-simple-server/*.yml tools/config.local tools/.config -# Ignore pkglist -tools/pkglist.sh +# Ignore pycache +**/__pycache__/ diff --git a/Makefile b/Makefile index 38b82bfe..f24dc1a1 100644 --- a/Makefile +++ b/Makefile @@ -21,12 +21,6 @@ install: ## Install crowsnest (needs leading sudo) uninstall: ## Uninstall crowsnest @bash -c 'tools/uninstall.sh' -build: ## Compile backends / streamer - bash -c 'bin/build.sh --build' - -buildclean: ## Clean backends / streamer (for rebuilding) - bash -c 'bin/build.sh --clean' - clean: ## Clean .config @if [ -f tools/.config ]; then rm -f tools/.config; fi @printf "Removed installer config file ...\n" @@ -41,38 +35,7 @@ help: ## Shows this help update: ## Update crowsnest (fetches and pulls repository changes) @git fetch && git pull - @bash -c 'bin/build.sh --reclone' - ${MAKE} build - -upgrade: ## Upgrade crowsnest from v4 to v5 - @sudo -v || (printf "Sudo privileges required for upgrade\n" && exit 1) - @printf "Backup crowsnest.conf and moonraker.conf, then migrate them ...\n" - @MIGRATED_PATH="$$(bash -c 'tools/migrate_configs.sh')"; \ - if [ -n "$$MIGRATED_PATH" ]; then \ - printf "%s\n" "$$MIGRATED_PATH" > tools/.migrated_conf_path; \ - printf "Saved migrated config path to tools/.migrated_conf_path\n"; \ - else \ - printf "No migrated config path returned\n"; \ - fi - @printf "Uninstalling crowsnest v4 ...\n" - @yes | bash -c 'tools/uninstall.sh' - @rm -rf bin/ustreamer bin/camera-streamer - @printf "Updating repository ...\n" - @git fetch --all - @git switch v5 - @git pull origin v5 - @printf "Installing crowsnest v5 ...\n" - @sudo env CROWSNEST_SKIP_REBOOT_PROMPT=1 bash -c 'tools/install.sh' - @printf "Restoring migrated crowsnest.conf ...\n" - @if [ -f tools/.migrated_conf_path ]; then \ - MIGRATED_PATH="$$(cat tools/.migrated_conf_path)"; \ - ORIG_PATH="$${MIGRATED_PATH%.v5}"; \ - printf "Restoring %s -> %s\n" "$$MIGRATED_PATH" "$$ORIG_PATH"; \ - mv "$$MIGRATED_PATH" "$$ORIG_PATH"; \ - rm -f tools/.migrated_conf_path; \ - else \ - printf "No migrated config to restore\n"; \ - fi + @sudo bash -c 'tools/libs/manage_apps.sh --reinstall' report: ## Generate report.txt @if [ -f ~/report.txt ]; then rm -f ~/report.txt; fi diff --git a/README.md b/README.md index e726511e..b7ae0a15 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ # crowsnest -A wrapper script for webcam streaming on Raspberry Pi OS Lite images like [MainsailOS](https://github.com/mainsail-crew/MainsailOS). Mainly written in bash. +A wrapper script for webcam streaming on Debian based images, especially for Raspberry Pi OS Lite images like [MainsailOS](https://github.com/mainsail-crew/MainsailOS). Mainly written in Python. - [Why is it called crowsnest?](#why-is-it-called-crowsnest) - [Support](#support) @@ -18,7 +18,6 @@ A wrapper script for webcam streaming on Raspberry Pi OS Lite images like [Mains - [Compatibility](#compatibility) - [Contribute](#contribute) - [How to support us?](#how-to-support-us) -- [CustomPiOS-module](#custompios-module) - [What 'Backends' does crowsnest use](#what-backends-does-crowsnest-use) - [Credits](#credits) @@ -39,7 +38,7 @@ So, this will be the 'lookout point' for your printer. Please read carefully on [how to configure](https://docs.mainsail.xyz/crowsnest/configuration/cam-section/) crowsnest to your needs! Check out the [help](https://docs.mainsail.xyz/getting-help/) section for first aid or join our [Discord](https://discord.gg/mainsail) server if you need further help. For some topics that are not covered in the documentation, just read below. -_**PS: Do not open issues that are based on misconfiguration! This makes it harder for me to keep track of problems in my code.**_ +_**PS: Do not open issues that are based on misconfiguration! The issue tracker is only there to track actual bugs in the code and feature requests.**_ --- @@ -53,71 +52,32 @@ If there is something in our documentation that is not covered, is described in ## Compatibility -Tested on the following distributions: +To be able to use all features and streamers you need a Raspberry Pi with a Raspberry Pi OS based image with at least Python 3.10. -**Legend:** \ -Tested and work as intended: :heavy_check_mark: \ -Tested and/or did not work: :x: \ -Should work but not tested: :question: \ -Not available: :heavy_minus_sign: +For other systems we recommend Debian based images, as they come with `bash` and the `apt` package manager that we rely on for installation. -| Operating System | X86 Architecture | ARM Architecture | -| :-------------------------------: | :----------------: | :-----------------------------------------: | -| Raspberry Pi OS (buster) | :heavy_minus_sign: | :x: ([Hint](#hint-buster)) | -| Raspberry Pi OS (bullseye) | :heavy_minus_sign: | :heavy_check_mark: | -| Raspberry Pi OS (bookworm) | :heavy_minus_sign: | :heavy_check_mark: ([Hint](#hint-bookworm)) | -| MainsailOS (<0.7.1) | :heavy_minus_sign: | :x: ([Hint](#hint-buster)) | -| MainsailOS (>1.0.0) | :heavy_minus_sign: | :heavy_check_mark: (rpi) | -| Armbian | :question: | :heavy_check_mark: | -| DietPi | :question: | :heavy_check_mark: (rpi) | -| Ubuntu Server 20.04 | :heavy_check_mark: | :question: | -| Ubuntu Server 22.04 LTS | :heavy_check_mark: | :heavy_check_mark: (rpi) | -| Ubuntu Server 23.10 | :question: | :heavy_check_mark: (rpi) | -| Linux Mint 21 (codename: vanessa) | :heavy_check_mark: | :question: | -| Archlinux (and derivatives) | :x: | :x: | -| Alpine Linux | :x: | :x: | - -_If you test that on other distributions, feel free to open a Pull Request to enhance documentation._ - -#### Hint Buster - -OS images that are based on Debian 10 (codename 'buster') are no longer supported with Crowsnest version 4 (current `master` branch)! - -Please use the `legacy/v3` branch for these OS types.\ -See the [README.md](https://github.com/mainsail-crew/crowsnest/tree/legacy/v3) of this branch for usage instructions. - -#### Hint Bookworm - -Raspberry Pi OS images that are based on Debian 12 (codename 'bookworm') currently use a different branch of [camera-streamer](https://github.com/ayufan/camera-streamer)! -The Raspberry Pi 5 has only [ustreamer](https://github.com/pikvm/ustreamer) support! This is due to the missing JPEG and H264 Hardware encoders. +Non Debian based images are not officially supported and never will be. --- ## Contribute -1. Create an Issue related to your topic. -2. Prepare an _tested_ Pull Request against the `develop` branch +1. Create an [Issue](https://github.com/mainsail-crew/crowsnest/issues) related to your topic. +2. Prepare a _tested_ Pull Request against the `develop` branch - Please use commits formatted according to [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - Make sure you do not ignore the code formatting as provided by the [_.editorconfig_](.editorconfig) of this repo -3. Be patient. Every PR has to go through some sort of "internal gates" before it reaches the master branch, unless an immediate response is crucial. +3. Be patient. Every PR has to go through some sort of "internal gates" before it reaches the current default branch, unless an immediate response is crucial. --- ## How to support us -Buy [KwadFan](https://github.com/KwadFan) a coffee at [ko-fi.com](https://ko-fi.com/KwadFan) or [support the mainsail project](https://docs.mainsail.xyz/sponsors/) +[Support the mainsail project](https://docs.mainsail.xyz/sponsors) Please consider hitting the :star: button in the upper right hand corner to show some love for this project. --- -## CustomPIOS Module - -I have decided to provide a [CustomPiOS Module](https://github.com/guysoft/CustomPiOS) to make it easier to integrate with other distributions like MainsailOS or similar. \ -Please see [README.md](./custompios/README.md) in the module folder for more information. - ---- - ## What 'Backends' does crowsnest use? Please see the according [backends](https://docs.mainsail.xyz/crowsnest/faq/backends/) section in our documentation. @@ -126,15 +86,12 @@ Please see the according [backends](https://docs.mainsail.xyz/crowsnest/faq/back ## Credits -I want to give a huge shoutout to [_lixxbox_](https://github.com/lixxbox) and [_alexz_](https://github.com/zellneralex) from the [mainsail-crew](https://github.com/orgs/mainsail-crew/people). \ +A huge thank you to [_KwadFan_](https://github.com/KwadFan/) for the [original bash implementation](https://github.com/mainsail-crew/v4), and a huge shoutout to [_lixxbox_](https://github.com/lixxbox) and [_alexz_](https://github.com/zellneralex) from the [mainsail-crew](https://github.com/orgs/mainsail-crew/people), who gave KwadFan ideas for improvements and tested the original code. \ Without these guys it simply were not possible to get that done. -They both mentioned improvements and tested a heck out of there machines to get this all functioning well. \ -Thank you, mates :) Proud to be a part of. - Thanks to [Pedro Lamas](https://github.com/pedrolamas), for the ISSUE_TEMPLATES. -Thanks to [ayufan](https://github.com/ayufan) for keep going on camera-streamer, even I stressed him to get rid of some bugs ;) +Thanks to [ayufan](https://github.com/ayufan) for helping with the original camera-streamer implementation. --- diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 00000000..0ee2df2a --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,4 @@ +# Ignore every file in this folder +* +# Except this one +!.gitignore diff --git a/bin/build.sh b/bin/build.sh deleted file mode 100755 index d7954907..00000000 --- a/bin/build.sh +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/env bash - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 - 2024 -#### Co-authored by Patrick Gehrsitz aka mryel00 -#### Copyright 2024 - till today -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -### Disable SC2317 due Trap usage -# shellcheck disable=SC2317,SC2329 - -# Exit on errors -set -Ee - -# Debug -# set -x - -# Global vars -# Base Path -BASE_CN_BIN_PATH="$(dirname "$(readlink -f "${0}")")" - -# Clone Flags -CLONE_FLAGS=(--depth=1 --single-branch) - -# Ustreamer repo -USTREAMER_PATH="ustreamer" -if [[ -z "${CROWSNEST_USTREAMER_REPO_SHIP}" ]]; then - CROWSNEST_USTREAMER_REPO_SHIP="https://github.com/pikvm/ustreamer.git" -fi -if [[ -z "${CROWSNEST_USTREAMER_REPO_BRANCH}" ]]; then - CROWSNEST_USTREAMER_REPO_BRANCH="v6.36" -fi - -# Camera-streamer repo -CSTREAMER_PATH="camera-streamer" -if [[ -z "${CROWSNEST_CAMERA_STREAMER_REPO_SHIP}" ]]; then - CROWSNEST_CAMERA_STREAMER_REPO_SHIP="https://github.com/mryel00/camera-streamer.git" -fi -if [[ -z "${CROWSNEST_CAMERA_STREAMER_REPO_BRANCH}" ]]; then - CROWSNEST_CAMERA_STREAMER_REPO_BRANCH="main" -fi - - -# Paths of repos -ALL_PATHS=( - "${BASE_CN_BIN_PATH}"/"${USTREAMER_PATH}" - "${BASE_CN_BIN_PATH}"/"${CSTREAMER_PATH}" -) - -# Helper messages -show_help() { - printf "Usage %s [options]\n" "$(basename "${0}")" - printf "\t-h or --help\t\tShows this help message\n" - printf "\t-b or --build\t\tBuild Apps\n" - printf "\t-c or --clean\t\tClean Apps\n" - printf "\t-d or --delete\t\tDelete cloned Apps\n" - printf "\t-r or --reclone\t\tClone Apps again\n\n" -} - -## Helper funcs -# shellcheck source=./libs/helper_fn.sh -. "${BASE_CN_BIN_PATH}/../libs/helper_fn.sh" - -### Get avail mem -get_avail_mem() { - grep "MemTotal" /proc/meminfo | awk '{print $2}' -} - -## MAIN funcs -### Delete repo folder -delete_apps() { - for path in "${ALL_PATHS[@]}"; do - if [[ ! -d "${path}" ]]; then - printf "'%s' does not exist! Delete ... [SKIPPED]\n" "${path}" - fi - if [[ -d "${path}" ]]; then - printf "Deleting '%s' ... [DONE]\n" "${path}" - rm -rf "${path}" - fi - done -} - -### Clone ustreamer -clone_ustreamer() { - if [[ -d "${BASE_CN_BIN_PATH}"/"${USTREAMER_PATH}" ]]; then - printf "%s already exist ... [SKIPPED]\n" "${USTREAMER_PATH}" - return - fi - git clone "${CROWSNEST_USTREAMER_REPO_SHIP}" \ - -b "${CROWSNEST_USTREAMER_REPO_BRANCH}" \ - "${BASE_CN_BIN_PATH}"/"${USTREAMER_PATH}" \ - "${CLONE_FLAGS[@]}" -} - -### Clone camera-streamer -clone_cstreamer() { - ## Special handling because only supported on Raspberry Pi - [[ -n "${CROWSNEST_UNATTENDED}" ]] || CROWSNEST_UNATTENDED="0" - if [[ "$(use_cs)" = "0" ]] && - [[ "${CROWSNEST_UNATTENDED}" = "0" ]]; then - printf "Device is not supported! Cloning camera-streamer ... [SKIPPED]\n" - return - fi - if [[ -d "${BASE_CN_BIN_PATH}"/"${CSTREAMER_PATH}" ]]; then - printf "%s already exist ... [SKIPPED]\n" "${CSTREAMER_PATH}" - return - fi - if [[ "$(is_os_release "bullseye")" = "1" ]]; then - printf "\nBullseye detected!\n" - printf "Using legacy/bullseye branch of camera-streamer for Bullseye ...\n\n" - CROWSNEST_CAMERA_STREAMER_REPO_BRANCH="legacy/bullseye" - fi - git clone "${CROWSNEST_CAMERA_STREAMER_REPO_SHIP}" \ - -b "${CROWSNEST_CAMERA_STREAMER_REPO_BRANCH}" \ - "${BASE_CN_BIN_PATH}"/"${CSTREAMER_PATH}" \ - "${CLONE_FLAGS[@]}" --recurse-submodules --shallow-submodules -} - -### Clone Apps -clone_apps() { - local apps - apps="ustreamer cstreamer" - for app in ${apps}; do - clone_"${app}" - done -} - -### Run 'make clean' in cloned folders -clean_apps() { - for app in "${ALL_PATHS[@]}"; do - if [[ ! -d "${app}" ]]; then - printf "'%s' does not exist! Clean ... [SKIPPED]\n" "${app}" - else - printf "\nRunning 'make clean' in %s ... \n" "${app}" - pushd "${app}" &> /dev/null || exit 1 - make clean - popd &> /dev/null || exit 1 - fi - done - printf "\nRunning 'make clean' ... [DONE]\n" -} - -build_apps() { - ## Determine Ramsize and export MAKEFLAG - if [[ "$(get_avail_mem)" -le 524288 ]]; then - USE_PROCS=-j1 - elif [[ "$(get_avail_mem)" -le 1048576 ]]; then - USE_PROCS=-j2 - else - USE_PROCS=-j4 - fi - - for path in "${ALL_PATHS[@]}"; do - if [[ ! -d "${path}" ]]; then - printf "'%s' does not exist! Build skipped ... [WARN]\n" "${path}" - fi - if [[ -d "${path}" ]]; then - printf "Build '%s' using ${USE_PROCS##-j} Cores ... \n" "${path##*/}" - pushd "${path}" &> /dev/null || exit 1 - make "${USE_PROCS}" - popd &> /dev/null || exit 1 - printf "Build '%s' ... [SUCCESS]\n" "${path##*/}" - fi - done -} - -## MAIN FUNC -main() { - ## Error exit if no args given, show help - if [[ $# -eq "0" ]]; then - printf "ERROR: No options given ...\n" - show_help - exit 1 - fi - ## Error exit if too many args given - if [[ $# -gt "1" ]]; then - printf "ERROR: Too many options given ...\n" - show_help - exit 1 - fi - ## Get opts - while true; do - case "${1}" in - -b|--build) - build_apps - break - ;; - -c|--clean) - clean_apps - break - ;; - -d|--delete) - delete_apps - break - ;; - -h|--help) - show_help - break - ;; - -r|--reclone) - delete_apps - clone_apps - break - ;; - *) - printf "Unknown option: %s" "${1}" - show_help - break - ;; - esac - done -} - -#### MAIN -main "${@}" -exit 0 - -#### EOF diff --git a/crowsnest b/crowsnest deleted file mode 100755 index 39669681..00000000 --- a/crowsnest +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -#### Webcamd Core Application. - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit upon Errors -set -Ee - -# Base Path -BASE_CN_PATH="$(dirname "$(readlink -f "${0}")")" - -## Import Librarys -# shellcheck source-path=SCRIPTDIR/../libs/ -. "${BASE_CN_PATH}/libs/camera-streamer.sh" -. "${BASE_CN_PATH}/libs/configparser.sh" -. "${BASE_CN_PATH}/libs/core.sh" -. "${BASE_CN_PATH}/libs/hwhandler.sh" -. "${BASE_CN_PATH}/libs/init_stream.sh" -. "${BASE_CN_PATH}/libs/logging.sh" -. "${BASE_CN_PATH}/libs/messages.sh" -. "${BASE_CN_PATH}/libs/ustreamer.sh" -. "${BASE_CN_PATH}/libs/v4l2_control.sh" -. "${BASE_CN_PATH}/libs/versioncontrol.sh" -. "${BASE_CN_PATH}/libs/watchdog.sh" - -#### MAIN -## Args given? -if [ "$#" -eq 0 ]; then - missing_args_msg - exit 1 -fi - -## Parse Args -while getopts ":vhc:s:d" arg; do - case "${arg}" in - v ) - echo -e "\ncrowsnest Version: $(self_version)\n" - exit 0 - ;; - h ) - help_msg - exit 0 - ;; - c ) - check_cfg "${OPTARG}" - export CROWSNEST_CFG="${OPTARG}" - ;; - s ) - if [[ "$(awk '{print $1}' /proc/uptime | cut -d '.' -f 1)" -lt "120" ]]; then - if [[ "${OPTARG}" ]]; then - sleep "${OPTARG}" - else - sleep 5 - fi - fi - ;; - d ) - set -x - ;; - \?) - wrong_args_msg - exit 1 - ;; - esac -done - -init_logging -initial_check -construct_streamer - -## Loop and Watchdog -## In this case watchdog acts more like a "cable defect detector" -## The User gets a message if Device is lost. -clean_watchdog -while true ; do - crowsnest_watchdog - sleep 120 & sleep_pid="$!" - wait "${sleep_pid}" -done diff --git a/crowsnest/__init__.py b/crowsnest/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/crowsnest/__main__.py b/crowsnest/__main__.py new file mode 100644 index 00000000..a370b841 --- /dev/null +++ b/crowsnest/__main__.py @@ -0,0 +1,8 @@ +#!/usr/bin/python3 + +import asyncio + +from crowsnest.crowsnest import main + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/crowsnest/camera/__init__.py b/crowsnest/camera/__init__.py new file mode 100644 index 00000000..7c3526e5 --- /dev/null +++ b/crowsnest/camera/__init__.py @@ -0,0 +1,5 @@ +from . import camera_manager +from .camera import Camera +from .types.legacy import Legacy +from .types.libcamera import Libcamera +from .types.uvc import UVC diff --git a/crowsnest/camera/camera.py b/crowsnest/camera/camera.py new file mode 100644 index 00000000..fc238a1d --- /dev/null +++ b/crowsnest/camera/camera.py @@ -0,0 +1,34 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import os +from abc import ABC, abstractmethod + + +class Camera(ABC): + def __init__(self, path: str, *args, **kwargs) -> None: + self.path = path + self.control_values = {} + self.formats = {} + + def path_equals(self, path: str) -> bool: + return self.path == os.path.realpath(path) + + @abstractmethod + def get_formats_string(self) -> str: + pass + + @abstractmethod + def get_controls_string(self) -> str: + pass + + @staticmethod + @abstractmethod + def init_camera_type() -> list: + pass diff --git a/crowsnest/camera/camera_manager.py b/crowsnest/camera/camera_manager.py new file mode 100644 index 00000000..e851f698 --- /dev/null +++ b/crowsnest/camera/camera_manager.py @@ -0,0 +1,24 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +from typing import List, Optional + +from .camera import Camera + +_cameras: List[Camera] = [] + + +def get_cam_by_path(path: str) -> Optional[Camera]: + return next((cam for cam in _cameras if cam.path_equals(path)), None) + + +def init_camera_type(obj: Camera) -> List[Camera]: + cams = obj.init_camera_type() + _cameras.extend(cams) + return cams diff --git a/crowsnest/camera/types/legacy.py b/crowsnest/camera/types/legacy.py new file mode 100644 index 00000000..c1313a8d --- /dev/null +++ b/crowsnest/camera/types/legacy.py @@ -0,0 +1,20 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +from ... import v4l2 +from . import uvc + + +class Legacy(uvc.UVC): + @staticmethod + def init_camera_type() -> list: + legacy_path = v4l2.ctl.get_dev_path_by_name("mmal") + if not legacy_path: + return [] + return [Legacy(legacy_path)] diff --git a/crowsnest/camera/types/libcamera.py b/crowsnest/camera/types/libcamera.py new file mode 100644 index 00000000..c90515e2 --- /dev/null +++ b/crowsnest/camera/types/libcamera.py @@ -0,0 +1,92 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import re +import shutil + +from ... import utils +from .. import camera + + +class Libcamera(camera.Camera): + def __init__(self, path, *args, **kwargs) -> None: + super().__init__(path, *args, **kwargs) + self.control_values = self._get_controls() + self.formats = [] + + def _get_controls(self) -> dict: + ctrls = {} + try: + from libcamera import CameraManager, Rectangle + except ImportError: + return ctrls + + def parse_value(rectangle): + if isinstance(rectangle, Rectangle): + return (rectangle.x, rectangle.y, rectangle.width, rectangle.height) + return rectangle + + libcam_cm = CameraManager.singleton() + cam = next((cam for cam in libcam_cm.cameras if cam.id == self.path), None) + if cam is None: + return ctrls + for k, v in cam.controls.items(): + ctrls[k.name] = { + "min": parse_value(v.min), + "max": parse_value(v.max), + "default": parse_value(v.default), + } + return ctrls + + def _get_formats(self, libcamera_output: str) -> list: + resolutions = re.findall( + rf"{self.path}.*?:.*?: (.*?)(?=\n\n|\n *')", + libcamera_output, + flags=re.DOTALL, + ) + res = [] + if resolutions: + res = [r.strip() for r in resolutions[0].split("\n")] + return res + + def get_formats_string(self) -> str: + message = "" + for res in self.formats: + message += f"{res}\n" + return message[:-1] + + def get_controls_string(self) -> str: + if not self.control_values: + return ( + "apt package 'python3-libcamera' is not installed! " + "Make sure to install it to log the controls!" + ) + message = "" + for name, value in self.control_values.items(): + min, max, default = value.values() + str_first = f"{name} ({self.get_type_str(min)})" + str_indent = (30 - len(str_first)) * " " + ": " + str_second = f"min={min} max={max} default={default}" + message += str_first + str_indent + str_second + "\n" + return message.strip() + + def get_type_str(self, obj) -> str: + return str(type(obj)).split("'")[1] + + @staticmethod + def init_camera_type() -> list: + cmd = shutil.which("rpicam-hello") or shutil.which("libcamera-hello") + if not cmd: + return [] + libcam_cmd = f"{cmd} --list-cameras" + libcam = utils.execute_shell_command(libcam_cmd, strip=False) + cams = [Libcamera(path) for path in re.findall(r"\((/base.*?)\)", libcam)] + for cam in cams: + cam.formats = cam._get_formats(libcam) + return cams diff --git a/crowsnest/camera/types/uvc.py b/crowsnest/camera/types/uvc.py new file mode 100644 index 00000000..adb9be78 --- /dev/null +++ b/crowsnest/camera/types/uvc.py @@ -0,0 +1,111 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import os + +from ... import logger, v4l2 +from .. import camera + + +class UVC(camera.Camera): + def __init__(self, path: str, *args, **kwargs) -> None: + super().__init__(path, *args, **kwargs) + self.path_by_path = None + self.path_by_id = None + if path.startswith("/dev/video"): + other = kwargs.get("other", None) + if other: + self.path_by_path = other.get("by_path", None) + self.path_by_id = other.get("by_id", None) + else: + self.path = os.path.realpath(path) + self.path_by_id = path + self.query_controls = v4l2.ctl.get_query_controls(self.path) + + cur_sec = "" + for name, qc in self.query_controls.items(): + parsed_qc = v4l2.ctl.parse_qc_of_path(self.path, qc) + if not parsed_qc: + cur_sec = name + self.control_values[cur_sec] = {} + continue + self.control_values[cur_sec][name] = parsed_qc + self.formats = v4l2.ctl.get_formats(self.path) + + def get_formats_string(self) -> str: + message = "" + indent = " " * 8 + for fmt, data in self.formats.items(): + message += f"{fmt}:\n" + for res, fps_list in data.items(): + message += f"{indent}{res}\n" + for fps in fps_list: + message += f"{indent*2}{fps}\n" + return message[:-1] + + def has_mjpg_hw_encoder(self) -> bool: + return any("Motion-JPEG" in fmt for fmt in self.formats.keys()) + + def get_controls_string(self) -> str: + message = "" + for section, controls in self.control_values.items(): + message += f"{section}:\n" + for control, data in controls.items(): + line = f"{control} ({data['type']})" + line += max(0, 35 - len(line)) * " " + ":" + if data["type"] in ("int",): + line += f" min={data['min']} max={data['max']} step={data['step']}" + line += f" default={data['default']}" + line += f" value={self.get_current_control_value(control)}" + if "flags" in data: + line += f" flags={data['flags']}" + message += logger.indentation + line + "\n" + if "menu" in data: + for value, name in data["menu"].items(): + message += logger.indentation * 2 + f"{value}: {name}\n" + message += "\n" + return message[:-1] + + def set_control(self, control: str, value: int) -> bool: + return v4l2.ctl.set_control_with_qc( + self.path, self.query_controls[control], value + ) + + def get_current_control_value(self, control: str) -> int: + return v4l2.ctl.get_control_cur_value_with_qc( + self.path, self.query_controls[control] + ) + + @staticmethod + def init_camera_type() -> list: + def get_avail_uvc(search_path): + avail_uvc = {} + if not os.path.exists(search_path): + return avail_uvc + for file in os.listdir(search_path): + dev_path = os.path.join(search_path, file) + if os.path.islink(dev_path) and dev_path.endswith("index0"): + avail_uvc[os.path.realpath(dev_path)] = dev_path + return avail_uvc + + avail_by_id = get_avail_uvc("/dev/v4l/by-id/") + + avail_uvc_cameras = { + dev_path: { + "by_path": by_path, + "by_id": avail_by_id.get(dev_path, None), + } + for dev_path, by_path in get_avail_uvc("/dev/v4l/by-path").items() + if "usb" in by_path + } + + return [ + UVC(dev_path, other=other_paths) + for dev_path, other_paths in avail_uvc_cameras.items() + ] diff --git a/crowsnest/components/cam.py b/crowsnest/components/cam.py new file mode 100644 index 00000000..3cbf6aee --- /dev/null +++ b/crowsnest/components/cam.py @@ -0,0 +1,72 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import asyncio +import traceback +from configparser import SectionProxy +from typing import Optional + +from .. import logger, utils, watchdog +from .section import Section +from .streamer.streamer import Streamer + + +class Cam(Section): + section_name = "cam" + keyword = "cam" + + def parse_config_section( + self, config_section: SectionProxy, *args, **kwargs + ) -> None: + super().parse_config_section(config_section, *args, **kwargs) + self.parameters.update({"mode": config_section.get("mode", None)}) + component = utils.load_component( + self.parameters["mode"], + self.name, + config_section, + path="crowsnest.components.streamer", + ) + if component is None or not isinstance(component, Streamer): + self.log_error("Tried to load a component that is not a Streamer!") + return + self.streamer: Streamer = component + + def check_config_section(self, config_section: SectionProxy) -> bool: + if not hasattr(self, "streamer"): + return False + return self.streamer.initialized + + async def execute(self, lock: asyncio.Lock) -> Optional[asyncio.subprocess.Process]: + if self.streamer is None: + self.log_error("No streamer loaded!") + return + try: + await lock.acquire() + self.log_quiet( + f"Start {self.streamer.keyword} with device " + f"{self.streamer.parameters['device']} ..." + ) + watchdog.configured_devices.append(self.streamer.parameters["device"]) + process = await self.streamer.execute(lock) + if process: + await process.wait() + else: + self.log_error(f"Start of {self.parameters['mode']} failed!") + except Exception: + self.log_multiline(traceback.format_exc().strip(), logger.log_error) + self.log_error(f"Start of {self.parameters['mode']} failed!") + finally: + if self.streamer.parameters["device"] in watchdog.configured_devices: + watchdog.configured_devices.remove(self.streamer.parameters["device"]) + if lock.locked(): + lock.release() + + +def load_component(name: str, config_section: SectionProxy) -> Cam: + return Cam(name, config_section) diff --git a/crowsnest/components/crowsnest.py b/crowsnest/components/crowsnest.py new file mode 100644 index 00000000..1ba3afb3 --- /dev/null +++ b/crowsnest/components/crowsnest.py @@ -0,0 +1,37 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import asyncio +from configparser import SectionProxy +from typing import Optional + +from .section import Section + + +class Crowsnest(Section): + def __init__(self, config_section: SectionProxy) -> None: + self.section = "[crowsnest]" + super().__init__("crowsnest", config_section) + + def parse_config_section(self, section: SectionProxy) -> None: + super().parse_config_section(section) + self.parameters.update( + { + "log_level": section.getloglevel("log_level", "INFO"), + "rollover_on_start": section.getboolean("rollover_on_start", False), + "no_proxy": section.getboolean("no_proxy", False), + } + ) + + async def execute(self, lock: asyncio.Lock) -> Optional[asyncio.subprocess.Process]: + raise NotImplementedError("If you see this, something went wrong!!!") + + +def load_component(name: str, config_section: SectionProxy) -> Crowsnest: + return Crowsnest(config_section) diff --git a/crowsnest/components/section.py b/crowsnest/components/section.py new file mode 100644 index 00000000..d78cad44 --- /dev/null +++ b/crowsnest/components/section.py @@ -0,0 +1,89 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import asyncio +import functools +from abc import ABC, abstractmethod +from configparser import SectionProxy +from typing import Any, Optional + +from .. import logger + + +class Section(ABC): + section_name = "section" + keyword = "section" + available_sections = {} + + # Section looks like this: + # [ ] + # param1: value1 + # param2: value2 + def __init__(self, name: str, config_section: SectionProxy) -> None: + self.name = name + self.parse_config_section(config_section) + if not hasattr(self, "section"): + self.section = f"[{self.section_name} {self.name}]" + self.initialized = self.check_config_section(config_section) + + # Check if the config section has only valid options + def check_config_section(self, config_section: SectionProxy) -> bool: + success = True + for option, value in config_section.items(): + if option not in self.parameters: + self.log_warning( + f"Parameter '{option}' is not supported by {self.keyword}!" + ) + for option, value in self.parameters.items(): + if value is None: + self.log_error( + f"Parameter '{option}' incorrectly set or missing but is required!" + ) + success = False + return success + + # Parse config according to the needs of the section + def parse_config_section( + self, config_section: SectionProxy, *args, **kwargs + ) -> None: + self.parameters: dict[str, Any] = {} + + @abstractmethod + async def execute(self, lock: asyncio.Lock) -> Optional[asyncio.subprocess.Process]: + raise NotImplementedError("If you see this, something went wrong!!!") + + def __getattr__(self, name): + """Dynamically handle log method calls (e.g., log_info, log_debug).""" + if not name.startswith("log_"): + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) + + log_function = getattr(logger, name, None) + + if not callable(log_function): + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) + + @functools.wraps(log_function) + def log_wrapper(msg, prefix="", postfix=""): + formatted_msg = f"{prefix}{self.section}{postfix}: {msg}" + log_function(formatted_msg) + + return log_wrapper + + def log_multiline(self, msg, log_func, prefix="", postfix="", *args): + logger.log_multiline( + msg, log_func, line_prefix=f"{prefix}{self.section}{postfix}", *args + ) + + +def load_component(*args, **kwargs) -> Section: + raise NotImplementedError("If you see this, something went wrong!!!") diff --git a/crowsnest/components/streamer/camera-streamer.py b/crowsnest/components/streamer/camera-streamer.py new file mode 100644 index 00000000..5d3862b7 --- /dev/null +++ b/crowsnest/components/streamer/camera-streamer.py @@ -0,0 +1,99 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import asyncio +from configparser import SectionProxy +from typing import Optional + +from crowsnest import logging_helper + +from ... import camera, utils +from .streamer import Streamer + + +class Camera_Streamer(Streamer): + keyword = "camera-streamer" + binary_names = ["camera-streamer"] + binary_paths = ["bin/camera-streamer"] + + async def execute(self, lock: asyncio.Lock) -> Optional[asyncio.subprocess.Process]: + if utils.is_pi5(): + self.log_warning("Mode 'camera-streamer' is not supported on Pi5/CM5!") + self.log_warning(f"Please change the mode of this section.") + return None + + host = "127.0.0.1" + if self.parameters["no_proxy"]: + host = "0.0.0.0" + self.log_info("Set to 'no_proxy' mode! Using 0.0.0.0!") + + port = self.parameters["port"] + width, height = self.parameters["resolution"] + + fps = self.parameters["max_fps"] + device = self.parameters["device"] + cam = camera.camera_manager.get_cam_by_path(device) + if cam is None: + logging_helper.log_camera_not_found(self) + return None + + streamer_args = [ + f"--camera-path={device}", + f"--http-listen={host}", + f"--http-port={port}", + f"--camera-fps={fps}", + f"--camera-width={width}", + f"--camera-height={height}", + f"--camera-snapshot.height={height}", + f"--camera-video.height={height}", + f"--camera-stream.height={height}", + "--camera-auto_reconnect=1", + ] + + v4l2ctl = self.parameters["v4l2ctl"] + if v4l2ctl: + postfix = " V4L2 Control" + self.log_quiet(f"Handling done by {self.keyword}", postfix=postfix) + self.log_quiet(f"Trying to set: {v4l2ctl}", postfix=postfix) + for ctrl in v4l2ctl.split(","): + streamer_args.append(f"--camera-options={ctrl.strip()}") + + if isinstance(cam, camera.Libcamera): + streamer_args.extend(["--camera-type=libcamera", "--camera-format=YUYV"]) + elif isinstance(cam, (camera.UVC, camera.Legacy)): + streamer_args.append("--camera-type=v4l2") + if cam.has_mjpg_hw_encoder(): + streamer_args.append("--camera-format=MJPEG") + + # custom flags + streamer_args.extend(self.parameters["custom_flags"].split()) + + cmd = f"{self.binary_path} " + " ".join(streamer_args) + log_pre = f"{self.keyword} " + + self.log_debug(f"Parameters: {' '.join(streamer_args)}", prefix=log_pre) + process, _, _ = await utils.execute_command( + cmd, + info_log_pre=log_pre, + info_log_func=self.log_debug, + error_log_pre=log_pre, + error_log_func=self.log_debug, + ) + if lock.locked(): + lock.release() + + return process + + +def load_streamer() -> tuple[list[str], list[str]]: + return Camera_Streamer.binary_names, Camera_Streamer.binary_paths + + +def load_component(name: str, config_section: SectionProxy) -> Camera_Streamer: + return Camera_Streamer(name, config_section) diff --git a/crowsnest/components/streamer/spyglass.py b/crowsnest/components/streamer/spyglass.py new file mode 100644 index 00000000..5d68d241 --- /dev/null +++ b/crowsnest/components/streamer/spyglass.py @@ -0,0 +1,93 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import asyncio +from configparser import SectionProxy +from typing import Optional + +from crowsnest import logging_helper +from crowsnest.camera.types.libcamera import Libcamera + +from ... import camera, utils +from .streamer import Streamer + + +class Spyglass(Streamer): + keyword = "spyglass" + binary_names = ["run.py", "spyglass"] + binary_paths = ["bin/spyglass"] + + async def execute(self, lock: asyncio.Lock) -> Optional[asyncio.subprocess.Process]: + host = "127.0.0.1" + if self.parameters["no_proxy"]: + host = "0.0.0.0" + self.log_info("Set to 'no_proxy' mode! Using 0.0.0.0!") + + port = self.parameters["port"] + res = "x".join(self.parameters["resolution"]) + fps = self.parameters["max_fps"] + device = self.parameters["device"] + cam = camera.camera_manager.get_cam_by_path(device) + if not isinstance(cam, Libcamera): + logging_helper.log_camera_not_found(self, wrong_cam_type=True) + return None + + try: + int(device) + device_option = "camera_num" + except ValueError: + device_option = "device" + + streamer_args = [ + f"--{device_option}={device}", + f"--bindaddress={host}", + f"--port={port}", + f"--fps={fps}", + f"--resolution={res}", + "--stream_url=/?action=stream", + "--snapshot_url=/?action=snapshot", + ] + + v4l2ctl = self.parameters["v4l2ctl"] + if v4l2ctl: + postfix = " V4L2 Control" + self.log_quiet(f"Handling done by {self.keyword}", postfix=postfix) + self.log_quiet(f"Trying to set: {v4l2ctl}", postfix=postfix) + for ctrl in v4l2ctl.split(","): + streamer_args.append(f"--controls={ctrl.strip()}") + + # custom flags + streamer_args.extend(self.parameters["custom_flags"].split()) + + venv_path = "" + if "run.py" in self.binary_path: + venv_path = f"{Spyglass.binary_paths[0]}/.venv/bin/python3 " + cmd = f"{venv_path}{self.binary_path} " + " ".join(streamer_args) + log_pre = f"{self.keyword} " + + self.log_debug(f"Parameters: {' '.join(streamer_args)}", prefix=log_pre) + process, _, _ = await utils.execute_command( + cmd, + info_log_pre=log_pre, + info_log_func=self.log_debug, + error_log_pre=log_pre, + error_log_func=self.log_debug, + ) + if lock.locked(): + lock.release() + + return process + + +def load_streamer() -> tuple[list[str], list[str]]: + return Spyglass.binary_names, Spyglass.binary_paths + + +def load_component(name: str, config_section: SectionProxy) -> Spyglass: + return Spyglass(name, config_section) diff --git a/crowsnest/components/streamer/streamer.py b/crowsnest/components/streamer/streamer.py new file mode 100644 index 00000000..aa0edf7a --- /dev/null +++ b/crowsnest/components/streamer/streamer.py @@ -0,0 +1,103 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import asyncio +import os +import textwrap +from abc import ABC, abstractmethod +from configparser import SectionProxy +from os import listdir +from os.path import isfile, join +from typing import Optional + +from ... import logger, utils +from ..section import Section + + +class Streamer(Section, ABC): + section_name = "cam" + binary_names = [] + binary_paths = [] + global_no_proxy = False + + binaries = {} + missing_bin_txt = textwrap.dedent( + """\ + '%s' executable not found! + Please make sure everything is installed correctly and up to date! + Run 'make update' inside the crowsnest directory to install and update everything.""" + ) + + def parse_config_section( + self, config_section: SectionProxy, *args, **kwargs + ) -> None: + super().parse_config_section(config_section, *args, **kwargs) + self.parameters.update( + { + "mode": self.keyword, + "port": config_section.getint("port", None), + "device": config_section.get("device", None), + "resolution": config_section.getresolution("resolution", None), + "max_fps": config_section.getint("max_fps", None), + "no_proxy": config_section.getboolean("no_proxy", False) + or Streamer.global_no_proxy, + "custom_flags": config_section.get("custom_flags", ""), + "v4l2ctl": config_section.get("v4l2ctl", ""), + } + ) + mode = self.keyword + if mode not in Streamer.binaries: + Streamer.binaries[mode] = utils.get_executable( + self.binary_names, self.binary_paths + ) + self.binary_path = Streamer.binaries[mode] + + def check_config_section(self, config_section) -> bool: + success = super().check_config_section(config_section) + if self.binary_path is None: + self.log_multiline( + Streamer.missing_bin_txt % self.keyword, logger.log_error + ) + success = False + return success + + @abstractmethod + async def execute(self, lock: asyncio.Lock) -> Optional[asyncio.subprocess.Process]: + raise NotImplementedError("If you see this, something went wrong!!!") + + +def load_all_streamers() -> None: + streamer_path = os.path.dirname(__file__) + streamer_files = [ + f + for f in listdir(streamer_path) + if isfile(join(streamer_path, f)) and f.endswith(".py") + ] + for streamer_file in streamer_files: + streamer_name = streamer_file.removesuffix(".py") + try: + tup = utils.load_streamer( + streamer_name, path="crowsnest.components.streamer" + ) + if tup is None: + continue + binary_names, binary_paths = tup + except NotImplementedError: + continue + Streamer.binaries[streamer_name] = utils.get_executable( + binary_names, binary_paths + ) + + +def load_streamer() -> tuple[list[str], list[str]]: + raise NotImplementedError("If you see this, something went wrong!!!") + + +def load_component(name: str, config_section: SectionProxy) -> Streamer: + raise NotImplementedError("If you see this, something went wrong!!!") diff --git a/crowsnest/components/streamer/ustreamer.py b/crowsnest/components/streamer/ustreamer.py new file mode 100644 index 00000000..e51e7394 --- /dev/null +++ b/crowsnest/components/streamer/ustreamer.py @@ -0,0 +1,168 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import asyncio +import re +from configparser import SectionProxy +from typing import Optional + +from crowsnest import logging_helper +from crowsnest.camera.types.uvc import UVC + +from ... import camera, logger, utils +from .streamer import Streamer + + +class Ustreamer(Streamer): + keyword = "ustreamer" + binary_names = ["ustreamer.bin", "ustreamer"] + binary_paths = ["bin/ustreamer"] + + async def execute(self, lock: asyncio.Lock) -> Optional[asyncio.subprocess.Process]: + host = "127.0.0.1" + if self.parameters["no_proxy"]: + host = "0.0.0.0" + self.log_info("Set to 'no_proxy' mode! Using 0.0.0.0!") + + port = self.parameters["port"] + res = "x".join(self.parameters["resolution"]) + fps = self.parameters["max_fps"] + device = self.parameters["device"] + self.cam = camera.camera_manager.get_cam_by_path(device) + if not isinstance(self.cam, UVC): + logging_helper.log_camera_not_found(self, wrong_cam_type=True) + return None + + streamer_args = [ + f"--host {host}", + f"--port {port}", + f"--resolution {res}", + f"--desired-fps {fps}", + # webroot & allow crossdomain requests + "--allow-origin *", + '--static "resources/ustreamer-www"', + ] + + if self._is_device_legacy(): + streamer_args.extend( + [ + "--format MJPEG", + "--device-timeout 5", + "--buffers 3", + ] + ) + self._blockyfix() + else: + streamer_args.extend([f"--device {device}", "--device-timeout 2"]) + if self.cam.has_mjpg_hw_encoder(): + streamer_args.extend(["--format MJPEG", "--encoder HW"]) + + v4l2ctl = self.parameters["v4l2ctl"] + if v4l2ctl: + self._set_v4l2_ctrls(v4l2ctl.split(",")) + + # custom flags + streamer_args.extend(self.parameters["custom_flags"].split()) + + cmd = self.binary_path + " " + " ".join(streamer_args) + log_pre = f"{self.keyword} " + + self.log_debug(f"Parameters: {' '.join(streamer_args)}", prefix=log_pre) + process, _, _ = await utils.execute_command( + cmd, + info_log_pre=log_pre, + info_log_func=self.log_debug, + error_log_pre=log_pre, + error_log_func=self._custom_log, + ) + if lock.locked(): + lock.release() + + await asyncio.sleep(0.5) + ctl = next((ctl for ctl in v4l2ctl.split(",") if "focus_absolute" in ctl), None) + if ctl is not None: + focus_absolute = ctl.split("=")[1].strip() + self._brokenfocus(focus_absolute) + + return process + + def _custom_log(self, msg: str, prefix=""): + if msg.endswith("==="): + msg = msg[:-28] + else: + msg = re.sub(r"-- (.*?) \[.*?\] --", r"\1", msg) + self.log_debug(msg, prefix) + + def _set_v4l2_ctrl(self, ctrl: str, postfix="") -> None: + try: + c = ctrl.split("=")[0].strip().lower() + v = int(ctrl.split("=")[1].strip()) + if not self.cam or not self.cam.set_control(c, v): + raise ValueError + except (ValueError, IndexError): + self.log_quiet( + f"Failed to set parameter: '{ctrl.strip()}'", postfix=postfix + ) + + def _set_v4l2_ctrls(self, ctrls: Optional[list[str]] = None) -> None: + postfix = " V4L2 Control" + if not ctrls: + self.log_quiet(f"No parameters set. Skipped.", postfix=postfix) + return + self.log_quiet(f"Options: {', '.join(ctrls)}", postfix=postfix) + avail_ctrls = self.cam.get_controls_string() + for ctrl in ctrls: + c = ctrl.split("=")[0].strip().lower() + # TODO: make check more robust + if c not in avail_ctrls: + self.log_quiet( + f"Parameter '{ctrl.strip()}' not available for '{self.parameters['device']}'. Skipped.", + postfix=postfix, + ) + continue + self._set_v4l2_ctrl(ctrl, postfix) + # Repulls the string to print current values + self.log_multiline( + self.cam.get_controls_string(), + logger.log_debug, + postfix=" v4l2ctl", + ) + + def _brokenfocus(self, focus_absolute_conf: str) -> None: + cur_val = self.cam.get_current_control_value("focus_absolute") + if cur_val is not None and cur_val != int(focus_absolute_conf): + self.log_warning(f"Detected 'brokenfocus' device.") + self.log_info(f"Try to set to configured Value.") + self._set_v4l2_ctrl(f"focus_absolute={focus_absolute_conf}") + self.log_debug( + f"Value is now: {self.cam.get_current_control_value('focus_absolute')}" + ) + + def _blockyfix(self): + """ + This function is to set bitrate on legacy raspicams. + If legacy raspicams set to variable bitrate, they tend to show + a "block-like" view after reboots + To prevent that blockyfix should apply constant bitrate before start of ustreamer + See https://github.com/mainsail-crew/crowsnest/issues/33 + """ + self.cam.set_control("video_bitrate_mode", 1) + self.cam.set_control("video_bitrate", 15000000) + self.log_info("Blockyfix: Setting video_bitrate_mode to constant.") + + def _is_device_legacy(self) -> bool: + return isinstance(self.cam, camera.Legacy) + + +def load_streamer() -> tuple[list[str], list[str]]: + return Ustreamer.binary_names, Ustreamer.binary_paths + + +def load_component(name: str, config_section: SectionProxy) -> Ustreamer: + return Ustreamer(name, config_section) diff --git a/crowsnest/crowsnest.py b/crowsnest/crowsnest.py new file mode 100644 index 00000000..9d53811d --- /dev/null +++ b/crowsnest/crowsnest.py @@ -0,0 +1,188 @@ +#!/usr/bin/python3 + +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import argparse +import asyncio +import configparser +import signal +import time +import traceback +from logging.handlers import RotatingFileHandler + +from crowsnest import logger, logging_helper, utils, watchdog +from crowsnest.components.crowsnest import Crowsnest +from crowsnest.components.streamer.streamer import Streamer + + +def initial_parse_config(config_path, config): + try: + config.read(config_path) + except configparser.Error as e: + logger.log_multiline(e.message, logger.log_error) + logger.log_error("Failed to parse config! Exiting...") + exit(1) + crowsnest = ( + None if not config.has_section("crowsnest") else Crowsnest(config["crowsnest"]) + ) + if crowsnest is None or not crowsnest.initialized: + logger.log_error("Failed to parse config for '[crowsnest]' section! Exiting...") + exit(1) + + Streamer.global_no_proxy = crowsnest.parameters["no_proxy"] + # We don't need the section anymore so remove it + config.remove_section("crowsnest") + return crowsnest + + +async def start_sections(config): + sect_objs = [] + sect_exec_tasks = set() + + # Catches SIGINT and SIGTERM to exit gracefully and cancel all tasks + signal.signal(signal.SIGINT, exit_gracefully) + signal.signal(signal.SIGTERM, exit_gracefully) + if hasattr(signal, "SIGHUP"): + signal.signal(signal.SIGHUP, exit_gracefully) + + if len(config.sections()) <= 0: + logger.log_quiet("No Cams / Services to start! Exiting ...") + return + logger.log_quiet("Try to parse configured Cams / Services...") + + try: + for section in config.sections(): + section_header = section.split(" ") + section_object = None + section_keyword = section_header[0] + + log_prefix = f"[{section}]: " + section_name = " ".join(section_header[1:]) + logger.log_quiet(f"Parse configuration ...", log_prefix) + component = utils.load_component( + section_keyword, section_name, config[section] + ) + if component is not None and component.initialized: + sect_objs.append(component) + logger.log_quiet(f"Configuration looks good. Continue ...", log_prefix) + else: + logger.log_error(f"Failed to parse config! Skipping ...", log_prefix) + + logger.log_quiet("Try to start configured Cams / Services ...") + if sect_objs: + lock = asyncio.Lock() + for section_object in sect_objs: + task = asyncio.create_task(section_object.execute(lock)) + sect_exec_tasks.add(task) + + # Lets sect_exec_tasks finish first + await asyncio.sleep(0) + async with lock: + logger.log_quiet("... Done!") + else: + logger.log_quiet("No Service started! Exiting ...") + + for task in sect_exec_tasks: + if task is not None: + await task + except Exception as e: + logger.log_multiline(traceback.format_exc().strip(), logger.log_error) + finally: + for task in sect_exec_tasks: + if task is not None: + task.cancel() + watchdog.running = False + logger.log_quiet("Shutdown or Killed by User!") + logger.log_quiet("Please come again :)") + logger.log_quiet("Goodbye...") + + +def exit_gracefully(signum, frame): + # We just log the exit + # Childs will get same signal and trigger the except/finally block + logger.log_quiet(f"Received signal {signum}. Shutting down...") + + +def check_uptime_and_sleep(sleep_time): + if sleep_time <= 0: + return + try: + with open("/proc/uptime", "r") as f: + uptime_seconds = float(f.readline().split()[0]) + + if uptime_seconds < 120: + time.sleep(sleep_time) + except (IOError, ValueError): + logger.log_error( + "Couldn't properly read /proc/uptime! Skipping sleep! Please report this!" + ) + + +async def main(): + parser = argparse.ArgumentParser( + prog="Crowsnest", + description="Crowsnest - A webcam daemon for Debian based distributions", + ) + config = configparser.ConfigParser( + inline_comment_prefixes="#", + converters={ + "loglevel": utils.log_level_converter, + "resolution": utils.resolution_converter, + }, + ) + + parser.add_argument( + "-c", "--config_path", help="Path to config file", type=str, required=True + ) + parser.add_argument( + "-l", "--log_path", help="Path to log file", type=str, required=True + ) + parser.add_argument( + "-s", + "--sleep_boot", + help="Delay start (in seconds) after system boot", + type=int, + default=0, + ) + + args = parser.parse_args() + + logger.setup_logging(args.log_path) + + check_uptime_and_sleep(args.sleep_boot) + + logging_helper.log_initial() + + crowsnest = initial_parse_config(args.config_path, config) + + if crowsnest is None: + logger.log_error("Something went terribly wrong!") + exit(1) + + if crowsnest.parameters["rollover_on_start"]: + for h in logger.logger.handlers: + if isinstance(h, RotatingFileHandler): + h.doRollover() + logging_helper.log_initial() + + logger.set_log_level(crowsnest.parameters["log_level"]) + + logging_helper.log_host_info() + logging_helper.log_streamer() + logging_helper.log_config(args.config_path) + logging_helper.log_cams() + + task1 = asyncio.create_task(start_sections(config)) + await asyncio.sleep(0) + task2 = asyncio.create_task(watchdog.run_watchdog()) + + await task1 + if task2: + task2.cancel() diff --git a/crowsnest/logger.py b/crowsnest/logger.py new file mode 100644 index 00000000..6859ee34 --- /dev/null +++ b/crowsnest/logger.py @@ -0,0 +1,86 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import functools +import logging +import logging.handlers +import os +import sys + +DEV = 10 +DEBUG = 15 +QUIET = 35 + +indentation = 6 * " " + +logger = logging.getLogger("crowsnest") + + +def setup_logging(log_path, filemode="a", log_level=logging.INFO): + # Create log directory if it does not exist. + os.makedirs(os.path.dirname(log_path), exist_ok=True) + + logging.addLevelName(DEV, "DEV") + logging.addLevelName(DEBUG, "DEBUG") + logging.addLevelName(QUIET, "QUIET") + + logger.propagate = False + formatter = logging.Formatter( + "[%(asctime)s] %(message)s", datefmt="%d/%m/%y %H:%M:%S" + ) + + filehandler = logging.handlers.RotatingFileHandler( + log_path, + mode=filemode, + encoding="utf-8", + maxBytes=3 * 1024 * 1024, + backupCount=5, + ) + filehandler.setFormatter(formatter) + logger.addHandler(filehandler) + + # StreamHandler for stdout. + streamhandler = logging.StreamHandler(sys.stdout) + streamhandler.setFormatter(formatter) + logger.addHandler(streamhandler) + + # Set log level. + logger.setLevel(log_level) + + +def set_log_level(level): + logger.setLevel(level) + + +def log(level, msg, prefix="", **kwargs): + level_prefix = kwargs.pop("level_prefix", "") + if level_prefix: + final_msg = f"{level_prefix}: {prefix}{msg}" + else: + final_msg = f"{prefix}{msg}" + logger.log(level, final_msg, **kwargs) + + +log_quiet = functools.partial(log, QUIET) +log_info = functools.partial(log, logging.INFO, level_prefix="INFO") +log_info_silent = functools.partial(log, logging.INFO) +log_debug = functools.partial(log, DEBUG, level_prefix="DEBUG") +log_warning = functools.partial(log, logging.WARNING, level_prefix="WARN") +log_error = functools.partial(log, logging.ERROR, level_prefix="ERROR") + + +def log_multiline(msg, log_func, *args, **kwargs): + lines = msg.split("\n") + line_prefix = kwargs.pop("line_prefix", "") + for line in lines: + if line_prefix: + final_line = f"{line_prefix}: {line}" + else: + final_line = f"{line}" + log_func(final_line, *args, **kwargs) diff --git a/crowsnest/logging_helper.py b/crowsnest/logging_helper.py new file mode 100644 index 00000000..1d29cf8f --- /dev/null +++ b/crowsnest/logging_helper.py @@ -0,0 +1,185 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import os +import re +import shutil +import sys + +from . import camera, logger, utils +from .components.streamer.streamer import Streamer, load_all_streamers + + +def log_initial(): + logger.log_quiet( + "crowsnest - A webcam Service for multiple Cams and Stream Services." + ) + command = "git describe --always --tags" + version = utils.execute_shell_command(command) + logger.log_quiet(f"Version: {version}") + logger.log_quiet("Prepare Startup ...") + + +def log_host_info(): + logger.log_info("Host Information:") + log_pre = logger.indentation + + ### OS Infos + # OS Version + distribution = utils.grep("/etc/os-release", "PRETTY_NAME").strip() + _, _, distribution = distribution.partition("=") + distribution = distribution.strip('"') or "Unknown" + logger.log_info_silent(f"Distribution: {distribution}", log_pre) + + # Release Version of MainsailOS (if file present) + try: + with open("/etc/mainsailos-release", "r") as file: + content = file.read() + logger.log_info_silent(f"Release: {content.strip()}", log_pre) + except FileNotFoundError: + pass + + # Kernel Version + uname = os.uname() + logger.log_info_silent( + f"Kernel: {uname.sysname} {uname.release} {uname.machine}", log_pre + ) + + ### Host Machine Infos + # Host model + model = utils.grep("/proc/cpuinfo", "Model").split(":") + if len(model) == 1: + model = utils.grep("/proc/cpuinfo", "model name").split(":") + if len(model) == 1: + model = "Unknown" + else: + model = model[1].strip() + + logger.log_info_silent(f"Model: {model}", log_pre) + + # CPU count + cpu_count = os.cpu_count() + logger.log_info_silent(f"Available CPU Cores: {cpu_count}", log_pre) + + # Avail mem + memtotal = utils.grep("/proc/meminfo", "MemTotal:") + _, _, memtotal = memtotal.partition(":") + memtotal = memtotal.strip() or "Unknown" + logger.log_info_silent(f"Available Memory: {memtotal}", log_pre) + + # Avail disk size + total, _, free = shutil.disk_usage("/") + total = utils.bytes_to_gigabytes(total) + free = utils.bytes_to_gigabytes(free) + logger.log_info_silent(f"Diskspace (avail. / total): {free}G / {total}G", log_pre) + + +def log_streamer(): + logger.log_info("Found Streamer:") + load_all_streamers() + log_pre = logger.indentation + for bin in Streamer.binaries: + if Streamer.binaries[bin] is None: + continue + logger.log_info_silent(f"{bin}: {Streamer.binaries[bin]}", log_pre) + + +def log_config(config_path): + logger.log_info(f"Print Configfile: '{config_path}'") + with open(config_path, "r") as file: + config_txt = file.read() + # Remove comments + config_txt = re.sub(r"#.*$", "", config_txt, flags=re.MULTILINE) + # Remove multiple whitespaces next to each other at the end of a line + config_txt = re.sub(r"\s*$", "", config_txt, flags=re.MULTILINE) + # Add newlines before sections + config_txt = re.sub(r"(\[.*\])$", "\n\\1", config_txt, flags=re.MULTILINE) + # Remove leading and trailing whitespaces + config_txt = config_txt.strip() + # Split the config file into lines + logger.log_multiline( + config_txt, logger.log_info_silent, prefix=logger.indentation + ) + + +def log_cams(): + logger.log_info("Detect available Devices") + libcamera = camera.camera_manager.init_camera_type(camera.Libcamera) + uvc = camera.camera_manager.init_camera_type(camera.UVC) + legacy = camera.camera_manager.init_camera_type(camera.Legacy) + total = len(libcamera) + len(legacy) + len(uvc) + + if total == 0: + logger.log_error("No usable Devices Found. Stopping ") + sys.exit(1) + + logger.log_info(f"Found {total} total available Device(s)") + if libcamera: + logger.log_info(f"Found {len(libcamera)} available 'libcamera' device(s)") + for cam in libcamera: + log_libcam(cam) + if legacy: + for cam in legacy: + log_legacy_cam(cam) + if uvc: + logger.log_info(f"Found {len(uvc)} available v4l2 (UVC) camera(s)") + for cam in uvc: + log_uvc_cam(cam) + + +def log_libcam(cam: camera.Libcamera) -> None: + logger.log_info(f"Detected 'libcamera' device -> {cam.path}") + logger.log_info_silent(f"Advertised Formats:") + log_camera_formats(cam) + logger.log_info_silent(f"Supported Controls:") + log_camera_ctrls(cam) + + +def log_uvc_cam(cam: camera.UVC) -> None: + if cam.path_by_id: + logger.log_info_silent(f"{cam.path_by_id} -> {cam.path}") + elif cam.path_by_path: + logger.log_info_silent(f"{cam.path_by_path} -> {cam.path}") + else: + logger.log_info_silent(f"{cam.path}") + logger.log_info_silent(f"Supported Formats:") + log_camera_formats(cam) + logger.log_info_silent(f"Supported Controls:") + log_camera_ctrls(cam) + + +def log_legacy_cam(cam: camera.Legacy) -> None: + logger.log_info(f"Detected 'Raspicam' Device -> {cam.path}") + logger.log_info_silent(f"Supported Formats:") + log_camera_formats(cam) + logger.log_info_silent(f"Supported Controls:") + log_camera_ctrls(cam) + + +def log_camera_formats(cam: camera.Camera) -> None: + logger.log_multiline( + cam.get_formats_string(), logger.log_info_silent, logger.indentation + ) + + +def log_camera_ctrls(cam: camera.Camera) -> None: + logger.log_multiline( + cam.get_controls_string(), logger.log_info_silent, logger.indentation + ) + + +def log_camera_not_found(streamer: Streamer, wrong_cam_type: bool = False): + if wrong_cam_type: + first_sentence = "Wrong camera type or device not found." + else: + first_sentence = "Device not found." + streamer.log_warning( + f"{first_sentence} Make sure the device path is correct " + f"and points to a camera supported by {streamer.keyword}!" + ) diff --git a/crowsnest/utils.py b/crowsnest/utils.py new file mode 100644 index 00000000..4caf8eaa --- /dev/null +++ b/crowsnest/utils.py @@ -0,0 +1,157 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import asyncio +import importlib +import os +import shlex +import shutil +import subprocess +from configparser import SectionProxy +from typing import Any, Callable, Optional + +from . import logger + + +# Dynamically import functions +# Requires module to have a function with function_name +def load_function( + function_name: str, module_name: str, path="crowsnest.components" +) -> Callable[..., Any]: + module = importlib.import_module(f"{path}.{module_name}") + return getattr(module, function_name) + + +def load_component( + module_name: str, + name: str, + config_section: SectionProxy, + path="crowsnest.components", +) -> Optional[Any]: + try: + return load_function("load_component", module_name, path)(name, config_section) + except (ModuleNotFoundError, AttributeError) as e: + logger.log_error( + f"Failed to load module '{module_name}' from '{path}' ({e.name})" + ) + return None + + +def load_streamer(module_name: str, path="crowsnest.components") -> Optional[Any]: + try: + return load_function("load_streamer", module_name, path)() + except (ModuleNotFoundError, AttributeError) as e: + logger.log_error( + f"Failed to load streamer '{module_name}' from '{path}' ({e.name})" + ) + return None + + +async def log_subprocess_output(stream, log_func, line_prefix=""): + line = await stream.readline() + while line: + l = line.decode("utf-8").strip() + log_func(l, prefix=line_prefix) + line = await stream.readline() + + +async def execute_command( + command: str, + info_log_func=logger.log_debug, + error_log_func=logger.log_error, + info_log_pre="", + error_log_pre="", +): + args = shlex.split(command) + process = await asyncio.create_subprocess_exec( + *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + + stdout_task = asyncio.create_task( + log_subprocess_output(process.stdout, info_log_func, info_log_pre) + ) + stderr_task = asyncio.create_task( + log_subprocess_output(process.stderr, error_log_func, error_log_pre) + ) + + return process, stdout_task, stderr_task + + +def execute_shell_command(command: str, strip: bool = True) -> str: + try: + output = subprocess.check_output(shlex.split(command)).decode("utf-8") + if strip: + output = output.strip() + return output + except subprocess.CalledProcessError as e: + return "" + + +def bytes_to_gigabytes(value: int) -> int: + return round(value / 1024**3) + + +def find_file(name: str, path: str) -> Optional[str]: + for dpath, _, fnames in os.walk(path): + for fname in fnames: + if fname == name: + return os.path.join(dpath, fname) + return None + + +def get_executable(names: list[str], paths: list[str]) -> Optional[str]: + if names is None or paths is None: + return None + for name in names: + for path in paths: + found = find_file(name, path) + if found: + return found + # Only search for installed packages, if there are no manually compiled binaries + for name in names: + exec = shutil.which(name) + if exec: + return exec + return None + + +def grep(path: str, search: str) -> str: + try: + with open(path, "r") as file: + lines = file.readlines() + for line in lines: + if search in line: + return line + except FileNotFoundError: + logger.log_error(f"File '{path}' not found!") + return "" + + +def log_level_converter(log_level: str) -> str: + if log_level.lower() in ["quiet", "debug", "dev"]: + return log_level.upper() + return "INFO" + + +def resolution_converter(resolution: str) -> tuple[str, str]: + try: + width, height = resolution.split("x") + # Check if width and height are integers but return strings + return str(int(width)), str(int(height)) + except ValueError: + raise ValueError( + "Custom Error", f"'{resolution}' is not of format 'x'!" + ) + + +def is_pi5() -> bool: + model_path = "/proc/device-tree/model" + pi5 = grep(model_path, "Raspberry Pi 5") + cm5 = grep(model_path, "Raspberry Pi Compute Module 5") + return bool(pi5) or bool(cm5) diff --git a/crowsnest/v4l2/__init__.py b/crowsnest/v4l2/__init__.py new file mode 100644 index 00000000..3da1e45b --- /dev/null +++ b/crowsnest/v4l2/__init__.py @@ -0,0 +1 @@ +from . import constants, ctl, ioctl_macros, raw, utils diff --git a/crowsnest/v4l2/constants.py b/crowsnest/v4l2/constants.py new file mode 100644 index 00000000..b9fe8569 --- /dev/null +++ b/crowsnest/v4l2/constants.py @@ -0,0 +1,75 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +V4L2_CTRL_MAX_DIMS = 4 + +V4L2_CTRL_TYPE_INTEGER = 1 +V4L2_CTRL_TYPE_BOOLEAN = 2 +V4L2_CTRL_TYPE_MENU = 3 +V4L2_CTRL_TYPE_BUTTON = 4 +V4L2_CTRL_TYPE_INTEGER64 = 5 +V4L2_CTRL_TYPE_CTRL_CLASS = 6 +V4L2_CTRL_TYPE_STRING = 7 +V4L2_CTRL_TYPE_BITMASK = 8 +V4L2_CTRL_TYPE_INTEGER_MENU = 9 + +V4L2_BUF_TYPE_VIDEO_CAPTURE = 1 +V4L2_BUF_TYPE_VIDEO_OUTPUT = 2 +V4L2_BUF_TYPE_VIDEO_OVERLAY = 3 +V4L2_BUF_TYPE_VBI_CAPTURE = 4 +V4L2_BUF_TYPE_VBI_OUTPUT = 5 +V4L2_BUF_TYPE_SLICED_VBI_CAPTURE = 6 +V4L2_BUF_TYPE_SLICED_VBI_OUTPUT = 7 +V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8 +V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE = 9 +V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE = 10 +V4L2_BUF_TYPE_SDR_CAPTURE = 11 +V4L2_BUF_TYPE_SDR_OUTPUT = 12 +V4L2_BUF_TYPE_META_CAPTURE = 13 +V4L2_BUF_TYPE_META_OUTPUT = 14 + +V4L2_FMT_FLAG_COMPRESSED = 0x0001 +V4L2_FMT_FLAG_EMULATED = 0x0002 +V4L2_FMT_FLAG_CONTINUOUS_BYTESTREAM = 0x0004 +V4L2_FMT_FLAG_DYN_RESOLUTION = 0x0008 +V4L2_FMT_FLAG_ENC_CAP_FRAME_INTERVAL = 0x0010 +V4L2_FMT_FLAG_CSC_COLORSPACE = 0x0020 +V4L2_FMT_FLAG_CSC_XFER_FUNC = 0x0040 +V4L2_FMT_FLAG_CSC_YCBCR_ENC = 0x0080 +V4L2_FMT_FLAG_CSC_HSV_ENC = V4L2_FMT_FLAG_CSC_YCBCR_ENC +V4L2_FMT_FLAG_CSC_QUANTIZATION = 0x0100 + +V4L2_FRMSIZE_TYPE_DISCRETE = 1 +V4L2_FRMSIZE_TYPE_CONTINUOUS = 2 +V4L2_FRMSIZE_TYPE_STEPWISE = 3 + +V4L2_FRMIVAL_TYPE_DISCRETE = 1 +V4L2_FRMIVAL_TYPE_CONTINUOUS = 2 +V4L2_FRMIVAL_TYPE_STEPWISE = 3 + +# Control flags +V4L2_CTRL_FLAG_DISABLED = 0x0001 +V4L2_CTRL_FLAG_GRABBED = 0x0002 +V4L2_CTRL_FLAG_READ_ONLY = 0x0004 +V4L2_CTRL_FLAG_UPDATE = 0x0008 +V4L2_CTRL_FLAG_INACTIVE = 0x0010 +V4L2_CTRL_FLAG_SLIDER = 0x0020 +V4L2_CTRL_FLAG_WRITE_ONLY = 0x0040 +V4L2_CTRL_FLAG_VOLATILE = 0x0080 +V4L2_CTRL_FLAG_HAS_PAYLOAD = 0x0100 +V4L2_CTRL_FLAG_EXECUTE_ON_WRITE = 0x0200 +V4L2_CTRL_FLAG_MODIFY_LAYOUT = 0x0400 +V4L2_CTRL_FLAG_DYNAMIC_ARRAY = 0x0800 +# Query flags, to be ORed with the control ID +V4L2_CTRL_FLAG_NEXT_CTRL = 0x80000000 +V4L2_CTRL_FLAG_NEXT_COMPOUND = 0x40000000 +# User-class control IDs defined by V4L2 +V4L2_CID_MAX_CTRLS = 1024 +# IDs reserved for driver specific controls +V4L2_CID_PRIVATE_BASE = 0x08000000 diff --git a/crowsnest/v4l2/ctl.py b/crowsnest/v4l2/ctl.py new file mode 100644 index 00000000..2501619c --- /dev/null +++ b/crowsnest/v4l2/ctl.py @@ -0,0 +1,281 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import copy +import os +from typing import Optional + +from . import constants, raw, utils + +dev_ctls: dict[str, dict[str, dict[str, (raw.v4l2_ext_control, str)]]] = {} + + +def parse_qc(fd: int, qc: raw.v4l2_query_ext_ctrl) -> dict: + """ + Parses the query control to an easy to use dictionary + """ + if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: + return {} + controls = {} + controls["type"] = utils.v4l2_ctrl_type_to_string(qc.type) + if qc.type in (constants.V4L2_CTRL_TYPE_INTEGER, constants.V4L2_CTRL_TYPE_MENU): + controls["min"] = qc.minimum + controls["max"] = qc.maximum + if qc.type == constants.V4L2_CTRL_TYPE_INTEGER: + controls["step"] = qc.step + if qc.type in ( + constants.V4L2_CTRL_TYPE_INTEGER, + constants.V4L2_CTRL_TYPE_MENU, + constants.V4L2_CTRL_TYPE_INTEGER_MENU, + constants.V4L2_CTRL_TYPE_BOOLEAN, + ): + controls["default"] = qc.default_value + if qc.flags: + controls["flags"] = utils.ctrlflags2str(qc.flags) + if qc.type in ( + constants.V4L2_CTRL_TYPE_MENU, + constants.V4L2_CTRL_TYPE_INTEGER_MENU, + ): + controls["menu"] = {} + for menu in utils.ioctl_iter( + fd, + raw.VIDIOC_QUERYMENU, + raw.v4l2_querymenu(id=qc.id), + qc.minimum, + qc.maximum + 1, + qc.step, + True, + ): + if qc.type == constants.V4L2_CTRL_TYPE_MENU: + controls["menu"][menu.index] = menu.name.decode() + else: + controls["menu"][menu.index] = menu.value + return controls + + +def parse_qc_of_path(device_path: str, qc: raw.v4l2_query_ext_ctrl) -> dict: + """ + Parses the query control to an easy to use dictionary + """ + fd = None + try: + fd = os.open(device_path, os.O_RDWR) + controls = parse_qc(fd, qc) + return controls + except OSError: + return {} + finally: + if fd is not None: + os.close(fd) + + +def init_device(device_path: str) -> bool: + """ + Initialize a given device + """ + fd = None + try: + fd = os.open(device_path, os.O_RDWR) + next_fl = ( + constants.V4L2_CTRL_FLAG_NEXT_CTRL | constants.V4L2_CTRL_FLAG_NEXT_COMPOUND + ) + qctrl = raw.v4l2_query_ext_ctrl(id=next_fl) + dev_ctls[device_path] = {} + for qc in utils.ioctl_iter(fd, raw.VIDIOC_QUERY_EXT_CTRL, qctrl): + if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: + name = qc.name.decode() + else: + name = utils.name2var(qc.name.decode()) + dev_ctls[device_path][name] = { + "qc": copy.deepcopy(qc), + "values": parse_qc(fd, qc), + } + qc.id |= next_fl + return True + except OSError: + return False + finally: + if fd is not None: + os.close(fd) + + +def get_query_controls(device_path: str) -> dict[str, raw.v4l2_ext_control]: + """ + Initialize a given device + """ + fd = None + try: + fd = os.open(device_path, os.O_RDWR) + next_fl = ( + constants.V4L2_CTRL_FLAG_NEXT_CTRL | constants.V4L2_CTRL_FLAG_NEXT_COMPOUND + ) + qctrl = raw.v4l2_query_ext_ctrl(id=next_fl) + query_controls: dict[str, raw.v4l2_query_ext_ctrl] = {} + utils.ioctl_safe(fd, raw.VIDIOC_G_EXT_CTRLS, qctrl) + for qc in utils.ioctl_iter(fd, raw.VIDIOC_QUERY_EXT_CTRL, qctrl): + if qc.type == constants.V4L2_CTRL_TYPE_CTRL_CLASS: + name = qc.name.decode() + else: + name = utils.name2var(qc.name.decode()) + query_controls[name] = copy.deepcopy(qc) + qc.id |= next_fl + return query_controls + except OSError: + return {} + finally: + if fd is not None: + os.close(fd) + + +def get_dev_ctl(device_path: str) -> Optional[dict]: + if device_path not in dev_ctls: + init_successfull = init_device(device_path) + if not init_successfull: + return None + return dev_ctls[device_path] + + +def get_dev_ctl_parsed_dict(device_path: str) -> dict: + if device_path not in dev_ctls and not init_device(device_path): + return {} + return utils.ctl_to_parsed_dict(dev_ctls[device_path]) + + +def get_dev_path_by_name(name: str) -> str: + """ + Get the device path by its name + """ + prefix = "video" + for dev in os.listdir("/dev"): + is_video_device = dev.startswith(prefix) and dev[len(prefix) :].isdigit() + if not is_video_device: + continue + path = f"/dev/{dev}" + card = get_camera_capabilities(path).get("card", "") + if name in card: + return path + return "" + + +def get_camera_capabilities(device_path: str) -> dict: + """ + Get the capabilities of a given device + """ + fd = None + try: + fd = os.open(device_path, os.O_RDWR) + cap = raw.v4l2_capability() + utils.ioctl_safe(fd, raw.VIDIOC_QUERYCAP, cap) + cap_dict = { + "driver": cap.driver.decode(), + "card": cap.card.decode(), + "bus": cap.bus_info.decode(), + "version": cap.version, + "capabilities": cap.capabilities, + } + return cap_dict + except OSError: + return {} + finally: + if fd is not None: + os.close(fd) + + +def get_control_cur_value(device_path: str, control: str) -> int: + """ + Get the current value of a control of a given device + """ + qc: raw.v4l2_query_ext_ctrl = dev_ctls[device_path][utils.name2var(control)]["qc"] + return get_control_cur_value_with_qc(device_path, qc) + + +def get_control_cur_value_with_qc( + device_path: str, qc: raw.v4l2_query_ext_ctrl +) -> Optional[int]: + """ + Get the current value of a control of a given device + """ + fd = None + try: + fd = os.open(device_path, os.O_RDWR) + ctrl = raw.v4l2_control() + ctrl.id = qc.id + utils.ioctl_safe(fd, raw.VIDIOC_G_CTRL, ctrl) + return ctrl.value + except OSError: + return None + finally: + if fd is not None: + os.close(fd) + + +def set_control(device_path: str, control: str, value: int) -> bool: + """ + Set the value of a control of a given device + """ + key = utils.name2var(control) + qc: raw.v4l2_query_ext_ctrl = dev_ctls[device_path][key]["qc"] + return set_control_with_qc(device_path, qc, value) + + +def set_control_with_qc( + device_path: str, qc: raw.v4l2_query_ext_ctrl, value: int +) -> bool: + fd = None + try: + fd = os.open(device_path, os.O_RDWR) + ctrl = raw.v4l2_control() + ctrl.id = qc.id + ctrl.value = value + if utils.ioctl_safe(fd, raw.VIDIOC_S_CTRL, ctrl) != -1: + return True + except OSError: + pass + finally: + if fd is not None: + os.close(fd) + return False + + +def get_formats(device_path: str) -> dict: + """ + Get the available formats of a given device + """ + fd = None + try: + fd = os.open(device_path, os.O_RDWR) + fmt_desc = raw.v4l2_fmtdesc() + frmsize = raw.v4l2_frmsizeenum() + frmival = raw.v4l2_frmivalenum() + fmt_desc.index = 0 + fmt_desc.type = constants.V4L2_BUF_TYPE_VIDEO_CAPTURE + formats = {} + for fmt in utils.ioctl_iter(fd, raw.VIDIOC_ENUM_FMT, fmt_desc): + format_str = f"[{fmt.index}]: '{utils.fcc2s(fmt.pixelformat)}' ({fmt.description.decode()}" + if fmt.flags: + format_str += f", {utils.fmtflags2str(fmt.flags)}" + format_str += ")" + formats[format_str] = {} + frmsize.pixel_format = fmt.pixelformat + for size in utils.ioctl_iter(fd, raw.VIDIOC_ENUM_FRAMESIZES, frmsize): + size_str = utils.frmsize_to_str(size) + formats[format_str][size_str] = [] + frmival.pixel_format = fmt.pixelformat + frmival.width = frmsize.discrete.width + frmival.height = frmsize.discrete.height + for interval in utils.ioctl_iter( + fd, raw.VIDIOC_ENUM_FRAMEINTERVALS, frmival + ): + formats[format_str][size_str].append(utils.frmival_to_str(interval)) + return formats + except OSError: + return {} + finally: + if fd is not None: + os.close(fd) diff --git a/crowsnest/v4l2/ioctl_macros.py b/crowsnest/v4l2/ioctl_macros.py new file mode 100644 index 00000000..58e0de64 --- /dev/null +++ b/crowsnest/v4l2/ioctl_macros.py @@ -0,0 +1,85 @@ +# Methods to create IOCTL requests +# +# Copyright (C) 2023 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license + +from __future__ import annotations + +import ctypes +from typing import TYPE_CHECKING, Type, Union + +""" +This module contains of Python port of the macros available in +"/include/uapi/asm-generic/ioctl.h" from the linux kernel. +""" + +if TYPE_CHECKING: + IOCParamSize = Union[int, str, Type[ctypes._CData]] + +_IOC_NRBITS = 8 +_IOC_TYPEBITS = 8 + +# NOTE: The following could be platform specific. +_IOC_SIZEBITS = 14 +_IOC_DIRBITS = 2 + +_IOC_NRMASK = (1 << _IOC_NRBITS) - 1 +_IOC_TYPEMASK = (1 << _IOC_TYPEBITS) - 1 +_IOC_SIZEMASK = (1 << _IOC_SIZEBITS) - 1 +_IOC_DIRMASK = (1 << _IOC_DIRBITS) - 1 + +_IOC_NRSHIFT = 0 +_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS +_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS +_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS + +# The constants below may also be platform specific +IOC_NONE = 0 +IOC_WRITE = 1 +IOC_READ = 2 + + +def _check_value(val: int, name: str, maximum: int): + if val > maximum: + raise ValueError(f"Value '{val}' for '{name}' exceeds max of {maximum}") + + +def _IOC_TYPECHECK(param_size: IOCParamSize) -> int: + if isinstance(param_size, int): + return param_size + elif isinstance(param_size, bytearray): + return len(param_size) + elif isinstance(param_size, str): + ctcls = getattr(ctypes, param_size) + return ctypes.sizeof(ctcls) + return ctypes.sizeof(param_size) + + +def IOC(direction: int, cmd_type: int, cmd_number: int, param_size: int) -> int: + _check_value(direction, "direction", _IOC_DIRMASK) + _check_value(cmd_type, "cmd_type", _IOC_TYPEMASK) + _check_value(cmd_number, "cmd_number", _IOC_NRMASK) + _check_value(param_size, "ioc_size", _IOC_SIZEMASK) + return ( + (direction << _IOC_DIRSHIFT) + | (param_size << _IOC_SIZESHIFT) + | (cmd_type << _IOC_TYPESHIFT) + | (cmd_number << _IOC_NRSHIFT) + ) + + +def IO(cmd_type: int, cmd_number: int) -> int: + return IOC(IOC_NONE, cmd_type, cmd_number, 0) + + +def IOR(cmd_type: int, cmd_number: int, param_size: IOCParamSize) -> int: + return IOC(IOC_READ, cmd_type, cmd_number, _IOC_TYPECHECK(param_size)) + + +def IOW(cmd_type: int, cmd_number: int, param_size: IOCParamSize) -> int: + return IOC(IOC_WRITE, cmd_type, cmd_number, _IOC_TYPECHECK(param_size)) + + +def IOWR(cmd_type: int, cmd_number: int, param_size: IOCParamSize) -> int: + return IOC(IOC_READ | IOC_WRITE, cmd_type, cmd_number, _IOC_TYPECHECK(param_size)) diff --git a/crowsnest/v4l2/raw.py b/crowsnest/v4l2/raw.py new file mode 100644 index 00000000..5ba5202b --- /dev/null +++ b/crowsnest/v4l2/raw.py @@ -0,0 +1,195 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import ctypes + +from . import constants, ioctl_macros + + +class v4l2_capability(ctypes.Structure): + _fields_ = [ + ("driver", ctypes.c_char * 16), + ("card", ctypes.c_char * 32), + ("bus_info", ctypes.c_char * 32), + ("version", ctypes.c_uint32), + ("capabilities", ctypes.c_uint32), + ("device_caps", ctypes.c_uint32), + ("reserved", ctypes.c_uint32 * 3), + ] + + +class v4l2_fmtdesc(ctypes.Structure): + _fields_ = [ + ("index", ctypes.c_uint32), + ("type", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ("description", ctypes.c_char * 32), + ("pixelformat", ctypes.c_uint32), + ("mbus_code", ctypes.c_uint32), + ("reserved", ctypes.c_uint32 * 3), + ] + + +class v4l2_control(ctypes.Structure): + _fields_ = [("id", ctypes.c_uint32), ("value", ctypes.c_int32)] + + +class v4l2_queryctrl(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_uint32), + ("type", ctypes.c_uint32), + ("name", ctypes.c_char * 32), + ("minimum", ctypes.c_int32), + ("maximum", ctypes.c_int32), + ("step", ctypes.c_int32), + ("default_value", ctypes.c_int32), + ("flags", ctypes.c_uint32), + ("reserved", ctypes.c_uint32 * 2), + ] + + +class v4l2_querymenu(ctypes.Structure): + class UnionNameValue(ctypes.Union): + _fields_ = [("name", ctypes.c_char * 32), ("value", ctypes.c_int64)] + + _pack_ = True + _fields_ = [ + ("id", ctypes.c_uint32), + ("index", ctypes.c_uint32), + ("union", UnionNameValue), + ("reserved", ctypes.c_uint32), + ] + _anonymous_ = ("union",) + + +class v4l2_ext_control(ctypes.Structure): + _pack_ = True + + class ValueUnion(ctypes.Union): + _fields_ = [ + ("value", ctypes.c_int32), + ("value64", ctypes.c_int64), + ("string", ctypes.POINTER(ctypes.c_char)), + ("p_u8", ctypes.POINTER(ctypes.c_uint8)), + ("p_u16", ctypes.POINTER(ctypes.c_uint16)), + ("p_u32", ctypes.POINTER(ctypes.c_uint32)), + ("p_s32", ctypes.POINTER(ctypes.c_int32)), + ("p_s64", ctypes.POINTER(ctypes.c_int64)), + ("ptr", ctypes.POINTER(None)), + ] + + _fields_ = [ + ("id", ctypes.c_uint32), + ("size", ctypes.c_uint32), + ("reserved2", ctypes.c_uint32 * 1), + ("union", ValueUnion), + ] + _anonymous_ = ("union",) + + +class v4l2_ext_controls(ctypes.Structure): + class UnionControls(ctypes.Union): + _fields_ = [("ctrl_class", ctypes.c_uint32), ("which", ctypes.c_uint32)] + + _fields_ = [ + ("union", UnionControls), + ("count", ctypes.c_uint32), + ("error_idx", ctypes.c_uint32), + ("request_fd", ctypes.c_int32), + ("reserved", ctypes.c_uint32 * 1), + ("controls", ctypes.POINTER(v4l2_ext_control)), + ] + _anonymous_ = ("union",) + + +class v4l2_frmsize_discrete(ctypes.Structure): + _fields_ = [("width", ctypes.c_uint32), ("height", ctypes.c_uint32)] + + +class v4l2_frmsize_stepwise(ctypes.Structure): + _fields_ = [ + ("min_width", ctypes.c_uint32), + ("max_width", ctypes.c_uint32), + ("step_width", ctypes.c_uint32), + ("min_height", ctypes.c_uint32), + ("max_height", ctypes.c_uint32), + ("step_height", ctypes.c_uint32), + ] + + +class v4l2_frmsizeenum(ctypes.Structure): + class FrmSize(ctypes.Union): + _fields_ = [ + ("discrete", v4l2_frmsize_discrete), + ("stepwise", v4l2_frmsize_stepwise), + ] + + _fields_ = [ + ("index", ctypes.c_uint32), + ("pixel_format", ctypes.c_uint32), + ("type", ctypes.c_uint32), + ("union", FrmSize), + ("reserved", ctypes.c_uint32 * 2), + ] + _anonymous_ = ("union",) + + +class v4l2_fract(ctypes.Structure): + _fields_ = [("numerator", ctypes.c_uint32), ("denominator", ctypes.c_uint32)] + + +class v4l2_frmival_stepwise(ctypes.Structure): + _fields_ = [("min", v4l2_fract), ("max", v4l2_fract), ("step", v4l2_fract)] + + +class v4l2_frmivalenum(ctypes.Structure): + class FrmIval(ctypes.Union): + _fields_ = [("discrete", v4l2_fract), ("stepwise", v4l2_frmival_stepwise)] + + _fields_ = [ + ("index", ctypes.c_uint32), + ("pixel_format", ctypes.c_uint32), + ("width", ctypes.c_uint32), + ("height", ctypes.c_uint32), + ("type", ctypes.c_uint32), + ("union", FrmIval), + ("reserved", ctypes.c_uint32 * 2), + ] + _anonymous_ = ("union",) + + +class v4l2_query_ext_ctrl(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_uint32), + ("type", ctypes.c_uint32), + ("name", ctypes.c_char * 32), + ("minimum", ctypes.c_int64), + ("maximum", ctypes.c_int64), + ("step", ctypes.c_uint64), + ("default_value", ctypes.c_int64), + ("flags", ctypes.c_uint32), + ("elem_size", ctypes.c_uint32), + ("elems", ctypes.c_uint32), + ("nr_of_dims", ctypes.c_uint32), + ("dim", ctypes.c_uint32 * constants.V4L2_CTRL_MAX_DIMS), + ("reserved", ctypes.c_uint32 * 32), + ] + + +VIDIOC_QUERYCAP = ioctl_macros.IOR(ord("V"), 0, v4l2_capability) +VIDIOC_ENUM_FMT = ioctl_macros.IOWR(ord("V"), 2, v4l2_fmtdesc) +VIDIOC_G_CTRL = ioctl_macros.IOWR(ord("V"), 27, v4l2_control) +VIDIOC_S_CTRL = ioctl_macros.IOWR(ord("V"), 28, v4l2_control) +VIDIOC_QUERYCTRL = ioctl_macros.IOWR(ord("V"), 36, v4l2_queryctrl) +VIDIOC_QUERYMENU = ioctl_macros.IOWR(ord("V"), 37, v4l2_querymenu) +VIDIOC_G_EXT_CTRLS = ioctl_macros.IOWR(ord("V"), 71, v4l2_ext_controls) +VIDIOC_S_EXT_CTRLS = ioctl_macros.IOWR(ord("V"), 72, v4l2_ext_controls) +VIDIOC_ENUM_FRAMESIZES = ioctl_macros.IOWR(ord("V"), 74, v4l2_frmsizeenum) +VIDIOC_ENUM_FRAMEINTERVALS = ioctl_macros.IOWR(ord("V"), 75, v4l2_frmivalenum) +VIDIOC_QUERY_EXT_CTRL = ioctl_macros.IOWR(ord("V"), 103, v4l2_query_ext_ctrl) diff --git a/crowsnest/v4l2/utils.py b/crowsnest/v4l2/utils.py new file mode 100644 index 00000000..cbcc5b20 --- /dev/null +++ b/crowsnest/v4l2/utils.py @@ -0,0 +1,182 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import ctypes +import errno +import fcntl +import re +from typing import Generator + +from . import constants, raw + + +def ioctl_safe(fd: int, request: int, arg: ctypes.Structure) -> int: + try: + return fcntl.ioctl(fd, request, arg) + except OSError: + return -1 + + +def ioctl_iter( + fd: int, + cmd: int, + struct: ctypes.Structure, + start=0, + stop=128, + step=1, + ignore_einval=False, +) -> Generator[ctypes.Structure, None, None]: + for i in range(start, stop, step): + struct.index = i + try: + fcntl.ioctl(fd, cmd, struct) + yield struct + except OSError as e: + if e.errno == errno.EINVAL and ignore_einval: + continue + break + + +def v4l2_ctrl_type_to_string(ctrl_type: int) -> str: + dict_ctrl_type = { + constants.V4L2_CTRL_TYPE_INTEGER: "int", + constants.V4L2_CTRL_TYPE_BOOLEAN: "bool", + constants.V4L2_CTRL_TYPE_MENU: "menu", + constants.V4L2_CTRL_TYPE_BUTTON: "button", + constants.V4L2_CTRL_TYPE_INTEGER64: "int64", + constants.V4L2_CTRL_TYPE_CTRL_CLASS: "ctrl_class", + constants.V4L2_CTRL_TYPE_STRING: "str", + constants.V4L2_CTRL_TYPE_BITMASK: "bitmask", + constants.V4L2_CTRL_TYPE_INTEGER_MENU: "intmenu", + } + return dict_ctrl_type.get(ctrl_type, "unknown") + + +def name2var(name: str) -> str: + return re.sub("[^0-9a-zA-Z]+", "_", name).lower() + + +def flags2str(dict_flags: dict, flags: int) -> str: + return ", ".join(v for k, v in dict_flags.items() if k and (flags & k)) + + +def ctrlflags2str(flags: int) -> str: + dict_flags = { + constants.V4L2_CTRL_FLAG_GRABBED: "grabbed", + constants.V4L2_CTRL_FLAG_DISABLED: "disabled", + constants.V4L2_CTRL_FLAG_READ_ONLY: "read-only", + constants.V4L2_CTRL_FLAG_UPDATE: "update", + constants.V4L2_CTRL_FLAG_INACTIVE: "inactive", + constants.V4L2_CTRL_FLAG_SLIDER: "slider", + constants.V4L2_CTRL_FLAG_WRITE_ONLY: "write-only", + constants.V4L2_CTRL_FLAG_VOLATILE: "volatile", + constants.V4L2_CTRL_FLAG_HAS_PAYLOAD: "has-payload", + constants.V4L2_CTRL_FLAG_EXECUTE_ON_WRITE: "execute-on-write", + constants.V4L2_CTRL_FLAG_MODIFY_LAYOUT: "modify-layout", + constants.V4L2_CTRL_FLAG_DYNAMIC_ARRAY: "dynamic-array", + 0: None, + } + return flags2str(dict_flags, flags) + + +def fmtflags2str(flags: int) -> str: + dict_flags = { + constants.V4L2_FMT_FLAG_COMPRESSED: "compressed", + constants.V4L2_FMT_FLAG_EMULATED: "emulated", + constants.V4L2_FMT_FLAG_CONTINUOUS_BYTESTREAM: "continuous-bytestream", + constants.V4L2_FMT_FLAG_DYN_RESOLUTION: "dyn-resolution", + constants.V4L2_FMT_FLAG_ENC_CAP_FRAME_INTERVAL: "enc-cap-frame-interval", + constants.V4L2_FMT_FLAG_CSC_COLORSPACE: "csc-colorspace", + constants.V4L2_FMT_FLAG_CSC_YCBCR_ENC: "csc-ycbcr-enc", + constants.V4L2_FMT_FLAG_CSC_QUANTIZATION: "csc-quantization", + constants.V4L2_FMT_FLAG_CSC_XFER_FUNC: "csc-xfer-func", + } + return flags2str(dict_flags, flags) + + +def fcc2s(val: int) -> str: + s = "" + s += chr(val & 0x7F) + s += chr((val >> 8) & 0x7F) + s += chr((val >> 16) & 0x7F) + s += chr((val >> 24) & 0x7F) + return s + + +def frmtype2s(type) -> str: + types = ["Unknown", "Discrete", "Continuous", "Stepwise"] + if type >= len(types): + return "Unknown" + return types[type] + + +def fract2sec(fract: raw.v4l2_fract) -> str: + if fract.denominator == 0: + return "0.000" + return f"{fract.numerator / fract.denominator:.3f}" + + +def fract2fps(fract: raw.v4l2_fract) -> str: + if fract.numerator == 0: + return "0.000" + return f"{fract.denominator / fract.numerator:.3f}" + + +def frmsize_to_str(frmsize: raw.v4l2_frmsizeenum) -> str: + string = f"Size: {frmtype2s(frmsize.type)} " + if frmsize.type == constants.V4L2_FRMSIZE_TYPE_DISCRETE: + string += f"{frmsize.discrete.width}x{frmsize.discrete.height}" + elif frmsize.type == constants.V4L2_FRMSIZE_TYPE_CONTINUOUS: + sw = frmsize.stepwise + string += f"{sw.min_width}x{sw.min_height} - {sw.max_width}x{sw.max_height}" + elif frmsize.type == constants.V4L2_FRMSIZE_TYPE_STEPWISE: + sw = frmsize.stepwise + string += ( + f"{sw.min_width}x{sw.min_height} - " + f"{sw.max_width}x{sw.max_height} " + f"with step {sw.step_width}/{sw.step_height}" + ) + return string + + +def frmival_to_str(frmival: raw.v4l2_frmivalenum) -> str: + string = f"Interval: {frmtype2s(frmival.type)} " + if frmival.type == constants.V4L2_FRMIVAL_TYPE_DISCRETE: + string += "%ss (%s fps)" % ( + fract2sec(frmival.discrete), + fract2fps(frmival.discrete), + ) + elif frmival.type == constants.V4L2_FRMIVAL_TYPE_CONTINUOUS: + string += "%ss - %ss (%s-%s fps)" % ( + fract2sec(frmival.stepwise.min), + fract2sec(frmival.stepwise.max), + fract2fps(frmival.stepwise.max), + fract2fps(frmival.stepwise.min), + ) + elif frmival.type == constants.V4L2_FRMIVAL_TYPE_STEPWISE: + string += "%ss - %ss with step %ss (%s-%s fps)" % ( + fract2sec(frmival.stepwise.min), + fract2sec(frmival.stepwise.max), + fract2sec(frmival.stepwise.step), + fract2fps(frmival.stepwise.max), + fract2fps(frmival.stepwise.min), + ) + return string + + +def ctl_to_parsed_dict(dev_ctl: raw.v4l2_ext_control) -> dict: + values = {} + cur_sec = "" + for control, cur_ctl in dev_ctl.items(): + if not cur_ctl["values"]: + cur_sec = control + values[cur_sec] = {} + continue + values[cur_sec][control] = cur_ctl["values"] + return values diff --git a/crowsnest/watchdog.py b/crowsnest/watchdog.py new file mode 100644 index 00000000..868a065d --- /dev/null +++ b/crowsnest/watchdog.py @@ -0,0 +1,39 @@ +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +import asyncio +import os + +from . import logger + +configured_devices: list[str] = [] +lost_devices: list[str] = [] +running = True + + +def crowsnest_watchdog(): + global configured_devices, lost_devices + prefix = "Watchdog: " + + for device in configured_devices: + if device.startswith("/base"): + continue + if device not in lost_devices and not os.path.exists(device): + lost_devices.append(device) + logger.log_quiet(f"Lost Device: '{device}'", prefix) + elif device in lost_devices and os.path.exists(device): + lost_devices.remove(device) + logger.log_quiet(f"Device '{device}' returned.", prefix) + + +async def run_watchdog(): + global running + while running: + crowsnest_watchdog() + await asyncio.sleep(120) diff --git a/libs/camera-streamer.sh b/libs/camera-streamer.sh deleted file mode 100755 index 83db8769..00000000 --- a/libs/camera-streamer.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/bin/bash - -#### camera-streamer library - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 - 2022 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit upon Errors -set -Ee - -function run_multi() { - local cams - cams="${1}" - for instance in ${cams} ; do - run_ayucamstream "${instance}" & - done -} - -function run_ayucamstream() { - local cam_sec ust_bin dev pt res rtsp rtsp_pt fps cstm noprx start_param - local v4l2ctl - cam_sec="${1}" - ust_bin="${BASE_CN_PATH}/bin/camera-streamer/camera-streamer" - dev="$(get_param "cam ${cam_sec}" device)" - pt=$(get_param "cam ${cam_sec}" port) - res=$(get_param "cam ${cam_sec}" resolution) - fps=$(get_param "cam ${cam_sec}" max_fps) - rtsp=$(get_param "cam ${cam_sec}" enable_rtsp) - rtsp_pt=$(get_param "cam ${cam_sec}" rtsp_port) - cstm="$(get_param "cam ${cam_sec}" custom_flags 2> /dev/null)" - noprx="$(get_param "crowsnest" no_proxy 2> /dev/null)" - ## construct start parameter - # set http port - # - start_param=( --http-port="${pt}" ) - - if [[ -n "${noprx}" ]] && [[ "${noprx}" = "true" ]]; then - # See https://github.com/mainsail-crew/crowsnest/pull/221#issuecomment-1863555700 for why - # we cannot assume the binary has support for `--http-listen`. - if ${ust_bin} --help | grep -q "http-listen"; then - start_param+=( --http-listen=0.0.0.0 ) - log_msg "INFO: Set to 'no_proxy' mode! Using 0.0.0.0!" - fi - fi - - # Set device - start_param+=( --camera-path="${dev}" ) - - # Detect libcamera device and add start param accordingly - if [[ "${dev}" =~ "/base/soc" ]]; then - start_param+=( --camera-type=libcamera ) - start_param+=( --camera-format=YUYV ) - fi - - if [[ "${dev}" =~ "/dev/video" ]] || - [[ "${dev}" =~ "/dev/v4l/" ]]; then - start_param+=( --camera-type=v4l2 ) - fi - - # Use MJPEG Hardware encoder if possible - if [ "$(detect_mjpeg "${cam_sec}")" = "1" ] && - [[ ! "${dev}" =~ "/base/soc" ]]; then - start_param+=( --camera-format=MJPG ) - fi - - # Set resolution - get_height_val() { - (sed 's/#.*//' | cut -d'x' -f2) <<< "${res}" - } - get_width_val() { - (sed 's/#.*//' | cut -d'x' -f1) <<< "${res}" - } - - # Set snapshot heigth to 1080p by default - start_param+=( --camera-snapshot.height=1080 ) - - start_param+=( --camera-width="$(get_width_val)" ) - start_param+=( --camera-height="$(get_height_val)" ) - - # Set FPS - start_param+=( --camera-fps="${fps}" ) - - # Enable rtsp, if set true - if [[ -n "${rtsp}" ]] && [[ "${rtsp}" == "true" ]]; then - # ensure a port is set - start_param+=( --rtsp-port="${rtsp_pt:-8554}" ) - fi - - # Enable camera-auto_reconnect by default - start_param+=( --camera-auto_reconnect=1 ) - - # Custom Flag Handling (append to defaults) - if [[ -n "${cstm}" ]]; then - start_param+=( "${cstm}" ) - fi - - # v4l2 option handling - v4l2ctl="$(get_param "cam ${cam_sec}" v4l2ctl)" - if [ -n "${v4l2ctl}" ]; then - IFS="," read -ra opt < <(echo "${v4l2ctl}" | tr -d " "); unset IFS - log_msg "V4L2 Control: Handling done by camera-streamer ..." - log_msg "V4L2 Control: Trying to set: ${v4l2ctl}" - # loop through options - for param in "${opt[@]}"; do - start_param+=( --camera-options="${param}" ) - done - fi - - - # Log start_param - log_msg "Starting camera-streamer with Device ${dev} ..." - echo "Parameters: ${start_param[*]}" | \ - log_output "camera-streamer [cam ${cam_sec}]" - # Start camera-streamer - echo "${start_param[*]}" | xargs "${ust_bin}" 2>&1 | \ - log_output "camera-streamer [cam ${cam_sec}]" - # Should not be seen else failed. - log_msg "ERROR: Start of camera-streamer [cam ${cam_sec}] failed!" -} diff --git a/libs/configparser.sh b/libs/configparser.sh deleted file mode 100755 index 60acb80d..00000000 --- a/libs/configparser.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/bin/bash - -#### Configparser library - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit upon Errors -set -Ee - -# Read Configuration File -# call get_param section param -# spits out raw value -function get_param { - local cfg section param - cfg="${CROWSNEST_CFG}" - section="${1}" - param="${2}" - crudini --get "${cfg}" "${section}" "${param}" 2> /dev/null | \ - sed 's/\#.*//;s/[[:space:]]*$//' - return -} - -# Check for existing file -# Exit with error if not exist -function check_cfg { - if [ ! -r "${1}" ]; then - log_msg "ERROR: No Configuration File found. Exiting!" - exit 1 - else - return 0 - fi -} - -## Spits out all [cam ] configured sections -function configured_cams { - local cams cfg - cfg="${CROWSNEST_CFG}" - for i in $(crudini --existing=file --get "${cfg}" | \ - sed '/crowsnest/d;s/cam//'); do - cams+=("${i}") - done - echo "${cams[@]}" - return -} - -# Checks [cam ] if all needed configuration sections are present -# call check_section ex.: check_section foobar -function check_section { - local section exist param - local -a must_exist missing - section="cam ${1}" - # Ignore missing custom flags - exist="$(crudini --existing=param --get "${CROWSNEST_CFG}" "${section}" \ - 2> /dev/null | sed '/custom_flags/d;/v4l2ctl/d')" - for i in ${exist}; do - param+=("${i}") - done - # Stop on deprecated conf - for i in "${param[@]}"; do - if [ "${i}" = "streamer" ]; then - deprecated_msg_1 - exit 1 - fi - done - must_exist=(mode port device resolution max_fps) - missing=() - for i in "${must_exist[@]}"; do - if [[ -z "$(get_param "${section}" "${i}")" ]]; then - missing+=("${i}") - fi - done - - if [[ "${#missing[@]}" != "0" ]]; then - for param in "${missing[@]}"; do - log_msg "ERROR: Parameter ${param} not found in Section [${section}]." - done - log_msg "ERROR: Please check your configuration!" - exit 1 - fi - if [[ "${#missing[@]}" == "0" ]]; then - log_msg "INFO: Configuration of Section [${section}] looks good. Continue ..." - fi - return -} diff --git a/libs/core.sh b/libs/core.sh deleted file mode 100755 index 692884d3..00000000 --- a/libs/core.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/bin/bash - -#### Core library - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit upon Errors -set -Ee - -## Version of crowsnest -function self_version { - pushd "${BASE_CN_PATH}" &> /dev/null - git describe --always --tags - popd &> /dev/null -} - -# Init Traps -trap 'shutdown' 1 2 3 15 -trap 'err_exit $? $LINENO' ERR - -# Behavior of traps -# log_msg, see libs/logging.sh L#46 - -# Print Error Code and Line to Log -# and kill running jobs -function err_exit { - local file_trace func_trace line_trace - read -r LINE FUNC FILE < <(caller 0) - func_trace="${FUNC}" - file_trace="$(basename "${FILE}")" - line_trace="${LINE}" - if [ "${1}" != "0" ]; then - log_msg "ERROR: Error ${1} occured on line ${line_trace}" - log_msg "==> Error occured in file: ${file_trace} -> ${func_trace}" - log_msg "ERROR: Stopping $(basename "$0")." - log_msg "Goodbye..." - fi - if [ -n "$(jobs -pr)" ]; then - jobs -pr | while IFS='' read -r job_id; do - kill "${job_id}" - done - fi - exit 1 -} - -# Print Goodbye Message -# and kill running jobs -function shutdown { - log_msg "Shutdown or Killed by User!" - log_msg "Please come again :)" - if [ -n "$(jobs -pr)" ]; then - jobs -pr | while IFS='' read -r job_id; do - kill "${job_id}" - done - fi - log_msg "Goodbye..." - exit 0 -} - -## Sanity Checks -# Dependency Check -# call check_dep , ex.: check_dep vim -function check_dep { - local dep - dep="$(whereis "${1}" | awk '{print $2}')" - if [[ -z "${dep}" ]]; then - log_msg "Dependency: '${1}' not found. Exiting!" - exit 1 - else - log_msg "Dependency: '${1}' found in ${dep}." - fi -} - -function check_apps { - local cstreamer ustreamer - ustreamer="bin/ustreamer/src/ustreamer.bin" - cstreamer="bin/camera-streamer/camera-streamer" - if [[ -x "${BASE_CN_PATH}/${ustreamer}" ]]; then - log_msg "Dependency: 'ustreamer' found in ${ustreamer}." - UST_BIN="${BASE_CN_PATH}/${ustreamer}" - # shellcheck disable=SC2034 - declare -r UST_BIN - else - log_msg "Dependency: 'ustreamer' not found. Exiting!" - exit 1 - fi - - ## Avoid dependency check if non rpi sbc - if [[ "$(use_cs)" = "1" ]]; then - if [[ -x "${BASE_CN_PATH}/${cstreamer}" ]]; then - log_msg "Dependency: '${cstreamer##*/}' found in ${cstreamer}." - else - log_msg "Dependency: '${cstreamer##*/}' not found. Exiting!" - exit 1 - fi - fi -} - -# Check all needed Dependencies -# If pass print your set configfile to log. -# print_cfg, see libs/logging.sh L#75 -# pint_cams, see libs/logging.sh L#84 -function initial_check { - log_msg "INFO: Checking Dependencies" - check_dep "crudini" - check_dep "find" - check_dep "xargs" - check_apps - versioncontrol - # print cfg if ! "${CROWSNEST_LOG_LEVEL}": quiet - if [ -z "$(check_cfg "${CROWSNEST_CFG}")" ]; then - if [[ "${CROWSNEST_LOG_LEVEL}" != "quiet" ]]; then - print_cfg - fi - fi - log_msg "INFO: Detect available Devices" - print_cams - return -} diff --git a/libs/hwhandler.sh b/libs/hwhandler.sh deleted file mode 100755 index 19751170..00000000 --- a/libs/hwhandler.sh +++ /dev/null @@ -1,178 +0,0 @@ -#!/bin/bash - -#### Hardware Handling library - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit upon Errors -set -Ee - -### Detect Hardware -detect_avail_cams() { - local avail realpath - avail="$(find /dev/v4l/by-id/ -iname "*index0" 2> /dev/null)" - count="$(echo "${avail}" | wc -l)" - if [[ -d "/dev/v4l/by-id/" ]] && [[ -n "${avail}" ]]; then - log_msg "INFO: Found ${count} available v4l2 (UVC) camera(s)" - echo "${avail}" | while read -r v4l; do - realpath=$(readlink -e "${v4l}") - log_msg "${v4l} -> ${realpath}" - if [[ "${CROWSNEST_LOG_LEVEL}" != "quiet" ]]; then - list_cam_formats "${v4l}" - list_cam_v4l2ctrls "${v4l}" - fi - done - else - log_msg "INFO: No usable Cameras found." - fi -} - -## Used for "verbose" and "debug" logging in logging.sh -list_cam_formats() { - local device prefix - device="${1}" - prefix="$(date +'[%D %T]') crowsnest:" - log_msg "Supported Formats:" - while read -r i; do - printf "%s\t\t%s\n" "${prefix}" "${i}" >> "${CROWSNEST_LOG_PATH}" - done < <(v4l2-ctl -d "${device}" --list-formats-ext | sed '1,3d') -} - -list_cam_v4l2ctrls() { - local device prefix - device="${1}" - prefix="$(date +'[%D %T]') crowsnest:" - log_msg "Supported Controls:" - while read -r i; do - printf "%s\t\t%s\n" "${prefix}" "${i}" >> "${CROWSNEST_LOG_PATH}" - done < <(v4l2-ctl -d "${device}" --list-ctrls-menus) -} - -## Detect libcamera package -libcamera_installed() { - if [[ -x "$(command -v libcamera-hello)" ]] || - [[ -x "$(command -v rpicam-hello)" ]]; then - echo "1" - else - echo "0" - fi -} - -## List libcamera (CSI) device -list_libcameras() { - if [[ -x "$(command -v libcamera-hello)" ]]; then - libcamera-hello --list-cameras - else - rpicam-hello --list-cameras - fi -} - -## Determine connected libcamera (CSI) device -detect_libcamera() { - local avail - if [[ "$(is_raspberry_pi)" = "1" ]] && - [[ "$(libcamera_installed)" = "1" ]]; then - avail="$(list_libcameras | grep -c "Available" || echo "0")" - if [[ "${avail}" = "1" ]]; then - get_libcamera_path | wc -l - else - echo "0" - fi - else - echo "0" - fi -} - -## Split /base/soc path for libcamera device -get_libcamera_path() { - list_libcameras | sed '1,2d' \ - | grep "\(/base/*\)" | cut -d"(" -f2 | tr -d '$)' -} - -# print libcamera resolutions -list_picam_resolution() { - local prefix - prefix="$(date +'[%D %T]') crowsnest:" - log_msg "'libcamera' device(s) resolution(s) :" - while IFS= read -r i; do - printf "%s\t\t%s\n" "${prefix}" "${i}" >> "${CROWSNEST_LOG_PATH}" - done < <(list_libcameras | sed '1,2d;s/Modes:/Colorspace:/') -} - -get_libcamera_controls() { - local ust_bin flags - flags=( --camera-type=libcamera --camera-list_options ) - ust_bin="${BASE_CN_PATH}/bin/camera-streamer/camera-streamer" - if [[ -x "${ust_bin}" ]]; then - "${ust_bin}" "${flags[@]}" --camera-path="$(get_libcamera_path)" 2> /dev/null | \ - sed 's/device//g;/^SNAPSHOT/q' | sed '/^SNAPSHOT/d' | \ - sed '/^CAMERA/d;/- property/d' | sed '/camera-streamer Version:/d' | \ - sed 's/- available option: //g' | sed '/^$/d;' | \ - sed 's/([0-9]*[a-z,0-9]\, /(/g' | sed '/type=7/d;/type=4/d;/type=Rectangle/d' | \ - sed 's/type=1/bool/g;s/type=3/int/g;s/type=5/float/g' | sed 's/type=//g;' | \ - sed 's/\[/min=/g;s/\.\./ max=/g;s/\]$//g' - else - log_msg "WARN: 'libcamera' device option can not be displayed, because" - log_msg "WARN: camera-streamer is not installed" - fi -} - -list_picam_controls() { - local prefix - prefix="$(date +'[%D %T]') crowsnest:" - log_msg "'libcamera' device controls :" - while IFS= read -r i; do - if [[ ! "${i}" =~ "INFO" ]]; then - printf "%s\t\t%s\n" "${prefix}" "${i}" >>"${CROWSNEST_LOG_PATH}" - fi - done < <(get_libcamera_controls) - # blank line workaround - log_msg "" -} - -# Determine connected "legacy" device -function detect_legacy { - local avail - if [[ "$(is_raspberry_pi)" = "1" ]] && - command -v vcgencmd &> /dev/null; then - if vcgencmd get_camera &> /dev/null; then - avail="$( vcgencmd get_camera | awk -F '=' '{ print $3 }' | cut -d',' -f1)" - fi - fi - echo "${avail:-0}" -} - -function dev_is_legacy { - v4l2-ctl --list-devices | grep -A1 -e 'mmal' | \ - awk 'NR==2 {print $1}' -} - -## Determine if cam has H.264 Hardware encoder -## call detect_h264 ex.: detect_h264 foobar -## returns 1 = true / 0 = false ( numbers are strings! not int!) -detect_h264() { - local dev - dev="$(get_param "cam ${1}" device)" - v4l2-ctl -d "${dev}" --list-formats-ext | grep -c "[hH]264" -} - -## Determine if cam has MJPEG Hardware encoder -## call detect_mjpeg ex.: detect_mjpeg foobar -## returns 1 = true / 0 = false ( numbers are strings! not int!) -detect_mjpeg() { - local dev - dev="$(get_param "cam ${1}" device)" - v4l2-ctl -d "${dev}" --list-formats-ext | grep -c "Motion-JPEG, compressed" -} - -## Helper funcs -. "${BASE_CN_PATH}/libs/helper_fn.sh" diff --git a/libs/init_stream.sh b/libs/init_stream.sh deleted file mode 100755 index 70ee91be..00000000 --- a/libs/init_stream.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -#### Init Stream library - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit upon Errors -set -Ee - -## Start Stream Service -# sleep to prevent cpu cycle spikes -function construct_streamer { - local cams sleep_pid - # See configparser.sh L#53 - log_msg "Try to start configured Cams / Services..." - for cams in $(configured_cams); do - mode="$(get_param "cam ${cams}" mode)" - check_section "${cams}" - case ${mode} in - [mM]ulti | camera-streamer) - if [[ "$(use_cs)" = "1" ]]; then - MULTI_INSTANCES+=( "${cams}" ) - else - log_msg "WARN: Mode 'camera-streamer' is not supported on your device!" - log_msg "WARN: Falling back to Mode 'ustreamer'" - MJPG_INSTANCES+=( "${cams}" ) - fi - ;; - mjpg | mjpeg | ustreamer) - MJPG_INSTANCES+=( "${cams}" ) - ;; - ?|*) - unknown_mode_msg - MJPG_INSTANCES+=( "${cams}" ) - - ;; - esac - done - if [ "${#MULTI_INSTANCES[@]}" != "0" ]; then - run_multi "${MULTI_INSTANCES[*]}" - fi - if [ "${#MJPG_INSTANCES[@]}" != "0" ]; then - run_mjpg "${MJPG_INSTANCES[*]}" - fi - sleep 2 & sleep_pid="$!" ; wait "${sleep_pid}" - log_msg " ... Done!" -} diff --git a/libs/logging.sh b/libs/logging.sh deleted file mode 100755 index bb40a8e8..00000000 --- a/libs/logging.sh +++ /dev/null @@ -1,167 +0,0 @@ -#!/bin/bash - -#### Logging library - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit upon Errors -set -Ee - -## Logging -function set_log_path { - #Workaround sed ~ to BASH VAR $HOME - CROWSNEST_LOG_PATH=$(get_param "crowsnest" log_path | sed "s#^~#${HOME}#gi") - declare -g CROWSNEST_LOG_PATH - #Workaround: Make Dir if not exist - if [ ! -d "$(dirname "${CROWSNEST_LOG_PATH}")" ]; then - mkdir -p "$(dirname "${CROWSNEST_LOG_PATH}")" - fi -} - -function init_logging { - set_log_path - set_log_level - delete_log - log_msg "crowsnest - A webcam Service for multiple Cams and Stream Services." - log_msg "Version: $(self_version)" - log_msg "Prepare Startup ..." - print_host -} - -function set_log_level { - local loglevel - loglevel="$(get_param crowsnest log_level 2> /dev/null)" - # Set default log_level to quiet - if [ -z "${loglevel}" ] || [[ "${loglevel}" != @(quiet|verbose|debug) ]]; then - CROWSNEST_LOG_LEVEL="quiet" - else - CROWSNEST_LOG_LEVEL="${loglevel}" - fi - declare -r CROWSNEST_LOG_LEVEL -} - -function delete_log { - local del_log - del_log="$(get_param "crowsnest" delete_log 2> /dev/null)" - if [ "${del_log}" = "true" ]; then - rm -rf "${CROWSNEST_LOG_PATH}" - fi -} - -function log_msg { - local msg prefix - msg="${1}" - prefix="$(date +'[%D %T]') crowsnest:" - printf "%s %s\n" "${prefix}" "${msg}" >> "${CROWSNEST_LOG_PATH}" - printf "%s\n" "${msg}" -} - -#call '| log_output ""' -function log_output { - local prefix - prefix="DEBUG: ${1}" - while read -r line; do - if [[ "${CROWSNEST_LOG_LEVEL}" = "debug" ]]; then - log_msg "${prefix}: ${line}" - fi - done -} - -function print_cfg { - local prefix - prefix="$(date +'[%D %T]') crowsnest:" - log_msg "INFO: Print Configfile: '${CROWSNEST_CFG}'" - (sed '/^#.*/d;/./,$!d' | cut -d'#' -f1) < "${CROWSNEST_CFG}" | \ - while read -r line; do - printf "%s\t\t%s\n" "${prefix}" "${line}" >> "${CROWSNEST_LOG_PATH}" - printf "\t\t%s\n" "${line}" - done -} - -function print_cams { - local device total v4l - v4l="$(find /dev/v4l/by-id/ -iname "*index0" 2> /dev/null | wc -l)" - libcamera="$(detect_libcamera)" - legacy="$(detect_legacy)" - total="$((v4l+libcamera+legacy))" - if [ "${total}" -eq 0 ]; then - log_msg "ERROR: No usable Devices Found. Stopping $(basename "${0}")." - exit 1 - else - log_msg "INFO: Found ${total} total available Device(s)" - fi - if [[ "${libcamera}" -ne 0 ]]; then - if [[ "$(is_pi5)" = "1" ]]; then - log_msg "================================================================" - log_msg " WARN: 'libcamera' devices are currently not supported on Pi 5! " - log_msg "================================================================" - fi - for device in $(get_libcamera_path); do - log_msg "Detected 'libcamera' device -> ${device}" - done - if [[ "$(is_pi5)" = "0" ]]; then - list_picam_resolution - list_picam_controls - fi - fi - if [[ "${legacy}" -ne 0 ]]; then - raspicam="$(v4l2-ctl --list-devices | grep -A1 -e 'mmal' | \ - awk 'NR==2 {print $1}')" - log_msg "Detected 'Raspicam' Device -> ${raspicam}" - if [[ ! "${CROWSNEST_LOG_LEVEL}" = "quiet" ]]; then - list_cam_formats "${raspicam}" - list_cam_v4l2ctrls "${raspicam}" - fi - fi - if [[ -d "/dev/v4l/by-id/" ]]; then - detect_avail_cams - fi -} - -function print_host { - local disksize generic_model memtotal sbc_model - generic_model="$(grep "model name" /proc/cpuinfo | cut -d':' -f2 | awk NR==1)" - sbc_model="$(grep "Model" /proc/cpuinfo | cut -d':' -f2)" - memtotal="$(grep "MemTotal:" /proc/meminfo | awk '{print $2" "$3}')" - disksize="$(LC_ALL=C df -h / | awk 'NR==2 {print $4" / "$2}')" - ## print only if not "${CROWSNEST_LOG_LEVEL}": quiet - if [[ "${CROWSNEST_LOG_LEVEL}" != "quiet" ]]; then - log_msg "INFO: Host information:" - ## OS Infos - ## OS Version - if [[ -f /etc/os-release ]]; then - log_msg "Host Info: Distribution: $(grep "PRETTY" /etc/os-release | \ - cut -d '=' -f2 | sed 's/^"//;s/"$//')" - fi - ## Release Version of MainsailOS (if file present) - if [[ -f /etc/mainsailos-release ]]; then - log_msg "Host Info: Release: $(cat /etc/mainsailos-release)" - fi - ## Kernel version - log_msg "Host Info: Kernel: $(uname -s) $(uname -rm)" - ## Host Machine Infos - ## Host model - if [[ -n "${sbc_model}" ]]; then - log_msg "Host Info: Model: ${sbc_model}" - fi - if [[ -n "${generic_model}" ]] && - [[ -z "${sbc_model}" ]]; then - log_msg "Host Info: Model: ${generic_model}" - fi - ## CPU count - log_msg "Host Info: Available CPU Cores: $(nproc)" - ## Avail mem - log_msg "Host Info: Available Memory: ${memtotal}" - ## Avail disk size - log_msg "Host Info: Diskspace (avail. / total): ${disksize}" - fi -} diff --git a/libs/messages.sh b/libs/messages.sh deleted file mode 100755 index 2fc725e2..00000000 --- a/libs/messages.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash - -#### message library - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit upon Errors -set -Ee - -## Message Helpers - -## core lib -function missing_args_msg { - echo -e "crowsnest: Missing Arguments!" - echo -e "\n\tTry: crowsnest -h\n" -} - -function wrong_args_msg { - echo -e "crowsnest: Wrong Arguments!" - echo -e "\n\tTry: crowsnest -h\n" -} - -function help_msg { - echo -e "crowsnest - webcam deamon\nUsage:" - echo -e "\t crowsnest [Options]" - echo -e "\n\t\t-h Prints this help." - echo -e "\n\t\t-v Prints Version of crowsnest." - echo -e "\n\t\t-c \n\t\t\tPath to your webcam.conf" - echo -e "\n\t\t-s \n\t\t\tDelay start \(in seconds\) after boot\n" -} - -function deprecated_msg_1 { - log_msg "Parameter 'streamer' is deprecated!" - log_msg "Please use mode: [ ustreamer | camera-streamer ]" - log_msg "ERROR: Please update your crowsnest.conf! Stopped." -} - -function unknown_mode_msg { - log_msg "WARN: Unknown Mode configured!" - log_msg "WARN: Using 'mode: ustreamer' as fallback!" -} - -## v4l2_control lib -function detected_broken_dev_msg { - log_msg "WARN: Detected 'brokenfocus' device." - log_msg "INFO: Trying to set to configured Value." -} - -# call debug_focus_val_msg -# ex.: debug_focus_val_msg focus_absolute=30 -function debug_focus_val_msg { - log_msg "DEBUG: Value is now: ${1}" -} - -## blockyfix -function blockyfix_msg_1 { - log_msg "INFO: Blockyfix: Setting video_bitrate_mode to constant." -} diff --git a/libs/ustreamer.sh b/libs/ustreamer.sh deleted file mode 100755 index 88fc1a33..00000000 --- a/libs/ustreamer.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/bin/bash - -#### ustreamer library - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit upon Errors -set -Ee - -run_mjpg() { - local cams - v4l2_control - cams="${1}" - for instance in ${cams} ; do - run_ustreamer "${instance}" & - done - blockyfix - brokenfocus "${cams}" - return -} - -run_ustreamer() { - local cam_sec ust_bin dev pt res fps cstm start_param - cam_sec="${1}" - # shellcheck disable=SC2153 - ust_bin="${UST_BIN}" - dev="$(get_param "cam ${cam_sec}" device)" - pt="$(get_param "cam ${cam_sec}" port)" - res="$(get_param "cam ${cam_sec}" resolution)" - fps="$(get_param "cam ${cam_sec}" max_fps)" - cstm="$(get_param "cam ${cam_sec}" custom_flags 2> /dev/null)" - noprx="$(get_param "crowsnest" no_proxy 2> /dev/null)" - # construct start parameter - if [[ -n "${noprx}" ]] && [[ "${noprx}" = "true" ]]; then - start_param=( --host 0.0.0.0 -p "${pt}" ) - log_msg "INFO: Set to 'no_proxy' mode! Using 0.0.0.0 !" - else - start_param=( --host 127.0.0.1 -p "${pt}" ) - fi - - #Raspicam Workaround - if [[ "${dev}" = "$(dev_is_legacy)" ]]; then - start_param+=( -m MJPEG --device-timeout=5 --buffers=3 ) - else - # Add device - start_param+=( -d "${dev}" --device-timeout=2 ) - - # Use MJPEG Hardware encoder if possible - if [ "$(detect_mjpeg "${cam_sec}")" = "1" ]; then - start_param+=( -m MJPEG --encoder=HW ) - fi - fi - - # set max framerate - start_param+=( -r "${res}" -f "${fps}" ) - - # webroot & allow crossdomain requests - start_param+=( --allow-origin=\* --static "${BASE_CN_PATH}/ustreamer-www" ) - # Custom Flag Handling (append to defaults) - if [[ -n "${cstm}" ]]; then - start_param+=( "${cstm}" ) - fi - # Log start_param - log_msg "Starting ustreamer with Device ${dev} ..." - echo "Parameters: ${start_param[*]}" | \ - log_output "ustreamer [cam ${cam_sec}]" - # Start ustreamer - echo "${start_param[*]}" | xargs "${ust_bin}" 2>&1 | \ - log_output "ustreamer [cam ${cam_sec}]" - # Should not be seen else failed. - log_msg "ERROR: Start of ustreamer [cam ${cam_sec}] failed!" -} diff --git a/libs/v4l2_control.sh b/libs/v4l2_control.sh deleted file mode 100755 index 167fc3a4..00000000 --- a/libs/v4l2_control.sh +++ /dev/null @@ -1,165 +0,0 @@ -#!/bin/bash - -#### v4l2 control library - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### -#### Description: Configure Cam with v4l2-ctl options -#### ex.: v4l2-ctl -c brightness=100 - -# shellcheck enable=require-variable-braces - -# Exit upon Errors -set -Ee - -function v4l2_control { - v4c_log_msg() { - log_msg "V4L2 Control: ${1}" - } - - function main { - local device v4l2ctl valueless opt_avail - for cam in $(configured_cams); do - # get device from cam section - device="$(get_param "cam ${cam}" device)" - # get v4l2ctl parameters - v4l2ctl="$(get_param "cam ${cam}" v4l2ctl)" - # if not empty do - if [[ -n "${v4l2ctl}" ]]; then - # Write configured options to Log - v4c_log_msg "Device: [cam ${cam}]" - v4c_log_msg "Options: ${v4l2ctl}" - # Split options to array - IFS="," read -ra opt < <(echo "${v4l2ctl}" | tr -d " "); unset IFS - # loop through options - for param in "${opt[@]}"; do - # parameter available for device - # needs || true to prevent script to exit - valueless="$(echo "${param}" | cut -d "=" -f1)" - opt_avail="$(v4l2-ctl -d "${device}" -L | \ - grep -c "${valueless}" || true)" - if [[ "${opt_avail}" -eq "0" ]]; then - v4c_log_msg "Parameter '${param}' not available for '${device}'. Skipped." - else - v4l2-ctl -d "${device}" -c "${param}" 2> /dev/null || - v4c_log_msg "Failed to set parameter: '${param}' ..." - fi - done - if [[ "${CROWSNEST_LOG_LEVEL}" == "debug" ]]; then - v4l2-ctl -d "${device}" -L | log_output "v4l2ctl" - fi - else - v4c_log_msg "No parameters set for [cam ${cam}]. Skipped." - fi - done - } - -### MAIN -main -} - -function brokenfocus { - # checks if "focus_absolute" is configured - # call if_focus_absolute - # returns 1 = true, 0 = false - function if_focus_absolute { - local cam - cam="${1}" - get_param "cam ${cam}" v4l2ctl | grep -c "focus_absolute" - } - - # checks if device has "focus_absolute" - # call has_focus_absolute - # returns 1 if true, 0 if false - function has_focus_absolute { - v4l2-ctl -d "${1}" -C "focus_absolute" &> /dev/null \ - && echo "1" || echo "0" - } - - # call get_conf_value - # spits out value from config file - function get_conf_value { - local cam conf_val - local -a params - cam="${1}" - conf_val="$(get_param "cam ${cam}" v4l2ctl)" - if [ -n "${conf_val}" ]; then - IFS=','; read -ra params <<< "${conf_val}" - unset IFS - for i in "${params[@]}"; do - grep "focus_absolute" <<< "${i}" || true - done - fi - } - - # call get_current_value - # ex.: get_current_value /dev/video0 - # spits out focus_absolute=20 ( if set to 20 ) - function get_current_value { - v4l2-ctl -d "${1}" -C "focus_absolute" 2> /dev/null | sed 's/:[[:space:]]/=/' - } - - # call set_current_value - # ex.: set_current_value /dev/video0 focus_absolute=30 - function set_focus_absolute { - local device value - device="${1}" - value="${2}" - v4l2-ctl -d "${device}" -c "${value}" - } - - function main { - local cur_val conf_val device - for cam in ${1}; do - device="$(get_param "cam ${cam}" device)" - cur_val="$(get_current_value "${device}")" - conf_val="$(get_conf_value "${cam}")" - if [ "$(has_focus_absolute "${device}")" == "1" ] && - [ "$(if_focus_absolute "${cam}")" == "1" ] && - [ "${cur_val}" != "${conf_val}" ]; then - detected_broken_dev_msg - set_focus_absolute "${device}" "${conf_val}" - fi - if [[ "${CROWSNEST_LOG_LEVEL}" == "debug" ]] && [[ -n "${cur_val}" ]]; then - debug_focus_val_msg "$(get_current_value "${device}")" - fi - done - } - -### MAIN -main "${1}" - -} - -# This function is to set bitrate on raspicams. -# If raspicams set to variable bitrate, they tend to show -# a "block-like" view after reboots -# To prevent that blockyfix should apply constant bitrate befor start of ustreamer -# See https://github.com/mainsail-crew/crowsnest/issues/33 -function blockyfix { - local dev v4l2ctl - - # call set_bitrate - function set_bitrate { - v4l2-ctl -d "${1}" -c video_bitrate_mode=1 2> /dev/null - v4l2-ctl -d "${1}" -c video_bitrate=15000000 2> /dev/null - } - - for cam in $(configured_cams); do - dev="$(get_param "cam ${cam}" device)" - v4l2ctl="$(get_param "cam ${cam}" v4l2ctl)" - if [ "${dev}" = "$(dev_is_legacy)" ]; then - if [ -z "${v4l2ctl}" ] || - [ "$(grep -c "video_bitrate" <<< "${v4l2ctl}")" == "0" ]; then - set_bitrate "${dev}" - blockyfix_msg_1 - fi - fi - done -} diff --git a/libs/versioncontrol.sh b/libs/versioncontrol.sh deleted file mode 100644 index f75ea605..00000000 --- a/libs/versioncontrol.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -#### version control library - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### -#### Description: Checks Versions of Streamer and ffmpeg -#### - -# shellcheck enable=require-variable-braces - -# Exit upon Errors -set -Ee - -versioncontrol() { - - vc_log_msg() { - log_msg "Version Control: ${1}" - } - - get_ustreamer_version() { - local cur_ver avail_ver - pushd "${BASE_CN_PATH}"/bin/ustreamer &> /dev/null || exit 1 - avail_ver="$(git describe --tags --always)" - cur_ver="v$(${UST_BIN} -v)" - if [[ "${cur_ver}" == "${avail_ver}" ]]; then - vc_log_msg "ustreamer is up to date. (${cur_ver})" - fi - if [[ "${cur_ver}" != "${avail_ver}" ]]; then - vc_log_msg "ustreamer new version available: ${avail_ver} (${cur_ver})." - fi - popd &> /dev/null || exit 1 - } - - - # Camera Streamer has no version Output yet - get_ayucamstream_version() { - local cur_ver avail_ver - if [[ "$(use_cs)" = "1" ]]; then - pushd "${BASE_CN_PATH}"/bin/camera-streamer &> /dev/null || exit 1 - avail_ver="($(git describe --tags --always))" - cur_ver="$("${PWD}"/camera-streamer --version | tr -d " ")" - if [ "${cur_ver}" == "${avail_ver}" ]; then - vc_log_msg "camera-streamer is up to date. (${cur_ver})" - fi - if [ "${cur_ver}" != "${avail_ver}" ]; then - vc_log_msg "camera-streamer new version available: ${avail_ver} (${cur_ver})." - fi - popd &> /dev/null || exit 1 - fi - } - - ### MAIN - function main { - if [[ "${CROWSNEST_LOG_LEVEL}" != "quiet" ]]; then - get_ustreamer_version - if [[ "$(is_raspberry_pi)" = "1" ]]; then - get_ayucamstream_version - fi - fi - } - - main - return -} diff --git a/libs/watchdog.sh b/libs/watchdog.sh deleted file mode 100755 index f535c75c..00000000 --- a/libs/watchdog.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash - -#### Watchdog - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit upon Errors -set -Ee - -#### Watchdog Functions and Variables -## Do not reuse functions from other libs/scripts! -# We want watchdog operating independently! - -function clean_watchdog { - rm -f /tmp/lost-* -} - -function crowsnest_watchdog { - # Helper Functions - function available { - find "${1}" &> /dev/null - echo "${?}" - } - - function lost_dev { - local lostfile - lostfile="$(echo "${1}" | awk -F '/' '{print $NF}')" - touch /tmp/lost-"${lostfile}" - } - - function is_lost { - local lostdev - lostdev="$(echo "${1}" | awk -F '/' '{print $NF}')" - find /tmp/lost-"${lostdev}" &> /dev/null - echo "${?}" - } - - function returned_dev { - local lostdev - lostdev="$(echo "${1}"| awk -F '/' '{print $NF}')" - rm -f /tmp/lost-"${lostdev}" &> /dev/null - } - - function get_conf_devices { - local gcd - for i in $(crudini --existing=file --get "${CROWSNEST_CFG}" | \ - sed '/crowsnest/d' | cut -d ' ' -f2); do - gcd+=("${i}") - done - echo "${gcd[@]}" - } - - ### MAIN - - for i in $(get_conf_devices); do - cc="$(crudini --get "${CROWSNEST_CFG}" "cam ${i}" "device" \ - | awk '{print $1}')" - if [[ ! "${cc}" =~ "/base/soc" ]] && - [[ "$(available "${cc}")" -ne 0 ]] && [[ "$(is_lost "${cc}")" -ne 0 ]]; then - log_msg "WATCHDOG: Lost Device: '${cc}'" - lost_dev "${cc}" - elif [[ ! "${cc}" =~ "/base/soc" ]] && - [[ "$(is_lost "${cc}")" -eq 0 ]] && [[ "$(available "${cc}")" -eq 0 ]]; then - log_msg "WATCHDOG: Device '${cc}' returned." - returned_dev "${cc}" - fi - done -} diff --git a/log-example.md b/log-example.md deleted file mode 100644 index 6c5b4f93..00000000 --- a/log-example.md +++ /dev/null @@ -1,211 +0,0 @@ -# Example Log - -## This is an example of a Logfile in 'verbose' mode - - [06/16/22 10:07:45] crowsnest: crowsnest - A webcam Service for multiple Cams and Stream Services. - [06/16/22 10:07:45] crowsnest: Version: v2.4.0-15-ge42799b - [06/16/22 10:07:45] crowsnest: Prepare Startup ... - [06/16/22 10:07:45] crowsnest: INFO: Host information: - [06/16/22 10:07:45] crowsnest: Host Info: Distribution: Raspbian GNU/Linux 10 (buster) - [06/16/22 10:07:45] crowsnest: Host Info: Release: MainsailOS release 0.6.1 (buster) - [06/16/22 10:07:45] crowsnest: Host Info: Kernel: Linux 5.10.63-v7l+ armv7l - [06/16/22 10:07:45] crowsnest: Host Info: Model: Raspberry Pi 4 Model B Rev 1.2 - [06/16/22 10:07:45] crowsnest: Host Info: Available CPU Cores: 4 - [06/16/22 10:07:45] crowsnest: Host Info: Available Memory: 3748160 kB - [06/16/22 10:07:45] crowsnest: Host Info: Diskspace (used / total): 2.9G / 7.1G - [06/16/22 10:07:45] crowsnest: INFO: Checking Dependencys - [06/16/22 10:07:45] crowsnest: Dependency: 'crudini' found in /usr/bin/crudini. - [06/16/22 10:07:45] crowsnest: Dependency: 'find' found in /usr/bin/find. - [06/16/22 10:07:45] crowsnest: Dependency: 'logger' found in /usr/bin/logger. - [06/16/22 10:07:45] crowsnest: Dependency: 'xargs' found in /usr/bin/xargs. - [06/16/22 10:07:45] crowsnest: Dependency: 'ffmpeg' found in /usr/bin/ffmpeg. - [06/16/22 10:07:45] crowsnest: Dependency: 'ustreamer' found in bin/ustreamer/ustreamer. - [06/16/22 10:07:45] crowsnest: Dependency: 'rtsp-simple-server' found in bin/rtsp-simple-server/rtsp-simple-server. - [06/16/22 10:07:46] crowsnest: Version Control: ustreamer is up to date. (v4.13) - [06/16/22 10:07:46] crowsnest: Version Control: rtsp-simple-server is up to date. (v0.19.1) - [06/16/22 10:07:46] crowsnest: Version Control: ffmpeg is up to date. (4.1.9-0+deb10u1+rpt1) - [06/16/22 10:07:46] crowsnest: INFO: Print Configfile: '/home/pi/klipper_config/crowsnest.conf' - [06/16/22 10:07:46] crowsnest: [crowsnest] - [06/16/22 10:07:46] crowsnest: log_path: ~/klipper_logs/crowsnest.log - [06/16/22 10:07:46] crowsnest: log_level: verbose - [06/16/22 10:07:46] crowsnest: delete_log: false - [06/16/22 10:07:46] crowsnest: - [06/16/22 10:07:46] crowsnest: [cam 1] - [06/16/22 10:07:46] crowsnest: mode: mjpg - [06/16/22 10:07:46] crowsnest: port: 8080 - [06/16/22 10:07:46] crowsnest: device: /dev/video0 - [06/16/22 10:07:46] crowsnest: resolution: 640x480 - [06/16/22 10:07:46] crowsnest: max_fps: 15 - [06/16/22 10:07:46] crowsnest: INFO: Detect available Devices - [06/16/22 10:07:46] crowsnest: INFO: Found 1 total available Device(s) - [06/16/22 10:07:46] crowsnest: Detected 'Raspicam' Device -> /dev/video0 - [06/16/22 10:07:47] crowsnest: Supported Formats: - [06/16/22 10:07:47] crowsnest: [0]: 'YU12' (Planar YUV 4:2:0) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [1]: 'YUYV' (YUYV 4:2:2) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [2]: 'RGB3' (24-bit RGB 8-8-8) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [3]: 'JPEG' (JFIF JPEG, compressed) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [4]: 'H264' (H.264, compressed) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [5]: 'MJPG' (Motion-JPEG, compressed) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [6]: 'YVYU' (YVYU 4:2:2) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [7]: 'VYUY' (VYUY 4:2:2) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [8]: 'UYVY' (UYVY 4:2:2) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [9]: 'NV12' (Y/CbCr 4:2:0) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [10]: 'BGR3' (24-bit BGR 8-8-8) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [11]: 'YV12' (Planar YVU 4:2:0) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [12]: 'NV21' (Y/CrCb 4:2:0) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: [13]: 'RX24' (32-bit XBGR 8-8-8-8) - [06/16/22 10:07:47] crowsnest: Size: Stepwise 32x32 - 2592x1944 with step 2/2 - [06/16/22 10:07:47] crowsnest: Supported Controls: - [06/16/22 10:07:47] crowsnest: - [06/16/22 10:07:47] crowsnest: User Controls - [06/16/22 10:07:47] crowsnest: - [06/16/22 10:07:47] crowsnest: brightness 0x00980900 (int) : min=0 max=100 step=1 default=50 value=50 flags=slider - [06/16/22 10:07:47] crowsnest: contrast 0x00980901 (int) : min=-100 max=100 step=1 default=0 value=0 flags=slider - [06/16/22 10:07:47] crowsnest: saturation 0x00980902 (int) : min=-100 max=100 step=1 default=0 value=0 flags=slider - [06/16/22 10:07:47] crowsnest: red_balance 0x0098090e (int) : min=1 max=7999 step=1 default=1000 value=1000 flags=slider - [06/16/22 10:07:47] crowsnest: blue_balance 0x0098090f (int) : min=1 max=7999 step=1 default=1000 value=1000 flags=slider - [06/16/22 10:07:47] crowsnest: horizontal_flip 0x00980914 (bool) : default=0 value=0 - [06/16/22 10:07:47] crowsnest: vertical_flip 0x00980915 (bool) : default=0 value=0 - [06/16/22 10:07:47] crowsnest: power_line_frequency 0x00980918 (menu) : min=0 max=3 default=1 value=1 - [06/16/22 10:07:47] crowsnest: 0: Disabled - [06/16/22 10:07:47] crowsnest: 1: 50 Hz - [06/16/22 10:07:47] crowsnest: 2: 60 Hz - [06/16/22 10:07:47] crowsnest: 3: Auto - [06/16/22 10:07:47] crowsnest: sharpness 0x0098091b (int) : min=-100 max=100 step=1 default=0 value=0 flags=slider - [06/16/22 10:07:47] crowsnest: color_effects 0x0098091f (menu) : min=0 max=15 default=0 value=0 - [06/16/22 10:07:47] crowsnest: 0: None - [06/16/22 10:07:47] crowsnest: 1: Black & White - [06/16/22 10:07:47] crowsnest: 2: Sepia - [06/16/22 10:07:47] crowsnest: 3: Negative - [06/16/22 10:07:48] crowsnest: 4: Emboss - [06/16/22 10:07:48] crowsnest: 5: Sketch - [06/16/22 10:07:48] crowsnest: 6: Sky Blue - [06/16/22 10:07:48] crowsnest: 7: Grass Green - [06/16/22 10:07:48] crowsnest: 8: Skin Whiten - [06/16/22 10:07:48] crowsnest: 9: Vivid - [06/16/22 10:07:48] crowsnest: 10: Aqua - [06/16/22 10:07:48] crowsnest: 11: Art Freeze - [06/16/22 10:07:48] crowsnest: 12: Silhouette - [06/16/22 10:07:48] crowsnest: 13: Solarization - [06/16/22 10:07:48] crowsnest: 14: Antique - [06/16/22 10:07:48] crowsnest: 15: Set Cb/Cr - [06/16/22 10:07:48] crowsnest: rotate 0x00980922 (int) : min=0 max=360 step=90 default=0 value=0 flags=modify-layout - [06/16/22 10:07:48] crowsnest: color_effects_cbcr 0x0098092a (int) : min=0 max=65535 step=1 default=32896 value=32896 - [06/16/22 10:07:48] crowsnest: - [06/16/22 10:07:48] crowsnest: Codec Controls - [06/16/22 10:07:48] crowsnest: - [06/16/22 10:07:48] crowsnest: video_bitrate_mode 0x009909ce (menu) : min=0 max=1 default=0 value=0 flags=update - [06/16/22 10:07:48] crowsnest: 0: Variable Bitrate - [06/16/22 10:07:48] crowsnest: 1: Constant Bitrate - [06/16/22 10:07:48] crowsnest: video_bitrate 0x009909cf (int) : min=25000 max=25000000 step=25000 default=10000000 value=10000000 - [06/16/22 10:07:48] crowsnest: repeat_sequence_header 0x009909e2 (bool) : default=0 value=0 - [06/16/22 10:07:48] crowsnest: h264_i_frame_period 0x00990a66 (int) : min=0 max=2147483647 step=1 default=60 value=60 - [06/16/22 10:07:48] crowsnest: h264_level 0x00990a67 (menu) : min=0 max=13 default=11 value=11 - [06/16/22 10:07:48] crowsnest: 0: 1 - [06/16/22 10:07:48] crowsnest: 1: 1b - [06/16/22 10:07:48] crowsnest: 2: 1.1 - [06/16/22 10:07:48] crowsnest: 3: 1.2 - [06/16/22 10:07:48] crowsnest: 4: 1.3 - [06/16/22 10:07:48] crowsnest: 5: 2 - [06/16/22 10:07:48] crowsnest: 6: 2.1 - [06/16/22 10:07:48] crowsnest: 7: 2.2 - [06/16/22 10:07:48] crowsnest: 8: 3 - [06/16/22 10:07:48] crowsnest: 9: 3.1 - [06/16/22 10:07:48] crowsnest: 10: 3.2 - [06/16/22 10:07:48] crowsnest: 11: 4 - [06/16/22 10:07:48] crowsnest: 12: 4.1 - [06/16/22 10:07:48] crowsnest: 13: 4.2 - [06/16/22 10:07:48] crowsnest: h264_profile 0x00990a6b (menu) : min=0 max=4 default=4 value=4 - [06/16/22 10:07:48] crowsnest: 0: Baseline - [06/16/22 10:07:48] crowsnest: 1: Constrained Baseline - [06/16/22 10:07:48] crowsnest: 2: Main - [06/16/22 10:07:48] crowsnest: 4: High - [06/16/22 10:07:48] crowsnest: - [06/16/22 10:07:48] crowsnest: Camera Controls - [06/16/22 10:07:48] crowsnest: - [06/16/22 10:07:48] crowsnest: auto_exposure 0x009a0901 (menu) : min=0 max=3 default=0 value=0 - [06/16/22 10:07:48] crowsnest: 0: Auto Mode - [06/16/22 10:07:48] crowsnest: 1: Manual Mode - [06/16/22 10:07:48] crowsnest: exposure_time_absolute 0x009a0902 (int) : min=1 max=10000 step=1 default=1000 value=1000 - [06/16/22 10:07:48] crowsnest: exposure_dynamic_framerate 0x009a0903 (bool) : default=0 value=0 - [06/16/22 10:07:48] crowsnest: auto_exposure_bias 0x009a0913 (intmenu): min=0 max=24 default=12 value=12 - [06/16/22 10:07:48] crowsnest: 0: -4000 (0xfffffffffffff060) - [06/16/22 10:07:49] crowsnest: 1: -3667 (0xfffffffffffff1ad) - [06/16/22 10:07:49] crowsnest: 2: -3333 (0xfffffffffffff2fb) - [06/16/22 10:07:49] crowsnest: 3: -3000 (0xfffffffffffff448) - [06/16/22 10:07:49] crowsnest: 4: -2667 (0xfffffffffffff595) - [06/16/22 10:07:49] crowsnest: 5: -2333 (0xfffffffffffff6e3) - [06/16/22 10:07:49] crowsnest: 6: -2000 (0xfffffffffffff830) - [06/16/22 10:07:49] crowsnest: 7: -1667 (0xfffffffffffff97d) - [06/16/22 10:07:49] crowsnest: 8: -1333 (0xfffffffffffffacb) - [06/16/22 10:07:49] crowsnest: 9: -1000 (0xfffffffffffffc18) - [06/16/22 10:07:49] crowsnest: 10: -667 (0xfffffffffffffd65) - [06/16/22 10:07:49] crowsnest: 11: -333 (0xfffffffffffffeb3) - [06/16/22 10:07:49] crowsnest: 12: 0 (0x0) - [06/16/22 10:07:49] crowsnest: 13: 333 (0x14d) - [06/16/22 10:07:49] crowsnest: 14: 667 (0x29b) - [06/16/22 10:07:49] crowsnest: 15: 1000 (0x3e8) - [06/16/22 10:07:49] crowsnest: 16: 1333 (0x535) - [06/16/22 10:07:49] crowsnest: 17: 1667 (0x683) - [06/16/22 10:07:49] crowsnest: 18: 2000 (0x7d0) - [06/16/22 10:07:49] crowsnest: 19: 2333 (0x91d) - [06/16/22 10:07:49] crowsnest: 20: 2667 (0xa6b) - [06/16/22 10:07:49] crowsnest: 21: 3000 (0xbb8) - [06/16/22 10:07:49] crowsnest: 22: 3333 (0xd05) - [06/16/22 10:07:49] crowsnest: 23: 3667 (0xe53) - [06/16/22 10:07:49] crowsnest: 24: 4000 (0xfa0) - [06/16/22 10:07:49] crowsnest: white_balance_auto_preset 0x009a0914 (menu) : min=0 max=10 default=1 value=1 - [06/16/22 10:07:49] crowsnest: 0: Manual - [06/16/22 10:07:49] crowsnest: 1: Auto - [06/16/22 10:07:49] crowsnest: 2: Incandescent - [06/16/22 10:07:49] crowsnest: 3: Fluorescent - [06/16/22 10:07:49] crowsnest: 4: Fluorescent H - [06/16/22 10:07:49] crowsnest: 5: Horizon - [06/16/22 10:07:49] crowsnest: 6: Daylight - [06/16/22 10:07:49] crowsnest: 7: Flash - [06/16/22 10:07:49] crowsnest: 8: Cloudy - [06/16/22 10:07:49] crowsnest: 9: Shade - [06/16/22 10:07:49] crowsnest: 10: Greyworld - [06/16/22 10:07:49] crowsnest: image_stabilization 0x009a0916 (bool) : default=0 value=0 - [06/16/22 10:07:49] crowsnest: iso_sensitivity 0x009a0917 (intmenu): min=0 max=4 default=0 value=0 - [06/16/22 10:07:49] crowsnest: 0: 0 (0x0) - [06/16/22 10:07:49] crowsnest: 1: 100000 (0x186a0) - [06/16/22 10:07:49] crowsnest: 2: 200000 (0x30d40) - [06/16/22 10:07:49] crowsnest: 3: 400000 (0x61a80) - [06/16/22 10:07:49] crowsnest: 4: 800000 (0xc3500) - [06/16/22 10:07:49] crowsnest: iso_sensitivity_auto 0x009a0918 (menu) : min=0 max=1 default=1 value=1 - [06/16/22 10:07:49] crowsnest: 0: Manual - [06/16/22 10:07:49] crowsnest: 1: Auto - [06/16/22 10:07:49] crowsnest: exposure_metering_mode 0x009a0919 (menu) : min=0 max=3 default=0 value=0 - [06/16/22 10:07:49] crowsnest: 0: Average - [06/16/22 10:07:49] crowsnest: 1: Center Weighted - [06/16/22 10:07:49] crowsnest: 2: Spot - [06/16/22 10:07:49] crowsnest: 3: Matrix - [06/16/22 10:07:49] crowsnest: scene_mode 0x009a091a (menu) : min=0 max=13 default=0 value=0 - [06/16/22 10:07:50] crowsnest: 0: None - [06/16/22 10:07:50] crowsnest: 8: Night - [06/16/22 10:07:50] crowsnest: 11: Sports - [06/16/22 10:07:50] crowsnest: - [06/16/22 10:07:50] crowsnest: JPEG Compression Controls - [06/16/22 10:07:50] crowsnest: - [06/16/22 10:07:50] crowsnest: compression_quality 0x009d0903 (int) : min=1 max=100 step=1 default=30 value=30 - [06/16/22 10:07:50] crowsnest: INFO: No usable CSI Devices found. - [06/16/22 10:07:50] crowsnest: V4L2 Control: - [06/16/22 10:07:50] crowsnest: No parameters set for [cam 1]. Skipped. - [06/16/22 10:07:50] crowsnest: Try to start configured Cams / Services... - [06/16/22 10:07:50] crowsnest: INFO: Configuration of Section [cam 1] looks good. Continue... - [06/16/22 10:07:51] crowsnest: Starting ustreamer with Device /dev/video0 ... - [06/16/22 10:07:52] crowsnest: ... Done! diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e69de29b diff --git a/resources/crowsnest.conf b/resources/crowsnest.conf index 86c9ba94..8ae509d5 100644 --- a/resources/crowsnest.conf +++ b/resources/crowsnest.conf @@ -2,7 +2,7 @@ #### This is the default config after installation. #### It is also used as the default config in MainsailOS. #### For details on how to configure this to your needs, see: -#### https://github.com/mainsail-crew/crowsnest/blob/master/README.md +#### https://github.com/mainsail-crew/crowsnest/blob/v5/README.md ##################################################################### @@ -23,23 +23,18 @@ #### http://:/?action=[stream/snapshot] ##### #### ##### ##################################################################### -#### RTSP Stream URL: ( if enabled and supported ) ##### -#### rtsp://:/stream.h264 ##### -##################################################################### [crowsnest] -log_path: %LOGPATH% log_level: verbose # Valid Options are quiet/verbose/debug -delete_log: false # Deletes log on every restart, if set to true +rollover_on_start: false # Creates a backup and clears the log on every restart, if set to true no_proxy: false # If set to true, no reverse proxy is required. Only change this, if you know what you are doing. [cam 1] -mode: ustreamer # ustreamer - Provides MJPG and snapshots. (All devices) - # camera-streamer - Provides WebRTC, MJPG and snapshots. (only RPiOS + RPi 0/1/2/3/4) +mode: ustreamer # https://docs.mainsail.xyz/crowsnest/faq/backends port: 8080 # HTTP/MJPG stream/snapshot port device: /dev/video0 # See log for available devices resolution: 640x480 # x format max_fps: 15 # If hardware supports it, it will be forced, otherwise ignored/coerced. #custom_flags: # You can run the stream services with custom flags. -#v4l2ctl: # Add v4l2-ctl parameters to setup your camera, see log for your camera capabilities. +#v4l2ctl: # Add v4l2-ctl parameters to set up your camera, see log for your camera capabilities. diff --git a/resources/crowsnest.env b/resources/crowsnest.env index 2b61d8ed..e0e2afd9 100644 --- a/resources/crowsnest.env +++ b/resources/crowsnest.env @@ -1 +1 @@ -CROWSNEST_ARGS="-c %CONFPATH%/crowsnest.conf" +CROWSNEST_ARGS="-m crowsnest -c %CONFPATH%/crowsnest.conf -l %LOGPATH%/crowsnest.log -s 5" diff --git a/resources/crowsnest.service b/resources/crowsnest.service index 34e8c0a9..db696d8b 100644 --- a/resources/crowsnest.service +++ b/resources/crowsnest.service @@ -12,8 +12,7 @@ Description=crowsnest - Multi Webcam/Streamer Control Deamon Documentation=https://github.com/mainsail-crew/crowsnest After=udev.service network-online.target nss-lookup.target Wants=udev.service network-online.target -Before=nginx.service -StartLimitBurst=10 +StartLimitBurst=3 StartLimitIntervalSec=180 [Install] @@ -25,6 +24,6 @@ User=%USER% RemainAfterExit=Yes WorkingDirectory=/home/%USER%/crowsnest EnvironmentFile=%ENV% -ExecStart=/usr/local/bin/crowsnest $CROWSNEST_ARGS +ExecStart=%PYTHON_VENV%/bin/python3 $CROWSNEST_ARGS Restart=on-failure -RestartSec=5 +RestartSec=30 diff --git a/resources/logrotate_crowsnest b/resources/logrotate_crowsnest deleted file mode 100644 index 805a36c0..00000000 --- a/resources/logrotate_crowsnest +++ /dev/null @@ -1,20 +0,0 @@ -# crowsnest logrotate file -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 - 2022 -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -%LOGPATH% { -rotate 3 -missingok -notifempty -copy -daily -dateext -dateformat .%Y-%m-%d -maxsize 4M -} diff --git a/resources/moonraker_update.txt b/resources/moonraker_update.txt index e3fc55f5..abd044b7 100644 --- a/resources/moonraker_update.txt +++ b/resources/moonraker_update.txt @@ -1,8 +1,9 @@ - -# Crowsnest update_manager entry [update_manager crowsnest] type: git_repo path: ~/crowsnest origin: https://github.com/mainsail-crew/crowsnest.git +primary_branch: v5 managed_services: crowsnest -install_script: tools/pkglist.sh +system_dependencies: system-dependencies.json +virtualenv: ~/crowsnest-env +requirements: requirements.txt diff --git a/ustreamer-www/index.html b/resources/ustreamer-www/index.html similarity index 100% rename from ustreamer-www/index.html rename to resources/ustreamer-www/index.html diff --git a/ustreamer-www/script.js b/resources/ustreamer-www/script.js similarity index 100% rename from ustreamer-www/script.js rename to resources/ustreamer-www/script.js diff --git a/ustreamer-www/style.css b/resources/ustreamer-www/style.css similarity index 100% rename from ustreamer-www/style.css rename to resources/ustreamer-www/style.css diff --git a/system-dependencies.json b/system-dependencies.json new file mode 100644 index 00000000..740c3c1f --- /dev/null +++ b/system-dependencies.json @@ -0,0 +1,9 @@ +{ + "debian": [ + "python3-libcamera; vendor == 'raspberry-pi' and distro_version >= '11'", + "mainsail-ustreamer; distro_version >= '11'", + "mainsail-spyglass; vendor == 'raspberry-pi' and distro_version >= '11'", + "mainsail-camera-streamer-generic; vendor != 'raspberry-pi' and distro_version >= '11'", + "mainsail-camera-streamer-raspi; vendor == 'raspberry-pi' and distro_version >= '11'" + ] +} diff --git a/tools/configure.sh b/tools/configure.sh index cadaa478..c0c85ec4 100755 --- a/tools/configure.sh +++ b/tools/configure.sh @@ -25,11 +25,10 @@ CN_CONFIG_ROOTPATH="/home/${CN_CONFIG_USER}/printer_data" CN_CONFIG_CONFIGPATH="${CN_CONFIG_ROOTPATH}/config" CN_CONFIG_LOGPATH="${CN_CONFIG_ROOTPATH}/logs" CN_CONFIG_ENVPATH="${CN_CONFIG_ROOTPATH}/systemd" +CN_PYTHON_VENVPATH="/home/${CN_CONFIG_USER}/crowsnest-env" CN_MOONRAKER_CONFIG_PATH="${CN_CONFIG_CONFIGPATH}/moonraker.conf" CN_USTREAMER_REPO="https://github.com/pikvm/ustreamer.git" -CN_USTREAMER_BRANCH="v6.36" -CN_CAMERA_STREAMER_REPO="https://github.com/mryel00/camera-streamer.git" -CN_CAMERA_STREAMER_BRANCH="main" +CN_USTREAMER_BRANCH="master" ### Messages header_msg() { @@ -89,6 +88,13 @@ env_path_msg() { echo -e "\tDefault: \e[32m${CN_CONFIG_ENVPATH}\e[0m\n" } +venv_path_msg() { + header_msg + echo -e "Please specify path to python virtual environment \n" + echo -e "\t\e[34mNOTE:\e[0m File names are hardcoded! Also skip trailing backslash!" + echo -e "\tDefault: \e[32m${CN_PYTHON_VENVPATH}\e[0m\n" +} + add_moonraker_entry_msg() { header_msg echo -e "Should the update_manager entry added to your moonraker.conf?\n" @@ -156,8 +162,6 @@ create_config_header() { echo -e "BASE_USER=\"${CN_CONFIG_USER}\""; echo -e "CROWSNEST_USTREAMER_REPO_SHIP=\"${CN_USTREAMER_REPO}\""; echo -e "CROWSNEST_USTREAMER_REPO_BRANCH=\"${CN_USTREAMER_BRANCH}\"" - echo -e "CROWSNEST_CAMERA_STREAMER_REPO_SHIP=\"${CN_CAMERA_STREAMER_REPO}\""; - echo -e "CROWSNEST_CAMERA_STREAMER_REPO_BRANCH=\"${CN_CAMERA_STREAMER_BRANCH}\"" } >> "${CN_CONFIG_CONFIGFILE}" } @@ -225,6 +229,22 @@ specify_env_path() { fi } +specify_venv_path() { + local reply + venv_path_msg + default_path_msg + read -erp "Please enter path: " -i "${CN_PYTHON_VENVPATH}" reply + if [[ -z "${reply}" ]]; then + echo -e "CROWSNEST_VENV_PATH=\"${CN_PYTHON_VENVPATH}\"" >> \ + "${CN_CONFIG_CONFIGFILE}" + return 0 + fi + if [[ -n "${reply}" ]]; then + echo -e "CROWSNEST_VENV_PATH=\"${reply}\"" >> "${CN_CONFIG_CONFIGFILE}" + return 0 + fi +} + add_moonraker_entry() { local reply add_moonraker_entry_msg @@ -258,6 +278,7 @@ main() { specify_config_path specify_log_path specify_env_path + specify_venv_path add_moonraker_entry goodbye_msg } diff --git a/tools/dev-helper.sh b/tools/dev-helper.sh index 1c54c0c8..2ca32c4b 100755 --- a/tools/dev-helper.sh +++ b/tools/dev-helper.sh @@ -99,34 +99,6 @@ get_os_info() { fi } -### Import config -import_config() { - ## Source config if present - if [[ -s tools/.config ]]; then - # shellcheck disable=SC1091 - source tools/.config - return 0 - else - echo -e "\[31mNo .config found!\e[0m" - return 0 - fi -} - -is_raspberry_pi() { - if [[ -f /proc/device-tree/model ]] && - grep -q "Raspberry" /proc/device-tree/model; then - echo "1" - else - echo "0" - fi -} - -# get_val
-# spits out value -get_val() { - crudini --get "${1}" "${2}" "${3}" 2> /dev/null -} - host_info() { grep "model name" /proc/cpuinfo | head -1 | awk -F': ' '{print $2}' } diff --git a/tools/install.sh b/tools/install.sh index 88610fc6..8ca4307d 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -24,11 +24,18 @@ set -Ee ## Global Vars SRC_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd -P)" [[ -n "${CROWSNEST_UNATTENDED}" ]] || CROWSNEST_UNATTENDED="0" +[[ -n "${CROWSNEST_SKIP_REBOOT_PROMPT}" ]] || CROWSNEST_SKIP_REBOOT_PROMPT="0" + +### Crowsnest Dependencies +# shellcheck disable=SC2034 +PKGLIST=("curl" "crudini" "python3" "python3-venv") +# shellcheck disable=SC2034 +PKGLIST_PI=("python3-libcamera") main() { - . "${SRC_DIR}/../libs/helper_fn.sh" - . "${SRC_DIR}/libs/build_apps.sh" + . "${SRC_DIR}/libs/helper_fn.sh" . "${SRC_DIR}/libs/config.sh" + . "${SRC_DIR}/libs/manage_apps.sh" . "${SRC_DIR}/libs/core.sh" . "${SRC_DIR}/libs/interactive.sh" . "${SRC_DIR}/libs/messages.sh" @@ -59,7 +66,7 @@ main() { welcome_msg - msg "Running apt-get update first ...\n" + msg "Running apt-get update first ..." if run_apt_update; then status_msg "Running apt-get update first ..." "0" else @@ -67,33 +74,11 @@ main() { fi if [[ "${CROWSNEST_UNATTENDED}" != "1" ]]; then - msg "Doing some tests ...\n" detect_existing_webcamd - if shallow_cs_dependencies_check; then - CN_INSTALL_CS="1" - else - CN_INSTALL_CS="0" - fi - status_msg "Doing some tests ..." "0" - else - if [[ "$(is_raspios)" = "1" ]]; then - CN_INSTALL_CS="1" - else - CN_INSTALL_CS="0" - fi - fi - - if [[ "${CN_INSTALL_CS}" = "1" ]]; then - msg "Installing with camera-streamer ...\n" - link_pkglist_rpi - else - msg "Installing without camera-streamer ...\n" - link_pkglist_generic fi - source_pkglist_file msg "Installing dependencies ...\n" - if install_dependencies ;then + if install_dependencies; then status_msg "Install dependencies ..." "0" else status_msg "Install dependencies ..." "1" @@ -102,19 +87,12 @@ main() { import_config msg "Creating file structure ..." - if create_filestructure ;then + if create_filestructure; then status_msg "Creating file structure ..." "0" else status_msg "Creating file structure ..." "1" fi - - if link_main_executable; then - status_msg "Link crowsnest to /usr/local/bin ..." "0" - else - status_msg "Link crowsnest to /usr/local/bin ..." "1" - fi - if install_service_file; then status_msg "Install service file ..." "0" else @@ -127,18 +105,6 @@ main() { status_msg "Install environment file ..." "1" fi - if [[ "$(is_speederpad)" = "1" ]]; then - msg "\nSpeederpad detected!" - msg "Add startup delay to environment file ...\n" - add_sleep_to_crowsnest_env - fi - - if install_logrotate_conf; then - status_msg "Install logrotate configuration ..." "0" - else - status_msg "Install logrotate configuration ..." "1" - fi - if install_crowsnest_conf; then status_msg "Install crowsnest.conf ..." "0" else @@ -153,14 +119,12 @@ main() { add_group_video - if [[ "$(is_os_release "bullseye")" = "1" ]] && [[ "${CN_INSTALL_CS}" = "1" ]]; then - msg "\nBullseye detected!" - msg "Using legacy/bullseye branch of camera-streamer for Bullseye ...\n" - CROWSNEST_CAMERA_STREAMER_REPO_BRANCH="legacy/bullseye" + if setup_runtime_env; then + status_msg "Setup streamer apps ..." "0" + else + status_msg "Setup streamer apps ..." "1" fi - build_apps - if [[ "${CROWSNEST_UNATTENDED}" = "0" ]]; then set_gpu_mem fi @@ -174,19 +138,14 @@ main() { if [[ "${CROWSNEST_UNATTENDED}" = "0" ]]; then ask_update_entry - fi - - if [[ "${CROWSNEST_UNATTENDED}" = "1" ]] && - [[ "${CROWSNEST_ADD_CROWSNEST_MOONRAKER}" = "1" ]]; then - add_update_entry - fi - - if [[ "${CROWSNEST_UNATTENDED}" = "0" ]]; then goodbye_msg - ask_reboot - fi - - if [[ "${CROWSNEST_UNATTENDED}" = "1" ]]; then + if [[ "${CROWSNEST_SKIP_REBOOT_PROMPT}" = "0" ]]; then + ask_reboot + fi + elif [[ "${CROWSNEST_UNATTENDED}" = "1" ]]; then + if [[ "${CROWSNEST_ADD_CROWSNEST_MOONRAKER}" = "1" ]]; then + add_update_entry + fi unattended_success_msg fi diff --git a/tools/libs/build_apps.sh b/tools/libs/build_apps.sh deleted file mode 100755 index 2ed84f24..00000000 --- a/tools/libs/build_apps.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 - 2023 -#### Co-authored by Patrick Gehrsitz aka mryel00 -#### Copyright 2023 - till today -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit on errors -set -Ee - -# Debug -# set -x - -clone_ustreamer() { - ## remove bin/ustreamer if exist - if [[ -d bin/ustreamer ]]; then - rm -rf bin/ustreamer - fi - sudo -u "${BASE_USER}" \ - git clone "${CROWSNEST_USTREAMER_REPO_SHIP}" \ - -b "${CROWSNEST_USTREAMER_REPO_BRANCH}" \ - --depth=1 --single-branch bin/ustreamer -} - -clone_cstreamer() { - ## remove bin/ustreamer if exist - if [[ -d bin/camera-streamer ]]; then - rm -rf bin/camera-streamer - fi - sudo -u "${BASE_USER}" \ - git clone "${CROWSNEST_CAMERA_STREAMER_REPO_SHIP}" \ - --recurse-submodules --shallow-submodules \ - -b "${CROWSNEST_CAMERA_STREAMER_REPO_BRANCH}" \ - --depth=1 --single-branch bin/camera-streamer -} - -build_apps() { - msg "Build dependend Stream Apps ..." - msg "Cloning ustreamer repository ..." - clone_ustreamer - ## Detect Image build for Raspberrys - if [[ "${CN_INSTALL_CS}" = "1" ]]; then - msg "Cloning camera-streamer repository ..." - clone_cstreamer - else - msg "Install of camera-streamer skipped, only supported on Raspberry SBC's! ... " - fi - sudo -u "${BASE_USER}" "${PWD}"/bin/build.sh --build -} diff --git a/tools/libs/config.sh b/tools/libs/config.sh index b9b398ec..752d94f1 100755 --- a/tools/libs/config.sh +++ b/tools/libs/config.sh @@ -35,10 +35,6 @@ import_config() { [[ -n "${CROWSNEST_CONFIG_PATH}" ]] || CROWSNEST_CONFIG_PATH="/home/${BASE_USER}/printer_data/config" [[ -n "${CROWSNEST_LOG_PATH}" ]] || CROWSNEST_LOG_PATH="/home/${BASE_USER}/printer_data/logs" [[ -n "${CROWSNEST_ENV_PATH}" ]] || CROWSNEST_ENV_PATH="/home/${BASE_USER}/printer_data/systemd" - [[ -n "${CROWSNEST_USTREAMER_REPO_SHIP}" ]] || CROWSNEST_USTREAMER_REPO_SHIP="https://github.com/pikvm/ustreamer.git" - [[ -n "${CROWSNEST_USTREAMER_REPO_BRANCH}" ]] || CROWSNEST_USTREAMER_REPO_BRANCH="v6.36" - [[ -n "${CROWSNEST_CAMERA_STREAMER_REPO_SHIP}" ]] || CROWSNEST_CAMERA_STREAMER_REPO_SHIP="https://github.com/mryel00/camera-streamer.git" - [[ -n "${CROWSNEST_CAMERA_STREAMER_REPO_BRANCH}" ]] || CROWSNEST_CAMERA_STREAMER_REPO_BRANCH="main" status_msg "Using default configuration ..." "0" fi } diff --git a/tools/libs/core.sh b/tools/libs/core.sh index 5efae0ad..b3d998ab 100755 --- a/tools/libs/core.sh +++ b/tools/libs/core.sh @@ -20,91 +20,16 @@ set -Ee # set -x ## Funcs -get_os_version() { - if [[ -n "${1}" ]]; then - grep -c "${1}" /etc/os-release &> /dev/null && echo "1" || echo "0" - fi -} - get_host_arch() { uname -m } -test_load_module() { - if modprobe -n "${1}" &> /dev/null; then - echo 1 - else - echo 0 - fi -} - -shallow_cs_dependencies_check() { - msg "Checking for camera-streamer dependencies ...\n" - - msg "Checking if device is a Raspberry Pi ...\n" - if [[ "$(is_raspberry_pi)" = "0" ]]; then - status_msg "Checking if device is a Raspberry Pi ..." "3" - msg "This device is not a Raspberry Pi therefore camera-streamer cannot be installed ..." - return 1 - fi - status_msg "Checking if device is a Raspberry Pi ..." "0" - - msg "Checking if device is not a Raspberry Pi 5 ...\n" - if [[ "$(is_pi5)" = "1" ]]; then - status_msg "Checking if device is not a Raspberry Pi 5 ..." "3" - msg "This device is a Raspberry Pi 5 therefore camera-streamer cannot be installed ..." - return 1 - fi - status_msg "Checking if device is not a Raspberry Pi 5 ..." "0" - - msg "Checking for required kernel module ...\n" - SHALLOW_CHECK_MODULESLIST="bcm2835_codec" - if [[ "$(test_load_module ${SHALLOW_CHECK_MODULESLIST})" = "0" ]]; then - status_msg "Checking for required kernel module ..." "3" - msg "Not all required kernel modules for camera-streamer can be loaded ..." - return 1 - fi - status_msg "Checking for required kernel module ..." "0" - - msg "Checking for required packages ...\n" - # Update the number below if you update SHALLOW_CHECK_PKGLIST - SHALLOW_CHECK_PKGLIST="^(libavformat-dev|libavutil-dev|libavcodec-dev|liblivemedia-dev|libcamera-dev|libcamera-apps-lite)$" - if [[ $(apt-cache search --names-only "${SHALLOW_CHECK_PKGLIST}" | wc -l) -lt 6 ]]; then - status_msg "Checking for required packages ..." "3" - msg "Not all required packages for camera-streamer can be installed ..." - return 1 - fi - status_msg "Checking for required packages ..." "0" - - status_msg "Checking for camera-streamer dependencies ..." "0" - return 0 -} - -link_pkglist_rpi() { - sudo -u "${BASE_USER}" ln -sf "${SRC_DIR}/libs/pkglist-rpi.sh" "${SRC_DIR}/pkglist.sh" &> /dev/null || return 1 -} - -link_pkglist_generic() { - sudo -u "${BASE_USER}" ln -sf "${SRC_DIR}/libs/pkglist-generic.sh" "${SRC_DIR}/pkglist.sh" &> /dev/null || return 1 -} - -run_apt_update() { - apt-get -q --allow-releaseinfo-change update -} - -source_pkglist_file() { - # shellcheck disable=SC1091 - . "${SRC_DIR}/pkglist.sh" -} - install_dependencies() { - local dep - local -a pkg - pkg=() - for dep in ${PKGLIST}; do - pkg+=("${dep}") - done - apt-get --yes --no-install-recommends install "${pkg[@]}" || return 1 + local pkgs=("${PKGLIST[@]}") + if [[ "$(use_pi_specifics)" = "1" ]]; then + pkgs+=("${PKGLIST_PI[@]}") + fi + apt-get --yes --no-install-recommends install "${pkgs[@]}" || return 1 } create_filestructure() { @@ -115,29 +40,12 @@ create_filestructure() { else status_msg "Created ${dir} ..." "1" fi - fi - if [[ -d "${dir}" ]]; then + else msg "Directory ${dir} already exists ..." "0" fi done || return 1 } -link_main_executable() { - local crowsnest_main_bin_path crowsnest_src_bin_path - crowsnest_main_bin_path="/usr/local/bin" - crowsnest_src_bin_path="${PWD}/crowsnest" - - if [[ -f "${crowsnest_main_bin_path}/crowsnest" ]]; then - rm -f "${crowsnest_main_bin_path}/crowsnest" - fi - if [[ -f "${crowsnest_src_bin_path}" ]]; then - ln -sf "${crowsnest_src_bin_path}" "${crowsnest_main_bin_path}" - else - msg "File ${crowsnest_src_bin_path} does not exist!" - return 1 - fi -} - install_service_file() { local service_file target_dir service_file="${PWD}/resources/crowsnest.service" @@ -147,39 +55,21 @@ install_service_file() { rm -f "${target_dir}/crowsnest.service" fi cp -f "${service_file}" "${target_dir}" - sed -i 's|%USER%|'"${BASE_USER}"'|g;s|%ENV%|'"${CROWSNEST_ENV_PATH}/crowsnest.env"'|g' \ + sed -i 's|%USER%|'"${BASE_USER}"'|g;s|%ENV%|'"${CROWSNEST_ENV_PATH}/crowsnest.env"'|g;s|%PYTHON_VENV%|'"${CROWSNEST_VENV_PATH}"'|g' \ "${target_dir}/crowsnest.service" [[ -f "${target_dir}/crowsnest.service" ]] && grep -q "${BASE_USER}" "${target_dir}/crowsnest.service" || return 1 } -add_sleep_to_crowsnest_env() { - local service_file - env_file="${CROWSNEST_ENV_PATH}/crowsnest.env" - - if [[ -f "${env_file}" ]]; then - sed -i 's/\(CROWSNEST_ARGS="[^"]*\)"/\1 -s"/' "${env_file}" - fi -} - install_env_file() { local env_file env_target env_file="${PWD}/resources/crowsnest.env" env_target="${CROWSNEST_ENV_PATH}/crowsnest.env" sudo -u "${BASE_USER}" cp -f "${env_file}" "${env_target}" sed -i "s|%CONFPATH%|${CROWSNEST_CONFIG_PATH}|" "${env_target}" + sed -i "s|%LOGPATH%|${CROWSNEST_LOG_PATH}|" "${env_target}" [[ -f "${env_target}" ]] && - grep -q "${CROWSNEST_CONFIG_PATH}" "${env_target}" || return 1 -} - -install_logrotate_conf() { - local logrotatefile logpath - logrotatefile="${PWD}/resources/logrotate_crowsnest" - logpath="${CROWSNEST_LOG_PATH}/crowsnest.log" - cp -rf "${logrotatefile}" /etc/logrotate.d/crowsnest - sed -i "s|%LOGPATH%|${logpath}|g" /etc/logrotate.d/crowsnest - [[ -f "/etc/logrotate.d/crowsnest" ]] && - grep -q "${logpath}" "/etc/logrotate.d/crowsnest" || return 1 + grep -q "${CROWSNEST_CONFIG_PATH}" "${env_target}" && grep -q "${CROWSNEST_LOG_PATH}" "${env_target}" || return 1 } backup_crowsnest_conf() { @@ -195,12 +85,9 @@ backup_crowsnest_conf() { install_crowsnest_conf() { local conf_template conf_template="${PWD}/resources/crowsnest.conf" - logpath="${CROWSNEST_LOG_PATH}/crowsnest.log" backup_crowsnest_conf sudo -u "${BASE_USER}" cp -rf "${conf_template}" "${CROWSNEST_CONFIG_PATH}" - sed -i "s|%LOGPATH%|${logpath}|g" "${CROWSNEST_CONFIG_PATH}/crowsnest.conf" - [[ -f "${CROWSNEST_CONFIG_PATH}/crowsnest.conf" ]] && - grep -q "${logpath}" "${CROWSNEST_CONFIG_PATH}/crowsnest.conf" || return 1 + [[ -f "${CROWSNEST_CONFIG_PATH}/crowsnest.conf" ]] || return 1 } enable_service() { diff --git a/libs/helper_fn.sh b/tools/libs/helper_fn.sh similarity index 89% rename from libs/helper_fn.sh rename to tools/libs/helper_fn.sh index 3d233a8c..b2e5be16 100755 --- a/libs/helper_fn.sh +++ b/tools/libs/helper_fn.sh @@ -62,9 +62,13 @@ is_pi5() { fi } -is_speederpad() { - if grep -q "Ubuntu 20.04." /etc/os-release && - [[ "$(uname -rm)" = "4.9.191 aarch64" ]]; then +run_apt_update() { + apt-get -q --allow-releaseinfo-change update +} + +use_pi_specifics() { + if [[ "$(is_raspios)" = "1" ]] || + [[ "$(is_dietpi)" = "1" ]]; then echo "1" else echo "0" diff --git a/tools/libs/manage_apps.sh b/tools/libs/manage_apps.sh new file mode 100755 index 00000000..c516d2b1 --- /dev/null +++ b/tools/libs/manage_apps.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env bash + +#### crowsnest - A webcam Service for multiple Cams and Stream Services. +#### +#### Written by Patrick Gehrsitz aka mryel00 +#### Copyright 2025 - till today +#### https://github.com/mainsail-crew/crowsnest +#### +#### This File is distributed under GPLv3 +#### + +# shellcheck enable=require-variable-braces + +# Exit on errors +set -Ee + +# Debug +# set -x + +TOOLS_LIB_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" +# shellcheck disable=SC1091 +. "${TOOLS_LIB_DIR}/helper_fn.sh" +# shellcheck disable=SC1091 +. "${TOOLS_LIB_DIR}/messages.sh" + +# Ustreamer repo +if [[ -z "${CROWSNEST_USTREAMER_REPO_SHIP}" ]]; then + CROWSNEST_USTREAMER_REPO_SHIP="https://github.com/pikvm/ustreamer.git" +fi +if [[ -z "${CROWSNEST_USTREAMER_REPO_BRANCH}" ]]; then + CROWSNEST_USTREAMER_REPO_BRANCH="master" +fi +USTREAMER_PATH="bin/ustreamer" + +# These are install dependencies for manual building +PKGLIST_USTREAMER=("git" "build-essential" "libevent-dev" "libjpeg-dev" "libbsd-dev" "pkg-config") + +# Paths of repos +ALL_PATHS=( + "${USTREAMER_PATH}" +) + +APPS=("mainsail-ustreamer") +if [[ "$(use_pi_specifics)" = "1" ]]; then + APPS+=("mainsail-spyglass") + if [[ "$(is_pi5)" = "0" ]]; then + APPS+=("mainsail-camera-streamer-raspi") + fi +else + APPS+=("mainsail-camera-streamer-generic") +fi + +: "${BASE_USER:=${SUDO_USER:-${USER}}}" +CROWSNEST_VENV_PATH="/home/${BASE_USER}/crowsnest-env" + +clone_ustreamer() { + ## remove bin/ustreamer if exist + if [[ -d bin/ustreamer ]]; then + rm -rf bin/ustreamer + fi + sudo -u "${BASE_USER}" \ + git clone "${CROWSNEST_USTREAMER_REPO_SHIP}" \ + -b "${CROWSNEST_USTREAMER_REPO_BRANCH}" \ + --depth=1 --single-branch "${USTREAMER_PATH}" +} + +get_avail_mem() { + grep "MemTotal" /proc/meminfo | awk '{print $2}' +} + +build_ustreamer() { + ## Determine Ramsize and export MAKEFLAG + if [[ "$(get_avail_mem)" -le 524288 ]]; then + USE_PROCS=-j1 + elif [[ "$(get_avail_mem)" -le 1048576 ]]; then + USE_PROCS=-j2 + else + USE_PROCS=-j4 + fi + + if [[ ! -d "${USTREAMER_PATH}" ]]; then + msg "'${USTREAMER_PATH}' does not exist! Build skipped ... [WARN]\n" + else + msg "Build '${USTREAMER_PATH##*/}' using ${USE_PROCS##-j} Cores ... \n" + pushd "${USTREAMER_PATH}" &> /dev/null || exit 1 + make "${USE_PROCS}" + popd &> /dev/null || exit 1 + msg "Build '${USTREAMER_PATH##*/}' ... [SUCCESS]\n" + fi +} + +install_apt_sources() { + local id version_id + + id=$(grep '^ID=' /etc/os-release | cut -d'=' -f2 | cut -d'"' -f2) + version_id=$(grep '^VERSION_ID=' /etc/os-release | cut -d'=' -f2 | cut -d'"' -f2) + variant="generic" + + if [[ "$(is_raspios)" = "1" || "$(is_dietpi)" = "1" ]]; then + variant="rpi" + id="debian" + fi + + if [[ "${id}" = "debian" ]] && [[ "${version_id}" = "11" ]]; then + curl -s --compressed "https://apt.mainsail.xyz/mainsail.gpg.key" | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/mainsail.gpg > /dev/null + curl -s --compressed --fail -o /etc/apt/sources.list.d/mainsail.list "https://apt.mainsail.xyz/mainsail-${id}-${version_id}-${variant}.list" + echo "1" + else + if curl -s --compressed --fail -o /etc/apt/sources.list.d/mainsail.sources "https://apt.mainsail.xyz/mainsail-${id}-${version_id}-${variant}.sources"; then + curl -s --compressed "https://apt.mainsail.xyz/mainsail.gpg.key" | gpg --dearmor | sudo tee /usr/share/keyrings/mainsail.gpg > /dev/null + echo "1" + else + echo "0" + fi + fi +} + +install_apt_streamer() { + msg "Running apt-get update again ..." + if run_apt_update; then + status_msg "Running apt-get update again ..." "0" + else + status_msg "Running apt-get update again ..." "1" + fi + + for pkg in "${APPS[@]}"; do + if apt-get --yes --no-install-recommends install "${pkg}"; then + echo "${pkg} installed successfully." + else + echo "${pkg} not found or failed to install." + fi + done +} + +install_venv() { + msg "Setup python venv ..." + if [[ -d "${CROWSNEST_VENV_PATH}" ]]; then + msg "Python venv already exists." + delete_venv + fi + sudo -u "${BASE_USER}" python3 -m venv --system-site-packages "${CROWSNEST_VENV_PATH}" +} + +install_apps() { + msg "Setup Mainsail apt repository ..." + if [[ "$(install_apt_sources)" = "0" ]]; then + msg "We do not support your Distro with the Mainsail apt repository." + msg "Trying to install ustreamer manually." + msg "Installing build dependencies ..." + apt-get --yes --no-install-recommends install "${PKGLIST_USTREAMER[@]}" || return 1 + msg "Cloning ustreamer repository ..." + clone_ustreamer + msg "Building ustreamer ..." + build_ustreamer + else + msg "Install streamer apps ..." + install_apt_streamer + fi +} + +setup_runtime_env() { + install_venv + install_apps +} + +delete_venv() { + msg "Deleting python venv ..." + if [[ -d "${CROWSNEST_VENV_PATH}" ]]; then + msg "Deleting '${CROWSNEST_VENV_PATH}' ... [DONE]" + rm -rf "${CROWSNEST_VENV_PATH}" + else + msg "'${CROWSNEST_VENV_PATH}' does not exist! Delete ... [SKIPPED]" + fi +} + +delete_apps() { + for path in "${ALL_PATHS[@]}"; do + if [[ ! -d "${path}" ]]; then + printf "'%s' does not exist! Delete ... [SKIPPED]\n" "${path}" + fi + if [[ -d "${path}" ]]; then + printf "Deleting '%s' ... [DONE]\n" "${path}" + rm -rf "${path}" + fi + done + + for pkg in "${APPS[@]}"; do + if apt-get --yes remove "${pkg}"; then + echo "${pkg} removed successfully." + else + echo "${pkg} not found or failed to remove." + fi + done +} + +delete_runtime_env() { + delete_venv + delete_apps +} + +reinstall_runtime_env() { + delete_runtime_env + setup_runtime_env +} + +main() { + ## Error exit if no args given, show help + if [[ $# -eq "0" ]]; then + printf "ERROR: No options given ...\n" + exit 1 + fi + ## Error exit if too many args given + if [[ $# -gt "1" ]]; then + printf "ERROR: Too many options given ...\n" + exit 1 + fi + ## Get opts + while true; do + case "${1}" in + -i|--install) + setup_runtime_env + break + ;; + -d|--delete) + delete_runtime_env + break + ;; + -r|--reinstall) + reinstall_runtime_env + break + ;; + *) + printf "Unknown option: %s" "${1}" + break + ;; + esac + done +} + +## Error exit if no args given, show help +if [[ $# -gt "0" ]]; then + main "${@}" + exit 0 +fi diff --git a/tools/libs/messages.sh b/tools/libs/messages.sh index 48a1b48e..aef93ccd 100755 --- a/tools/libs/messages.sh +++ b/tools/libs/messages.sh @@ -82,11 +82,6 @@ welcome_msg() { sleep 1 } -config_msg() { - msg "\nConfig file not found!\n\tUsing defaults ..." - msg "\tThis uses paths located in 'printer_data' of your home folder." -} - goodbye_msg() { msg "\nInstallation \e[32msuccessful\e[0m.\n" msg "\t\e[33mReboot your machine for the changes to take effect!\e[0m\n" diff --git a/tools/libs/pkglist-generic.sh b/tools/libs/pkglist-generic.sh deleted file mode 100644 index bb578909..00000000 --- a/tools/libs/pkglist-generic.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 - till today -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit on errors -set -Ee - -# Debug -# set -x - -### Crowsnest Dependencies -PKGLIST="git crudini bsdutils findutils v4l-utils curl" -### Ustreamer Dependencies -PKGLIST="${PKGLIST} build-essential libevent-dev libjpeg-dev libbsd-dev pkg-config" diff --git a/tools/libs/pkglist-rpi.sh b/tools/libs/pkglist-rpi.sh deleted file mode 100644 index 0a229bde..00000000 --- a/tools/libs/pkglist-rpi.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 - 2023 -#### Co-authored by Patrick Gehrsitz aka mryel00 -#### Copyright 2023 - till today -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces - -# Exit on errors -set -Ee - -# Debug -# set -x - -### Crowsnest Dependencies -PKGLIST="git crudini bsdutils findutils v4l-utils curl" -### Ustreamer Dependencies -PKGLIST="${PKGLIST} build-essential libevent-dev libjpeg-dev libbsd-dev pkg-config" -### Camera-Streamer Dependencies -### If you change something below, also have a look at tools/libs/core.sh->shallow_cs_dependencies_check -PKGLIST="${PKGLIST} cmake libavformat-dev libavutil-dev libavcodec-dev libcamera-dev libcamera-apps-lite" -PKGLIST="${PKGLIST} liblivemedia-dev pkg-config xxd build-essential cmake libssl-dev" diff --git a/tools/libs/update_manager.sh b/tools/libs/update_manager.sh index 412ddbbf..b3579516 100755 --- a/tools/libs/update_manager.sh +++ b/tools/libs/update_manager.sh @@ -31,6 +31,9 @@ add_update_entry() { if [[ -f "/tmp/moonraker.conf" ]]; then sudo rm -f /tmp/moonraker.conf fi + if [[ $(tail -c1 "${moonraker_conf}" | wc -l) -eq 0 ]]; then + echo "" >> "${moonraker_conf}" + fi sudo -u "${BASE_USER}" \ cp "${moonraker_conf}" "${moonraker_conf}.backup" && cat "${moonraker_conf}" "${moonraker_update}" > /tmp/moonraker.conf && diff --git a/tools/migrate_configs.sh b/tools/migrate_configs.sh deleted file mode 100755 index 1ebfa1df..00000000 --- a/tools/migrate_configs.sh +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env bash - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Patrick Gehrsitz aka mryel00 -#### Copyright 2026 - till today -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -set -e - -# --- Configuration --- -CROWSNEST_CFG_NAME="crowsnest.conf" -MOONRAKER_CFG_NAME="moonraker.conf" - -# --- Functions --- - -log_info() { - echo -e "\e[32m[INFO]\e[0m $1" >&2 -} - -log_warn() { - echo -e "\e[33m[WARN]\e[0m $1" >&2 -} - -log_error() { - echo -e "\e[31m[ERROR]\e[0m $1" >&2 -} - -find_config() { - if systemctl cat crowsnest.service >/dev/null 2>&1; then - local service_content - service_content=$(systemctl cat crowsnest.service) - - local env_file_path - env_file_path=$(echo "$service_content" | grep "^EnvironmentFile=" | cut -d= -f2) - - if [[ -n "${env_file_path}" && -f "${env_file_path}" ]]; then - local args_line - args_line=$(grep "^CROWSNEST_ARGS=" "${env_file_path}") - - if [[ -n "${args_line}" ]]; then - local extracted_path - if [[ $args_line =~ -c[[:space:]]+([^[:space:]\"]+) ]]; then - extracted_path="${BASH_REMATCH[1]}" - fi - - if [[ -n "${extracted_path}" && -f "${extracted_path}" ]]; then - echo "${extracted_path}" - return 0 - fi - fi - fi - fi - - local base_user - if [[ -n "${SUDO_USER}" ]]; then - base_user="${SUDO_USER}" - else - base_user="$(whoami)" - fi - local user_home - user_home="/home/${base_user}" - - local found_config - found_config=$(find "${user_home}" -maxdepth 4 -type d -name "crowsnest" -prune -o -type f -name "${CROWSNEST_CFG_NAME}" -print | head -n 1) - - if [[ -n "${found_config}" ]]; then - echo "${found_config}" - return 0 - fi - - log_error "Could not find ${CROWSNEST_CFG_NAME} in ${user_home} or an installed crowsnest.service." - log_error "Skipping crowsnest.conf backup." - return 1 -} - -backup_config() { - local extension - local cfg="$1" - local mr_cfg="$2" - extension="$(date +%Y-%m-%d-%H%M)" - cp "${cfg}" "${cfg}.${extension}" - cp "${mr_cfg}" "${mr_cfg}.${extension}.bkp" -} - -migrate_crudini() { - local crowsnest_cfg="$1" - local val - - log_info "Using crudini for migration..." - - while IFS= read -r section; do - if [[ "$section" != "crowsnest" ]] && [[ ! "$section" =~ ^cam\ .* ]]; then - log_info "Removing unknown section: [${section}]" - crudini --del "${crowsnest_cfg}" "${section}" - continue - fi - - if [[ "$section" == "crowsnest" ]]; then - if crudini --get "${crowsnest_cfg}" "${section}" "log_path" >/dev/null 2>&1; then - log_info "Removing log_path from [crowsnest]" - crudini --del "${crowsnest_cfg}" "${section}" "log_path" - fi - fi - - if [[ "$section" =~ ^cam\ .* ]]; then - if crudini --get "${crowsnest_cfg}" "${section}" "enable_rtsp" >/dev/null 2>&1; then - log_info "Removing enable_rtsp from [${section}]" - crudini --del "${crowsnest_cfg}" "${section}" "enable_rtsp" - fi - - if crudini --get "${crowsnest_cfg}" "${section}" "rtsp_port" >/dev/null 2>&1; then - log_info "Removing rtsp_port from [${section}]" - crudini --del "${crowsnest_cfg}" "${section}" "rtsp_port" - fi - - if val=$(crudini --get "${crowsnest_cfg}" "${section}" "mode" 2>/dev/null); then - val=$(echo "$val" | sed 's/[#;].*//' | xargs) - if [[ "$val" != "ustreamer" ]] && [[ "$val" != "camera-streamer" ]] && [[ "$val" != "spyglass" ]]; then - log_info "Updating invalid mode '$val' to 'ustreamer' in [${section}]" - crudini --set "${crowsnest_cfg}" "${section}" "mode" "ustreamer" - fi - fi - fi - done < <(crudini --get --list "${crowsnest_cfg}") - - log_info "Migrate delete_log option with sed..." - sed -i -E 's/delete_log:[[:space:]]*([^ #]*).*/rollover_on_start: \1 # Creates a backup and clears the log on every restart, if set to true/' "${crowsnest_cfg}" - sed -i -E 's/(mode:[[:space:]]*[^ #]*[[:space:]]*).*/\1# https:\/\/docs.mainsail.xyz\/crowsnest\/faq\/backends/' "${crowsnest_cfg}" - sed -i -E '/[[:space:]]*# camera-streamer - Provides WebRTC, MJPG and snapshots.*/d' "${crowsnest_cfg}" -} - -cleanup_legacy_comments() { - local cfg="$1" - log_info "Cleaning up legacy RTSP comments..." - sed -i '/RTSP Stream URL:/,/^##*$/d' "${cfg}" -} - -cleanup_moonraker_config() { - local cfg="$1" - log_info "Cleaning up moonraker.conf entries..." - crudini --del "${cfg}" "update_manager crowsnest" - sed -i '/# Crowsnest update_manager entry/d' "${cfg}" -} - -CROWSNEST_CFG_PATH=$(find_config) || exit 1 -MOONRAKER_CFG_PATH="${CROWSNEST_CFG_PATH%"$CROWSNEST_CFG_NAME"}${MOONRAKER_CFG_NAME}" -MIGRATED_TEMP="${CROWSNEST_CFG_PATH}.v5" - -if ! command -v crudini >/dev/null 2>&1; then - log_error "crudini is required but not found. If it isn't installed, you most likely don't need to run this script." - exit 1 -fi - -log_info "Found config at: ${CROWSNEST_CFG_PATH}" - -backup_config "${CROWSNEST_CFG_PATH}" "${MOONRAKER_CFG_PATH}" -migrate_crudini "${CROWSNEST_CFG_PATH}" "${MOONRAKER_CFG_PATH}" -cleanup_legacy_comments "${CROWSNEST_CFG_PATH}" -cleanup_moonraker_config "${MOONRAKER_CFG_PATH}" - -cp "${CROWSNEST_CFG_PATH}" "${MIGRATED_TEMP}" - -log_info "Migration complete." - -echo "${MIGRATED_TEMP}" diff --git a/tools/test_install.sh b/tools/test_install.sh deleted file mode 100755 index 65a7f3a5..00000000 --- a/tools/test_install.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash - -#### crowsnest - A webcam Service for multiple Cams and Stream Services. -#### -#### Written by Stephan Wendel aka KwadFan -#### Copyright 2021 - till today -#### https://github.com/mainsail-crew/crowsnest -#### -#### This File is distributed under GPLv3 -#### - -# shellcheck enable=require-variable-braces -# shellcheck disable=SC1090,SC2154,SC2317 - -set -eou pipefail - -TEST_SERVICE_FILE="/etc/systemd/system/crowsnest.service" - - -get_vars() { - INSTALLED_AS="$(grep "User" /etc/systemd/system/crowsnest.service | cut -f2 -d= | sed 's/^ //' | cut -f1 -d' ')" - REPO_PATH="$(grep "WorkingDirectory" /etc/systemd/system/crowsnest.service | cut -f2 -d= | sed 's/^ //' | cut -f1 -d' ')" - MAIN_EXE="$(grep "ExecStart" /etc/systemd/system/crowsnest.service | cut -f2 -d= | sed 's/^ //' | cut -f1 -d' ')" - ENV_FILE="$(grep "EnvironmentFile" /etc/systemd/system/crowsnest.service | cut -f2 -d= | sed 's/^ //' | cut -f1 -d' ')" -} - -print_vars() { - printf "TEST: Path to repository -> %s \n" "${REPO_PATH}" - printf "TEST: Main executable -> %s \n" "${MAIN_EXE}" - printf "TEST: EnvironmentFile -> %s \n" "${ENV_FILE}" -} - -main() { - clear - printf "\nTest crowsnest installation ...\n" - - printf "TEST: service file installed? ... " - [[ -f "${TEST_SERVICE_FILE}" ]] 2> /dev/null && printf "[OK]\n" || printf "[FAILED]\n" - - printf "TEST: Print service file ...\n" - ([[ -f "${TEST_SERVICE_FILE}" ]] && cat "${TEST_SERVICE_FILE}") \ - || ( - printf "TEST: Not found or empty ...\n" - printf "TEST: All tests aborted! Crowsnest not installed or missing!\n" - exit 1 - ) - - printf "TEST: service enabled? ... " - if systemctl is-enabled crowsnest.service &> /dev/null; then - printf "[OK]\n" - else - printf "[FAILED]\n" - fi - - printf "TEST: Grab paths from service file ...\n" - get_vars - print_vars - - printf "TEST: Installed as non root user? ... " - [[ "${INSTALLED_AS}" != "root" ]] 2> /dev/null && printf "[OK]\n"; \ - printf "\t-> Installed as user '%s'\n" "${INSTALLED_AS}"|| printf "[FAILED]\n" - - printf "TEST: Main script (crowsnest) installed? ... " - [[ -x "${MAIN_EXE}" ]] 2> /dev/null && printf "[OK]\n" || printf "[FAILED]\n" - - printf "TEST: crowsnest.env installed? ... " - [[ -f "${ENV_FILE}" ]] 2> /dev/null && printf "[OK]\n" || printf "[FAILED]\n" - - printf "TEST: Print EnvironmentFile file ...\n" - [[ -s "${ENV_FILE}" ]] && cat "${ENV_FILE}" || printf "TEST: Not found or empty ...\n" - - printf "TEST: logrotate installed? ... " - [[ -f "/etc/logrotate.d/crowsnest" ]] 2> /dev/null && printf "[OK]\n" || printf "[FAILED]\n" - - printf "TEST: Print logrotate file ...\n" - [[ -s "/etc/logrotate.d/crowsnest" ]] && cat "/etc/logrotate.d/crowsnest" || printf "TEST: Not found or empty ...\n" - - printf "TEST: ustreamer repo cloned? ..." - [[ -d "${REPO_PATH}/bin/ustreamer" ]] && printf "[OK]\n" || printf "[NOT FOUND]\n" - - printf "TEST: ustreamer binary build? ..." - [[ -x "${REPO_PATH}/bin/ustreamer/ustreamer" ]] && printf "[YES]\n" || printf "[NO]\n" - - printf "TEST: Import helper functions ...\n" - . "${REPO_PATH}/libs/helper_fn.sh" - printf "TEST: camera-streamer repo cloned? ..." - if [[ "$(is_raspios)" = "1" ]]; then - [[ -d "${REPO_PATH}/bin/camera-streamer" ]] && printf "[OK]\n" || printf "[NOT FOUND]\n" - printf "TEST: camera-streamer binary build? ..." - [[ -x "${REPO_PATH}/bin/camera-streamer/camera-streamer" ]] && printf "[YES]\n" || printf "[NO]\n" - else - printf "[NON RPI -> SKIPPED]\n" - fi -} - -main "${@}" -exit 0 diff --git a/tools/uninstall.sh b/tools/uninstall.sh index d46abc75..07f0d8a6 100755 --- a/tools/uninstall.sh +++ b/tools/uninstall.sh @@ -177,19 +177,6 @@ remove_log_files() { fi } -remove_logrotate() { - local logrotate_conf - logrotate_conf="/etc/logrotate.d/crowsnest" - if [[ ! -f "${logrotate_conf}" ]]; then - status_msg "Removing Logrotate Rule ..." "2" - msg "\t==> File does not exist!" - return 0 - fi - if [[ -f "${logrotate_conf}" ]]; then - sudo rm -f /etc/logrotate.d/crowsnest || return 1 - fi -} - get_path() { local cn_base_path cn_base_path="$( @@ -248,17 +235,12 @@ main() { msg "\t==> crowsnest service is enabled" fi + sudo SRC_DIR="${SRC_DIR}" bash -c '. "${SRC_DIR}/libs/manage_apps.sh" && delete_runtime_env' remove_service_file remove_env_file - if remove_logrotate; then - status_msg "Removing Logrotate Rule ..." "0" - else - status_msg "Removing Logrotate Rule ..." "1" - fi - remove_log_files ask_remove_config diff --git a/ustreamer_manpage.md b/ustreamer_manpage.md deleted file mode 100644 index ad6b3a89..00000000 --- a/ustreamer_manpage.md +++ /dev/null @@ -1,359 +0,0 @@ -USTREAMER(1) General Commands Manual USTREAMER(1) - -NAME\ -ustreamer - stream MJPG video from any V4L2 device to the network - -SYNOPSIS ustreamer [OPTIONS] - -DESCRIPTION\ - µStreamer (ustreamer) is a lightweight and very quick server to stream - MJPG video from any V4L2 device to the network.\ - All new browsers have native support of this video format, as well as most video players such as mplayer, VLC etc.\ - µStreamer is a part of the Pi-KVM project designed to stream VGA and HDMI screencast hardware data with the highest reso‐ - lution and FPS possible. - -USAGE\ - Without arguments, ustreamer will try to open /dev/video0 with 640x480 resolution and start streaming on http://127.0.0.1:8080.\ - You can override this behavior using parameters --device, --host and --port. For example, to stream to the world, run: ustreamer --device=/dev/video1 --host=0.0.0.0 --port=80 - - Please note that since µStreamer v2.0 cross-domain requests were - disabled by default for security reasons. - To enable the old behavior, use the option --allow-origin=\*. - - For example, the recommended way of running µStreamer with - Auvidea B101 on a Raspberry Pi is: - - ustreamer \ - --format=uyvy \ # Device input format - --encoder=omx \ # Hardware encoding with OpenMAX - --workers=3 \ # Maximum workers for OpenMAX - --persistent \ # Don´t re-initialize device on timeout \ - (for example when HDMI cable was disconnected) - --dv-timings \ # Use DV-timings - --drop-same-frames=30 # Save the traffic - - Please note that to use --drop-same-frames for different browsers you need\ - to use some specific URL /stream parameters (see URL / for details). - - You can always view the full list of options with ustreamer --help. - Some features may not be available on your platform. - To find out which features are enabled, use ustreamer --features. - -OPTIONS - - Capturing options - -d /dev/path, --device /dev/path - Path to V4L2 device. Default: /dev/video0. - - -i N, --input N - Input channel. Default: 0. - - -r WxH, --resolution WxH - Initial image resolution. Default: 640x480. - - -m fmt, --format fmt - Image format. Available: YUYV, UYVY, RGB565, RGB24, JPEG; - default: YUYV. - - -a std, --tv-standard std - Force TV standard. Available: PAL, NTSC, SECAM; - Default: disabled. - - -I method, --io-method method - Set V4L2 IO method (see kernel documentation). Changing of this - parameter may increase the performance. Or not. - Available: MMAP, USERPTR; default: MMAP. - - -f N, --desired-fps N - Desired FPS. Default: maximum possible. - - -z N, --min-frame-size N - Drop frames smaller then this limit. - Useful if the device produces small-sized garbage frames. - Default: 128 bytes. - - -n, --persistent - Don't re-initialize device on timeout. - Default: disabled. - - -t, --dv-timings - Enable DV timings querying and events processing to - automatic resolution change. - Default: disabled. - - -b N, --buffers N - The number of buffers to receive data from the device. Each buf‐ - fer may processed using an independent thread. - Default: 2 (the number of CPU cores (but not more than 4) + 1). - - -w N, --workers N - The number of worker threads but not more than buffers. De‐ - fault: 1 (the number of CPU cores (but not more than 4)). - - -q N, --quality N - Set quality of JPEG encoding from 1 to 100 (best). Default: 80. - Note: If HW encoding is used (JPEG source format selected), this - parameter attempts to configure the camera or capture device - hardware's internal encoder. It does not re-encode MJPG to MJPG - to change the quality level for sources that already output - MJPG. - - -c type, --encoder type - Use specified encoder. It may affect the number of workers. - - CPU ─ Software MJPG encoding (default). - - OMX ─ GPU hardware accelerated MJPG encoding with OpenMax (re‐ - quired WITH_OMX feature). - - HW ─ Use pre-encoded MJPG frames directly from camera hardware. - - NOOP ─ Don't compress MJPG stream (do nothing). - - -g WxH,..., --glitched-resolutions WxH,... - It doesn't do anything. Still here for compatibility. Required - WITH_OMX feature. - - -k path, --blank path - Path to JPEG file that will be shown when the device is discon‐ - nected during the streaming. Default: black screen 640x480 with - 'NO SIGNAL'. - - -K sec, --last-as-blank sec - Show the last frame received from the camera after it was dis‐ - connected, but no more than specified time (or endlessly if 0 is - specified). If the device has not yet been online, display 'NO - SIGNAL' or the image specified by option --blank. Note: cur‐ - rently this option has no effect on memory sinks. - Default: disabled. - - -l, --slowdown - Slowdown capturing to 1 FPS or less when no stream or sink - clients are connected. Useful to reduce CPU consumption. - Default: disabled. - - --device-timeout sec - Timeout for device querying. Default: 1. - - --device-error-delay sec - Delay before trying to connect to the device again after an - error (timeout for example). Default: 1. - - Image control options - --image-default - Reset all image settings below to default. Default: no change. - - --brightness N, auto, default - Set brightness. Default: no change. - - --contrast N, default - Set contrast. Default: no change. - - --saturation N, default - Set saturation. Default: no change. - - --hue N, auto, default - Set hue. Default: no change. - - --gamma N, default - Set gamma. Default: no change. - - --sharpness N, default - Set sharpness. Default: no change. - - --backlight-compensation N, default - Set backlight compensation. Default: no change. - - --white-balance N, auto, default - Set white balance. Default: no change. - - --gain N, auto, default - Set gain. Default: no change. - - --color-effect N, default - Set color effect. Default: no change. - - --flip-vertical 1, 0, default - Set vertical flip. Default: no change. - - --flip-horizontal 1, 0, default - Set horizontal flip. Default: no change. - - HTTP server options - -s address, --host address - Listen on Hostname or IP. Default: 127.0.0.1. - - -p N, --port N - Bind to this TCP port. Default: 8080. - - -U path, --unix path - Bind to UNIX domain socket. Default: disabled. - - -d, --unix-rm - Try to remove old unix socket file before binding. default: dis‐ - abled. - - -M mode, --unix-mode mode - Set UNIX socket file permissions (like 777). Default: disabled. - - --user name - HTTP basic auth user. Default: disabled. - - --passwd str - HTTP basic auth passwd. Default: empty. - - --static path - Path to dir with static files instead of embedded root index - page. Symlinks are not supported for security reasons. Default: - disabled. - - -e N, --drop-same-frames N - Don't send identical frames to clients, but no more than speci‐ - fied number. It can significantly reduce the outgoing traffic, - but will increase the CPU loading. Don't use this option with - analog signal sources or webcams, it's useless. Default: dis‐ - abled. - - -R WxH, --fake-resolution WxH - Override image resolution for the /state. Default: disabled. - - --tcp-nodelay - Set TCP_NODELAY flag to the client /stream socket. Ignored for - --unix. Default: disabled. - - --allow-origin str - Set Access-Control-Allow-Origin header. Default: disabled. - - --server-timeout sec - Timeout for client connections. Default: 10. - -JPEG sink options\ -With shared memory sink you can write a stream to a file.\ -See ustreamer-dump(1) for more info. - - --sink name - Use the specified shared memory object to sink JPEG frames. De‐ - fault: disabled. - - --sink-mode mode - Set JPEG sink permissions (like 777). Default: 660. - - --sink-rm - Remove shared memory on stop. Default: disabled. - - --sink-client-ttl sec - Client TTL. Default: 10. - - --sink-timeout sec - Timeout for lock. Default: 1. - -H264 sink options\ -Available only if WITH_OMX feature enabled. - - --h264-sink name - Use the specified shared memory object to sink H264 frames en‐ - coded by MMAL. Default: disabled. - - --h264-sink-mode mode - Set H264 sink permissions (like 777). Default: 660. - - --h264-sink-rm - Remove shared memory on stop. Default: disabled. - - --h264-sink-client-ttl sec - Client TTL. Default: 10. - - --h264-sink-timeout sec - Timeout for lock. Default: 1. - - --h264-bitrate kbps - H264 bitrate in Kbps. Default: 5000. - - --h264-gop N - Intarval between keyframes. Default: 30. - - Process options - - --exit-on-parent-death - Exit the program if the parent process is dead. - Required HAS_PDEATHSIG feature. Default: disabled. - - --process-name-prefix str - Set process name prefix which will be displayed in the process - list like 'str: ustreamer --blah-blah-blah'. Required WITH_SET‐ - PROCTITLE feature. Default: disabled. - - --notify-parent - Send SIGUSR2 to the parent process when the stream parameters - are changed. Checking changes is performed for the online flag - and image resolution. Required WITH_SETPROCTITLE feature. - -GPIO options\ -Available only if WITH_GPIO feature enabled. - - --gpio-device /dev/path - Path to GPIO character device. Default: /dev/gpiochip0. - - --gpio-consumer-prefix str - Consumer prefix for GPIO outputs. Default: ustreamer. - - --gpio-prog-running pin - Set 1 on GPIO pin while µStreamer is running. Default: disabled. - - --gpio-stream-online pin - Set 1 while streaming. Default: disabled. - - --gpio-has-http-clients pin - Set 1 while stream has at least one client. Default: disabled. - -Logging options - - --log-level N - Verbosity level of messages from 0 (info) to 3 (debug). Enabling - debugging messages can slow down the program. Available levels: - 0 (info), 1 (performance), 2 (verbose), 3 (debug). Default: 0. - - --perf Enable performance messages (same as --log-level=1). Default: - disabled. - - --verbose - Enable verbose messages and lower (same as --log-level=2). De‐ - fault: disabled. - - --debug - Enable debug messages and lower (same as --log-level=3). De‐ - fault: disabled. - - --force-log-colors - Force color logging. Default: colored if stderr is a TTY. - - --no-log-colors - Disable color logging. Default: ditto. - -Help options - - -h, --help - Print this text and exit. - - -v, --version - Print version and exit. - - --features - Print list of supported features. - -SEE ALSO - ustreamer-dump(1) - -BUGS - Please file any bugs and issues at https://github.com/pikvm/ustreamer/issues - -AUTHOR - Maxim Devaev - -HOMEPAGE - https://pikvm.org/ - -COPYRIGHT - GNU General Public License v3.0 - -November 2020 version 4.8 USTREAMER(1)