only remote sources are supported, logging for playout; decoder; encoder
This commit is contained in:
parent
7c7719e224
commit
72d7fdb391
20
README.md
20
README.md
@ -27,7 +27,7 @@ Features
|
|||||||
- no GPU power is needed
|
- no GPU power is needed
|
||||||
- stream to server or play on desktop
|
- stream to server or play on desktop
|
||||||
- on posix systems ffplayout can reload config with *SIGHUP*
|
- on posix systems ffplayout can reload config with *SIGHUP*
|
||||||
- logging to file, or colored output to console
|
- logging to files, or colored output to console
|
||||||
- add filters to input, if is necessary to match output stream:
|
- add filters to input, if is necessary to match output stream:
|
||||||
- **yadif** (deinterlacing)
|
- **yadif** (deinterlacing)
|
||||||
- **pad** (letterbox or pillarbox to fit aspect)
|
- **pad** (letterbox or pillarbox to fit aspect)
|
||||||
@ -83,9 +83,9 @@ JSON Playlist Example
|
|||||||
#### Warning:
|
#### Warning:
|
||||||
(Endless) streaming over multiple days will only work when config have **day_start** value and the **length** value is **24 hours**. If you need only some hours for every day, use a *cron* job, or something similar.
|
(Endless) streaming over multiple days will only work when config have **day_start** value and the **length** value is **24 hours**. If you need only some hours for every day, use a *cron* job, or something similar.
|
||||||
|
|
||||||
Source from URL / Live Stream
|
Remote source from URL
|
||||||
-----
|
-----
|
||||||
You can use sources from url or live stream in that way:
|
You can use sources from remote URL in that way:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
...
|
...
|
||||||
@ -94,19 +94,11 @@ You can use sources from url or live stream in that way:
|
|||||||
"out": 149,
|
"out": 149,
|
||||||
"duration": 149,
|
"duration": 149,
|
||||||
"source": "https://example.org/big_buck_bunny.webm"
|
"source": "https://example.org/big_buck_bunny.webm"
|
||||||
},
|
|
||||||
...
|
|
||||||
{
|
|
||||||
"in": 0,
|
|
||||||
"out": 2531.36,
|
|
||||||
"duration": 0,
|
|
||||||
"source": "rtmp://example.org/live/stream"
|
|
||||||
}
|
}
|
||||||
...
|
|
||||||
```
|
```
|
||||||
But be careful with it, better test it multiple times!
|
But be careful with it, better test it multiple times!
|
||||||
|
|
||||||
More informations in [Wiki](https://github.com/ffplayout/ffplayout-engine/wiki/URL---Live-Source)
|
More informations in [Wiki](https://github.com/ffplayout/ffplayout-engine/wiki/Remote-URL-Source)
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
-----
|
-----
|
||||||
@ -127,7 +119,7 @@ ffplayout also allows the passing of parameters:
|
|||||||
- `-c, --config` use given config file
|
- `-c, --config` use given config file
|
||||||
- `-d, --desktop` preview on desktop
|
- `-d, --desktop` preview on desktop
|
||||||
- `-f, --folder` use folder for playing
|
- `-f, --folder` use folder for playing
|
||||||
- `-l, --log` for user-defined log file, *none* for console output
|
- `-l, --log` for user-defined log path, *none* for console output
|
||||||
- `-i, --loop` loop playlist infinitely
|
- `-i, --loop` loop playlist infinitely
|
||||||
- `-p, --playlist` for playlist file
|
- `-p, --playlist` for playlist file
|
||||||
- `-s, --start` set start time in *hh:mm:ss*, *now* for start with first'
|
- `-s, --start` set start time in *hh:mm:ss*, *now* for start with first'
|
||||||
@ -136,7 +128,7 @@ ffplayout also allows the passing of parameters:
|
|||||||
You can run the command like:
|
You can run the command like:
|
||||||
|
|
||||||
```
|
```
|
||||||
python3 ffplayout.py -l ~/ffplayout.log -p ~/playlist.json -d -s now -t none
|
python3 ffplayout.py -l ~/ -p ~/playlist.json -d -s now -t none
|
||||||
```
|
```
|
||||||
|
|
||||||
Play on Desktop
|
Play on Desktop
|
||||||
|
@ -44,13 +44,15 @@ mail_level = ERROR
|
|||||||
|
|
||||||
|
|
||||||
; Logging to file
|
; Logging to file
|
||||||
; if log_to_file = False > log to stderr (console)
|
; if log_to_file = False > log to console
|
||||||
; path to /var/log/ only if you run this program as deamon
|
; path to /var/log/ only if you run this program as deamon
|
||||||
; log_level can be: DEBUG, INFO, WARNING, ERROR
|
; log_level can be: DEBUG, INFO, WARNING, ERROR
|
||||||
|
; ffmpeg_level can be: INFO, WARNING, ERROR
|
||||||
[LOGGING]
|
[LOGGING]
|
||||||
log_to_file = True
|
log_to_file = True
|
||||||
log_file = /var/log/ffplayout/ffplayout.log
|
log_path = /var/log/ffplayout/
|
||||||
log_level = INFO
|
log_level = INFO
|
||||||
|
ffmpeg_level = ERROR
|
||||||
|
|
||||||
|
|
||||||
; output settings for the pre-compression
|
; output settings for the pre-compression
|
||||||
|
173
ffplayout.py
173
ffplayout.py
@ -122,7 +122,7 @@ def get_time(time_format):
|
|||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# default variables and values from config file
|
# default variables and values
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
_general = SimpleNamespace()
|
_general = SimpleNamespace()
|
||||||
@ -141,6 +141,36 @@ _WINDOWS = os.name == 'nt'
|
|||||||
COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 64 * 1024
|
COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 64 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def ffmpeg_libs():
|
||||||
|
"""
|
||||||
|
check which external libs are compiled in ffmpeg,
|
||||||
|
for using them later
|
||||||
|
"""
|
||||||
|
cmd = ['ffprobe', '-version']
|
||||||
|
libs = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = check_output(cmd).decode('UTF-8')
|
||||||
|
except CalledProcessError as err:
|
||||||
|
messenger.error('ffprobe - libs could not be readed!\n'
|
||||||
|
'Processing is not possible. Error:\n{}'.format(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-', ''))
|
||||||
|
break
|
||||||
|
|
||||||
|
return libs
|
||||||
|
|
||||||
|
|
||||||
|
FF_LIBS = ffmpeg_libs()
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
"""
|
"""
|
||||||
this function can reload most settings from configuration file,
|
this function can reload most settings from configuration file,
|
||||||
@ -223,8 +253,9 @@ def load_config():
|
|||||||
|
|
||||||
if _init.load:
|
if _init.load:
|
||||||
_log.to_file = cfg.getboolean('LOGGING', 'log_to_file')
|
_log.to_file = cfg.getboolean('LOGGING', 'log_to_file')
|
||||||
_log.path = cfg.get('LOGGING', 'log_file')
|
_log.path = cfg.get('LOGGING', 'log_path')
|
||||||
_log.level = cfg.get('LOGGING', 'log_level')
|
_log.level = cfg.get('LOGGING', 'log_level')
|
||||||
|
_log.ff_level = cfg.get('LOGGING', 'ffmpeg_level')
|
||||||
|
|
||||||
_pre_comp.w = cfg.getint('PRE_COMPRESS', 'width')
|
_pre_comp.w = cfg.getint('PRE_COMPRESS', 'width')
|
||||||
_pre_comp.h = cfg.getint('PRE_COMPRESS', 'height')
|
_pre_comp.h = cfg.getint('PRE_COMPRESS', 'height')
|
||||||
@ -284,7 +315,9 @@ class CustomFormatter(logging.Formatter):
|
|||||||
if '"' in msg and '[' in msg:
|
if '"' in msg and '[' in msg:
|
||||||
msg = re.sub('(".*?")', self.cyan + r'\1' + self.reset, msg)
|
msg = re.sub('(".*?")', self.cyan + r'\1' + self.reset, msg)
|
||||||
elif '[decoder]' in msg:
|
elif '[decoder]' in msg:
|
||||||
msg = re.sub(r'(\[decoder\])', self.red + r'\1' + self.reset, 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:
|
elif '/' in msg or '\\' in msg:
|
||||||
msg = re.sub(
|
msg = re.sub(
|
||||||
r'(["\w.:/]+/|["\w.:]+\\.*?)', self.magenta + r'\1', msg)
|
r'(["\w.:/]+/|["\w.:]+\\.*?)', self.magenta + r'\1', msg)
|
||||||
@ -305,20 +338,49 @@ class CustomFormatter(logging.Formatter):
|
|||||||
if stdin_args.log:
|
if stdin_args.log:
|
||||||
_log.path = stdin_args.log
|
_log.path = stdin_args.log
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
playout_logger = logging.getLogger('playout')
|
||||||
logger.setLevel(_log.level)
|
playout_logger.setLevel(_log.level)
|
||||||
console_handler = logging.StreamHandler()
|
decoder_logger = logging.getLogger('decoder')
|
||||||
formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s')
|
decoder_logger.setLevel(_log.ff_level)
|
||||||
|
encoder_logger = logging.getLogger('encoder')
|
||||||
|
encoder_logger.setLevel(_log.ff_level)
|
||||||
|
|
||||||
if _log.to_file and _log.path != 'none':
|
if _log.to_file and _log.path != 'none':
|
||||||
file_handler = TimedRotatingFileHandler(_log.path, when='midnight',
|
if _log.path and os.path.isdir(_log.path):
|
||||||
backupCount=5)
|
playout_log = os.path.join(_log.path, 'ffplayout.log')
|
||||||
file_handler.setFormatter(formatter)
|
decoder_log = os.path.join(_log.path, 'decoder.log')
|
||||||
logger.addHandler(file_handler)
|
encoder_log = os.path.join(_log.path, 'encoder.log')
|
||||||
|
else:
|
||||||
|
playout_log = os.path.join(os.getcwd(), 'ffplayout.log')
|
||||||
|
decoder_log = os.path.join(os.getcwd(), 'ffdecoder.log')
|
||||||
|
encoder_log = os.path.join(os.getcwd(), 'ffencoder.log')
|
||||||
|
|
||||||
|
formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s')
|
||||||
|
p_file_handler = TimedRotatingFileHandler(playout_log, when='midnight',
|
||||||
|
backupCount=5)
|
||||||
|
d_file_handler = TimedRotatingFileHandler(decoder_log, when='midnight',
|
||||||
|
backupCount=5)
|
||||||
|
e_file_handler = TimedRotatingFileHandler(encoder_log, when='midnight',
|
||||||
|
backupCount=5)
|
||||||
|
|
||||||
|
p_file_handler.setFormatter(formatter)
|
||||||
|
d_file_handler.setFormatter(formatter)
|
||||||
|
e_file_handler.setFormatter(formatter)
|
||||||
|
playout_logger.addHandler(p_file_handler)
|
||||||
|
decoder_logger.addHandler(d_file_handler)
|
||||||
|
encoder_logger.addHandler(e_file_handler)
|
||||||
|
|
||||||
|
DEC_PREFIX = ''
|
||||||
|
ENC_PREFIX = ''
|
||||||
else:
|
else:
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
console_handler.setFormatter(CustomFormatter())
|
console_handler.setFormatter(CustomFormatter())
|
||||||
logger.addHandler(console_handler)
|
playout_logger.addHandler(console_handler)
|
||||||
|
decoder_logger.addHandler(console_handler)
|
||||||
|
encoder_logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
DEC_PREFIX = '[decoder] '
|
||||||
|
ENC_PREFIX = '[encoder] '
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@ -352,7 +414,7 @@ class Mailer:
|
|||||||
try:
|
try:
|
||||||
server = smtplib.SMTP(_mail.server, _mail.port)
|
server = smtplib.SMTP(_mail.server, _mail.port)
|
||||||
except socket.error as err:
|
except socket.error as err:
|
||||||
logger.error(err)
|
playout_logger.error(err)
|
||||||
server = None
|
server = None
|
||||||
|
|
||||||
if server is not None:
|
if server is not None:
|
||||||
@ -360,7 +422,7 @@ class Mailer:
|
|||||||
try:
|
try:
|
||||||
login = server.login(_mail.s_addr, _mail.s_pass)
|
login = server.login(_mail.s_addr, _mail.s_pass)
|
||||||
except smtplib.SMTPAuthenticationError as serr:
|
except smtplib.SMTPAuthenticationError as serr:
|
||||||
logger.error(serr)
|
playout_logger.error(serr)
|
||||||
login = None
|
login = None
|
||||||
|
|
||||||
if login is not None:
|
if login is not None:
|
||||||
@ -390,18 +452,18 @@ class Messenger:
|
|||||||
self._mailer = Mailer()
|
self._mailer = Mailer()
|
||||||
|
|
||||||
def debug(self, msg):
|
def debug(self, msg):
|
||||||
logger.debug(msg.replace('\n', ' '))
|
playout_logger.debug(msg.replace('\n', ' '))
|
||||||
|
|
||||||
def info(self, msg):
|
def info(self, msg):
|
||||||
logger.info(msg.replace('\n', ' '))
|
playout_logger.info(msg.replace('\n', ' '))
|
||||||
self._mailer.info(msg)
|
self._mailer.info(msg)
|
||||||
|
|
||||||
def warning(self, msg):
|
def warning(self, msg):
|
||||||
logger.warning(msg.replace('\n', ' '))
|
playout_logger.warning(msg.replace('\n', ' '))
|
||||||
self._mailer.warning(msg)
|
self._mailer.warning(msg)
|
||||||
|
|
||||||
def error(self, msg):
|
def error(self, msg):
|
||||||
logger.error(msg.replace('\n', ' '))
|
playout_logger.error(msg.replace('\n', ' '))
|
||||||
self._mailer.error(msg)
|
self._mailer.error(msg)
|
||||||
|
|
||||||
|
|
||||||
@ -412,15 +474,13 @@ messenger = Messenger()
|
|||||||
# probe media infos
|
# probe media infos
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class MediaProbe:
|
class MediaProbe:
|
||||||
"""
|
"""
|
||||||
get infos about media file, similare to mediainfo
|
get infos about media file, similare to mediainfo
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def load(self, file):
|
def load(self, file):
|
||||||
self.remote_source = ['http', 'https', 'ftp', 'rtmp', 'rtmpe',
|
self.remote_source = ['http', 'https', 'ftp', 'smb', 'sftp']
|
||||||
'rtmps', 'rtp', 'rtsp', 'srt', 'tcp', 'udp']
|
|
||||||
self.src = file
|
self.src = file
|
||||||
self.format = None
|
self.format = None
|
||||||
self.audio = []
|
self.audio = []
|
||||||
@ -510,10 +570,18 @@ def terminate_processes(watcher=None):
|
|||||||
watcher.stop()
|
watcher.stop()
|
||||||
|
|
||||||
|
|
||||||
def decoder_error_reader(std_errors):
|
def ffmpeg_stderr_reader(std_errors, logger, prefix):
|
||||||
try:
|
try:
|
||||||
for line in std_errors:
|
for line in std_errors:
|
||||||
messenger.error('[decoder] {}'.format(line.decode("utf-8")))
|
if _log.ff_level == 'INFO':
|
||||||
|
logger.info('{}{}'.format(
|
||||||
|
prefix, line.decode("utf-8").rstrip()))
|
||||||
|
elif _log.ff_level == 'WARNING':
|
||||||
|
logger.warning('{}{}'.format(
|
||||||
|
prefix, line.decode("utf-8").rstrip()))
|
||||||
|
else:
|
||||||
|
logger.error('{}{}'.format(
|
||||||
|
prefix, line.decode("utf-8").rstrip()))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -874,6 +942,21 @@ def timed_source(probe, src, begin, dur, seek, out, first, last):
|
|||||||
return None, 0, 0, True
|
return None, 0, 0, True
|
||||||
|
|
||||||
|
|
||||||
|
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_comp.add_loudnorm:
|
||||||
|
acodec = 'libtwolame' if 'libtwolame' in FF_LIBS else 'mp2'
|
||||||
|
audio = ['-c:a', acodec, '-b:a', '384k', '-ar', '48000', '-ac', '2']
|
||||||
|
else:
|
||||||
|
audio = ['-c:a', 's302m', '-strict', '-2', '-ar', '48000', '-ac', '2']
|
||||||
|
|
||||||
|
return audio
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# building filters,
|
# building filters,
|
||||||
# when is needed add individuell filters to match output format
|
# when is needed add individuell filters to match output format
|
||||||
@ -1008,14 +1091,10 @@ def add_loudnorm(probe):
|
|||||||
add single pass loudnorm filter to audio line
|
add single pass loudnorm filter to audio line
|
||||||
"""
|
"""
|
||||||
loud_filter = []
|
loud_filter = []
|
||||||
a_samples = int(192000 / _pre_comp.fps)
|
|
||||||
|
|
||||||
if probe.audio and _pre_comp.add_loudnorm:
|
if probe.audio and _pre_comp.add_loudnorm:
|
||||||
loud_filter = [('loudnorm=I={}:TP={}:LRA={},'
|
loud_filter = [('loudnorm=I={}:TP={}:LRA={}').format(
|
||||||
'asetnsamples=n={}').format(_pre_comp.loud_i,
|
_pre_comp.loud_i, _pre_comp.loud_tp, _pre_comp.loud_lra)]
|
||||||
_pre_comp.loud_tp,
|
|
||||||
_pre_comp.loud_lra,
|
|
||||||
a_samples)]
|
|
||||||
|
|
||||||
return loud_filter
|
return loud_filter
|
||||||
|
|
||||||
@ -1496,9 +1575,8 @@ def main():
|
|||||||
'-b:v', '{}k'.format(_pre_comp.v_bitrate),
|
'-b:v', '{}k'.format(_pre_comp.v_bitrate),
|
||||||
'-minrate', '{}k'.format(_pre_comp.v_bitrate),
|
'-minrate', '{}k'.format(_pre_comp.v_bitrate),
|
||||||
'-maxrate', '{}k'.format(_pre_comp.v_bitrate),
|
'-maxrate', '{}k'.format(_pre_comp.v_bitrate),
|
||||||
'-bufsize', '{}k'.format(_pre_comp.v_bufsize),
|
'-bufsize', '{}k'.format(_pre_comp.v_bufsize)
|
||||||
'-c:a', 's302m', '-strict', '-2', '-ar', '48000', '-ac', '2',
|
] + pre_audio_codec() + ['-f', 'mpegts', '-']
|
||||||
'-f', 'mpegts', '-']
|
|
||||||
|
|
||||||
if _text.add_text and os.path.isfile(_text.textfile):
|
if _text.add_text and os.path.isfile(_text.textfile):
|
||||||
messenger.info('Overlay text file: "{}"'.format(_text.textfile))
|
messenger.info('Overlay text file: "{}"'.format(_text.textfile))
|
||||||
@ -1516,17 +1594,24 @@ def main():
|
|||||||
# preview playout to player
|
# preview playout to player
|
||||||
_ff.encoder = Popen([
|
_ff.encoder = Popen([
|
||||||
'ffplay', '-hide_banner', '-nostats', '-i', 'pipe:0'
|
'ffplay', '-hide_banner', '-nostats', '-i', 'pipe:0'
|
||||||
] + overlay, stderr=None, stdin=PIPE, stdout=None)
|
] + overlay, stderr=PIPE, stdin=PIPE, stdout=None)
|
||||||
else:
|
else:
|
||||||
_ff.encoder = Popen([
|
_ff.encoder = Popen([
|
||||||
'ffmpeg', '-v', 'info', '-hide_banner', '-nostats',
|
'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner',
|
||||||
'-re', '-thread_queue_size', '256',
|
'-nostats', '-re', '-thread_queue_size', '256',
|
||||||
'-i', 'pipe:0'] + overlay + _playout.post_comp_video
|
'-i', 'pipe:0'] + overlay + _playout.post_comp_video
|
||||||
+ _playout.post_comp_audio + [
|
+ _playout.post_comp_audio + [
|
||||||
'-metadata', 'service_name=' + _playout.name,
|
'-metadata', 'service_name=' + _playout.name,
|
||||||
'-metadata', 'service_provider=' + _playout.provider,
|
'-metadata', 'service_provider=' + _playout.provider,
|
||||||
'-metadata', 'year={}'.format(year)
|
'-metadata', 'year={}'.format(year)
|
||||||
] + _playout.post_comp_extra + [_playout.out_addr], stdin=PIPE)
|
] + _playout.post_comp_extra + [_playout.out_addr],
|
||||||
|
stdin=PIPE, stderr=PIPE)
|
||||||
|
|
||||||
|
enc_err_thread = Thread(target=ffmpeg_stderr_reader,
|
||||||
|
args=(_ff.encoder.stderr, encoder_logger,
|
||||||
|
ENC_PREFIX))
|
||||||
|
enc_err_thread.daemon = True
|
||||||
|
enc_err_thread.start()
|
||||||
|
|
||||||
if _playlist.mode and not stdin_args.folder:
|
if _playlist.mode and not stdin_args.folder:
|
||||||
watcher = None
|
watcher = None
|
||||||
@ -1548,14 +1633,16 @@ def main():
|
|||||||
messenger.info('Play: "{}"'.format(current_file))
|
messenger.info('Play: "{}"'.format(current_file))
|
||||||
|
|
||||||
with Popen([
|
with Popen([
|
||||||
'ffmpeg', '-v', 'error', '-hide_banner', '-nostats'
|
'ffmpeg', '-v', _log.ff_level.lower(), '-hide_banner',
|
||||||
] + src_cmd + ff_pre_settings,
|
'-nostats'] + src_cmd + ff_pre_settings,
|
||||||
stdout=PIPE, stderr=PIPE) as _ff.decoder:
|
stdout=PIPE, stderr=PIPE) as _ff.decoder:
|
||||||
|
|
||||||
err_thread = Thread(target=decoder_error_reader,
|
dec_err_thread = Thread(target=ffmpeg_stderr_reader,
|
||||||
args=(_ff.decoder.stderr,))
|
args=(_ff.decoder.stderr,
|
||||||
err_thread.daemon = True
|
decoder_logger,
|
||||||
err_thread.start()
|
DEC_PREFIX))
|
||||||
|
dec_err_thread.daemon = True
|
||||||
|
dec_err_thread.start()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
buf = _ff.decoder.stdout.read(COPY_BUFSIZE)
|
buf = _ff.decoder.stdout.read(COPY_BUFSIZE)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user