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:
And the successed convert file from ffmpeg infromation:
It's strange when I was manually converted these failed convert file from command line, and it will be converted successed.
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#.
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):
apng_pipe.apng
): apng_pipe = "apng_pipe.apng"
os.mkfifo(apng_pipe)
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)
-i apng_pipe.apng
argument instead of pipe:
. p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=output_info)
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
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)