Source code for pyctools.components.io.videofilereader

#  Pyctools - a picture processing algorithm development kit.
#  http://github.com/jim-easterbrook/pyctools
#  Copyright (C) 2014-20  Pyctools contributors
#
#  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
#  <http://www.gnu.org/licenses/>.

from __future__ import print_function

__all__ = ['VideoFileReader']
__docformat__ = 'restructuredtext en'

from contextlib import contextmanager
import json
import logging
import os
import re
import signal
import subprocess
import sys

import numpy

from pyctools.core.config import ConfigBool, ConfigEnum, ConfigInt, ConfigPath
from pyctools.core.base import Component
from pyctools.core.frame import Metadata
from pyctools.core.types import pt_float


[docs] class VideoFileReader(Component): """Read conventional video files (mp4, flv, AVI, etc.). :py:class:`VideoFileReader` has been superseded by :py:class:`~.videofilereader2.VideoFileReader2`. This component uses FFmpeg_ to read video from a wide variety of formats. Make sure you have installed FFmpeg before attempting to use :py:class:`VideoFileReader`. The ``zperiod`` config item can be used to adjust the repeat period so it is an integer multiple of a chosen number, e.g. 4 frames for a PAL encoded sequence. It has no effect if ``looping`` is ``off``. =========== ==== ==== Config =========== ==== ==== ``path`` str Path name of file to be read. ``looping`` str Whether to play continuously. Can be ``'off'`` or ``'repeat'``. ``type`` str Output data type. Can be ``'RGB'`` or ``'Y'``. ``16bit`` bool Attempt to get greater precision than normal 8-bit range. ``noaudit`` bool Don't output file's "audit trail" metadata. ``zperiod`` int Adjust repeat period to an integer multiple of ``zperiod``. =========== ==== ==== .. _FFmpeg: https://www.ffmpeg.org/ """ inputs = [] def initialise(self): print('Deprecation warning: ' 'please use VideoFileReader2 instead of VideoFileReader') self.config['path'] = ConfigPath() self.config['looping'] = ConfigEnum(choices=('off', 'repeat')) self.config['type'] = ConfigEnum(choices=('RGB', 'Y')) self.config['16bit'] = ConfigBool() self.config['noaudit'] = ConfigBool() self.config['zperiod'] = ConfigInt(min_value=0) def on_start(self): self.generator = self.file_reader() def process_frame(self): frame = next(self.generator) self.send('output', frame) @contextmanager def subprocess(self, *arg, **kw): try: sp = subprocess.Popen(*arg, **kw) yield sp finally: sp.send_signal(signal.SIGINT) sp.stdout.close() sp.stderr.close() sp.wait()
[docs] def file_reader(self): """Generator process to read file""" self.update_config() path = self.config['path'] # probe file to get dimensions cmd = ['ffprobe', '-hide_banner', '-loglevel', 'warning', '-show_streams', '-select_streams', 'v:0', '-print_format', 'json', path] p = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, error = p.communicate() if p.returncode: error = error.decode('utf-8') error = error.splitlines()[0] self.logger.critical('ffprobe: %s', error) return output = output.decode('utf-8') header = json.loads(output)['streams'][0] xlen = header['width'] ylen = header['height'] if 'nb_frames' in header: zlen = int(header['nb_frames']) else: zlen = header['duration_ts'] # read file repeatedly to allow looping frame_no = 0 while True: # can change config once per outer loop self.update_config() bit16 = self.config['16bit'] frame_type = self.config['type'] zperiod = self.config['zperiod'] looping = self.config['looping'] if frame_no > 0 and looping == 'off': break noaudit = self.config['noaudit'] if header['pix_fmt'] in ( 'gray16be', 'gray16le', 'rgb48be', 'rgb48le', 'bgr48be', 'bgr48le', 'yuv444p10be', 'yuv444p10le', 'yuv422p16be', 'yuv422p16le', 'yuv444p16be', 'yuv444p16le') and not bit16: self.logger.warning( 'Reading %s data as 8 bit', header['pix_fmt']) # update metadata metadata = Metadata().from_file(path) metadata.set_audit( self, 'data = {}\n'.format(os.path.basename(path)), with_history=not noaudit, with_config=self.config) # set data parameters bps = {'RGB': 3, 'Y': 1}[frame_type] pix_fmt = {'RGB': ('rgb24', 'rgb48le'), 'Y': ('gray', 'gray16le')}[frame_type][bit16] bytes_per_frame = xlen * ylen * bps if bit16: bytes_per_frame *= 2 frames = zlen if zlen > zperiod and zperiod > 1 and looping != 'off': frames -= (frame_no + zlen) % zperiod # open file to read data with self.subprocess( ['ffmpeg', '-v', 'warning', '-an', '-i', path, '-f', 'image2pipe', '-pix_fmt', pix_fmt, '-c:v', 'rawvideo', '-'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=open(os.devnull), bufsize=bytes_per_frame) as sp: for z in range(frames): try: raw_data = sp.stdout.read(bytes_per_frame) except Exception as ex: self.logger.exception(ex) return if not raw_data: # premature end of file self.logger.warning( 'Adjusting zlen from %d to %d', zlen, z) zlen = z - 1 break if bit16: image = numpy.fromstring(raw_data, dtype=numpy.uint16) image = image.astype(pt_float) / pt_float(256.0) else: image = numpy.fromstring(raw_data, dtype=numpy.uint8) frame = self.outframe_pool['output'].get() frame.data = image.reshape((ylen, xlen, bps)) frame.metadata.copy(metadata) frame.type = frame_type frame.frame_no = frame_no yield frame frame_no += 1