1

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/stickerpack@2x.zip";
    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/stickerpack@2x.zip"
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#.

martin wang
  • 129
  • 1
  • 2
  • 12
  • I don't think we can use `-i pipe:.png`. Replace it with `-f image2pipe pipe:` (I never tried `image2pipe` with apng... you may also try `-f png_pipe`, and try `-i pipe:` without the `-f` before it). – Rotem Nov 27 '22 at 15:31
  • @Rotem hi there, I have tried -f `-f image2pipe` and `-f png_pipe` and they will converted successfully without error, but the result will be static file, It look likes only convert only one frame png file to video file webm not whole apng. – martin wang Nov 28 '22 at 01:53
  • Have you tried without any `-f` (without `-f` before the `-i`)? – Rotem Nov 28 '22 at 06:34
  • @Rotem yes, I have tried it. and the original success file show error message `image2pipe: Invalid argument` ,`png_pipe: Invalid argument` and the original failed file still same error message `pipe:: Function not implemented` – martin wang Nov 28 '22 at 06:53
  • I meant without `-f image2pipe` and without `-f png_pipe`. – Rotem Nov 28 '22 at 10:28
  • @Rotem Do you mean only `-i pipe:`? if yes and the result will be as same as `-i pipe:.png,` some of them success some of them not. – martin wang Nov 28 '22 at 10:49
  • I don't think I can post an answer with C#... I may try similar case in Python (if you want). For testing, please share one APNG sample that works, and one sample that fails. Instead of sharing APNG files, there is an option to post a command line that build synthetic pattern. Example: `ffmpeg -f lavfi -i mandelbrot=size=320x240:rate=3 -t 10 test.apng` or `ffmpeg -y -f lavfi -i testsrc=size=320x240:rate=3 -pix_fmt rgba -t 10 test.apng`. Make sure to fine one sample that fails and one that succeed (try different lengths, different resolutions, different `pix_fmt`...). – Rotem Nov 28 '22 at 12:25
  • @Rotem I have add python code to article, you can try it. Each file will output three file, the original apng file, output file .webm and info file .txt, you can see the ffmpeg output info in the info txt file. – martin wang Nov 28 '22 at 15:14

2 Answers2

1

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 397189868@2x.png | 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/stickerpack@2x.zip"
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)
Rotem
  • 30,366
  • 4
  • 32
  • 65
1

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/stickerpack@2x.zip";
    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);
    }
}

martin wang
  • 129
  • 1
  • 2
  • 12