I'm using the fluent-ffmpeg
library in Node to automatically generate a single thumbnail at the halfway mark of a given video file.
const screenshot = async (pathToFile: string) => {
// Generate a temporary file path outside of the working directory with the extension .jpg
const tempFileName = tmp.tmpNameSync({ postfix: ".jpg" });
try{
await new Promise((resolve, reject) => {
ffmpeg(pathToFile)
.thumbnail({
// This works fine when NOT using tmpNameSync
filename: tempFileName,
count: 1,
timestamps: ["50%"]
})
.on("end", resolve)
.on("error", reject);
});
} catch(err){
console.log(err);
return null;
}
return tempFileName;
};
This implementation works very well when I'm using a "non-temporary" output path, such as /path/to/thumbnail.jpg
. But, when I use a library such as tmp to generate a temporary file name outside of the working directory, ffmpeg throws an error.
Error: ffmpeg exited with code 1: av_interleaved_write_frame(): Input/output error
frame= 1 fps=0.0 q=7.8 size=N/A time=00:00:00.04 bitrate=N/A speed=0.152x
frame= 1 fps=0.0 q=7.8 Lsize=N/A time=00:00:00.04 bitrate=N/A speed=0.141x
video:119kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown
Conversion failed!
I cannot seem to find anything about ffmpeg struggling with accessing temporary directories online, and using the command directly in the terminal works as expected, so I don't believe this to be a permissions issue. Although, I may be going about this incorrectly.
This is the full ffmpeg command that fluent-ffmpeg
generates (reduced filenames so it doesn't look horrible):
ffmpeg -ss 14.118271 -i /var/folders/__/XYZ/T/tmp-XYZ/tmp-XYZ -y -filter_complex scale=w=trunc(oh*a/2)*2:h=720[size0];[size0]split=1[screen0] -vframes 1 -map [screen0] var/folders/__/XYZ/T/tmp-XYZ.jpg
After hours of debugging, I found that the problem was a result of two things.
Firstly, ffmpeg
and os.tmpdir()
do not mix on MacOS.
The tmpdir
method generates a symlink when using MacOS instead of an absolute path, which ffmpeg doesn't seem to like. Although, this seems to be inconsistent in when it does and doesn't affect the outcome.
Regardless, the fix for this is simple.
const fixSymlinkPath = (path: string) => {
// If the current platform is MacOS, prefix the path generated with /private.
// This is the true location of the path.
return process.platform === "darwin"
? `/private${path}`
: path;
};
// Use like so.
let path = fixSymlinkPath(tmp.tmpNameSync());
Secondly, (and this is something I should've noticed earlier) fluent-ffmpeg
strips leading /
from the filename
property, resulting in a relative path and not an absolute one.
This effectively meant that ffmpeg was outputting to a non-existent directory inside __DIRNAME
.
ffmpeg(pathToFile).thumbnail({
folder: "/", // Ensure absolute path, essentially.
filename: tempFileName,
count: 1,
timestamps: ["50%"]
});
Hopefully this helps someone down the track.