Repository: imageio/imageio

Issue:
ffmpeg: you are trying to apply an input option to an output file or vice versa. Move this option before the file it belongs to.
I'm trying to get imageio reading only I-frames with ffmpeg plugin. Therefore, I'm using the following line in python:

`video_reader = imageio.get_reader(video_path, 'ffmpeg', ffmpeg_params=['-vf', 'select eq(pict_type\,PICT_TYPE_I)'])`

During the initialisation I get the following message from ffmpeg:

`ffmpeg version 3.3.3 Copyright (c) 2000-2017 the FFmpeg developers
  built with gcc 4.8 (Ubuntu 4.8.4-2ubuntu1~14.04.3)
  configuration: --extra-libs=-ldl --prefix=/opt/ffmpeg --mandir=/usr/share/man --enable-avresample --disable-debug --enable-nonfree --enable-gpl --enable-version3 --enable-libopencore-amrnb --enable-libopencore-amrwb --disable-decoder=amrnb --disable-decoder=amrwb --enable-libpulse --enable-libfreetype --enable-gnutls --disable-ffserver --enable-libx264 --enable-libx265 --enable-libfdk-aac --enable-libvorbis --enable-libtheora --enable-libmp3lame --enable-libopus --enable-libvpx --enable-libspeex --enable-libass --enable-avisynth --enable-libsoxr --enable-libxvid --enable-libvidstab --enable-libwavpack --enable-nvenc --enable-libzimg
  libavutil      55. 58.100 / 55. 58.100
  libavcodec     57. 89.100 / 57. 89.100
  libavformat    57. 71.100 / 57. 71.100
  libavdevice    57.  6.100 / 57.  6.100
  libavfilter     6. 82.100 /  6. 82.100
  libavresample   3.  5.  0 /  3.  5.  0
  libswscale      4.  6.100 /  4.  6.100
  libswresample   2.  7.100 /  2.  7.100
  libpostproc    54.  5.100 / 54.  5.100
Option vf (set video filters) cannot be applied to input url /home/konsof01/Work/FaceBackend/TestData/TestMedia/me.mp4 -- you are trying to apply an input option to an output file or vice versa. Move this option before the file it belongs to.
Error parsing options for input file /home/konsof01/Work/FaceBackend/TestData/TestMedia/me.mp4.
Error opening input files: Invalid argument`

The end of this message clearly says that the given option is being put into a wrong place. Is this by-design or am I doing something wrong?

Is it a good idea to have a way to tell that some options are for output files, not for input?

Thanks,
Konstantin

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/15938362/badge)](https://coveralls.io/builds/15938362)

Coverage increased (+0.01%) to 91.796% when pulling **ca0257922f85a3b76bcb813670da9feab6702b7e on ffmpeg** into **a22145a9ebbd3aa3bc742911e052ed1903381486 on master**.


Patch:
diff --git a/imageio/plugins/ffmpeg.py b/imageio/plugins/ffmpeg.py
index ae17fe2..61be3bf 100644
--- a/imageio/plugins/ffmpeg.py
+++ b/imageio/plugins/ffmpeg.py
@@ -174,6 +174,11 @@ class FfmpegFormat(Format):
     
     Parameters for reading
     ----------------------
+    fps : scalar
+        The number of frames per second to read the data at. Default None (i.e.
+        read at the file's own fps). One can use this for files with a
+        variable fps, or in cases where imageio is unable to correctly detect
+        the fps.
     loop : bool
         If True, the video will rewind as soon as a frame is requested
         beyond the last frame. Otherwise, IndexError is raised. Default False.
@@ -187,10 +192,14 @@ class FfmpegFormat(Format):
         "gray"). The camera needs to support the format in order for
         this to take effect. Note that the images produced by this
         reader are always rgb8.
-    ffmpeg_params: list
+    input_params : list
         List additional arguments to ffmpeg for input file options.
+        (Can also be provided as ``ffmpeg_params`` for backwards compatibility)
         Example ffmpeg arguments to use aggressive error handling:
         ['-err_detect', 'aggressive']
+    output_params : list
+        List additional arguments to ffmpeg for output file options (i.e. the
+        stream being read by imageio).
     print_info : bool
         Print information about the video file as reported by ffmpeg.
     
@@ -205,7 +214,7 @@ class FfmpegFormat(Format):
     quality : float | None
         Video output quality. Default is 5. Uses variable bit rate. Highest
         quality is 10, lowest is 0. Set to None to prevent variable bitrate
-        flags to FFMPEG so you can manually specify them using ffmpeg_params
+        flags to FFMPEG so you can manually specify them using output_params
         instead. Specifying a fixed bitrate using 'bitrate' disables this
         parameter.
     bitrate : int | None
@@ -217,8 +226,12 @@ class FfmpegFormat(Format):
     pixelformat: str
         The output video pixel format. Default is 'yuv420p' which most widely
         supported by video players.
-    ffmpeg_params: list
+    input_params : list
+        List additional arguments to ffmpeg for input file options (i.e. the
+        stream that imageio provides).
+    output_params : list
         List additional arguments to ffmpeg for output file options.
+        (Can also be provided as ``ffmpeg_params`` for backwards compatibility)
         Example ffmpeg arguments to use only intra frames and set aspect ratio:
         ['-intra', '-aspect', '16:9']
     ffmpeg_log_level: str
@@ -285,7 +298,7 @@ class FfmpegFormat(Format):
                 infos = proc.stderr.read().decode('utf-8')
                 # Return device name at index
                 try:
-                    name = self._parse_device_names(infos)[index]
+                    name = parse_device_names(infos)[index]
                 except IndexError:
                     raise IndexError('No ffdshow camera at index %i.' % index)
                 return 'video=%s' % name
@@ -299,26 +312,9 @@ class FfmpegFormat(Format):
             else:  # pragma: no cover
                 return '??'
 
-        @staticmethod
-        def _parse_device_names(ffmpeg_output):
-            """ Parse the output of the ffmpeg -list-devices command"""
-            device_names = []
-            in_video_devices = False
-            for line in ffmpeg_output.splitlines():
-                if line.startswith('[dshow'):
-                    logging.debug(line)
-                    line = line.split(']', 1)[1].strip()
-                    if in_video_devices and line.startswith('"'):
-                        device_names.append(line[1:-1])
-                    elif 'video devices' in line:
-                        in_video_devices = True
-                    elif 'devices' in line:
-                        # set False for subsequent "devices" sections
-                        in_video_devices = False
-            return device_names
-
         def _open(self, loop=False, size=None, pixelformat=None,
-                  ffmpeg_params=None, print_info=False):
+                  print_info=False, ffmpeg_params=None,
+                  input_params=None, output_params=None, fps=None):
             # Get exe
             self._exe = self._get_exe()
             # Process input args
@@ -336,7 +332,9 @@ class FfmpegFormat(Format):
             elif not isinstance(pixelformat, string_types):
                 raise ValueError('FFMPEG pixelformat must be str')
             self._arg_pixelformat = pixelformat
-            self._arg_ffmpeg_params = ffmpeg_params if ffmpeg_params else []
+            self._arg_input_params = input_params or []
+            self._arg_output_params = output_params or []
+            self._arg_input_params += ffmpeg_params or []  # backward compat
             # Write "_video"_arg
             self.request._video = None
             if self.request.filename in ['<video%i>' % i for i in range(10)]:
@@ -418,9 +416,13 @@ class FfmpegFormat(Format):
                      '-pix_fmt', self._pix_fmt,
                      '-vcodec', 'rawvideo']
             oargs.extend(['-s', self._arg_size] if self._arg_size else [])
+            if self.request.kwargs.get('fps', None):
+                fps = float(self.request.kwargs['fps'])
+                oargs += ['-r', "%.02f" % fps]
             # Create process
-            cmd = [self._exe] + self._arg_ffmpeg_params
-            cmd += iargs + ['-i', self._filename] + oargs + ['-']
+            cmd = [self._exe] + self._arg_input_params
+            cmd += iargs + ['-i', self._filename]
+            cmd += oargs + self._arg_output_params + ['-']
             # For Windows, set `shell=True` in sp.Popen to prevent popup
             # of a command line window in frozen applications.
             self._proc = sp.Popen(cmd, stdin=sp.PIPE,
@@ -447,19 +449,21 @@ class FfmpegFormat(Format):
                 # Also appears this epsilon below is needed to ensure frame
                 # accurate seeking in some cases
                 epsilon = -1/self._meta['fps']*0.1
-                iargs = ['-ss', "%.06f" % (starttime+epsilon),
-                         '-i', self._filename,
-                         ]
+                iargs = ['-ss', "%.06f" % (starttime+epsilon)]
+                iargs += ['-i', self._filename]
 
                 # Output args, for writing to pipe
                 oargs = ['-f', 'image2pipe',
                          '-pix_fmt', self._pix_fmt,
                          '-vcodec', 'rawvideo']
                 oargs.extend(['-s', self._arg_size] if self._arg_size else [])
-
+                if self.request.kwargs.get('fps', None):
+                    fps = float(self.request.kwargs['fps'])
+                    oargs += ['-r', "%.02f" % fps]
+                
                 # Create process
-                cmd = [self._exe] + self._arg_ffmpeg_params
-                cmd += iargs + oargs + ['-']
+                cmd = [self._exe] + self._arg_input_params + iargs
+                cmd += oargs + self._arg_output_params + ['-']
                 # For Windows, set `shell=True` in sp.Popen to prevent popup
                 # of a command line window in frozen applications.
                 self._proc = sp.Popen(cmd, stdin=sp.PIPE,
@@ -481,7 +485,7 @@ class FfmpegFormat(Format):
             # Using kill since self._proc.terminate() does not seem
             # to work for ffmpeg, leaves processes hanging
             self._proc.kill()
-
+            
             # Tell threads to stop when they have a chance. They are probably
             # blocked on reading from their file, but let's play it safe.
             if self._stderr_catcher:
@@ -528,51 +532,20 @@ class FfmpegFormat(Format):
                     raise IOError("Could not open steam %s." % self._filename)
                 else:  # pragma: no cover - this is checked by Request
                     raise IOError("%s not found! Wrong path?" % self._filename)
-
-            # Get version
-            ver = lines[0].split('version', 1)[-1].split('Copyright')[0]
-            self._meta['ffmpeg_version'] = ver.strip() + ' ' + lines[1].strip()
-
-            # get the output line that speaks about video
-            videolines = [l for l in lines if l.lstrip().startswith('Stream ')
-                          and ' Video: ' in l]
-            line = videolines[0]
             
-            # get the frame rate
-            matches = re.findall(" ([0-9]+\.?[0-9]*) (tbr|fps)", line)
-            fps = 0
-            if matches:  # Can be empty, see #171, assume nframes = inf
-                fps = float(matches[0][0].strip())
-            self._meta['fps'] = fps
-
-            # get the size of the original stream, of the form 460x320 (w x h)
-            match = re.search(" [0-9]*x[0-9]*(,| )", line)
-            parts = line[match.start():match.end()-1].split('x')
-            self._meta['source_size'] = tuple(map(int, parts))
-
-            # get the size of what we receive, of the form 460x320 (w x h)
-            line = videolines[-1]  # Pipe output
-            match = re.search(" [0-9]*x[0-9]*(,| )", line)
-            parts = line[match.start():match.end()-1].split('x')
-            self._meta['size'] = tuple(map(int, parts))
-
-            # Check the two sizes
-            if self._meta['source_size'] != self._meta['size']:
-                logging.warning('Warning: the frame size for reading %s is '
-                                'different from the source frame size %s.' %
-                                (self._meta['size'],
-                                 self._meta['source_size']))
-
-            # get duration (in seconds)
-            line = [l for l in lines if 'Duration: ' in l][0]
-            match = re.search(" [0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9]",
-                              line)
-            if match is not None:
-                hms = map(float, line[match.start()+1:match.end()].split(':'))
-                self._meta['duration'] = duration = cvsecs(*hms)
-                if fps:
-                    self._meta['nframes'] = int(round(duration*fps))
-
+            # Go!
+            self._meta.update(parse_ffmpeg_info(lines))
+            
+            # Update with fps with user-value?
+            if self.request.kwargs.get('fps', None):
+                self._meta['fps'] = float(self.request.kwargs['fps'])
+            
+            # Estimate nframes
+            self._meta['nframes'] = np.inf
+            if self._meta['fps'] > 0 and 'duration' in self._meta:
+                n = int(round(self._meta['duration'] * self._meta['fps']))
+                self._meta['nframes'] = n
+        
         def _read_frame_data(self):
             # Init and check
             w, h = self._meta['size']
@@ -632,6 +605,7 @@ class FfmpegFormat(Format):
 
         def _open(self, fps=10, codec='libx264', bitrate=None,
                   pixelformat='yuv420p', ffmpeg_params=None,
+                  input_params=None, output_params=None,
                   ffmpeg_log_level="quiet", quality=5,
                   macro_block_size=16):
             self._exe = self._get_exe()
@@ -714,7 +688,9 @@ class FfmpegFormat(Format):
             quality = self.request.kwargs.get('quality', 5)
             ffmpeg_log_level = self.request.kwargs.get('ffmpeg_log_level',
                                                        'warning')
-            extra_ffmpeg_params = self.request.kwargs.get('ffmpeg_params', [])
+            input_params = self.request.kwargs.get('input_params') or []
+            output_params = self.request.kwargs.get('output_params') or []
+            output_params += self.request.kwargs.get('ffmpeg_params') or []
             # You may need to use -pix_fmt yuv420p for your output to work in
             # QuickTime and most other players. These players only supports
             # the YUV planar color space with 4:2:0 chroma subsampling for
@@ -730,11 +706,12 @@ class FfmpegFormat(Format):
                    "-vcodec", "rawvideo",
                    '-s', sizestr,
                    '-pix_fmt', self._pix_fmt,
-                   '-r', "%.02f" % fps,
-                   '-i', '-', '-an',
-                   '-vcodec', codec,
-                   '-pix_fmt', pixelformat,
-                   ]
+                   '-r', "%.02f" % fps] + input_params
+            cmd += ['-i', '-']
+            cmd += ['-an',
+                    '-vcodec', codec,
+                    '-pix_fmt', pixelformat,
+                    ]
             # Add fixed bitrate or variable bitrate compression flags
             if bitrate is not None:
                 cmd += ['-b:v', str(bitrate)]
@@ -789,7 +766,7 @@ class FfmpegFormat(Format):
                 # output from ffmpeg by default. That way if there are warnings
                 # the user will see them.
                 cmd += ['-v', ffmpeg_log_level]
-            cmd += extra_ffmpeg_params
+            cmd += output_params
             cmd.append(self._filename)
             self._cmd = " ".join(cmd)  # For showing command if needed
             if any([level in ffmpeg_log_level for level in
@@ -956,6 +933,80 @@ class StreamCatcher(threading.Thread):
                 self._lines = limit_lines_local(self._lines)
 
 
+def parse_device_names(ffmpeg_output):
+    """ Parse the output of the ffmpeg -list-devices command"""
+    device_names = []
+    in_video_devices = False
+    for line in ffmpeg_output.splitlines():
+        if line.startswith('[dshow'):
+            logging.debug(line)
+            line = line.split(']', 1)[1].strip()
+            if in_video_devices and line.startswith('"'):
+                device_names.append(line[1:-1])
+            elif 'video devices' in line:
+                in_video_devices = True
+            elif 'devices' in line:
+                # set False for subsequent "devices" sections
+                in_video_devices = False
+    return device_names
+
+
+def parse_ffmpeg_info(text):
+    meta = {}
+    
+    if isinstance(text, list):
+        lines = text
+    else:
+        lines = text.splitlines()
+    
+    # Get version
+    ver = lines[0].split('version', 1)[-1].split('Copyright')[0]
+    meta['ffmpeg_version'] = ver.strip() + ' ' + lines[1].strip()
+
+    # get the output line that speaks about video
+    videolines = [l for l in lines if l.lstrip().startswith('Stream ')
+                  and ' Video: ' in l]
+    line = videolines[0]
+    
+    # get the frame rate.
+    # matches can be empty, see #171, assume nframes = inf
+    # the regexp omits values of "1k tbr" which seems a specific edge-case #262
+    # it seems that tbr is generally to be preferred #262
+    matches = re.findall(" ([0-9]+\.?[0-9]*) (tbr|fps)", line)
+    fps = 0
+    matches.sort(key=lambda x: x[1] == 'tbr', reverse=True)
+    if matches:
+        fps = float(matches[0][0].strip())
+    meta['fps'] = fps
+
+    # get the size of the original stream, of the form 460x320 (w x h)
+    match = re.search(" [0-9]*x[0-9]*(,| )", line)
+    parts = line[match.start():match.end()-1].split('x')
+    meta['source_size'] = tuple(map(int, parts))
+
+    # get the size of what we receive, of the form 460x320 (w x h)
+    line = videolines[-1]  # Pipe output
+    match = re.search(" [0-9]*x[0-9]*(,| )", line)
+    parts = line[match.start():match.end()-1].split('x')
+    meta['size'] = tuple(map(int, parts))
+
+    # Check the two sizes
+    if meta['source_size'] != meta['size']:
+        logging.warning('Warning: the frame size for reading %s is '
+                        'different from the source frame size %s.' %
+                        (meta['size'], meta['source_size']))
+
+    # get duration (in seconds)
+    line = [l for l in lines if 'Duration: ' in l][0]
+    match = re.search(" [0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9]",
+                      line)
+    if match is not None:
+        hms = map(float, line[match.start()+1:match.end()].split(':'))
+        meta['duration'] = cvsecs(*hms)
+    
+    return meta
+
+
 def get_output_video_line(lines):
     """Get the line that defines the video stream that ffmpeg outputs,
     and which we read.


Test patch:
diff --git a/tests/test_dicom.py b/tests/test_dicom.py
index 54c915d..23b24a2 100644
--- a/tests/test_dicom.py
+++ b/tests/test_dicom.py
@@ -58,6 +58,12 @@ def test_read_empty_dir():
     assert imageio.formats.search_read_format(request) is None
     
 
+def test_dcmtk():
+    # This should not crach, we make no assumptions on whether its
+    # available or not
+    imageio.plugins.dicom.get_dcmdjpeg_exe()
+
+
 def test_selection():
     
     dname1, dname2, fname1, fname2 = _prepare()
diff --git a/tests/test_ffmpeg.py b/tests/test_ffmpeg.py
index 2229c10..73710c5 100644
--- a/tests/test_ffmpeg.py
+++ b/tests/test_ffmpeg.py
@@ -393,12 +393,6 @@ def test_framecatcher():
     file.close()
 
 
-def test_dcmtk():
-    # This should not crach, we make no assumptions on whether its
-    # available or not
-    imageio.plugins.dicom.get_dcmdjpeg_exe()
-    
-
 def test_webcam():
     need_internet()
     
@@ -408,49 +402,6 @@ def test_webcam():
         skip('no web cam')
 
 
-# Sample from ffmpeg -device-list (distilled down to two lines per section)
-ffmpeg_output_sample = \
-    u'ffmpeg version 3.2.4 Copyright (c) 2000-2017 the FFmpeg developers\r\n' \
-    u'  built with gcc 6.3.0 (GCC)\r\n' \
-    u'  configuration: --enable-gpl --enable-version3 --enable-d3d11va' \
-    u' --enable-dxva2 --enable-libmfx --enable-nvenc --enable-avisynth' \
-    u'libswresample   2.  3.100 /  2.  3.100\r\n  ' \
-    u'libpostproc    54.  1.100 / 54.  1.100\r\n' \
-    u'[dshow @ 039a7e20] DirectShow video devices (some may be both video ' \
-    u'and audio devices)\r\n' \
-    u'[dshow @ 039a7e20]  "AVerMedia USB Polaris Analog Capture"\r\n' \
-    u'[dshow @ 039a7e20]     Alternative name "@device_pnp_\\\\?\\usb#vid_0' \
-    u'7ca&pid_c039&mi_01#8&55f1102&0&0001#{65e8773d-8f56-11d0-a3b9-00a0c922' \
-    u'3196}\\{9b365890-165f-11d0-a195-0020afd156e4}"\r\n' \
-    u'[dshow @ 039a7e20]  "Lenovo EasyCamera"\r\n' \
-    u'[dshow @ 039a7e20]     Alternative name "@device_pnp_\\\\?\\usb#vid_0' \
-    u'4f2&pid_b50f&mi_00#6&bbc4ae1&1&0000#{65e8773d-8f56-11d0-a3b9-00a0c922' \
-    u'3196}\\global"\r\n' \
-    u'[dshow @ 039a7e20] DirectShow audio devices\r\n' \
-    u'[dshow @ 039a7e20]  "Microphone (2- USB Multimedia Audio Device)"\r\n' \
-    u'[dshow @ 039a7e20]     Alternative name "@device_cm_{33D9A762-90C8-11D' \
-    u'0-BD43-00A0C911CE86}\\wave_{73C17834-AA57-4CA1-847A-6BBEB1E0F2E6}"\r\n' \
-    u'[dshow @ 039a7e20]  "SPDIF Interface (Multimedia Audio Device)"\r\n' \
-    u'[dshow @ 039a7e20]     Alternative name "@device_cm_{33D9A762-90C8-11D' \
-    u'0-BD43-00A0C911CE86}\\wave_{617B63FB-CFC0-4D10-AE30-42A66CAF6A4E}"\r\n' \
-    u'dummy: Immediate exit requested\r\n'
-
-
-def test_webcam_parse_device_names():
-    # Ensure that the device list parser returns all video devices (issue #283)
-
-    # Specify test parameters
-    number_of_video_devices_in_sample = 2  # see ffmpeg_output_sample above
-
-    # Parse the sample
-    device_names = \
-        imageio.plugins.ffmpeg.FfmpegFormat.Reader._parse_device_names(
-            ffmpeg_output_sample)
-
-    # Assert that the device_names list has the correct length
-    assert len(device_names) == number_of_video_devices_in_sample
-
-
 def test_webcam_get_next_data():
     need_internet()
 
diff --git a/tests/test_ffmpeg_info.py b/tests/test_ffmpeg_info.py
new file mode 100644
index 0000000..6501380
--- /dev/null
+++ b/tests/test_ffmpeg_info.py
@@ -0,0 +1,165 @@
+# styletest: ignore E501
+""" Tests specific to parsing ffmpeg info.
+"""
+
+from imageio.testing import run_tests_if_main, need_internet
+
+import imageio
+
+
+def dedent(text, dedent=8):
+    lines = [line[dedent:] for line in text.splitlines()]
+    text = '\n'.join(lines)
+    return text.strip() + '\n'
+
+
+def test_webcam_parse_device_names():
+    # Ensure that the device list parser returns all video devices (issue #283)
+    
+    sample = dedent(r"""
+        ffmpeg version 3.2.4 Copyright (c) 2000-2017 the FFmpeg developers
+        built with gcc 6.3.0 (GCC)
+        configuration: --enable-gpl --enable-version3 --enable-d3d11va --enable-dxva2 --enable-libmfx --enable-nvenc --enable-avisynthlibswresample   2.  3.100 /  2.  3.100
+        libpostproc    54.  1.100 / 54.  1.100
+        [dshow @ 039a7e20] DirectShow video devices (some may be both video and audio devices)
+        [dshow @ 039a7e20]  "AVerMedia USB Polaris Analog Capture"
+        [dshow @ 039a7e20]     Alternative name "@device_pnp_\\?\usb#vid_07ca&pid_c039&mi_01#8&55f1102&0&0001#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\{9b365890-165f-11d0-a195-0020afd156e4}"
+        [dshow @ 039a7e20]  "Lenovo EasyCamera"
+        [dshow @ 039a7e20]     Alternative name "@device_pnp_\\?\usb#vid_04f2&pid_b50f&mi_00#6&bbc4ae1&1&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global"
+        [dshow @ 039a7e20] DirectShow audio devices
+        [dshow @ 039a7e20]  "Microphone (2- USB Multimedia Audio Device)"
+        [dshow @ 039a7e20]     Alternative name "@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{73C17834-AA57-4CA1-847A-6BBEB1E0F2E6}"
+        [dshow @ 039a7e20]  "SPDIF Interface (Multimedia Audio Device)"
+        [dshow @ 039a7e20]     Alternative name "@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{617B63FB-CFC0-4D10-AE30-42A66CAF6A4E}"
+        dummy: Immediate exit requested
+        """)
+    
+    # Parse the sample
+    device_names = imageio.plugins.ffmpeg.parse_device_names(sample)
+
+    # Assert that the device_names list has the correct length
+    assert len(device_names) == 2
+
+
+def test_get_correct_fps1():
+    # from issue #262
+    
+    sample = dedent(r"""
+        fmpeg version 3.2.2 Copyright (c) 2000-2016 the FFmpeg developers
+        built with Apple LLVM version 8.0.0 (clang-800.0.42.1)
+        configuration: --prefix=/usr/local/Cellar/ffmpeg/3.2.2 --enable-shared --enable-pthreads --enable-gpl --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-frei0r --enable-libass --enable-libfdk-aac --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopus --enable-librtmp --enable-libschroedinger --enable-libspeex --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libxvid --enable-opencl --disable-lzma --enable-libopenjpeg --disable-decoder=jpeg2000 --extra-cflags=-I/usr/local/Cellar/openjpeg/2.1.2/include/openjpeg-2.1 --enable-nonfree --enable-vda
+        libavutil      55. 34.100 / 55. 34.100
+        libavcodec     57. 64.101 / 57. 64.101
+        libavformat    57. 56.100 / 57. 56.100
+        libavdevice    57.  1.100 / 57.  1.100
+        libavfilter     6. 65.100 /  6. 65.100
+        libavresample   3.  1.  0 /  3.  1.  0
+        libswscale      4.  2.100 /  4.  2.100
+        libswresample   2.  3.100 /  2.  3.100
+        libpostproc    54.  1.100 / 54.  1.100
+        Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/echeng/video.mp4':
+        Metadata:
+            major_brand     : mp42
+            minor_version   : 1
+            compatible_brands: isom3gp43gp5
+        Duration: 00:16:05.80, start: 0.000000, bitrate: 1764 kb/s
+            Stream #0:0(eng): Audio: aac (LC) (mp4a / 0x6134706D), 8000 Hz, mono, fltp, 40 kb/s (default)
+            Metadata:
+            handler_name    : soun
+            Stream #0:1(eng): Video: mpeg4 (Simple Profile) (mp4v / 0x7634706D), yuv420p, 640x480 [SAR 1:1 DAR 4:3], 1720 kb/s, 29.46 fps, 26.58 tbr, 90k tbn, 1k tbc (default)
+            Metadata:
+            handler_name    : vide
+        Output #0, image2pipe, to 'pipe:':
+        Metadata:
+            major_brand     : mp42
+            minor_version   : 1
+            compatible_brands: isom3gp43gp5
+            encoder         : Lavf57.56.100
+            Stream #0:0(eng): Video: rawvideo (RGB[24] / 0x18424752), rgb24, 640x480 [SAR 1:1 DAR 4:3], q=2-31, 200 kb/s, 26.58 fps, 26.58 tbn, 26.58 tbc (default)
+            Metadata:
+            handler_name    : vide
+            encoder         : Lavc57.64.101 rawvideo
+        Stream mapping:
+        """)
+    
+    info = imageio.plugins.ffmpeg.parse_ffmpeg_info(sample)
+    assert info['fps'] == 26.58
+
+
+def test_get_correct_fps2():
+    # from issue #262
+    
+    sample = dedent(r"""
+        ffprobe version 3.2.2 Copyright (c) 2007-2016 the FFmpeg developers
+        built with Apple LLVM version 8.0.0 (clang-800.0.42.1)
+        configuration: --prefix=/usr/local/Cellar/ffmpeg/3.2.2 --enable-shared --enable-pthreads --enable-gpl --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-frei0r --enable-libass --enable-libfdk-aac --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopus --enable-librtmp --enable-libschroedinger --enable-libspeex --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libxvid --enable-opencl --disable-lzma --enable-libopenjpeg --disable-decoder=jpeg2000 --extra-cflags=-I/usr/local/Cellar/openjpeg/2.1.2/include/openjpeg-2.1 --enable-nonfree --enable-vda
+        libavutil      55. 34.100 / 55. 34.100
+        libavcodec     57. 64.101 / 57. 64.101
+        libavformat    57. 56.100 / 57. 56.100
+        libavdevice    57.  1.100 / 57.  1.100
+        libavfilter     6. 65.100 /  6. 65.100
+        libavresample   3.  1.  0 /  3.  1.  0
+        libswscale      4.  2.100 /  4.  2.100
+        libswresample   2.  3.100 /  2.  3.100
+        libpostproc    54.  1.100 / 54.  1.100
+        Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'video.mp4':
+        Metadata:
+            major_brand     : mp42
+            minor_version   : 1
+            compatible_brands: isom3gp43gp5
+        Duration: 00:08:44.53, start: 0.000000, bitrate: 1830 kb/s
+            Stream #0:0(eng): Audio: aac (LC) (mp4a / 0x6134706D), 8000 Hz, mono, fltp, 40 kb/s (default)
+            Metadata:
+            handler_name    : soun
+            Stream #0:1(eng): Video: mpeg4 (Simple Profile) (mp4v / 0x7634706D), yuv420p, 640x480 [SAR 1:1 DAR 4:3], 1785 kb/s, 29.27 fps, 1k tbr, 90k tbn, 1k tbc (default)
+            Metadata:
+            handler_name    : vide
+        """)
+    
+    info = imageio.plugins.ffmpeg.parse_ffmpeg_info(sample)
+    assert info['fps'] == 29.27
+
+
+def test_overload_fps():
+    
+    need_internet()
+    
+    # Native
+    r = imageio.get_reader('imageio:cockatoo.mp4')
+    assert len(r) == 280  # native
+    ims = [im for im in r]
+    assert len(ims) == 280
+    # imageio.mimwrite('~/parot280.gif', ims[:30])
+    
+    # Less
+    r = imageio.get_reader('imageio:cockatoo.mp4', fps=8)
+    assert len(r) == 112
+    ims = [im for im in r]
+    assert len(ims) == 112
+    # imageio.mimwrite('~/parot112.gif', ims[:30])
+    
+    # More
+    r = imageio.get_reader('imageio:cockatoo.mp4', fps=24)
+    assert len(r) == 336
+    ims = [im for im in r]
+    assert len(ims) == 336
+    # imageio.mimwrite('~/parot336.gif', ims[:30])
+    
+    # Do we calculate nframes correctly? To be fair, the reader wont try to
+    # read beyond what it thinks how many frames it has. But this at least 
+    # makes sure that this works.
+    for fps in (8.0, 8.02, 8.04, 8.06, 8.08):
+        r = imageio.get_reader('imageio:cockatoo.mp4', fps=fps)
+        n = len(r)
+        i = 0
+        try:
+            while True:
+                r.get_next_data()
+                i += 1
+        except (StopIteration, IndexError):
+            pass
+        # print(r._meta['duration'], r._meta['fps'], r._meta['duration'] * fps, r._meta['nframes'], n)
+        assert i == n
+
+
+run_tests_if_main()