Source code for cutcutcodec.core.analysis.video.properties.duration

"""Find the duration of a video stream.

This allows not only the characteristics of the files but also the tags if there are any.
"""

import collections
import pathlib
from fractions import Fraction

import cv2  # pip install opencv-contrib-python-headless

from cutcutcodec.core.analysis._helper_properties import _check_pathexists_index, _mix_and_check
from cutcutcodec.core.analysis.ffprobe import (
    _decode_duration_ffmpeg,
    _estimate_duration_ffmpeg,
    _map_index_rel_to_abs,
)
from cutcutcodec.core.exceptions import MissingInformation, MissingStreamError


def _decode_duration_cv2(filename: str, index: int) -> Fraction:
    """Extract the duration by the complete decoding of the stream.

    Slow but 100% accurate method.

    Examples
    --------
    >>> from cutcutcodec.core.analysis.video.properties.duration import _decode_duration_cv2
    >>> from cutcutcodec.utils import get_project_root
    >>> video = str(get_project_root() / "media" / "video" / "intro.webm")
    >>> _decode_duration_cv2(video, 0)
    Fraction(294281, 30000)
    >>>

    """
    cap = cv2.VideoCapture(filename, index)
    if not cap.isOpened():
        raise MissingStreamError(f"impossible to open '{filename}' stream {index} with 'cv2'")
    if (fps := Fraction(cap.get(cv2.CAP_PROP_FPS)).limit_denominator(1001)) <= 0:
        one_over_fps = 0
    else:
        one_over_fps = 1 / fps
    duration = Fraction(0)
    while True:
        duration = (
            Fraction(round(cap.get(cv2.CAP_PROP_POS_MSEC))) / 1000
            or duration + one_over_fps
        )
        if not cap.read()[0]:
            break
    cap.release()
    if not duration:
        raise MissingStreamError(f"'cv2' did not find duration '{filename}' stream {index}")
    return duration + one_over_fps


def _estimate_duration_cv2(filename: str, index: int) -> Fraction:
    """Extract the duration from the metadata.

    Very fast method but inaccurate. It doesn't work all the time.

    Examples
    --------
    >>> from cutcutcodec.core.analysis.video.properties.duration import _estimate_duration_cv2
    >>> from cutcutcodec.utils import get_project_root
    >>> video = str(get_project_root() / "media" / "video" / "intro.webm")
    >>> _estimate_duration_cv2(video, 0)
    Fraction(37037, 3750)
    >>>

    """
    cap = cv2.VideoCapture(filename, index)
    if not cap.isOpened():
        raise MissingStreamError(f"impossible to open '{filename}' stream {index} with 'cv2'")
    frames = round(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    fps = Fraction(cap.get(cv2.CAP_PROP_FPS)).limit_denominator(1001)
    duration = frames / fps if fps and frames else 0
    cap.release()
    if duration <= 0:
        raise MissingInformation(
            f"'cv2' does not detect any duration in '{filename}' stream {index}",
        )
    return duration


[docs] def get_duration_video( filename: pathlib.Path | str | bytes, index: int = 0, *, backend: str | None = None, accurate: bool = False, ) -> Fraction: """Recovers the total duration of a video stream. The duration includes the display time of the last frame. Parameters ---------- filename : pathlike The pathlike of the file containing a video stream. index : int The relative index of the video stream being considered, by default the first video stream encountered is selected. backend : str, optional - None (default) : Try to read the stream by trying differents backends. - 'ffmpeg' : Uses the ``ffmpeg`` program in the background. - 'cv2' : Uses the module ``pip install opencv-contrib-python-headless``. accurate : boolean, default=False If True, recovers the duration by fully decoding all the frames in the video. It is very accurate but very slow. If False (default), first tries to get the duration from the file metadata. It's not accurate but very fast. Returns ------- duration : Fraction The total duration of the considerated video stream. Raises ------ MissingStreamError If the file does not contain a playable video stream. MissingInformation If the information is unavailable. Examples -------- >>> from cutcutcodec.core.analysis.video.properties.duration import get_duration_video >>> from cutcutcodec.utils import get_project_root >>> video = get_project_root() / "media" / "video" / "intro.webm" >>> get_duration_video(video) Fraction(9809, 1000) >>> """ _check_pathexists_index(filename, index) return _mix_and_check( backend, accurate, (str(pathlib.Path(filename)), index), collections.OrderedDict([ ( ( lambda filename, index: _estimate_duration_ffmpeg( filename, _map_index_rel_to_abs(filename, index, "video"), ) ), {"accurate": False, "backend": "ffmpeg"}, ), ( ( lambda filename, index: _decode_duration_ffmpeg( filename, _map_index_rel_to_abs(filename, index, "video"), accurate=False, ) ), {"accurate": False, "backend": "ffmpeg"}, ), (_estimate_duration_cv2, {"accurate": False, "backend": "cv2"}), ( ( lambda filename, index: _decode_duration_ffmpeg( filename, _map_index_rel_to_abs(filename, index, "video"), accurate=True, ) ), {"accurate": True, "backend": "ffmpeg"}, ), (_decode_duration_cv2, {"accurate": True, "backend": "cv2"}), ]), )