diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 22bb3930..0594c9c6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -21,7 +21,7 @@ Steps to reproduce the behavior: A clear and concise description of what you expected to happen. **Desktop/Server/Software (please complete the following information):** - - OS: [e.g. debian 10] + - OS: [e.g. debian 11] - python version - ffmpeg version - are you using the current master of ffplayout? diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index fb99e5bd..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,67 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '38 5 * * 3' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml deleted file mode 100644 index e0abd6cf..00000000 --- a/.github/workflows/pythonapp.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Python application - -on: [push] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.9 - uses: actions/setup-python@v1 - with: - python-version: 3.9 - - name: Install dependencies - run: | - sudo apt update - sudo apt install ffmpeg - python -m pip install --upgrade pip - pip install -r requirements-base.txt - pip install -r requirements-dev.txt - - name: Lint with flake8 - run: | - pip install flake8 - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - pytest -vv diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 192e8a08..00000000 --- a/.pylintrc +++ /dev/null @@ -1,606 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Specify a score threshold to be exceeded before program exits with error. -fail-under=10.0 - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - too-few-public-methods, - logging-fstring-interpolation, - too-many-instance-attributes, - missing-function-docstring, - import-error, - consider-using-with - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'error', 'warning', 'refactor', and 'convention' -# which contain the number of messages in each category, as well as 'statement' -# which is the total number of statements analyzed. This score is used by the -# global evaluation report (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=new - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no - -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expression of note tags to take in consideration. -#notes-rgx= - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members=LINGER,REQ,ROUTER,NOBLOCK - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: de_AT (hunspell), de_BE -# (hunspell), de_CH (hunspell), de_DE (hunspell), de_LI (hunspell), de_LU -# (hunspell), en_AG (hunspell), en_AU (hunspell), en_BS (hunspell), en_BW -# (hunspell), en_BZ (hunspell), en_CA (hunspell), en_DK (hunspell), en_GB -# (hunspell), en_GH (hunspell), en_HK (hunspell), en_IE (hunspell), en_IN -# (hunspell), en_JM (hunspell), en_MW (hunspell), en_NA (hunspell), en_NG -# (hunspell), en_NZ (hunspell), en_PH (hunspell), en_SG (hunspell), en_TT -# (hunspell), en_US (hunspell), en_ZA (hunspell), en_ZM (hunspell), en_ZW -# (hunspell). -spelling-dict=en_US - -# List of comma separated words that should not be checked. -spelling-ignore-words=rtmp,srs,supervisord,xmlrpclib,systemd,zmq,ffmpeg,CSS,JWT, - auth,Django,django,ffplayout,startproject,playlist,playlists, - http,www,init,json,cmd,config,configs,loudnorm,stderr,stdout, - ctrl,ffprobe,yaml,HH,MM,SS,libs,mediainfo,formatter,hls,HLS, - realtime,cutted,pillarbox,deinterlacing,drawtext,pre,rtp,svt, - ffplay,codecs - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=15 - -# Maximum number of locals for function / method body. -max-locals=25 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=60 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 88178a5b..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,26 +0,0 @@ -How to contribute to ffplayout engine ------ - -#### Did you need general help? - -- Search in all issues if your question was already ask. -- Please give a detailed explanation of your problem and your goal. - - Give as much information as you can, like: - 1. which system you are using - 2. python version - 3. ffmpeg version and libs - 4. your ffplayout.yml config file - 5. your log files -- Ask your question in a way, that we don't need to ask for more details and background. - -#### Did you found a bug? - -Try first the main branch, if this bug still exists there use the **Bug Report** issue template and fill up everything. - -#### You have a feature request? - -Please use the **Feature Request** issue template and fill up everything. - -#### You want to make a pull request? -That is wonderful! But please use the same code style. This project tries to be PEP8 conform. -If you add new functions, create also a [test](https://github.com/ffplayout/ffplayout_engine/tree/master/tests) for it. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702d..00000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - 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 3 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. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/CONFIG.md b/docs/CONFIG.md deleted file mode 100644 index 935c3a3b..00000000 --- a/docs/CONFIG.md +++ /dev/null @@ -1,211 +0,0 @@ -The configuration file **ffplayout.yml** has this sections: - ---- - -```YAML -general: - stop_threshold: 11 -``` -Sometimes it can happen, that a file is corrupt but still playable, -this can produce an streaming error over all following files. The only way -in this case is, to stop ffplayout and start it again. Here we only say when -it stops, the starting process is in your hand. Best way is a **systemd service** -on linux. `stop_threshold` stop ffplayout, if it is async in time above this -value. A number below 3 can cause unexpected errors. - ---- - -```YAML -mail: - subject: "Playout Error" - smpt_server: "mail.example.org" - smpt_port: 587 - sender_addr: "ffplayout@example.org" - sender_pass: "12345" - recipient: - mail_level: "ERROR" -``` -Send error messages to email address, like: -- missing playlist -- invalid json format -- missing clip path - -leave recipient blank, if you don't need this. -`mail_level` can be: **WARNING, ERROR** - ---- - -```YAML -logging: - log_to_file: True - backup_count: 7 - log_path: "/var/log/ffplayout/" - log_level: "DEBUG" - ffmpeg_level: "ERROR" -``` - -Logging to file, if `log_to_file = False` > log to console. -`backup_count` says how long log files will be saved in days. -Path to **/var/log/** only if you run this program as *deamon*. -`log_level` can be: **DEBUG, INFO, WARNING, ERROR** -`ffmpeg_level` can be: **INFO, WARNING, ERROR** - ---- - -```YAML -processing: - width: 1024 - height: 576 - aspect: 1.778 - fps: 25 - add_logo: True - logo: "docs/logo.png" - logo_scale: "100:-1" - logo_opacity: 0.7 - logo_filter: "overlay=W-w-12:12" - add_loudnorm: False - loud_I: -18 - loud_TP: -1.5 - loud_LRA: 11 - output_count: 1 -``` - -ffmpeg pre-compression settings, all clips get prepared in that way, -so the input for the final compression is unique. -- `aspect` mus be a float number. -- with `logo_scale = 100:-1` logo can be scaled -- with `logo_opacity` logo can make transparent -- with `logo_filter = overlay=W-w-12:12` you can modify the logo position -- with `use_loudnorm` you can activate single pass EBU R128 loudness normalization -- `loud_*` can adjust the loudnorm filter -- `output_count` sets the outputs for the filtering, > 1 gives the option to use the same filters for multiple outputs. This outputs can be taken in 'stream_param', names will be vout2, vout3; -aout2, aout2 etc. - -**INFO:** output is progressive! - ---- - -```YAML -ingest: - stream_input: >- - -f live_flv - -listen 1 - -i rtmp://localhost:1936/live/stream -``` -**ingest** works only in combination with output -> mode = **live_switch**! -It run a server for a ingest stream. This stream will override the normal streaming -until is done. -There is no authentication, this is up to you. The recommend way is to set address to localhost, stream to a local server with authentication and from there stream to this app. - ---- - -```YAML -play: - mode: playlist -``` -Set playing mode, like **playlist**; **folder**, or your own custom one. - ---- - -```YAML -playlist: - path: "/playlists" - day_start: "5:59:25" - length: "24:00:00" -``` - -Put only the root path here, for example: **"/playlists"**. -Subfolders is read by the script and needs this structur: -- **"/playlists/2018/01"** (/playlists/year/month) - -`day_start` means at which time the playlist should start. Leave `day_start` blank when playlist should always start at the begin. -`length` represent the target length from playlist, when is blank real length will not consider. - ---- - -```YAML -storage: - path: "/mediaStorage" - filler_clip: "/mediaStorage/filler/filler.mp4" - extensions: - - ".mp4" - - ".mkv" - shuffle: True -``` -Play ordered or ramdomly files from path, `filler_clip` is for fill the end -to reach 24 hours, it will loop when is necessary. `extensions:` search only files -with this extension, add as many as you want. Set `shuffle` to **True** to pick files randomly. - ---- - -```YAML -text: - add_text: True - over_pre: False - bind_address: "tcp://127.0.0.1:5555" - fontfile: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" - text_from_filename: False - style: "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4" - regex: "^(.*)_" -``` -Overlay text in combination with [messenger](https://github.com/ffplayout/messenger) or the web [frontend](https://github.com/ffplayout/ffplayout-frontend). -On windows `fontfile` path need to be like this: **C\:/WINDOWS/fonts/DejaVuSans.ttf**. -In a standard environment the filter drawtext node is: **Parsed_drawtext_2**. -`over_pre` if True text will be overlay in pre processing. Continue same text -over multiple files is in that mode not possible. -`text_from_filename` activate the extraction from text of a filename. With `style` you can define the drawtext parameters like position, color, etc. Post Text over API will override this. -With `regex` you can format file names, to get a title from it. - ---- - -```YAML -out: - mode: 'stream' - preview: False - preview_param: >- - -s 512+288 - -c:v libx264 - -crf 24 - -x264-params keyint=50:min-keyint=25:scenecut=-1 - -maxrate 800k - -bufsize 1600k - -preset ultrafast - -profile:v Main - -level 3.1 - -c:a aac - -ar 44100 - -b:a 128k - -flags +global_header - -f flv rtmp://preview.local/live/stream - stream_param: >- - -c:v libx264 - -crf 23 - -x264-params keyint=50:min-keyint=25:scenecut=-1 - -maxrate 1300k - -bufsize 2600k - -preset medium - -profile:v Main - -level 3.1 - -c:a aac - -ar 44100 - -b:a 128k - -flags +global_header - -f flv rtmp://localhost/live/stream -``` - -The final ffmpeg post compression, Set the settings to your needs! -`mode` has the standard options **desktop**, **hls**, **live_switch**, **stream**. Self made outputs -can be define, by adding script in output folder with an **output()** function inside. -'preview' works only in streaming output and creates a separate preview stream. - -For output mode hls, output can look like: - -```YAML - [...] - -flags +cgop - -f hls - -hls_time 6 - -hls_list_size 600 - -hls_flags append_list+delete_segments+omit_endlist+program_date_time - -hls_segment_filename /var/www/srs/live/stream-%09d.ts /var/www/srs/live/stream.m3u8 -``` diff --git a/docs/INSTALL.md b/docs/INSTALL.md deleted file mode 100644 index 6c5a442b..00000000 --- a/docs/INSTALL.md +++ /dev/null @@ -1,55 +0,0 @@ -**ffplayout_engine Installation** -================ - -Here are a description on how to install *ffplayout engine* on a standard Linux server. - -Requirements ------ - -- python version 3.7+ -- **ffmpeg v4.2+** and **ffprobe** - -Installation ------ - -- install **ffmpeg**, **ffprobe** (and **ffplay** if you need the preview mode) -- clone repo to **/opt/**: `git clone https://github.com/ffplayout/ffplayout_engine.git` -- `cd /opt/ffplayout_engine` -- create virtual environment: `virtualenv -p python3 venv` -- run `source ./venv/bin/activate` -- install dependencies: `pip3 install -r requirements.txt` -- create logging folder: **/var/log/ffplayout** -- create playlists folder, in that format: **/playlists/year/month** -- create folder for media storage: **/tv-media** -- set variables in config file to your needs - -Single Channel Setup ------ - -**systemd** is required - -- copy **docs/ffplayout_engine.service** to **/etc/systemd/system/** -- copy **ffplayout.yml** to **/etc/ffplayout/** -- change user and group in service file (for example to **www-data**) -- activate service: `sudo systemctl enable ffplayout_engine` -- edit **/etc/ffplayout/ffplayout.yml** -- when playlists are exists, run service: `sudo systemctl start ffplayout_engine` - -Multi Channel Setup ------ - -- copy **docs/ffplayout_engine-multichannel.service** to **/etc/systemd/system/** -- change user and group in service file (for example to **www-data**) -- copy **ffplayout.yml** to **/etc/ffplayout/ffplayout-001.yml** -- copy **docs/supervisor** folder to **/etc/ffplayout/** -- every channel needs its own engine config **ffplayout-002.yml**, **ffplayout-003.yml**, etc. -- every channel needs also its own service file under **/etc/ffplayout/supervisor/config.d** -- create for every channel a subfolder for logging: **/var/log/ffplayout/channel-001**, **/var/log/ffplayout/channel-002**, etc. -- edit **/etc/ffplayout/ffplayout-00*.yml** -- when you want to use the web frontend, create only the first channel and the other ones in the frontend -- activate service: `sudo systemctl enable ffplayout_engine-multichannel` -- when playlists are exists, run service: `sudo systemctl start ffplayout_engine-multichannel` - -Using it Without Installation ------ -Of course you can just run it too. Install only the dependencies from **requirements.txt** and run it with **python ffplayout.py [parameters]**. diff --git a/docs/ffplayout_engine-multichannel.service b/docs/ffplayout_engine-multichannel.service deleted file mode 100644 index 4b00abb1..00000000 --- a/docs/ffplayout_engine-multichannel.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=Supervisor process control system for UNIX -Documentation=http://supervisord.org -After=network.target - -[Service] -ExecStart=/opt/ffplayout_engine/venv/bin/supervisord -n -c /etc/ffplayout/supervisor/supervisord.conf -ExecStop=/opt/ffplayout_engine/venv/bin/supervisorctl $OPTIONS shutdown -ExecReload=/opt/ffplayout_engine/venv/bin/supervisorctl $OPTIONS reload -KillMode=process -Restart=on-failure -RestartSec=5s -User=root -Group=root - -[Install] -WantedBy=multi-user.target diff --git a/docs/ffplayout_engine.service b/docs/ffplayout_engine.service deleted file mode 100644 index f7b8031b..00000000 --- a/docs/ffplayout_engine.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=python and ffmpeg based playout -After=network.target - -[Service] -ExecStart=/opt/ffplayout_engine/venv/bin/python /opt/ffplayout_engine/ffplayout.py -ExecReload=/bin/kill -1 $MAINPID -Restart=always -RestartSec=1 -User=root -Group=root - -[Install] -WantedBy=multi-user.target diff --git a/docs/logo.png b/docs/logo.png deleted file mode 100644 index 37892844..00000000 Binary files a/docs/logo.png and /dev/null differ diff --git a/docs/supervisor/README.md b/docs/supervisor/README.md deleted file mode 100644 index cd66a383..00000000 --- a/docs/supervisor/README.md +++ /dev/null @@ -1,11 +0,0 @@ -SupervisorD ------ - -The supervisor config is only needed when you want to run multiple channels. - -Every channel has his own config in [conf.d](/supervisor/conf.d/) folder. In the configuration you have to change this line: - -``` -command=./venv/bin/python3 ffplayout.py -c /etc/ffplayout/ffplayout-001.yml -``` -to the correct ffpalyout YAML config file. diff --git a/docs/supervisor/conf.d/engine-001.conf b/docs/supervisor/conf.d/engine-001.conf deleted file mode 100644 index f956fe5b..00000000 --- a/docs/supervisor/conf.d/engine-001.conf +++ /dev/null @@ -1,11 +0,0 @@ -[program:engine-001] -directory=/opt/ffplayout_engine -command=/opt/ffplayout_engine/venv/bin/python ffplayout.py -c /etc/ffplayout/ffplayout-001.yml -redirect_stderr=true -stdout_logfile=/var/log/ffplayout/engine-001.log -killasgroup=true -stopasgroup=true -autorestart=true -autostart=true -startsecs=2 -startretries=10 diff --git a/docs/supervisor/supervisord.conf b/docs/supervisor/supervisord.conf deleted file mode 100644 index 40979171..00000000 --- a/docs/supervisor/supervisord.conf +++ /dev/null @@ -1,19 +0,0 @@ -[supervisord] -pidfile=/tmp/supervisord.pid -nodaemon=true -logfile=/dev/null -logfile_maxbytes=0 - -[include] -files = conf.d/*.conf - -[inet_http_server] -port=127.0.0.1:9001 -username = ffplayout -password = hsF0wQkl5zopEy1mBlT3g - -[supervisorctl] -serverurl=http://127.0.0.1:9001 - -[rpcinterface:supervisor] -supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface diff --git a/ffplayout.py b/ffplayout.py deleted file mode 100755 index eef369ad..00000000 --- a/ffplayout.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 - -# This file is part of ffplayout. -# -# ffplayout 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 3 of the License, or -# (at your option) any later version. -# -# ffplayout 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. -# -# You should have received a copy of the GNU General Public License -# along with ffplayout. If not, see . - -# ------------------------------------------------------------------------------ - -""" -This module is the starting program for running ffplayout engine. -""" - -from importlib import import_module -from pathlib import Path -from platform import system - -from ffplayout.utils import messenger, playout, validate_ffmpeg_libs - -try: - if system() == 'Windows': - import colorama - colorama.init() -except ImportError: - print('colorama import failed, no colored console output on windows...') - - -# ------------------------------------------------------------------------------ -# main functions -# ------------------------------------------------------------------------------ - -def main(): - """ - play out depending on output mode - """ - - script_dir = Path(__file__).parent.absolute() - output_dir = script_dir.joinpath('ffplayout', 'output') - mode_exists = False - - for output in output_dir.glob('*.py'): - if output != '__init__.py': - mode = Path(output).stem - - if mode == playout.mode: - mode_exists = True - output = import_module(f'ffplayout.output.{mode}').output - output() - - if not mode_exists: - messenger.error('Output mode not exist!') - - -if __name__ == '__main__': - # check if ffmpeg contains all codecs and filters - validate_ffmpeg_libs() - main() diff --git a/ffplayout.yml b/ffplayout.yml deleted file mode 100644 index aca105de..00000000 --- a/ffplayout.yml +++ /dev/null @@ -1,149 +0,0 @@ -general: - helptext: Sometimes it can happen, that a file is corrupt but still playable, - this can produce an streaming error over all following files. The only way - in this case is, to stop ffplayout and start it again. Here we only say when - it stops, the starting process is in your hand. Best way is a systemd service - on linux. 'stop_threshold' stop ffplayout, if it is async in time above this - value. A number below 3 can cause unexpected errors. - stop_threshold: 11 - -mail: - helptext: Send error messages to email address, like missing playlist; invalid - json format; missing clip path. Leave recipient blank, if you don't need this. - 'mail_level' can be WARNING or ERROR. - subject: "Playout Error" - smtp_server: "mail.example.org" - smtp_port: 587 - sender_addr: "ffplayout@example.org" - sender_pass: "abc123" - recipient: - mail_level: "ERROR" - -logging: - helptext: Logging to file, if 'log_to_file' false log to console. 'backup_count' - says how long log files will be saved in days. Path to /var/log/ only if you - run this program as daemon. 'log_level' can be DEBUG, INFO, WARNING, - ERROR. 'ffmpeg_level' can be info, warning, error. - log_to_file: true - backup_count: 7 - log_path: "/var/log/ffplayout/" - log_level: "DEBUG" - ffmpeg_level: "error" - -processing: - helptext: Set playing mode, like playlist; folder, or you own custom one. - Default processing, for all clips that they get prepared in that way, - so the output is unique. 'aspect' must be a float number. 'logo' is only used - if the path exist. 'logo_scale' scale the logo to target size, leave it blank - when no scaling is needed, format is 'number:number', for example '100:-1' - for proportional scaling. With 'logo_opacity' logo can become transparent. - With 'logo_filter' 'overlay=W-w-12:12' you can modify the logo position. - With 'use_loudnorm' you can activate single pass EBU R128 loudness normalization. - 'loud_*' can adjust the loudnorm filter. 'output_count' sets the outputs for - the filtering, > 1 gives the option to use the same filters for multiple outputs. - This outputs can be taken in 'stream_param', names will be vout2, vout3; - aout2, aout2 etc. - mode: playlist - width: 1024 - height: 576 - aspect: 1.778 - fps: 25 - add_logo: true - logo: "docs/logo.png" - logo_scale: - logo_opacity: 0.7 - logo_filter: "overlay=W-w-12:12" - add_loudnorm: false - loud_i: -18 - loud_tp: -1.5 - loud_lra: 11 - output_count: 1 - -ingest: - helptext: Works not with direct hls output, it always needs full processing! Run a server - for a ingest stream. This stream will override the normal streaming until is done. - There is no authentication, this is up to you. The recommend way is to set address - to localhost, stream to a local server with authentication and from there stream to this app. - enable: false - input_param: -f live_flv -listen 1 -i rtmp://localhost:1936/live/stream - -playlist: - helptext: > - 'path' can be a path to a single file, or a directory. For directory put - only the root folder, for example '/playlists', subdirectories are read by the - script. Subdirectories needs this structure '/playlists/2018/01'. 'day_start' - means at which time the playlist should start, leave day_start blank when playlist - should always start at the begin. 'length' represent the target length from - playlist, when is blank real length will not consider. 'infinit true' works with - single playlist file and loops it infinitely. - path: "/playlists" - day_start: "5:59:25" - length: "24:00:00" - infinit: false - -storage: - helptext: Play ordered or randomly files from path. 'filler_clip' is for fill - the end to reach 24 hours, it will loop when is necessary. 'extensions' search - only files with this extension. Set 'shuffle' to 'true' to pick files randomly. - path: "/mediaStorage" - filler_clip: "/mediaStorage/filler/filler.mp4" - extensions: - - ".mp4" - - ".mkv" - shuffle: true - -text: - helptext: Overlay text in combination with libzmq for remote text manipulation. - On windows fontfile path need to be like this 'C\:/WINDOWS/fonts/DejaVuSans.ttf'. - In a standard environment the filter drawtext node is Parsed_drawtext_2. - 'over_pre' if true text will be overlay in pre processing. Continue same text - over multiple files is in that mode not possible. 'text_from_filename' activate the - extraction from text of a filename. With 'style' you can define the drawtext - parameters like position, color, etc. Post Text over API will override this. - With 'regex' you can format file names, to get a title from it. - add_text: false - over_pre: false - bind_address: "127.0.0.1:5555" - fontfile: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" - text_from_filename: false - style: "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4" - regex: "^(.*)_" - -out: - helptext: The final playout compression. Set the settings to your needs. - 'mode' has the standard options 'desktop', 'hls', 'stream'. Self made - outputs can be define, by adding script in output folder with an 'output' function - inside. 'preview' works only in streaming output and creates a separate preview stream. - mode: 'stream' - preview: false - preview_param: >- - -s 512x288 - -c:v libx264 - -crf 24 - -x264-params keyint=50:min-keyint=25:scenecut=-1 - -maxrate 800k - -bufsize 1600k - -preset ultrafast - -tune zerolatency - -profile:v Main - -level 3.1 - -c:a aac - -ar 44100 - -b:a 128k - -flags +global_header - -f flv rtmp://preview.local/live/stream - output_param: >- - -c:v libx264 - -crf 23 - -x264-params keyint=50:min-keyint=25:scenecut=-1 - -maxrate 1300k - -bufsize 2600k - -preset faster - -tune zerolatency - -profile:v Main - -level 3.1 - -c:a aac - -ar 44100 - -b:a 128k - -flags +global_header - -f flv rtmp://localhost/live/stream diff --git a/ffplayout/__init__.py b/ffplayout/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ffplayout/conf.d/README.md b/ffplayout/conf.d/README.md deleted file mode 100644 index a78f5971..00000000 --- a/ffplayout/conf.d/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Custom Configuration - -Extend your arguments for using them in your custom extensions. - -The file name must have the **argparse_** prefix. The content should look like: - -```YAML -short: -v -long: --volume -help: set audio volume -``` - -At least **short** or **long** have to exist, all other parameters are optional. You can also extend the config, with keys which are exist in **ArgumentParser.add_argument()**. - -**Every argument must have its own yaml file!** diff --git a/ffplayout/conf.d/argparse_volume.yml b/ffplayout/conf.d/argparse_volume.yml deleted file mode 100644 index a015200e..00000000 --- a/ffplayout/conf.d/argparse_volume.yml +++ /dev/null @@ -1,3 +0,0 @@ -short: -v -long: --volume -help: set audio volume diff --git a/ffplayout/filters/README.md b/ffplayout/filters/README.md deleted file mode 100644 index 3f4eadb3..00000000 --- a/ffplayout/filters/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Custom Filters - -Add your one filters here. They must have the correct file naming: - -- for audio filter: a_[filter name].py -- for video filter: v_[filter name].py - -The file itself should contain only one filter in a function named `def filter_link(prope):` - -Check **v_addtext.py** for example. - -In your filter you can also read custom properties from the current program node. That you can use for any usecase you wish, like reading a subtitle file, or a different logo for every clip and so on. - -The normal program node looks like: - -```JSON -{ - "in": 0, - "out": 3600.162, - "duration": 3600.162, - "source": "/dir/input.mp4" -} -``` - -This you can extend to your needs, and apply this values to your filters. diff --git a/ffplayout/filters/__init__.py b/ffplayout/filters/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ffplayout/filters/a_volume.py b/ffplayout/filters/a_volume.py deleted file mode 100644 index 15097977..00000000 --- a/ffplayout/filters/a_volume.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -custom audio filter, which get loaded automatically -""" - -from ..utils import get_float, stdin_args - - -# pylint: disable=unused-argument -def filter_link(node): - """ - set audio volume - """ - - if stdin_args.volume and get_float(stdin_args.volume, False): - return f'volume={stdin_args.volume}' - - return None diff --git a/ffplayout/filters/default.py b/ffplayout/filters/default.py deleted file mode 100644 index c9bc80a2..00000000 --- a/ffplayout/filters/default.py +++ /dev/null @@ -1,342 +0,0 @@ -# This file is part of ffplayout. -# -# ffplayout 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 3 of the License, or -# (at your option) any later version. -# -# ffplayout 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. -# -# You should have received a copy of the GNU General Public License -# along with ffplayout. If not, see . - -# ------------------------------------------------------------------------------ - -""" -This module prepare all ffmpeg filters. -This is mainly for unify clips to have a unique output. -""" - -import math -import re -from importlib import import_module -from pathlib import Path - -from ..utils import (get_float, is_advertisement, lower_third, messenger, pre, - sync_op) - -# ------------------------------------------------------------------------------ -# building filters, -# when is needed add individual filters to match output format -# ------------------------------------------------------------------------------ - - -def text_filter(): - """ - add drawtext filter for lower thirds messages - """ - filter_chain = [] - font = '' - - if lower_third.add_text and lower_third.over_pre: - if lower_third.fontfile and Path(lower_third.fontfile).is_file(): - font = f":fontfile='{lower_third.fontfile}'" - filter_chain = [ - "null,zmq=b=tcp\\\\://'{}',drawtext=text=''{}".format( - lower_third.address, font)] - - return filter_chain - - -def deinterlace_filter(probe): - """ - when material is interlaced, - set deinterlacing filter - """ - filter_chain = [] - - if 'field_order' in probe.video[0] and \ - probe.video[0]['field_order'] != 'progressive': - filter_chain.append('yadif=0:-1:0') - - return filter_chain - - -def pad_filter(probe): - """ - if source and target aspect is different, - fix it with pillarbox or letterbox - """ - filter_chain = [] - - if not math.isclose(probe.video[0]['aspect'], - pre.aspect, abs_tol=0.03): - if probe.video[0]['aspect'] < pre.aspect: - filter_chain.append( - f'pad=ih*{pre.w}/{pre.h}/sar:ih:(ow-iw)/2:(oh-ih)/2') - elif probe.video[0]['aspect'] > pre.aspect: - filter_chain.append( - f'pad=iw:iw*{pre.h}/{pre.w}/sar:(ow-iw)/2:(oh-ih)/2') - - return filter_chain - - -def fps_filter(probe): - """ - changing frame rate - """ - filter_chain = [] - - if probe.video[0]['fps'] != pre.fps: - filter_chain.append(f'fps={pre.fps}') - - return filter_chain - - -def scale_filter(probe): - """ - if target resolution is different to source add scale filter, - apply also an aspect filter, when is different - """ - filter_chain = [] - - if int(probe.video[0]['width']) != pre.w or \ - int(probe.video[0]['height']) != pre.h: - filter_chain.append(f'scale={pre.w}:{pre.h}') - - if not math.isclose(probe.video[0]['aspect'], - pre.aspect, abs_tol=0.03): - filter_chain.append(f'setdar=dar={pre.aspect}') - - return filter_chain - - -def fade_filter(duration, seek, out, track=''): - """ - fade in/out video, when is cutted at the begin or end - """ - filter_chain = [] - - if seek > 0.0: - filter_chain.append(f'{track}fade=in:st=0:d=0.5') - - if out != duration and out - seek - 1.0 > 0: - filter_chain.append(f'{track}fade=out:st={out - seek - 1.0}:d=1.0') - - return filter_chain - - -def overlay_filter(duration, advertisement, ad_last, ad_next): - """ - overlay logo: when is an ad don't overlay, - when ad is coming next fade logo out, - when clip before was an ad fade logo in - """ - logo_filter = '[v]null' - scale = '' - - if pre.add_logo and pre.logo and Path(pre.logo).is_file() \ - and not advertisement: - logo_chain = [] - if pre.logo_scale and \ - re.match(r'\d+:-?\d+', pre.logo_scale): - scale = f'scale={pre.logo_scale},' - logo_extras = (f'format=rgba,{scale}' - f'colorchannelmixer=aa={pre.logo_opacity}') - loop = 'loop=loop=-1:size=1:start=0' - logo_chain.append(f'movie={pre.logo},{loop},{logo_extras}') - if ad_last: - logo_chain.append('fade=in:st=0:d=1.0:alpha=1') - if ad_next: - logo_chain.append(f'fade=out:st={duration - 1}:d=1.0:alpha=1') - - logo_filter = (f'{",".join(logo_chain)}[l];[v][l]' - f'{pre.logo_filter}:shortest=1') - - return logo_filter - - -def add_audio(probe, duration): - """ - when clip has no audio we generate an audio line - """ - line = [] - - if not probe.audio: - messenger.warning(f'Clip "{probe.src}" has no audio!') - line = [(f'aevalsrc=0:channel_layout=stereo:duration={duration}:' - f'sample_rate=48000')] - - return line - - -def add_loudnorm(probe): - """ - add single pass loudnorm filter to audio line - """ - loud_filter = [] - - if probe.audio and pre.add_loudnorm: - loud_filter = [ - f'loudnorm=I={pre.loud_i}:TP={pre.loud_tp}:LRA={pre.loud_lra}'] - - return loud_filter - - -def extend_audio(probe, out, seek): - """ - check audio duration, is it shorter then clip duration - pad it - """ - pad = [] - aud_dur = get_float(probe.audio[0].get('duration'), None) - - if aud_dur and out - seek > aud_dur - seek + 0.1: - pad.append(f'apad=whole_dur={out - seek}') - - return pad - - -def extend_video(probe, out, seek): - """ - check video duration, is it shorter then clip duration - pad it - """ - pad = [] - vid_dur = probe.video[0].get('duration') - - if vid_dur and out - seek > float(vid_dur) - seek + 0.1: - pad.append( - 'tpad=stop_mode=add:' - f'stop_duration={(out - seek) - (float(vid_dur) - seek)}') - - return pad - - -def realtime_filter(duration, track=''): - """ - this realtime filter is important for HLS output to stay in sync - """ - speed_filter = '' - - if sync_op.realtime: - speed_filter = f',{track}realtime=speed=1' - - if sync_op.time_delta < 0: - speed = duration / (duration + sync_op.time_delta) - - if speed < 1.1: - speed_filter = f',{track}realtime=speed={speed}' - - return speed_filter - - -def split_filter(filter_type): - """ - this filter splits the media input in multiple outputs, - to be able to have different streaming/HLS outputs - """ - map_node = [] - filter_ = '' - - prefix = 'a' if filter_type == 'a' else '' - - if pre.output_count > 1: - for num in range(pre.output_count): - map_node.append(f'[{filter_type}out{num + 1}]') - - filter_ = f',{prefix}split={pre.output_count}{"".join(map_node)}' - - else: - filter_ = f'[{filter_type}out1]' - - return filter_ - - -def custom_filter(filter_type, node): - """ - read custom filters from filters folder - """ - filter_dir = Path(__file__).parent.absolute() - filters = [] - - for filter_file in filter_dir.glob(f'{filter_type}_*'): - filter_ = Path(filter_file).stem - filter_function = import_module( - f'ffplayout.filters.{filter_}').filter_link - link = filter_function(node) - - if link is not None: - filters.append(link) - - return filters - - -def build_filtergraph(node, node_last, node_next): - """ - build final filter graph, with video and audio chain - """ - - advertisement = is_advertisement(node) - ad_last = is_advertisement(node_last) - ad_next = is_advertisement(node_next) - - duration = node['duration'] - seek = node['seek'] - out = node['out'] - probe = node['probe'] - - video_chain = [] - audio_chain = [] - - if out > duration: - seek = 0 - - if probe and probe.video[0]: - custom_v_filter = custom_filter('v', node) - video_chain += text_filter() \ - + deinterlace_filter(probe) \ - + pad_filter(probe) \ - + fps_filter(probe) \ - + scale_filter(probe) \ - + extend_video(probe, out, seek) - if custom_v_filter: - video_chain += custom_v_filter - video_chain += fade_filter(duration, seek, out) - - audio_chain += add_audio(probe, out - seek) - - if not audio_chain: - custom_a_filter = custom_filter('a', node) - - audio_chain += ['[0:a]anull'] \ - + add_loudnorm(probe) \ - + extend_audio(probe, out, seek) - if custom_a_filter: - audio_chain += custom_a_filter - audio_chain += fade_filter(duration, seek, out, 'a') - - if video_chain: - video_filter = f'{",".join(video_chain)}[v]' - else: - video_filter = 'null[v]' - - logo_filter = overlay_filter(out - seek, advertisement, ad_last, ad_next) - v_speed = realtime_filter(out - seek) - v_split = split_filter('v') - video_map = ['-map', '[vout1]'] - video_filter = [ - '-filter_complex', - f'[0:v]{video_filter};{logo_filter}{v_speed}{v_split}'] - - a_speed = realtime_filter(out - seek, 'a') - a_split = split_filter('a') - audio_map = ['-map', '[aout1]'] - audio_filter = [ - '-filter_complex', f'{",".join(audio_chain)}{a_speed}{a_split}'] - - if probe and probe.video[0]: - return video_filter + audio_filter + video_map + audio_map - - return video_filter + video_map + ['-map', '1:a'] diff --git a/ffplayout/filters/v_drawtext.py b/ffplayout/filters/v_drawtext.py deleted file mode 100644 index 01e7a966..00000000 --- a/ffplayout/filters/v_drawtext.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -custom video filter, which get loaded automatically -""" - -import re -from pathlib import Path - -from ..utils import lower_third - - -def filter_link(node): - """ - extract title from file name and overlay it - """ - font = '' - source = str(Path(node.get('source')).name) - match = re.match(lower_third.regex, source) - title = match[1] if match else source - - if lower_third.fontfile and Path(lower_third.fontfile).is_file(): - font = f":fontfile='{lower_third.fontfile}'" - - if lower_third.text_from_filename: - escape = title.replace("'", "'\\\\\\''").replace("%", "\\\\\\%") - return f"drawtext=text='{escape}':{lower_third.style}{font}" - - return None diff --git a/ffplayout/ingest_server.py b/ffplayout/ingest_server.py deleted file mode 100644 index 155dfdbd..00000000 --- a/ffplayout/ingest_server.py +++ /dev/null @@ -1,74 +0,0 @@ -# This file is part of ffplayout. -# -# ffplayout 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 3 of the License, or -# (at your option) any later version. -# -# ffplayout 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. -# -# You should have received a copy of the GNU General Public License -# along with ffplayout. If not, see . - -# ------------------------------------------------------------------------------ - -""" -Start a streaming server and forword it to the playout. -This stream will have the first priority and -play instead of the normal stream (playlist/folder). -""" -from queue import Queue -from subprocess import PIPE, Popen -from threading import Thread -from time import sleep - -from .filters.default import overlay_filter -from .utils import ff_proc, ffmpeg_stderr_reader, ingest, messenger, pre - - -def listener(que): - filter_ = (f'[0:v]fps={str(pre.fps)},scale={pre.w}:{pre.h},' - + f'setdar=dar={pre.aspect}[v];') - filter_ += overlay_filter(0, False, False, False) - - server_cmd = [ - 'ffmpeg', '-hide_banner', '-nostats', '-v', 'level+error' - ] + ingest.input_param + [ - '-filter_complex', f'{filter_}[vout1]', - '-map', '[vout1]', '-map', '0:a' - ] + pre.settings - - messenger.warning( - 'Ingest stream is experimental, use it at your own risk!') - messenger.debug(f'Server CMD: "{" ".join(server_cmd)}"') - - while True: - with Popen(server_cmd, stderr=PIPE, stdout=PIPE) as ff_proc.server: - err_thread = Thread(name='stderr_server', - target=ffmpeg_stderr_reader, - args=(ff_proc.server.stderr, '[Server]')) - err_thread.daemon = True - err_thread.start() - - while True: - buffer = ff_proc.server.stdout.read(pre.buffer_size) - if not buffer: - break - - que.put(buffer) - - sleep(.33) - - -def ingest_stream(): - streaming_queue = Queue(maxsize=0) - - rtmp_server_thread = Thread(name='ffmpeg_server',target=listener, - args=(streaming_queue,)) - rtmp_server_thread.daemon = True - rtmp_server_thread.start() - - return streaming_queue diff --git a/ffplayout/output/README.md b/ffplayout/output/README.md deleted file mode 100644 index c5c168a1..00000000 --- a/ffplayout/output/README.md +++ /dev/null @@ -1,5 +0,0 @@ -## Outputs -ffplayout has a modularized output system, which mean you can write your own output function. Just create a python file in this folder with an **output()** function in it. In this function you ca do what ever you want. Use the other output files as references. - -#### Activating Output -To use one of the outputs you need to edit the **ffplayout.yml** config, here under **out** set your **mode** to the file name, without extension. if you need it feel free to extend the config to your needs. diff --git a/ffplayout/output/__init__.py b/ffplayout/output/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ffplayout/output/desktop.py b/ffplayout/output/desktop.py deleted file mode 100644 index e99cbd49..00000000 --- a/ffplayout/output/desktop.py +++ /dev/null @@ -1,142 +0,0 @@ -# This file is part of ffplayout. -# -# ffplayout 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 3 of the License, or -# (at your option) any later version. -# -# ffplayout 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. -# -# You should have received a copy of the GNU General Public License -# along with ffplayout. If not, see . - -# ------------------------------------------------------------------------------ - -""" -This module plays the compressed output directly on the desktop. -""" - -from importlib import import_module -from subprocess import PIPE, Popen -from threading import Thread - -from ..ingest_server import ingest_stream -from ..utils import (ff_proc, ffmpeg_stderr_reader, ingest, - log, lower_third, messenger, pre, terminate_processes) - - -def output(): - """ - this output is for playing on desktop with ffplay - """ - overlay = [] - live_on = False - stream_queue = None - - if ingest.enable: - stream_queue = ingest_stream() - - if lower_third.add_text and not lower_third.over_pre: - messenger.info( - f'Using drawtext node, listening on address: {lower_third.address}' - ) - overlay = [ - '-vf', - "null,zmq=b=tcp\\\\://'{}',drawtext=text='':fontfile='{}'".format( - lower_third.address, lower_third.fontfile) - ] - - try: - enc_cmd = [ - 'ffplay', '-hide_banner', '-nostats', - '-v', f'level+{log.ff_level}', '-i', 'pipe:0' - ] + overlay - - messenger.debug(f'Encoder CMD: "{" ".join(enc_cmd)}"') - - ff_proc.encoder = Popen(enc_cmd, stderr=PIPE, stdin=PIPE, stdout=None) - - enc_err_thread = Thread(target=ffmpeg_stderr_reader, - args=(ff_proc.encoder.stderr, '[Encoder]')) - enc_err_thread.daemon = True - enc_err_thread.start() - - Iter = import_module(f'ffplayout.player.{pre.mode}').GetSourceIter - get_source = Iter() - - try: - for node in get_source.next(): - messenger.info( - f'Play for {node["out"] - node["seek"]:.2f} ' - f'seconds: {node.get("source")}') - - dec_cmd = [ - 'ffmpeg', '-v', f'level+{log.ff_level}', - '-hide_banner', '-nostats' - ] + node['src_cmd'] + node['filter'] + pre.settings - - messenger.debug(f'Decoder CMD: "{" ".join(dec_cmd)}"') - - kill_dec = True - - with Popen( - dec_cmd, stdout=PIPE, stderr=PIPE) as ff_proc.decoder: - dec_err_thread = Thread(target=ffmpeg_stderr_reader, - args=(ff_proc.decoder.stderr, - '[Decoder]')) - dec_err_thread.daemon = True - dec_err_thread.start() - - while True: - if stream_queue and not stream_queue.empty(): - if kill_dec: - kill_dec = False - live_on = True - get_source.first = True - - messenger.info( - "Switch from offline source to live ingest") - - if ff_proc.decoder.poll() is None: - ff_proc.decoder.kill() - ff_proc.decoder.wait() - - buf_live = stream_queue.get() - ff_proc.encoder.stdin.write(buf_live) - else: - if live_on: - messenger.info( - "Switch from live ingest to offline source") - kill_dec = True - live_on = False - - buf_dec = ff_proc.decoder.stdout.read( - pre.buffer_size) - if buf_dec: - ff_proc.encoder.stdin.write(buf_dec) - else: - break - - except BrokenPipeError: - messenger.error('Broken Pipe!') - terminate_processes(getattr(get_source, 'stop', None)) - - except SystemExit: - messenger.info('Got close command') - terminate_processes(getattr(get_source, 'stop', None)) - - except KeyboardInterrupt: - messenger.warning('Program terminated') - terminate_processes(getattr(get_source, 'stop', None)) - - # close encoder when nothing is to do anymore - if ff_proc.encoder.poll() is None: - ff_proc.encoder.terminate() - - finally: - if ff_proc.encoder.poll() is None: - ff_proc.encoder.terminate() - ff_proc.encoder.wait() diff --git a/ffplayout/output/hls.py b/ffplayout/output/hls.py deleted file mode 100644 index 2f676f0a..00000000 --- a/ffplayout/output/hls.py +++ /dev/null @@ -1,126 +0,0 @@ -# This file is part of ffplayout. -# -# ffplayout 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 3 of the License, or -# (at your option) any later version. -# -# ffplayout 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. -# -# You should have received a copy of the GNU General Public License -# along with ffplayout. If not, see . - -# ------------------------------------------------------------------------------ - -""" -This module write the files compression directly to a hls (m3u8) playlist, -without pre- and post-processing. - -Example config: - -out: - stream_output: >- - -flags +cgop - -f hls - -hls_time 6 - -hls_list_size 600 - -hls_flags append_list+delete_segments+omit_endlist+program_date_time - -hls_segment_filename /var/www/srs/live/stream-%09d.ts /var/www/srs/live/stream.m3u8 - -""" - -import re -from importlib import import_module -from pathlib import Path -from subprocess import PIPE, Popen -from threading import Thread - -from ..utils import (ff_proc, ffmpeg_stderr_reader, log, messenger, playout, - pre, sync_op, terminate_processes) - - -def clean_ts(): - """ - this function get all *.m3u8 playlists from config, - read lines from them until it founds first *.ts file, - then it checks if files on hard drive are older then this first *.ts - and if so delete them - """ - m3u8_files = [p for p in playout.stream_output if 'm3u8' in p] - - for m3u8_file in m3u8_files: - messenger.debug(f'cleanup *.ts files from: "{m3u8_file}"') - test_num = 0 - hls_path = Path(m3u8_file).parent - - if Path(m3u8_file).is_file(): - with open(m3u8_file, 'r') as m3u8: - for line in m3u8: - if '.ts' in line: - test_num = int(re.findall(r'(\d+).ts', line)[0]) - break - - for ts_file in hls_path.rglob('*.ts'): - ts_num = int(re.findall(r'(\d+).ts', str(ts_file))[0]) - - if test_num > ts_num: - ts_file.unlink(missing_ok=True) - - -def output(): - """ - this output is hls output, no pre-process is needed. - """ - sync_op.realtime = True - - try: - Iter = import_module(f'ffplayout.player.{pre.mode}').GetSourceIter - get_source = Iter() - - try: - for node in get_source.next(): - messenger.info(f'Play: {node.get("source")}') - - cmd = [ - 'ffmpeg', '-v', f'level+{log.ff_level}', - '-hide_banner', '-nostats' - ] + node['src_cmd'] + node['filter'] + playout.output_param - - messenger.debug(f'Encoder CMD: "{" ".join(cmd)}"') - - ff_proc.encoder = Popen(cmd, stdin=PIPE, stderr=PIPE) - - stderr_reader_thread = Thread(target=ffmpeg_stderr_reader, - args=(ff_proc.encoder.stderr, - '[Encoder]')) - stderr_reader_thread.daemon = True - stderr_reader_thread.start() - stderr_reader_thread.join() - - ts_cleaning_thread = Thread(target=clean_ts) - ts_cleaning_thread.daemon = True - ts_cleaning_thread.start() - - except BrokenPipeError: - messenger.error('Broken Pipe!') - terminate_processes(getattr(get_source, 'stop', None)) - - except SystemExit: - messenger.info('Got close command') - terminate_processes(getattr(get_source, 'stop', None)) - - except KeyboardInterrupt: - messenger.warning('Program terminated') - terminate_processes(getattr(get_source, 'stop', None)) - - # close encoder when nothing is to do anymore - if ff_proc.encoder.poll() is None: - ff_proc.encoder.terminate() - - finally: - if ff_proc.encoder.poll() is None: - ff_proc.encoder.terminate() - ff_proc.encoder.wait() diff --git a/ffplayout/output/null.py b/ffplayout/output/null.py deleted file mode 100644 index 129751b1..00000000 --- a/ffplayout/output/null.py +++ /dev/null @@ -1,134 +0,0 @@ -# This file is part of ffplayout. -# -# ffplayout 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 3 of the License, or -# (at your option) any later version. -# -# ffplayout 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. -# -# You should have received a copy of the GNU General Public License -# along with ffplayout. If not, see . - -# ------------------------------------------------------------------------------ - -""" -This module streams to -f null, so it is only for debugging. -""" - -from importlib import import_module -from subprocess import PIPE, Popen -from threading import Thread - -from ..ingest_server import ingest_stream -from ..utils import (ff_proc, ffmpeg_stderr_reader, ingest, - log, messenger, playout, pre, terminate_processes) - - -def output(): - """ - this output is for streaming to a target address, - like rtmp, rtp, svt, etc. - """ - live_on = False - stream_queue = None - - if ingest.enable: - stream_queue = ingest_stream() - - messenger.info(f'Stream to null output, only usefull for debugging...') - - try: - enc_cmd = [ - 'ffmpeg', '-v', f'level+{log.ff_level}', '-hide_banner', - '-nostats', '-re', '-thread_queue_size', '160', '-i', 'pipe:0' - ] + playout.output_param[:-3] + ['-f', 'null', '-'] - - messenger.debug(f'Encoder CMD: "{" ".join(enc_cmd)}"') - - ff_proc.encoder = Popen(enc_cmd, stdin=PIPE, stderr=PIPE) - - enc_err_thread = Thread(target=ffmpeg_stderr_reader, - args=(ff_proc.encoder.stderr, '[Encoder]')) - enc_err_thread.daemon = True - enc_err_thread.start() - - Iter = import_module(f'ffplayout.player.{pre.mode}').GetSourceIter - get_source = Iter() - - try: - for node in get_source.next(): - messenger.info( - f'Play for {node["out"] - node["seek"]:.2f} ' - f'seconds: {node.get("source")}') - - dec_cmd = [ - 'ffmpeg', '-v', f'level+{log.ff_level}', - '-hide_banner', '-nostats' - ] + node['src_cmd'] + node['filter'] + pre.settings - - messenger.debug(f'Decoder CMD: "{" ".join(dec_cmd)}"') - - kill_dec = True - - with Popen( - dec_cmd, stdout=PIPE, stderr=PIPE) as ff_proc.decoder: - dec_err_thread = Thread(target=ffmpeg_stderr_reader, - args=(ff_proc.decoder.stderr, - '[Decoder]')) - dec_err_thread.daemon = True - dec_err_thread.start() - - while True: - if stream_queue and not stream_queue.empty(): - if kill_dec: - kill_dec = False - live_on = True - get_source.first = True - - messenger.info( - "Switch from offline source to live ingest") - - if ff_proc.decoder.poll() is None: - ff_proc.decoder.kill() - ff_proc.decoder.wait() - - buf_live = stream_queue.get() - ff_proc.encoder.stdin.write(buf_live) - else: - if live_on: - messenger.info( - "Switch from live ingest to offline source") - kill_dec = True - live_on = False - - buf_dec = ff_proc.decoder.stdout.read( - pre.buffer_size) - if buf_dec: - ff_proc.encoder.stdin.write(buf_dec) - else: - break - - except BrokenPipeError: - messenger.error('Broken Pipe!') - terminate_processes(getattr(get_source, 'stop', None)) - - except SystemExit: - messenger.info('Got close command') - terminate_processes(getattr(get_source, 'stop', None)) - - except KeyboardInterrupt: - messenger.warning('Program terminated') - terminate_processes(getattr(get_source, 'stop', None)) - - # close encoder when nothing is to do anymore - if ff_proc.encoder.poll() is None: - ff_proc.encoder.kill() - - finally: - if ff_proc.encoder.poll() is None: - ff_proc.encoder.kill() - ff_proc.encoder.wait() diff --git a/ffplayout/output/stream.py b/ffplayout/output/stream.py deleted file mode 100644 index 3aa53654..00000000 --- a/ffplayout/output/stream.py +++ /dev/null @@ -1,153 +0,0 @@ -# This file is part of ffplayout. -# -# ffplayout 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 3 of the License, or -# (at your option) any later version. -# -# ffplayout 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. -# -# You should have received a copy of the GNU General Public License -# along with ffplayout. If not, see . - -# ------------------------------------------------------------------------------ - -""" -This module streams the files out to a remote target. -""" - -from importlib import import_module -from subprocess import PIPE, Popen -from threading import Thread - -from ..ingest_server import ingest_stream -from ..utils import (ff_proc, ffmpeg_stderr_reader, ingest, - log, lower_third, messenger, playout, pre, - terminate_processes) - - -def output(): - """ - this output is for streaming to a target address, - like rtmp, rtp, svt, etc. - """ - filtering = [] - node = None - dec_cmd = [] - preview = [] - live_on = False - stream_queue = None - - if ingest.enable: - stream_queue = ingest_stream() - - if lower_third.add_text and not lower_third.over_pre: - messenger.info( - f'Using drawtext node, listening on address: {lower_third.address}' - ) - filtering = [ - '-filter_complex', - f"[0:v]null,zmq=b=tcp\\\\://'{lower_third.address}'," - + f"drawtext=text='':fontfile='{lower_third.fontfile}'" - ] - - if playout.preview: - filtering[-1] += ',split=2[v_out1][v_out2]' - preview = ['-map', '[v_out1]', '-map', '0:a' - ] + playout.preview_param + ['-map', '[v_out2]', '-map', '0:a'] - - elif playout.preview: - preview = playout.preview_param - - try: - enc_cmd = [ - 'ffmpeg', '-v', f'level+{log.ff_level}', '-hide_banner', - '-nostats', '-re', '-thread_queue_size', '160', '-i', 'pipe:0' - ] + filtering + preview + playout.output_param - - messenger.debug(f'Encoder CMD: "{" ".join(enc_cmd)}"') - - ff_proc.encoder = Popen(enc_cmd, stdin=PIPE, stderr=PIPE) - - enc_err_thread = Thread(target=ffmpeg_stderr_reader, - args=(ff_proc.encoder.stderr, '[Encoder]')) - enc_err_thread.daemon = True - enc_err_thread.start() - - Iter = import_module(f'ffplayout.player.{pre.mode}').GetSourceIter - get_source = Iter() - - try: - for node in get_source.next(): - messenger.info(f'Play: {node.get("source")}') - - dec_cmd = [ - 'ffmpeg', '-v', f'level+{log.ff_level}', - '-hide_banner', '-nostats' - ] + node['src_cmd'] + node['filter'] + pre.settings - - messenger.debug(f'Decoder CMD: "{" ".join(dec_cmd)}"') - - kill_dec = True - - with Popen( - dec_cmd, stdout=PIPE, stderr=PIPE) as ff_proc.decoder: - dec_err_thread = Thread(target=ffmpeg_stderr_reader, - args=(ff_proc.decoder.stderr, - '[Decoder]')) - dec_err_thread.daemon = True - dec_err_thread.start() - - while True: - if stream_queue and not stream_queue.empty(): - if kill_dec: - kill_dec = False - live_on = True - get_source.first = True - - messenger.info( - "Switch from offline source to live ingest") - - if ff_proc.decoder.poll() is None: - ff_proc.decoder.kill() - ff_proc.decoder.wait() - - buf_live = stream_queue.get() - ff_proc.encoder.stdin.write(buf_live) - else: - if live_on: - messenger.info( - "Switch from live ingest to offline source") - kill_dec = True - live_on = False - - buf_dec = ff_proc.decoder.stdout.read( - pre.buffer_size) - if buf_dec: - ff_proc.encoder.stdin.write(buf_dec) - else: - break - - except BrokenPipeError: - messenger.error('Broken Pipe!') - terminate_processes(getattr(get_source, 'stop', None)) - - except SystemExit: - messenger.info('Got close command') - terminate_processes(getattr(get_source, 'stop', None)) - - except KeyboardInterrupt: - messenger.warning('Program terminated') - terminate_processes(getattr(get_source, 'stop', None)) - - # close encoder when nothing is to do anymore - if ff_proc.encoder.poll() is None: - ff_proc.encoder.kill() - - finally: - if ff_proc.encoder.poll() is None: - ff_proc.encoder.kill() - ff_proc.encoder.wait() diff --git a/ffplayout/player/Readme.md b/ffplayout/player/Readme.md deleted file mode 100644 index 353a7971..00000000 --- a/ffplayout/player/Readme.md +++ /dev/null @@ -1,7 +0,0 @@ -Here you have the possibility to add you own player module. Defaults are: playing a playlist, or the content of a folder. - -If you need your own module, create a python file with the desire name. Inside it need a generator class with the name: **GetSourceIter**. - -Check **folder.py** and **playlist.py** to get an idea how it needs to work. - -After creating the custom module, set in config **play: -> mode:** the file name of your module without extension. diff --git a/ffplayout/player/__init__.py b/ffplayout/player/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ffplayout/player/folder.py b/ffplayout/player/folder.py deleted file mode 100644 index 2d667b72..00000000 --- a/ffplayout/player/folder.py +++ /dev/null @@ -1,231 +0,0 @@ -# This file is part of ffplayout. -# -# ffplayout 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 3 of the License, or -# (at your option) any later version. -# -# ffplayout 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. -# -# You should have received a copy of the GNU General Public License -# along with ffplayout. If not, see . - -# ------------------------------------------------------------------------------ - -""" -This module handles folder reading. It monitor file adding, deleting or moving -""" - -import random -import time -from copy import deepcopy -from pathlib import Path - -from watchdog.events import PatternMatchingEventHandler -from watchdog.observers import Observer - -from ..filters.default import build_filtergraph -from ..utils import (MediaProbe, ff_proc, get_float, messenger, stdin_args, - storage) - -# ------------------------------------------------------------------------------ -# folder watcher -# ------------------------------------------------------------------------------ - - -class MediaStore: - """ - fill media list for playing - MediaWatch will interact with add and remove - """ - - def __init__(self): - self.store = [] - - if stdin_args.folder: - self.folder = stdin_args.folder - else: - self.folder = storage.path - - self.fill() - - def fill(self): - """ - fill media list - """ - for ext in storage.extensions: - self.store.extend( - [str(f) for f in Path(self.folder).rglob(f'*{ext}')]) - - def sort_or_radomize(self): - """ - sort or randomize file list - """ - if storage.shuffle: - self.rand() - else: - self.sort() - - def add(self, file): - """ - add new file to media list - """ - self.store.append(file) - self.sort_or_radomize() - - def remove(self, file): - """ - remove file from media list - """ - self.store.remove(file) - self.sort_or_radomize() - - def sort(self): - """ - sort list for sorted playing - """ - self.store = sorted(self.store) - - def rand(self): - """ - randomize list for playing - """ - random.shuffle(self.store) - - -class MediaWatcher: - """ - watch given folder for file changes and update media list - """ - - def __init__(self, media): - self._media = media - self.extensions = [f'*{ext}' for ext in storage.extensions] - self.current_clip = None - - self.event_handler = PatternMatchingEventHandler( - patterns=self.extensions) - self.event_handler.on_created = self.on_created - self.event_handler.on_moved = self.on_moved - self.event_handler.on_deleted = self.on_deleted - - self.observer = Observer() - self.observer.schedule(self.event_handler, self._media.folder, - recursive=True) - - self.observer.start() - - def on_created(self, event): - """ - add file to media list only if it is completely copied - """ - file_size = -1 - while file_size != Path(event.src_path).stat().st_size: - file_size = Path(event.src_path).stat().st_size - time.sleep(1) - - self._media.add(event.src_path) - - messenger.info(f'Add file to media list: "{event.src_path}"') - - def on_moved(self, event): - """ - operation when file on storage are moved - """ - self._media.remove(event.src_path) - self._media.add(event.dest_path) - - messenger.info( - f'Move file from "{event.src_path}" to "{event.dest_path}"') - - if self.current_clip == event.src_path: - ff_proc.decoder.terminate() - - def on_deleted(self, event): - """ - operation when file on storage are deleted - """ - self._media.remove(event.src_path) - - messenger.info(f'Remove file from media list: "{event.src_path}"') - - if self.current_clip == event.src_path: - ff_proc.decoder.terminate() - - def stop(self): - """ - stop monitoring storage - """ - self.observer.stop() - self.observer.join() - - -class GetSourceIter: - """ - give next clip, depending on shuffle mode - """ - - def __init__(self): - self.media = MediaStore() - self.watcher = MediaWatcher(self.media) - - self.last_played = [] - self.index = 0 - self.probe = MediaProbe() - self.next_probe = MediaProbe() - self.node = None - self.node_last = None - self.node_next = None - - def stop(self): - self.watcher.stop() - - def next(self): - """ - generator for getting always a new file - """ - while True: - while self.index < len(self.media.store): - if self.node_next: - self.node = deepcopy(self.node_next) - self.probe = deepcopy(self.next_probe) - else: - self.probe.load(self.media.store[self.index]) - duration = get_float(self.probe.format.get('duration'), 0) - self.node = { - 'in': 0, - 'seek': 0, - 'out': duration, - 'duration': duration, - 'source': self.media.store[self.index], - 'probe': self.probe - } - if self.index < len(self.media.store) - 1: - self.next_probe.load(self.media.store[self.index + 1]) - next_duration = get_float( - self.next_probe.format.get('duration'), 0) - self.node_next = { - 'in': 0, - 'seek': 0, - 'out': next_duration, - 'duration': next_duration, - 'source': self.media.store[self.index + 1], - 'probe': self.next_probe - } - else: - self.media.rand() - self.node_next = None - - self.node['src_cmd'] = ['-i', self.media.store[self.index]] - self.node['filter'] = build_filtergraph( - self.node, self.node_last, self.node_next) - - self.watcher.current_clip = self.node.get('source') - yield self.node - self.index += 1 - self.node_last = deepcopy(self.node) - - self.index = 0 diff --git a/ffplayout/player/playlist.py b/ffplayout/player/playlist.py deleted file mode 100644 index a458f4e6..00000000 --- a/ffplayout/player/playlist.py +++ /dev/null @@ -1,497 +0,0 @@ -# This file is part of ffplayout. -# -# ffplayout 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 3 of the License, or -# (at your option) any later version. -# -# ffplayout 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. -# -# You should have received a copy of the GNU General Public License -# along with ffplayout. If not, see . - -# ------------------------------------------------------------------------------ - -""" -This module handles playlists, it can be aware of time syncing. -Empty, missing or any other playlist related failure should be compensate. -Missing clips will be replaced by a dummy clip. -""" - -import socket -import time -from copy import deepcopy -from pathlib import Path -from threading import Thread - -import requests - -from ..filters.default import build_filtergraph -from ..utils import (MediaProbe, check_sync, get_date, get_delta, get_float, - get_time, messenger, playlist, sec_to_time, src_or_dummy, - storage, sync_op, valid_json) - - -def validate_thread(clip_nodes, list_date): - """ - validate json values in new thread - and test if source paths exist - """ - def check_json(clip_nodes, list_date): - error = '' - counter = 0 - probe = MediaProbe() - - # check if all values are valid - for node in clip_nodes['program']: - source = node.get('source') - probe.load(source) - missing = [] - _in = get_float(node.get('in'), 0) - _out = get_float(node.get('out'), 0) - duration = get_float(node.get('duration'), 0) - - if probe.is_remote: - if not probe.video[0]: - missing.append(f'Remote file not exist: "{source}"') - elif source is None or not Path(source).is_file(): - missing.append(f'File not exist: "{source}", ' - f'at "{sec_to_time(counter + playlist.start)}"') - - if not type(node.get('in')) in [int, float]: - missing.append(f'No in Value in: "{node}"') - - if _out == 0: - missing.append(f'No out Value in: "{node}"') - - if duration == 0: - missing.append(f'No duration Value in: "{node}"') - - counter += _out - _in - - line = '\n'.join(missing) - if line: - error += line + f'\nIn line: {node}\n\n' - - if error: - messenger.error( - 'Validation error, check JSON playlist, ' - f'values are missing:\n{error}' - ) - - check_length(counter, list_date) - - if clip_nodes and clip_nodes.get('program') and \ - len(clip_nodes.get('program')) > 0: - validate = Thread(name='check_json', target=check_json, - args=(clip_nodes, list_date)) - validate.daemon = True - validate.start() - else: - messenger.error('Validation error: playlist are empty') - - -def handle_list_init(node): - """ - handle init clip, but this clip can be the last one in playlist, - this we have to figure out and calculate the right length - """ - messenger.debug('List init') - - delta, total_delta = get_delta(node['begin']) - seek = abs(delta) + node['seek'] if abs(delta) + node['seek'] >= 1 else 0 - seek = round(seek, 3) - - if node['out'] - seek > total_delta: - out = total_delta + seek - else: - out = node['out'] - - if out - seek > 1: - node['out'] = out - node['seek'] = seek - return src_or_dummy(node) - - messenger.warning(f'Clip less then a second, skip:\n{node["source"]}') - - return None - - -def handle_list_end(duration, node): - """ - when we come to last clip in playlist, - or when we reached total playtime, - we end up here - """ - messenger.debug('List end') - - out = node['seek'] + duration if node['seek'] > 0 else duration - - # prevent looping - if out > node['duration']: - out = node['duration'] - else: - messenger.warning( - f'Clip length is not in time, new duration is: {duration:.2f}') - - if node['duration'] > duration > 1 and \ - node['duration'] - node['seek'] >= duration: - node['out'] = out - node = src_or_dummy(node) - elif node['duration'] > duration < 1.0: - messenger.warning( - f'Last clip less then 1 second long, skip:\n{node["source"]}') - node = None - else: - _, total_delta = get_delta(node['begin']) - messenger.error( - f'Playlist is not long enough:\n{total_delta:.2f} seconds needed') - node = src_or_dummy(node) - - return node - - -def timed_source(node, last): - """ - prepare input clip - check begin and length from clip - return clip only if we are in 24 hours time range - """ - delta, total_delta = get_delta(node['begin']) - node_ = None - - if playlist.start and playlist.length: - messenger.debug(f'delta: {delta:f}') - messenger.debug(f'total_delta: {total_delta:f}') - check_sync(delta, node) - - if (total_delta > node['out'] - node['seek'] and not last) \ - or not playlist.length: - # when we are in the 24 hour range, get the clip - node_ = src_or_dummy(node) - - elif total_delta <= 0: - messenger.info(f'Begin is over play time, skip:\n{node["source"]}') - - elif total_delta < node['duration'] - node['seek'] or last: - node_ = handle_list_end(total_delta, node) - - return node_ - - -def check_length(total_play_time, list_date): - """ - check if playlist is long enough - """ - if playlist.length and total_play_time < playlist.length - 5 \ - and not playlist.loop: - messenger.error( - f'Playlist from {list_date} is not long enough!\n' - f'Total play time is: {sec_to_time(total_play_time)}, ' - f'target length is: {sec_to_time(playlist.length)}' - ) - - -class PlaylistReader: - """ - Class which read playlists, it checks if playlist got modified, - when yes it reads the file new, when not it used the cached one - """ - - def __init__(self, list_date, last_mod_time): - self.list_date = list_date - self.last_mod_time = last_mod_time - self.nodes = None - self.error = False - - def read(self): - """ - read and process playlist - """ - self.nodes = {'program': []} - self.error = False - - if '://' in playlist.path: - json_file = playlist.path.replace('\\', '/') - - try: - result = requests.get(json_file, timeout=1, verify=False) - b_time = result.headers['last-modified'] - temp_time = time.strptime(b_time, "%a, %d %b %Y %H:%M:%S %Z") - mod_time = time.mktime(temp_time) - - if mod_time > self.last_mod_time: - if isinstance(result.json(), dict): - self.nodes = result.json() - self.last_mod_time = mod_time - messenger.info('Open: ' + json_file) - validate_thread(deepcopy(self.nodes), self.list_date) - except (requests.exceptions.ConnectionError, socket.timeout): - messenger.error(f'No valid playlist from url: {json_file}') - self.error = True - - return - - json_file = Path(playlist.path) - - if json_file.is_dir(): - year, month, _ = self.list_date.split('-') - json_file = json_file.joinpath( - year, month, f'{self.list_date}.json') - - if json_file.is_file(): - # check last modification time from playlist - mod_time = json_file.stat().st_mtime - if mod_time > self.last_mod_time: - with open(json_file, 'r', encoding='utf-8') as playlist_file: - self.nodes = valid_json(playlist_file) - - self.last_mod_time = mod_time - messenger.info(f'Open: {str(json_file)}') - validate_thread(deepcopy(self.nodes), self.list_date) - else: - messenger.error(f'Playlist not exists: {str(json_file)}') - self.error = True - - -class GetSourceIter: - """ - read values from json playlist, - get current clip in time, - set ffmpeg source command - """ - - def __init__(self): - self.prev_date = get_date(True) - self.list_start = playlist.start - self.last_time = 0 - self.first = True - self.last = False - self.clip_nodes = [] - self.node_count = 0 - self.node = None - self.prev_node = None - self.next_node = None - self.playlist_reader = PlaylistReader(get_date(True), 0.0) - self.last_error = False - - probe = MediaProbe() - probe.load(storage.filler) - - self.filler_duration = get_float(probe.format.get('duration'), 60) - - def get_playlist(self): - """ - read playlist from given date and fill clip_nodes - when playlist is not available, reset relevant values - """ - self.playlist_reader.read() - - if self.last_error and not self.playlist_reader.error and \ - self.playlist_reader.list_date == self.prev_date: - # when last playlist where not exists but now is there and - # is still the same playlist date, - # set self.first to true to seek in clip - # only in this situation seek in is correct!! - self.first = True - self.last_error = self.playlist_reader.error - - if self.playlist_reader.nodes and \ - self.playlist_reader.nodes.get('program'): - self.clip_nodes = self.playlist_reader.nodes.get('program') - - if playlist.loop and playlist.length: - self.loop_nodes() - - self.node_count = len(self.clip_nodes) - - if self.playlist_reader.error: - self.clip_nodes = [] - self.node_count = 0 - self.playlist_reader.last_mod_time = 0.0 - self.last_error = self.playlist_reader.error - - def loop_nodes(self): - total_duration = 0 - nodes_ = deepcopy(self.clip_nodes) - - while total_duration < playlist.length: - for node in nodes_: - total_duration += node['out'] - node['in'] - self.clip_nodes.append(node) - - if total_duration >= playlist.length: - break - - def init_time(self): - """ - get current time in second and shift it when is necessary - """ - self.last_time = get_time('full_sec') - - if playlist.length: - total_playtime = playlist.length - else: - total_playtime = 86400.0 - - if self.last_time < playlist.start: - self.last_time += total_playtime - - def check_for_next_playlist(self, begin): - """ - check if playlist length is 24 hours and matches current length, - to get the date for a new playlist - """ - - if self.node is not None: - out = self.node['out'] - delta = 0 - - if self.node['duration'] > self.node['out']: - out = self.node['duration'] - - if self.last: - seek = self.node['seek'] if self.node['seek'] > 0 else 0 - delta, _ = get_delta(begin) - delta += seek + sync_op.threshold - - next_start = begin - playlist.start + out + delta - else: - delta, _ = get_delta(begin) - next_start = begin - playlist.start + sync_op.threshold + delta - - if playlist.length and next_start >= playlist.length: - self.prev_date = get_date(False, next_start) - self.playlist_reader.list_date = self.prev_date - self.playlist_reader.last_mod_time = 0.0 - self.last_time = playlist.start - 1 - self.clip_nodes = [] - - def previous_and_next_node(self, index): - """ - set previous and next clip node - """ - self.prev_node = self.clip_nodes[index - 1] if index > 0 else None - - if index < self.node_count - 1: - self.next_node = self.clip_nodes[index + 1] - else: - self.next_node = None - - def generate_cmd(self): - """ - extend clip node with ffmpeg source cmd and filters - """ - self.node = timed_source(self.node, self.last) - if self.node: - self.node['filter'] = build_filtergraph(self.node, self.prev_node, - self.next_node) - - def generate_placeholder(self): - """ - when playlist not exists, or is not long enough, - generate a placeholder node - """ - - self.init_time() - begin = self.last_time - - self.node = { - 'begin': begin, - 'number': 0, - 'in': 0, - 'seek': 0, - 'out': self.filler_duration - 0.001, - 'duration': self.filler_duration - } - - self.generate_cmd() - self.check_for_next_playlist(begin) - - def eof_handling(self, begin): - """ - handle except playlist end - """ - if playlist.loop and self.node: - # when loop parameter is set and playlist node exists, - # jump to playlist start and play again - self.list_start = get_time('full_sec') - self.node = None - messenger.info('Loop playlist') - - elif begin == playlist.start or not self.clip_nodes: - # playlist not exist or is corrupt/empty - messenger.error('Clip nodes are empty!') - self.first = False - self.generate_placeholder() - - else: - messenger.error('Playlist not long enough!') - self.first = False - self.last = True - self.generate_placeholder() - - def next(self): - """ - endless loop for reading playlists - and getting the right clip node - """ - while True: - self.get_playlist() - begin = self.list_start - - for index, self.node in enumerate(self.clip_nodes): - self.node['seek'] = get_float(self.node.get('in'), 0) - self.node['duration'] = get_float(self.node.get('duration'), - 30) - self.node['out'] = get_float(self.node.get('out'), - self.node['duration']) - self.node['begin'] = begin - self.node['number'] = index + 1 - - # first time we end up here - if self.first: - self.init_time() - out = self.node['out'] - - if self.node['duration'] > self.node['out']: - out = self.node['duration'] - - if self.last_time < begin + out - self.node['seek']: - self.previous_and_next_node(index) - self.node = handle_list_init(self.node) - if self.node: - self.node['filter'] = build_filtergraph( - self.node, self.prev_node, self.next_node) - self.first = False - self.last_time = begin - - self.check_for_next_playlist(begin) - break - elif self.last_time < begin: - if index == self.node_count - 1: - self.last = True - else: - self.last = False - - self.previous_and_next_node(index) - self.generate_cmd() - self.last_time = begin - - self.check_for_next_playlist(begin) - break - - begin += self.node['out'] - self.node['seek'] - else: - if not playlist.length and not playlist.loop: - # when we reach playlist end, stop script - messenger.info('Playlist reached end!') - return None - - self.eof_handling(begin) - - if self.node: - yield self.node diff --git a/ffplayout/utils.py b/ffplayout/utils.py deleted file mode 100644 index f31505dd..00000000 --- a/ffplayout/utils.py +++ /dev/null @@ -1,976 +0,0 @@ -# This file is part of ffplayout. -# -# ffplayout 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 3 of the License, or -# (at your option) any later version. -# -# ffplayout 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. -# -# You should have received a copy of the GNU General Public License -# along with ffplayout. If not, see . - -# ------------------------------------------------------------------------------ - -""" -This module contains default variables and helper functions -""" - -import json -import logging -import math -import re -import shlex -import signal -import smtplib -import socket -import sys -import tempfile -import urllib -from argparse import ArgumentParser -from datetime import date, datetime, timedelta -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from email.utils import formatdate -from logging.handlers import TimedRotatingFileHandler -from pathlib import Path -from platform import system -from shutil import which -from subprocess import STDOUT, CalledProcessError, check_output -from types import SimpleNamespace - -import yaml - -# path to user define configs -CONFIG_PATH = Path(__file__).parent.absolute().joinpath('conf.d') - - -# ------------------------------------------------------------------------------ -# argument parsing -# ------------------------------------------------------------------------------ - -stdin_parser = ArgumentParser(description='python and ffmpeg based playout') - -stdin_parser.add_argument( - '-c', '--config', help='file path to ffplayout.conf' -) - -stdin_parser.add_argument( - '-f', '--folder', help='play folder content' -) - -stdin_parser.add_argument( - '-l', '--log', help='file path for logfile' -) - -stdin_parser.add_argument( - '-i', '--loop', help='loop playlist infinitely', action='store_true' -) - -stdin_parser.add_argument( - '-o', '--output', help='set output mode: desktop, hls, stream' -) - -stdin_parser.add_argument( - '-p', '--playlist', help='path from playlist' -) - -stdin_parser.add_argument( - '-s', '--start', - help='start time in "hh:mm:ss", "now" for start with first' -) - -stdin_parser.add_argument( - '-t', '--length', - help='set length in "hh:mm:ss", "none" for no length check' -) - -stdin_parser.add_argument( - '-pm', '--play_mode', help='playing mode: folder, playlist, custom...' -) - -# read dynamical new arguments -for arg_file in CONFIG_PATH.glob('argparse_*'): - with open(arg_file, 'r') as _file: - config = yaml.safe_load(_file) - - short = config.pop('short') if config.get('short') else None - long = config.pop('long') if config.get('long') else None - - stdin_parser.add_argument( - *filter(None, [short, long]), - **config - ) - -stdin_args = stdin_parser.parse_args() - - -# ------------------------------------------------------------------------------ -# clock -# ------------------------------------------------------------------------------ - -def get_time(time_format): - """ - get different time formats: - - full_sec > current time in seconds - - stamp > current date time in seconds - - or current time in HH:MM:SS - """ - date_time = datetime.today() - - if time_format == 'full_sec': - return date_time.hour * 3600 + date_time.minute * 60 \ - + date_time.second + date_time.microsecond / 1000000 - - if time_format == 'stamp': - return float(datetime.now().timestamp()) - - return date_time.strftime('%H:%M:%S') - - -# ------------------------------------------------------------------------------ -# default variables and values -# ------------------------------------------------------------------------------ - -sync_op = SimpleNamespace(time_delta=0, realtime=False) -mail = SimpleNamespace() -log = SimpleNamespace() -pre = SimpleNamespace() -ingest = SimpleNamespace() -playlist = SimpleNamespace() -storage = SimpleNamespace() -lower_third = SimpleNamespace() -playout = SimpleNamespace() - -ff_proc = SimpleNamespace(decoder=None, encoder=None, server=None) - - -def str_to_sec(time_str): - """ - convert time is string in seconds as float - """ - if time_str in ['now', '', None, 'none']: - return None - - tms = time_str.split(':') - try: - return float(tms[0]) * 3600 + float(tms[1]) * 60 + float(tms[2]) - except ValueError: - print('Wrong time format!') - sys.exit(1) - - -def sec_to_time(seconds): - """ - convert float number to time string in hh:mm:ss - """ - min, sec = divmod(seconds, 60) - hours, min = divmod(min, 60) - return f'{int(hours):d}:{int(min):02d}:{int(sec):02d}' - - -def get_float(value, default=False): - """ - test if value is float - """ - try: - return float(value) - except (ValueError, TypeError): - return default - - -def read_config(): - """ - read yaml config - """ - - if stdin_args.config: - cfg_path = stdin_args.config - elif Path('/etc/ffplayout/ffplayout.yml').is_file(): - cfg_path = '/etc/ffplayout/ffplayout.yml' - else: - cfg_path = 'ffplayout.yml' - - with open(cfg_path, 'r') as config_file: - return yaml.safe_load(config_file) - - -def load_config(): - """ - this function can reload most settings from configuration file, - the change does not take effect immediately, but with the after next file, - some settings cannot be changed - like resolution, aspect, or output - """ - - cfg = read_config() - - sync_op.threshold = int(cfg['general']['stop_threshold']) - - mail.subject = cfg['mail']['subject'] - mail.server = cfg['mail']['smtp_server'] - mail.port = cfg['mail']['smtp_port'] - mail.s_addr = cfg['mail']['sender_addr'] - mail.s_pass = cfg['mail']['sender_pass'] - mail.recip = cfg['mail']['recipient'] - mail.level = cfg['mail']['mail_level'] - - pre.add_logo = cfg['processing']['add_logo'] - pre.logo = cfg['processing']['logo'] - pre.logo_scale = cfg['processing']['logo_scale'] - pre.logo_filter = cfg['processing']['logo_filter'] - pre.logo_opacity = cfg['processing']['logo_opacity'] - pre.add_loudnorm = cfg['processing']['add_loudnorm'] - pre.loud_i = cfg['processing']['loud_i'] - pre.loud_tp = cfg['processing']['loud_tp'] - pre.loud_lra = cfg['processing']['loud_lra'] - - storage.path = cfg['storage']['path'] - storage.filler = cfg['storage']['filler_clip'] - storage.extensions = cfg['storage']['extensions'] - storage.shuffle = cfg['storage']['shuffle'] - - lower_third.add_text = cfg['text']['add_text'] - lower_third.over_pre = cfg['text']['over_pre'] - lower_third.address = cfg['text']['bind_address'].replace(':', '\\:') - lower_third.fontfile = cfg['text']['fontfile'] - lower_third.text_from_filename = cfg['text']['text_from_filename'] - lower_third.style = cfg['text']['style'] - lower_third.regex = cfg['text']['regex'] - - return cfg - - -_cfg = load_config() - -if stdin_args.playlist: - playlist.path = stdin_args.playlist -else: - playlist.path = _cfg['playlist']['path'] - -if stdin_args.start is not None: - playlist.start = str_to_sec(stdin_args.start) -else: - playlist.start = str_to_sec(_cfg['playlist']['day_start']) - -if playlist.start is None: - playlist.start = get_time('full_sec') - -if stdin_args.length: - playlist.length = str_to_sec(stdin_args.length) -else: - playlist.length = str_to_sec(_cfg['playlist']['length']) - -if stdin_args.loop: - playlist.loop = stdin_args.loop -else: - playlist.loop = _cfg['playlist']['infinit'] - -log.to_file = _cfg['logging']['log_to_file'] -log.backup_count = _cfg['logging']['backup_count'] -log.path = Path(_cfg['logging']['log_path']) -log.level = _cfg['logging']['log_level'] -log.ff_level = _cfg['logging']['ffmpeg_level'] - - -def pre_audio_codec(): - """ - when add_loudnorm is False we use a different audio encoder, - s302m has higher quality, but is experimental - and works not well together with the loudnorm filter - """ - if pre.add_loudnorm: - return ['-c:a', 'mp2', '-b:a', '384k', '-ar', '48000', '-ac', '2'] - - return ['-c:a', 's302m', '-strict', '-2', '-ar', '48000', '-ac', '2'] - - -ingest.enable = _cfg['ingest']['enable'] -ingest.input_param = shlex.split(_cfg['ingest']['input_param']) - -if stdin_args.play_mode: - pre.mode = stdin_args.play_mode -else: - pre.mode = _cfg['processing']['mode'] - -pre.w = _cfg['processing']['width'] -pre.h = _cfg['processing']['height'] -pre.aspect = _cfg['processing']['aspect'] -pre.fps = _cfg['processing']['fps'] -pre.v_bitrate = _cfg['processing']['width'] * _cfg['processing']['height'] / 10 -pre.v_bufsize = pre.v_bitrate / 2 -pre.output_count = _cfg['processing']['output_count'] -pre.buffer_size = 1024 * 1024 if system() == 'Windows' else 65424 - -pre.settings = [ - '-pix_fmt', 'yuv420p', '-r', str(pre.fps), - '-c:v', 'mpeg2video', '-g', '1', - '-b:v', f'{pre.v_bitrate}k', - '-minrate', f'{pre.v_bitrate}k', - '-maxrate', f'{pre.v_bitrate}k', - '-bufsize', f'{pre.v_bufsize}k' -] + pre_audio_codec() + ['-f', 'mpegts', '-'] - -if stdin_args.output: - playout.mode = stdin_args.output -else: - playout.mode = _cfg['out']['mode'] - -playout.preview = _cfg['out']['preview'] -playout.preview_param =shlex.split(_cfg['out']['preview_param']) -playout.output_param = shlex.split(_cfg['out']['output_param']) - - -# ------------------------------------------------------------------------------ -# logging -# ------------------------------------------------------------------------------ - -class CustomFormatter(logging.Formatter): - """ - Logging formatter to add colors and count warning / errors - """ - - grey = '\x1b[38;1m' - darkgrey = '\x1b[30;1m' - yellow = '\x1b[33;1m' - red = '\x1b[31;1m' - magenta = '\x1b[35;1m' - green = '\x1b[32;1m' - blue = '\x1b[34;1m' - cyan = '\x1b[36;1m' - reset = '\x1b[0m' - - timestamp = darkgrey + '[%(asctime)s]' + reset - level = '[%(levelname)s]' + reset - message = grey + ' %(message)s' + reset - - FORMATS = { - logging.DEBUG: timestamp + blue + level + ' ' + message + reset, - logging.INFO: timestamp + green + level + ' ' + message + reset, - logging.WARNING: timestamp + yellow + level + message + reset, - logging.ERROR: timestamp + red + level + ' ' + message + reset - } - - def format_message(self, msg): - """ - match strings with regex and add different color tags to it - """ - if '"' in msg: - msg = re.sub('(".*?")', self.cyan + r'\1' + self.reset, msg) - elif '[decoder]' in msg: - msg = re.sub(r'(\[decoder\])', self.reset + r'\1', msg) - elif '[encoder]' in msg: - msg = re.sub(r'(\[encoder\])', self.reset + r'\1', msg) - elif '/' in msg or '\\' in msg: - msg = re.sub( - r'("?/[\w.:/]+|["\w.:]+\\.*?)', self.magenta + r'\1', msg) - elif re.search(r'\d', msg): - msg = re.sub( - r'(\d+-\d+-\d+|\d+:\d+:[\d.]+|-?[\d.]+)', - self.yellow + r'\1' + self.reset, msg) - - return msg - - def format(self, record): - """ - override logging format - """ - record.msg = self.format_message(record.getMessage()) - log_fmt = self.FORMATS.get(record.levelno) - formatter = logging.Formatter(log_fmt) - return formatter.format(record) - - -# If the log file is specified on the command line then override the default -if stdin_args.log: - log.path = stdin_args.log - -logger = logging.getLogger('playout') -logger.setLevel(log.level) - -if log.to_file and log.path != 'none': - if log.path.is_dir(): - playout_log = log.path.joinpath('ffplayout.log') - else: - log_dir = Path(__file__).parent.parent.absolute().joinpath('log') - log_dir.mkdir(exist_ok=True) - playout_log = log_dir.joinpath('ffplayout.log') - - p_format = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s') - handler = TimedRotatingFileHandler(playout_log, when='midnight', - backupCount=log.backup_count) - - handler.setFormatter(p_format) - logger.addHandler(handler) -else: - console_handler = logging.StreamHandler() - console_handler.setFormatter(CustomFormatter()) - logger.addHandler(console_handler) - - -# ------------------------------------------------------------------------------ -# mail sender -# ------------------------------------------------------------------------------ - -class Mailer: - """ - mailer class for sending log messages, with level selector - """ - - def __init__(self): - self.level = mail.level - self.time = None - self.timestamp = get_time('stamp') - self.rate_limit = 600 - self.temp_msg = Path(tempfile.gettempdir()).joinpath('ffplayout.txt') - - def current_time(self): - """ - set sending time - """ - self.time = get_time(None) - - def send_mail(self, msg): - """ - send emails to specified recipients - """ - if mail.recip: - # write message to temp file for rate limit - with open(self.temp_msg, 'w+') as msg_file: - msg_file.write(msg) - - self.current_time() - - message = MIMEMultipart() - message['From'] = mail.s_addr - message['To'] = mail.recip - message['Subject'] = mail.subject - message['Date'] = formatdate(localtime=True) - message.attach(MIMEText(f'{self.time} {msg}', 'plain')) - text = message.as_string() - - try: - server = smtplib.SMTP(mail.server, mail.port) - except socket.error as err: - logger.error(err) - server = None - - if server is not None: - server.starttls() - try: - login = server.login(mail.s_addr, mail.s_pass) - except smtplib.SMTPAuthenticationError as serr: - logger.error(serr) - login = None - - if login is not None: - server.sendmail(mail.s_addr, - re.split(', |; |,|;', mail.recip), text) - server.quit() - - def check_if_new(self, msg): - """ - send message only when is new or the rate_limit is pass - """ - if Path(self.temp_msg).is_file(): - mod_time = Path(self.temp_msg).stat().st_mtime - - with open(self.temp_msg, 'r', encoding='utf-8') as msg_file: - last_msg = msg_file.read() - - if msg != last_msg \ - or get_time('stamp') - mod_time > self.rate_limit: - self.send_mail(msg) - else: - self.send_mail(msg) - - def info(self, msg): - """ - send emails with level INFO, WARNING and ERROR - """ - if self.level in ['INFO']: - self.check_if_new(msg) - - def warning(self, msg): - """ - send emails with level WARNING and ERROR - """ - if self.level in ['INFO', 'WARNING']: - self.check_if_new(msg) - - def error(self, msg): - """ - send emails with level ERROR - """ - if self.level in ['INFO', 'WARNING', 'ERROR']: - self.check_if_new(msg) - - -class Messenger: - """ - all logging and mail messages end up here, - from here they go to logger and mailer - """ - - def __init__(self): - self._mailer = Mailer() - - # pylint: disable=no-self-use - def debug(self, msg): - """ - log debugging messages - """ - logger.debug(msg.replace('\n', ' ')) - - def info(self, msg): - """ - log and mail info messages - """ - logger.info(msg.replace('\n', ' ')) - self._mailer.info(msg) - - def warning(self, msg): - """ - log and mail warning messages - """ - logger.warning(msg.replace('\n', ' ')) - self._mailer.warning(msg) - - def error(self, msg): - """ - log and mail error messages - """ - logger.error(msg.replace('\n', ' ')) - self._mailer.error(msg) - - -messenger = Messenger() - - -# ------------------------------------------------------------------------------ -# check binaries and ffmpeg libs -# ------------------------------------------------------------------------------ - -def is_in_system(name): - """ - Check whether name is on PATH and marked as executable - """ - if which(name) is None: - messenger.error(f'{name} is not found on system') - sys.exit(1) - - -def ffmpeg_libs(): - """ - check which external libs are compiled in ffmpeg, - for using them later - """ - is_in_system('ffmpeg') - is_in_system('ffprobe') - - cmd = ['ffmpeg', '-filters'] - libs = [] - filters = [] - - try: - info = check_output(cmd, stderr=STDOUT).decode('UTF-8') - except CalledProcessError as err: - messenger.error('ffmpeg - libs could not be readed!\n' - f'Processing is not possible. Error:\n{err}') - sys.exit(1) - - for line in info.split('\n'): - if 'configuration:' in line: - configs = line.split() - - for cfg in configs: - if '--enable-lib' in cfg: - libs.append(cfg.replace('--enable-', '')) - elif re.match(r'^(?!.*=) [TSC.]+', line): - filter_list = line.split() - if len(filter_list) > 3: - filters.append(filter_list[1]) - - return {'libs': libs, 'filters': filters} - - -FF_LIBS = ffmpeg_libs() - - -def validate_ffmpeg_libs(): - """ - check if ffmpeg contains some basic libs - """ - if 'libx264' not in FF_LIBS['libs']: - logger.error('ffmpeg contains no libx264!') - if 'libfdk-aac' not in FF_LIBS['libs']: - logger.warning( - 'ffmpeg contains no libfdk-aac! No high quality aac...') - if 'tpad' not in FF_LIBS['filters']: - logger.error('ffmpeg contains no tpad filter!') - if 'zmq' not in FF_LIBS['filters']: - lower_third.add_text = False - logger.warning( - 'ffmpeg contains no zmq filter! Text messages will not work...') - - -# ------------------------------------------------------------------------------ -# probe media info's -# ------------------------------------------------------------------------------ - -class MediaProbe: - """ - get info's about media file, similar to mediainfo - """ - - def __init__(self): - self.remote_source = ['http', 'https', 'ftp', 'smb', 'sftp'] - self.src = None - self.format = {} - self.audio = [] - self.video = [] - self.is_remote = False - - def load(self, file): - """ - load media file with ffprobe and get info's out of it - """ - self.src = file - self.format = {} - self.audio = [] - self.video = [] - - if self.src and self.src.split('://')[0] in self.remote_source: - url = self.src.split('://') - self.src = f'{url[0]}://{urllib.parse.quote(url[1])}' - self.is_remote = True - else: - self.is_remote = False - - if not self.src or not Path(self.src).is_file(): - self.audio.append(None) - self.video.append(None) - - return - - cmd = ['ffprobe', '-v', 'quiet', '-print_format', - 'json', '-show_format', '-show_streams', self.src] - - try: - info = json.loads(check_output(cmd).decode('UTF-8')) - except CalledProcessError as err: - messenger.error(f'MediaProbe error in: "{self.src}"\n{err}') - self.audio.append(None) - self.video.append(None) - - return - - self.format = info['format'] - - if get_float(self.format.get('duration'), 0) > 0.1: - self.format['duration'] = float(self.format['duration']) - - for stream in info['streams']: - if stream['codec_type'] == 'audio': - self.audio.append(stream) - - if stream['codec_type'] == 'video': - if stream.get('display_aspect_ratio'): - width, height = stream['display_aspect_ratio'].split(':') - stream['aspect'] = float(width) / float(height) - else: - stream['aspect'] = float( - stream['width']) / float(stream['height']) - - rate, factor = stream['r_frame_rate'].split('/') - stream['fps'] = float(rate) / float(factor) - - self.video.append(stream) - - -# ------------------------------------------------------------------------------ -# global helper functions -# ------------------------------------------------------------------------------ - -def handle_sigterm(sig, frame): - """ - handler for ctrl+c signal - """ - raise SystemExit - - -# pylint: disable=unused-argument -def handle_sighub(sig, frame): - """ - handling SIGHUP signal for reload configuration - Linux/macOS only - """ - messenger.info('Reload config file') - load_config() - - -signal.signal(signal.SIGTERM, handle_sigterm) - -if system() == 'Linux': - signal.signal(signal.SIGHUP, handle_sighub) - - -def terminate_processes(custom_process=None): - """ - kill orphaned processes - """ - if ff_proc.decoder and ff_proc.decoder.poll() is None: - ff_proc.decoder.terminate() - - if ff_proc.encoder and ff_proc.encoder.poll() is None: - ff_proc.encoder.kill() - - if ff_proc.server and ff_proc.server.poll() is None: - ff_proc.server.kill() - - if custom_process: - custom_process() - - -def ffmpeg_stderr_reader(std_errors, prefix): - """ - read ffmpeg stderr decoder and encoder instance - and log the output - """ - def form_line(line, level): - return f'{prefix} {line.replace(level, "").rstrip()}' - - def write_log(line): - if '[info]' in line: - logger.info(form_line(line, '[info] ')) - elif '[warning]' in line: - logger.warning(form_line(line, '[warning] ')) - elif '[error]' in line: - logger.error(form_line(line, '[error] ')) - - try: - for line in std_errors: - if log.ff_level == 'info': - write_log(line.decode()) - elif log.ff_level == 'warning': - write_log(line.decode()) - else: - write_log(line.decode()) - except (ValueError, AttributeError): - pass - - -def get_delta(begin): - """ - get difference between current time and begin from clip in playlist - """ - current_time = get_time('full_sec') - - if stdin_args.length and str_to_sec(stdin_args.length): - target_playtime = str_to_sec(stdin_args.length) - elif playlist.length: - target_playtime = playlist.length - else: - target_playtime = 86400.0 - - if begin == playlist.start == 0 and 86400.0 - current_time < 4: - current_time -= target_playtime - - elif playlist.start >= current_time and not begin == playlist.start: - current_time += target_playtime - - current_delta = begin - current_time - - if math.isclose(current_delta, 86400.0, abs_tol=sync_op.threshold): - current_delta -= 86400.0 - - ref_time = target_playtime + playlist.start - total_delta = ref_time - begin + current_delta - - return current_delta, total_delta - - -def get_date(seek_day, next_start=0): - """ - get date for correct playlist, - when seek_day is set: - check if playlist date must be from yesterday - """ - date_ = date.today() - - if seek_day and playlist.start > get_time('full_sec'): - return (date_ - timedelta(1)).strftime('%Y-%m-%d') - - if playlist.start == 0 and next_start >= 86400: - return (date_ + timedelta(1)).strftime('%Y-%m-%d') - - return date_.strftime('%Y-%m-%d') - - -def is_advertisement(node): - """ - check if clip in node is advertisement - """ - if node and node.get('category') == 'advertisement': - return True - - return False - - -def valid_json(file): - """ - simple json validation - """ - try: - json_object = json.load(file) - return json_object - except ValueError: - messenger.error(f'Playlist {file.name} is not JSON conform') - return None - - -def check_sync(delta, node=None): - """ - check that we are in tolerance time - """ - - if pre.mode == 'playlist' and playlist.start and playlist.length: - # save time delta to global variable for syncing - # this is needed for real time filter - sync_op.time_delta = delta - - if abs(delta) > sync_op.threshold > 0: - messenger.error( - f'Sync tolerance value exceeded with {delta:.2f} seconds,\n' - 'program terminated!') - messenger.debug(f'Terminate on node: {node}') - terminate_processes() - sys.exit(1) - - -def seek_in(seek): - """ - seek in clip - """ - return ['-ss', str(seek)] if seek > 0.0 else [] - - -def set_length(duration, seek, out): - """ - set new clip length - """ - return ['-t', str(out - seek)] if out < duration else [] - - -def loop_input(source, src_duration, target_duration): - """ - loop files n times - """ - loop_count = math.ceil(target_duration / src_duration) - messenger.info(f'Loop "{source}" {loop_count} times, ' - f'total duration: {target_duration:.2f}') - return ['-stream_loop', str(loop_count), - '-i', source, '-t', str(target_duration)] - - -def gen_dummy(duration): - """ - generate a dummy clip, with black color and empty audio track - """ - color = '#121212' - duration = round(duration, 3) - # IDEA: add noise could be an config option - # noise = 'noise=alls=50:allf=t+u,hue=s=0' - return [ - '-f', 'lavfi', '-i', - f'color=c={color}:s={pre.w}x{pre.h}:d={duration}:r={pre.fps},' - 'format=pix_fmts=yuv420p', - '-f', 'lavfi', '-i', f'anoisesrc=d={duration}:c=pink:r=48000:a=0.05' - ] - - -def gen_filler(node): - """ - generate filler clip to fill empty space in playlist - """ - probe = MediaProbe() - probe.load(storage.filler) - duration = node['out'] - node['seek'] - - node['probe'] = probe - - if probe.format.get('duration'): - node['duration'] = probe.format['duration'] - node['source'] = storage.filler - if node['duration'] > duration: - # cut filler - messenger.info( - f'Generate filler') - node['src_cmd'] = ['-i', storage.filler] + set_length( - node['duration'], 0, duration) - return node - - # loop file n times - node['src_cmd'] = loop_input(storage.filler, node['duration'], - duration) - return node - - # when no filler is set, generate a dummy - messenger.warning('No filler clipt is set! Add dummy...') - dummy = gen_dummy(duration) - node['source'] = dummy[3] - node['src_cmd'] = dummy - return node - - -def src_or_dummy(node): - """ - when source path exist, generate input with seek and out time - when path not exist, generate dummy clip - """ - - probe = MediaProbe() - probe.load(node.get('source')) - node['probe'] = probe - - # check if input is a remote source - if probe.is_remote and probe.video[0]: - if node['seek'] > 0.0: - messenger.warning( - f'Seek in remote source "{node.get("source")}" not supported!') - node['src_cmd'] = [ - '-i', node['source'] - ] + set_length(86400, node['seek'], node['out']) - elif node.get('source') and Path(node['source']).is_file(): - if probe.format.get('duration') and not math.isclose( - probe.format['duration'], node['duration'], abs_tol=3): - messenger.debug( - f"fix duration for: \"{node['source']}\" " - f"at \"{sec_to_time(node['begin'])}\"") - node['duration'] = probe.format['duration'] - - if node['out'] > node['duration']: - if node['seek'] > 0.0: - messenger.warning( - f'Seek in looped source "{node["source"]}" not supported!') - node['src_cmd'] = [ - '-i', node['source'] - ] + set_length(node['duration'], node['seek'], - node['out'] - node['seek']) - else: - # when list starts with looped clip, - # the logo length will be wrong - node['src_cmd'] = loop_input(node['source'], node['duration'], - node['out']) - else: - node['src_cmd'] = seek_in(node['seek']) + \ - ['-i', node['source']] + set_length(node['duration'], - node['seek'], node['out']) - else: - if 'source' in node: - messenger.error(f'File not exist: {node.get("source")}') - node = gen_filler(node) - - return node diff --git a/requirements-base.txt b/requirements-base.txt deleted file mode 100644 index 5f7ccf36..00000000 --- a/requirements-base.txt +++ /dev/null @@ -1,5 +0,0 @@ -colorama -pyyaml -requests -supervisor -watchdog diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 62e8925b..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest -time-machine diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8642badd..00000000 --- a/requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -attrs==21.2.0 -certifi==2020.12.5 -chardet==4.0.0 -colorama==0.4.4 -idna==2.10 -iniconfig==1.1.1 -packaging==20.9 -pluggy==0.13.1 -py==1.10.0 -pyparsing==2.4.7 -pytest==6.2.4 -python-dateutil==2.8.1 -PyYAML==5.4.1 -requests==2.25.1 -six==1.16.0 -supervisor==4.2.2 -time-machine==2.1.0 -toml==0.10.2 -urllib3==1.26.5 -watchdog==2.1.2 diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index 944088cb..00000000 --- a/tests/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Developer Content ------ - -The content of this folder is not for normal usage, it contains test scripts for debugging purposes. diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/run_multiple_tests.py b/tests/run_multiple_tests.py deleted file mode 100755 index 5bcefa64..00000000 --- a/tests/run_multiple_tests.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 - -""" -Test script, for testing different situations, like: - - different day_start times - - different situ where playlist is empty, not long enough or to long -""" - -import json -import os -import sys -from datetime import datetime -from threading import Thread -from time import sleep -from unittest.mock import patch -from zoneinfo import ZoneInfo - -import time_machine - -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -# from ffplayout import playlist - -# set time zone -_TZ = ZoneInfo("Europe/Berlin") - - -def run_at(time_tuple): - dt = datetime(*time_tuple, tzinfo=_TZ).strftime("%Y-%m-%d %H:%M:%S") - - @time_machine.travel(dt) - def run_in_time_machine(): - desktop.output() - - print(f'simulated date and time: {dt}\n') - - run_in_time_machine() - - -def run_time(seconds): - """ - validate json values in new thread - and test if source paths exist - """ - def timer(seconds): - print(f'run test for {seconds} seconds...') - sleep(seconds) - terminate_processes() - print('terminated successfully') - - terminator = Thread(name='timer', target=timer, args=(seconds,)) - terminator.daemon = True - terminator.start() - - -def print_separater(): - print('\n') - print(79 * '-') - print(79 * '-') - - -def shorten_playlist(file): - json_object = json.load(file) - del json_object['program'][-1:] - return json_object - - -def extend_playlist(file): - json_object = json.load(file) - elems = json_object['program'][:2] - json_object['program'].extend(elems) - return json_object - - -def clear_playlist(file): - return {} - - -@patch('ffplayout.playlist.valid_json', shorten_playlist) -def run_with_less_elements(time_tuple): - run_at(time_tuple) - - -@patch('ffplayout.playlist.valid_json', extend_playlist) -def run_with_more_elements(time_tuple): - run_at(time_tuple) - - -@patch('ffplayout.playlist.valid_json', clear_playlist) -def run_with_no_elements(time_tuple): - run_at(time_tuple) - - -if __name__ == '__main__': - from ffplayout.output import desktop - from ffplayout.utils import playlist, terminate_processes - - print('\ntest playlists, which are empty') - playlist.start = 0 - run_time(140) - run_with_no_elements((2021, 2, 15, 23, 59, 53)) - - print_separater() - - print('\ntest playlists, which are to short') - playlist.start = 0 - run_time(140) - run_with_less_elements((2021, 2, 15, 23, 58, 3)) - - print_separater() - - print('\ntest playlists, which are to long') - playlist.start = 0 - run_time(140) - run_with_more_elements((2021, 2, 15, 23, 59, 33)) - - print_separater() - - print('\ntest transition from playlists, with day_start at: 05:59:25') - playlist.start = 21575 - run_time(140) - run_at((2021, 2, 17, 5, 58, 3)) - - print_separater() - - print('\ntest transition from playlists, with day_start at: 20:00:00') - playlist.start = 72000 - run_time(140) - run_at((2021, 2, 17, 19, 58, 23)) diff --git a/tests/run_playlist.py b/tests/run_playlist.py deleted file mode 100755 index ae794ce4..00000000 --- a/tests/run_playlist.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 - -import datetime -import os -import sys -from time import sleep -from zoneinfo import ZoneInfo - -import time_machine - -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - - -# set time zone -_TZ = ZoneInfo("Europe/Berlin") -# fake date and time -SOURCE_TIME = [2021, 2, 27, 23, 55, 0] - - -@time_machine.travel(datetime.datetime(*SOURCE_TIME, tzinfo=_TZ)) -def main(): - get_source = GetSourceIter() - - for node in get_source.next(): - messenger.info(f'Play: {node["source"]}') - # print(node) - sleep(node['out'] - node['seek']) - - -if __name__ == '__main__': - from ffplayout.player.playlist import GetSourceIter - from ffplayout.utils import messenger - try: - main() - except KeyboardInterrupt: - print('\n', end='') diff --git a/tests/run_time_machine.py b/tests/run_time_machine.py deleted file mode 100755 index 7c6230cb..00000000 --- a/tests/run_time_machine.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 - -""" -Test script, for simulating different date and time. -This is useful for testing the transition from one playlist to another, -specially when the day_start time is in the night. -""" - -import datetime -import os -import sys -from importlib import import_module -from unittest.mock import patch -from zoneinfo import ZoneInfo - -import time_machine - -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -# set time zone -_TZ = ZoneInfo("Europe/Berlin") -# fake date and time -SOURCE_TIME = [2022, 1, 5, 5, 57, 10] -FAKE_DELTA = -2.2 - - -def fake_delta(node): - """ - override list init function for fake delta - """ - - delta, total_delta = get_delta(node['begin']) - seek = abs(delta) + node['seek'] if abs(delta) + node['seek'] >= 1 else 0 - seek = round(seek, 3) - - seek += FAKE_DELTA - - if node['out'] - seek > total_delta: - out = total_delta + seek - else: - out = node['out'] - - if out - seek > 1: - node['out'] = out - node['seek'] = seek - return src_or_dummy(node) - - return None - - -@patch('ffplayout.player.playlist.handle_list_init', fake_delta) -@time_machine.travel(datetime.datetime(*SOURCE_TIME, tzinfo=_TZ)) -def run_in_time_machine(): - if stdin_args.output: - output = import_module(f'ffplayout.output.{stdin_args.output}').output - output() - else: - desktop.output() - - -if __name__ == '__main__': - from ffplayout.output import desktop - from ffplayout.utils import get_delta, src_or_dummy, stdin_args - run_in_time_machine() diff --git a/tests/run_time_warp.py b/tests/run_time_warp.py deleted file mode 100755 index 294f7ce8..00000000 --- a/tests/run_time_warp.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python3 - -""" -Test script, for simulating speed up the clock. -With the WARP_FACTOR you can transform a second to a fraction. -With this functionality it is possible to run a 24 hours playlist in a minute, -and debug the playlist reader. -""" - -import datetime -import os -import sys -import time -from zoneinfo import ZoneInfo - -import time_machine - -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -# set time zone -_TZ = ZoneInfo("Europe/Berlin") -# fake date and time -SOURCE_TIME = [2021, 2, 12, 5, 0, 0] -USE_TIME_MACHINE = True - -# warp time by factor -WARP_FACTOR = 1000 - - -def warp_time(): - get_source = GetSourceIter() - stamp = time.time() - duration = 0 - with time_machine.travel(stamp, tick=False) as traveller: - for node in get_source.next(): - duration = node['out'] - node['seek'] - messenger.info(f'Play: "{node["source"]}"') - - warp_duration = duration / WARP_FACTOR - messenger.debug(f'Original duration {duration} ' - f'warped to {warp_duration:.3f}') - - time.sleep(warp_duration) - stamp += duration - - traveller.move_to(stamp) - - -@time_machine.travel(datetime.datetime(*SOURCE_TIME, tzinfo=_TZ)) -def run_in_time_machine(): - warp_time() - - -def run_in_time_warp(): - warp_time() - - -if __name__ == '__main__': - from ffplayout.player.playlist import GetSourceIter - from ffplayout.utils import messenger - - try: - if USE_TIME_MACHINE: - run_in_time_machine() - else: - warp_time() - except KeyboardInterrupt: - print('Interrupted') diff --git a/tests/test_filters.py b/tests/test_filters.py deleted file mode 100644 index 62ef19f2..00000000 --- a/tests/test_filters.py +++ /dev/null @@ -1,138 +0,0 @@ -""" -test classes and functions in filters/default.py -""" - -import argparse -import sys -from types import SimpleNamespace -from unittest.mock import patch - -from ..ffplayout.filters.default import (add_audio, add_loudnorm, - custom_filter, deinterlace_filter, - extend_audio, extend_video, - fade_filter, fps_filter, - overlay_filter, pad_filter, - realtime_filter, scale_filter, - split_filter, text_filter) -from ..ffplayout.utils import lower_third, pre, sync_op - - -def test_text_filter(): - lower_third.add_text = True - lower_third.over_pre = True - lower_third.address = '127.0.0.1:5555' - lower_third.fontfile = '' - - assert text_filter() == [ - "null,zmq=b=tcp\\\\://'127.0.0.1:5555',drawtext=text=''"] - - -def test_deinterlace_filter(): - probe = SimpleNamespace(video=[{'field_order': 'tff'}]) - - assert deinterlace_filter(probe) == ['yadif=0:-1:0'] - - -def test_pad_filter(): - probe = SimpleNamespace(video=[{'aspect': 1.333}]) - - assert pad_filter(probe) == ['pad=ih*1024/576/sar:ih:(ow-iw)/2:(oh-ih)/2'] - - -def test_fps_filter(): - probe = SimpleNamespace(video=[{'fps': 29.97}]) - - assert fps_filter(probe) == ['fps=25'] - - -def test_scale_filter(): - probe = SimpleNamespace(video=[{'width': 1440, 'height': 1080, - 'aspect': 1.333}]) - - assert scale_filter(probe) == ['scale=1024:576', 'setdar=dar=1.778'] - - -def test_fade_filter(): - assert fade_filter(300, 5, 300) == ['fade=in:st=0:d=0.5'] - assert fade_filter(300, 5, 300, 'a') == ['afade=in:st=0:d=0.5'] - assert fade_filter(300, 0, 200) == ['fade=out:st=199.0:d=1.0'] - assert fade_filter(300, 0, 200, 'a') == ['afade=out:st=199.0:d=1.0'] - - -def test_overlay_filter(): - assert overlay_filter(300, True, False, False) == '[v]null' - assert overlay_filter(300, False, True, False) == ( - 'movie=docs/logo.png,loop=loop=-1:size=1:start=0,format=rgba,' - 'colorchannelmixer=aa=0.7,fade=in:st=0:d=1.0:alpha=1[l];' - '[v][l]overlay=W-w-12:12:shortest=1') - assert overlay_filter(300, False, False, True) == ( - 'movie=docs/logo.png,loop=loop=-1:size=1:start=0,format=rgba,' - 'colorchannelmixer=aa=0.7,fade=out:st=299:d=1.0:alpha=1[l];' - '[v][l]overlay=W-w-12:12:shortest=1') - assert overlay_filter(300, False, False, False) == ( - 'movie=docs/logo.png,loop=loop=-1:size=1:start=0,format=rgba,' - 'colorchannelmixer=aa=0.7[l];[v][l]overlay=W-w-12:12:shortest=1') - - -def test_add_audio(): - probe = SimpleNamespace(audio=False, src='/path/file.mp4') - - assert add_audio(probe, 300) == [ - ('aevalsrc=0:channel_layout=stereo:duration=300:sample_rate=48000')] - - -def test_add_loudnorm(): - pre.add_loudnorm = True - pre.loud_i = -18 - pre.loud_tp = -1.5 - pre.loud_lra = 11 - probe = SimpleNamespace(audio=True) - - assert add_loudnorm(probe) == ['loudnorm=I=-18:TP=-1.5:LRA=11'] - - -def test_extend_audio(): - probe = SimpleNamespace(audio=[{'duration': 299}]) - - assert extend_audio(probe, 300, 0) == ['apad=whole_dur=300'] - assert extend_audio(probe, 300, 10) == ['apad=whole_dur=290'] - - -def test_extend_video(): - probe = SimpleNamespace(video=[{'duration': 299}]) - - assert extend_video(probe, 300, 0) == [ - 'tpad=stop_mode=add:stop_duration=1.0'] - - assert extend_video(probe, 300, 10) == [ - 'tpad=stop_mode=add:stop_duration=1.0'] - - -def test_realtime_filter(): - sync_op.realtime = True - - assert realtime_filter(300) == ',realtime=speed=1' - assert realtime_filter(300, 'a') == ',arealtime=speed=1' - - sync_op.time_delta = -1.0 - - assert realtime_filter(300) == ',realtime=speed=1.0033444816053512' - - -def test_split_filter(): - pre.output_count = 1 - assert split_filter('v') == '[vout1]' - - pre.output_count = 3 - assert split_filter('v') == ',split=3[vout1][vout2][vout3]' - assert split_filter('a') == ',asplit=3[aout1][aout2][aout3]' - - -# pylint: disable=unused-argument -@patch('argparse.ArgumentParser.parse_args', return_value=argparse.Namespace( - config='', start='', length='', log='', output='', play_mode='', - playlist='', loop='', volume='0.001')) -def test_custom_filter(*args): - sys.path.append('') - # lower_third.fontfile = '' - assert custom_filter('a', None) == ['volume=0.001'] diff --git a/tests/test_playlist.py b/tests/test_playlist.py deleted file mode 100644 index b6100ed7..00000000 --- a/tests/test_playlist.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -test classes and functions in playlist.py -""" - -import datetime -from zoneinfo import ZoneInfo - -import time_machine - -from ..ffplayout.player.playlist import (handle_list_end, handle_list_init, - timed_source) -from ..ffplayout.utils import playlist, storage - -# set time zone -_TZ = ZoneInfo("Europe/Berlin") - - -@time_machine.travel(datetime.datetime(*[2021, 5, 31, 6, 0, 20], tzinfo=_TZ)) -def test_handle_list_init(): - playlist.start = 6 * 60 * 60 - storage.filler = '' - node = {'source': '/store/file.mp4', 'begin': 21620, - 'in': 0, 'out': 300, 'duration': 300, 'seek': 20} - - color_src = ('color=c=#121212:s=1024x576:d=280.0:r=25,' - 'format=pix_fmts=yuv420p') - - list_init = handle_list_init(node) - list_init.pop('probe') - check_result = { - 'source': color_src, - 'begin': 21620, 'in': 0, 'out': 300, - 'duration': 300, 'seek': 20.0, - 'src_cmd': [ - '-f', 'lavfi', '-i', color_src, - '-f', 'lavfi', '-i', 'anoisesrc=d=280.0:c=pink:r=48000:a=0.05']} - - assert list_init == check_result - - -@time_machine.travel(datetime.datetime(*[2021, 5, 31, 5, 59, 30], tzinfo=_TZ)) -def test_handle_list_end(): - playlist.start = 6 * 60 * 60 - storage.filler = '' - - node = {'source': '/store/file.mp4', 'begin': 24 * 3600 - 30, - 'in': 0, 'out': 300, 'duration': 300, 'seek': 0} - - color_src = ('color=c=#121212:s=1024x576:d=30:r=25,' - 'format=pix_fmts=yuv420p') - - check_result = { - 'source': color_src, - 'begin': 24 * 3600 - 30, 'in': 0, 'out': 30, - 'duration': 300, 'seek': 0, - 'src_cmd': [ - '-f', 'lavfi', '-i', color_src, - '-f', 'lavfi', '-i', 'anoisesrc=d=30:c=pink:r=48000:a=0.05']} - - list_end = handle_list_end(30, node) - list_end.pop('probe') - - assert list_end == check_result - - -@time_machine.travel(datetime.datetime(*[2021, 5, 31, 5, 50, 00], tzinfo=_TZ)) -def test_timed_source(): - playlist.start = 6 * 60 * 60 - storage.filler = '' - - node = {'source': '/store/file.mp4', 'begin': 24 * 3600 + 21600 - 600, - 'in': 0, 'out': 300, 'duration': 300, 'seek': 0} - - color_src = ('color=c=#121212:s=1024x576:d=300:r=25,' - 'format=pix_fmts=yuv420p') - - check_result = { - 'source': color_src, - 'begin': 24 * 3600 + 21600 - 600, 'in': 0, 'out': 300, - 'duration': 300, 'seek': 0, - 'src_cmd': [ - '-f', 'lavfi', '-i', color_src, - '-f', 'lavfi', '-i', 'anoisesrc=d=300:c=pink:r=48000:a=0.05']} - - src = timed_source(node, False) - src.pop('probe') - - assert src == check_result diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index ce255404..00000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -test classes and functions in utils.py -""" - - -import datetime -from zoneinfo import ZoneInfo - -import time_machine - -from ..ffplayout.utils import (gen_dummy, gen_filler, get_date, get_delta, - get_float, is_advertisement, loop_input, - playlist, pre, seek_in, set_length, - src_or_dummy, storage, str_to_sec) - -# set time zone -_TZ = ZoneInfo("Europe/Berlin") - - -def test_str_to_sec(): - assert str_to_sec('06:00:00') == 21600 - - -@time_machine.travel(datetime.datetime(*[2021, 5, 26, 15, 30, 5], tzinfo=_TZ)) -def test_get_delta(): - playlist.start = 0 - current, total = get_delta(15 * 3600 + 30 * 60 + 1) - assert current == -4 - assert total == 8 * 3600 + 29 * 60 + 55 - - -@time_machine.travel(datetime.datetime(*[2021, 5, 26, 0, 0, 0], tzinfo=_TZ)) -def test_playlist_start_zero(): - playlist.start = 0 - assert get_date(False, 24 * 60 * 60 + 1) == '2021-05-27' - assert get_date(False, 24 * 60 * 60 - 1) == '2021-05-26' - - -@time_machine.travel(datetime.datetime(*[2021, 5, 26, 5, 59, 59], tzinfo=_TZ)) -def test_playlist_start_six_before(): - playlist.start = 6 * 60 * 60 - assert get_date(True) == '2021-05-25' - assert get_date(False) == '2021-05-26' - - -@time_machine.travel(datetime.datetime(*[2021, 5, 26, 6, 0, 0], tzinfo=_TZ)) -def test_playlist_start_six_after(): - playlist.start = 6 * 60 * 60 - assert get_date(False) == '2021-05-26' - - -def test_get_float(): - assert get_float('5') == 5 - assert get_float('5', None) == 5.0 - assert get_float('5a', None) is None - - -def test_is_advertisement(): - assert is_advertisement({'category': 'advertisement'}) is True - assert is_advertisement({'category': ''}) is False - assert is_advertisement({}) is False - - -def test_seek_in(): - assert seek_in(10) == ['-ss', '10'] - assert seek_in(0) == [] - - -def test_set_length(): - assert set_length(300, 50, 200) == ['-t', '150'] - assert set_length(300, 0, 300) == [] - - -def test_loop_input(): - assert loop_input('/store/file.mp4', 300, 450) == ['-stream_loop', '2', - '-i', '/store/file.mp4', - '-t', '450'] - - -def test_gen_dummy(): - pre.w = 1024 - pre.h = 576 - pre.fps = 25 - assert gen_dummy(30) == ['-f', 'lavfi', '-i', - 'color=c=#121212:s=1024x576:d=30:r=25,' - 'format=pix_fmts=yuv420p', '-f', 'lavfi', - '-i', 'anoisesrc=d=30:c=pink:r=48000:a=0.05'] - - -def test_gen_filler(): - storage.filler = '' - color_src = 'color=c=#121212:s=1024x576:d=300:r=25,format=pix_fmts=yuv420p' - source = gen_filler({'source': '/store/file.mp4', - 'in': 0, 'out': 300, 'duration': 300, 'seek': 0}) - filler = {'duration': 300, 'in': 0, 'out': 300, 'seek': 0, - 'source': color_src, - 'src_cmd': [ - '-f', 'lavfi', '-i', color_src, - '-f', 'lavfi', '-i', 'anoisesrc=d=300:c=pink:r=48000:a=0.05'] - } - - source.pop('probe') - - assert source == filler - - -def test_src_or_dummy(): - storage.filler = '' - color_src = 'color=c=#121212:s=1024x576:d=300:r=25,format=pix_fmts=yuv420p' - source = src_or_dummy({'source': '/store/file.mp4', - 'in': 0, 'out': 300, 'duration': 300, 'seek': 0}) - - dummy = {'duration': 300, 'in': 0, 'out': 300, 'seek': 0, - 'source': color_src, - 'src_cmd': [ - '-f', 'lavfi', '-i', color_src, - '-f', 'lavfi', '-i', 'anoisesrc=d=300:c=pink:r=48000:a=0.05']} - - source.pop('probe') - - assert source == dummy