Skip to content

Commit

Permalink
Convert to path-based API to work around ffmpeg issue
Browse files Browse the repository at this point in the history
  • Loading branch information
parshap committed Aug 25, 2013
1 parent 9ef30bc commit 6a00ccf
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 159 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/node_modules/
/test/__*
25 changes: 8 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,21 @@
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
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");
});
```
147 changes: 36 additions & 111 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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
Expand All @@ -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",
];
Expand All @@ -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"),
Expand Down
92 changes: 61 additions & 31 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});

0 comments on commit 6a00ccf

Please sign in to comment.