martin wang
martin wang

Reputation: 129

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#.

Upvotes: 1

Views: 779

Answers (2)

martin wang
martin wang

Reputation: 129

Thanks for @Rotem help. I finally used named pipe to solve the problem. and here is final C# result

async Task Main()
{

    var downloadUrl = @"http://dl.stickershop.LINE.naver.jp/products/0/0/1/23303/iphone/[email protected]";
    var arg = @$"-i  \\.\pipe\apng_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";

    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
                    {
                        // apng output
                        using (var pngFileStream = File.Create(Path.Combine("D:", "Projects", "ffmpeg", "Temp", $"{entry.Name}")))
                        {
                            entry.Open().CopyTo(pngFileStream);
                        }

                        // convert to  webm output
                        using (var fileStream = File.Create(Path.Combine("D:", "Projects", "ffmpeg", "Temp", $"{Path.GetFileNameWithoutExtension(entry.Name)}.webm")))
                        using (var entryStream = entry.Open())
                        using (MemoryStream ms = new MemoryStream())
                        {
                            StartNamePipedServer(entryStream);
                            var result = await Cli.Wrap("ffmpeg")
                                         .WithArguments(arg)
                                         .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();

}

public void StartNamePipedServer(Stream data)
{
    Task.Factory.StartNew(() =>
    {
        using (var server = new NamedPipeServerStream("apng_pipe"))
        {
            server.WaitForConnection();
            CopyStream(data, server);
        }
    });
}

public static void CopyStream(Stream input, Stream output)
{
    int read;
    byte[] buffer = new byte[0x1024];
    while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
    {
        output.Write(buffer, 0, read);
    }
}

Upvotes: 1

Rotem
Rotem

Reputation: 32094

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)

Upvotes: 1

Related Questions