pax_global_header00006660000000000000000000000064147134110010014502gustar00rootroot0000000000000052 comment=5c3fd210147ae9112ba27c4cd23803fc3baa7fd6 home-assistant-libs-ha-ffmpeg-5c3fd21/000077500000000000000000000000001471341100100176315ustar00rootroot00000000000000home-assistant-libs-ha-ffmpeg-5c3fd21/.github/000077500000000000000000000000001471341100100211715ustar00rootroot00000000000000home-assistant-libs-ha-ffmpeg-5c3fd21/.github/dependabot.yml000066400000000000000000000004101471341100100240140ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: daily open-pull-requests-limit: 10 - package-ecosystem: pip directory: "/" schedule: interval: weekly open-pull-requests-limit: 10 home-assistant-libs-ha-ffmpeg-5c3fd21/.github/release-drafter.yml000066400000000000000000000000541471341100100247600ustar00rootroot00000000000000template: | ## What's Changed $CHANGES home-assistant-libs-ha-ffmpeg-5c3fd21/.github/workflows/000077500000000000000000000000001471341100100232265ustar00rootroot00000000000000home-assistant-libs-ha-ffmpeg-5c3fd21/.github/workflows/pythonpublish.yml000066400000000000000000000015651471341100100266700ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [published] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.2.2 - name: Set up Python uses: actions/setup-python@v5.3.0 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | python setup.py sdist bdist_wheel twine upload dist/* home-assistant-libs-ha-ffmpeg-5c3fd21/.github/workflows/release-drafter.yml000066400000000000000000000005141471341100100270160ustar00rootroot00000000000000name: Release Drafter on: push: branches: - master jobs: update_release_draft: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into "master" - uses: release-drafter/release-drafter@v6.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} home-assistant-libs-ha-ffmpeg-5c3fd21/.github/workflows/test.yml000066400000000000000000000012161471341100100247300ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Run Tests on: push: branches: [master] pull_request: branches: [master] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.2.2 - name: Set up Python 3.8 uses: actions/setup-python@v5.3.0 with: python-version: 3.8 - name: Install dependencies run: | pip install tox - name: Run tox run: | tox home-assistant-libs-ha-ffmpeg-5c3fd21/.gitignore000066400000000000000000000021121471341100100216150ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # PyCharm project settings .idea # vscode .vscode/ home-assistant-libs-ha-ffmpeg-5c3fd21/LICENSE000066400000000000000000000027121471341100100206400ustar00rootroot00000000000000Copyright (c) 2016, Pascal Vizeli All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 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. * Neither the name of hass_ffmpeg nor the names of its contributors may 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 HOLDER 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. home-assistant-libs-ha-ffmpeg-5c3fd21/MANIFEST.in000066400000000000000000000000431471341100100213640ustar00rootroot00000000000000include README.rst include LICENSE home-assistant-libs-ha-ffmpeg-5c3fd21/README.md000066400000000000000000000005411471341100100211100ustar00rootroot00000000000000# Home-Assistant ffmpeg interface A Python library to control ffmepg from asyncio for [Home Assistant](https://www.home-assistant.io). - Emulate webcam from any video input source for HA - Analyse a video/audio stream for noise or motion detection - Grab image from a stream Take care to protect function calls to this library with `asyncio.shield`. home-assistant-libs-ha-ffmpeg-5c3fd21/haffmpeg/000077500000000000000000000000001471341100100214065ustar00rootroot00000000000000home-assistant-libs-ha-ffmpeg-5c3fd21/haffmpeg/__init__.py000066400000000000000000000001321471341100100235130ustar00rootroot00000000000000"""homeassistant ffmpeg shell wrapper.""" __all__ = ["core", "camera", "sensor", "tools"] home-assistant-libs-ha-ffmpeg-5c3fd21/haffmpeg/camera.py000066400000000000000000000011401471341100100232040ustar00rootroot00000000000000"""For HA camera components.""" from typing import Coroutine, Optional from .core import HAFFmpeg class CameraMjpeg(HAFFmpeg): """Implement a camera they convert video stream to MJPEG.""" def open_camera( self, input_source: str, extra_cmd: Optional[str] = None ) -> Coroutine: """Open FFmpeg process as mjpeg video stream. Return A coroutine. """ command = ["-an", "-c:v", "mjpeg"] return self.open( cmd=command, input_source=input_source, output="-f mpjpeg -", extra_cmd=extra_cmd, ) home-assistant-libs-ha-ffmpeg-5c3fd21/haffmpeg/core.py000066400000000000000000000201141471341100100227060ustar00rootroot00000000000000"""Base functionality of ffmpeg HA wrapper.""" import asyncio import logging import re import shlex from typing import List, Optional, Set from .timeout import asyncio_timeout _LOGGER = logging.getLogger(__name__) FFMPEG_STDOUT = "stdout" FFMPEG_STDERR = "stderr" _BACKGROUND_TASKS: Set[asyncio.Task] = set() class HAFFmpeg: """HA FFmpeg process async. Object is iterable or use the process property to call from Popen object. """ def __init__(self, ffmpeg_bin: str): """Base initialize.""" self._loop = asyncio.get_running_loop() self._ffmpeg = ffmpeg_bin self._argv = None self._proc: Optional["asyncio.subprocess.Process"] = None @property def process(self) -> "asyncio.subprocess.Process": """Return a Popen object or None of not running.""" return self._proc @property def is_running(self) -> bool: """Return True if ffmpeg is running.""" if self._proc is None or self._proc.returncode is not None: return False return True def _generate_ffmpeg_cmd( self, cmd: List[str], input_source: Optional[str], output: Optional[str], extra_cmd: Optional[str] = None, ) -> None: """Generate ffmpeg command line.""" self._argv = [self._ffmpeg] # start command init if input_source is not None: self._put_input(input_source) self._argv.extend(cmd) # exists a extra cmd from customer if extra_cmd is not None: self._argv.extend(shlex.split(extra_cmd)) self._merge_filters() self._put_output(output) def _put_input(self, input_source: str) -> None: """Put input string to ffmpeg command.""" input_cmd = shlex.split(str(input_source)) if len(input_cmd) > 1: self._argv.extend(input_cmd) else: self._argv.extend(["-i", input_source]) def _put_output(self, output: Optional[str]) -> None: """Put output string to ffmpeg command.""" if output is None: self._argv.extend(["-f", "null", "-"]) return output_cmd = shlex.split(str(output)) if len(output_cmd) > 1: self._argv.extend(output_cmd) else: self._argv.append(output) def _merge_filters(self) -> None: """Merge all filter config in command line.""" for opts in (["-filter:a", "-af"], ["-filter:v", "-vf"]): filter_list = [] new_argv = [] cmd_iter = iter(self._argv) for element in cmd_iter: if element in opts: filter_list.insert(0, next(cmd_iter)) else: new_argv.append(element) # update argv if changes if filter_list: new_argv.extend([opts[0], ",".join(filter_list)]) self._argv = new_argv.copy() def _clear(self) -> None: """Clear member variable after close.""" self._argv = None self._proc = None async def open( self, cmd: List[str], input_source: Optional[str], output: Optional[str] = "-", extra_cmd: Optional[str] = None, stdout_pipe: bool = True, stderr_pipe: bool = False, ) -> bool: """Start a ffmpeg instance and pipe output.""" stdout = asyncio.subprocess.PIPE if stdout_pipe else asyncio.subprocess.DEVNULL stderr = asyncio.subprocess.PIPE if stderr_pipe else asyncio.subprocess.DEVNULL if self.is_running: _LOGGER.warning("FFmpeg is already running!") return True # set command line self._generate_ffmpeg_cmd(cmd, input_source, output, extra_cmd) # start ffmpeg _LOGGER.debug("Start FFmpeg with %s", str(self._argv)) try: self._proc = await asyncio.create_subprocess_exec( *self._argv, bufsize=0, stdin=asyncio.subprocess.PIPE, stdout=stdout, stderr=stderr, close_fds=False, ) except Exception as err: # pylint: disable=broad-except _LOGGER.exception("FFmpeg fails %s", err) self._clear() return False return self._proc is not None async def close(self, timeout=5) -> None: """Stop a ffmpeg instance.""" if not self.is_running: _LOGGER.debug("FFmpeg isn't running!") return # Can't use communicate because we attach the output to a streamreader # send stop to ffmpeg try: self._proc.stdin.write(b"q") async with asyncio_timeout(timeout): await self._proc.wait() _LOGGER.debug("Close FFmpeg process") except (asyncio.TimeoutError, ValueError): _LOGGER.warning("Timeout while waiting of FFmpeg") self.kill() finally: self._clear() def kill(self) -> None: """Kill ffmpeg job.""" self._proc.kill() background_task = asyncio.create_task(self._proc.communicate()) _BACKGROUND_TASKS.add(background_task) background_task.add_done_callback(_BACKGROUND_TASKS.remove) async def get_reader(self, source=FFMPEG_STDOUT) -> asyncio.StreamReader: """Create and return streamreader.""" if source == FFMPEG_STDOUT: return self._proc.stdout return self._proc.stderr class HAFFmpegWorker(HAFFmpeg): """Read FFmpeg output to queue.""" def __init__(self, ffmpeg_bin: str): """Init noise sensor.""" super().__init__(ffmpeg_bin) self._queue = asyncio.Queue() self._input = None self._read_task = None async def close(self, timeout: int = 5) -> None: """Stop a ffmpeg instance. Return a coroutine """ if self._read_task is not None and not self._read_task.cancelled(): self._read_task.cancel() return await super().close(timeout) async def _process_lines(self, pattern: Optional[str] = None) -> None: """Read line from pipe they match with pattern.""" if pattern is not None: cmp = re.compile(pattern) _LOGGER.debug("Start working with pattern '%s'.", pattern) # read lines while self.is_running: try: line = await self._input.readline() if not line: break line = line.decode() except Exception: # pylint: disable=broad-except break match = True if pattern is None else cmp.search(line) if match: _LOGGER.debug("Process: %s", line) await self._queue.put(line) try: await self._proc.wait() finally: await self._queue.put(None) _LOGGER.debug("Stopped reading ffmpeg output.") async def _worker_process(self) -> None: """Process output line.""" raise NotImplementedError() async def start_worker( self, cmd: List[str], input_source: str, output: Optional[str] = None, extra_cmd: Optional[str] = None, pattern: Optional[str] = None, reading: str = FFMPEG_STDERR, ) -> None: """Start ffmpeg do process data from output.""" if self.is_running: _LOGGER.warning("Can't start worker. It is allready running!") return if reading == FFMPEG_STDERR: stdout = False stderr = True else: stdout = True stderr = False # start ffmpeg and reading to queue await self.open( cmd=cmd, input_source=input_source, output=output, extra_cmd=extra_cmd, stdout_pipe=stdout, stderr_pipe=stderr, ) self._input = await self.get_reader(reading) # start background processing self._read_task = self._loop.create_task(self._process_lines(pattern)) self._loop.create_task(self._worker_process()) home-assistant-libs-ha-ffmpeg-5c3fd21/haffmpeg/sensor.py000066400000000000000000000167431471341100100233040ustar00rootroot00000000000000"""For HA sensor components.""" import asyncio import logging import re from time import time from typing import Callable, Coroutine, Optional from .core import FFMPEG_STDOUT, HAFFmpegWorker from .timeout import asyncio_timeout _LOGGER = logging.getLogger(__name__) class SensorNoise(HAFFmpegWorker): """Implement a noise detection on a autio stream.""" STATE_NONE = 0 STATE_NOISE = 1 STATE_END = 2 STATE_DETECT = 3 def __init__(self, ffmpeg_bin: str, callback: Callable): """Init noise sensor.""" super().__init__(ffmpeg_bin) self._callback = callback self._peak = -30 self._time_duration = 1 self._time_reset = 2 def set_options( self, time_duration: int = 1, time_reset: int = 2, peak: int = -30 ) -> None: """Set option parameter for noise sensor.""" self._time_duration = time_duration self._time_reset = time_reset self._peak = peak def open_sensor( self, input_source: str, output_dest: Optional[str] = None, extra_cmd: Optional[str] = None, ) -> Coroutine: """Open FFmpeg process for read autio stream. Return a coroutine. """ command = ["-vn", "-filter:a", f"silencedetect=n={self._peak}dB:d=1"] # run ffmpeg, read output return self.start_worker( cmd=command, input_source=input_source, output=output_dest, extra_cmd=extra_cmd, pattern="silence", ) async def _worker_process(self) -> None: """This function processing data.""" state = self.STATE_DETECT timeout = self._time_duration self._loop.call_soon(self._callback, False) re_start = re.compile("silence_start") re_end = re.compile("silence_end") # process queue data while True: try: _LOGGER.debug("Reading State: %d, timeout: %s", state, timeout) async with asyncio_timeout(timeout): data = await self._queue.get() timeout = None if data is None: self._loop.call_soon(self._callback, None) return except asyncio.TimeoutError: _LOGGER.debug("Blocking timeout") # noise if state == self.STATE_DETECT: # noise detected self._loop.call_soon(self._callback, True) state = self.STATE_NOISE elif state == self.STATE_END: # no noise self._loop.call_soon(self._callback, False) state = self.STATE_NONE timeout = None continue if re_start.search(data): if state == self.STATE_NOISE: # stop noise detection state = self.STATE_END timeout = self._time_reset elif state == self.STATE_DETECT: # reset if only a peak state = self.STATE_NONE continue if re_end.search(data): if state == self.STATE_NONE: # detect noise begin state = self.STATE_DETECT timeout = self._time_duration elif state == self.STATE_END: # back to noise status state = self.STATE_NOISE continue _LOGGER.warning("Unknown data from queue!") class SensorMotion(HAFFmpegWorker): """Implement motion detection with ffmpeg scene detection.""" STATE_NONE = 0 STATE_REPEAT = 1 STATE_MOTION = 2 MATCH = r"\d,.*\d,.*\d,.*\d,.*\d,.*\w" def __init__(self, ffmpeg_bin: str, callback: Callable): """Init motion sensor.""" super().__init__(ffmpeg_bin) self._callback = callback self._changes = 10 self._time_reset = 60 self._time_repeat = 0 self._repeat = 0 def set_options( self, time_reset: int = 60, time_repeat: int = 0, repeat: int = 0, changes: int = 10, ) -> None: """Set option parameter for noise sensor.""" self._time_reset = time_reset self._time_repeat = time_repeat self._repeat = repeat self._changes = changes async def open_sensor( self, input_source: str, extra_cmd: Optional[str] = None ) -> Coroutine: """Open FFmpeg process a video stream for motion detection. Return a coroutine. """ command = [ "-an", "-filter:v", f"select=gt(scene\\,{self._changes / 100})", ] # run ffmpeg, read output return await self.start_worker( cmd=command, input_source=input_source, output="-f framemd5 -", extra_cmd=extra_cmd, pattern=self.MATCH, reading=FFMPEG_STDOUT, ) async def _worker_process(self) -> None: """This function processing data.""" state = self.STATE_NONE timeout = None self._loop.call_soon(self._callback, False) # for repeat feature re_frame = 0 re_time = 0 re_data = re.compile(self.MATCH) # process queue data while True: try: _LOGGER.debug("Reading State: %d, timeout: %s", state, timeout) async with asyncio_timeout(timeout): data = await self._queue.get() if data is None: self._loop.call_soon(self._callback, None) return except asyncio.TimeoutError: _LOGGER.debug("Blocking timeout") # reset motion detection if state == self.STATE_MOTION: state = self.STATE_NONE self._loop.call_soon(self._callback, False) timeout = None # reset repeate state if state == self.STATE_REPEAT: state = self.STATE_NONE timeout = None continue frames = re_data.search(data) if frames: # repeat not used if self._repeat == 0 and state == self.STATE_NONE: state = self.STATE_MOTION self._loop.call_soon(self._callback, True) timeout = self._time_reset # repeat feature is on / first motion if state == self.STATE_NONE: state = self.STATE_REPEAT timeout = self._time_repeat re_frame = 0 re_time = time() elif state == self.STATE_REPEAT: re_frame += 1 # REPEAT ready? if re_frame >= self._repeat: state = self.STATE_MOTION self._loop.call_soon(self._callback, True) timeout = self._time_reset else: past = time() - re_time timeout -= past # REPEAT time down if timeout <= 0: _LOGGER.debug("Reset repeat to none") state = self.STATE_NONE timeout = None continue _LOGGER.warning("Unknown data from queue!") home-assistant-libs-ha-ffmpeg-5c3fd21/haffmpeg/timeout.py000066400000000000000000000003661471341100100234530ustar00rootroot00000000000000"""Timeouts.""" import sys if sys.version_info[:2] < (3, 11): # pylint: disable-next=unused-import from async_timeout import timeout as asyncio_timeout # noqa: F401 else: from asyncio import timeout as asyncio_timeout # noqa: F401 home-assistant-libs-ha-ffmpeg-5c3fd21/haffmpeg/tools.py000066400000000000000000000046051471341100100231250ustar00rootroot00000000000000"""For HA varios tools.""" import asyncio import logging import re from typing import Optional from .core import HAFFmpeg from .timeout import asyncio_timeout _LOGGER = logging.getLogger(__name__) IMAGE_JPEG = "mjpeg" IMAGE_PNG = "png" class ImageFrame(HAFFmpeg): """Implement a single image capture from a stream.""" async def get_image( self, input_source: str, output_format: str = IMAGE_JPEG, extra_cmd: Optional[str] = None, timeout: int = 15, ) -> Optional[bytes]: """Open FFmpeg process as capture 1 frame.""" command = ["-an", "-frames:v", "1", "-c:v", output_format] # open input for capture 1 frame is_open = await self.open( cmd=command, input_source=input_source, output="-f image2pipe -", extra_cmd=extra_cmd, ) # error after open? if not is_open: _LOGGER.warning("Error starting FFmpeg.") return None # read image try: async with asyncio_timeout(timeout): image, _ = await self._proc.communicate() return image except (asyncio.TimeoutError, ValueError): _LOGGER.warning("Timeout reading image.") self.kill() return None finally: await self.close(0) class FFVersion(HAFFmpeg): """Retrieve FFmpeg version information.""" async def get_version(self, timeout: int = 15) -> Optional[str]: """Execute FFmpeg process and parse the version information. Return full FFmpeg version string. Such as 3.4.2-tessus """ command = ["-version"] # open input for capture 1 frame is_open = await self.open(cmd=command, input_source=None, output="") # error after open? if not is_open: _LOGGER.warning("Error starting FFmpeg.") return # read output try: async with asyncio_timeout(timeout): output, _ = await self._proc.communicate() result = re.search(r"ffmpeg version (\S*)", output.decode()) if result is not None: return result.group(1) except (asyncio.TimeoutError, ValueError): _LOGGER.warning("Timeout reading stdout.") self.kill() finally: await self.close(0) return None home-assistant-libs-ha-ffmpeg-5c3fd21/pylintrc000066400000000000000000000013441471341100100214220ustar00rootroot00000000000000[MAIN] # Reasons disabled: # locally-disabled - it spams too much # duplicate-code - unavoidable # unused-argument - generic callbacks and setup methods create a lot of warnings # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* disable= duplicate-code, locally-disabled, too-few-public-methods, too-many-arguments, too-many-branches, too-many-instance-attributes, too-many-lines, too-many-locals, too-many-public-methods, too-many-return-statements, too-many-statements, too-many-locals, too-many-branches, unused-argument, enable= useless-suppression, use-symbolic-message-instead, fail-on= I, ignore=test load-plugins= pylint_strict_informational, score=no home-assistant-libs-ha-ffmpeg-5c3fd21/requirements.txt000066400000000000000000000000251471341100100231120ustar00rootroot00000000000000async-timeout==5.0.0 home-assistant-libs-ha-ffmpeg-5c3fd21/requirements_dev.txt000066400000000000000000000001301471341100100237450ustar00rootroot00000000000000-r requirements.txt -r requirements_lint.txt -r requirements_test.txt tox==4.23.2 -e . home-assistant-libs-ha-ffmpeg-5c3fd21/requirements_lint.txt000066400000000000000000000000751471341100100241450ustar00rootroot00000000000000flake8==7.1.1 pylint==3.1.0 pylint-strict-informational==0.1 home-assistant-libs-ha-ffmpeg-5c3fd21/requirements_test.txt000066400000000000000000000000151471341100100241500ustar00rootroot00000000000000click==8.1.7 home-assistant-libs-ha-ffmpeg-5c3fd21/setup.cfg000066400000000000000000000005751471341100100214610ustar00rootroot00000000000000[isort] multi_line_output = 3 include_trailing_comma=True force_grid_wrap=0 line_length=88 indent = " " not_skip = __init__.py force_sort_within_sections = true sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER default_section = THIRDPARTY forced_separate = tests combine_as_imports = true use_parentheses = true [flake8] max-line-length = 88 ignore = E501 home-assistant-libs-ha-ffmpeg-5c3fd21/setup.py000066400000000000000000000023311471341100100213420ustar00rootroot00000000000000from setuptools import setup with open("README.md") as f: long_description = f.read() VERSION = "3.2.2" setup( name="ha-ffmpeg", version=VERSION, license="BSD License", author="Pascal Vizeli", author_email="pvizeli@syshack.ch", url="https://github.com/pvizeli/ha-ffmpeg", download_url="https://github.com/pvizeli/ha-ffmpeg/tarball/" + VERSION, description=("A library that handling with ffmpeg for home-assistant"), long_description=long_description, classifiers=[ "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Scientific/Engineering :: Atmospheric Science", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", ], keywords=["ffmpeg", "homeassistant", "wrapper", "api"], zip_safe=False, platforms="any", packages=["haffmpeg"], include_package_data=True, install_requires=["async_timeout"], ) home-assistant-libs-ha-ffmpeg-5c3fd21/test/000077500000000000000000000000001471341100100206105ustar00rootroot00000000000000home-assistant-libs-ha-ffmpeg-5c3fd21/test/camera_mjpeg.py000066400000000000000000000017211471341100100235750ustar00rootroot00000000000000import asyncio import logging import click from haffmpeg.camera import CameraMjpeg logging.basicConfig(level=logging.DEBUG) @click.command() @click.option("--ffmpeg", "-f", default="ffmpeg", help="FFmpeg binary") @click.option("--source", "-s", help="Input file for ffmpeg") @click.option("--output", "-o", help="Output image path") @click.option("--extra", "-e", help="Extra ffmpeg command line arguments") def cli(ffmpeg, source, output, extra): """FFMPEG capture frame as image.""" async def read_stream(): """Read stream inside loop.""" stream = CameraMjpeg(ffmpeg_bin=ffmpeg) await stream.open_camera(source, extra) reader = await stream.get_reader() try: while True: data = await reader.read(2048) print(data) except OSError: pass finally: await stream.close() asyncio.run(read_stream()) if __name__ == "__main__": cli() home-assistant-libs-ha-ffmpeg-5c3fd21/test/sensor_motion.py000066400000000000000000000030551471341100100240630ustar00rootroot00000000000000import asyncio import logging import click from haffmpeg.sensor import SensorMotion logging.basicConfig(level=logging.DEBUG) @click.command() @click.option("--ffmpeg", "-f", default="ffmpeg", help="FFmpeg binary") @click.option("--source", "-s", help="Input file for ffmpeg") @click.option( "--reset", "-r", default=60, type=int, help="Time duration to need no motion before reset state", ) @click.option( "--repeat-time", "-rt", default=0, type=int, help="Need repeat motion in this time period for trigger state", ) @click.option( "--repeat", "-rc", default=0, type=int, help="Need repeat motion to trigger state in repeat-time", ) @click.option( "--changes", "-c", default=10, type=float, help="Scene change settings or percent of image they need change", ) @click.option("--extra", "-e", help="Extra ffmpeg command line arguments") def cli(ffmpeg, source, reset, repeat_time, repeat, changes, extra): """FFMPEG noise detection.""" def callback(state): print("Motion detection is: %s" % str(state)) async def run(): sensor = SensorMotion(ffmpeg_bin=ffmpeg, callback=callback) sensor.set_options( time_reset=reset, changes=changes, repeat=repeat, time_repeat=repeat_time ) await sensor.open_sensor(input_source=source, extra_cmd=extra) try: while True: await asyncio.sleep(0.1) finally: await sensor.close() asyncio.run(run()) if __name__ == "__main__": cli() home-assistant-libs-ha-ffmpeg-5c3fd21/test/sensor_noise.py000066400000000000000000000026401471341100100236720ustar00rootroot00000000000000import asyncio import logging import click from haffmpeg.sensor import SensorNoise logging.basicConfig(level=logging.DEBUG) @click.command() @click.option("--ffmpeg", "-f", default="ffmpeg", help="FFmpeg binary") @click.option("--source", "-s", help="Input file for ffmpeg") @click.option("--output", "-o", default=None, help="Output ffmpeg target") @click.option( "--duration", "-d", default=1, type=int, help="Time duration to detect as noise (peak filter)", ) @click.option( "--reset", "-r", default=2, type=int, help="Time of silent to set the end of detection", ) @click.option( "--peak", "-p", default=-30, type=int, help="dB for detect a peak. Default -30" ) @click.option("--extra", "-e", help="Extra ffmpeg command line arguments") def cli(ffmpeg, source, output, duration, reset, peak, extra): """FFMPEG noise detection.""" def callback(state): print("Noise detection is: %s" % str(state)) async def run(): sensor = SensorNoise(ffmpeg_bin=ffmpeg, callback=callback) sensor.set_options(time_duration=duration, time_reset=reset, peak=peak) await sensor.open_sensor( input_source=source, output_dest=output, extra_cmd=extra ) try: while True: await asyncio.sleep(0.1) finally: await sensor.close() asyncio.run(run()) if __name__ == "__main__": cli() home-assistant-libs-ha-ffmpeg-5c3fd21/test/tools_imageframe.py000066400000000000000000000020621471341100100244770ustar00rootroot00000000000000import asyncio import logging import click from haffmpeg.tools import ImageFrame, IMAGE_JPEG logging.basicConfig(level=logging.DEBUG) @click.command() @click.option("--ffmpeg", "-f", default="ffmpeg", help="FFmpeg binary") @click.option("--source", "-s", required=True, help="Input file for ffmpeg") @click.option("--format_img", "-f", default=IMAGE_JPEG, help="Image output format") @click.option("--output", "-o", required=True, help="Output image file") @click.option("--extra", "-e", help="Extra ffmpeg command line arguments") def cli(ffmpeg, source, format_img, output, extra): """FFMPEG capture frame as image.""" async def capture_image(): stream = ImageFrame(ffmpeg_bin=ffmpeg) return await stream.get_image( input_source=source, output_format=format_img, extra_cmd=extra ) image = asyncio.run(capture_image()) if image: with open(output, "wb") as fh_img: fh_img.write(image) else: print("No image returned. Skipping writing output") if __name__ == "__main__": cli() home-assistant-libs-ha-ffmpeg-5c3fd21/test/tools_version.py000066400000000000000000000007051471341100100240710ustar00rootroot00000000000000import asyncio import logging import click from haffmpeg.tools import FFVersion logging.basicConfig(level=logging.DEBUG) @click.command() @click.option("--ffmpeg", "-f", default="ffmpeg", help="FFmpeg binary") def cli(ffmpeg): """FFMPEG version.""" async def get_version(): ffversion = FFVersion(ffmpeg_bin=ffmpeg) print(await ffversion.get_version()) asyncio.run(get_version()) if __name__ == "__main__": cli() home-assistant-libs-ha-ffmpeg-5c3fd21/tox.ini000066400000000000000000000003511471341100100211430ustar00rootroot00000000000000[tox] envlist = lint skip_missing_interpreters = True [testenv] [testenv:lint] basepython = python3 ignore_errors = True deps = -rrequirements.txt -rrequirements_lint.txt commands = flake8 haffmpeg/ pylint haffmpeg