Functions to extract frames from a video file or device as images using Xav, an Elixir wrapper around FFmpeg.
Frames can be extracted by frame number or by millisecond
offset with Image.Video.image_from_video/2. Streams of
frames can be produced with Image.Video.stream!/2.
A video must first be opened with Image.Video.open/2. The
underlying Xav reader is garbage-collected, so explicit
close/1 is no longer required, but the function is provided
as a no-op for source compatibility.
The pattern can be wrapped by Image.Video.with_video/2 which
opens a video, executes a function with the video reference,
and (since closing is no longer required) simply discards the
reference at the end.
Note
This module is only available if the optional dependency
Xav is in your mix.exs. Xav
in turn requires FFmpeg ≥ 6.0 to be installed on the system.
Migration from the eVision-backed implementation
Earlier releases of Image used :evision (OpenCV) for
video frame extraction. From version 0.66.0 the implementation
is FFmpeg-based via :xav. The public function shapes are the
same with these intentional differences:
The opaque video struct is
%Image.Video{}rather than%Evision.VideoCapture{}. Pattern-match on the new struct module if your code does so.Backend selection (the
:backendoption toopen/2) has been removed. FFmpeg is the only backend.Camera input is now opened with a device path string rather than an integer index.
:default_camerastill works on Linux (resolves to/dev/video0) and on macOS (resolves to AVFoundation device 0). Other camera indices need an explicit device string.Frame-based seeking (
seek(video, frame: n)andimage_from_video(video, frame: n)) is now implemented as a time-based seek ton / fpsfollowed by zero or morenext_framecalls to land on the exact frame. For keyframe-only files this is exact; for inter-frame compressed files (the common case) the behaviour is the same since FFmpeg seeks to the nearest keyframe and decodes forward.
Summary
Files and streams
Closes a video.
Closes a video, raising on error.
Opens a video for frame extraction.
Opens a video for frame extraction, raising on error.
Returns a Stream of images from a video.
Opens a video, calls the given function with the video reference, and discards the reference when the function returns.
Operations
Reads a single frame from a video as an Vix.Vips.Image.t/0.
Reads a single frame from a video as an
Vix.Vips.Image.t/0, raising on error. See
image_from_video/2.
Advances the video head by frames frames without
decoding them as images.
Seeks the video head to a frame or millisecond offset.
Seeks the video head to a frame or millisecond offset,
raising on error. See seek/2.
Guards
Guards that a frame offset is valid for a video
Guards that a stream identifier is valid for a video device
Guards that a millisecond count is valid for a video
Types
Options for Image.Video.open/2. Currently empty — the
:backend option supported by the previous eVision-backed
implementation has been removed.
The valid options for Image.Video.seek/2 and
Image.Video.image_from_video/2.
A video source. Either a file path / URL accepted by FFmpeg,
:default_camera for the system's first webcam, or an
explicit device path / integer index.
Options for Image.Video.stream!/2.
The representation of an open video.
Files and streams
Closes a video.
Xav's reader is garbage-collected so explicit close is not
required. This function is provided for source compatibility
with the previous implementation: it returns
{:ok, %Image.Video{reader: nil}} so subsequent operations
against the same struct will fail with a clear error.
Arguments
videois anyImage.Video.t/0returned fromopen/2.
Returns
{:ok, video}wherevideo.readeris nownil.
Closes a video, raising on error.
See close/1.
@spec open(source(), open_options()) :: {:ok, t()} | {:error, Image.error()}
Opens a video for frame extraction.
Arguments
sourceis one of:- a file path to a video file;
- a URL accepted by FFmpeg (
http://,https://,rtmp://,rtsp://, …); - the atom
:default_camerafor the system's first webcam. Resolves to/dev/video0on Linux. On macOS this is passed to FFmpeg's AVFoundation input; - a non-negative integer camera index. Resolves to
/dev/videoNon Linux. Use a device path string on other platforms; - a device path string interpreted by FFmpeg directly.
optionsis a keyword list. Currently no options are defined; the:backendoption supported by previous releases has been removed.
Returns
{:ok, %Image.Video{}}on success or{:error, %Image.Error{}}.
Example
iex> {:ok, video} = Image.Video.open("./test/support/video/video_sample.mp4")
iex> video.fps
30.0
Opens a video for frame extraction, raising on error.
See open/2.
@spec stream!(t(), stream_options()) :: Enumerable.t()
Returns a Stream of images from a video.
Arguments
videois anyImage.Video.t/0returned fromopen/2.optionsis a keyword list of options.
Options
:frame— start frame offset (default0).:millisecond— start millisecond offset.:start— same as:frame(kept for back-compat).:finish— last frame offset, inclusive. Default-1(meaning to the end of the video).:step— number of frames to advance between yielded frames. Default1.
Only one of :frame / :millisecond may be supplied.
Returns
- A
Streamthat producesVix.Vips.Image.t/0images lazily as enumerated.
Example
iex> video = Image.Video.open!("./test/support/video/video_sample.mp4")
iex> video |> Image.Video.stream!(start: 0, finish: 2) |> Enum.count()
3
Opens a video, calls the given function with the video reference, and discards the reference when the function returns.
Arguments
sourceis the filename of a video file, a URL accepted by FFmpeg, or a device specifier — seeopen/2.funis a 1-arity function called with the open%Image.Video{}struct.
Returns
The result of
fun.(video)or{:error, reason}if the video could not be opened.
Example
iex> result = Image.Video.with_video("./test/support/video/video_sample.mp4", &Image.Video.image_from_video/1)
iex> match?({:ok, %Vix.Vips.Image{}}, result)
true
Operations
@spec image_from_video(t(), seek_options()) :: {:ok, Vix.Vips.Image.t()} | {:error, Image.error()}
Reads a single frame from a video as an Vix.Vips.Image.t/0.
Arguments
videois anyImage.Video.t/0returned fromopen/2.optionsis[],[frame: n], or[millisecond: n].
Returns
{:ok, image}or{:error, %Image.Error{}}.
Example
iex> {:ok, video} = Image.Video.open("./test/support/video/video_sample.mp4")
iex> {:ok, _image} = Image.Video.image_from_video(video)
iex> {:ok, _image} = Image.Video.image_from_video(video, frame: 0)
iex> {:ok, _image} = Image.Video.image_from_video(video, millisecond: 1_000)
iex> {:error, %Image.Error{reason: :negative_offset}} = Image.Video.image_from_video(video, frame: -1)
iex> {:error, %Image.Error{reason: :frame_out_of_range}} = Image.Video.image_from_video(video, frame: 500)
iex> :ok
:ok
@spec image_from_video!(t(), seek_options()) :: Vix.Vips.Image.t() | no_return()
Reads a single frame from a video as an
Vix.Vips.Image.t/0, raising on error. See
image_from_video/2.
@spec scrub(t(), pos_integer()) :: {:ok, t()} | {:error, Image.error()}
Advances the video head by frames frames without
decoding them as images.
Arguments
videois anyImage.Video.t/0returned fromopen/2.framesis the number of frames to advance.
Returns
{:ok, video}after advancing or{:error, %Image.Error{}}.
@spec seek(t(), seek_options()) :: {:ok, t()} | {:error, Image.error()}
Seeks the video head to a frame or millisecond offset.
Note that seeking is not supported on live video streams such as a webcam.
Arguments
videois anyImage.Video.t/0returned fromopen/2.optionsis a keyword list with exactly one of:frame: non_neg_integer()— seek to a frame offset.millisecond: non_neg_integer()— seek to a millisecond offset.
Returns
{:ok, video}on success or{:error, %Image.Error{}}.
Example
iex> {:ok, video} = Image.Video.open("./test/support/video/video_sample.mp4")
iex> {:ok, _} = Image.Video.seek(video, frame: 0)
iex> {:ok, _} = Image.Video.seek(video, millisecond: 1_000)
iex> {:error, %Image.Error{reason: :negative_offset}} = Image.Video.seek(video, frame: -1)
iex> :ok
:ok
@spec seek!(t(), seek_options()) :: t() | no_return()
Seeks the video head to a frame or millisecond offset,
raising on error. See seek/2.
Guards
Types
@type open_options() :: []
Options for Image.Video.open/2. Currently empty — the
:backend option supported by the previous eVision-backed
implementation has been removed.
@type seek_options() :: [{:frame, non_neg_integer()}] | [{:millisecond, non_neg_integer()}]
The valid options for Image.Video.seek/2 and
Image.Video.image_from_video/2.
@type source() :: Path.t() | :default_camera | non_neg_integer() | String.t()
A video source. Either a file path / URL accepted by FFmpeg,
:default_camera for the system's first webcam, or an
explicit device path / integer index.
@type stream_options() :: [ start: non_neg_integer(), finish: integer(), step: pos_integer(), frame: non_neg_integer() | nil, millisecond: non_neg_integer() | nil ]
Options for Image.Video.stream!/2.
@type t() :: %Image.Video{ duration_seconds: float(), fps: float(), frame_count: non_neg_integer(), height: pos_integer() | nil, reader: Xav.Reader.t() | nil, source: source(), width: pos_integer() | nil }
The representation of an open video.
:reader holds the underlying Xav.Reader struct. The
derived :fps, :duration_seconds, :frame_count, :width,
and :height fields are computed at open time so callers can
pattern-match without re-querying FFmpeg.