diff --git a/.travis.yml b/.travis.yml index e3eb3ce..6f7d486 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,19 +8,16 @@ script: # Install the binary, running the debian scripts in the process - sudo apt install ../*.deb -y - # Confirm the cv2 module has been installed correctly - - sudo /usr/bin/env python3 -c "import cv2; print(cv2.__version__);" - # Confirm the face_recognition module has been installed correctly - - sudo /usr/bin/env python3 -c "import face_recognition; print(face_recognition.__version__);" - - # Check if the username passthough works correctly with sudo - - 'howdy | ack-grep --passthru --color "current active user: travis"' - - 'sudo howdy | ack-grep --passthru --color "current active user: travis"' + # Go through function tests + - ./tests/importing.sh + - ./tests/passthrough.sh + # Skip PAM integration tests for now because of broken pamtester + # - ./tests/pam.sh + - ./tests/compare.sh # Remove howdy from the installation - sudo apt purge howdy -y - notifications: email: on_success: never @@ -34,3 +31,4 @@ addons: - ack-grep - devscripts - fakeroot + - pamtester diff --git a/README.md b/README.md index 4493f0c..d216bb8 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,14 @@ Install the `howdy` package from the AUR. For AUR installation instructions, tak You will need to do some additional configuration steps. Please read the [ArchWiki entry](https://wiki.archlinux.org/index.php/Howdy) for more information. +### Fedora +The `howdy` package is now available in a [Fedora COPR repository](https://copr.fedorainfracloud.org/coprs/luya/howdy/) by simply execute the following command from a terminal: + +``` +sudo dnf copr enable luya/howdy +sudo dnf install howdy +``` + ## Setup After installation, you need to let Howdy learn your face. Run `sudo howdy add` to add a face model. diff --git a/debian/changelog b/debian/changelog index 6e8f309..66a0ecb 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +howdy (2.5.0) xenial; urgency=medium + + * Added FFmpeg and v4l2 recorders (thanks @timwelch!) + * Added automatic PAM inclusion on installation + * Added optional notice on detection attempt (thanks @mrkmg!) + * Added support for grayscale frame encoding (thanks @dmig and @sapjunior!) + * Massively improved recognition speed (thanks @dmig!) + * Fixed typo in "timout" config value + * Removed unneeded dependencies (thanks @dmig!) + + -- boltgolt Sun, 06 Jan 2019 14:37:41 +0100 + howdy (2.4.0) xenial; urgency=medium * Cameras are now selected by path instead of by video device number (thanks @Rhiyo!) diff --git a/debian/control b/debian/control index 8d2efa1..2b4262b 100644 --- a/debian/control +++ b/debian/control @@ -9,7 +9,9 @@ Vcs-Git: https://github.com/boltgolt/howdy Package: howdy Homepage: https://github.com/boltgolt/howdy Architecture: all -Depends: ${misc:Depends}, git, python3, python3-pip, python3-dev, python3-setuptools, libpam-python, fswebcam, libopencv-dev, python-opencv, cmake, streamer +Depends: ${misc:Depends}, curl|wget, python3, python3-pip, python3-dev, python3-setuptools, libpam-python, fswebcam, libopencv-dev, cmake, streamer +Recommends: libatlas-base-dev | libopenblas-dev | liblapack-dev +Suggests: nvidia-cuda-dev (>= 7.5) Description: Howdy: Windows Hello style authentication for Linux. Use your built-in IR emitters and camera in combination with face recognition to prove who you are. diff --git a/debian/install b/debian/install index c35719b..b4e148f 100644 --- a/debian/install +++ b/debian/install @@ -1,2 +1,3 @@ src/. lib/security/howdy +src/pam-config/. /usr/share/pam-configs autocomplete/. usr/share/bash-completion/completions diff --git a/debian/postinst b/debian/postinst index dffcc5d..7ceb41c 100755 --- a/debian/postinst +++ b/debian/postinst @@ -2,6 +2,7 @@ # Installation script to install howdy # Executed after primary apt install + def col(id): """Add color escape sequences""" if id == 1: return "\033[32m" @@ -9,15 +10,15 @@ def col(id): if id == 3: return "\033[31m" return "\033[0m" + # Import required modules +import fileinput import subprocess -import time import sys import os import re -import signal -import fileinput -import urllib.parse +import tarfile +from shutil import rmtree, which # Don't run unless we need to configure the install # Will also happen on upgrade but we will catch that later on @@ -29,6 +30,7 @@ def log(text): """Print a nicely formatted line to stdout""" print("\n>>> " + col(1) + text + col(0) + "\n") + def handleStatus(status): """Abort if a command fails""" if (status != 0): @@ -36,6 +38,9 @@ def handleStatus(status): sys.exit(1) +# Create shorthand for subprocess creation +sc = subprocess.call + # We're not in fresh configuration mode so don't continue the setup if not os.path.exists("/tmp/howdy_picked_device"): # Check if we have an older config we can restore @@ -53,15 +58,22 @@ if not os.path.exists("/tmp/howdy_picked_device"): # Go through every setting in the old config and apply it to the new file for section in oldConf.sections(): for (key, value) in oldConf.items(section): + + # MIGRATION 2.3.1 -> 2.4.0 # If config is still using the old device_id parameter, convert it to a path if key == "device_id": key = "device_path" value = "/dev/video" + value + # MIGRATION 2.4.0 -> 2.5.0 + # Finally correct typo in "timout" config value + if key == "timout": + key = "timeout" + try: newConf.set(section, key, value) # Add a new section where needed - except configparser.NoSectionError as e: + except configparser.NoSectionError: newConf.add_section(section) newConf.set(section, key, value) @@ -69,6 +81,11 @@ if not os.path.exists("/tmp/howdy_picked_device"): with open("/lib/security/howdy/config.ini", "w") as configfile: newConf.write(configfile) + # Install dlib data files if needed + if not os.path.exists("/lib/security/howdy/dlib-data/shape_predictor_5_face_landmarks.dat"): + print("Attempting installation of missing data files") + handleStatus(subprocess.call(["./install.sh"], shell=True, cwd="/lib/security/howdy/dlib-data")) + sys.exit(0) # Open the temporary file containing the device ID @@ -78,137 +95,139 @@ picked = in_file.read() in_file.close() # Remove the temporary file -subprocess.call(["rm /tmp/howdy_picked_device"], shell=True) +os.unlink("/tmp/howdy_picked_device") log("Upgrading pip to the latest version") # Update pip -handleStatus(subprocess.call(["pip3 install --upgrade pip"], shell=True)) +handleStatus(sc(["pip3", "install", "--upgrade", "pip"])) -log("Cloning dlib") +log("Downloading and unpacking data files") -# Clone the dlib git to /tmp, but only the last commit -handleStatus(subprocess.call(["git", "clone", "--depth", "1", "https://github.com/davisking/dlib.git", "/tmp/dlib_clone"])) +# Run the bash script to download and unpack the .dat files needed +handleStatus(subprocess.call(["./install.sh"], shell=True, cwd="/lib/security/howdy/dlib-data")) + +log("Downloading dlib") + +dlib_archive = "/tmp/v19.16.tar.gz" +loader = which("wget") +LOADER_CMD = None + +# If wget is installed, use that as the downloader +if loader: + LOADER_CMD = [loader, "--tries", "5", "--output-document"] +# Otherwise, fall back on curl +else: + loader = which("curl") + LOADER_CMD = [loader, "--retry", "5", "--location", "--output"] + +# Assemble and execute the download command +cmd = LOADER_CMD + [dlib_archive, "https://github.com/davisking/dlib/archive/v19.16.tar.gz"] +handleStatus(sc(cmd)) + +# The folder containing the dlib source +DLIB_DIR = None +# A regex of all files to ignore while unpacking the archive +excludes = re.compile( + "davisking-dlib-\w+/(dlib/(http_client|java|matlab|test/)|" + "(docs|examples|python_examples)|" + "tools/(archive|convert_dlib_nets_to_caffe|htmlify|imglab|python/test|visual_studio_natvis))" +) + +# Open the archive +with tarfile.open(dlib_archive) as tf: + for item in tf: + # Set the destenation dir if unset + if not DLIB_DIR: + DLIB_DIR = "/tmp/" + item.name + + # extract only files sufficient for building + if not excludes.match(item.name): + tf.extract(item, "/tmp") + +# Delete the downloaded archive +os.unlink(dlib_archive) log("Building dlib") -# Start the build without GPU -handleStatus(subprocess.call(["cd /tmp/dlib_clone/; python3 setup.py install --yes USE_AVX_INSTRUCTIONS --no DLIB_USE_CUDA"], shell=True)) +cmd = ["sudo", "python3", "setup.py", "install"] +cuda_used = False +flags = "" + +# Get the CPU details +with open("/proc/cpuinfo") as info: + for line in info: + if "flags" in line: + flags = line + break + +# Use the most efficient instruction set the CPU supports +if "avx" in flags: + cmd += ["--yes", "USE_AVX_INSTRUCTIONS"] +elif "sse4" in flags: + cmd += ["--yes", "USE_SSE4_INSTRUCTIONS"] +elif "sse3" in flags: + cmd += ["--yes", "USE_SSE3_INSTRUCTIONS"] +elif "sse2" in flags: + cmd += ["--yes", "USE_SSE2_INSTRUCTIONS"] + +# Compile and link dlib +try: + sp = subprocess.Popen(cmd, cwd=DLIB_DIR, stdout=subprocess.PIPE) +except subprocess.CalledProcessError: + print("Error while building dlib") + raise + +# Go through each line from stdout +while sp.poll() is None: + line = sp.stdout.readline().decode("utf-8") + + if "DLIB WILL USE CUDA" in line: + cuda_used = True + + print(line, end="") log("Cleaning up dlib") # Remove the no longer needed git clone -handleStatus(subprocess.call(["rm", "-rf", "/tmp/dlib_clone"])) +del sp +rmtree(DLIB_DIR) + print("Temporary dlib files removed") -log("Installing python dependencies") +log("Installing OpenCV") -# Install direct dependencies so pip does not freak out with the manual dlib install -handleStatus(subprocess.call(["pip3", "install", "--cache-dir", "/tmp/pip_howdy", "face_recognition_models==0.3.0", "Click>=6.0", "numpy", "Pillow"])) - -log("Installing face_recognition") - -# Install face_recognition though pip -handleStatus(subprocess.call(["pip3", "install", "--cache-dir", "/tmp/pip_howdy", "--no-deps", "face_recognition==1.2.2"])) - -try: - import cv2 -except Exception as e: - log("Reinstalling opencv2") - handleStatus(subprocess.call(["pip3", "install", "opencv-python"])) +handleStatus(subprocess.call(["pip3", "install", "--no-cache-dir", "opencv-python"])) log("Configuring howdy") # Manually change the camera id to the one picked -for line in fileinput.input(["/lib/security/howdy/config.ini"], inplace = 1): - print(line.replace("device_path = none", "device_path = " + picked), end="") +for line in fileinput.input(["/lib/security/howdy/config.ini"], inplace=1): + line = line.replace("device_path = none", "device_path = " + picked) + line = line.replace("use_cnn = false", "use_cnn = " + str(cuda_used).lower()) + + print(line, end="") + print("Camera ID saved") # Secure the howdy folder -handleStatus(subprocess.call(["chmod 744 -R /lib/security/howdy/"], shell=True)) +handleStatus(sc(["chmod 744 -R /lib/security/howdy/"], shell=True)) # Allow anyone to execute the python CLI -handleStatus(subprocess.call(["chmod 755 /lib/security/howdy"], shell=True)) -handleStatus(subprocess.call(["chmod 755 /lib/security/howdy/cli.py"], shell=True)) -handleStatus(subprocess.call(["chmod 755 -R /lib/security/howdy/cli"], shell=True)) +os.chmod("/lib/security/howdy", 0o755) +os.chmod("/lib/security/howdy/cli.py", 0o755) +handleStatus(sc(["chmod 755 -R /lib/security/howdy/cli"], shell=True)) print("Permissions set") # Make the CLI executable as howdy -handleStatus(subprocess.call(["ln -s /lib/security/howdy/cli.py /usr/local/bin/howdy"], shell=True)) -handleStatus(subprocess.call(["chmod +x /usr/local/bin/howdy"], shell=True)) +os.symlink("/lib/security/howdy/cli.py", "/usr/local/bin/howdy") +os.chmod("/usr/local/bin/howdy", 0o755) print("Howdy command installed") log("Adding howdy as PAM module") -# Will be filled with the actual output lines -outlines = [] -# Will be fillled with lines that contain coloring -printlines = [] -# Track if the new lines have been insterted yet -inserted = False - -# Open the PAM config file -with open("/etc/pam.d/common-auth") as fp: - # Read the first line - line = fp.readline() - - while line: - # Add the line to the output directly, we're not deleting anything - outlines.append(line) - - # Print the comments in gray and don't insert into comments - if line[:1] == "#": - printlines.append("\033[37m" + line + "\033[0m") - else: - printlines.append(line) - - # If it's not a comment and we haven't inserted yet - if not inserted: - # Set both the comment and the linking line - line_comment = "# Howdy IR face recognition\n" - line_link = "auth sufficient pam_python.so /lib/security/howdy/pam.py\n\n" - - # Add them to the output without any markup - outlines.append(line_comment) - outlines.append(line_link) - - # Make the print orange to make it clear what's being added - printlines.append("\033[33m" + line_comment + "\033[0m") - printlines.append("\033[33m" + line_link + "\033[0m") - - # Mark as inserted - inserted = True - - # Go to the next line - line = fp.readline() - -# Print a file Header -print("\033[33m" + ">>> START OF /etc/pam.d/common-auth" + "\033[0m") - -# Loop though all printing lines and use the enters from the file -for line in printlines: - print(line, end="") - -# Print a footer -print("\033[33m" + ">>> END OF /etc/pam.d/common-auth" + "\033[0m" + "\n") - -# Do not prompt for a yes if we're in no promt mode -if "HOWDY_NO_PROMPT" not in os.environ: - # Ask the user if this change is okay - print("Lines will be insterted in /etc/pam.d/common-auth as shown above") - ans = input("Apply this change? [y/N]: ") - - # Abort the whole thing if it's not - if ans.lower().strip() != "y" or ans.lower().strip() == "yes": - print("Interpreting as a \"NO\", aborting") - sys.exit(1) - -print("Adding lines to PAM\n") - -# Write to PAM -common_auth = open("/etc/pam.d/common-auth", "w") -common_auth.write("".join(outlines)) -common_auth.close() +# Activate the pam-config file +handleStatus(subprocess.call(["pam-auth-update --package"], shell=True)) # Sign off print("Installation complete.") diff --git a/debian/preinst b/debian/preinst index 4acfb39..e355a15 100755 --- a/debian/preinst +++ b/debian/preinst @@ -9,6 +9,7 @@ def col(id): if id == 3: return "\033[31m" return "\033[0m" + import subprocess import time import sys @@ -24,7 +25,7 @@ if "upgrade" in sys.argv: # Let the user know so he knows where to look on a failed install print("Backup of Howdy config file created in /tmp/howdy_config_backup_v" + sys.argv[2] + ".ini") - except e: + except subprocess.CalledProcessError: print("Could not make an backup of old Howdy config file") # Don't continue setup when we're just upgrading diff --git a/debian/prerm b/debian/prerm index e0c5126..150e7b8 100755 --- a/debian/prerm +++ b/debian/prerm @@ -2,17 +2,11 @@ # Executed on deinstallation # Completely remove howdy from the system -def col(id): - """Add color escape sequences""" - if id == 1: return "\033[32m" - if id == 2: return "\033[33m" - if id == 3: return "\033[31m" - return "\033[0m" - # Import required modules import subprocess import sys import os +from shutil import rmtree # Only run when we actually want to remove if "remove" not in sys.argv and "purge" not in sys.argv: @@ -24,24 +18,28 @@ if not os.path.exists("/lib/security/howdy/cli"): # Remove files and symlinks try: - subprocess.call(["rm /usr/local/bin/howdy"], shell=True) -except e: + os.unlink('/usr/local/bin/howdy') +except Exception: print("Can't remove executable") try: - subprocess.call(["rm /usr/share/bash-completion/completions/howdy"], shell=True) -except e: + os.unlink('/usr/share/bash-completion/completions/howdy') +except Exception: print("Can't remove autocompletion script") + +# Refresh and remove howdy from pam-config try: - subprocess.call(["rm -rf /lib/security/howdy"], shell=True) -except e: + subprocess.call(["pam-auth-update --package"], shell=True) + subprocess.call(["rm /usr/share/pam-configs/howdy"], shell=True) + subprocess.call(["pam-auth-update --package"], shell=True) +except Exception: + print("Can't remove pam module") + +# Remove full installation folder, just to be sure +try: + rmtree('/lib/security/howdy') +except Exception: # This error is normal pass -# Remove face_recognition and dlib -subprocess.call(["pip3 uninstall face_recognition face_recognition_models dlib -y --no-cache-dir"], shell=True) - -# Print a tearbending message -print(col(2) + """ -There are still lines in /etc/pam.d/common-auth that can't be removed automatically -Run "nano /etc/pam.d/common-auth" to remove them by hand\ -""" + col(0)) +# Remove dlib +subprocess.call(['pip3', 'uninstall', 'dlib', '-y', '--no-cache-dir']) diff --git a/debian/source/options b/debian/source/options index 593ee7b..1216d00 100644 --- a/debian/source/options +++ b/debian/source/options @@ -1,6 +1,7 @@ tar-ignore = ".git" -tar-ignore = "models" tar-ignore = ".gitignore" +tar-ignore = ".github" +tar-ignore = "models" +tar-ignore = "tests" tar-ignore = "README.md" -tar-ignore = "LICENSE" tar-ignore = ".travis.yml" diff --git a/src/cli.py b/src/cli.py index a937594..5906f4d 100755 --- a/src/cli.py +++ b/src/cli.py @@ -4,7 +4,6 @@ # Import required modules import sys import os -import subprocess import getpass import argparse import builtins @@ -12,11 +11,11 @@ import builtins # Try to get the original username (not "root") from shell try: user = os.getlogin() -except: +except Exception: user = os.environ.get("SUDO_USER") # If that fails, try to get the direct user -if user == "root" or user == None: +if user == "root" or user is None: env_user = getpass.getuser().strip() # If even that fails, error out @@ -28,37 +27,37 @@ if user == "root" or user == None: # Basic command setup parser = argparse.ArgumentParser(description="Command line interface for Howdy face authentication.", - formatter_class=argparse.RawDescriptionHelpFormatter, - add_help=False, - prog="howdy", - epilog="For support please visit\nhttps://github.com/boltgolt/howdy") + formatter_class=argparse.RawDescriptionHelpFormatter, + add_help=False, + prog="howdy", + epilog="For support please visit\nhttps://github.com/boltgolt/howdy") # Add an argument for the command parser.add_argument("command", - help="The command option to execute, can be one of the following: add, clear, config, disable, list, remove or test.", - metavar="command", - choices=["add", "clear", "config", "disable", "list", "remove", "test"]) + help="The command option to execute, can be one of the following: add, clear, config, disable, list, remove or test.", + metavar="command", + choices=["add", "clear", "config", "disable", "list", "remove", "test"]) # Add an argument for the extra arguments of diable and remove parser.add_argument("argument", - help="Either 0 (enable) or 1 (disable) for the disable command, or the model ID for the remove command.", - nargs="?") + help="Either 0 (enable) or 1 (disable) for the disable command, or the model ID for the remove command.", + nargs="?") # Add the user flag parser.add_argument("-U", "--user", - default=user, - help="Set the user account to use.") + default=user, + help="Set the user account to use.") # Add the -y flag parser.add_argument("-y", - help="Skip all questions.", - action="store_true") + help="Skip all questions.", + action="store_true") # Overwrite the default help message so we can use a uppercase S parser.add_argument("-h", "--help", - action="help", - default=argparse.SUPPRESS, - help="Show this help message and exit.") + action="help", + default=argparse.SUPPRESS, + help="Show this help message and exit.") # If we only have 1 argument we print the help text if len(sys.argv) < 2: diff --git a/src/cli/add.py b/src/cli/add.py index 8df9c65..3106055 100644 --- a/src/cli/add.py +++ b/src/cli/add.py @@ -1,33 +1,53 @@ # Save the face of the user in encoded form # Import required modules -import subprocess import time import os import sys import json -import cv2 import configparser import builtins +import cv2 +import numpy as np -# Try to import face_recognition and give a nice error if we can't +# Try to import dlib and give a nice error if we can't # Add should be the first point where import issues show up try: - import face_recognition + import dlib except ImportError as err: print(err) - print("\nCan't import the face_recognition module, check the output of") - print("pip3 show face_recognition") + print("\nCan't import the dlib module, check the output of") + print("pip3 show dlib") sys.exit(1) -# Get the absolute path to the current file -path = os.path.dirname(os.path.abspath(__file__)) +# Get the absolute path to the current directory +path = os.path.abspath(__file__ + "/..") + +# Test if at lest 1 of the data files is there and abort if it's not +if not os.path.isfile(path + "/../dlib-data/shape_predictor_5_face_landmarks.dat"): + print("Data files have not been downloaded, please run the following commands:") + print("\n\tcd " + os.path.realpath(path + "/../dlib-data")) + print("\tsudo ./install.sh\n") + sys.exit(1) # Read config from disk config = configparser.ConfigParser() config.read(path + "/../config.ini") +if not os.path.exists(config.get("video", "device_path")): + print("Camera path is not configured correctly, please edit the 'device_path' config value.") + sys.exit(1) + +use_cnn = config.getboolean("core", "use_cnn", fallback=False) +if use_cnn: + face_detector = dlib.cnn_face_detection_model_v1(path + "/../dlib-data/mmod_human_face_detector.dat") +else: + face_detector = dlib.get_frontal_face_detector() + +pose_predictor = dlib.shape_predictor(path + "/../dlib-data/shape_predictor_5_face_landmarks.dat") +face_encoder = dlib.face_recognition_model_v1(path + "/../dlib-data/dlib_face_recognition_resnet_model_v1.dat") + user = builtins.howdy_user # The permanent file to store the encoded model in enc_file = path + "/../models/" + user + ".dat" @@ -47,8 +67,8 @@ except FileNotFoundError: # Print a warning if too many encodings are being added if len(encodings) > 3: - print("WARNING: Every additional model slows down the face recognition engine") - print("Press ctrl+C to cancel\n") + print("NOTICE: Each additional model slows down the face recognition engine slightly") + print("Press Ctrl+C to cancel\n") print("Adding face model for the user " + user) @@ -56,15 +76,15 @@ print("Adding face model for the user " + user) label = "Initial model" # If models already exist, set that default label -if len(encodings) > 0: +if encodings: label = "Model #" + str(len(encodings) + 1) # Keep de default name if we can't ask questions if builtins.howdy_args.y: - print("Using default label \"" + label + "\" because of -y flag") + print('Using default label "%s" because of -y flag' % (label, )) else: # Ask the user for a custom label - label_in = input("Enter a label for this new model [" + label + "]: ") + label_in = input("Enter a label for this new model [" + label + "] (max 24 characters): ") # Set the custom label (if any) and limit it to 24 characters if label_in != "": @@ -78,22 +98,35 @@ insert_model = { "data": [] } -# Open the camera -video_capture = cv2.VideoCapture(config.get("video", "device_path")) +# Check if the user explicitly set ffmpeg as recorder +if config.get("video", "recording_plugin") == "ffmpeg": + # Set the capture source for ffmpeg + from recorders.ffmpeg_reader import ffmpeg_reader + video_capture = ffmpeg_reader(config.get("video", "device_path"), config.get("video", "device_format")) +elif config.get("video", "recording_plugin") == "pyv4l2": + # Set the capture source for pyv4l2 + from recorders.pyv4l2_reader import pyv4l2_reader + video_capture = pyv4l2_reader(config.get("video", "device_path"), config.get("video", "device_format")) +else: + # Start video capture on the IR camera through OpenCV + video_capture = cv2.VideoCapture(config.get("video", "device_path")) # Force MJPEG decoding if true -if config.get("video", "force_mjpeg") == "true": +if config.getboolean("video", "force_mjpeg", fallback=False): + # Set a magic number, will enable MJPEG but is badly documentated video_capture.set(cv2.CAP_PROP_FOURCC, 1196444237) # Set the frame width and height if requested -if int(config.get("video", "frame_width")) != -1: - video_capture.set(cv2.CAP_PROP_FRAME_WIDTH, int(config.get("video", "frame_width"))) +fw = config.getint("video", "frame_width", fallback=-1) +fh = config.getint("video", "frame_height", fallback=-1) +if fw != -1: + video_capture.set(cv2.CAP_PROP_FRAME_WIDTH, fw) -if int(config.get("video", "frame_height")) != -1: - video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, int(config.get("video", "frame_height"))) +if fh != -1: + video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, fh) # Request a frame to wake the camera up -video_capture.read() +video_capture.grab() print("\nPlease look straight into the camera") @@ -104,40 +137,53 @@ time.sleep(2) enc = [] # Count the amount or read frames frames = 0 +dark_threshold = config.getfloat("video", "dark_threshold") # Loop through frames till we hit a timeout while frames < 60: - frames += 1 - # Grab a single frame of video # Don't remove ret, it doesn't work without it ret, frame = video_capture.read() + gsframe = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - # Get the encodings in the frame - enc = face_recognition.face_encodings(frame) + # Create a histogram of the image with 8 values + hist = cv2.calcHist([gsframe], [0], None, [8], [0, 256]) + # All values combined for percentage calculation + hist_total = np.sum(hist) + + # If the image is fully black or the frame exceeds threshold, + # skip to the next frame + if hist_total == 0 or (hist[0] / hist_total * 100 > dark_threshold): + continue + + frames += 1 + + # Get all faces from that frame as encodings + face_locations = face_detector(gsframe, 1) # If we've found at least one, we can continue - if len(enc) > 0: + if face_locations: break -# If 0 faces are detected we can't continue -if len(enc) == 0: +video_capture.release() + +# If more than 1 faces are detected we can't know wich one belongs to the user +if len(face_locations) > 1: + print("Multiple faces detected, aborting") + sys.exit(1) +elif not face_locations: print("No face detected, aborting") sys.exit(1) -# If more than 1 faces are detected we can't know wich one belongs to the user -if len(enc) > 1: - print("Multiple faces detected, aborting") - sys.exit(1) +face_location = face_locations[0] +if use_cnn: + face_location = face_location.rect -# Totally clean array that can be exported as JSON -clean_enc = [] +# Get the encodings in the frame +face_landmark = pose_predictor(frame, face_location) +face_encoding = np.array(face_encoder.compute_face_descriptor(frame, face_landmark, 1)) -# Copy the values into a clean array so we can export it as JSON later on -for point in enc[0]: - clean_enc.append(point) - -insert_model["data"].append(clean_enc) +insert_model["data"].append(face_encoding.tolist()) # Insert full object into the list encodings.append(insert_model) diff --git a/src/cli/config.py b/src/cli/config.py index aaa5518..4f6caca 100644 --- a/src/cli/config.py +++ b/src/cli/config.py @@ -17,4 +17,4 @@ elif "EDITOR" in os.environ: editor = os.environ["EDITOR"] # Open the editor as a subprocess and fork it -subprocess.call([editor, os.path.dirname(os.path.realpath(__file__)) + "/../config.ini"]) +subprocess.call([editor, os.path.dirname(os.path.realpath(__file__)) + "/../config.ini"]) diff --git a/src/cli/disable.py b/src/cli/disable.py index 8bc0341..cba8931 100644 --- a/src/cli/disable.py +++ b/src/cli/disable.py @@ -16,7 +16,7 @@ config = configparser.ConfigParser() config.read(config_path) # Check if enough arguments have been passed -if builtins.howdy_args.argument == None: +if builtins.howdy_args.argument is None: print("Please add a 0 (enable) or a 1 (disable) as an argument") sys.exit(1) diff --git a/src/cli/list.py b/src/cli/list.py index 061bf49..08a1e9f 100644 --- a/src/cli/list.py +++ b/src/cli/list.py @@ -8,7 +8,7 @@ import time import builtins # Get the absolute path and the username -path = os.path.dirname(os.path.realpath(__file__)) + "/.." +path = os.path.dirname(os.path.realpath(__file__)) + "/.." user = builtins.howdy_user # Check if the models file has been created yet diff --git a/src/cli/remove.py b/src/cli/remove.py index deef825..45e2036 100644 --- a/src/cli/remove.py +++ b/src/cli/remove.py @@ -7,11 +7,11 @@ import json import builtins # Get the absolute path and the username -path = os.path.dirname(os.path.realpath(__file__)) + "/.." +path = os.path.dirname(os.path.realpath(__file__)) + "/.." user = builtins.howdy_user # Check if enough arguments have been passed -if builtins.howdy_args.argument == None: +if builtins.howdy_args.argument is None: print("Please add the ID of the model you want to remove as an argument") print("You can find the IDs by running:") print("\n\thowdy list\n") diff --git a/src/cli/test.py b/src/cli/test.py index 3a55053..c3cbebb 100644 --- a/src/cli/test.py +++ b/src/cli/test.py @@ -1,14 +1,12 @@ # Show a windows with the video stream and testing information # Import required modules -import face_recognition -import cv2 import configparser import os import sys -import json -import numpy import time +import cv2 +import dlib # Get the absolute path to the current file path = os.path.dirname(os.path.abspath(__file__)) @@ -17,19 +15,27 @@ path = os.path.dirname(os.path.abspath(__file__)) config = configparser.ConfigParser() config.read(path + "/../config.ini") +if config.get("video", "recording_plugin") != "opencv": + print("Howdy has been configured to use a recorder which doesn't support the test command yet") + print("Aborting") + sys.exit(12) + # Start capturing from the configured webcam video_capture = cv2.VideoCapture(config.get("video", "device_path")) # Force MJPEG decoding if true -if config.get("video", "force_mjpeg") == "true": +if config.getboolean("video", "force_mjpeg", fallback=False): + # Set a magic number, will enable MJPEG but is badly documented video_capture.set(cv2.CAP_PROP_FOURCC, 1196444237) # Set the frame width and height if requested -if int(config.get("video", "frame_width")) != -1: - video_capture.set(cv2.CAP_PROP_FRAME_WIDTH, int(config.get("video", "frame_width"))) +fw = config.getint("video", "frame_width", fallback=-1) +fh = config.getint("video", "frame_height", fallback=-1) +if fw != -1: + video_capture.set(cv2.CAP_PROP_FRAME_WIDTH, fw) -if int(config.get("video", "frame_height")) != -1: - video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, int(config.get("video", "frame_height"))) +if fh != -1: + video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, fh) # Let the user know what's up print(""" @@ -39,6 +45,7 @@ Press ctrl+C in this terminal to quit Click on the image to enable or disable slow mode """) + def mouse(event, x, y, flags, param): """Handle mouse events""" global slow_mode @@ -47,6 +54,21 @@ def mouse(event, x, y, flags, param): if event == cv2.EVENT_LBUTTONDOWN: slow_mode = not slow_mode + +def print_text(line_number, text): + """Print the status text by line number""" + cv2.putText(overlay, text, (10, height - 10 - (10 * line_number)), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA) + +use_cnn = config.getboolean('core', 'use_cnn', fallback=False) +if use_cnn: + face_detector = dlib.cnn_face_detection_model_v1( + path + '/../dlib-data/mmod_human_face_detector.dat' + ) +else: + face_detector = dlib.get_frontal_face_detector() + +clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) + # Open the window and attach a a mouse listener cv2.namedWindow("Howdy Test") cv2.setMouseCallback("Howdy Test", mouse) @@ -61,26 +83,31 @@ sec_frames = 0 fps = 0 # The current second we're counting sec = int(time.time()) +# recognition time +rec_tm = 0 # Wrap everything in an keyboard interupt handler try: while True: - # Inclement the frames + frame_tm = time.time() + + # Increment the frames total_frames += 1 sec_frames += 1 # Id we've entered a new second - if sec != int(time.time()): + if sec != int(frame_tm): # Set the last seconds FPS fps = sec_frames # Set the new second and reset the counter - sec = int(time.time()) + sec = int(frame_tm) sec_frames = 0 - # Grab a single frame of video - ret, frame = (video_capture.read()) + ret, frame = video_capture.read() + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + frame = clahe.apply(frame) # Make a frame to put overlays in overlay = frame.copy() @@ -94,7 +121,7 @@ try: # Fill with the overal containing percentage hist_perc = [] - # Loop though all values to calculate a pensentage and add it to the overlay + # Loop though all values to calculate a percentage and add it to the overlay for index, value in enumerate(hist): value_perc = float(value[0]) / hist_total * 100 hist_perc.append(value_perc) @@ -109,14 +136,11 @@ try: # Draw a stripe indicating the dark threshold cv2.rectangle(overlay, (8, 35), (20, 36), (255, 0, 0), thickness=cv2.FILLED) - def print_text(line_number, text): - """Print the status text by line number""" - cv2.putText(overlay, text, (10, height - 10 - (10 * line_number)), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA) - # Print the statis in the bottom left - print_text(0, "RESOLUTION: " + str(height) + "x" + str(width)) - print_text(1, "FPS: " + str(fps)) - print_text(2, "FRAMES: " + str(total_frames)) + print_text(0, "RESOLUTION: %dx%d" % (height, width)) + print_text(1, "FPS: %d" % (fps, )) + print_text(2, "FRAMES: %d" % (total_frames, )) + print_text(3, "RECOGNITION: %dms" % (round(rec_tm * 1000), )) # Show that slow mode is on, if it's on if slow_mode: @@ -130,17 +154,22 @@ try: # SHow that this is an active frame cv2.putText(overlay, "SCAN FRAME", (width - 68, 16), cv2.FONT_HERSHEY_SIMPLEX, .3, (0, 255, 0), 0, cv2.LINE_AA) + rec_tm = time.time() # Get the locations of all faces and their locations - face_locations = face_recognition.face_locations(frame) + face_locations = face_detector(frame, 1) # upsample 1 time + rec_tm = time.time() - rec_tm # Loop though all faces and paint a circle around them for loc in face_locations: + if use_cnn: + loc = loc.rect + # Get the center X and Y from the rectangular points - x = int((loc[1] - loc[3]) / 2) + loc[3] - y = int((loc[2] - loc[0]) / 2) + loc[0] + x = int((loc.right() - loc.left()) / 2) + loc.left() + y = int((loc.bottom() - loc.top()) / 2) + loc.top() # Get the raduis from the with of the square - r = (loc[1] - loc[3]) / 2 + r = (loc.right() - loc.left()) / 2 # Add 20% padding r = int(r + (r * 0.2)) @@ -158,9 +187,11 @@ try: if cv2.waitKey(1) != -1: raise KeyboardInterrupt() + frame_time = time.time() - frame_tm + # Delay the frame if slowmode is on if slow_mode: - time.sleep(.55) + time.sleep(.5 - frame_time) # On ctrl+C except KeyboardInterrupt: diff --git a/src/compare.py b/src/compare.py index 62824d6..3f472b8 100644 --- a/src/compare.py +++ b/src/compare.py @@ -5,33 +5,62 @@ import time # Start timing -timings = [time.time()] +timings = { + "st": time.time() +} # Import required modules -import cv2 import sys import os import json -import math import configparser +import cv2 +import dlib +import numpy as np +import _thread as thread + + +def init_detector(lock): + """Start face detector, encoder and predictor in a new thread""" + global face_detector, pose_predictor, face_encoder + + # Test if at lest 1 of the data files is there and abort if it's not + if not os.path.isfile(PATH + "/dlib-data/shape_predictor_5_face_landmarks.dat"): + print("Data files have not been downloaded, please run the following commands:") + print("\n\tcd " + PATH + "/dlib-data") + print("\tsudo ./install.sh\n") + lock.release() + sys.exit(1) + + # Use the CNN detector if enabled + if use_cnn: + face_detector = dlib.cnn_face_detection_model_v1(PATH + "/dlib-data/mmod_human_face_detector.dat") + else: + face_detector = dlib.get_frontal_face_detector() + + # Start the others regardless + pose_predictor = dlib.shape_predictor(PATH + "/dlib-data/shape_predictor_5_face_landmarks.dat") + face_encoder = dlib.face_recognition_model_v1(PATH + "/dlib-data/dlib_face_recognition_resnet_model_v1.dat") + + # Note the time it took to initialize detectors + timings["ll"] = time.time() - timings["ll"] + lock.release() -# Read config from disk -config = configparser.ConfigParser() -config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") def stop(status): """Stop the execution and close video stream""" video_capture.release() sys.exit(status) -# Make sure we were given an username to tast against -try: - if not isinstance(sys.argv[1], str): - sys.exit(1) -except IndexError: - sys.exit(1) -# The username of the authenticating user +# Make sure we were given an username to tast against +if len(sys.argv) < 2: + sys.exit(12) + +# Get the absolute path to the current directory +PATH = os.path.abspath(__file__ + "/..") + +# The username of the user being authenticated user = sys.argv[1] # The model file contents models = [] @@ -39,10 +68,19 @@ models = [] encodings = [] # Amount of ingnored dark frames dark_tries = 0 +# Total amount of frames captured +frames = 0 +# face recognition/detection instances +face_detector = None +pose_predictor = None +face_encoder = None # Try to load the face model from the models folder try: - models = json.load(open(os.path.dirname(os.path.abspath(__file__)) + "/models/" + user + ".dat")) + models = json.load(open(PATH + "/models/" + user + ".dat")) + + for model in models: + encodings += model["data"] except FileNotFoundError: sys.exit(10) @@ -50,127 +88,176 @@ except FileNotFoundError: if len(models) < 1: sys.exit(10) -# Put all models together into 1 array -for model in models: - encodings += model["data"] +# Read config from disk +config = configparser.ConfigParser() +config.read(PATH + "/config.ini") -# Add the time needed to start the script -timings.append(time.time()) +# Get all config values needed +use_cnn = config.getboolean("core", "use_cnn", fallback=False) +timeout = config.getint("video", "timout", fallback=5) +dark_threshold = config.getfloat("video", "dark_threshold", fallback=50.0) +video_certainty = config.getfloat("video", "certainty", fallback=3.5) / 10 +end_report = config.getboolean("debug", "end_report", fallback=False) + +# Save the time needed to start the script +timings["in"] = time.time() - timings["st"] + +# Import face recognition, takes some time +timings["ll"] = time.time() + +# Start threading and wait for init to finish +lock = thread.allocate_lock() +lock.acquire() +thread.start_new_thread(init_detector, (lock, )) # Start video capture on the IR camera -video_capture = cv2.VideoCapture(config.get("video", "device_path")) +timings["ic"] = time.time() + +# Check if the user explicitly set ffmpeg as recorder +if config.get("video", "recording_plugin") == "ffmpeg": + # Set the capture source for ffmpeg + from recorders.ffmpeg_reader import ffmpeg_reader + video_capture = ffmpeg_reader(config.get("video", "device_path"), config.get("video", "device_format")) +elif config.get("video", "recording_plugin") == "pyv4l2": + # Set the capture source for pyv4l2 + from recorders.pyv4l2_reader import pyv4l2_reader + video_capture = pyv4l2_reader(config.get("video", "device_path"), config.get("video", "device_format")) +else: + # Start video capture on the IR camera through OpenCV + video_capture = cv2.VideoCapture(config.get("video", "device_path")) # Force MJPEG decoding if true -if config.get("video", "force_mjpeg") == "true": - # Set a magic number, will enable MJPEG but is badly documentated +if config.getboolean("video", "force_mjpeg", fallback=False): + # Set a magic number, will enable MJPEG but is badly documented + # 1196444237 is "GPJM" in ASCII video_capture.set(cv2.CAP_PROP_FOURCC, 1196444237) # Set the frame width and height if requested -if int(config.get("video", "frame_width")) != -1: - video_capture.set(cv2.CAP_PROP_FRAME_WIDTH, int(config.get("video", "frame_width"))) - -if int(config.get("video", "frame_height")) != -1: - video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, int(config.get("video", "frame_height"))) +fw = config.getint("video", "frame_width", fallback=-1) +fh = config.getint("video", "frame_height", fallback=-1) +if fw != -1: + video_capture.set(cv2.CAP_PROP_FRAME_WIDTH, fw) +if fh != -1: + video_capture.set(cv2.CAP_PROP_FRAME_HEIGHT, fh) # Capture a single frame so the camera becomes active # This will let the camera adjust its light levels while we're importing for faster scanning -video_capture.read() +video_capture.grab() # Note the time it took to open the camera -timings.append(time.time()) +timings["ic"] = time.time() - timings["ic"] -# Import face recognition, takes some time -import face_recognition -timings.append(time.time()) +# wait for thread to finish +lock.acquire() +lock.release() +del lock # Fetch the max frame height -max_height = int(config.get("video", "max_height")) +max_height = config.getfloat("video", "max_height", fallback=0.0) +# Get the height of the image +height = video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT) or 1 + +# Calculate the amount the image has to shrink +scaling_factor = (max_height / height) or 1 + +# Fetch config settings out of the loop +timeout = config.getint("video", "timeout") +dark_threshold = config.getfloat("video", "dark_threshold") +end_report = config.getboolean("debug", "end_report") # Start the read loop frames = 0 +timings["fr"] = time.time() + while True: # Increment the frame count every loop frames += 1 # Stop if we've exceded the time limit - if time.time() - timings[3] > int(config.get("video", "timout")): + if time.time() - timings["fr"] > timeout: stop(11) # Grab a single frame of video - # Don't remove ret, it doesn't work without it ret, frame = video_capture.read() + try: + # Convert from color to grayscale + # First processing of frame, so frame errors show up here + gsframe = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + except cv2.error: + print("\nUnknown camera, please check your 'device_path' config value.\n") + raise + # Create a histogram of the image with 8 values - hist = cv2.calcHist([frame], [0], None, [8], [0, 256]) + hist = cv2.calcHist([gsframe], [0], None, [8], [0, 256]) # All values combined for percentage calculation - hist_total = int(sum(hist)[0]) + hist_total = np.sum(hist) - # If the image is fully black, skip to the next frame - if hist_total == 0: + # If the image is fully black or the frame exceeds threshold, + # skip to the next frame + if hist_total == 0 or (hist[0] / hist_total * 100 > dark_threshold): dark_tries += 1 continue - # Scrip the frame if it exceeds the threshold - if float(hist[0]) / hist_total * 100 > float(config.get("video", "dark_threshold")): - dark_tries += 1 - continue - - # Get the height and with of the image - height, width = frame.shape[:2] - # If the hight is too high - if max_height < height: - # Calculate the amount the image has to shrink - scaling_factor = max_height / float(height) + if scaling_factor != 1: # Apply that factor to the frame frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) - - # Save the new size for diagnostics - scale_height, scale_width = frame.shape[:2] + gsframe = cv2.resize(gsframe, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA) # Get all faces from that frame as encodings - face_encodings = face_recognition.face_encodings(frame) + # Upsamples 1 time + face_locations = face_detector(gsframe, 1) # Loop through each face - for face_encoding in face_encodings: + for fl in face_locations: + if use_cnn: + fl = fl.rect + + # Fetch the faces in the image + face_landmark = pose_predictor(frame, fl) + face_encoding = np.array(face_encoder.compute_face_descriptor(frame, face_landmark, 1)) + # Match this found face against a known face - matches = face_recognition.face_distance(encodings, face_encoding) + matches = np.linalg.norm(encodings - face_encoding, axis=1) - # Check if any match is certain enough to be the user we're looking for - match_index = 0 - for match in matches: - match_index += 1 + # Get best match + match_index = np.argmin(matches) + match = matches[match_index] - # Try to find a match that's confident enough - if match * 10 < float(config.get("video", "certainty")) and match > 0: - timings.append(time.time()) + # Check if a match that's confident enough + if 0 < match < video_certainty: + timings["tt"] = time.time() - timings["st"] + timings["fr"] = time.time() - timings["fr"] - # If set to true in the config, print debug text - if config.get("debug", "end_report") == "true": - def print_timing(label, offset): - """Helper function to print a timing from the list""" - print(" " + label + ": " + str(round((timings[1 + offset] - timings[offset]) * 1000)) + "ms") + # If set to true in the config, print debug text + if end_report: + def print_timing(label, k): + """Helper function to print a timing from the list""" + print(" %s: %dms" % (label, round(timings[k] * 1000))) - print("Time spend") - print_timing("Starting up", 0) - print_timing("Opening the camera", 1) - print_timing("Importing face_recognition", 2) - print_timing("Searching for known face", 3) + # Print a nice timing report + print("Time spent") + print_timing("Starting up", "in") + print(" Open cam + load libs: %dms" % (round(max(timings["ll"], timings["ic"]) * 1000, ))) + print_timing(" Opening the camera", "ic") + print_timing(" Importing recognition libs", "ll") + print_timing("Searching for known face", "fr") + print_timing("Total time", "tt") - print("\nResolution") - print(" Native: " + str(height) + "x" + str(width)) - print(" Used: " + str(scale_height) + "x" + str(scale_width)) + print("\nResolution") + width = video_capture.get(cv2.CAP_PROP_FRAME_WIDTH) or 1 + print(" Native: %dx%d" % (height, width)) + # Save the new size for diagnostics + scale_height, scale_width = frame.shape[:2] + print(" Used: %dx%d" % (scale_height, scale_width)) - # Show the total number of frames and calculate the FPS by deviding it by the total scan time - print("\nFrames searched: " + str(frames) + " (" + str(round(float(frames) / (timings[4] - timings[3]), 2)) + " fps)") - print("Dark frames ignored: " + str(dark_tries)) - print("Certainty of winning frame: " + str(round(match * 10, 3))) + # Show the total number of frames and calculate the FPS by deviding it by the total scan time + print("\nFrames searched: %d (%.2f fps)" % (frames, frames / timings["fr"])) + print("Dark frames ignored: %d " % (dark_tries, )) + print("Certainty of winning frame: %.3f" % (match * 10, )) - # Catch older 3-encoding models - if not match_index in models: - match_index = 0 + print("Winning model: %d (\"%s\")" % (match_index, models[match_index]["label"])) - print("Winning model: " + str(match_index) + " (\"" + models[match_index]["label"] + "\")") - - # End peacefully - stop(0) + # End peacefully + stop(0) diff --git a/src/config.ini b/src/config.ini index 1f577e9..34d5685 100644 --- a/src/config.ini +++ b/src/config.ini @@ -2,6 +2,9 @@ # Press CTRL + X to save in the nano editor [core] +# Print that face detection is being attempted +detection_notice = false + # Do not print anything when a face verification succeeds no_confirmation = false @@ -14,33 +17,34 @@ ignore_ssh = true # Auto dismiss lock screen on confirmation # Will run loginctl unlock-sessions after every auth -# Expirimental, can behave incorrectly on some systems +# Experimental, can behave incorrectly on some systems dismiss_lockscreen = false # Disable howdy in the PAM # The howdy command will still function disabled = false +# Use CNN instead of HOG +# CNN model is much more accurate than the HOG based model, but takes much more +# computational power to run, and is meant to be executed on a GPU to attain reasonable speed. +use_cnn = false + [video] # The certainty of the detected face belonging to the user of the account # On a scale from 1 to 10, values above 5 are not recommended certainty = 3.5 # The number of seconds to search before timing out -timout = 4 +timeout = 4 # The path of the device to capture frames from -# Should be set automatically by the installer +# Should be set automatically by an installer if your distro has one device_path = none # Scale down the video feed to this maximum height # Speeds up face recognition but can make it less precise max_height = 320 -# Force the use of Motion JPEG when decoding frames, fixes issues with -# YUYV raw frame deconding -force_mjpeg = false - # Set the camera input profile to this width and height # The largest profile will be used if set to -1 # Automatically ignored if not a valid profile @@ -53,6 +57,19 @@ frame_height = -1 # The lower this setting is, the more dark frames are ignored dark_threshold = 50 +# The recorder to use. Can be either opencv (default), ffmpeg or pyv4l2. +# Switching from the default opencv to ffmpeg can help with grayscale issues. +recording_plugin = opencv + +# Video format used by ffmpeg. Options include vfwcap or v4l2. +# FFMPEG only. +device_format = v4l2 + +# Force the use of Motion JPEG when decoding frames, fixes issues with YUYV +# raw frame decoding. +# OPENCV only. +force_mjpeg = false + [debug] # Show a short but detailed diagnostic report in console end_report = false diff --git a/src/dlib-data/.gitignore b/src/dlib-data/.gitignore new file mode 100644 index 0000000..e4d30eb --- /dev/null +++ b/src/dlib-data/.gitignore @@ -0,0 +1,2 @@ +*.dat +*.dat.bz2 diff --git a/src/dlib-data/Readme.md b/src/dlib-data/Readme.md new file mode 100644 index 0000000..a940a5f --- /dev/null +++ b/src/dlib-data/Readme.md @@ -0,0 +1,7 @@ +Download and unpack `dlib` data files from https://github.com/davisking/dlib-models repository: +```shell +wget https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2 +wget https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2 +wget https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2 +bunzip *bz2 +``` diff --git a/src/dlib-data/install.sh b/src/dlib-data/install.sh new file mode 100755 index 0000000..7fcabe6 --- /dev/null +++ b/src/dlib-data/install.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +echo "Downloading 3 required data files..." + +# Check if wget is installed +if hash wget;then + # Check if wget supports the option to only show the progress bar + wget --help | grep -q "\--show-progress" && \ + _PROGRESS_OPT="-q --show-progress" || _PROGRESS_OPT="" + + # Download the archives + wget $_PROGRESS_OPT --tries 5 https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2 + wget $_PROGRESS_OPT --tries 5 https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2 + wget $_PROGRESS_OPT --tries 5 https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2 + +# Otherwise fall back on curl +else + curl --location --retry 5 --output dlib_face_recognition_resnet_model_v1.dat.bz2 https://github.com/davisking/dlib-models/raw/master/dlib_face_recognition_resnet_model_v1.dat.bz2 + curl --location --retry 5 --output mmod_human_face_detector.dat.bz2 https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2 + curl --location --retry 5 --output shape_predictor_5_face_landmarks.dat.bz2 https://github.com/davisking/dlib-models/raw/master/shape_predictor_5_face_landmarks.dat.bz2 +fi + +# Uncompress the data files and delete the original archive +echo "Unpacking..." +bzip2 -d *.bz2 diff --git a/src/pam-config/howdy b/src/pam-config/howdy new file mode 100644 index 0000000..28fb6b1 --- /dev/null +++ b/src/pam-config/howdy @@ -0,0 +1,6 @@ +Name: Howdy +Default: yes +Priority: 512 +Auth-Type: Primary +Auth: + [success=end default=ignore] pam_python.so /lib/security/howdy/pam.py diff --git a/src/pam.py b/src/pam.py index 42ee919..da98cf1 100644 --- a/src/pam.py +++ b/src/pam.py @@ -12,38 +12,46 @@ import ConfigParser config = ConfigParser.ConfigParser() config.read(os.path.dirname(os.path.abspath(__file__)) + "/config.ini") + def doAuth(pamh): """Starts authentication in a seperate process""" # Abort is Howdy is disabled - if config.get("core", "disabled") == "true": + if config.getboolean("core", "disabled"): sys.exit(0) # Abort if we're in a remote SSH env - if config.get("core", "ignore_ssh") == "true": + if config.getboolean("core", "ignore_ssh"): if "SSH_CONNECTION" in os.environ or "SSH_CLIENT" in os.environ or "SSHD_OPTS" in os.environ: sys.exit(0) + # Alert the user that we are doing face detection + if config.get("core", "detection_notice") == "true": + pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Attempting face detection")) + # Run compare as python3 subprocess to circumvent python version and import issues status = subprocess.call(["/usr/bin/python3", os.path.dirname(os.path.abspath(__file__)) + "/compare.py", pamh.get_user()]) # Status 10 means we couldn't find any face models if status == 10: - if config.get("core", "suppress_unknown") != "true": + if not config.getboolean("core", "suppress_unknown"): pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "No face model known")) return pamh.PAM_USER_UNKNOWN # Status 11 means we exceded the maximum retry count - if status == 11: + elif status == 11: pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Face detection timeout reached")) return pamh.PAM_AUTH_ERR + # Status 12 means we aborted + elif status == 12: + return pamh.PAM_AUTH_ERR # Status 0 is a successful exit - if status == 0: + elif status == 0: # Show the success message if it isn't suppressed - if config.get("core", "no_confirmation") != "true": + if not config.getboolean("core", "no_confirmation"): pamh.conversation(pamh.Message(pamh.PAM_TEXT_INFO, "Identified face as " + pamh.get_user())) # Try to dismiss the lock screen if enabled - if config.get("core", "dismiss_lockscreen") == "true": + if config.get("core", "dismiss_lockscreen"): # Run it as root with a timeout of 1s, and never ask for a password through the UI subprocess.Popen(["sudo", "timeout", "1", "loginctl", "unlock-sessions", "--no-ask-password"]) @@ -53,18 +61,22 @@ def doAuth(pamh): pamh.conversation(pamh.Message(pamh.PAM_ERROR_MSG, "Unknown error: " + str(status))) return pamh.PAM_SYSTEM_ERR + def pam_sm_authenticate(pamh, flags, args): """Called by PAM when the user wants to authenticate, in sudo for example""" return doAuth(pamh) + def pam_sm_open_session(pamh, flags, args): """Called when starting a session, such as su""" return doAuth(pamh) + def pam_sm_close_session(pamh, flags, argv): """We don't need to clean anyting up at the end of a session, so returns true""" return pamh.PAM_SUCCESS + def pam_sm_setcred(pamh, flags, argv): """We don't need set any credentials, so returns true""" return pamh.PAM_SUCCESS diff --git a/src/recorders/__init__.py b/src/recorders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/recorders/ffmpeg_reader.py b/src/recorders/ffmpeg_reader.py new file mode 100644 index 0000000..e9856f6 --- /dev/null +++ b/src/recorders/ffmpeg_reader.py @@ -0,0 +1,132 @@ +# Class that simulates the functionality of opencv so howdy can use ffmpeg seamlessly + +# Import required modules +import numpy +import sys +import re +from subprocess import Popen, PIPE +from cv2 import CAP_PROP_FRAME_WIDTH +from cv2 import CAP_PROP_FRAME_HEIGHT + +try: + import ffmpeg +except ImportError: + print("Missing ffmpeg module, please run:") + print(" pip3 install ffmpeg-python\n") + sys.exit(12) + + +class ffmpeg_reader: + """ This class was created to look as similar to the openCV features used in Howdy as possible for overall code cleanliness. """ + + def __init__(self, device_path, device_format, numframes=10): + self.device_path = device_path + self.device_format = device_format + self.numframes = numframes + self.video = () + self.num_frames_read = 0 + self.height = 0 + self.width = 0 + self.init_camera = True + + def set(self, prop, setting): + """ Setter method for height and width """ + if prop == CAP_PROP_FRAME_WIDTH: + self.width = setting + elif prop == CAP_PROP_FRAME_HEIGHT: + self.height = setting + + def get(self, prop): + """ Getter method for height and width """ + if prop == CAP_PROP_FRAME_WIDTH: + return self.width + elif prop == CAP_PROP_FRAME_HEIGHT: + return self.height + + def probe(self): + """ Probe the video device to get height and width info """ + + # Running this command on ffmpeg unfortunately returns with an exit code of 1, which is silly. + # Returns an error code of 1 and this text: "/dev/video2: Immediate exit requested" + args = ["ffmpeg", "-f", self.device_format, "-list_formats", "all", "-i", self.device_path] + process = Popen(args, stdout=PIPE, stderr=PIPE) + out, err = process.communicate() + return_code = process.poll() + + # Worst case scenario, err will equal en empty byte string, b'', so probe will get set to [] here. + regex = re.compile(r"\s\d{3,4}x\d{3,4}") + probe = regex.findall(str(err.decode("utf-8"))) + + if not return_code == 1 or len(probe) < 1: + # Could not determine the resolution from ffmpeg call. Reverting to ffmpeg.probe() + probe = ffmpeg.probe(self.device_path) + height = probe["streams"][0]["height"] + width = probe["streams"][0]["width"] + else: + (height, width) = [x.strip() for x in probe[0].split("x")] + + # Set height and width from probe if they haven't been set already + if height.isdigit() and self.get(CAP_PROP_FRAME_HEIGHT) == 0: + self.set(CAP_PROP_FRAME_HEIGHT, int(height)) + if width.isdigit() and self.get(CAP_PROP_FRAME_WIDTH) == 0: + self.set(CAP_PROP_FRAME_WIDTH, int(width)) + + def record(self, numframes): + """ Record a video, saving it to self.video array for processing later """ + + # Eensure we have set our width and height before we record, otherwise our numpy call will fail + if self.get(CAP_PROP_FRAME_WIDTH) == 0 or self.get(CAP_PROP_FRAME_HEIGHT) == 0: + self.probe() + + # Ensure num_frames_read is reset to 0 + self.num_frames_read = 0 + + # Record a predetermined amount of frames from the camera + stream, ret = ( + ffmpeg + .input(self.device_path, format=self.device_format) + .output("pipe:", format="rawvideo", pix_fmt="rgb24", vframes=numframes) + .run(capture_stdout=True, quiet=True) + ) + self.video = ( + numpy + .frombuffer(stream, numpy.uint8) + .reshape([-1, self.width, self.height, 3]) + ) + + def read(self): + """ Read a sigle frame from the self.video array. Will record a video if array is empty. """ + + # First time we are called, we want to initialize the camera by probing it, to ensure we have height/width + # and then take numframes of video to fill the buffer for faster recognition. + if self.init_camera: + self.init_camera = False + self.video = () + self.record(self.numframes) + return 0, self.video + + # If we are called and self.video is empty, we should record self.numframes to fill the video buffer + if self.video == (): + self.record(self.numframes) + + # If we've read max frames, but still are being requested to read more, we simply record another batch. + # Note, the video array is 0 based, so if numframes is 10, we must subtract 1 or run into an array index + # error. + if self.num_frames_read >= (self.numframes - 1): + self.record(self.numframes) + + # Add one to num_frames_read. If we were at 0, that's fine as frame 0 is almost 100% going to be black + # as the IR lights aren't fully active yet anyways. Saves us one iteration in the while loop ni add/compare.py. + self.num_frames_read += 1 + + # Return a single frame of video + return 0, self.video[self.num_frames_read] + + def release(self): + """ Empty our array. If we had a hold on the camera, we would give it back here. """ + self.video = () + self.num_frames_read = 0 + + def grab(self): + """ Redirect grab() to read() for compatibility """ + self.read() diff --git a/src/recorders/pyv4l2_reader.py b/src/recorders/pyv4l2_reader.py new file mode 100644 index 0000000..6d52871 --- /dev/null +++ b/src/recorders/pyv4l2_reader.py @@ -0,0 +1,102 @@ +# Class that simulates the functionality of opencv so howdy can use v4l2 devices seamlessly + +# Import required modules. lib4l-dev package is also required. +from recorders import v4l2 +import fcntl +import numpy +import sys +from cv2 import cvtColor, COLOR_GRAY2BGR, CAP_PROP_FRAME_WIDTH, CAP_PROP_FRAME_HEIGHT + +try: + from v4l2.frame import Frame +except ImportError: + print("Missing pyv4l2 module, please run:") + print(" pip3 install pyv4l2\n") + sys.exit(13) + + +class pyv4l2_reader: + """ This class was created to look as similar to the openCV features used in Howdy as possible for overall code cleanliness. """ + + # Init + def __init__(self, device_name, device_format): + self.device_name = device_name + self.device_format = device_format + self.height = 0 + self.width = 0 + self.probe() + self.frame = "" + + def set(self, prop, setting): + """ Setter method for height and width """ + if prop == CAP_PROP_FRAME_WIDTH: + self.width = setting + elif prop == CAP_PROP_FRAME_HEIGHT: + self.height = setting + + def get(self, prop): + """ Getter method for height and width """ + if prop == CAP_PROP_FRAME_WIDTH: + return self.width + elif prop == CAP_PROP_FRAME_HEIGHT: + return self.height + + def probe(self): + """ Probe the video device to get height and width info """ + + vd = open(self.device_name, 'r') + fmt = v4l2.v4l2_format() + fmt.type = v4l2.V4L2_BUF_TYPE_VIDEO_CAPTURE + ret = fcntl.ioctl(vd, v4l2.VIDIOC_G_FMT, fmt) + vd.close() + if ret == 0: + height = fmt.fmt.pix.height + width = fmt.fmt.pix.width + else: + # Could not determine the resolution from ioctl call. Reverting to slower ffmpeg.probe() method + import ffmpeg + probe = ffmpeg.probe(self.device_name) + height = int(probe['streams'][0]['height']) + width = int(probe['streams'][0]['width']) + + if self.get(CAP_PROP_FRAME_HEIGHT) == 0: + self.set(CAP_PROP_FRAME_HEIGHT, int(height)) + + if self.get(CAP_PROP_FRAME_WIDTH) == 0: + self.set(CAP_PROP_FRAME_WIDTH, int(width)) + + def record(self): + """ Start recording """ + self.frame = Frame(self.device_name) + + def grab(self): + """ Read a sigle frame from the IR camera. """ + self.read() + + def read(self): + """ Read a sigle frame from the IR camera. """ + + if not self.frame: + self.record() + + # Grab a raw frame from the camera + frame_data = self.frame.get_frame() + + # Convert the raw frame_date to a numpy array + img = (numpy.frombuffer(frame_data, numpy.uint8)) + + # Convert the numpy array to a proper grayscale image array + img_bgr = cvtColor(img, COLOR_GRAY2BGR) + + # Convert the grayscale image array into a proper RGB style numpy array + img2 = (numpy.frombuffer(img_bgr, numpy.uint8).reshape([352, 352, 3])) + + # Return a single frame of video + return 0, img2 + + def release(self): + """ Empty our array. If we had a hold on the camera, we would give it back here. """ + self.video = () + self.num_frames_read = 0 + if self.frame: + self.frame.close() diff --git a/src/recorders/v4l2.py b/src/recorders/v4l2.py new file mode 100644 index 0000000..6edf49b --- /dev/null +++ b/src/recorders/v4l2.py @@ -0,0 +1,1914 @@ +# Python bindings for the v4l2 userspace api + +# Copyright (C) 1999-2009 the contributors + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# Alternatively you can redistribute this file under the terms of the +# BSD license as stated below: + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# 3. The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Python bindings for the v4l2 userspace api in Linux 2.6.34 +""" + +# see linux/videodev2.h + +import ctypes + + +_IOC_NRBITS = 8 +_IOC_TYPEBITS = 8 +_IOC_SIZEBITS = 14 +_IOC_DIRBITS = 2 + +_IOC_NRSHIFT = 0 +_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS +_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS +_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS + +_IOC_NONE = 0 +_IOC_WRITE = 1 +_IOC_READ = 2 + + +def _IOC(dir_, type_, nr, size): + return ( + ctypes.c_int32(dir_ << _IOC_DIRSHIFT).value | + ctypes.c_int32(ord(type_) << _IOC_TYPESHIFT).value | + ctypes.c_int32(nr << _IOC_NRSHIFT).value | + ctypes.c_int32(size << _IOC_SIZESHIFT).value) + + +def _IOC_TYPECHECK(t): + return ctypes.sizeof(t) + + +def _IO(type_, nr): + return _IOC(_IOC_NONE, type_, nr, 0) + + +def _IOW(type_, nr, size): + return _IOC(_IOC_WRITE, type_, nr, _IOC_TYPECHECK(size)) + + +def _IOR(type_, nr, size): + return _IOC(_IOC_READ, type_, nr, _IOC_TYPECHECK(size)) + + +def _IOWR(type_, nr, size): + return _IOC(_IOC_READ | _IOC_WRITE, type_, nr, _IOC_TYPECHECK(size)) + + +# +# type alias +# + +enum = ctypes.c_uint +c_int = ctypes.c_int + + +# +# time +# + +class timeval(ctypes.Structure): + _fields_ = [ + ('secs', ctypes.c_long), + ('usecs', ctypes.c_long), + ] + + +# +# v4l2 +# + + +VIDEO_MAX_FRAME = 32 + + +VID_TYPE_CAPTURE = 1 +VID_TYPE_TUNER = 2 +VID_TYPE_TELETEXT = 4 +VID_TYPE_OVERLAY = 8 +VID_TYPE_CHROMAKEY = 16 +VID_TYPE_CLIPPING = 32 +VID_TYPE_FRAMERAM = 64 +VID_TYPE_SCALES = 128 +VID_TYPE_MONOCHROME = 256 +VID_TYPE_SUBCAPTURE = 512 +VID_TYPE_MPEG_DECODER = 1024 +VID_TYPE_MPEG_ENCODER = 2048 +VID_TYPE_MJPEG_DECODER = 4096 +VID_TYPE_MJPEG_ENCODER = 8192 + + +def v4l2_fourcc(a, b, c, d): + return ord(a) | (ord(b) << 8) | (ord(c) << 16) | (ord(d) << 24) + + +v4l2_field = enum +( + V4L2_FIELD_ANY, + V4L2_FIELD_NONE, + V4L2_FIELD_TOP, + V4L2_FIELD_BOTTOM, + V4L2_FIELD_INTERLACED, + V4L2_FIELD_SEQ_TB, + V4L2_FIELD_SEQ_BT, + V4L2_FIELD_ALTERNATE, + V4L2_FIELD_INTERLACED_TB, + V4L2_FIELD_INTERLACED_BT, +) = range(10) + + +def V4L2_FIELD_HAS_TOP(field): + return ( + field == V4L2_FIELD_TOP or + field == V4L2_FIELD_INTERLACED or + field == V4L2_FIELD_INTERLACED_TB or + field == V4L2_FIELD_INTERLACED_BT or + field == V4L2_FIELD_SEQ_TB or + field == V4L2_FIELD_SEQ_BT) + + +def V4L2_FIELD_HAS_BOTTOM(field): + return ( + field == V4L2_FIELD_BOTTOM or + field == V4L2_FIELD_INTERLACED or + field == V4L2_FIELD_INTERLACED_TB or + field == V4L2_FIELD_INTERLACED_BT or + field == V4L2_FIELD_SEQ_TB or + field == V4L2_FIELD_SEQ_BT) + + +def V4L2_FIELD_HAS_BOTH(field): + return ( + field == V4L2_FIELD_INTERLACED or + field == V4L2_FIELD_INTERLACED_TB or + field == V4L2_FIELD_INTERLACED_BT or + field == V4L2_FIELD_SEQ_TB or + field == V4L2_FIELD_SEQ_BT) + + +v4l2_buf_type = enum +( + V4L2_BUF_TYPE_VIDEO_CAPTURE, + V4L2_BUF_TYPE_VIDEO_OUTPUT, + V4L2_BUF_TYPE_VIDEO_OVERLAY, + V4L2_BUF_TYPE_VBI_CAPTURE, + V4L2_BUF_TYPE_VBI_OUTPUT, + V4L2_BUF_TYPE_SLICED_VBI_CAPTURE, + V4L2_BUF_TYPE_SLICED_VBI_OUTPUT, + V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY, + V4L2_BUF_TYPE_PRIVATE, +) = list(range(1, 9)) + [0x80] + + +v4l2_ctrl_type = enum +( + V4L2_CTRL_TYPE_INTEGER, + V4L2_CTRL_TYPE_BOOLEAN, + V4L2_CTRL_TYPE_MENU, + V4L2_CTRL_TYPE_BUTTON, + V4L2_CTRL_TYPE_INTEGER64, + V4L2_CTRL_TYPE_CTRL_CLASS, + V4L2_CTRL_TYPE_STRING, +) = range(1, 8) + + +v4l2_tuner_type = enum +( + V4L2_TUNER_RADIO, + V4L2_TUNER_ANALOG_TV, + V4L2_TUNER_DIGITAL_TV, +) = range(1, 4) + + +v4l2_memory = enum +( + V4L2_MEMORY_MMAP, + V4L2_MEMORY_USERPTR, + V4L2_MEMORY_OVERLAY, +) = range(1, 4) + + +v4l2_colorspace = enum +( + V4L2_COLORSPACE_SMPTE170M, + V4L2_COLORSPACE_SMPTE240M, + V4L2_COLORSPACE_REC709, + V4L2_COLORSPACE_BT878, + V4L2_COLORSPACE_470_SYSTEM_M, + V4L2_COLORSPACE_470_SYSTEM_BG, + V4L2_COLORSPACE_JPEG, + V4L2_COLORSPACE_SRGB, +) = range(1, 9) + + +v4l2_priority = enum +( + V4L2_PRIORITY_UNSET, + V4L2_PRIORITY_BACKGROUND, + V4L2_PRIORITY_INTERACTIVE, + V4L2_PRIORITY_RECORD, + V4L2_PRIORITY_DEFAULT, +) = list(range(0, 4)) + [2] + + +class v4l2_rect(ctypes.Structure): + _fields_ = [ + ('left', ctypes.c_int32), + ('top', ctypes.c_int32), + ('width', ctypes.c_int32), + ('height', ctypes.c_int32), + ] + + +class v4l2_fract(ctypes.Structure): + _fields_ = [ + ('numerator', ctypes.c_uint32), + ('denominator', ctypes.c_uint32), + ] + + +# +# Driver capabilities +# + +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), + ('reserved', ctypes.c_uint32 * 4), + ] + + +# +# Values for 'capabilities' field +# + +V4L2_CAP_VIDEO_CAPTURE = 0x00000001 +V4L2_CAP_VIDEO_OUTPUT = 0x00000002 +V4L2_CAP_VIDEO_OVERLAY = 0x00000004 +V4L2_CAP_VBI_CAPTURE = 0x00000010 +V4L2_CAP_VBI_OUTPUT = 0x00000020 +V4L2_CAP_SLICED_VBI_CAPTURE = 0x00000040 +V4L2_CAP_SLICED_VBI_OUTPUT = 0x00000080 +V4L2_CAP_RDS_CAPTURE = 0x00000100 +V4L2_CAP_VIDEO_OUTPUT_OVERLAY = 0x00000200 +V4L2_CAP_HW_FREQ_SEEK = 0x00000400 +V4L2_CAP_RDS_OUTPUT = 0x00000800 + +V4L2_CAP_TUNER = 0x00010000 +V4L2_CAP_AUDIO = 0x00020000 +V4L2_CAP_RADIO = 0x00040000 +V4L2_CAP_MODULATOR = 0x00080000 + +V4L2_CAP_READWRITE = 0x01000000 +V4L2_CAP_ASYNCIO = 0x02000000 +V4L2_CAP_STREAMING = 0x04000000 + + +# +# Video image format +# + +class v4l2_pix_format(ctypes.Structure): + _fields_ = [ + ('width', ctypes.c_uint32), + ('height', ctypes.c_uint32), + ('pixelformat', ctypes.c_uint32), + ('field', v4l2_field), + ('bytesperline', ctypes.c_uint32), + ('sizeimage', ctypes.c_uint32), + ('colorspace', v4l2_colorspace), + ('priv', ctypes.c_uint32), + ] + +# RGB formats +V4L2_PIX_FMT_RGB332 = v4l2_fourcc('R', 'G', 'B', '1') +V4L2_PIX_FMT_RGB444 = v4l2_fourcc('R', '4', '4', '4') +V4L2_PIX_FMT_RGB555 = v4l2_fourcc('R', 'G', 'B', 'O') +V4L2_PIX_FMT_RGB565 = v4l2_fourcc('R', 'G', 'B', 'P') +V4L2_PIX_FMT_RGB555X = v4l2_fourcc('R', 'G', 'B', 'Q') +V4L2_PIX_FMT_RGB565X = v4l2_fourcc('R', 'G', 'B', 'R') +V4L2_PIX_FMT_BGR24 = v4l2_fourcc('B', 'G', 'R', '3') +V4L2_PIX_FMT_RGB24 = v4l2_fourcc('R', 'G', 'B', '3') +V4L2_PIX_FMT_BGR32 = v4l2_fourcc('B', 'G', 'R', '4') +V4L2_PIX_FMT_RGB32 = v4l2_fourcc('R', 'G', 'B', '4') + +# Grey formats +V4L2_PIX_FMT_GREY = v4l2_fourcc('G', 'R', 'E', 'Y') +V4L2_PIX_FMT_Y10 = v4l2_fourcc('Y', '1', '0', ' ') +V4L2_PIX_FMT_Y16 = v4l2_fourcc('Y', '1', '6', ' ') + +# Palette formats +V4L2_PIX_FMT_PAL8 = v4l2_fourcc('P', 'A', 'L', '8') + +# Luminance+Chrominance formats +V4L2_PIX_FMT_YVU410 = v4l2_fourcc('Y', 'V', 'U', '9') +V4L2_PIX_FMT_YVU420 = v4l2_fourcc('Y', 'V', '1', '2') +V4L2_PIX_FMT_YUYV = v4l2_fourcc('Y', 'U', 'Y', 'V') +V4L2_PIX_FMT_YYUV = v4l2_fourcc('Y', 'Y', 'U', 'V') +V4L2_PIX_FMT_YVYU = v4l2_fourcc('Y', 'V', 'Y', 'U') +V4L2_PIX_FMT_UYVY = v4l2_fourcc('U', 'Y', 'V', 'Y') +V4L2_PIX_FMT_VYUY = v4l2_fourcc('V', 'Y', 'U', 'Y') +V4L2_PIX_FMT_YUV422P = v4l2_fourcc('4', '2', '2', 'P') +V4L2_PIX_FMT_YUV411P = v4l2_fourcc('4', '1', '1', 'P') +V4L2_PIX_FMT_Y41P = v4l2_fourcc('Y', '4', '1', 'P') +V4L2_PIX_FMT_YUV444 = v4l2_fourcc('Y', '4', '4', '4') +V4L2_PIX_FMT_YUV555 = v4l2_fourcc('Y', 'U', 'V', 'O') +V4L2_PIX_FMT_YUV565 = v4l2_fourcc('Y', 'U', 'V', 'P') +V4L2_PIX_FMT_YUV32 = v4l2_fourcc('Y', 'U', 'V', '4') +V4L2_PIX_FMT_YUV410 = v4l2_fourcc('Y', 'U', 'V', '9') +V4L2_PIX_FMT_YUV420 = v4l2_fourcc('Y', 'U', '1', '2') +V4L2_PIX_FMT_HI240 = v4l2_fourcc('H', 'I', '2', '4') +V4L2_PIX_FMT_HM12 = v4l2_fourcc('H', 'M', '1', '2') + +# two planes -- one Y, one Cr + Cb interleaved +V4L2_PIX_FMT_NV12 = v4l2_fourcc('N', 'V', '1', '2') +V4L2_PIX_FMT_NV21 = v4l2_fourcc('N', 'V', '2', '1') +V4L2_PIX_FMT_NV16 = v4l2_fourcc('N', 'V', '1', '6') +V4L2_PIX_FMT_NV61 = v4l2_fourcc('N', 'V', '6', '1') + +# Bayer formats - see http://www.siliconimaging.com/RGB%20Bayer.htm +V4L2_PIX_FMT_SBGGR8 = v4l2_fourcc('B', 'A', '8', '1') +V4L2_PIX_FMT_SGBRG8 = v4l2_fourcc('G', 'B', 'R', 'G') +V4L2_PIX_FMT_SGRBG8 = v4l2_fourcc('G', 'R', 'B', 'G') +V4L2_PIX_FMT_SRGGB8 = v4l2_fourcc('R', 'G', 'G', 'B') +V4L2_PIX_FMT_SBGGR10 = v4l2_fourcc('B', 'G', '1', '0') +V4L2_PIX_FMT_SGBRG10 = v4l2_fourcc('G', 'B', '1', '0') +V4L2_PIX_FMT_SGRBG10 = v4l2_fourcc('B', 'A', '1', '0') +V4L2_PIX_FMT_SRGGB10 = v4l2_fourcc('R', 'G', '1', '0') +V4L2_PIX_FMT_SGRBG10DPCM8 = v4l2_fourcc('B', 'D', '1', '0') +V4L2_PIX_FMT_SBGGR16 = v4l2_fourcc('B', 'Y', 'R', '2') + +# compressed formats +V4L2_PIX_FMT_MJPEG = v4l2_fourcc('M', 'J', 'P', 'G') +V4L2_PIX_FMT_JPEG = v4l2_fourcc('J', 'P', 'E', 'G') +V4L2_PIX_FMT_DV = v4l2_fourcc('d', 'v', 's', 'd') +V4L2_PIX_FMT_MPEG = v4l2_fourcc('M', 'P', 'E', 'G') + +# Vendor-specific formats +V4L2_PIX_FMT_CPIA1 = v4l2_fourcc('C', 'P', 'I', 'A') +V4L2_PIX_FMT_WNVA = v4l2_fourcc('W', 'N', 'V', 'A') +V4L2_PIX_FMT_SN9C10X = v4l2_fourcc('S', '9', '1', '0') +V4L2_PIX_FMT_SN9C20X_I420 = v4l2_fourcc('S', '9', '2', '0') +V4L2_PIX_FMT_PWC1 = v4l2_fourcc('P', 'W', 'C', '1') +V4L2_PIX_FMT_PWC2 = v4l2_fourcc('P', 'W', 'C', '2') +V4L2_PIX_FMT_ET61X251 = v4l2_fourcc('E', '6', '2', '5') +V4L2_PIX_FMT_SPCA501 = v4l2_fourcc('S', '5', '0', '1') +V4L2_PIX_FMT_SPCA505 = v4l2_fourcc('S', '5', '0', '5') +V4L2_PIX_FMT_SPCA508 = v4l2_fourcc('S', '5', '0', '8') +V4L2_PIX_FMT_SPCA561 = v4l2_fourcc('S', '5', '6', '1') +V4L2_PIX_FMT_PAC207 = v4l2_fourcc('P', '2', '0', '7') +V4L2_PIX_FMT_MR97310A = v4l2_fourcc('M', '3', '1', '0') +V4L2_PIX_FMT_SN9C2028 = v4l2_fourcc('S', 'O', 'N', 'X') +V4L2_PIX_FMT_SQ905C = v4l2_fourcc('9', '0', '5', 'C') +V4L2_PIX_FMT_PJPG = v4l2_fourcc('P', 'J', 'P', 'G') +V4L2_PIX_FMT_OV511 = v4l2_fourcc('O', '5', '1', '1') +V4L2_PIX_FMT_OV518 = v4l2_fourcc('O', '5', '1', '8') +V4L2_PIX_FMT_STV0680 = v4l2_fourcc('S', '6', '8', '0') + + +# +# Format enumeration +# + +class v4l2_fmtdesc(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('type', ctypes.c_int), + ('flags', ctypes.c_uint32), + ('description', ctypes.c_char * 32), + ('pixelformat', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + +V4L2_FMT_FLAG_COMPRESSED = 0x0001 +V4L2_FMT_FLAG_EMULATED = 0x0002 + + +# +# Experimental frame size and frame rate enumeration +# + +v4l2_frmsizetypes = enum +( + V4L2_FRMSIZE_TYPE_DISCRETE, + V4L2_FRMSIZE_TYPE_CONTINUOUS, + V4L2_FRMSIZE_TYPE_STEPWISE, +) = range(1, 4) + + +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), + ('min_height', 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 _u(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), + ('_u', _u), + ('reserved', ctypes.c_uint32 * 2) + ] + + _anonymous_ = ('_u',) + + +# +# Frame rate enumeration +# + +v4l2_frmivaltypes = enum +( + V4L2_FRMIVAL_TYPE_DISCRETE, + V4L2_FRMIVAL_TYPE_CONTINUOUS, + V4L2_FRMIVAL_TYPE_STEPWISE, +) = range(1, 4) + + +class v4l2_frmival_stepwise(ctypes.Structure): + _fields_ = [ + ('min', v4l2_fract), + ('max', v4l2_fract), + ('step', v4l2_fract), + ] + + +class v4l2_frmivalenum(ctypes.Structure): + class _u(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), + ('_u', _u), + ('reserved', ctypes.c_uint32 * 2), + ] + + _anonymous_ = ('_u',) + + +# +# Timecode +# + +class v4l2_timecode(ctypes.Structure): + _fields_ = [ + ('type', ctypes.c_uint32), + ('flags', ctypes.c_uint32), + ('frames', ctypes.c_uint8), + ('seconds', ctypes.c_uint8), + ('minutes', ctypes.c_uint8), + ('hours', ctypes.c_uint8), + ('userbits', ctypes.c_uint8 * 4), + ] + + +V4L2_TC_TYPE_24FPS = 1 +V4L2_TC_TYPE_25FPS = 2 +V4L2_TC_TYPE_30FPS = 3 +V4L2_TC_TYPE_50FPS = 4 +V4L2_TC_TYPE_60FPS = 5 + +V4L2_TC_FLAG_DROPFRAME = 0x0001 +V4L2_TC_FLAG_COLORFRAME = 0x0002 +V4L2_TC_USERBITS_field = 0x000C +V4L2_TC_USERBITS_USERDEFINED = 0x0000 +V4L2_TC_USERBITS_8BITCHARS = 0x0008 + + +class v4l2_jpegcompression(ctypes.Structure): + _fields_ = [ + ('quality', ctypes.c_int), + ('APPn', ctypes.c_int), + ('APP_len', ctypes.c_int), + ('APP_data', ctypes.c_char * 60), + ('COM_len', ctypes.c_int), + ('COM_data', ctypes.c_char * 60), + ('jpeg_markers', ctypes.c_uint32), + ] + + +V4L2_JPEG_MARKER_DHT = 1 << 3 +V4L2_JPEG_MARKER_DQT = 1 << 4 +V4L2_JPEG_MARKER_DRI = 1 << 5 +V4L2_JPEG_MARKER_COM = 1 << 6 +V4L2_JPEG_MARKER_APP = 1 << 7 + + +# +# Memory-mapping buffers +# + +class v4l2_requestbuffers(ctypes.Structure): + _fields_ = [ + ('count', ctypes.c_uint32), + ('type', v4l2_buf_type), + ('memory', v4l2_memory), + ('reserved', ctypes.c_uint32 * 2), + ] + + +class v4l2_buffer(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('offset', ctypes.c_uint32), + ('userptr', ctypes.c_ulong), + ] + + _fields_ = [ + ('index', ctypes.c_uint32), + ('type', v4l2_buf_type), + ('bytesused', ctypes.c_uint32), + ('flags', ctypes.c_uint32), + ('field', v4l2_field), + ('timestamp', timeval), + ('timecode', v4l2_timecode), + ('sequence', ctypes.c_uint32), + ('memory', v4l2_memory), + ('m', _u), + ('length', ctypes.c_uint32), + ('input', ctypes.c_uint32), + ('reserved', ctypes.c_uint32), + ] + + +V4L2_BUF_FLAG_MAPPED = 0x0001 +V4L2_BUF_FLAG_QUEUED = 0x0002 +V4L2_BUF_FLAG_DONE = 0x0004 +V4L2_BUF_FLAG_KEYFRAME = 0x0008 +V4L2_BUF_FLAG_PFRAME = 0x0010 +V4L2_BUF_FLAG_BFRAME = 0x0020 +V4L2_BUF_FLAG_TIMECODE = 0x0100 +V4L2_BUF_FLAG_INPUT = 0x0200 + + +# +# Overlay preview +# + +class v4l2_framebuffer(ctypes.Structure): + _fields_ = [ + ('capability', ctypes.c_uint32), + ('flags', ctypes.c_uint32), + ('base', ctypes.c_void_p), + ('fmt', v4l2_pix_format), + ] + +V4L2_FBUF_CAP_EXTERNOVERLAY = 0x0001 +V4L2_FBUF_CAP_CHROMAKEY = 0x0002 +V4L2_FBUF_CAP_LIST_CLIPPING = 0x0004 +V4L2_FBUF_CAP_BITMAP_CLIPPING = 0x0008 +V4L2_FBUF_CAP_LOCAL_ALPHA = 0x0010 +V4L2_FBUF_CAP_GLOBAL_ALPHA = 0x0020 +V4L2_FBUF_CAP_LOCAL_INV_ALPHA = 0x0040 +V4L2_FBUF_CAP_SRC_CHROMAKEY = 0x0080 + +V4L2_FBUF_FLAG_PRIMARY = 0x0001 +V4L2_FBUF_FLAG_OVERLAY = 0x0002 +V4L2_FBUF_FLAG_CHROMAKEY = 0x0004 +V4L2_FBUF_FLAG_LOCAL_ALPHA = 0x0008 +V4L2_FBUF_FLAG_GLOBAL_ALPHA = 0x0010 +V4L2_FBUF_FLAG_LOCAL_INV_ALPHA = 0x0020 +V4L2_FBUF_FLAG_SRC_CHROMAKEY = 0x0040 + + +class v4l2_clip(ctypes.Structure): + pass +v4l2_clip._fields_ = [ + ('c', v4l2_rect), + ('next', ctypes.POINTER(v4l2_clip)), +] + + +class v4l2_window(ctypes.Structure): + _fields_ = [ + ('w', v4l2_rect), + ('field', v4l2_field), + ('chromakey', ctypes.c_uint32), + ('clips', ctypes.POINTER(v4l2_clip)), + ('clipcount', ctypes.c_uint32), + ('bitmap', ctypes.c_void_p), + ('global_alpha', ctypes.c_uint8), + ] + + +# +# Capture parameters +# + +class v4l2_captureparm(ctypes.Structure): + _fields_ = [ + ('capability', ctypes.c_uint32), + ('capturemode', ctypes.c_uint32), + ('timeperframe', v4l2_fract), + ('extendedmode', ctypes.c_uint32), + ('readbuffers', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +V4L2_MODE_HIGHQUALITY = 0x0001 +V4L2_CAP_TIMEPERFRAME = 0x1000 + + +class v4l2_outputparm(ctypes.Structure): + _fields_ = [ + ('capability', ctypes.c_uint32), + ('outputmode', ctypes.c_uint32), + ('timeperframe', v4l2_fract), + ('extendedmode', ctypes.c_uint32), + ('writebuffers', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +# +# Input image cropping +# + +class v4l2_cropcap(ctypes.Structure): + _fields_ = [ + ('type', v4l2_buf_type), + ('bounds', v4l2_rect), + ('defrect', v4l2_rect), + ('pixelaspect', v4l2_fract), + ] + + +class v4l2_crop(ctypes.Structure): + _fields_ = [ + ('type', ctypes.c_int), + ('c', v4l2_rect), + ] + + +# +# Analog video standard +# + +v4l2_std_id = ctypes.c_uint64 + + +V4L2_STD_PAL_B = 0x00000001 +V4L2_STD_PAL_B1 = 0x00000002 +V4L2_STD_PAL_G = 0x00000004 +V4L2_STD_PAL_H = 0x00000008 +V4L2_STD_PAL_I = 0x00000010 +V4L2_STD_PAL_D = 0x00000020 +V4L2_STD_PAL_D1 = 0x00000040 +V4L2_STD_PAL_K = 0x00000080 + +V4L2_STD_PAL_M = 0x00000100 +V4L2_STD_PAL_N = 0x00000200 +V4L2_STD_PAL_Nc = 0x00000400 +V4L2_STD_PAL_60 = 0x00000800 + +V4L2_STD_NTSC_M = 0x00001000 +V4L2_STD_NTSC_M_JP = 0x00002000 +V4L2_STD_NTSC_443 = 0x00004000 +V4L2_STD_NTSC_M_KR = 0x00008000 + +V4L2_STD_SECAM_B = 0x00010000 +V4L2_STD_SECAM_D = 0x00020000 +V4L2_STD_SECAM_G = 0x00040000 +V4L2_STD_SECAM_H = 0x00080000 +V4L2_STD_SECAM_K = 0x00100000 +V4L2_STD_SECAM_K1 = 0x00200000 +V4L2_STD_SECAM_L = 0x00400000 +V4L2_STD_SECAM_LC = 0x00800000 + +V4L2_STD_ATSC_8_VSB = 0x01000000 +V4L2_STD_ATSC_16_VSB = 0x02000000 + + +# some common needed stuff +V4L2_STD_PAL_BG = (V4L2_STD_PAL_B | V4L2_STD_PAL_B1 | V4L2_STD_PAL_G) +V4L2_STD_PAL_DK = (V4L2_STD_PAL_D | V4L2_STD_PAL_D1 | V4L2_STD_PAL_K) +V4L2_STD_PAL = (V4L2_STD_PAL_BG | V4L2_STD_PAL_DK | V4L2_STD_PAL_H | V4L2_STD_PAL_I) +V4L2_STD_NTSC = (V4L2_STD_NTSC_M | V4L2_STD_NTSC_M_JP | V4L2_STD_NTSC_M_KR) +V4L2_STD_SECAM_DK = (V4L2_STD_SECAM_D | V4L2_STD_SECAM_K | V4L2_STD_SECAM_K1) +V4L2_STD_SECAM = (V4L2_STD_SECAM_B | V4L2_STD_SECAM_G | V4L2_STD_SECAM_H | V4L2_STD_SECAM_DK | V4L2_STD_SECAM_L | V4L2_STD_SECAM_LC) + +V4L2_STD_525_60 = (V4L2_STD_PAL_M | V4L2_STD_PAL_60 | V4L2_STD_NTSC | V4L2_STD_NTSC_443) +V4L2_STD_625_50 = (V4L2_STD_PAL | V4L2_STD_PAL_N | V4L2_STD_PAL_Nc | V4L2_STD_SECAM) +V4L2_STD_ATSC = (V4L2_STD_ATSC_8_VSB | V4L2_STD_ATSC_16_VSB) + +V4L2_STD_UNKNOWN = 0 +V4L2_STD_ALL = (V4L2_STD_525_60 | V4L2_STD_625_50) + +# some merged standards +V4L2_STD_MN = (V4L2_STD_PAL_M | V4L2_STD_PAL_N | V4L2_STD_PAL_Nc | V4L2_STD_NTSC) +V4L2_STD_B = (V4L2_STD_PAL_B | V4L2_STD_PAL_B1 | V4L2_STD_SECAM_B) +V4L2_STD_GH = (V4L2_STD_PAL_G | V4L2_STD_PAL_H|V4L2_STD_SECAM_G | V4L2_STD_SECAM_H) +V4L2_STD_DK = (V4L2_STD_PAL_DK | V4L2_STD_SECAM_DK) + + +class v4l2_standard(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('id', v4l2_std_id), + ('name', ctypes.c_char * 24), + ('frameperiod', v4l2_fract), + ('framelines', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +# +# Video timings dv preset +# + +class v4l2_dv_preset(ctypes.Structure): + _fields_ = [ + ('preset', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4) + ] + + +# +# DV preset enumeration +# + +class v4l2_dv_enum_preset(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('preset', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('width', ctypes.c_uint32), + ('height', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + +# +# DV preset values +# + +V4L2_DV_INVALID = 0 +V4L2_DV_480P59_94 = 1 +V4L2_DV_576P50 = 2 +V4L2_DV_720P24 = 3 +V4L2_DV_720P25 = 4 +V4L2_DV_720P30 = 5 +V4L2_DV_720P50 = 6 +V4L2_DV_720P59_94 = 7 +V4L2_DV_720P60 = 8 +V4L2_DV_1080I29_97 = 9 +V4L2_DV_1080I30 = 10 +V4L2_DV_1080I25 = 11 +V4L2_DV_1080I50 = 12 +V4L2_DV_1080I60 = 13 +V4L2_DV_1080P24 = 14 +V4L2_DV_1080P25 = 15 +V4L2_DV_1080P30 = 16 +V4L2_DV_1080P50 = 17 +V4L2_DV_1080P60 = 18 + + +# +# DV BT timings +# + +class v4l2_bt_timings(ctypes.Structure): + _fields_ = [ + ('width', ctypes.c_uint32), + ('height', ctypes.c_uint32), + ('interlaced', ctypes.c_uint32), + ('polarities', ctypes.c_uint32), + ('pixelclock', ctypes.c_uint64), + ('hfrontporch', ctypes.c_uint32), + ('hsync', ctypes.c_uint32), + ('hbackporch', ctypes.c_uint32), + ('vfrontporch', ctypes.c_uint32), + ('vsync', ctypes.c_uint32), + ('vbackporch', ctypes.c_uint32), + ('il_vfrontporch', ctypes.c_uint32), + ('il_vsync', ctypes.c_uint32), + ('il_vbackporch', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 16), + ] + + _pack_ = True + +# Interlaced or progressive format +V4L2_DV_PROGRESSIVE = 0 +V4L2_DV_INTERLACED = 1 + +# Polarities. If bit is not set, it is assumed to be negative polarity +V4L2_DV_VSYNC_POS_POL = 0x00000001 +V4L2_DV_HSYNC_POS_POL = 0x00000002 + + +class v4l2_dv_timings(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('bt', v4l2_bt_timings), + ('reserved', ctypes.c_uint32 * 32), + ] + + _fields_ = [ + ('type', ctypes.c_uint32), + ('_u', _u), + ] + + _anonymous_ = ('_u',) + _pack_ = True + + +# Values for the type field +V4L2_DV_BT_656_1120 = 0 + + +# +# Video inputs +# + +class v4l2_input(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('type', ctypes.c_uint32), + ('audioset', ctypes.c_uint32), + ('tuner', ctypes.c_uint32), + ('std', v4l2_std_id), + ('status', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +V4L2_INPUT_TYPE_TUNER = 1 +V4L2_INPUT_TYPE_CAMERA = 2 + +V4L2_IN_ST_NO_POWER = 0x00000001 +V4L2_IN_ST_NO_SIGNAL = 0x00000002 +V4L2_IN_ST_NO_COLOR = 0x00000004 + +V4L2_IN_ST_HFLIP = 0x00000010 +V4L2_IN_ST_VFLIP = 0x00000020 + +V4L2_IN_ST_NO_H_LOCK = 0x00000100 +V4L2_IN_ST_COLOR_KILL = 0x00000200 + +V4L2_IN_ST_NO_SYNC = 0x00010000 +V4L2_IN_ST_NO_EQU = 0x00020000 +V4L2_IN_ST_NO_CARRIER = 0x00040000 + +V4L2_IN_ST_MACROVISION = 0x01000000 +V4L2_IN_ST_NO_ACCESS = 0x02000000 +V4L2_IN_ST_VTR = 0x04000000 + +V4L2_IN_CAP_PRESETS = 0x00000001 +V4L2_IN_CAP_CUSTOM_TIMINGS = 0x00000002 +V4L2_IN_CAP_STD = 0x00000004 + +# +# Video outputs +# + +class v4l2_output(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('type', ctypes.c_uint32), + ('audioset', ctypes.c_uint32), + ('modulator', ctypes.c_uint32), + ('std', v4l2_std_id), + ('reserved', ctypes.c_uint32 * 4), + ] + + +V4L2_OUTPUT_TYPE_MODULATOR = 1 +V4L2_OUTPUT_TYPE_ANALOG = 2 +V4L2_OUTPUT_TYPE_ANALOGVGAOVERLAY = 3 + +V4L2_OUT_CAP_PRESETS = 0x00000001 +V4L2_OUT_CAP_CUSTOM_TIMINGS = 0x00000002 +V4L2_OUT_CAP_STD = 0x00000004 + +# +# Controls +# + +class v4l2_control(ctypes.Structure): + _fields_ = [ + ('id', ctypes.c_uint32), + ('value', ctypes.c_int32), + ] + + +class v4l2_ext_control(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('value', ctypes.c_int32), + ('value64', ctypes.c_int64), + ('reserved', ctypes.c_void_p), + ] + + _fields_ = [ + ('id', ctypes.c_uint32), + ('reserved2', ctypes.c_uint32 * 2), + ('_u', _u) + ] + + _anonymous_ = ('_u',) + _pack_ = True + + +class v4l2_ext_controls(ctypes.Structure): + _fields_ = [ + ('ctrl_class', ctypes.c_uint32), + ('count', ctypes.c_uint32), + ('error_idx', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ('controls', ctypes.POINTER(v4l2_ext_control)), + ] + + +V4L2_CTRL_CLASS_USER = 0x00980000 +V4L2_CTRL_CLASS_MPEG = 0x00990000 +V4L2_CTRL_CLASS_CAMERA = 0x009a0000 +V4L2_CTRL_CLASS_FM_TX = 0x009b0000 + + +def V4L2_CTRL_ID_MASK(): + return 0x0fffffff + + +def V4L2_CTRL_ID2CLASS(id_): + return id_ & 0x0fff0000 # unsigned long + + +def V4L2_CTRL_DRIVER_PRIV(id_): + return (id_ & 0xffff) >= 0x1000 + + +class v4l2_queryctrl(ctypes.Structure): + _fields_ = [ + ('id', ctypes.c_uint32), + ('type', v4l2_ctrl_type), + ('name', ctypes.c_char * 32), + ('minimum', ctypes.c_int32), + ('maximum', ctypes.c_int32), + ('step', ctypes.c_int32), + ('default', ctypes.c_int32), + ('flags', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ] + + +class v4l2_querymenu(ctypes.Structure): + _fields_ = [ + ('id', ctypes.c_uint32), + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('reserved', ctypes.c_uint32), + ] + + +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_NEXT_CTRL = 0x80000000 + +V4L2_CID_BASE = V4L2_CTRL_CLASS_USER | 0x900 +V4L2_CID_USER_BASE = V4L2_CID_BASE +V4L2_CID_PRIVATE_BASE = 0x08000000 + +V4L2_CID_USER_CLASS = V4L2_CTRL_CLASS_USER | 1 +V4L2_CID_BRIGHTNESS = V4L2_CID_BASE + 0 +V4L2_CID_CONTRAST = V4L2_CID_BASE + 1 +V4L2_CID_SATURATION = V4L2_CID_BASE + 2 +V4L2_CID_HUE = V4L2_CID_BASE + 3 +V4L2_CID_AUDIO_VOLUME = V4L2_CID_BASE + 5 +V4L2_CID_AUDIO_BALANCE = V4L2_CID_BASE + 6 +V4L2_CID_AUDIO_BASS = V4L2_CID_BASE + 7 +V4L2_CID_AUDIO_TREBLE = V4L2_CID_BASE + 8 +V4L2_CID_AUDIO_MUTE = V4L2_CID_BASE + 9 +V4L2_CID_AUDIO_LOUDNESS = V4L2_CID_BASE + 10 +V4L2_CID_BLACK_LEVEL = V4L2_CID_BASE + 11 # Deprecated +V4L2_CID_AUTO_WHITE_BALANCE = V4L2_CID_BASE + 12 +V4L2_CID_DO_WHITE_BALANCE = V4L2_CID_BASE + 13 +V4L2_CID_RED_BALANCE = V4L2_CID_BASE + 14 +V4L2_CID_BLUE_BALANCE = V4L2_CID_BASE + 15 +V4L2_CID_GAMMA = V4L2_CID_BASE + 16 +V4L2_CID_WHITENESS = V4L2_CID_GAMMA # Deprecated +V4L2_CID_EXPOSURE = V4L2_CID_BASE + 17 +V4L2_CID_AUTOGAIN = V4L2_CID_BASE + 18 +V4L2_CID_GAIN = V4L2_CID_BASE + 19 +V4L2_CID_HFLIP = V4L2_CID_BASE + 20 +V4L2_CID_VFLIP = V4L2_CID_BASE + 21 + +# Deprecated; use V4L2_CID_PAN_RESET and V4L2_CID_TILT_RESET +V4L2_CID_HCENTER = V4L2_CID_BASE + 22 +V4L2_CID_VCENTER = V4L2_CID_BASE + 23 + +V4L2_CID_POWER_LINE_FREQUENCY = V4L2_CID_BASE + 24 + +v4l2_power_line_frequency = enum +( + V4L2_CID_POWER_LINE_FREQUENCY_DISABLED, + V4L2_CID_POWER_LINE_FREQUENCY_50HZ, + V4L2_CID_POWER_LINE_FREQUENCY_60HZ, +) = range(3) + +V4L2_CID_HUE_AUTO = V4L2_CID_BASE + 25 +V4L2_CID_WHITE_BALANCE_TEMPERATURE = V4L2_CID_BASE + 26 +V4L2_CID_SHARPNESS = V4L2_CID_BASE + 27 +V4L2_CID_BACKLIGHT_COMPENSATION = V4L2_CID_BASE + 28 +V4L2_CID_CHROMA_AGC = V4L2_CID_BASE + 29 +V4L2_CID_COLOR_KILLER = V4L2_CID_BASE + 30 +V4L2_CID_COLORFX = V4L2_CID_BASE + 31 + +v4l2_colorfx = enum +( + V4L2_COLORFX_NONE, + V4L2_COLORFX_BW, + V4L2_COLORFX_SEPIA, +) = range(3) + +V4L2_CID_AUTOBRIGHTNESS = V4L2_CID_BASE + 32 +V4L2_CID_BAND_STOP_FILTER = V4L2_CID_BASE + 33 + +V4L2_CID_ROTATE = V4L2_CID_BASE + 34 +V4L2_CID_BG_COLOR = V4L2_CID_BASE + 35 +V4L2_CID_LASTP1 = V4L2_CID_BASE + 36 + +V4L2_CID_MPEG_BASE = V4L2_CTRL_CLASS_MPEG | 0x900 +V4L2_CID_MPEG_CLASS = V4L2_CTRL_CLASS_MPEG | 1 + +# MPEG streams +V4L2_CID_MPEG_STREAM_TYPE = V4L2_CID_MPEG_BASE + 0 + +v4l2_mpeg_stream_type = enum +( + V4L2_MPEG_STREAM_TYPE_MPEG2_PS, + V4L2_MPEG_STREAM_TYPE_MPEG2_TS, + V4L2_MPEG_STREAM_TYPE_MPEG1_SS, + V4L2_MPEG_STREAM_TYPE_MPEG2_DVD, + V4L2_MPEG_STREAM_TYPE_MPEG1_VCD, + V4L2_MPEG_STREAM_TYPE_MPEG2_SVCD, +) = range(6) + +V4L2_CID_MPEG_STREAM_PID_PMT = V4L2_CID_MPEG_BASE + 1 +V4L2_CID_MPEG_STREAM_PID_AUDIO = V4L2_CID_MPEG_BASE + 2 +V4L2_CID_MPEG_STREAM_PID_VIDEO = V4L2_CID_MPEG_BASE + 3 +V4L2_CID_MPEG_STREAM_PID_PCR = V4L2_CID_MPEG_BASE + 4 +V4L2_CID_MPEG_STREAM_PES_ID_AUDIO = V4L2_CID_MPEG_BASE + 5 +V4L2_CID_MPEG_STREAM_PES_ID_VIDEO = V4L2_CID_MPEG_BASE + 6 +V4L2_CID_MPEG_STREAM_VBI_FMT = V4L2_CID_MPEG_BASE + 7 + +v4l2_mpeg_stream_vbi_fmt = enum +( + V4L2_MPEG_STREAM_VBI_FMT_NONE, + V4L2_MPEG_STREAM_VBI_FMT_IVTV, +) = range(2) + +V4L2_CID_MPEG_AUDIO_SAMPLING_FREQ = V4L2_CID_MPEG_BASE + 100 + +v4l2_mpeg_audio_sampling_freq = enum +( + V4L2_MPEG_AUDIO_SAMPLING_FREQ_44100, + V4L2_MPEG_AUDIO_SAMPLING_FREQ_48000, + V4L2_MPEG_AUDIO_SAMPLING_FREQ_32000, +) = range(3) + +V4L2_CID_MPEG_AUDIO_ENCODING = V4L2_CID_MPEG_BASE + 101 + +v4l2_mpeg_audio_encoding = enum +( + V4L2_MPEG_AUDIO_ENCODING_LAYER_1, + V4L2_MPEG_AUDIO_ENCODING_LAYER_2, + V4L2_MPEG_AUDIO_ENCODING_LAYER_3, + V4L2_MPEG_AUDIO_ENCODING_AAC, + V4L2_MPEG_AUDIO_ENCODING_AC3, +) = range(5) + +V4L2_CID_MPEG_AUDIO_L1_BITRATE = V4L2_CID_MPEG_BASE + 102 + +v4l2_mpeg_audio_l1_bitrate = enum +( + V4L2_MPEG_AUDIO_L1_BITRATE_32K, + V4L2_MPEG_AUDIO_L1_BITRATE_64K, + V4L2_MPEG_AUDIO_L1_BITRATE_96K, + V4L2_MPEG_AUDIO_L1_BITRATE_128K, + V4L2_MPEG_AUDIO_L1_BITRATE_160K, + V4L2_MPEG_AUDIO_L1_BITRATE_192K, + V4L2_MPEG_AUDIO_L1_BITRATE_224K, + V4L2_MPEG_AUDIO_L1_BITRATE_256K, + V4L2_MPEG_AUDIO_L1_BITRATE_288K, + V4L2_MPEG_AUDIO_L1_BITRATE_320K, + V4L2_MPEG_AUDIO_L1_BITRATE_352K, + V4L2_MPEG_AUDIO_L1_BITRATE_384K, + V4L2_MPEG_AUDIO_L1_BITRATE_416K, + V4L2_MPEG_AUDIO_L1_BITRATE_448K, +) = range(14) + +V4L2_CID_MPEG_AUDIO_L2_BITRATE = V4L2_CID_MPEG_BASE + 103 + +v4l2_mpeg_audio_l2_bitrate = enum +( + V4L2_MPEG_AUDIO_L2_BITRATE_32K, + V4L2_MPEG_AUDIO_L2_BITRATE_48K, + V4L2_MPEG_AUDIO_L2_BITRATE_56K, + V4L2_MPEG_AUDIO_L2_BITRATE_64K, + V4L2_MPEG_AUDIO_L2_BITRATE_80K, + V4L2_MPEG_AUDIO_L2_BITRATE_96K, + V4L2_MPEG_AUDIO_L2_BITRATE_112K, + V4L2_MPEG_AUDIO_L2_BITRATE_128K, + V4L2_MPEG_AUDIO_L2_BITRATE_160K, + V4L2_MPEG_AUDIO_L2_BITRATE_192K, + V4L2_MPEG_AUDIO_L2_BITRATE_224K, + V4L2_MPEG_AUDIO_L2_BITRATE_256K, + V4L2_MPEG_AUDIO_L2_BITRATE_320K, + V4L2_MPEG_AUDIO_L2_BITRATE_384K, +) = range(14) + +V4L2_CID_MPEG_AUDIO_L3_BITRATE = V4L2_CID_MPEG_BASE + 104 + +v4l2_mpeg_audio_l3_bitrate = enum +( + V4L2_MPEG_AUDIO_L3_BITRATE_32K, + V4L2_MPEG_AUDIO_L3_BITRATE_40K, + V4L2_MPEG_AUDIO_L3_BITRATE_48K, + V4L2_MPEG_AUDIO_L3_BITRATE_56K, + V4L2_MPEG_AUDIO_L3_BITRATE_64K, + V4L2_MPEG_AUDIO_L3_BITRATE_80K, + V4L2_MPEG_AUDIO_L3_BITRATE_96K, + V4L2_MPEG_AUDIO_L3_BITRATE_112K, + V4L2_MPEG_AUDIO_L3_BITRATE_128K, + V4L2_MPEG_AUDIO_L3_BITRATE_160K, + V4L2_MPEG_AUDIO_L3_BITRATE_192K, + V4L2_MPEG_AUDIO_L3_BITRATE_224K, + V4L2_MPEG_AUDIO_L3_BITRATE_256K, + V4L2_MPEG_AUDIO_L3_BITRATE_320K, +) = range(14) + +V4L2_CID_MPEG_AUDIO_MODE = V4L2_CID_MPEG_BASE + 105 + +v4l2_mpeg_audio_mode = enum +( + V4L2_MPEG_AUDIO_MODE_STEREO, + V4L2_MPEG_AUDIO_MODE_JOINT_STEREO, + V4L2_MPEG_AUDIO_MODE_DUAL, + V4L2_MPEG_AUDIO_MODE_MONO, +) = range(4) + +V4L2_CID_MPEG_AUDIO_MODE_EXTENSION = V4L2_CID_MPEG_BASE + 106 + +v4l2_mpeg_audio_mode_extension = enum +( + V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_4, + V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_8, + V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_12, + V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_16, +) = range(4) + +V4L2_CID_MPEG_AUDIO_EMPHASIS = V4L2_CID_MPEG_BASE + 107 + +v4l2_mpeg_audio_emphasis = enum +( + V4L2_MPEG_AUDIO_EMPHASIS_NONE, + V4L2_MPEG_AUDIO_EMPHASIS_50_DIV_15_uS, + V4L2_MPEG_AUDIO_EMPHASIS_CCITT_J17, +) = range(3) + +V4L2_CID_MPEG_AUDIO_CRC = V4L2_CID_MPEG_BASE + 108 + +v4l2_mpeg_audio_crc = enum +( + V4L2_MPEG_AUDIO_CRC_NONE, + V4L2_MPEG_AUDIO_CRC_CRC16, +) = range(2) + +V4L2_CID_MPEG_AUDIO_MUTE = V4L2_CID_MPEG_BASE + 109 +V4L2_CID_MPEG_AUDIO_AAC_BITRATE = V4L2_CID_MPEG_BASE + 110 +V4L2_CID_MPEG_AUDIO_AC3_BITRATE = V4L2_CID_MPEG_BASE + 111 + +v4l2_mpeg_audio_ac3_bitrate = enum +( + V4L2_MPEG_AUDIO_AC3_BITRATE_32K, + V4L2_MPEG_AUDIO_AC3_BITRATE_40K, + V4L2_MPEG_AUDIO_AC3_BITRATE_48K, + V4L2_MPEG_AUDIO_AC3_BITRATE_56K, + V4L2_MPEG_AUDIO_AC3_BITRATE_64K, + V4L2_MPEG_AUDIO_AC3_BITRATE_80K, + V4L2_MPEG_AUDIO_AC3_BITRATE_96K, + V4L2_MPEG_AUDIO_AC3_BITRATE_112K, + V4L2_MPEG_AUDIO_AC3_BITRATE_128K, + V4L2_MPEG_AUDIO_AC3_BITRATE_160K, + V4L2_MPEG_AUDIO_AC3_BITRATE_192K, + V4L2_MPEG_AUDIO_AC3_BITRATE_224K, + V4L2_MPEG_AUDIO_AC3_BITRATE_256K, + V4L2_MPEG_AUDIO_AC3_BITRATE_320K, + V4L2_MPEG_AUDIO_AC3_BITRATE_384K, + V4L2_MPEG_AUDIO_AC3_BITRATE_448K, + V4L2_MPEG_AUDIO_AC3_BITRATE_512K, + V4L2_MPEG_AUDIO_AC3_BITRATE_576K, + V4L2_MPEG_AUDIO_AC3_BITRATE_640K, +) = range(19) + +V4L2_CID_MPEG_VIDEO_ENCODING = V4L2_CID_MPEG_BASE + 200 + +v4l2_mpeg_video_encoding = enum +( + V4L2_MPEG_VIDEO_ENCODING_MPEG_1, + V4L2_MPEG_VIDEO_ENCODING_MPEG_2, + V4L2_MPEG_VIDEO_ENCODING_MPEG_4_AVC, +) = range(3) + +V4L2_CID_MPEG_VIDEO_ASPECT = V4L2_CID_MPEG_BASE + 201 + +v4l2_mpeg_video_aspect = enum +( + V4L2_MPEG_VIDEO_ASPECT_1x1, + V4L2_MPEG_VIDEO_ASPECT_4x3, + V4L2_MPEG_VIDEO_ASPECT_16x9, + V4L2_MPEG_VIDEO_ASPECT_221x100, +) = range(4) + +V4L2_CID_MPEG_VIDEO_B_FRAMES = V4L2_CID_MPEG_BASE + 202 +V4L2_CID_MPEG_VIDEO_GOP_SIZE = V4L2_CID_MPEG_BASE + 203 +V4L2_CID_MPEG_VIDEO_GOP_CLOSURE = V4L2_CID_MPEG_BASE + 204 +V4L2_CID_MPEG_VIDEO_PULLDOWN = V4L2_CID_MPEG_BASE + 205 +V4L2_CID_MPEG_VIDEO_BITRATE_MODE = V4L2_CID_MPEG_BASE + 206 + +v4l2_mpeg_video_bitrate_mode = enum +( + V4L2_MPEG_VIDEO_BITRATE_MODE_VBR, + V4L2_MPEG_VIDEO_BITRATE_MODE_CBR, +) = range(2) + +V4L2_CID_MPEG_VIDEO_BITRATE = V4L2_CID_MPEG_BASE + 207 +V4L2_CID_MPEG_VIDEO_BITRATE_PEAK = V4L2_CID_MPEG_BASE + 208 +V4L2_CID_MPEG_VIDEO_TEMPORAL_DECIMATION = V4L2_CID_MPEG_BASE + 209 +V4L2_CID_MPEG_VIDEO_MUTE = V4L2_CID_MPEG_BASE + 210 +V4L2_CID_MPEG_VIDEO_MUTE_YUV = V4L2_CID_MPEG_BASE + 211 + +V4L2_CID_MPEG_CX2341X_BASE = V4L2_CTRL_CLASS_MPEG | 0x1000 +V4L2_CID_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE = V4L2_CID_MPEG_CX2341X_BASE + 0 + +v4l2_mpeg_cx2341x_video_spatial_filter_mode = enum +( + V4L2_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE_MANUAL, + V4L2_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE_AUTO, +) = range(2) + +V4L2_CID_MPEG_CX2341X_VIDEO_SPATIAL_FILTER = V4L2_CID_MPEG_CX2341X_BASE + 1 +V4L2_CID_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 2 + +v4l2_mpeg_cx2341x_video_luma_spatial_filter_type = enum +( + V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_OFF, + V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_1D_HOR, + V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_1D_VERT, + V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_2D_HV_SEPARABLE, + V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_2D_SYM_NON_SEPARABLE, +) = range(5) + +V4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 3 + +v4l2_mpeg_cx2341x_video_chroma_spatial_filter_type = enum +( + V4L2_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE_OFF, + V4L2_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE_1D_HOR, +) = range(2) + +V4L2_CID_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE = V4L2_CID_MPEG_CX2341X_BASE + 4 + +v4l2_mpeg_cx2341x_video_temporal_filter_mode = enum +( + V4L2_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE_MANUAL, + V4L2_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE_AUTO, +) = range(2) + +V4L2_CID_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER = V4L2_CID_MPEG_CX2341X_BASE + 5 +V4L2_CID_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 6 + +v4l2_mpeg_cx2341x_video_median_filter_type = enum +( + V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_OFF, + V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_HOR, + V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_VERT, + V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_HOR_VERT, + V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_DIAG, +) = range(5) + +V4L2_CID_MPEG_CX2341X_VIDEO_LUMA_MEDIAN_FILTER_BOTTOM = V4L2_CID_MPEG_CX2341X_BASE + 7 +V4L2_CID_MPEG_CX2341X_VIDEO_LUMA_MEDIAN_FILTER_TOP = V4L2_CID_MPEG_CX2341X_BASE + 8 +V4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_MEDIAN_FILTER_BOTTOM = V4L2_CID_MPEG_CX2341X_BASE + 9 +V4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_MEDIAN_FILTER_TOP = V4L2_CID_MPEG_CX2341X_BASE + 10 +V4L2_CID_MPEG_CX2341X_STREAM_INSERT_NAV_PACKETS = V4L2_CID_MPEG_CX2341X_BASE + 11 + +V4L2_CID_CAMERA_CLASS_BASE = V4L2_CTRL_CLASS_CAMERA | 0x900 +V4L2_CID_CAMERA_CLASS = V4L2_CTRL_CLASS_CAMERA | 1 + +V4L2_CID_EXPOSURE_AUTO = V4L2_CID_CAMERA_CLASS_BASE + 1 + +v4l2_exposure_auto_type = enum +( + V4L2_EXPOSURE_AUTO, + V4L2_EXPOSURE_MANUAL, + V4L2_EXPOSURE_SHUTTER_PRIORITY, + V4L2_EXPOSURE_APERTURE_PRIORITY, +) = range(4) + +V4L2_CID_EXPOSURE_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 2 +V4L2_CID_EXPOSURE_AUTO_PRIORITY = V4L2_CID_CAMERA_CLASS_BASE + 3 + +V4L2_CID_PAN_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 4 +V4L2_CID_TILT_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 5 +V4L2_CID_PAN_RESET = V4L2_CID_CAMERA_CLASS_BASE + 6 +V4L2_CID_TILT_RESET = V4L2_CID_CAMERA_CLASS_BASE + 7 + +V4L2_CID_PAN_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 8 +V4L2_CID_TILT_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 9 + +V4L2_CID_FOCUS_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 10 +V4L2_CID_FOCUS_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 11 +V4L2_CID_FOCUS_AUTO = V4L2_CID_CAMERA_CLASS_BASE + 12 + +V4L2_CID_ZOOM_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 13 +V4L2_CID_ZOOM_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 14 +V4L2_CID_ZOOM_CONTINUOUS = V4L2_CID_CAMERA_CLASS_BASE + 15 + +V4L2_CID_PRIVACY = V4L2_CID_CAMERA_CLASS_BASE + 16 + +V4L2_CID_FM_TX_CLASS_BASE = V4L2_CTRL_CLASS_FM_TX | 0x900 +V4L2_CID_FM_TX_CLASS = V4L2_CTRL_CLASS_FM_TX | 1 + +V4L2_CID_RDS_TX_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 1 +V4L2_CID_RDS_TX_PI = V4L2_CID_FM_TX_CLASS_BASE + 2 +V4L2_CID_RDS_TX_PTY = V4L2_CID_FM_TX_CLASS_BASE + 3 +V4L2_CID_RDS_TX_PS_NAME = V4L2_CID_FM_TX_CLASS_BASE + 5 +V4L2_CID_RDS_TX_RADIO_TEXT = V4L2_CID_FM_TX_CLASS_BASE + 6 + +V4L2_CID_AUDIO_LIMITER_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 64 +V4L2_CID_AUDIO_LIMITER_RELEASE_TIME = V4L2_CID_FM_TX_CLASS_BASE + 65 +V4L2_CID_AUDIO_LIMITER_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 66 + +V4L2_CID_AUDIO_COMPRESSION_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 80 +V4L2_CID_AUDIO_COMPRESSION_GAIN = V4L2_CID_FM_TX_CLASS_BASE + 81 +V4L2_CID_AUDIO_COMPRESSION_THRESHOLD = V4L2_CID_FM_TX_CLASS_BASE + 82 +V4L2_CID_AUDIO_COMPRESSION_ATTACK_TIME = V4L2_CID_FM_TX_CLASS_BASE + 83 +V4L2_CID_AUDIO_COMPRESSION_RELEASE_TIME = V4L2_CID_FM_TX_CLASS_BASE + 84 + +V4L2_CID_PILOT_TONE_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 96 +V4L2_CID_PILOT_TONE_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 97 +V4L2_CID_PILOT_TONE_FREQUENCY = V4L2_CID_FM_TX_CLASS_BASE + 98 + +V4L2_CID_TUNE_PREEMPHASIS = V4L2_CID_FM_TX_CLASS_BASE + 112 + +v4l2_preemphasis = enum +( + V4L2_PREEMPHASIS_DISABLED, + V4L2_PREEMPHASIS_50_uS, + V4L2_PREEMPHASIS_75_uS, +) = range(3) + +V4L2_CID_TUNE_POWER_LEVEL = V4L2_CID_FM_TX_CLASS_BASE + 113 +V4L2_CID_TUNE_ANTENNA_CAPACITOR = V4L2_CID_FM_TX_CLASS_BASE + 114 + + +# +# Tuning +# + +class v4l2_tuner(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('type', v4l2_tuner_type), + ('capability', ctypes.c_uint32), + ('rangelow', ctypes.c_uint32), + ('rangehigh', ctypes.c_uint32), + ('rxsubchans', ctypes.c_uint32), + ('audmode', ctypes.c_uint32), + ('signal', ctypes.c_int32), + ('afc', ctypes.c_int32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +class v4l2_modulator(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('capability', ctypes.c_uint32), + ('rangelow', ctypes.c_uint32), + ('rangehigh', ctypes.c_uint32), + ('txsubchans', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +V4L2_TUNER_CAP_LOW = 0x0001 +V4L2_TUNER_CAP_NORM = 0x0002 +V4L2_TUNER_CAP_STEREO = 0x0010 +V4L2_TUNER_CAP_LANG2 = 0x0020 +V4L2_TUNER_CAP_SAP = 0x0020 +V4L2_TUNER_CAP_LANG1 = 0x0040 +V4L2_TUNER_CAP_RDS = 0x0080 + +V4L2_TUNER_SUB_MONO = 0x0001 +V4L2_TUNER_SUB_STEREO = 0x0002 +V4L2_TUNER_SUB_LANG2 = 0x0004 +V4L2_TUNER_SUB_SAP = 0x0004 +V4L2_TUNER_SUB_LANG1 = 0x0008 +V4L2_TUNER_SUB_RDS = 0x0010 + +V4L2_TUNER_MODE_MONO = 0x0000 +V4L2_TUNER_MODE_STEREO = 0x0001 +V4L2_TUNER_MODE_LANG2 = 0x0002 +V4L2_TUNER_MODE_SAP = 0x0002 +V4L2_TUNER_MODE_LANG1 = 0x0003 +V4L2_TUNER_MODE_LANG1_LANG2 = 0x0004 + + +class v4l2_frequency(ctypes.Structure): + _fields_ = [ + ('tuner', ctypes.c_uint32), + ('type', v4l2_tuner_type), + ('frequency', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 8), + ] + + +class v4l2_hw_freq_seek(ctypes.Structure): + _fields_ = [ + ('tuner', ctypes.c_uint32), + ('type', v4l2_tuner_type), + ('seek_upward', ctypes.c_uint32), + ('wrap_around', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 8), + ] + + +# +# RDS +# + +class v4l2_rds_data(ctypes.Structure): + _fields_ = [ + ('lsb', ctypes.c_char), + ('msb', ctypes.c_char), + ('block', ctypes.c_char), + ] + + _pack_ = True + + +V4L2_RDS_BLOCK_MSK = 0x7 +V4L2_RDS_BLOCK_A = 0 +V4L2_RDS_BLOCK_B = 1 +V4L2_RDS_BLOCK_C = 2 +V4L2_RDS_BLOCK_D = 3 +V4L2_RDS_BLOCK_C_ALT = 4 +V4L2_RDS_BLOCK_INVALID = 7 + +V4L2_RDS_BLOCK_CORRECTED = 0x40 +V4L2_RDS_BLOCK_ERROR = 0x80 + + +# +# Audio +# + +class v4l2_audio(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('capability', ctypes.c_uint32), + ('mode', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ] + + +V4L2_AUDCAP_STEREO = 0x00001 +V4L2_AUDCAP_AVL = 0x00002 + +V4L2_AUDMODE_AVL = 0x00001 + + +class v4l2_audioout(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('capability', ctypes.c_uint32), + ('mode', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ] + + +# +# Mpeg services (experimental) +# + +V4L2_ENC_IDX_FRAME_I = 0 +V4L2_ENC_IDX_FRAME_P = 1 +V4L2_ENC_IDX_FRAME_B = 2 +V4L2_ENC_IDX_FRAME_MASK = 0xf + + +class v4l2_enc_idx_entry(ctypes.Structure): + _fields_ = [ + ('offset', ctypes.c_uint64), + ('pts', ctypes.c_uint64), + ('length', ctypes.c_uint32), + ('flags', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ] + + +V4L2_ENC_IDX_ENTRIES = 64 + + +class v4l2_enc_idx(ctypes.Structure): + _fields_ = [ + ('entries', ctypes.c_uint32), + ('entries_cap', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ('entry', v4l2_enc_idx_entry * V4L2_ENC_IDX_ENTRIES), + ] + + +V4L2_ENC_CMD_START = 0 +V4L2_ENC_CMD_STOP = 1 +V4L2_ENC_CMD_PAUSE = 2 +V4L2_ENC_CMD_RESUME = 3 + +V4L2_ENC_CMD_STOP_AT_GOP_END = 1 << 0 + + +class v4l2_encoder_cmd(ctypes.Structure): + class _u(ctypes.Union): + class _s(ctypes.Structure): + _fields_ = [ + ('data', ctypes.c_uint32 * 8), + ] + + _fields_ = [ + ('raw', _s), + ] + + _fields_ = [ + ('cmd', ctypes.c_uint32), + ('flags', ctypes.c_uint32), + ('_u', _u), + ] + + _anonymous_ = ('_u',) + + +# +# Data services (VBI) +# + +class v4l2_vbi_format(ctypes.Structure): + _fields_ = [ + ('sampling_rate', ctypes.c_uint32), + ('offset', ctypes.c_uint32), + ('samples_per_line', ctypes.c_uint32), + ('sample_format', ctypes.c_uint32), + ('start', ctypes.c_int32 * 2), + ('count', ctypes.c_uint32 * 2), + ('flags', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ] + + +V4L2_VBI_UNSYNC = 1 << 0 +V4L2_VBI_INTERLACED = 1 << 1 + + +class v4l2_sliced_vbi_format(ctypes.Structure): + _fields_ = [ + ('service_set', ctypes.c_uint16), + ('service_lines', ctypes.c_uint16 * 2 * 24), + ('io_size', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ] + + +V4L2_SLICED_TELETEXT_B = 0x0001 +V4L2_SLICED_VPS = 0x0400 +V4L2_SLICED_CAPTION_525 = 0x1000 +V4L2_SLICED_WSS_625 = 0x4000 +V4L2_SLICED_VBI_525 = V4L2_SLICED_CAPTION_525 +V4L2_SLICED_VBI_625 = ( + V4L2_SLICED_TELETEXT_B | V4L2_SLICED_VPS | V4L2_SLICED_WSS_625) + + +class v4l2_sliced_vbi_cap(ctypes.Structure): + _fields_ = [ + ('service_set', ctypes.c_uint16), + ('service_lines', ctypes.c_uint16 * 2 * 24), + ('type', v4l2_buf_type), + ('reserved', ctypes.c_uint32 * 3), + ] + + +class v4l2_sliced_vbi_data(ctypes.Structure): + _fields_ = [ + ('id', ctypes.c_uint32), + ('field', ctypes.c_uint32), + ('line', ctypes.c_uint32), + ('reserved', ctypes.c_uint32), + ('data', ctypes.c_char * 48), + ] + + +# +# Sliced VBI data inserted into MPEG Streams +# + + +V4L2_MPEG_VBI_IVTV_TELETEXT_B = 1 +V4L2_MPEG_VBI_IVTV_CAPTION_525 = 4 +V4L2_MPEG_VBI_IVTV_WSS_625 = 5 +V4L2_MPEG_VBI_IVTV_VPS = 7 + + +class v4l2_mpeg_vbi_itv0_line(ctypes.Structure): + _fields_ = [ + ('id', ctypes.c_char), + ('data', ctypes.c_char * 42), + ] + + _pack_ = True + + +class v4l2_mpeg_vbi_itv0(ctypes.Structure): + _fields_ = [ + ('linemask', ctypes.c_uint32 * 2), # how to define __le32 in ctypes? + ('line', v4l2_mpeg_vbi_itv0_line * 35), + ] + + _pack_ = True + + +class v4l2_mpeg_vbi_ITV0(ctypes.Structure): + _fields_ = [ + ('line', v4l2_mpeg_vbi_itv0_line * 36), + ] + + _pack_ = True + + +V4L2_MPEG_VBI_IVTV_MAGIC0 = "itv0" +V4L2_MPEG_VBI_IVTV_MAGIC1 = "ITV0" + + +class v4l2_mpeg_vbi_fmt_ivtv(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('itv0', v4l2_mpeg_vbi_itv0), + ('ITV0', v4l2_mpeg_vbi_ITV0), + ] + + _fields_ = [ + ('magic', ctypes.c_char * 4), + ('_u', _u) + ] + + _anonymous_ = ('_u',) + _pack_ = True + + +# +# Aggregate structures +# + +class v4l2_format(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('pix', v4l2_pix_format), + ('win', v4l2_window), + ('vbi', v4l2_vbi_format), + ('sliced', v4l2_sliced_vbi_format), + ('raw_data', ctypes.c_char * 200), + ] + + _fields_ = [ + ('type', v4l2_buf_type), + ('fmt', _u), + ] + + +class v4l2_streamparm(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('capture', v4l2_captureparm), + ('output', v4l2_outputparm), + ('raw_data', ctypes.c_char * 200), + ] + + _fields_ = [ + ('type', v4l2_buf_type), + ('parm', _u) + ] + + +# +# Advanced debugging +# + +V4L2_CHIP_MATCH_HOST = 0 +V4L2_CHIP_MATCH_I2C_DRIVER = 1 +V4L2_CHIP_MATCH_I2C_ADDR = 2 +V4L2_CHIP_MATCH_AC97 = 3 + + +class v4l2_dbg_match(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('addr', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ] + + _fields_ = [ + ('type', ctypes.c_uint32), + ('_u', _u), + ] + + _anonymous_ = ('_u',) + _pack_ = True + + +class v4l2_dbg_register(ctypes.Structure): + _fields_ = [ + ('match', v4l2_dbg_match), + ('size', ctypes.c_uint32), + ('reg', ctypes.c_uint64), + ('val', ctypes.c_uint64), + ] + + _pack_ = True + + +class v4l2_dbg_chip_ident(ctypes.Structure): + _fields_ = [ + ('match', v4l2_dbg_match), + ('ident', ctypes.c_uint32), + ('revision', ctypes.c_uint32), + ] + + _pack_ = True + + +# +# ioctl codes for video devices +# + +VIDIOC_QUERYCAP = _IOR('V', 0, v4l2_capability) +VIDIOC_RESERVED = _IO('V', 1) +VIDIOC_ENUM_FMT = _IOWR('V', 2, v4l2_fmtdesc) +VIDIOC_G_FMT = _IOWR('V', 4, v4l2_format) +VIDIOC_S_FMT = _IOWR('V', 5, v4l2_format) +VIDIOC_REQBUFS = _IOWR('V', 8, v4l2_requestbuffers) +VIDIOC_QUERYBUF = _IOWR('V', 9, v4l2_buffer) +VIDIOC_G_FBUF = _IOR('V', 10, v4l2_framebuffer) +VIDIOC_S_FBUF = _IOW('V', 11, v4l2_framebuffer) +VIDIOC_OVERLAY = _IOW('V', 14, ctypes.c_int) +VIDIOC_QBUF = _IOWR('V', 15, v4l2_buffer) +VIDIOC_DQBUF = _IOWR('V', 17, v4l2_buffer) +VIDIOC_STREAMON = _IOW('V', 18, ctypes.c_int) +VIDIOC_STREAMOFF = _IOW('V', 19, ctypes.c_int) +VIDIOC_G_PARM = _IOWR('V', 21, v4l2_streamparm) +VIDIOC_S_PARM = _IOWR('V', 22, v4l2_streamparm) +VIDIOC_G_STD = _IOR('V', 23, v4l2_std_id) +VIDIOC_S_STD = _IOW('V', 24, v4l2_std_id) +VIDIOC_ENUMSTD = _IOWR('V', 25, v4l2_standard) +VIDIOC_ENUMINPUT = _IOWR('V', 26, v4l2_input) +VIDIOC_G_CTRL = _IOWR('V', 27, v4l2_control) +VIDIOC_S_CTRL = _IOWR('V', 28, v4l2_control) +VIDIOC_G_TUNER = _IOWR('V', 29, v4l2_tuner) +VIDIOC_S_TUNER = _IOW('V', 30, v4l2_tuner) +VIDIOC_G_AUDIO = _IOR('V', 33, v4l2_audio) +VIDIOC_S_AUDIO = _IOW('V', 34, v4l2_audio) +VIDIOC_QUERYCTRL = _IOWR('V', 36, v4l2_queryctrl) +VIDIOC_QUERYMENU = _IOWR('V', 37, v4l2_querymenu) +VIDIOC_G_INPUT = _IOR('V', 38, ctypes.c_int) +VIDIOC_S_INPUT = _IOWR('V', 39, ctypes.c_int) +VIDIOC_G_OUTPUT = _IOR('V', 46, ctypes.c_int) +VIDIOC_S_OUTPUT = _IOWR('V', 47, ctypes.c_int) +VIDIOC_ENUMOUTPUT = _IOWR('V', 48, v4l2_output) +VIDIOC_G_AUDOUT = _IOR('V', 49, v4l2_audioout) +VIDIOC_S_AUDOUT = _IOW('V', 50, v4l2_audioout) +VIDIOC_G_MODULATOR = _IOWR('V', 54, v4l2_modulator) +VIDIOC_S_MODULATOR = _IOW('V', 55, v4l2_modulator) +VIDIOC_G_FREQUENCY = _IOWR('V', 56, v4l2_frequency) +VIDIOC_S_FREQUENCY = _IOW('V', 57, v4l2_frequency) +VIDIOC_CROPCAP = _IOWR('V', 58, v4l2_cropcap) +VIDIOC_G_CROP = _IOWR('V', 59, v4l2_crop) +VIDIOC_S_CROP = _IOW('V', 60, v4l2_crop) +VIDIOC_G_JPEGCOMP = _IOR('V', 61, v4l2_jpegcompression) +VIDIOC_S_JPEGCOMP = _IOW('V', 62, v4l2_jpegcompression) +VIDIOC_QUERYSTD = _IOR('V', 63, v4l2_std_id) +VIDIOC_TRY_FMT = _IOWR('V', 64, v4l2_format) +VIDIOC_ENUMAUDIO = _IOWR('V', 65, v4l2_audio) +VIDIOC_ENUMAUDOUT = _IOWR('V', 66, v4l2_audioout) +VIDIOC_G_PRIORITY = _IOR('V', 67, v4l2_priority) +VIDIOC_S_PRIORITY = _IOW('V', 68, v4l2_priority) +VIDIOC_G_SLICED_VBI_CAP = _IOWR('V', 69, v4l2_sliced_vbi_cap) +VIDIOC_LOG_STATUS = _IO('V', 70) +VIDIOC_G_EXT_CTRLS = _IOWR('V', 71, v4l2_ext_controls) +VIDIOC_S_EXT_CTRLS = _IOWR('V', 72, v4l2_ext_controls) +VIDIOC_TRY_EXT_CTRLS = _IOWR('V', 73, v4l2_ext_controls) + +VIDIOC_ENUM_FRAMESIZES = _IOWR('V', 74, v4l2_frmsizeenum) +VIDIOC_ENUM_FRAMEINTERVALS = _IOWR('V', 75, v4l2_frmivalenum) +VIDIOC_G_ENC_INDEX = _IOR('V', 76, v4l2_enc_idx) +VIDIOC_ENCODER_CMD = _IOWR('V', 77, v4l2_encoder_cmd) +VIDIOC_TRY_ENCODER_CMD = _IOWR('V', 78, v4l2_encoder_cmd) + +VIDIOC_DBG_S_REGISTER = _IOW('V', 79, v4l2_dbg_register) +VIDIOC_DBG_G_REGISTER = _IOWR('V', 80, v4l2_dbg_register) + +VIDIOC_DBG_G_CHIP_IDENT = _IOWR('V', 81, v4l2_dbg_chip_ident) + +VIDIOC_S_HW_FREQ_SEEK = _IOW('V', 82, v4l2_hw_freq_seek) +VIDIOC_ENUM_DV_PRESETS = _IOWR('V', 83, v4l2_dv_enum_preset) +VIDIOC_S_DV_PRESET = _IOWR('V', 84, v4l2_dv_preset) +VIDIOC_G_DV_PRESET = _IOWR('V', 85, v4l2_dv_preset) +VIDIOC_QUERY_DV_PRESET = _IOR('V', 86, v4l2_dv_preset) +VIDIOC_S_DV_TIMINGS = _IOWR('V', 87, v4l2_dv_timings) +VIDIOC_G_DV_TIMINGS = _IOWR('V', 88, v4l2_dv_timings) + +VIDIOC_OVERLAY_OLD = _IOWR('V', 14, ctypes.c_int) +VIDIOC_S_PARM_OLD = _IOW('V', 22, v4l2_streamparm) +VIDIOC_S_CTRL_OLD = _IOW('V', 28, v4l2_control) +VIDIOC_G_AUDIO_OLD = _IOWR('V', 33, v4l2_audio) +VIDIOC_G_AUDOUT_OLD = _IOWR('V', 49, v4l2_audioout) +VIDIOC_CROPCAP_OLD = _IOR('V', 58, v4l2_cropcap) + +BASE_VIDIOC_PRIVATE = 192 diff --git a/tests/compare.sh b/tests/compare.sh new file mode 100755 index 0000000..969d550 --- /dev/null +++ b/tests/compare.sh @@ -0,0 +1,29 @@ +# TEST MODEL-FRAME COMPARE FUNCTIONS +set -o xtrace +set -e + +# Make sure howdy is clean before starting +sudo howdy clear -y || true + +# Learn match 1 +sudo sed -i "s,device_path.*,device_path = $PWD\/tests\/video\/match1.m4v,g" /lib/security/howdy/config.ini +sudo howdy add -y + +# Text compare matching with same camera input +sudo python3 /lib/security/howdy/compare.py $USER + +# Change to match 2 and compare against the modal of match 1, which should fail +sudo sed -i "s,device_path.*,device_path = $PWD\/tests\/video\/match2.m4v,g" /lib/security/howdy/config.ini +! sudo python3 /lib/security/howdy/compare.py $USER + +# Add match 2 as a model to compare both 1 and 2 at the same time +sudo howdy add -y +sudo python3 /lib/security/howdy/compare.py $USER + +# Compare against a camera with no visible face +sudo sed -i "s,device_path.*,device_path = $PWD\/tests\/video\/noMatch.m4v,g" /lib/security/howdy/config.ini +! sudo python3 /lib/security/howdy/compare.py $USER + +# Clean up +sudo howdy clear -y +sudo sed -i "s,device_path.*,device_path = none,g" /lib/security/howdy/config.ini diff --git a/tests/importing.sh b/tests/importing.sh new file mode 100755 index 0000000..9a75c76 --- /dev/null +++ b/tests/importing.sh @@ -0,0 +1,9 @@ +# TEST INSTALLATION OF DEPENDENCIES +set -o xtrace +set -e + +# Confirm the cv2 module has been installed correctly +sudo /usr/bin/env python3 -c "import cv2; print(cv2.__version__);" + +# Confirm the dlib module has been installed correctly +sudo /usr/bin/env python3 -c "import dlib; print(dlib.__version__);" diff --git a/tests/pam.sh b/tests/pam.sh new file mode 100755 index 0000000..7633546 --- /dev/null +++ b/tests/pam.sh @@ -0,0 +1,32 @@ +# TEST THE PAM INTEGRATION +set -o xtrace +set -e + +# Make sure howdy is clean before starting +sudo howdy clear -y || true + +# Change active camera to match video 1 +sudo sed -i "s,device_path.*,device_path = $PWD/tests\/video\/match1.m4v,g" /lib/security/howdy/config.ini + +# Let howdy add the match face +sudo howdy add -y + +# Test the PAM auth +timeout 10 pamtester login $USER authenticate + +# Clear the face models and change the camera to video 2 +sudo howdy clear -y +sudo sed -i "s,device_path.*,device_path = $PWD\/tests\/video\/match2.m4v,g" /lib/security/howdy/config.ini + +# Let howdy add the match face +sudo howdy add -y + +# Try to open a elevated session through PAM +timeout 10 pamtester login $USER open_session + +# Verify we can close sessions, even though howdy does not use this PAM function +timeout 10 pamtester login $USER close_session + +# Clean up +sudo howdy clear -y +sudo sed -i "s,device_path.*,device_path = none,g" /lib/security/howdy/config.ini diff --git a/tests/passthrough.sh b/tests/passthrough.sh new file mode 100755 index 0000000..629d142 --- /dev/null +++ b/tests/passthrough.sh @@ -0,0 +1,7 @@ +# TEST USER SUDO PASSTHOUGH (NON-ROOT) +set -o xtrace +set -e + +# Check if the username passthough works correctly with sudo +howdy | ack-grep --passthru --color "current active user: travis" +sudo howdy | ack-grep --passthru --color "current active user: travis" diff --git a/tests/video/match1.m4v b/tests/video/match1.m4v new file mode 100644 index 0000000..b3c2497 Binary files /dev/null and b/tests/video/match1.m4v differ diff --git a/tests/video/match2.m4v b/tests/video/match2.m4v new file mode 100644 index 0000000..7797c28 Binary files /dev/null and b/tests/video/match2.m4v differ diff --git a/tests/video/noMatch.m4v b/tests/video/noMatch.m4v new file mode 100644 index 0000000..dacd9a1 Binary files /dev/null and b/tests/video/noMatch.m4v differ