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