提取帧失败:处理输入时发现无效数据

Extracting frame fails with: Invalid data found when processing input

我有以下创建虚拟视频文件的方法:

def create_dummy_mp4_video() -> None:
    cmd = (
        f"ffmpeg -y "  # rewrite if exists
        f"-f lavfi -i color=size=100x100:rate=10:color=black "  # blank video
        f"-f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 "  # silent audio
        f"-t 1 "  # video duration, seconds
        "output.mp4"  # file name
    )
    proc = subprocess.run(
        shlex.split(cmd),
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        shell=False,
    )

    if proc.returncode != 0:
        raise Exception()


@dataclass(frozen=True)
class FakeVideo:
    body: bytes
    width: int
    height: int
    fps: int
    size: int
    frames: int
    length_s: int


def video() -> FakeVideo:
    w, h, fps, sec, filename = 100, 100, 10, 1, "output.mp4"
    create_dummy_mp4_video()
    video_path = os.path.join(os.getcwd(), filename)
    with open(video_path, "rb") as file:
        body = file.read()
        size = len(body)
        frames = fps // sec
        return FakeVideo(
            body=body, width=w, height=h, fps=fps,
            size=size, frames=frames, length_s=sec,
        )

然后我想在特定时间提取一帧,我是这样做的:

async def run_shell_command(frame_millisecond, data: bytes) -> bytes:
    async with aiofiles.tempfile.NamedTemporaryFile("wb") as file:
        await file.write(data)
        proc = await asyncio.create_subprocess_exec(
            "ffmpeg",
            "-i",
            file.name,
            "-ss",
            f"{frame_millisecond}ms",  # seek the position to the specific millisecond
            "-vframes", "1",  # only handle one video frame
            "-c:v", "png",  # select the output encoder
            "-f", "image2pipe", "-",  # force output file to stdout,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await proc.communicate()
        level = logging.DEBUG if proc.returncode == 0 else logging.WARN
        LOGGER.log(level, f"[cmd exited with {proc.returncode}]")
        if stderr:
            print(level, f"[stderr]{stderr.decode()}")
            LOGGER.log(level, f"[stderr]{stderr.decode()}")
        return stdout


async def runner():
    v = video()
    time = int(v.length_s / 2 * 1000)
    res = await run_shell_command(time, v.body)
    assert isinstance(res, bytes)
    assert imghdr.what(h=res, file=None) == "png"


loop = asyncio.get_event_loop()
loop.run_until_complete(runner())

此代码失败并出现以下错误:

/tmp/tmpzo786lfg: Invalid data found when processing input

请帮忙找出我代码中的问题。 在调查过程中,我发现如果我像这样更改视频的大小,它会起作用:

f"-f lavfi -i color=size=1280x720:rate=25:color=black "  # blank video

但我希望能够处理任何视频。

我用的是ffmpg 4.3.3-0+deb11u1

看来你必须确保在执行 FFmpeg 之前将数据写入临时文件。

我对 asyncioaiofiles 没有任何经验,我是 运行 Windows 10,所以我不确定 Linux 行为...

我试着在file.write(data)后面加上await file.flush(),但是FFmpeg执行结果是“Permission denied”。

我使用以下 post 中的解决方案解决了它:

  • delete=False 参数添加到 tempfile.NamedTemporaryFile:

     async with aiofiles.tempfile.NamedTemporaryFile("wb", delete=False) as file:
    
  • await file.write(data)之后添加await file.close()
    关闭文件用于确保所有数据都写入文件,然后再执行 FFmpeg。

  • return stdout前添加os.unlink(file.name)


完整代码:

import subprocess
import asyncio
from dataclasses import dataclass
import shlex
import aiofiles
import os
import logging
import imghdr

def create_dummy_mp4_video() -> None:
    cmd = (
        f"ffmpeg -y "  # rewrite if exists
        f"-f lavfi -i color=size=100x100:rate=10:color=black "  # blank video
        f"-f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 "  # silent audio
        f"-t 1 "  # video duration, seconds
        "output.mp4"  # file name
    )
    proc = subprocess.run(
        shlex.split(cmd),
        stdout=subprocess.PIPE,
        stderr=subprocess.DEVNULL, #stderr=subprocess.PIPE,
        shell=False,
    )

    if proc.returncode != 0:
        raise Exception()


@dataclass(frozen=True)
class FakeVideo:
    body: bytes
    width: int
    height: int
    fps: int
    size: int
    frames: int
    length_s: int


def video() -> FakeVideo:
    w, h, fps, sec, filename = 100, 100, 10, 1, "output.mp4"
    create_dummy_mp4_video()
    video_path = os.path.join(os.getcwd(), filename)
    with open(video_path, "rb") as file:
        body = file.read()
        size = len(body)
        frames = fps // sec
        return FakeVideo(
            body=body, width=w, height=h, fps=fps,
            size=size, frames=frames, length_s=sec,
        )



async def run_shell_command(frame_millisecond, data: bytes) -> bytes:
    # 
    async with aiofiles.tempfile.NamedTemporaryFile("wb", delete=False) as file:
        await file.write(data)
        #await file.flush()  # Flush data to file before executing FFmpeg ?
        await file.close()  # Close the file before executing FFmpeg.
        proc = await asyncio.create_subprocess_exec(
            "ffmpeg",
            "-i",
            file.name,
            "-ss",
            f"{frame_millisecond}ms",  # seek the position to the specific millisecond
            "-vframes", "1",  # only handle one video frame
            "-c:v", "png",  # select the output encoder
            "-f", "image2pipe", "-",  # force output file to stdout,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )

        stdout, stderr = await proc.communicate()
        level = logging.DEBUG if proc.returncode == 0 else logging.WARN
        #LOGGER.log(level, f"[cmd exited with {proc.returncode}]")
        if stderr:
            print(level, f"[stderr]{stderr.decode()}")
            #LOGGER.log(level, f"[stderr]{stderr.decode()}")

        os.unlink(file.name)  # Unlink is required because delete=False was used

        return stdout


async def runner():
    v = video()
    time = int(v.length_s / 2 * 1000)
    res = await run_shell_command(time, v.body)
    assert isinstance(res, bytes)
    assert imghdr.what(h=res, file=None) == "png"


loop = asyncio.get_event_loop()
loop.run_until_complete(runner())

备注:

  • 我删除了 LOGGER 因为找不到 LOGGER 模块。
  • 下次,请将所有导入添加到您发布的代码中(找到它们并非易事)。