Search code examples
c#ffmpegpngwebm

Use C# to converting apng to webm with ffmpeg from pipe input and output


I was using ffmpeg to convert Line sticker from apng file to webm file. And the result is weird, some of them was converted successed and some of them failed. not sure what happend with these failed convert.

Here is my c# code to convert Line sticker to webm, and I use CliWrap to run ffmpeg command line.

async Task Main()
{

    var downloadUrl = @"http://dl.stickershop.LINE.naver.jp/products/0/0/1/23303/iphone/[email protected]";
    var arg = @$"-i pipe:.png -vf scale=512:512:force_original_aspect_ratio=decrease:flags=lanczos -pix_fmt yuva420p -c:v libvpx-vp9 -cpu-used 5 -minrate 50k -b:v 350k -maxrate 450k -to 00:00:02.900 -an -y -f webm pipe:1";

    var errorCount = 0;
    try
    {
        using (var hc = new HttpClient())
        {
            var imgsZip = await hc.GetStreamAsync(downloadUrl);

            using (ZipArchive zipFile = new ZipArchive(imgsZip))
            {
                var files = zipFile.Entries.Where(entry => Regex.IsMatch(entry.FullName, @"animation@2x\/\d+\@2x.png"));
                foreach (var entry in files)
                {
                    try
                    {
                        using (var fileStream = File.Create(Path.Combine("D:", "Projects", "ffmpeg", "Temp", $"{Path.GetFileNameWithoutExtension(entry.Name)}.webm")))
                        using (var pngFileStream = File.Create(Path.Combine("D:", "Projects", "ffmpeg", "Temp", $"{entry.Name}")))
                        using (var entryStream = entry.Open())
                        using (MemoryStream ms = new MemoryStream())
                        {
                            entry.Open().CopyTo(pngFileStream);

                            var result = await Cli.Wrap("ffmpeg")
                                         .WithArguments(arg)
                                         .WithStandardInputPipe(PipeSource.FromStream(entryStream))
                                         .WithStandardOutputPipe(PipeTarget.ToStream(ms))
                                         .WithStandardErrorPipe(PipeTarget.ToFile(Path.Combine("D:", "Projects", "ffmpeg", "Temp", $"{Path.GetFileNameWithoutExtension(entry.Name)}Info.txt")))
                                         .WithValidation(CommandResultValidation.ZeroExitCode)
                                         .ExecuteAsync();
                            ms.Seek(0, SeekOrigin.Begin);
                            ms.WriteTo(fileStream);
                        }
                    }
                    catch (Exception ex)
                    {
                        entry.FullName.Dump();
                        ex.Dump();
                        errorCount++;
                    }
                }
            }

        }
    }
    catch (Exception ex)
    {
        ex.Dump();
    }
    $"Error Count:{errorCount.Dump()}".Dump();

}

This is the failed convert file's error information from ffmpeg:

enter image description here

And the successed convert file from ffmpeg infromation: enter image description here

It's strange when I was manually converted these failed convert file from command line, and it will be converted successed. enter image description here

The question is the resource of images are all the same apng file, so I just can't understan why some of files will convert failed from my c# code but also when I manually use command line will be converted successed?


I have written same exampe from C# to Python... and here is python code:

from io import BytesIO
import os
import re
import subprocess
import zipfile

import requests


downloadUrl = "http://dl.stickershop.LINE.naver.jp/products/0/0/1/23303/iphone/[email protected]"
args = [
    'ffmpeg',
    '-i', 'pipe:',
    '-vf', 'scale=512:512:force_original_aspect_ratio=decrease:flags=lanczos',
    '-pix_fmt', 'yuva420p',
    '-c:v', 'libvpx-vp9',
    '-cpu-used', '5',
    '-minrate', '50k',
    '-b:v', '350k',
    '-maxrate', '450k', '-to', '00:00:02.900', '-an', '-y', '-f', 'webm', 'pipe:1'
]


imgsZip = requests.get(downloadUrl)
with zipfile.ZipFile(BytesIO(imgsZip.content)) as archive:
    files = [file for file in archive.infolist() if re.match(
        "animation@2x\/\d+\@2x.png", file.filename)]
    for entry in files:
        fileName = entry.filename.replace(
            "animation@2x/", "").replace(".png", "")
        rootPath = 'D:\\' + os.path.join("Projects", "ffmpeg", "Temp")
        # original file
        apngFile = os.path.join(rootPath, fileName+'.png')
        # output file
        webmFile = os.path.join(rootPath, fileName+'.webm')
        # output info
        infoFile = os.path.join(rootPath, fileName+'info.txt')

        with archive.open(entry) as file, open(apngFile, 'wb') as output_apng, open(webmFile, 'wb') as output_webm, open(infoFile, 'wb') as output_info:
            p = subprocess.Popen(args, stdin=subprocess.PIPE,
                                 stdout=subprocess.PIPE, stderr=output_info)
            outputBytes = p.communicate(input=file.read())[0]

            output_webm.write(outputBytes)
            file.seek(0)
            output_apng.write(file.read())

And you can try it,the result will be the as same as C#.


Solution

  • It looks like writing APNG to stdin PIPE is not officially supported by FFmpeg.

    According to Wikipedia, APNG files starts with one PNG image, and continue with APNG specific data, so we can't identify APNG format only from the header bytes.
    Passing APNG to pipe may require the non-existed apng_pipe demuxer.
    It could also be a bug in FFmpeg.
    It's just (partially) not working...


    The same APNGs that are not working from Python and C# are also not working from the console.

    Executing:
    type [email protected] | ffmpeg.exe -i pipe: -pix_fmt yuva420p -c:v libvpx-vp9 -y test.webm

    Returns an error message:

    pipe:: Function not implemented


    We may solve it using a Named PIPE (instead of stdin pipe).

    In Python os.mkfifo creates a named pipe (but it's not working in Windows).

    There is an example for using named pipes in C# that supposed to work in Windows (I didn't try it).


    Solving the issue using a named pipe using Python (in Linux):

    • Create the named pipe (name it apng_pipe.apng):
        apng_pipe = "apng_pipe.apng"
        os.mkfifo(apng_pipe)
    
    • Define a "writer" thread that writes to the named pipe in small chunks.
      We have to use a thread because writing to named pipe is a "blocking" operation.
      Writing in small chunks, because the default buffer size of a named pipe is relatively small.
        def writer(data_buf, pipe_name, chunk_size):
            # Open the pipe as opening files (open for "open for writing only").
            fd_pipe = os.open(pipe_name, os.O_WRONLY)  # fd_pipe is a file descriptor (an integer)
        
            for i in range(0, len(data_buf), chunk_size):
                # Write to named pipe as writing to a file (but write the data in small chunks).
                os.write(fd_pipe, data_buf[i:min(chunk_size+i, len(data_buf))])  # Write 1024 bytes of data to fd_pipe
        
            # Closing the pipes as closing files.
            os.close(fd_pipe)
    
    • Start FFmpeg subprocess with -i apng_pipe.apng argument instead of pipe:.
        p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=output_info)
    
    • Initialize "writer" thread, start the thread and wait for the thread to finish, and read the output using p.communicate()[0].
        writer_thread = Thread(target=writer, args=(data, apng_pipe, 1024))
        writer_thread.start()
        writer_thread.join()
    
        outputBytes = p.communicate()[0]  # Read the output from stdout, and ends FFmpeg sub-process
    
    • Remove the named pipe at the end.
        os.unlink(apng_pipe)
    

    Complete code sample (not working in Windows):

    from io import BytesIO
    import os
    import re
    import subprocess
    import zipfile
    from threading import Thread
    import requests
    
    # Name of the "Named pipe"
    apng_pipe = "apng_pipe.apng"
    
    downloadUrl = "http://dl.stickershop.LINE.naver.jp/products/0/0/1/23303/iphone/[email protected]"
    args = [
        'ffmpeg',
        '-i', apng_pipe, #'-i', 'pipe:',
        '-vf', 'scale=512:512:force_original_aspect_ratio=decrease:flags=lanczos',
        '-pix_fmt', 'yuva420p',
        '-c:v', 'libvpx-vp9',
        '-cpu-used', '5',
        '-minrate', '50k',
        '-b:v', '350k',
        '-maxrate', '450k', '-to', '00:00:02.900', '-an', '-y', '-f', 'webm', 'pipe:1'
    ]
    
    
    def writer(data_buf, pipe_name, chunk_size):
        # Open the pipe as opening files (open for "open for writing only").
        fd_pipe = os.open(pipe_name, os.O_WRONLY)  # fd_pipe is a file descriptor (an integer)
    
        for i in range(0, len(data_buf), chunk_size):
            # Write to named pipe as writing to a file (but write the data in small chunks).
            os.write(fd_pipe, data_buf[i:min(chunk_size+i, len(data_buf))])  # Write 1024 bytes of data to fd_pipe
    
        # Closing the pipes as closing files.
        os.close(fd_pipe)
    
    
    # Create "named pipe" (not supported by Windows).
    os.mkfifo(apng_pipe)
    
    
    #imgsZip = requests.get(downloadUrl)
    rootPath = './'
    
    imgsZip = requests.get(downloadUrl)
    with zipfile.ZipFile(BytesIO(imgsZip.content)) as archive:
        files = [file for file in archive.infolist() if re.match(
            "animation@2x\/\d+\@2x.png", file.filename)]
        for entry in files:
            fileName = entry.filename.replace(
                "animation@2x/", "").replace(".png", "")
            # original file
            apngFile = os.path.join(rootPath, fileName+'.png')
            # output file
            webmFile = os.path.join(rootPath, fileName+'.webm')
            # output info
            infoFile = os.path.join(rootPath, fileName+'info.txt')
    
            with archive.open(entry) as file, open(apngFile, 'wb') as output_apng, open(webmFile, 'wb') as output_webm, open(infoFile, 'wb') as output_info:
                data = file.read()
    
                p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=output_info)  # Don't use stdin=subprocess.PIPE
    
                # Initialize "writer" thread (the writer writes data to named pipe in chunks of 1024 bytes).
                # We have to use a thread because writing to named pipe is a "blocking" operation.
                # Write in small chunks, because the default buffer size of a named pipe is relatively small
                writer_thread = Thread(target=writer, args=(data, apng_pipe, 1024))  # writer_thread writes data to apng_pipe
    
                # Start the thread
                writer_thread.start()
    
                # Wait for the writer thread to finish
                writer_thread.join()
    
                outputBytes = p.communicate()[0]
    
                output_webm.write(outputBytes)
                file.seek(0)
                output_apng.write(file.read())
    
    # Remove the "named pipe".
    os.unlink(apng_pipe)