Ctrl K
ComunidadGuías…

files/helpers.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Kudos to Werner Robitza, AVEQ GmbH, for helping with ffmpeg
# related content

import hashlib
import json
import logging
import os
import random
import shutil
import subprocess
import tempfile
from fractions import Fraction

import filetype
from django.conf import settings
from django.utils.safestring import mark_safe

CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

logger = logging.getLogger(__name__)


CRF_ENCODING_NUM_SECONDS = 2  # 0 * 60 # videos with greater duration will get
# CRF encoding and not two-pass
# Encoding individual chunks may yield quality variations if you use a
# too low bitrate, so if you go for the chunk-based variant
# you should use CRF encoding.

MAX_RATE_MULTIPLIER = 1.5
MIN_RATE_MULTIPLIER = 0.5

BUF_SIZE_MULTIPLIER = 1.5

# in seconds, anything between 2 and 6 makes sense
KEYFRAME_DISTANCE = 4
KEYFRAME_DISTANCE_MIN = 2

# VP9_SPEED = 1  # between 0 and 4, lower is slower
VP9_SPEED = 2


VIDEO_CRFS = {
    "h264_baseline": 23,
    "h264": 23,
    "h265": 28,
    "vp9": 32,
}

# video rates for 25 or 60 fps input, for different codecs, in kbps
VIDEO_BITRATES = {
    "h264": {
        25: {
            144: 150,
            240: 300,
            360: 500,
            480: 1000,
            720: 2500,
            1080: 4500,
            1440: 9000,
            2160: 18000,
        },
        60: {720: 3500, 1080: 7500, 1440: 18000, 2160: 40000},
    },
    "h265": {
        25: {
            144: 75,
            240: 150,
            360: 275,
            480: 500,
            720: 1024,
            1080: 1800,
            1440: 4500,
            2160: 10000,
        },
        60: {720: 1800, 1080: 3000, 1440: 8000, 2160: 18000},
    },
    "vp9": {
        25: {
            144: 75,
            240: 150,
            360: 275,
            480: 500,
            720: 1024,
            1080: 1800,
            1440: 4500,
            2160: 10000,
        },
        60: {720: 1800, 1080: 3000, 1440: 8000, 2160: 18000},
    },
}


AUDIO_ENCODERS = {"h264": "aac", "h265": "aac", "vp9": "libopus"}

AUDIO_BITRATES = {"h264": 128, "h265": 128, "vp9": 96}

EXTENSIONS = {"h264": "mp4", "h265": "mp4", "vp9": "webm"}

VIDEO_PROFILES = {"h264": "main", "h265": "main"}


def get_portal_workflow():
    return settings.PORTAL_WORKFLOW


def get_media_is_reviewed():
    return settings.MEDIA_IS_REVIEWED


def get_default_state(user=None):
    # possible states given the portal workflow setting
    state = "private"
    if settings.PORTAL_WORKFLOW == "public":
        state = "public"
    if settings.PORTAL_WORKFLOW == "unlisted":
        state = "unlisted"
    if settings.PORTAL_WORKFLOW == "private_verified":
        if user and user.advancedUser:
            state = "unlisted"
    return state


def get_file_name(filename):
    return filename.split("/")[-1]


def get_file_type(filename):
    if not os.path.exists(filename):
        return None
    file_type = None
    kind = filetype.guess(filename)
    if kind is not None:
        if kind.mime.startswith("video"):
            file_type = "video"
        elif kind.mime.startswith("image"):
            file_type = "image"
        elif kind.mime.startswith("audio"):
            file_type = "audio"
        elif "pdf" in kind.mime:
            file_type = "pdf"
    else:
        # TODO: do something for files not supported by filetype lib
        pass
    return file_type


def rm_file(filename):
    if os.path.isfile(filename):
        try:
            os.remove(filename)
            return True
        except OSError:
            pass
    return False


def rm_files(filenames):
    if isinstance(filenames, list):
        for filename in filenames:
            rm_file(filename)
    return True


def rm_dir(directory):
    if os.path.isdir(directory):
        # refuse to delete a dir inside project BASE_DIR
        if directory.startswith(settings.BASE_DIR):
            try:
                shutil.rmtree(directory)
                return True
            except (FileNotFoundError, PermissionError):
                pass
    return False


SCRIPT_JSON_ESCAPES = str.maketrans({"<": r"\u003C", ">": r"\u003E", "&": r"\u0026"})


def json_for_script(value):
    return mark_safe(json.dumps(value).translate(SCRIPT_JSON_ESCAPES))


def url_from_path(filename):
    # TODO: find a way to preserver http - https ...
    return f"{settings.MEDIA_URL}{filename.replace(settings.MEDIA_ROOT, '')}"


def create_temp_file(suffix=None, dir=settings.TEMP_DIRECTORY):
    tf = tempfile.NamedTemporaryFile(delete=False, suffix=suffix, dir=dir)
    return tf.name


def create_temp_dir(suffix=None, dir=settings.TEMP_DIRECTORY):
    td = tempfile.mkdtemp(dir=dir)
    return td


def produce_friendly_token(token_len=settings.FRIENDLY_TOKEN_LEN):
    token = ""
    while len(token) != token_len:
        token += CHARS[random.randint(0, len(CHARS) - 1)]
    return token


def clean_friendly_token(token):
    # cleans token
    for char in token:
        if char not in CHARS:
            token.replace(char, "")
    return token


def mask_ip(ip_address):
    return hashlib.md5(ip_address.encode("utf-8")).hexdigest()


def run_command(cmd, cwd=None):
    """
    Run a command directly
    """
    if isinstance(cmd, str):
        cmd = cmd.split()
    ret = {}
    if cwd:
        process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
    else:
        process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = process.communicate()
    # TODO: catch unicodedecodeerrors here...
    if process.returncode == 0:
        try:
            ret["out"] = stdout.decode("utf-8")
        except BaseException:
            ret["out"] = ""
        try:
            ret["error"] = stderr.decode("utf-8")
        except BaseException:
            ret["error"] = ""
    else:
        try:
            ret["error"] = stderr.decode("utf-8")
        except BaseException:
            ret["error"] = ""
    return ret


def media_file_info(input_file):
    """
    Get the info about an input file, as determined by ffprobe

    Returns a dict, with the keys:
    - `filename`: Filename
    - `file_size`: Size of the file in bytes
    - `video_duration`: Duration of the video in `s.msec`
    - `video_frame_rate_d`: Framerate franction denominator
    - `video_frame_rate_n`: Framerate fraction nominator
    - `video_bitrate`: Bitrate of the video stream in kBit/s
    - `video_width`: Width in pixels
    - `video_height`: Height in pixels
    - `interlaced` : True if the video is interlaced
    - `video_codec`: Video codec
    - `audio_duration`: Duration of the audio in `s.msec`
    - `audio_sample_rate`: Audio sample rate in Hz
    - `audio_codec`: Audio codec name (`aac`)
    - `audio_bitrate`: Bitrate of the video stream in kBit/s

    Also returns the video and audio info raw from ffprobe.
    """
    ret = {}

    if not os.path.isfile(input_file):
        ret["fail"] = True
        return ret

    video_info = {}
    audio_info = {}
    cmd = ["stat", "-c", "%s", input_file]

    stdout = run_command(cmd).get("out")
    if stdout:
        file_size = int(stdout.strip())
    else:
        ret["fail"] = True
        return ret

    cmd = ["md5sum", input_file]
    stdout = run_command(cmd).get("out")
    if stdout:
        md5sum = stdout.split()[0]
    else:
        md5sum = ""

    cmd = [
        settings.FFPROBE_COMMAND,
        "-loglevel",
        "error",
        "-show_streams",
        "-show_entries",
        "format=format_name",
        "-of",
        "json",
        input_file,
    ]
    stdout = run_command(cmd).get("out")
    try:
        info = json.loads(stdout)
    except TypeError:
        ret["fail"] = True
        return ret

    has_video = False
    has_audio = False
    for stream_info in info["streams"]:
        if stream_info["codec_type"] == "video":
            video_info = stream_info
            has_video = True
            if info.get("format") and info["format"].get("format_name", "") in [
                "tty",
                "image2",
                "image2pipe",
                "bin",
                "png_pipe",
                "gif",
            ]:
                ret["fail"] = True
                return ret
        elif stream_info["codec_type"] == "audio":
            audio_info = stream_info
            has_audio = True

    if not has_video:
        ret["is_video"] = False
        ret["is_audio"] = has_audio
        ret["audio_info"] = audio_info
        return ret

    if "duration" in video_info.keys():
        video_duration = float(video_info["duration"])
    elif "tags" in video_info.keys() and "DURATION" in video_info["tags"]:
        duration_str = video_info["tags"]["DURATION"]
        try:
            hms, msec = duration_str.split(".")
        except ValueError:
            hms, msec = duration_str.split(",")

        total_dur = sum(int(x) * 60**i for i, x in enumerate(reversed(hms.split(":"))))
        video_duration = total_dur + float("0." + msec)
    else:
        # fallback to format, eg for webm
        cmd = [
            settings.FFPROBE_COMMAND,
            "-loglevel",
            "error",
            "-show_format",
            "-of",
            "json",
            input_file,
        ]
        stdout = run_command(cmd).get("out")
        format_info = json.loads(stdout)["format"]
        try:
            video_duration = float(format_info["duration"])
        except KeyError:
            ret["fail"] = True
            return ret

    if "bit_rate" in video_info.keys():
        video_bitrate = round(float(video_info["bit_rate"]) / 1024.0, 2)
    else:
        cmd = [
            settings.FFPROBE_COMMAND,
            "-loglevel",
            "error",
            "-select_streams",
            "v",
            "-show_entries",
            "packet=size",
            "-of",
            "compact=p=0:nk=1",
            input_file,
        ]
        stdout = run_command(cmd).get("out")
        stream_size = sum([int(line.replace("|", "")) for line in stdout.split("\n") if line != ""])
        video_bitrate = round((stream_size * 8 / 1024.0) / video_duration, 2)

    if "r_frame_rate" in video_info.keys():
        video_frame_rate = video_info["r_frame_rate"].partition("/")
        video_frame_rate_n = video_frame_rate[0]
        video_frame_rate_d = video_frame_rate[2]

    interlaced = False
    if video_info.get("field_order") in ("tt", "tb", "bt", "bb"):
        interlaced = True

    ret = {
        "filename": input_file,
        "file_size": file_size,
        "video_duration": video_duration,
        "video_frame_rate_n": video_frame_rate_n,
        "video_frame_rate_d": video_frame_rate_d,
        "video_bitrate": video_bitrate,
        "video_width": video_info["width"],
        "video_height": video_info["height"],
        "video_codec": video_info["codec_name"],
        "has_video": has_video,
        "has_audio": has_audio,
        "color_range": video_info.get("color_range"),
        "color_space": video_info.get("color_space"),
        "color_transfer": video_info.get("color_space"),
        "color_primaries": video_info.get("color_primaries"),
        "interlaced": interlaced,
        "display_aspect_ratio": video_info.get("display_aspect_ratio"),
        "sample_aspect_ratio": video_info.get("sample_aspect_ratio"),
    }

    if has_audio:
        if "duration" in audio_info.keys():
            audio_duration = float(audio_info["duration"])
        elif "tags" in audio_info.keys() and "DURATION" in audio_info["tags"]:
            duration_str = audio_info["tags"]["DURATION"]
            try:
                hms, msec = duration_str.split(".")
            except ValueError:
                hms, msec = duration_str.split(",")
            total_dur = sum(int(x) * 60**i for i, x in enumerate(reversed(hms.split(":"))))
            audio_duration = total_dur + float("0." + msec)
        else:
            # fallback to format, eg for webm
            cmd = [
                settings.FFPROBE_COMMAND,
                "-loglevel",
                "error",
                "-show_format",
                "-of",
                "json",
                input_file,
            ]
            stdout = run_command(cmd).get("out")
            format_info = json.loads(stdout)["format"]
            audio_duration = float(format_info["duration"])

        if "bit_rate" in audio_info.keys():
            audio_bitrate = round(float(audio_info["bit_rate"]) / 1024.0, 2)
        else:
            # fall back to calculating from accumulated frame duration
            cmd = [
                settings.FFPROBE_COMMAND,
                "-loglevel",
                "error",
                "-select_streams",
                "a",
                "-show_entries",
                "packet=size",
                "-of",
                "compact=p=0:nk=1",
                input_file,
            ]
            stdout = run_command(cmd).get("out")
            # ffprobe appends a pipe at the end of the output, thus we have to remove it
            stream_size = sum([int(line.replace("|", "")) for line in stdout.split("\n") if line != ""])
            audio_bitrate = round((stream_size * 8 / 1024.0) / audio_duration, 2)

        ret.update(
            {
                "audio_duration": audio_duration,
                "audio_sample_rate": audio_info["sample_rate"],
                "audio_codec": audio_info["codec_name"],
                "audio_bitrate": audio_bitrate,
                "audio_channels": audio_info["channels"],
            }
        )

    ret["video_info"] = video_info
    ret["audio_info"] = audio_info
    ret["is_video"] = True
    ret["md5sum"] = md5sum
    return ret


def calculate_seconds(duration):
    # returns seconds, given a ffmpeg extracted string
    ret = 0
    if isinstance(duration, str):
        duration = duration.split(":")
        if len(duration) != 3:
            return ret
    else:
        return ret

    ret += int(float(duration[2]))
    ret += int(float(duration[1])) * 60
    ret += int(float(duration[0])) * 60 * 60
    return ret


def show_file_size(size):
    if size:
        size = size / 1000000
        size = round(size, 1)
        size = f"{str(size)}MB"
    return size


def get_base_ffmpeg_command(
    input_file,
    output_file,
    has_audio,
    codec,
    encoder,
    audio_encoder,
    target_fps,
    interlaced,
    target_height,
    target_rate,
    target_rate_audio,
    pass_file,
    pass_number,
    enc_type,
    chunk,
):
    """Get the base command for a specific codec, height/rate, and pass

    Arguments:
        input_file {str} -- input file name
        output_file {str} -- output file name
        has_audio {bool} -- does the input have audio?
        codec {str} -- video codec
        encoder {str} -- video encoder
        audio_encoder {str} -- audio encoder
        target_fps {fractions.Fraction} -- target FPS
        interlaced {bool} -- true if interlaced
        target_height {int} -- height
        target_rate {int} -- target bitrate in kbps
        target_rate_audio {int} -- audio target bitrate
        pass_file {str} -- path to temp pass file
        pass_number {int} -- number of passes
        enc_type {str} -- encoding type (twopass or crf)
    """

    # avoid very high frame rates
    while target_fps > 60:
        target_fps = target_fps / 2

    if target_fps < 1:
        target_fps = 1

    filters = []

    if interlaced:
        filters.append("yadif")

    target_width = round(target_height * 16 / 9)
    scale_filter_opts = [
        f"if(lt(iw\\,ih)\\,{target_height}\\,{target_width})",  # noqa
        f"if(lt(iw\\,ih)\\,{target_width}\\,{target_height})",  # noqa
        "force_original_aspect_ratio=decrease",
        "force_divisible_by=2",
        "flags=lanczos",
    ]
    scale_filter_str = "scale=" + ":".join(scale_filter_opts)
    filters.append(scale_filter_str)

    fps_str = f"fps=fps={target_fps}"
    filters.append(fps_str)

    filters_str = ",".join(filters)

    base_cmd = [
        settings.FFMPEG_COMMAND,
        "-y",
        "-i",
        input_file,
        "-c:v",
        encoder,
        "-filter:v",
        filters_str,
        "-pix_fmt",
        "yuv420p",
    ]

    if enc_type == "twopass":
        base_cmd.extend(["-b:v", str(target_rate) + "k"])
    elif enc_type == "crf":
        base_cmd.extend(["-crf", str(VIDEO_CRFS[codec])])
        if encoder == "libvpx-vp9":
            base_cmd.extend(["-b:v", str(target_rate) + "k"])

    if has_audio:
        base_cmd.extend(
            [
                "-c:a",
                audio_encoder,
                "-b:a",
                str(target_rate_audio) + "k",
                # stereo audio only, see https://trac.ffmpeg.org/ticket/5718
                "-ac",
                "2",
            ]
        )

    # get keyframe distance in frames
    keyframe_distance = int(target_fps * KEYFRAME_DISTANCE)

    # start building the command
    cmd = base_cmd[:]

    # preset settings
    preset = getattr(settings, "FFMPEG_DEFAULT_PRESET", "medium")

    if encoder == "libvpx-vp9":
        if pass_number == 1:
            speed = 4
        else:
            speed = VP9_SPEED

    if encoder == "libx264":
        level = "4.2" if target_height <= 1080 else "5.2"

        x264_params = [
            "keyint=" + str(keyframe_distance * 2),
            "keyint_min=" + str(keyframe_distance),
        ]

        cmd.extend(
            [
                "-maxrate",
                str(int(int(target_rate) * MAX_RATE_MULTIPLIER)) + "k",
                "-bufsize",
                str(int(int(target_rate) * BUF_SIZE_MULTIPLIER)) + "k",
                "-force_key_frames",
                "expr:gte(t,n_forced*" + str(KEYFRAME_DISTANCE) + ")",
                "-x264-params",
                ":".join(x264_params),
                "-preset",
                preset,
                "-profile:v",
                VIDEO_PROFILES[codec],
                "-level",
                level,
            ]
        )

        if enc_type == "twopass":
            cmd.extend(["-passlogfile", pass_file, "-pass", pass_number])

    elif encoder == "libx265":
        x265_params = [
            "vbv-maxrate=" + str(int(int(target_rate) * MAX_RATE_MULTIPLIER)),
            "vbv-bufsize=" + str(int(int(target_rate) * BUF_SIZE_MULTIPLIER)),
            "keyint=" + str(keyframe_distance * 2),
            "keyint_min=" + str(keyframe_distance),
        ]

        if enc_type == "twopass":
            x265_params.extend(["stats=" + str(pass_file), "pass=" + str(pass_number)])

        cmd.extend(
            [
                "-force_key_frames",
                "expr:gte(t,n_forced*" + str(KEYFRAME_DISTANCE) + ")",
                "-x265-params",
                ":".join(x265_params),
                "-preset",
                preset,
                "-profile:v",
                VIDEO_PROFILES[codec],
            ]
        )
    elif encoder == "libvpx-vp9":
        cmd.extend(
            [
                "-g",
                str(keyframe_distance),
                "-keyint_min",
                str(keyframe_distance),
                "-maxrate",
                str(int(int(target_rate) * MAX_RATE_MULTIPLIER)) + "k",
                "-minrate",
                str(int(int(target_rate) * MIN_RATE_MULTIPLIER)) + "k",
                "-bufsize",
                str(int(int(target_rate) * BUF_SIZE_MULTIPLIER)) + "k",
                "-speed",
                speed,
                #            '-deadline', 'realtime',
            ]
        )

        if enc_type == "twopass":
            cmd.extend(["-passlogfile", pass_file, "-pass", pass_number])

    cmd.extend(
        [
            "-strict",
            "-2",
        ]
    )

    # end of the command
    if pass_number == 1:
        cmd.extend(["-an", "-f", "null", "/dev/null"])
    elif pass_number == 2:
        if output_file.endswith("mp4") and chunk:
            cmd.extend(["-movflags", "+faststart"])
        cmd.extend([output_file])

    return cmd


def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_filename, pass_file, chunk=False):
    try:
        media_info = json.loads(media_info)
    except BaseException:
        media_info = {}

    if codec == "h264":
        encoder = "libx264"
        # ext = "mp4"
    elif codec in ["h265", "hevc"]:
        encoder = "libx265"
        # ext = "mp4"
    elif codec == "vp9":
        encoder = "libvpx-vp9"
        # ext = "webm"
    else:
        return False

    target_fps = Fraction(int(media_info.get("video_frame_rate_n", 30)), int(media_info.get("video_frame_rate_d", 1)))
    if target_fps <= 30:
        target_rate = VIDEO_BITRATES[codec][25].get(resolution)
    else:
        target_rate = VIDEO_BITRATES[codec][60].get(resolution)
    if not target_rate:  # INVESTIGATE MORE!
        target_rate = VIDEO_BITRATES[codec][25].get(resolution)
    if not target_rate:
        return False

    if media_info.get("video_height") < resolution:
        if resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
            return False

    #    if codec == "h264_baseline":
    #        target_fps = 25
    #    else:

    if media_info.get("video_duration") > CRF_ENCODING_NUM_SECONDS:
        enc_type = "crf"
    else:
        enc_type = "twopass"

    if enc_type == "twopass":
        passes = [1, 2]
    elif enc_type == "crf":
        passes = [2]

    interlaced = media_info.get("interlaced")

    cmds = []
    for pass_number in passes:
        cmds.append(
            get_base_ffmpeg_command(
                media_file,
                output_file=output_filename,
                has_audio=media_info.get("has_audio"),
                codec=codec,
                encoder=encoder,
                audio_encoder=AUDIO_ENCODERS[codec],
                target_fps=target_fps,
                interlaced=interlaced,
                target_height=resolution,
                target_rate=target_rate,
                target_rate_audio=AUDIO_BITRATES[codec],
                pass_file=pass_file,
                pass_number=pass_number,
                enc_type=enc_type,
                chunk=chunk,
            )
        )
    return cmds


def clean_query(query):
    """This is used to clear text in order to comply with SearchQuery
    known exception cases

    :param query: str - the query text that we want to clean
    :return:
    """

    if not query:
        return ""

    chars = ["^", "{", "}", "&", "|", "<", ">", '"', ")", "(", "!", ":", ";", "'", "#"]
    for char in chars:
        query = query.replace(char, "")

    return query.lower()


def timestamp_to_seconds(timestamp):
    """Convert a timestamp in format HH:MM:SS.mmm to seconds

    Args:
        timestamp (str): Timestamp in format HH:MM:SS.mmm

    Returns:
        float: Timestamp in seconds
    """
    h, m, s = timestamp.split(':')
    s, ms = s.split('.')
    return int(h) * 3600 + int(m) * 60 + int(s) + float('0.' + ms)


def seconds_to_timestamp(seconds):
    """Convert seconds to timestamp in format HH:MM:SS.mmm

    Args:
        seconds (float): Time in seconds

    Returns:
        str: Timestamp in format HH:MM:SS.mmm
    """
    hours = int(seconds // 3600)
    minutes = int((seconds % 3600) // 60)
    seconds_remainder = seconds % 60
    seconds_int = int(seconds_remainder)
    milliseconds = int((seconds_remainder - seconds_int) * 1000)

    return f"{hours:02d}:{minutes:02d}:{seconds_int:02d}.{milliseconds:03d}"  # noqa


def get_trim_timestamps(media_file_path, timestamps_list, run_ffprobe=False):
    """Process a list of timestamps to align start times with I-frames for better video trimming

    Args:
        media_file_path (str): Path to the media file
        timestamps_list (list): List of dictionaries with startTime and endTime

    Returns:
        list: Processed timestamps with adjusted startTime values
    """
    if not isinstance(timestamps_list, list):
        return []

    timestamps_results = []
    timestamps_to_process = []

    for item in timestamps_list:
        if isinstance(item, dict) and 'startTime' in item and 'endTime' in item:
            timestamps_to_process.append(item)

    if not timestamps_to_process:
        return []

    # just a single timestamp with no startTime, no need to process
    if len(timestamps_to_process) == 1 and timestamps_to_process[0]['startTime'] == "00:00:00.000":
        return timestamps_list

    # Process each timestamp
    for item in timestamps_to_process:
        startTime = item['startTime']
        endTime = item['endTime']

        # with ffmpeg -ss -i that is getting run, there is no need to call ffprobe to find the I-frame,
        # as ffmpeg will do that. Keeping this for now in case it is needed

        i_frames = []
        if run_ffprobe:
            SEC_TO_SUBTRACT = 10
            start_seconds = timestamp_to_seconds(startTime)
            search_start = max(0, start_seconds - SEC_TO_SUBTRACT)

            # Create ffprobe command to find nearest I-frame
            cmd = [
                settings.FFPROBE_COMMAND,
                "-v",
                "error",
                "-select_streams",
                "v:0",
                "-show_entries",
                "frame=pts_time,pict_type",
                "-of",
                "csv=p=0",
                "-read_intervals",
                f"{search_start}%{startTime}",
                media_file_path,
            ]
            cmd = [str(s) for s in cmd]
            logger.info(f"trim cmd: {cmd}")

            stdout = run_command(cmd).get("out")

            if stdout:
                for line in stdout.strip().split('\n'):
                    if line and line.endswith(',I'):
                        i_frames.append(line.replace(',I', ''))

            if i_frames:
                adjusted_startTime = seconds_to_timestamp(float(i_frames[-1]))

        if not i_frames:
            adjusted_startTime = startTime

        timestamps_results.append({'startTime': adjusted_startTime, 'endTime': endTime})

    return timestamps_results


def trim_video_method(media_file_path, timestamps_list):
    """Trim a video file based on a list of timestamps

    Args:
        media_file_path (str): Path to the media file
        timestamps_list (list): List of dictionaries with startTime and endTime

    Returns:
        bool: True if successful, False otherwise
    """
    if not isinstance(timestamps_list, list) or not timestamps_list:
        return False

    if not os.path.exists(media_file_path):
        return False

    with tempfile.TemporaryDirectory(dir=settings.TEMP_DIRECTORY) as temp_dir:
        # Detect input file extension to preserve original format
        _, input_ext = os.path.splitext(media_file_path)
        output_file = os.path.join(temp_dir, f"output{input_ext}")

        segment_files = []
        for i, item in enumerate(timestamps_list):
            start_time = timestamp_to_seconds(item['startTime'])
            end_time = timestamp_to_seconds(item['endTime'])
            duration = end_time - start_time

            # For single timestamp, we can use the output file directly
            # For multiple timestamps, we need to create segment files
            segment_file = output_file if len(timestamps_list) == 1 else os.path.join(temp_dir, f"segment_{i}{input_ext}")

            cmd = [settings.FFMPEG_COMMAND, "-y", "-ss", str(item['startTime']), "-i", media_file_path, "-t", str(duration), "-c", "copy", "-avoid_negative_ts", "1", segment_file]

            result = run_command(cmd)  # noqa

            if os.path.exists(segment_file) and os.path.getsize(segment_file) > 0:
                if len(timestamps_list) > 1:
                    segment_files.append(segment_file)
            else:
                return False

        if len(timestamps_list) > 1:
            if not segment_files:
                return False

            concat_list_path = os.path.join(temp_dir, "concat_list.txt")
            with open(concat_list_path, "w") as f:
                for segment in segment_files:
                    f.write(f"file '{segment}'\n")
            concat_cmd = [settings.FFMPEG_COMMAND, "-y", "-f", "concat", "-safe", "0", "-i", concat_list_path, "-c", "copy", output_file]

            concat_result = run_command(concat_cmd)  # noqa

            if not os.path.exists(output_file) or os.path.getsize(output_file) == 0:
                return False

        # Replace the original file with the trimmed version
        try:
            rm_file(media_file_path)
            shutil.copy2(output_file, media_file_path)
            return True
        except Exception as e:
            logger.info(f"Failed to replace original file: {str(e)}")
            return False


def get_alphanumeric_only(string):
    """Returns a query that contains only alphanumeric characters
    This include characters other than the English alphabet too
    """
    string = "".join([char for char in string if char.isalnum()])
    return string.lower()


def get_alphanumeric_and_spaces(string):
    """Returns a query that contains only alphanumeric characters and spaces
    This include characters other than the English alphabet too
    """
    string = "".join([char for char in string if char.isalnum() or char.isspace()])
    # Replace multiple spaces with single space and strip
    string = " ".join(string.split())
    return string