From 6a00ccf06965481e56680aff8c9d21ab03d99b04 Mon Sep 17 00:00:00 2001 From: Parsha Pourkhomami Date: Fri, 23 Aug 2013 18:41:09 -0700 Subject: [PATCH] Convert to path-based API to work around ffmpeg issue --- .gitignore | 1 + README.md | 25 +++------ index.js | 147 +++++++++++++-------------------------------------- test/test.js | 92 +++++++++++++++++++++----------- 4 files changed, 106 insertions(+), 159 deletions(-) diff --git a/.gitignore b/.gitignore index 2ccbe46..46c33e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /node_modules/ +/test/__* diff --git a/README.md b/README.md index a8a6d8a..58da82a 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,6 @@ Read and write media file meta data (e.g., MP3 ID3 tags) using ffmpeg's metadata framework. -**Known issue with embedded images:** ffmpeg seems to have an issue -when the input contains images embedded in the audio file (e.g., album -artwork). See this [*ffmpeg-users* post](http://ffmpeg.org/pipermail/ffmpeg-user/2013-August/016667.html) -for more information. - # Example ```js @@ -15,18 +10,14 @@ var ffmetadata = require("ffmetadata"), fs = require("fs"); // Read song.mp3 metadata -fs.createReadStream("song.mp3") - .pipe(ffmetadata(function(data) { - console.log(data); - })) - .on("error", function() { - console.error("Error getting metadata"); - }); +ffmetadata.read("song.mp3", function(err, data) { + console.log(data); +}); // Set the artist for song.mp3 -fs.createReadStream("song.mp3") - .pipe(ffmetadata({ - artist: "Me", - })) - .pipe(fs.createWriteStream("song.mp3")) +ffmetadata.write("song.mp3", { + artist: "Me", +}, function(err) { + if (err) console.error("Error writing metadata"); +}); ``` diff --git a/index.js b/index.js index 5172781..24c1570 100644 --- a/index.js +++ b/index.js @@ -6,24 +6,15 @@ var spawn = require("child_process").spawn, through = require("through"), concat = require("concat-stream"); -var domain = require("domain"); - -module.exports.read = function(callback) { +module.exports.read = function(src, callback) { var stream = through(), - proc = spawnRead(), + proc = spawnRead(src), output = parseini(), error = concat(); // Proxy any child process error events along proc.on("error", stream.emit.bind(stream, "error")); - // Work around pipe ECONNRESET error - proc.stdin.on("error", function(err) { - if (err.errno !== "ECONNRESET" && err.errno !== "EPIPE") { - stream.emit("error", err); - } - }); - // Parse ffmetadata "ini" output proc.stdout.pipe(output); @@ -40,69 +31,51 @@ module.exports.read = function(callback) { }); if (callback) { - stream.on("metadata", callback); + stream.on("metadata", callback.bind(null, null)); + stream.on("error", callback); } - stream.pipe(proc.stdin); return stream; }; -var stream = require("stream"); - -module.exports.write = function(data) { - var tee = through(); - var retstream = through(function(data) { - tee.write(data); - }, function() { - tee.end(); - }); - - var buffer = new stream.PassThrough({ highWaterMark: Infinity }); - - tee.pipe(buffer); - - var formatStream = tee.pipe(format(function(format) { - var proc = spawnWrite(data, format.format.format_name); - var error = concat(); - - buffer.pipe(proc.stdin); - - // Work around pipe ECONNRESET error - proc.stdin.on("error", function(err) { - if (err.errno !== "ECONNRESET" && err.errno !== "EPIPE") { - retstream.emit("error", err); - } - }); +module.exports.write = function(src, data, callback) { + var stream = through(), + proc = spawnWrite(src, data), + error = concat(); - // Proxy any child process error events - proc.on("error", retstream.emit.bind(retstream, "error")); + // Proxy any child process error events + proc.on("error", stream.emit.bind(stream, "error")); - // Proxy child process stdout but don't end the stream until we know - // the process exits with a zero exit code - proc.stdout.on("data", retstream.emit.bind(retstream, "data")); + // Proxy child process stdout but don't end the stream until we know + // the process exits with a zero exit code + proc.stdout.on("data", stream.emit.bind(stream, "data")); - // Capture stderr (to use in case of non-zero exit code) - proc.stderr.pipe(error); + // Capture stderr (to use in case of non-zero exit code) + proc.stderr.pipe(error); - proc.on("close", function(code) { - if (code === 0) { - retstream.emit("end"); - } - else { - retstream.emit("error", new Error(error.getBody().toString())); - } - }); - })); + proc.on("close", function(code) { + if (code === 0) { + stream.emit("end"); + } + else { + stream.emit("error", new Error(error.getBody().toString())); + } + }); - formatStream.on("error", retstream.emit.bind(retstream, "error")); + if (callback) { + stream.on("end", callback); + stream.on("error", callback); + } - return retstream; + return stream; }; -function spawnRead() { +// -- Child process helpers + +function spawnRead(src) { var args = [ "-i", - "pipe:0", // input from stdin + src, "-f", "ffmetadata", "pipe:1", // output to stdout @@ -111,10 +84,11 @@ function spawnRead() { return ffmpeg(args, { detached: true, encoding: "binary" }); } -function spawnWrite(data, format) { +function spawnWrite(src, data) { var args = [ + "-y", "-i", - "pipe:0", // input from stdin + src, // input from src path "-codec", "copy", ]; @@ -125,60 +99,11 @@ function spawnWrite(data, format) { args.push(escapeini(name) + "=" + escapeini(data[name])); }); - args.push("-f", format); - - args.push("pipe:1"); // output to stdout + args.push(src); // output to src path return ffmpeg(args); } -// -- ffprobe - -var format = module.exports.format = function format(callback) { - var stream = through(), - proc = ffprobe(), - output = concat(), - error = concat(); - - stream.pipe(proc.stdin); - proc.stdout.pipe(output); - proc.stderr.pipe(error); - proc.on("error", stream.emit.bind(stream, "error")); - - // Work around pipe ECONNRESET error - proc.stdin.on("error", function(err) { - if (err.errno !== "ECONNRESET" && err.errno !== "EPIPE") { - stream.emit("error", err); - } - }); - - proc.on("close", function(code) { - if (code === 0) { - stream.emit("format", JSON.parse(output.getBody().toString())); - } - else { - stream.emit("error", new Error(error.getBody().toString())); - } - }); - - if (callback) { - stream.on("format", callback); - } - - return stream; -}; - -function ffprobe() { - var args = [ - "-print_format", - "json", - "-show_format", - "pipe:0", - ]; - - return spawn("ffprobe", args); -} - // -- Parse ini var combine = require("stream-combiner"), diff --git a/test/test.js b/test/test.js index f55dc12..6e30631 100644 --- a/test/test.js +++ b/test/test.js @@ -3,47 +3,77 @@ var path = require("path"), fs = require("fs"), + through = require("through"), test = require("tape"), ffmetadata = require("../"); -var TEST_FILE = path.join(__dirname, "test.mp3"), - TEST_FILE_ARTWORK = path.join(__dirname, "test-artwork.mp3"); +var TEST_FILE_ORIG = path.join(__dirname, "test.mp3"), + TEST_FILE_ARTWORK_ORIG = path.join(__dirname, "test-artwork.mp3"), + TEST_FILE = path.join(__dirname, "__test.mp3"), + TEST_FILE_ARTWORK = path.join(__dirname, "__test-artwork.mp3"); + +function copy(src, dst) { + var stream = through(), + readStream = fs.createReadStream(src), + writeStream = fs.createWriteStream(dst); + readStream.pipe(writeStream); + writeStream.on("finish", stream.emit.bind(stream, "end")); + return stream; +} + +var PassThrough = require("stream").PassThrough; +function ender() { + var stream = new PassThrough(); + var remaining = 0; + var end = stream.end.bind(stream); + stream.end = function() { + remaining -= 1; + if (remaining === 0) end(); + }; + stream.on("pipe", function() { + remaining += 1; + }); + stream.resume(); + return stream; +} + +test("copy test files", function(t) { + var end = ender(); + copy(TEST_FILE_ORIG, TEST_FILE).pipe(end); + copy(TEST_FILE_ARTWORK_ORIG, TEST_FILE_ARTWORK).pipe(end); + end.on("end", t.end.bind(t)); +}); test("read metadata", function(t) { - fs.createReadStream(TEST_FILE) - .pipe(ffmetadata.read(function(data) { - t.ok(data); - t.ok(data.artist); - t.end(); - })); + ffmetadata.read(TEST_FILE, function(err, data) { + t.ifError(err); + t.ok(data); + t.ok(data.artist); + t.end(); + }); }); test("write metadata", function(t) { - fs.createReadStream(TEST_FILE) - .pipe(ffmetadata.write({ + ffmetadata.write(TEST_FILE, { artist: "foo", - })) - .pipe(ffmetadata.read(function(data) { - t.equal(data.artist, "foo"); - t.end(); - })); + }, function(err) { + t.ifError(err); + ffmetadata.read(TEST_FILE, function(err, data) { + t.ifError(err); + t.equal(data.artist, "foo"); + t.end(); + }); + }); }); - test("write metadata with artwork", function(t) { - fs.createReadStream(TEST_FILE_ARTWORK) - .pipe(ffmetadata.write({ + ffmetadata.write(TEST_FILE_ARTWORK, { artist: "foo", - })) - .pipe(ffmetadata.read(function(data) { - t.equal(data.artist, "foo"); - t.end(); - })); -}); - -test("get format data", function(t) { - fs.createReadStream(TEST_FILE) - .pipe(ffmetadata.format(function(data) { - t.ok(data); - t.end(); - })); + }, function(err) { + t.ifError(err); + ffmetadata.read(TEST_FILE_ARTWORK, function(err, data) { + t.ifError(err); + t.equal(data.artist, "foo"); + t.end(); + }); + }); });