From ec3ead528be0cb6a0fc12265111324c60924b7f2 Mon Sep 17 00:00:00 2001 From: PavelMuhortov Date: Sun, 20 Jun 2021 15:56:25 +0300 Subject: [PATCH] add ffmpeger.py --- README.md | 40 ++++++++++ ffmpeger.py | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 ffmpeger.py diff --git a/README.md b/README.md index 80dbf5b..9762751 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,45 @@ # utils +* [ffmpeger.py](https://git.hmp.today/pavel.muhortov/utils#ffmpeger-py) * [sendmail.py](https://git.hmp.today/pavel.muhortov/utils#sendmail-py) * [srchproc.py](https://git.hmp.today/pavel.muhortov/utils#srchproc-py) ____ +## ffmpeger.py +**Description:** FFmpeg management from Python +**Dependencies:** Python 3 (tested version 3.9.5), installed or downloaded ffmpeg, srchproc.py in the same directory + +| PARAMETERS | DESCRIPTION | DEFAULT| +|-------------|-------------|--------| +|**-s**, **--src**|sources urls|**REQUIRED**| +|**[-h]**|print help and exit|| +|**[--preset]**|240p, 360p, 480p, 720p, 1080p, 1440p, 2160p|`None`| +|**[--fps]**|frame per second encoding output|`None`| +|**[--dst]**|destination url|`None`| +|**[--ffpath]**|alternative path to bin|`None`| +|**[--watchdog]**|detect ffmpeg freeze and terminate|| +|**[--sec]**|seconds to wait before the watchdog terminates|15| +|**[--mono]**|detect ffmpeg running copy and terminate|| + +Example usage in cron with Python: +```shell +* * * * * /usr/bin/python3 ~/ffmpeger.py -s rtsp://user:pass@host:554/Streaming/Channels/101 --dst rtmp://a.rtmp.youtube.com/live2/YOUKEY --mono --watchdog --sec 30 >> /dev/null 2>&1 +``` +Example usage in terminal with make the script executable: +```shell +chmod u+x ./ffmpeger.py +ffmpeger.py -s rtsp://user:pass@host:554/Streaming/Channels/101 --dst rtp://239.0.0.1:5554 +``` +Example usage in Python: +```Python +from ffmpeger import FFmpeg + +FFmpeg.run(src='null, anull', preset='240p', fps=10) +``` +____ ## sendmail.py +**Description:** Sending email from Python +**Dependencies:** Python 3 (tested version 3.9.5) + | PARAMETERS | DESCRIPTION | DEFAULT| |-------------|-------------|--------| |**-u**, **--user**|smtp valid user|**REQUIRED**| @@ -38,6 +74,9 @@ print(log) ``` ____ ## srchproc.py +**Description:** Find a running process from Python +**Dependencies:** Python 3 (tested version 3.9.5) + | PARAMETERS | DESCRIPTION | DEFAULT| |-------------|-------------|--------| |**[-h]**|print help and exit|| @@ -65,3 +104,4 @@ if processes: for process in processes: print(process) ``` + diff --git a/ffmpeger.py b/ffmpeger.py new file mode 100644 index 0000000..21eaffb --- /dev/null +++ b/ffmpeger.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 + + +from multiprocessing import Process, Queue +from os import path, environ +from subprocess import Popen, PIPE, STDOUT +from sys import platform +from time import sleep +from srchproc import Proc # or copy class Proc from file srchproc.py here for autonomy of this file + + +class FFmpeg: + """ + FFmpeg management from Python + """ + @classmethod + def run(cls, src: str, preset: str = None, fps: int = None, dst: str = None, + ffpath: str = None, watchdog: bool = False, sec: int = 5, mono: bool = False): + """ + Running the installed ffmpeg + :param src: sources urls (example: "rtsp://user:pass@host:554/Streaming/Channels/101, anull") + :param preset: 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p + :param fps: frame per second encoding output + :param dst: destination url (example: rtp://239.0.0.1:5554) + :param ffpath: alternative path to bin (example: /usr/bin/ffmpeg) + :param watchdog: detect ffmpeg freeze and terminate + :param sec: seconds to wait before the watchdog terminates + :param mono: detect ffmpeg running copy and terminate + :return: None + """ + process = cls._bin(ffpath).split()+cls._src(src).split()+cls._preset(preset, fps).split()+cls._dst(dst).split() + if mono and Proc.search(' '.join(process)): + print('Process already exist, exit...') + else: + with Popen(process, stdout=PIPE, stderr=STDOUT) as proc: + if watchdog: + que = Queue() + Process(target=cls._watchdog, args=(proc.pid, sec, que,), daemon=True).start() + for line in proc.stdout: + if not que: + print(line, flush=True) + else: + que.put(line) + exit() + + @classmethod + def _bin(cls, path_ffmpeg: str): + """ + Returns the path to the ffmpeg depending on the OS + :param path_ffmpeg: alternative path to bin + :return: path to ffmpeg + """ + faq = ('\n' + 'Main download page: https://ffmpeg.org/download.html\n' + '\n' + 'Install on Linux (Debian):\n' + '\tsudo apt install -y ffmpeg\n' + '\tTarget: /usr/bin/ffmpeg\n' + '\n' + 'Install on Windows:\n' + '\tDownload and extract archive from: https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z\n' + '\tTarget: "%PROGRAMFILES%\\ffmpeg\\bin\\ffmpeg.exe"\n' + '\n' + 'Install on MacOS:\n' + '\tDownload and extract archive from: https://evermeet.cx/ffmpeg/\n' + '\tTarget: /usr/bin/ffmpeg\n') + if not path_ffmpeg: + if platform.startswith('linux'): + path_ffmpeg = '/usr/bin/ffmpeg' + elif platform.startswith('win32'): + path_ffmpeg = environ['PROGRAMFILES'] + "\\ffmpeg\\bin\\ffmpeg.exe" + elif platform.startswith('darwin'): + path_ffmpeg = '/usr/bin/ffmpeg' + if path.exists(path_ffmpeg): + return path_ffmpeg + else: + print('ON', platform, 'PLATFORM', 'not found ffmpeg', faq) + return None + + @classmethod + def _src(cls, sources: str): + """ + Parsing sources into ffmpeg format + :param sources: comma-separated list of sources in string format + :return: ffmpeg format list of sources + """ + list_sources = [] + for src in sources.split(','): + src = src.strip() + if 'null' in src: + src = ' '.join(['-f lavfi -i', src]) + elif 'rtsp' in src: + src = ' '.join(['-rtsp_transport tcp -i', src]) + else: + src = ' '.join(['-i', src]) + list_sources.append(src) + return ' '.join(list_sources) + + @classmethod + def _preset(cls, choice: str, fps: int): + """ + Parsing preset into ffmpeg format + :param choice: preset selection + :param fps: frame per second encoding output + :return: ffmpeg format encoding parameters + """ + tune = '-tune zerolatency' + video = '-c:v copy' + audio = '-c:a aac -b:a 128k' + width, height, kbps = None, None, None + if choice: + if choice == '240p': + width, height, kbps = 426, 240, 480 + if choice == '360p': + width, height, kbps = 640, 360, 720 + if choice == '480p': + width, height, kbps = 854, 480, 1920 + if choice == '720p': + width, height, kbps = 1280, 720, 3960 + if choice == '1080p': + width, height, kbps = 1920, 1080, 5940 + if choice == '1440p': + width, height, kbps = 2560, 1440, 12960 + if choice == '2160p': + width, height, kbps = 3840, 2160, 32400 + if width and height and kbps: + video = ''.join(['-vf scale=', str(width), ':', str(height), ',setsar=1:1']) + video = ' '.join([video, '-c:v libx264 -pix_fmt yuv420p -preset ultrafast']) + if fps: + video = ' '.join([video, '-r', str(fps), '-g', str(fps * 2)]) + video = ' '.join([video, '-b:v', str(kbps) + 'k']) + return ' '.join([tune, video, audio]) + + @classmethod + def _dst(cls, destination: str): + """ + Parsing destination into ffmpeg format + :param destination: + :return: ffmpeg format destination + """ + container = '-f null' + stdout = '-v debug' # '-nostdin -nostats' # '-report' + if destination: + if 'rtmp' in destination: + container = '-f flv' + elif "rtp" in destination: + container = '-f rtp_mpegts' + else: + destination = '-' + return ' '.join([container, destination, stdout]) + + @classmethod + def _watchdog(cls, pid: int, sec: int = 5, que: Queue = None): + """ + If no data arrives in the queue, kill the process + :param pid: process ID + :param sec: seconds to wait for data + :param que: queue pointer + :return: None + """ + if que: + while True: + while not que.empty(): + print(que.get()) + sleep(sec) + if que.empty(): + cls._kill(pid) + print('exit by watchdog') + break + exit() + + @classmethod + def _kill(cls, pid: int): + """ + Kill the process by means of the OS + :param pid: process ID + :return: None + """ + if platform.startswith('linux'): + Popen(['kill', '-s', 'SIGKILL', str(pid)]) + elif platform.startswith('win32'): + Popen(['taskkill', '/PID', str(pid), '/F']) + elif platform.startswith('darwin'): + Popen(['kill', '-s', 'SIGKILL', str(pid)]) + + +if __name__ == "__main__": + from argparse import ArgumentParser + + args = ArgumentParser( + prog='FFmpeger', + description='FFmpeg management from Python', + epilog='Dependencies: ' + 'Python 3 (tested version 3.9.5), ' + 'installed or downloaded ffmpeg, ' + 'srchproc.py in the same directory' + ) + args.add_argument('-s', '--src', type=str, required=True, + help='sources urls (example: "rtsp://user:pass@host:554/Streaming/Channels/101, anull")') + args.add_argument('--preset', type=str, default=None, required=False, + help='240p, 360p, 480p, 720p, 1080p, 1440p, 2160p') + args.add_argument('--fps', type=int, default=None, required=False, + help='frame per second encoding output') + args.add_argument('--dst', type=str, default=None, required=False, + help='destination url (example: rtp://239.0.0.1:5554)') + args.add_argument('--ffpath', type=str, default=None, required=False, + help='alternative path to bin (example: /usr/bin/ffmpeg)') + args.add_argument('--watchdog', action='store_true', required=False, + help='detect ffmpeg freeze and terminate') + args.add_argument('--sec', type=int, default=15, required=False, + help='seconds to wait before the watchdog terminates') + args.add_argument('--mono', action='store_true', required=False, + help='detect ffmpeg running copy and terminate') + args = vars(args.parse_args()) + + FFmpeg.run(src=args['src'], preset=args['preset'], fps=args['fps'], dst=args['dst'], + ffpath=args['ffpath'], watchdog=args['watchdog'], sec=args['sec'], mono=args['mono'])