generated from thinkode/modelRepository
initial commit and version 1.0
This commit is contained in:
167
moviepy/video/io/ImageSequenceClip.py
Normal file
167
moviepy/video/io/ImageSequenceClip.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""Implements ImageSequenceClip, a class to create a video clip from a set
|
||||
of image files.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
from imageio.v2 import imread
|
||||
|
||||
from moviepy.video.VideoClip import VideoClip
|
||||
|
||||
|
||||
class ImageSequenceClip(VideoClip):
|
||||
"""A VideoClip made from a series of images.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
sequence
|
||||
Can be one of these:
|
||||
|
||||
- The name of a folder (containing only pictures). The pictures
|
||||
will be considered in alphanumerical order.
|
||||
- A list of names of image files. In this case you can choose to
|
||||
load the pictures in memory pictures
|
||||
- A list of Numpy arrays representing images. In this last case,
|
||||
masks are not supported currently.
|
||||
|
||||
fps
|
||||
Number of picture frames to read per second. Instead, you can provide
|
||||
the duration of each image with durations (see below)
|
||||
|
||||
durations
|
||||
List of the duration of each picture.
|
||||
|
||||
with_mask
|
||||
Should the alpha layer of PNG images be considered as a mask ?
|
||||
|
||||
is_mask
|
||||
Will this sequence of pictures be used as an animated mask.
|
||||
|
||||
load_images
|
||||
Specify that all images should be loaded into the RAM. This is only
|
||||
interesting if you have a small number of images that will be used
|
||||
more than once.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sequence,
|
||||
fps=None,
|
||||
durations=None,
|
||||
with_mask=True,
|
||||
is_mask=False,
|
||||
load_images=False,
|
||||
):
|
||||
# CODE WRITTEN AS IT CAME, MAY BE IMPROVED IN THE FUTURE
|
||||
|
||||
if (fps is None) and (durations is None):
|
||||
raise ValueError("Please provide either 'fps' or 'durations'.")
|
||||
VideoClip.__init__(self, is_mask=is_mask)
|
||||
|
||||
# Parse the data
|
||||
|
||||
fromfiles = True
|
||||
|
||||
if isinstance(sequence, list):
|
||||
if isinstance(sequence[0], str):
|
||||
if load_images:
|
||||
sequence = [imread(file) for file in sequence]
|
||||
fromfiles = False
|
||||
else:
|
||||
fromfiles = True
|
||||
else:
|
||||
# sequence is already a list of numpy arrays
|
||||
fromfiles = False
|
||||
else:
|
||||
# sequence is a folder name, make it a list of files:
|
||||
fromfiles = True
|
||||
sequence = sorted(
|
||||
[os.path.join(sequence, file) for file in os.listdir(sequence)]
|
||||
)
|
||||
|
||||
# check that all the images are of the same size
|
||||
if isinstance(sequence[0], str):
|
||||
size = imread(sequence[0]).shape
|
||||
else:
|
||||
size = sequence[0].shape
|
||||
|
||||
for image in sequence:
|
||||
image1 = image
|
||||
if isinstance(image, str):
|
||||
image1 = imread(image)
|
||||
if size != image1.shape:
|
||||
raise Exception(
|
||||
"MoviePy: ImageSequenceClip requires all images to be the same size"
|
||||
)
|
||||
|
||||
self.fps = fps
|
||||
if fps is not None:
|
||||
durations = [1.0 / fps for image in sequence]
|
||||
self.images_starts = [
|
||||
1.0 * i / fps - np.finfo(np.float32).eps for i in range(len(sequence))
|
||||
]
|
||||
else:
|
||||
self.images_starts = [0] + list(np.cumsum(durations))
|
||||
self.durations = durations
|
||||
self.duration = sum(durations)
|
||||
self.end = self.duration
|
||||
self.sequence = sequence
|
||||
|
||||
if fps is None:
|
||||
self.fps = self.duration / len(sequence)
|
||||
|
||||
def find_image_index(t):
|
||||
return max(
|
||||
[i for i in range(len(self.sequence)) if self.images_starts[i] <= t]
|
||||
)
|
||||
|
||||
if fromfiles:
|
||||
self.last_index = None
|
||||
self.last_image = None
|
||||
|
||||
def frame_function(t):
|
||||
index = find_image_index(t)
|
||||
|
||||
if index != self.last_index:
|
||||
self.last_image = imread(self.sequence[index])[:, :, :3]
|
||||
self.last_index = index
|
||||
|
||||
return self.last_image
|
||||
|
||||
if with_mask and (imread(self.sequence[0]).shape[2] == 4):
|
||||
self.mask = VideoClip(is_mask=True)
|
||||
self.mask.last_index = None
|
||||
self.mask.last_image = None
|
||||
|
||||
def mask_frame_function(t):
|
||||
index = find_image_index(t)
|
||||
if index != self.mask.last_index:
|
||||
frame = imread(self.sequence[index])[:, :, 3]
|
||||
self.mask.last_image = frame.astype(float) / 255
|
||||
self.mask.last_index = index
|
||||
|
||||
return self.mask.last_image
|
||||
|
||||
self.mask.frame_function = mask_frame_function
|
||||
self.mask.size = mask_frame_function(0).shape[:2][::-1]
|
||||
|
||||
else:
|
||||
|
||||
def frame_function(t):
|
||||
index = find_image_index(t)
|
||||
return self.sequence[index][:, :, :3]
|
||||
|
||||
if with_mask and (self.sequence[0].shape[2] == 4):
|
||||
self.mask = VideoClip(is_mask=True)
|
||||
|
||||
def mask_frame_function(t):
|
||||
index = find_image_index(t)
|
||||
return 1.0 * self.sequence[index][:, :, 3] / 255
|
||||
|
||||
self.mask.frame_function = mask_frame_function
|
||||
self.mask.size = mask_frame_function(0).shape[:2][::-1]
|
||||
|
||||
self.frame_function = frame_function
|
||||
self.size = frame_function(0).shape[:2][::-1]
|
||||
175
moviepy/video/io/VideoFileClip.py
Normal file
175
moviepy/video/io/VideoFileClip.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""Implements VideoFileClip, a class for video clips creation using video files."""
|
||||
|
||||
from moviepy.audio.io.AudioFileClip import AudioFileClip
|
||||
from moviepy.decorators import convert_path_to_string
|
||||
from moviepy.video.io.ffmpeg_reader import FFMPEG_VideoReader
|
||||
from moviepy.video.VideoClip import VideoClip
|
||||
|
||||
|
||||
class VideoFileClip(VideoClip):
|
||||
"""A video clip originating from a movie file. For instance:
|
||||
|
||||
.. code:: python
|
||||
|
||||
clip = VideoFileClip("myHolidays.mp4")
|
||||
clip.close()
|
||||
with VideoFileClip("myMaskVideo.avi") as clip2:
|
||||
pass # Implicit close called by context manager.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
filename:
|
||||
The name of the video file, as a string or a path-like object.
|
||||
It can have any extension supported by ffmpeg:
|
||||
.ogv, .mp4, .mpeg, .avi, .mov etc.
|
||||
|
||||
has_mask:
|
||||
Set this to 'True' if there is a mask included in the videofile.
|
||||
Video files rarely contain masks, but some video codecs enable
|
||||
that. For instance if you have a MoviePy VideoClip with a mask you
|
||||
can save it to a videofile with a mask. (see also
|
||||
``VideoClip.write_videofile`` for more details).
|
||||
|
||||
audio:
|
||||
Set to `False` if the clip doesn't have any audio or if you do not
|
||||
wish to read the audio.
|
||||
|
||||
target_resolution:
|
||||
Set to (desired_width, desired_height) to have ffmpeg resize the frames
|
||||
before returning them. This is much faster than streaming in high-res
|
||||
and then resizing. If either dimension is None, the frames are resized
|
||||
by keeping the existing aspect ratio.
|
||||
|
||||
resize_algorithm:
|
||||
The algorithm used for resizing. Default: "bicubic", other popular
|
||||
options include "bilinear" and "fast_bilinear". For more information, see
|
||||
https://ffmpeg.org/ffmpeg-scaler.html
|
||||
|
||||
fps_source:
|
||||
The fps value to collect from the metadata. Set by default to 'fps', but
|
||||
can be set to 'tbr', which may be helpful if you are finding that it is reading
|
||||
the incorrect fps from the file.
|
||||
|
||||
pixel_format
|
||||
Optional: Pixel format for the video to read. If is not specified
|
||||
'rgb24' will be used as the default format unless ``has_mask`` is set
|
||||
as ``True``, then 'rgba' will be used.
|
||||
|
||||
is_mask
|
||||
`True` if the clip is going to be used as a mask.
|
||||
|
||||
|
||||
Attributes
|
||||
----------
|
||||
|
||||
filename:
|
||||
Name of the original video file.
|
||||
|
||||
fps:
|
||||
Frames per second in the original file.
|
||||
|
||||
|
||||
Read docs for Clip() and VideoClip() for other, more generic, attributes.
|
||||
|
||||
Lifetime
|
||||
--------
|
||||
|
||||
Note that this creates subprocesses and locks files. If you construct one
|
||||
of these instances, you must call close() afterwards, or the subresources
|
||||
will not be cleaned up until the process ends.
|
||||
|
||||
If copies are made, and close() is called on one, it may cause methods on
|
||||
the other copies to fail.
|
||||
"""
|
||||
|
||||
@convert_path_to_string("filename")
|
||||
def __init__(
|
||||
self,
|
||||
filename,
|
||||
decode_file=False,
|
||||
has_mask=False,
|
||||
audio=True,
|
||||
audio_buffersize=200000,
|
||||
target_resolution=None,
|
||||
resize_algorithm="bicubic",
|
||||
audio_fps=44100,
|
||||
audio_nbytes=2,
|
||||
fps_source="fps",
|
||||
pixel_format=None,
|
||||
is_mask=False,
|
||||
):
|
||||
VideoClip.__init__(self, is_mask=is_mask)
|
||||
|
||||
# Make a reader
|
||||
if not pixel_format:
|
||||
pixel_format = "rgba" if has_mask else "rgb24"
|
||||
|
||||
self.reader = FFMPEG_VideoReader(
|
||||
filename,
|
||||
decode_file=decode_file,
|
||||
pixel_format=pixel_format,
|
||||
target_resolution=target_resolution,
|
||||
resize_algo=resize_algorithm,
|
||||
fps_source=fps_source,
|
||||
)
|
||||
|
||||
# Make some of the reader's attributes accessible from the clip
|
||||
self.duration = self.reader.duration
|
||||
self.end = self.reader.duration
|
||||
|
||||
self.fps = self.reader.fps
|
||||
self.size = self.reader.size
|
||||
self.rotation = self.reader.rotation
|
||||
|
||||
self.filename = filename
|
||||
|
||||
if has_mask:
|
||||
self.frame_function = lambda t: self.reader.get_frame(t)[:, :, :3]
|
||||
|
||||
def mask_frame_function(t):
|
||||
return self.reader.get_frame(t)[:, :, 3] / 255.0
|
||||
|
||||
self.mask = VideoClip(
|
||||
is_mask=True, frame_function=mask_frame_function
|
||||
).with_duration(self.duration)
|
||||
self.mask.fps = self.fps
|
||||
|
||||
else:
|
||||
self.frame_function = lambda t: self.reader.get_frame(t)
|
||||
|
||||
# Make a reader for the audio, if any.
|
||||
if audio and self.reader.infos["audio_found"]:
|
||||
self.audio = AudioFileClip(
|
||||
filename,
|
||||
buffersize=audio_buffersize,
|
||||
fps=audio_fps,
|
||||
nbytes=audio_nbytes,
|
||||
)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
"""Implements ``copy.deepcopy(clip)`` behaviour as ``copy.copy(clip)``.
|
||||
|
||||
VideoFileClip class instances can't be deeply copied because the locked Thread
|
||||
of ``proc`` isn't pickleable. Without this override, calls to
|
||||
``copy.deepcopy(clip)`` would raise a ``TypeError``:
|
||||
|
||||
```
|
||||
TypeError: cannot pickle '_thread.lock' object
|
||||
```
|
||||
"""
|
||||
return self.__copy__()
|
||||
|
||||
def close(self):
|
||||
"""Close the internal reader."""
|
||||
if self.reader:
|
||||
self.reader.close()
|
||||
self.reader = None
|
||||
|
||||
try:
|
||||
if self.audio:
|
||||
self.audio.close()
|
||||
self.audio = None
|
||||
except AttributeError: # pragma: no cover
|
||||
pass
|
||||
1
moviepy/video/io/__init__.py
Normal file
1
moviepy/video/io/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Classes and methods for reading, writing and previewing video files."""
|
||||
284
moviepy/video/io/display_in_notebook.py
Normal file
284
moviepy/video/io/display_in_notebook.py
Normal file
@@ -0,0 +1,284 @@
|
||||
"""Implements ``display_in_notebook``, a function to embed images/videos/audio in the
|
||||
Jupyter Notebook.
|
||||
"""
|
||||
|
||||
# Notes:
|
||||
# All media are physically embedded in the Jupyter Notebook
|
||||
# (instead of simple links to the original files)
|
||||
# That is because most browsers use a cache system and they won't
|
||||
# properly refresh the media when the original files are changed.
|
||||
|
||||
import inspect
|
||||
import os
|
||||
from base64 import b64encode
|
||||
|
||||
from moviepy.audio.AudioClip import AudioClip
|
||||
from moviepy.tools import extensions_dict
|
||||
from moviepy.video.io.ffmpeg_reader import ffmpeg_parse_infos
|
||||
from moviepy.video.VideoClip import ImageClip, VideoClip
|
||||
|
||||
|
||||
try: # pragma: no cover
|
||||
from IPython.display import HTML
|
||||
|
||||
ipython_available = True
|
||||
|
||||
class HTML2(HTML): # noqa D101
|
||||
def __add__(self, other):
|
||||
return HTML2(self.data + other.data)
|
||||
|
||||
except ImportError:
|
||||
|
||||
def HTML2(content): # noqa D103
|
||||
return content
|
||||
|
||||
ipython_available = False
|
||||
|
||||
|
||||
sorry = "Sorry, seems like your browser doesn't support HTML5 audio/video"
|
||||
templates = {
|
||||
"audio": (
|
||||
"<audio controls>"
|
||||
"<source %(options)s src='data:audio/%(ext)s;base64,%(data)s'>"
|
||||
+ sorry
|
||||
+ "</audio>"
|
||||
),
|
||||
"image": "<img %(options)s src='data:image/%(ext)s;base64,%(data)s'>",
|
||||
"video": (
|
||||
"<video %(options)s"
|
||||
"src='data:video/%(ext)s;base64,%(data)s' controls>" + sorry + "</video>"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def html_embed(
|
||||
clip, filetype=None, maxduration=60, rd_kwargs=None, center=True, **html_kwargs
|
||||
):
|
||||
"""Returns HTML5 code embedding the clip.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
clip : moviepy.Clip.Clip
|
||||
Either a file name, or a clip to preview.
|
||||
Either an image, a sound or a video. Clips will actually be
|
||||
written to a file and embedded as if a filename was provided.
|
||||
|
||||
filetype : str, optional
|
||||
One of 'video','image','audio'. If None is given, it is determined
|
||||
based on the extension of ``filename``, but this can bug.
|
||||
|
||||
maxduration : float, optional
|
||||
An error will be raised if the clip's duration is more than the indicated
|
||||
value (in seconds), to avoid spoiling the browser's cache and the RAM.
|
||||
|
||||
rd_kwargs : dict, optional
|
||||
Keyword arguments for the rendering, like ``dict(fps=15, bitrate="50k")``.
|
||||
Allow you to give some options to the render process. You can, for
|
||||
example, disable the logger bar passing ``dict(logger=None)``.
|
||||
|
||||
center : bool, optional
|
||||
If true (default), the content will be wrapped in a
|
||||
``<div align=middle>`` HTML container, so the content will be displayed
|
||||
at the center.
|
||||
|
||||
html_kwargs
|
||||
Allow you to give some options, like ``width=260``, ``autoplay=True``,
|
||||
``loop=1`` etc.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. code:: python
|
||||
|
||||
from moviepy import *
|
||||
# later ...
|
||||
html_embed(clip, width=360)
|
||||
html_embed(clip.audio)
|
||||
|
||||
clip.write_gif("test.gif")
|
||||
html_embed('test.gif')
|
||||
|
||||
clip.save_frame("first_frame.jpeg")
|
||||
html_embed("first_frame.jpeg")
|
||||
"""
|
||||
if rd_kwargs is None: # pragma: no cover
|
||||
rd_kwargs = {}
|
||||
|
||||
if "Clip" in str(clip.__class__):
|
||||
TEMP_PREFIX = "__temp__"
|
||||
if isinstance(clip, ImageClip):
|
||||
filename = TEMP_PREFIX + ".png"
|
||||
kwargs = {"filename": filename, "with_mask": True}
|
||||
argnames = inspect.getfullargspec(clip.save_frame).args
|
||||
kwargs.update(
|
||||
{key: value for key, value in rd_kwargs.items() if key in argnames}
|
||||
)
|
||||
clip.save_frame(**kwargs)
|
||||
elif isinstance(clip, VideoClip):
|
||||
filename = TEMP_PREFIX + ".mp4"
|
||||
kwargs = {"filename": filename, "preset": "ultrafast"}
|
||||
kwargs.update(rd_kwargs)
|
||||
clip.write_videofile(**kwargs)
|
||||
elif isinstance(clip, AudioClip):
|
||||
filename = TEMP_PREFIX + ".mp3"
|
||||
kwargs = {"filename": filename}
|
||||
kwargs.update(rd_kwargs)
|
||||
clip.write_audiofile(**kwargs)
|
||||
else:
|
||||
raise ValueError("Unknown class for the clip. Cannot embed and preview.")
|
||||
|
||||
return html_embed(
|
||||
filename,
|
||||
maxduration=maxduration,
|
||||
rd_kwargs=rd_kwargs,
|
||||
center=center,
|
||||
**html_kwargs,
|
||||
)
|
||||
|
||||
filename = clip
|
||||
options = " ".join(["%s='%s'" % (str(k), str(v)) for k, v in html_kwargs.items()])
|
||||
name, ext = os.path.splitext(filename)
|
||||
ext = ext[1:]
|
||||
|
||||
if filetype is None:
|
||||
ext = filename.split(".")[-1].lower()
|
||||
if ext == "gif":
|
||||
filetype = "image"
|
||||
elif ext in extensions_dict:
|
||||
filetype = extensions_dict[ext]["type"]
|
||||
else:
|
||||
raise ValueError(
|
||||
"No file type is known for the provided file. Please provide "
|
||||
"argument `filetype` (one of 'image', 'video', 'sound') to the "
|
||||
"display_in_notebook function."
|
||||
)
|
||||
|
||||
if filetype == "video":
|
||||
# The next lines set the HTML5-cvompatible extension and check that the
|
||||
# extension is HTML5-valid
|
||||
exts_htmltype = {"mp4": "mp4", "webm": "webm", "ogv": "ogg"}
|
||||
allowed_exts = " ".join(exts_htmltype.keys())
|
||||
try:
|
||||
ext = exts_htmltype[ext]
|
||||
except Exception:
|
||||
raise ValueError(
|
||||
"This video extension cannot be displayed in the "
|
||||
"Jupyter Notebook. Allowed extensions: " + allowed_exts
|
||||
)
|
||||
|
||||
if filetype in ["audio", "video"]:
|
||||
duration = ffmpeg_parse_infos(filename, decode_file=True)["duration"]
|
||||
if duration > maxduration:
|
||||
raise ValueError(
|
||||
(
|
||||
"The duration of video %s (%.1f) exceeds the 'maxduration'"
|
||||
" attribute. You can increase 'maxduration', by passing"
|
||||
" 'maxduration' parameter to display_in_notebook function."
|
||||
" But note that embedding large videos may take all the memory"
|
||||
" away!"
|
||||
)
|
||||
% (filename, duration)
|
||||
)
|
||||
|
||||
with open(filename, "rb") as file:
|
||||
data = b64encode(file.read()).decode("utf-8")
|
||||
|
||||
template = templates[filetype]
|
||||
|
||||
result = template % {"data": data, "options": options, "ext": ext}
|
||||
if center:
|
||||
result = r"<div align=middle>%s</div>" % result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def display_in_notebook(
|
||||
clip,
|
||||
filetype=None,
|
||||
maxduration=60,
|
||||
t=None,
|
||||
fps=None,
|
||||
rd_kwargs=None,
|
||||
center=True,
|
||||
**html_kwargs,
|
||||
):
|
||||
"""Displays clip content in an Jupyter Notebook.
|
||||
|
||||
Remarks: If your browser doesn't support HTML5, this should warn you.
|
||||
If nothing is displayed, maybe your file or filename is wrong.
|
||||
Important: The media will be physically embedded in the notebook.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
clip : moviepy.Clip.Clip
|
||||
Either the name of a file, or a clip to preview. The clip will actually
|
||||
be written to a file and embedded as if a filename was provided.
|
||||
|
||||
filetype : str, optional
|
||||
One of ``"video"``, ``"image"`` or ``"audio"``. If None is given, it is
|
||||
determined based on the extension of ``filename``, but this can bug.
|
||||
|
||||
maxduration : float, optional
|
||||
An error will be raised if the clip's duration is more than the indicated
|
||||
value (in seconds), to avoid spoiling the browser's cache and the RAM.
|
||||
|
||||
t : float, optional
|
||||
If not None, only the frame at time t will be displayed in the notebook,
|
||||
instead of a video of the clip.
|
||||
|
||||
fps : int, optional
|
||||
Enables to specify an fps, as required for clips whose fps is unknown.
|
||||
|
||||
rd_kwargs : dict, optional
|
||||
Keyword arguments for the rendering, like ``dict(fps=15, bitrate="50k")``.
|
||||
Allow you to give some options to the render process. You can, for
|
||||
example, disable the logger bar passing ``dict(logger=None)``.
|
||||
|
||||
center : bool, optional
|
||||
If true (default), the content will be wrapped in a
|
||||
``<div align=middle>`` HTML container, so the content will be displayed
|
||||
at the center.
|
||||
|
||||
kwargs
|
||||
Allow you to give some options, like ``width=260``, etc. When editing
|
||||
looping gifs, a good choice is ``loop=1, autoplay=1``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code:: python
|
||||
|
||||
from moviepy import *
|
||||
# later ...
|
||||
clip.display_in_notebook(width=360)
|
||||
clip.audio.display_in_notebook()
|
||||
|
||||
clip.write_gif("test.gif")
|
||||
display_in_notebook('test.gif')
|
||||
|
||||
clip.save_frame("first_frame.jpeg")
|
||||
display_in_notebook("first_frame.jpeg")
|
||||
"""
|
||||
if not ipython_available:
|
||||
raise ImportError("Only works inside an Jupyter Notebook")
|
||||
|
||||
if rd_kwargs is None:
|
||||
rd_kwargs = {}
|
||||
|
||||
if fps is not None:
|
||||
rd_kwargs["fps"] = fps
|
||||
|
||||
if t is not None:
|
||||
clip = clip.to_ImageClip(t)
|
||||
|
||||
return HTML2(
|
||||
html_embed(
|
||||
clip,
|
||||
filetype=filetype,
|
||||
maxduration=maxduration,
|
||||
center=center,
|
||||
rd_kwargs=rd_kwargs,
|
||||
**html_kwargs,
|
||||
)
|
||||
)
|
||||
882
moviepy/video/io/ffmpeg_reader.py
Normal file
882
moviepy/video/io/ffmpeg_reader.py
Normal file
@@ -0,0 +1,882 @@
|
||||
"""Implements all the functions to read a video or a picture using ffmpeg."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess as sp
|
||||
import warnings
|
||||
from log import log_step
|
||||
|
||||
import numpy as np
|
||||
|
||||
from moviepy.config import FFMPEG_BINARY # ffmpeg, ffmpeg.exe, etc...
|
||||
from moviepy.tools import (
|
||||
convert_to_seconds,
|
||||
cross_platform_popen_params,
|
||||
ffmpeg_escape_filename,
|
||||
)
|
||||
|
||||
|
||||
class FFMPEG_VideoReader:
|
||||
"""Class for video byte-level reading with ffmpeg."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
filename,
|
||||
decode_file=True,
|
||||
print_infos=False,
|
||||
bufsize=None,
|
||||
pixel_format="rgb24",
|
||||
check_duration=True,
|
||||
target_resolution=None,
|
||||
resize_algo="bicubic",
|
||||
fps_source="fps",
|
||||
):
|
||||
self.filename = filename
|
||||
self.proc = None
|
||||
infos = ffmpeg_parse_infos(
|
||||
filename,
|
||||
check_duration=check_duration,
|
||||
fps_source=fps_source,
|
||||
decode_file=decode_file,
|
||||
print_infos=print_infos,
|
||||
)
|
||||
# If framerate is unavailable, assume 1.0 FPS to avoid divide-by-zero errors.
|
||||
self.fps = infos.get("video_fps", 1.0)
|
||||
# If frame size is unavailable, set 1x1 divide-by-zero errors.
|
||||
self.size = infos.get("video_size", (1, 1))
|
||||
|
||||
# ffmpeg automatically rotates videos if rotation information is
|
||||
# available, so exchange width and height
|
||||
self.rotation = abs(infos.get("video_rotation", 0))
|
||||
if self.rotation in [90, 270]:
|
||||
self.size = [self.size[1], self.size[0]]
|
||||
|
||||
if target_resolution:
|
||||
if None in target_resolution:
|
||||
ratio = 1
|
||||
for idx, target in enumerate(target_resolution):
|
||||
if target:
|
||||
ratio = target / self.size[idx]
|
||||
self.size = (int(self.size[0] * ratio), int(self.size[1] * ratio))
|
||||
else:
|
||||
self.size = target_resolution
|
||||
self.resize_algo = resize_algo
|
||||
|
||||
self.duration = infos.get("video_duration", 0.0)
|
||||
self.ffmpeg_duration = infos.get("duration", 0.0)
|
||||
self.n_frames = infos.get("video_n_frames", 0)
|
||||
self.bitrate = infos.get("video_bitrate", 0)
|
||||
|
||||
self.infos = infos
|
||||
|
||||
self.pixel_format = pixel_format
|
||||
self.depth = 4 if pixel_format[-1] == "a" else 3
|
||||
# 'a' represents 'alpha' which means that each pixel has 4 values instead of 3.
|
||||
# See https://github.com/Zulko/moviepy/issues/1070#issuecomment-644457274
|
||||
|
||||
if bufsize is None:
|
||||
w, h = self.size
|
||||
bufsize = self.depth * w * h + 100
|
||||
|
||||
self.bufsize = bufsize
|
||||
self.initialize()
|
||||
|
||||
def initialize(self, start_time=0):
|
||||
"""
|
||||
Opens the file, creates the pipe.
|
||||
|
||||
Sets self.pos to the appropriate value (1 if start_time == 0 because
|
||||
it pre-reads the first frame).
|
||||
"""
|
||||
self.close(delete_lastread=False) # if any
|
||||
|
||||
if start_time != 0:
|
||||
offset = min(1, start_time)
|
||||
i_arg = [
|
||||
"-ss",
|
||||
"%.06f" % (start_time - offset),
|
||||
"-i",
|
||||
ffmpeg_escape_filename(self.filename),
|
||||
"-ss",
|
||||
"%.06f" % offset,
|
||||
]
|
||||
else:
|
||||
i_arg = ["-i", ffmpeg_escape_filename(self.filename)]
|
||||
|
||||
# For webm video (vp8 and vp9) with transparent layer, force libvpx/libvpx-vp9
|
||||
# as ffmpeg native webm decoder dont decode alpha layer
|
||||
# (see
|
||||
# https://www.reddit.com/r/ffmpeg/comments/fgpyfb/help_with_webm_with_alpha_channel/
|
||||
# )
|
||||
if self.depth == 4:
|
||||
codec_name = self.infos.get("video_codec_name")
|
||||
if codec_name == "vp9":
|
||||
i_arg = ["-c:v", "libvpx-vp9"] + i_arg
|
||||
elif codec_name == "vp8":
|
||||
i_arg = ["-c:v", "libvpx"] + i_arg
|
||||
|
||||
# print(self.infos)
|
||||
log_step("init", 100, self.infos)
|
||||
|
||||
cmd = (
|
||||
[FFMPEG_BINARY]
|
||||
+ i_arg
|
||||
+ [
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-f",
|
||||
"image2pipe",
|
||||
"-vf",
|
||||
"scale=%d:%d" % tuple(self.size),
|
||||
"-sws_flags",
|
||||
self.resize_algo,
|
||||
"-pix_fmt",
|
||||
self.pixel_format,
|
||||
"-vcodec",
|
||||
"rawvideo",
|
||||
"-",
|
||||
]
|
||||
)
|
||||
|
||||
# print(" ".join(cmd))
|
||||
|
||||
popen_params = cross_platform_popen_params(
|
||||
{
|
||||
"bufsize": self.bufsize,
|
||||
"stdout": sp.PIPE,
|
||||
"stderr": sp.PIPE,
|
||||
"stdin": sp.DEVNULL,
|
||||
}
|
||||
)
|
||||
self.proc = sp.Popen(cmd, **popen_params)
|
||||
|
||||
# self.pos represents the (0-indexed) index of the frame that is next in line
|
||||
# to be read by self.read_frame().
|
||||
# Eg when self.pos is 1, the 2nd frame will be read next.
|
||||
self.pos = self.get_frame_number(start_time)
|
||||
self.last_read = self.read_frame()
|
||||
|
||||
def skip_frames(self, n=1):
|
||||
"""Reads and throws away n frames"""
|
||||
w, h = self.size
|
||||
for i in range(n):
|
||||
self.proc.stdout.read(self.depth * w * h)
|
||||
|
||||
# self.proc.stdout.flush()
|
||||
self.pos += n
|
||||
|
||||
def read_frame(self):
|
||||
"""
|
||||
Reads the next frame from the file.
|
||||
Note that upon (re)initialization, the first frame will already have been read
|
||||
and stored in ``self.last_read``.
|
||||
"""
|
||||
w, h = self.size
|
||||
nbytes = self.depth * w * h
|
||||
|
||||
s = self.proc.stdout.read(nbytes)
|
||||
|
||||
if len(s) != nbytes:
|
||||
warnings.warn(
|
||||
(
|
||||
"In file %s, %d bytes wanted but %d bytes read at frame index"
|
||||
" %d (out of a total %d frames), at time %.02f/%.02f sec."
|
||||
" Using the last valid frame instead."
|
||||
)
|
||||
% (
|
||||
self.filename,
|
||||
nbytes,
|
||||
len(s),
|
||||
self.pos,
|
||||
self.n_frames,
|
||||
1.0 * self.pos / self.fps,
|
||||
self.duration,
|
||||
),
|
||||
UserWarning,
|
||||
)
|
||||
if not hasattr(self, "last_read"):
|
||||
raise IOError(
|
||||
(
|
||||
"MoviePy error: failed to read the first frame of "
|
||||
f"video file {self.filename}. That might mean that the file is "
|
||||
"corrupted. That may also mean that you are using "
|
||||
"a deprecated version of FFMPEG. On Ubuntu/Debian "
|
||||
"for instance the version in the repos is deprecated. "
|
||||
"Please update to a recent version from the website."
|
||||
)
|
||||
)
|
||||
|
||||
result = self.last_read
|
||||
|
||||
else:
|
||||
if hasattr(np, "frombuffer"):
|
||||
result = np.frombuffer(s, dtype="uint8")
|
||||
else:
|
||||
result = np.fromstring(s, dtype="uint8")
|
||||
result.shape = (h, w, len(s) // (w * h)) # reshape((h, w, len(s)//(w*h)))
|
||||
self.last_read = result
|
||||
|
||||
# We have to do this down here because `self.pos` is used in the warning above
|
||||
self.pos += 1
|
||||
|
||||
return result
|
||||
|
||||
def get_frame(self, t):
|
||||
"""Read a file video frame at time t.
|
||||
|
||||
Note for coders: getting an arbitrary frame in the video with
|
||||
ffmpeg can be painfully slow if some decoding has to be done.
|
||||
This function tries to avoid fetching arbitrary frames
|
||||
whenever possible, by moving between adjacent frames.
|
||||
"""
|
||||
# + 1 so that it represents the frame position that it will be
|
||||
# after the frame is read. This makes the later comparisons easier.
|
||||
pos = self.get_frame_number(t) + 1
|
||||
|
||||
# Initialize proc if it is not open
|
||||
if not self.proc:
|
||||
# raise Exception("Proc not detected")
|
||||
self.initialize(t)
|
||||
return self.last_read
|
||||
|
||||
if pos == self.pos:
|
||||
return self.last_read
|
||||
elif (pos < self.pos) or (pos > self.pos + 100):
|
||||
# We can't just skip forward to `pos` or it would take too long
|
||||
self.initialize(t)
|
||||
return self.last_read
|
||||
else:
|
||||
# If pos == self.pos + 1, this line has no effect
|
||||
self.skip_frames(pos - self.pos - 1)
|
||||
result = self.read_frame()
|
||||
return result
|
||||
|
||||
@property
|
||||
def lastread(self):
|
||||
"""Alias of `self.last_read` for backwards compatibility with MoviePy 1.x."""
|
||||
return self.last_read
|
||||
|
||||
def get_frame_number(self, t):
|
||||
"""Helper method to return the frame number at time ``t``"""
|
||||
# I used this horrible '+0.00001' hack because sometimes due to numerical
|
||||
# imprecisions a 3.0 can become a 2.99999999... which makes the int()
|
||||
# go to the previous integer. This makes the fetching more robust when you
|
||||
# are getting the nth frame by writing get_frame(n/fps).
|
||||
return int(self.fps * t + 0.00001)
|
||||
|
||||
def close(self, delete_lastread=True):
|
||||
"""Closes the reader terminating the process, if is still open."""
|
||||
if self.proc:
|
||||
if self.proc.poll() is None:
|
||||
self.proc.terminate()
|
||||
self.proc.stdout.close()
|
||||
self.proc.stderr.close()
|
||||
self.proc.wait()
|
||||
self.proc = None
|
||||
if delete_lastread and hasattr(self, "last_read"):
|
||||
del self.last_read
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
|
||||
def ffmpeg_read_image(filename, with_mask=True, pixel_format=None):
|
||||
"""Read an image file (PNG, BMP, JPEG...).
|
||||
|
||||
Wraps FFMPEG_Videoreader to read just one image.
|
||||
Returns an ImageClip.
|
||||
|
||||
This function is not meant to be used directly in MoviePy.
|
||||
Use ImageClip instead to make clips out of image files.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
filename
|
||||
Name of the image file. Can be of any format supported by ffmpeg.
|
||||
|
||||
with_mask
|
||||
If the image has a transparency layer, ``with_mask=true`` will save
|
||||
this layer as the mask of the returned ImageClip
|
||||
|
||||
pixel_format
|
||||
Optional: Pixel format for the image to read. If is not specified
|
||||
'rgb24' will be used as the default format unless ``with_mask`` is set
|
||||
as ``True``, then 'rgba' will be used.
|
||||
|
||||
"""
|
||||
if not pixel_format:
|
||||
pixel_format = "rgba" if with_mask else "rgb24"
|
||||
reader = FFMPEG_VideoReader(
|
||||
filename, pixel_format=pixel_format, check_duration=False
|
||||
)
|
||||
im = reader.last_read
|
||||
del reader
|
||||
return im
|
||||
|
||||
|
||||
class FFmpegInfosParser:
|
||||
"""Finite state ffmpeg `-i` command option file information parser.
|
||||
Is designed to parse the output fast, in one loop. Iterates line by
|
||||
line of the `ffmpeg -i <filename> [-f null -]` command output changing
|
||||
the internal state of the parser.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
filename
|
||||
Name of the file parsed, only used to raise accurate error messages.
|
||||
|
||||
infos
|
||||
Information returned by FFmpeg.
|
||||
|
||||
fps_source
|
||||
Indicates what source data will be preferably used to retrieve fps data.
|
||||
|
||||
check_duration
|
||||
Enable or disable the parsing of the duration of the file. Useful to
|
||||
skip the duration check, for example, for images.
|
||||
|
||||
decode_file
|
||||
Indicates if the whole file has been decoded. The duration parsing strategy
|
||||
will differ depending on this argument.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
infos,
|
||||
filename,
|
||||
fps_source="fps",
|
||||
check_duration=True,
|
||||
decode_file=False,
|
||||
):
|
||||
self.infos = infos
|
||||
self.filename = filename
|
||||
self.check_duration = check_duration
|
||||
self.fps_source = fps_source
|
||||
self.duration_tag_separator = "time=" if decode_file else "Duration: "
|
||||
|
||||
self._reset_state()
|
||||
|
||||
def _reset_state(self):
|
||||
"""Reinitializes the state of the parser. Used internally at
|
||||
initialization and at the end of the parsing process.
|
||||
"""
|
||||
# could be 2 possible types of metadata:
|
||||
# - file_metadata: Metadata of the container. Here are the tags set
|
||||
# by the user using `-metadata` ffmpeg option
|
||||
# - stream_metadata: Metadata for each stream of the container.
|
||||
self._inside_file_metadata = False
|
||||
|
||||
# this state is needed if `duration_tag_separator == "time="` because
|
||||
# execution of ffmpeg decoding the whole file using `-f null -` appends
|
||||
# to the output the blocks "Stream mapping:" and "Output:", which
|
||||
# should be ignored
|
||||
self._inside_output = False
|
||||
|
||||
# flag which indicates that a default stream has not been found yet
|
||||
self._default_stream_found = False
|
||||
|
||||
# current input file, stream and chapter, which will be built at runtime
|
||||
self._current_input_file = {"streams": []}
|
||||
self._current_stream = None
|
||||
self._current_chapter = None
|
||||
|
||||
# resulting data of the parsing process
|
||||
self.result = {
|
||||
"video_found": False,
|
||||
"audio_found": False,
|
||||
"metadata": {},
|
||||
"inputs": [],
|
||||
}
|
||||
|
||||
# keep the value of latest metadata value parsed so we can build
|
||||
# at next lines a multiline metadata value
|
||||
self._last_metadata_field_added = None
|
||||
|
||||
def parse(self):
|
||||
"""Parses the information returned by FFmpeg in stderr executing their binary
|
||||
for a file with ``-i`` option and returns a dictionary with all data needed
|
||||
by MoviePy.
|
||||
"""
|
||||
# chapters by input file
|
||||
input_chapters = []
|
||||
|
||||
for line in self.infos.splitlines()[1:]:
|
||||
if (
|
||||
self.duration_tag_separator == "time="
|
||||
and self.check_duration
|
||||
and "time=" in line
|
||||
):
|
||||
# parse duration using file decodification
|
||||
self.result["duration"] = self.parse_duration(line)
|
||||
elif self._inside_output or line[0] != " ":
|
||||
if self.duration_tag_separator == "time=" and not self._inside_output:
|
||||
self._inside_output = True
|
||||
# skip lines like "At least one output file must be specified"
|
||||
elif not self._inside_file_metadata and line.startswith(" Metadata:"):
|
||||
# enter " Metadata:" group
|
||||
self._inside_file_metadata = True
|
||||
elif line.startswith(" Duration:"):
|
||||
# exit " Metadata:" group
|
||||
self._inside_file_metadata = False
|
||||
if self.check_duration and self.duration_tag_separator == "Duration: ":
|
||||
self.result["duration"] = self.parse_duration(line)
|
||||
|
||||
# parse global bitrate (in kb/s)
|
||||
bitrate_match = re.search(r"bitrate: (\d+) kb/s", line)
|
||||
self.result["bitrate"] = (
|
||||
int(bitrate_match.group(1)) if bitrate_match else None
|
||||
)
|
||||
|
||||
# parse start time (in seconds)
|
||||
start_match = re.search(r"start: (\d+\.?\d+)", line)
|
||||
self.result["start"] = (
|
||||
float(start_match.group(1)) if start_match else None
|
||||
)
|
||||
elif self._inside_file_metadata:
|
||||
# file metadata line
|
||||
field, value = self.parse_metadata_field_value(line)
|
||||
|
||||
# multiline metadata value parsing
|
||||
if field == "":
|
||||
field = self._last_metadata_field_added
|
||||
value = self.result["metadata"][field] + "\n" + value
|
||||
else:
|
||||
self._last_metadata_field_added = field
|
||||
self.result["metadata"][field] = value
|
||||
elif line.lstrip().startswith("Stream "):
|
||||
# exit stream " Metadata:"
|
||||
if self._current_stream:
|
||||
self._current_input_file["streams"].append(self._current_stream)
|
||||
|
||||
# get input number, stream number, language and type
|
||||
main_info_match = re.search(
|
||||
r"^Stream\s#(\d+):(\d+)(?:\[\w+\])?\(?(\w+)?\)?:\s(\w+):",
|
||||
line.lstrip(),
|
||||
)
|
||||
(
|
||||
input_number,
|
||||
stream_number,
|
||||
language,
|
||||
stream_type,
|
||||
) = main_info_match.groups()
|
||||
input_number = int(input_number)
|
||||
stream_number = int(stream_number)
|
||||
stream_type_lower = stream_type.lower()
|
||||
|
||||
if language == "und":
|
||||
language = None
|
||||
|
||||
# start builiding the current stream
|
||||
self._current_stream = {
|
||||
"input_number": input_number,
|
||||
"stream_number": stream_number,
|
||||
"stream_type": stream_type_lower,
|
||||
"language": language,
|
||||
"default": not self._default_stream_found
|
||||
or line.endswith("(default)"),
|
||||
}
|
||||
self._default_stream_found = True
|
||||
|
||||
# for default streams, set their numbers globally, so it's
|
||||
# easy to get without iterating all
|
||||
if self._current_stream["default"]:
|
||||
self.result[
|
||||
f"default_{stream_type_lower}_input_number"
|
||||
] = input_number
|
||||
self.result[
|
||||
f"default_{stream_type_lower}_stream_number"
|
||||
] = stream_number
|
||||
|
||||
# exit chapter
|
||||
if self._current_chapter:
|
||||
input_chapters[input_number].append(self._current_chapter)
|
||||
self._current_chapter = None
|
||||
|
||||
if "input_number" not in self._current_input_file:
|
||||
# first input file
|
||||
self._current_input_file["input_number"] = input_number
|
||||
elif self._current_input_file["input_number"] != input_number:
|
||||
# new input file
|
||||
|
||||
# include their chapters if there are for this input file
|
||||
if len(input_chapters) >= input_number + 1:
|
||||
self._current_input_file["chapters"] = input_chapters[
|
||||
input_number
|
||||
]
|
||||
|
||||
# add new input file to self.result
|
||||
self.result["inputs"].append(self._current_input_file)
|
||||
self._current_input_file = {"input_number": input_number}
|
||||
|
||||
# parse relevant data by stream type
|
||||
try:
|
||||
global_data, stream_data = self.parse_data_by_stream_type(
|
||||
stream_type, line
|
||||
)
|
||||
except NotImplementedError as exc:
|
||||
warnings.warn(
|
||||
f"{str(exc)}\nffmpeg output:\n\n{self.infos}", UserWarning
|
||||
)
|
||||
else:
|
||||
self.result.update(global_data)
|
||||
self._current_stream.update(stream_data)
|
||||
elif line.startswith(" Metadata:"):
|
||||
# enter group " Metadata:"
|
||||
continue
|
||||
elif self._current_stream:
|
||||
# stream metadata line
|
||||
if "metadata" not in self._current_stream:
|
||||
self._current_stream["metadata"] = {}
|
||||
|
||||
field, value = self.parse_metadata_field_value(line)
|
||||
|
||||
if self._current_stream["stream_type"] == "video":
|
||||
field, value = self.video_metadata_type_casting(field, value)
|
||||
if field == "rotate":
|
||||
self.result["video_rotation"] = value
|
||||
|
||||
# multiline metadata value parsing
|
||||
if field == "":
|
||||
field = self._last_metadata_field_added
|
||||
value = self._current_stream["metadata"][field] + "\n" + value
|
||||
else:
|
||||
self._last_metadata_field_added = field
|
||||
self._current_stream["metadata"][field] = value
|
||||
elif line.startswith(" Chapter"):
|
||||
# Chapter data line
|
||||
if self._current_chapter:
|
||||
# there is a previews chapter?
|
||||
if len(input_chapters) < self._current_chapter["input_number"] + 1:
|
||||
input_chapters.append([])
|
||||
# include in the chapters by input matrix
|
||||
input_chapters[self._current_chapter["input_number"]].append(
|
||||
self._current_chapter
|
||||
)
|
||||
|
||||
# extract chapter data
|
||||
chapter_data_match = re.search(
|
||||
r"^ Chapter #(\d+):(\d+): start (\d+\.?\d+?), end (\d+\.?\d+?)",
|
||||
line,
|
||||
)
|
||||
input_number, chapter_number, start, end = chapter_data_match.groups()
|
||||
|
||||
# start building the chapter
|
||||
self._current_chapter = {
|
||||
"input_number": int(input_number),
|
||||
"chapter_number": int(chapter_number),
|
||||
"start": float(start),
|
||||
"end": float(end),
|
||||
}
|
||||
elif self._current_chapter:
|
||||
# inside chapter metadata
|
||||
if "metadata" not in self._current_chapter:
|
||||
self._current_chapter["metadata"] = {}
|
||||
field, value = self.parse_metadata_field_value(line)
|
||||
|
||||
# multiline metadata value parsing
|
||||
if field == "":
|
||||
field = self._last_metadata_field_added
|
||||
value = self._current_chapter["metadata"][field] + "\n" + value
|
||||
else:
|
||||
self._last_metadata_field_added = field
|
||||
self._current_chapter["metadata"][field] = value
|
||||
|
||||
# last input file, must be included in self.result
|
||||
if self._current_input_file:
|
||||
self._current_input_file["streams"].append(self._current_stream)
|
||||
# include their chapters, if there are any
|
||||
if (
|
||||
"input_number" in self._current_input_file
|
||||
and len(input_chapters) == self._current_input_file["input_number"] + 1
|
||||
):
|
||||
self._current_input_file["chapters"] = input_chapters[
|
||||
self._current_input_file["input_number"]
|
||||
]
|
||||
self.result["inputs"].append(self._current_input_file)
|
||||
|
||||
# some video duration utilities
|
||||
if self.result["video_found"] and self.check_duration:
|
||||
self.result["video_duration"] = self.result["duration"]
|
||||
self.result["video_n_frames"] = int(
|
||||
self.result["duration"] * self.result.get("video_fps", 0)
|
||||
)
|
||||
else:
|
||||
self.result["video_n_frames"] = 0
|
||||
self.result["video_duration"] = 0.0
|
||||
# We could have also recomputed duration from the number of frames, as follows:
|
||||
# >>> result['video_duration'] = result['video_n_frames'] / result['video_fps']
|
||||
|
||||
# not default audio found, assume first audio stream is the default
|
||||
if self.result["audio_found"] and not self.result.get("audio_bitrate"):
|
||||
self.result["audio_bitrate"] = None
|
||||
for streams_input in self.result["inputs"]:
|
||||
for stream in streams_input["streams"]:
|
||||
if stream["stream_type"] == "audio" and stream.get("bitrate"):
|
||||
self.result["audio_bitrate"] = stream["bitrate"]
|
||||
break
|
||||
|
||||
if self.result["audio_bitrate"] is not None:
|
||||
break
|
||||
|
||||
result = self.result
|
||||
|
||||
# reset state of the parser
|
||||
self._reset_state()
|
||||
|
||||
return result
|
||||
|
||||
def parse_data_by_stream_type(self, stream_type, line):
|
||||
"""Parses data from "Stream ... {stream_type}" line."""
|
||||
try:
|
||||
return {
|
||||
"Audio": self.parse_audio_stream_data,
|
||||
"Video": self.parse_video_stream_data,
|
||||
"Data": lambda _line: ({}, {}),
|
||||
}[stream_type](line)
|
||||
except KeyError:
|
||||
raise NotImplementedError(
|
||||
f"{stream_type} stream parsing is not supported by moviepy and"
|
||||
" will be ignored"
|
||||
)
|
||||
|
||||
def parse_audio_stream_data(self, line):
|
||||
"""Parses data from "Stream ... Audio" line."""
|
||||
global_data, stream_data = ({"audio_found": True}, {})
|
||||
try:
|
||||
stream_data["fps"] = int(re.search(r" (\d+) Hz", line).group(1))
|
||||
except (AttributeError, ValueError):
|
||||
# AttributeError: 'NoneType' object has no attribute 'group'
|
||||
# ValueError: invalid literal for int() with base 10: '<string>'
|
||||
stream_data["fps"] = "unknown"
|
||||
match_audio_bitrate = re.search(r"(\d+) kb/s", line)
|
||||
stream_data["bitrate"] = (
|
||||
int(match_audio_bitrate.group(1)) if match_audio_bitrate else None
|
||||
)
|
||||
if self._current_stream["default"]:
|
||||
global_data["audio_fps"] = stream_data["fps"]
|
||||
global_data["audio_bitrate"] = stream_data["bitrate"]
|
||||
return (global_data, stream_data)
|
||||
|
||||
def parse_video_stream_data(self, line):
|
||||
"""Parses data from "Stream ... Video" line."""
|
||||
global_data, stream_data = ({"video_found": True}, {})
|
||||
|
||||
try:
|
||||
match_video_size = re.search(r" (\d+)x(\d+)[,\s]", line)
|
||||
if match_video_size:
|
||||
# size, of the form 460x320 (w x h)
|
||||
stream_data["size"] = [int(num) for num in match_video_size.groups()]
|
||||
except Exception:
|
||||
raise IOError(
|
||||
(
|
||||
"MoviePy error: failed to read video dimensions in"
|
||||
" file '%s'.\nHere are the file infos returned by"
|
||||
"ffmpeg:\n\n%s"
|
||||
)
|
||||
% (self.filename, self.infos)
|
||||
)
|
||||
|
||||
match_bitrate = re.search(r"(\d+) kb/s", line)
|
||||
stream_data["bitrate"] = int(match_bitrate.group(1)) if match_bitrate else None
|
||||
|
||||
# Get the frame rate. Sometimes it's 'tbr', sometimes 'fps', sometimes
|
||||
# tbc, and sometimes tbc/2...
|
||||
# Current policy: Trust fps first, then tbr unless fps_source is
|
||||
# specified as 'tbr' in which case try tbr then fps
|
||||
|
||||
# If result is near from x*1000/1001 where x is 23,24,25,50,
|
||||
# replace by x*1000/1001 (very common case for the fps).
|
||||
|
||||
if self.fps_source == "fps":
|
||||
try:
|
||||
fps = self.parse_fps(line)
|
||||
except (AttributeError, ValueError):
|
||||
fps = self.parse_tbr(line)
|
||||
elif self.fps_source == "tbr":
|
||||
try:
|
||||
fps = self.parse_tbr(line)
|
||||
except (AttributeError, ValueError):
|
||||
fps = self.parse_fps(line)
|
||||
else:
|
||||
raise ValueError(
|
||||
("fps source '%s' not supported parsing the video '%s'")
|
||||
% (self.fps_source, self.filename)
|
||||
)
|
||||
|
||||
# It is known that a fps of 24 is often written as 24000/1001
|
||||
# but then ffmpeg nicely rounds it to 23.98, which we hate.
|
||||
coef = 1000.0 / 1001.0
|
||||
for x in [23, 24, 25, 30, 50]:
|
||||
if (fps != x) and abs(fps - x * coef) < 0.01:
|
||||
fps = x * coef
|
||||
stream_data["fps"] = fps
|
||||
|
||||
# Try to extract video codec and profile
|
||||
main_info_match = re.search(
|
||||
r"Video:\s(\w+)?\s?(\([^)]+\))?",
|
||||
line.lstrip(),
|
||||
)
|
||||
if main_info_match is not None:
|
||||
(codec_name, profile) = main_info_match.groups()
|
||||
stream_data["codec_name"] = codec_name
|
||||
stream_data["profile"] = profile
|
||||
|
||||
if self._current_stream["default"] or "video_codec_name" not in self.result:
|
||||
global_data["video_codec_name"] = stream_data.get("codec_name", None)
|
||||
|
||||
if self._current_stream["default"] or "video_profile" not in self.result:
|
||||
global_data["video_profile"] = stream_data.get("profile", None)
|
||||
|
||||
if self._current_stream["default"] or "video_size" not in self.result:
|
||||
global_data["video_size"] = stream_data.get("size", None)
|
||||
if self._current_stream["default"] or "video_bitrate" not in self.result:
|
||||
global_data["video_bitrate"] = stream_data.get("bitrate", None)
|
||||
if self._current_stream["default"] or "video_fps" not in self.result:
|
||||
global_data["video_fps"] = stream_data["fps"]
|
||||
|
||||
return (global_data, stream_data)
|
||||
|
||||
def parse_fps(self, line):
|
||||
"""Parses number of FPS from a line of the ``ffmpeg -i`` command output."""
|
||||
return float(re.search(r" (\d+.?\d*) fps", line).group(1))
|
||||
|
||||
def parse_tbr(self, line):
|
||||
"""Parses number of TBS from a line of the ``ffmpeg -i`` command output."""
|
||||
s_tbr = re.search(r" (\d+.?\d*k?) tbr", line).group(1)
|
||||
|
||||
# Sometimes comes as e.g. 12k. We need to replace that with 12000.
|
||||
if s_tbr[-1] == "k":
|
||||
tbr = float(s_tbr[:-1]) * 1000
|
||||
else:
|
||||
tbr = float(s_tbr)
|
||||
return tbr
|
||||
|
||||
def parse_duration(self, line):
|
||||
"""Parse the duration from the line that outputs the duration of
|
||||
the container.
|
||||
"""
|
||||
try:
|
||||
time_raw_string = line.split(self.duration_tag_separator)[-1]
|
||||
match_duration = re.search(
|
||||
r"([0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9])",
|
||||
time_raw_string,
|
||||
)
|
||||
return convert_to_seconds(match_duration.group(1))
|
||||
except Exception:
|
||||
raise IOError(
|
||||
(
|
||||
"MoviePy error: failed to read the duration of file '%s'.\n"
|
||||
"Here are the file infos returned by ffmpeg:\n\n%s"
|
||||
)
|
||||
% (self.filename, self.infos)
|
||||
)
|
||||
|
||||
def parse_metadata_field_value(
|
||||
self,
|
||||
line,
|
||||
):
|
||||
"""Returns a tuple with a metadata field-value pair given a ffmpeg `-i`
|
||||
command output line.
|
||||
"""
|
||||
raw_field, raw_value = line.split(":", 1)
|
||||
return (raw_field.strip(" "), raw_value.strip(" "))
|
||||
|
||||
def video_metadata_type_casting(self, field, value):
|
||||
"""Cast needed video metadata fields to other types than the default str."""
|
||||
if field == "rotate":
|
||||
return (field, float(value))
|
||||
return (field, value)
|
||||
|
||||
|
||||
def ffmpeg_parse_infos(
|
||||
filename,
|
||||
check_duration=True,
|
||||
fps_source="fps",
|
||||
decode_file=False,
|
||||
print_infos=False,
|
||||
):
|
||||
"""Get the information of a file using ffmpeg.
|
||||
|
||||
Returns a dictionary with next fields:
|
||||
|
||||
- ``"duration"``
|
||||
- ``"metadata"``
|
||||
- ``"inputs"``
|
||||
- ``"video_found"``
|
||||
- ``"video_fps"``
|
||||
- ``"video_n_frames"``
|
||||
- ``"video_duration"``
|
||||
- ``"video_bitrate"``
|
||||
- ``"video_metadata"``
|
||||
- ``"audio_found"``
|
||||
- ``"audio_fps"``
|
||||
- ``"audio_bitrate"``
|
||||
- ``"audio_metadata"``
|
||||
- ``"video_codec_name"``
|
||||
- ``"video_profile"``
|
||||
|
||||
Note that "video_duration" is slightly smaller than "duration" to avoid
|
||||
fetching the incomplete frames at the end, which raises an error.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
filename
|
||||
Name of the file parsed, only used to raise accurate error messages.
|
||||
|
||||
infos
|
||||
Information returned by FFmpeg.
|
||||
|
||||
fps_source
|
||||
Indicates what source data will be preferably used to retrieve fps data.
|
||||
|
||||
check_duration
|
||||
Enable or disable the parsing of the duration of the file. Useful to
|
||||
skip the duration check, for example, for images.
|
||||
|
||||
decode_file
|
||||
Indicates if the whole file must be read to retrieve their duration.
|
||||
This is needed for some files in order to get the correct duration (see
|
||||
https://github.com/Zulko/moviepy/pull/1222).
|
||||
"""
|
||||
# Open the file in a pipe, read output
|
||||
cmd = [FFMPEG_BINARY, "-hide_banner", "-i", ffmpeg_escape_filename(filename)]
|
||||
if decode_file:
|
||||
cmd.extend(["-f", "null", "-"])
|
||||
|
||||
popen_params = cross_platform_popen_params(
|
||||
{
|
||||
"bufsize": 10**5,
|
||||
"stdout": sp.PIPE,
|
||||
"stderr": sp.PIPE,
|
||||
"stdin": sp.DEVNULL,
|
||||
}
|
||||
)
|
||||
|
||||
proc = sp.Popen(cmd, **popen_params)
|
||||
(output, error) = proc.communicate()
|
||||
infos = error.decode("utf8", errors="ignore")
|
||||
|
||||
proc.terminate()
|
||||
del proc
|
||||
|
||||
if print_infos:
|
||||
# print the whole info text returned by FFMPEG
|
||||
print(infos)
|
||||
|
||||
try:
|
||||
return FFmpegInfosParser(
|
||||
infos,
|
||||
filename,
|
||||
fps_source=fps_source,
|
||||
check_duration=check_duration,
|
||||
decode_file=decode_file,
|
||||
).parse()
|
||||
except Exception as exc:
|
||||
if os.path.isdir(filename):
|
||||
raise IsADirectoryError(f"'{filename}' is a directory")
|
||||
elif not os.path.exists(filename):
|
||||
raise FileNotFoundError(f"'{filename}' not found")
|
||||
raise IOError(f"Error passing `ffmpeg -i` command output:\n\n{infos}") from exc
|
||||
209
moviepy/video/io/ffmpeg_tools.py
Normal file
209
moviepy/video/io/ffmpeg_tools.py
Normal file
@@ -0,0 +1,209 @@
|
||||
"""Miscellaneous bindings to ffmpeg."""
|
||||
|
||||
import os
|
||||
|
||||
from moviepy.config import FFMPEG_BINARY
|
||||
from moviepy.decorators import convert_parameter_to_seconds, convert_path_to_string
|
||||
from moviepy.tools import ffmpeg_escape_filename, subprocess_call
|
||||
|
||||
|
||||
@convert_path_to_string(("inputfile", "outputfile"))
|
||||
@convert_parameter_to_seconds(("start_time", "end_time"))
|
||||
def ffmpeg_extract_subclip(
|
||||
inputfile, start_time, end_time, outputfile=None, logger="bar"
|
||||
):
|
||||
"""Makes a new video file playing video file between two times.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
inputfile : str
|
||||
Path to the file from which the subclip will be extracted.
|
||||
|
||||
start_time : float
|
||||
Moment of the input clip that marks the start of the produced subclip.
|
||||
|
||||
end_time : float
|
||||
Moment of the input clip that marks the end of the produced subclip.
|
||||
|
||||
outputfile : str, optional
|
||||
Path to the output file. Defaults to
|
||||
``<inputfile_name>SUB<start_time>_<end_time><ext>``.
|
||||
"""
|
||||
if not outputfile:
|
||||
name, ext = os.path.splitext(inputfile)
|
||||
t1, t2 = [int(1000 * t) for t in [start_time, end_time]]
|
||||
outputfile = "%sSUB%d_%d%s" % (name, t1, t2, ext)
|
||||
|
||||
cmd = [
|
||||
FFMPEG_BINARY,
|
||||
"-y",
|
||||
"-ss",
|
||||
"%0.2f" % start_time,
|
||||
"-i",
|
||||
ffmpeg_escape_filename(inputfile),
|
||||
"-t",
|
||||
"%0.2f" % (end_time - start_time),
|
||||
"-map",
|
||||
"0",
|
||||
"-vcodec",
|
||||
"copy",
|
||||
"-acodec",
|
||||
"copy",
|
||||
"-copyts",
|
||||
ffmpeg_escape_filename(outputfile),
|
||||
]
|
||||
subprocess_call(cmd, logger=logger)
|
||||
|
||||
|
||||
@convert_path_to_string(("videofile", "audiofile", "outputfile"))
|
||||
def ffmpeg_merge_video_audio(
|
||||
videofile,
|
||||
audiofile,
|
||||
outputfile,
|
||||
video_codec="copy",
|
||||
audio_codec="copy",
|
||||
logger="bar",
|
||||
):
|
||||
"""Merges video file and audio file into one movie file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
videofile : str
|
||||
Path to the video file used in the merge.
|
||||
|
||||
audiofile : str
|
||||
Path to the audio file used in the merge.
|
||||
|
||||
outputfile : str
|
||||
Path to the output file.
|
||||
|
||||
video_codec : str, optional
|
||||
Video codec used by FFmpeg in the merge.
|
||||
|
||||
audio_codec : str, optional
|
||||
Audio codec used by FFmpeg in the merge.
|
||||
"""
|
||||
cmd = [
|
||||
FFMPEG_BINARY,
|
||||
"-y",
|
||||
"-i",
|
||||
ffmpeg_escape_filename(audiofile),
|
||||
"-i",
|
||||
ffmpeg_escape_filename(videofile),
|
||||
"-vcodec",
|
||||
video_codec,
|
||||
"-acodec",
|
||||
audio_codec,
|
||||
ffmpeg_escape_filename(outputfile),
|
||||
]
|
||||
|
||||
subprocess_call(cmd, logger=logger)
|
||||
|
||||
|
||||
@convert_path_to_string(("inputfile", "outputfile"))
|
||||
def ffmpeg_extract_audio(inputfile, outputfile, bitrate=3000, fps=44100, logger="bar"):
|
||||
"""Extract the sound from a video file and save it in ``outputfile``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
inputfile : str
|
||||
The path to the file from which the audio will be extracted.
|
||||
|
||||
outputfile : str
|
||||
The path to the file to which the audio will be stored.
|
||||
|
||||
bitrate : int, optional
|
||||
Bitrate for the new audio file.
|
||||
|
||||
fps : int, optional
|
||||
Frame rate for the new audio file.
|
||||
"""
|
||||
cmd = [
|
||||
FFMPEG_BINARY,
|
||||
"-y",
|
||||
"-i",
|
||||
ffmpeg_escape_filename(inputfile),
|
||||
"-ab",
|
||||
"%dk" % bitrate,
|
||||
"-ar",
|
||||
"%d" % fps,
|
||||
ffmpeg_escape_filename(outputfile),
|
||||
]
|
||||
subprocess_call(cmd, logger=logger)
|
||||
|
||||
|
||||
@convert_path_to_string(("inputfile", "outputfile"))
|
||||
def ffmpeg_resize(inputfile, outputfile, size, logger="bar"):
|
||||
"""Resizes a file to new size and write the result in another.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
inputfile : str
|
||||
Path to the file to be resized.
|
||||
|
||||
outputfile : str
|
||||
Path to the output file.
|
||||
|
||||
size : list or tuple
|
||||
New size in format ``[width, height]`` for the output file.
|
||||
"""
|
||||
cmd = [
|
||||
FFMPEG_BINARY,
|
||||
"-i",
|
||||
ffmpeg_escape_filename(inputfile),
|
||||
"-vf",
|
||||
"scale=%d:%d" % (size[0], size[1]),
|
||||
ffmpeg_escape_filename(outputfile),
|
||||
]
|
||||
|
||||
subprocess_call(cmd, logger=logger)
|
||||
|
||||
|
||||
@convert_path_to_string(("inputfile", "outputfile", "output_dir"))
|
||||
def ffmpeg_stabilize_video(
|
||||
inputfile, outputfile=None, output_dir="", overwrite_file=True, logger="bar"
|
||||
):
|
||||
"""
|
||||
Stabilizes ``filename`` and write the result to ``output``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
inputfile : str
|
||||
The name of the shaky video.
|
||||
|
||||
outputfile : str, optional
|
||||
The name of new stabilized video. Defaults to appending '_stabilized' to
|
||||
the input file name.
|
||||
|
||||
output_dir : str, optional
|
||||
The directory to place the output video in. Defaults to the current
|
||||
working directory.
|
||||
|
||||
overwrite_file : bool, optional
|
||||
If ``outputfile`` already exists in ``output_dir``, then overwrite
|
||||
``outputfile`` Defaults to True.
|
||||
"""
|
||||
if not outputfile:
|
||||
without_dir = os.path.basename(inputfile)
|
||||
name, ext = os.path.splitext(without_dir)
|
||||
outputfile = f"{name}_stabilized{ext}"
|
||||
|
||||
outputfile = os.path.join(output_dir, outputfile)
|
||||
cmd = [
|
||||
FFMPEG_BINARY,
|
||||
"-i",
|
||||
ffmpeg_escape_filename(inputfile),
|
||||
"-vf",
|
||||
"deshake",
|
||||
ffmpeg_escape_filename(outputfile),
|
||||
]
|
||||
|
||||
if overwrite_file:
|
||||
cmd.append("-y")
|
||||
|
||||
subprocess_call(cmd, logger=logger)
|
||||
344
moviepy/video/io/ffmpeg_writer.py
Normal file
344
moviepy/video/io/ffmpeg_writer.py
Normal file
@@ -0,0 +1,344 @@
|
||||
"""
|
||||
On the long term this will implement several methods to make videos
|
||||
out of VideoClips
|
||||
"""
|
||||
|
||||
import subprocess as sp
|
||||
|
||||
import numpy as np
|
||||
from proglog import proglog
|
||||
|
||||
from moviepy.config import FFMPEG_BINARY
|
||||
from moviepy.tools import cross_platform_popen_params, ffmpeg_escape_filename
|
||||
|
||||
|
||||
class FFMPEG_VideoWriter:
|
||||
"""A class for FFMPEG-based video writing.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
filename : str
|
||||
Any filename like ``"video.mp4"`` etc. but if you want to avoid
|
||||
complications it is recommended to use the generic extension ``".avi"``
|
||||
for all your videos.
|
||||
|
||||
size : tuple or list
|
||||
Size of the output video in pixels (width, height).
|
||||
|
||||
fps : int
|
||||
Frames per second in the output video file.
|
||||
|
||||
codec : str, optional
|
||||
FFMPEG codec. It seems that in terms of quality the hierarchy is
|
||||
'rawvideo' = 'png' > 'mpeg4' > 'libx264'
|
||||
'png' manages the same lossless quality as 'rawvideo' but yields
|
||||
smaller files. Type ``ffmpeg -codecs`` in a terminal to get a list
|
||||
of accepted codecs.
|
||||
|
||||
Note for default 'libx264': by default the pixel format yuv420p
|
||||
is used. If the video dimensions are not both even (e.g. 720x405)
|
||||
another pixel format is used, and this can cause problem in some
|
||||
video readers.
|
||||
|
||||
audiofile : str, optional
|
||||
The name of an audio file that will be incorporated to the video.
|
||||
|
||||
preset : str, optional
|
||||
Sets the time that FFMPEG will take to compress the video. The slower,
|
||||
the better the compression rate. Possibilities are: ``"ultrafast"``,
|
||||
``"superfast"``, ``"veryfast"``, ``"faster"``, ``"fast"``, ``"medium"``
|
||||
(default), ``"slow"``, ``"slower"``, ``"veryslow"``, ``"placebo"``.
|
||||
|
||||
bitrate : str, optional
|
||||
Only relevant for codecs which accept a bitrate. "5000k" offers
|
||||
nice results in general.
|
||||
|
||||
with_mask : bool, optional
|
||||
Set to ``True`` if there is a mask in the video to be encoded.
|
||||
|
||||
pixel_format : str, optional
|
||||
Optional: Pixel format for the output video file. If is not specified
|
||||
``"rgb24"`` will be used as the default format unless ``with_mask`` is
|
||||
set as ``True``, then ``"rgba"`` will be used.
|
||||
|
||||
logfile : int, optional
|
||||
File descriptor for logging output. If not defined, ``subprocess.PIPE``
|
||||
will be used. Defined using another value, the log level of the ffmpeg
|
||||
command will be "info", otherwise "error".
|
||||
|
||||
threads : int, optional
|
||||
Number of threads used to write the output with ffmpeg.
|
||||
|
||||
ffmpeg_params : list, optional
|
||||
Additional parameters passed to ffmpeg command.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
filename,
|
||||
size,
|
||||
fps,
|
||||
codec="libx264",
|
||||
audiofile=None,
|
||||
preset="medium",
|
||||
bitrate=None,
|
||||
with_mask=False,
|
||||
logfile=None,
|
||||
threads=None,
|
||||
ffmpeg_params=None,
|
||||
pixel_format=None,
|
||||
):
|
||||
if logfile is None:
|
||||
logfile = sp.PIPE
|
||||
self.logfile = logfile
|
||||
self.filename = filename
|
||||
self.codec = codec
|
||||
self.ext = self.filename.split(".")[-1]
|
||||
|
||||
pixel_format = "rgba" if with_mask else "rgb24"
|
||||
|
||||
# order is important
|
||||
cmd = [
|
||||
FFMPEG_BINARY,
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error" if logfile == sp.PIPE else "info",
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-vcodec",
|
||||
"rawvideo",
|
||||
"-s",
|
||||
"%dx%d" % (size[0], size[1]),
|
||||
"-pix_fmt",
|
||||
pixel_format,
|
||||
"-r",
|
||||
"%.02f" % fps,
|
||||
"-an",
|
||||
"-i",
|
||||
"-",
|
||||
]
|
||||
if audiofile is not None:
|
||||
cmd.extend(["-i", audiofile, "-acodec", "copy"])
|
||||
|
||||
cmd.extend(["-vcodec", codec, "-preset", preset])
|
||||
|
||||
if ffmpeg_params is not None:
|
||||
cmd.extend(ffmpeg_params)
|
||||
|
||||
if bitrate is not None:
|
||||
cmd.extend(["-b", bitrate])
|
||||
|
||||
if threads is not None:
|
||||
cmd.extend(["-threads", str(threads)])
|
||||
|
||||
# Disable auto alt ref for transparent webm and set pix format yo yuva420p
|
||||
if codec == "libvpx" and with_mask:
|
||||
cmd.extend(["-pix_fmt", "yuva420p"])
|
||||
cmd.extend(["-auto-alt-ref", "0"])
|
||||
elif (codec == "libx264") and (size[0] % 2 == 0) and (size[1] % 2 == 0):
|
||||
cmd.extend(["-pix_fmt", "yuva420p"])
|
||||
|
||||
cmd.extend([ffmpeg_escape_filename(filename)])
|
||||
|
||||
popen_params = cross_platform_popen_params(
|
||||
{"stdout": sp.DEVNULL, "stderr": logfile, "stdin": sp.PIPE}
|
||||
)
|
||||
|
||||
self.proc = sp.Popen(cmd, **popen_params)
|
||||
|
||||
def write_frame(self, img_array):
|
||||
"""Writes one frame in the file."""
|
||||
try:
|
||||
self.proc.stdin.write(img_array.tobytes())
|
||||
except IOError as err:
|
||||
_, ffmpeg_error = self.proc.communicate()
|
||||
if ffmpeg_error is not None:
|
||||
ffmpeg_error = ffmpeg_error.decode()
|
||||
else:
|
||||
# The error was redirected to a logfile with `write_logfile=True`,
|
||||
# so read the error from that file instead
|
||||
self.logfile.seek(0)
|
||||
ffmpeg_error = self.logfile.read()
|
||||
|
||||
error = (
|
||||
f"{err}\n\nMoviePy error: FFMPEG encountered the following error while "
|
||||
f"writing file {self.filename}:\n\n {ffmpeg_error}"
|
||||
)
|
||||
|
||||
if "Unknown encoder" in ffmpeg_error:
|
||||
error += (
|
||||
"\n\nThe video export failed because FFMPEG didn't find the "
|
||||
f"specified codec for video encoding {self.codec}. "
|
||||
"Please install this codec or change the codec when calling "
|
||||
"write_videofile.\nFor instance:\n"
|
||||
" >>> clip.write_videofile('myvid.webm', codec='libvpx')"
|
||||
)
|
||||
|
||||
elif "incorrect codec parameters ?" in ffmpeg_error:
|
||||
error += (
|
||||
"\n\nThe video export failed, possibly because the codec "
|
||||
f"specified for the video {self.codec} is not compatible with "
|
||||
f"the given extension {self.ext}.\n"
|
||||
"Please specify a valid 'codec' argument in write_videofile.\n"
|
||||
"This would be 'libx264' or 'mpeg4' for mp4, "
|
||||
"'libtheora' for ogv, 'libvpx for webm.\n"
|
||||
"Another possible reason is that the audio codec was not "
|
||||
"compatible with the video codec. For instance, the video "
|
||||
"extensions 'ogv' and 'webm' only allow 'libvorbis' (default) as a"
|
||||
"video codec."
|
||||
)
|
||||
|
||||
elif "bitrate not specified" in ffmpeg_error:
|
||||
error += (
|
||||
"\n\nThe video export failed, possibly because the bitrate "
|
||||
"specified was too high or too low for the video codec."
|
||||
)
|
||||
|
||||
elif "Invalid encoder type" in ffmpeg_error:
|
||||
error += (
|
||||
"\n\nThe video export failed because the codec "
|
||||
"or file extension you provided is not suitable for video"
|
||||
)
|
||||
|
||||
raise IOError(error)
|
||||
|
||||
def close(self):
|
||||
"""Closes the writer, terminating the subprocess if is still alive."""
|
||||
if self.proc:
|
||||
self.proc.stdin.close()
|
||||
if self.proc.stderr is not None:
|
||||
self.proc.stderr.close()
|
||||
self.proc.wait()
|
||||
|
||||
self.proc = None
|
||||
|
||||
# Support the Context Manager protocol, to ensure that resources are cleaned up.
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
def ffmpeg_write_video(
|
||||
clip,
|
||||
filename,
|
||||
fps,
|
||||
codec="libx264",
|
||||
bitrate=None,
|
||||
preset="medium",
|
||||
write_logfile=False,
|
||||
audiofile=None,
|
||||
threads=None,
|
||||
ffmpeg_params=None,
|
||||
logger="bar",
|
||||
pixel_format=None,
|
||||
):
|
||||
"""Write the clip to a videofile. See VideoClip.write_videofile for details
|
||||
on the parameters.
|
||||
"""
|
||||
logger = proglog.default_bar_logger(logger)
|
||||
|
||||
if write_logfile:
|
||||
logfile = open(filename + ".log", "w+")
|
||||
else:
|
||||
logfile = None
|
||||
|
||||
logger(message="MoviePy - Writing video %s\n" % filename)
|
||||
|
||||
has_mask = clip.mask is not None
|
||||
|
||||
with FFMPEG_VideoWriter(
|
||||
filename,
|
||||
clip.size,
|
||||
fps,
|
||||
codec=codec,
|
||||
preset=preset,
|
||||
bitrate=bitrate,
|
||||
with_mask=has_mask,
|
||||
logfile=logfile,
|
||||
audiofile=audiofile,
|
||||
threads=threads,
|
||||
ffmpeg_params=ffmpeg_params,
|
||||
pixel_format=pixel_format,
|
||||
) as writer:
|
||||
for t, frame in clip.iter_frames(
|
||||
logger=logger, with_times=True, fps=fps, dtype="uint8"
|
||||
):
|
||||
if clip.mask is not None:
|
||||
mask = 255 * clip.mask.get_frame(t)
|
||||
if mask.dtype != "uint8":
|
||||
mask = mask.astype("uint8")
|
||||
frame = np.dstack([frame, mask])
|
||||
|
||||
writer.write_frame(frame)
|
||||
|
||||
if write_logfile:
|
||||
logfile.close()
|
||||
logger(message="MoviePy - Done !")
|
||||
|
||||
|
||||
def ffmpeg_write_image(filename, image, logfile=False, pixel_format=None):
|
||||
"""Writes an image (HxWx3 or HxWx4 numpy array) to a file, using ffmpeg.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
filename : str
|
||||
Path to the output file.
|
||||
|
||||
image : np.ndarray
|
||||
Numpy array with the image data.
|
||||
|
||||
logfile : bool, optional
|
||||
Writes the ffmpeg output inside a logging file (``True``) or not
|
||||
(``False``).
|
||||
|
||||
pixel_format : str, optional
|
||||
Pixel format for ffmpeg. If not defined, it will be discovered checking
|
||||
if the image data contains an alpha channel (``"rgba"``) or not
|
||||
(``"rgb24"``).
|
||||
"""
|
||||
if image.dtype != "uint8":
|
||||
image = image.astype("uint8")
|
||||
|
||||
if not pixel_format:
|
||||
pixel_format = "rgba" if (image.shape[2] == 4) else "rgb24"
|
||||
|
||||
cmd = [
|
||||
FFMPEG_BINARY,
|
||||
"-y",
|
||||
"-s",
|
||||
"%dx%d" % (image.shape[:2][::-1]),
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-pix_fmt",
|
||||
pixel_format,
|
||||
"-i",
|
||||
"-",
|
||||
ffmpeg_escape_filename(filename),
|
||||
]
|
||||
|
||||
if logfile:
|
||||
log_file = open(filename + ".log", "w+")
|
||||
else:
|
||||
log_file = sp.PIPE
|
||||
|
||||
popen_params = cross_platform_popen_params(
|
||||
{"stdout": sp.DEVNULL, "stderr": log_file, "stdin": sp.PIPE}
|
||||
)
|
||||
|
||||
proc = sp.Popen(cmd, **popen_params)
|
||||
out, err = proc.communicate(image.tobytes())
|
||||
|
||||
if proc.returncode:
|
||||
error = (
|
||||
f"{err}\n\nMoviePy error: FFMPEG encountered the following error while "
|
||||
f"writing file {filename} with command {cmd}:\n\n {err.decode()}"
|
||||
)
|
||||
|
||||
raise IOError(error)
|
||||
|
||||
del proc
|
||||
137
moviepy/video/io/ffplay_previewer.py
Normal file
137
moviepy/video/io/ffplay_previewer.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
On the long term this will implement several methods to make videos
|
||||
out of VideoClips
|
||||
"""
|
||||
|
||||
import subprocess as sp
|
||||
|
||||
from moviepy.config import FFPLAY_BINARY
|
||||
from moviepy.tools import cross_platform_popen_params
|
||||
|
||||
|
||||
class FFPLAY_VideoPreviewer:
|
||||
"""A class for FFPLAY-based video preview.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
size : tuple or list
|
||||
Size of the output video in pixels (width, height).
|
||||
|
||||
fps : int
|
||||
Frames per second in the output video file.
|
||||
|
||||
pixel_format : str
|
||||
Pixel format for the output video file, ``rgb24`` for normal video, ``rgba``
|
||||
if video with mask.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size,
|
||||
fps,
|
||||
pixel_format,
|
||||
):
|
||||
# order is important
|
||||
cmd = [
|
||||
FFPLAY_BINARY,
|
||||
"-autoexit", # If you don't precise, ffplay won't stop at end
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-pixel_format",
|
||||
pixel_format,
|
||||
"-video_size",
|
||||
"%dx%d" % (size[0], size[1]),
|
||||
"-framerate",
|
||||
"%.02f" % fps,
|
||||
"-",
|
||||
]
|
||||
|
||||
popen_params = cross_platform_popen_params(
|
||||
{"stdout": sp.DEVNULL, "stderr": sp.STDOUT, "stdin": sp.PIPE}
|
||||
)
|
||||
|
||||
self.proc = sp.Popen(cmd, **popen_params)
|
||||
|
||||
def show_frame(self, img_array):
|
||||
"""Writes one frame in the file."""
|
||||
try:
|
||||
self.proc.stdin.write(img_array.tobytes())
|
||||
except IOError as err:
|
||||
_, ffplay_error = self.proc.communicate()
|
||||
if ffplay_error is not None:
|
||||
ffplay_error = ffplay_error.decode()
|
||||
|
||||
error = (
|
||||
f"{err}\n\nMoviePy error: FFPLAY encountered the following error while "
|
||||
f"previewing clip :\n\n {ffplay_error}"
|
||||
)
|
||||
|
||||
raise IOError(error)
|
||||
|
||||
def close(self):
|
||||
"""Closes the writer, terminating the subprocess if is still alive."""
|
||||
if self.proc:
|
||||
self.proc.stdin.close()
|
||||
if self.proc.stderr is not None:
|
||||
self.proc.stderr.close()
|
||||
self.proc.wait()
|
||||
|
||||
self.proc = None
|
||||
|
||||
# Support the Context Manager protocol, to ensure that resources are cleaned up.
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
def ffplay_preview_video(
|
||||
clip, fps, pixel_format="rgb24", audio_flag=None, video_flag=None
|
||||
):
|
||||
"""Preview the clip using ffplay. See VideoClip.preview for details
|
||||
on the parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
clip : VideoClip
|
||||
The clip to preview
|
||||
|
||||
fps : int
|
||||
Number of frames per seconds in the displayed video.
|
||||
|
||||
pixel_format : str, optional
|
||||
Warning: This is not used anywhere in the code and should probably
|
||||
be removed.
|
||||
It is believed pixel format rgb24 does not work properly for now because
|
||||
it requires applying a mask on CompositeVideoClip and that is believed to
|
||||
not be working.
|
||||
|
||||
Pixel format for the output video file, ``rgb24`` for normal video, ``rgba``
|
||||
if video with mask
|
||||
|
||||
audio_flag : Thread.Event, optional
|
||||
A thread event that video will wait for. If not provided we ignore audio
|
||||
|
||||
video_flag : Thread.Event, optional
|
||||
A thread event that video will set after first frame has been shown. If not
|
||||
provided, we simply ignore
|
||||
"""
|
||||
with FFPLAY_VideoPreviewer(clip.size, fps, pixel_format) as previewer:
|
||||
first_frame = True
|
||||
for t, frame in clip.iter_frames(with_times=True, fps=fps, dtype="uint8"):
|
||||
previewer.show_frame(frame)
|
||||
|
||||
# After first frame is shown, if we have audio/video flag, set video ready
|
||||
# and wait for audio
|
||||
if first_frame:
|
||||
first_frame = False
|
||||
|
||||
if video_flag:
|
||||
video_flag.set() # say to the audio: video is ready
|
||||
|
||||
if audio_flag:
|
||||
audio_flag.wait() # wait for the audio to be ready
|
||||
20
moviepy/video/io/gif_writers.py
Normal file
20
moviepy/video/io/gif_writers.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""MoviePy video GIFs writing."""
|
||||
|
||||
import imageio.v3 as iio
|
||||
import proglog
|
||||
|
||||
from moviepy.decorators import requires_duration, use_clip_fps_by_default
|
||||
|
||||
|
||||
@requires_duration
|
||||
@use_clip_fps_by_default
|
||||
def write_gif_with_imageio(clip, filename, fps=None, loop=0, logger="bar"):
|
||||
"""Writes the gif with the Python library ImageIO (calls FreeImage)."""
|
||||
logger = proglog.default_bar_logger(logger)
|
||||
|
||||
with iio.imopen(filename, "w", plugin="pillow") as writer:
|
||||
logger(message="MoviePy - Building file %s with imageio." % filename)
|
||||
for frame in clip.iter_frames(fps=fps, logger=logger, dtype="uint8"):
|
||||
writer.write(
|
||||
frame, duration=1000 / fps, loop=loop
|
||||
) # Duration is in ms not s
|
||||
Reference in New Issue
Block a user