diff --git a/README.md b/README.md index 3d1c9c6..cc2f426 100644 --- a/README.md +++ b/README.md @@ -25,17 +25,16 @@ $ npm install node-video-lib const fs = require('fs'); const VideoLib = require('node-video-lib'); -fs.open('/path/to/file', 'r', function(err, fd) { - try { - let movie = VideoLib.MovieParser.parse(fd); - // Work with movie - console.log('Duration:', movie.relativeDuration()); - } catch (ex) { - console.error('Error:', ex); - } finally { - fs.closeSync(fd); - } -}); +let fd = fs.openSync('/path/to/file', 'r'); +try { + let movie = VideoLib.MovieParser.parse(fd); + // Work with movie + console.log('Duration:', movie.relativeDuration()); +} catch (ex) { + console.error('Error:', ex); +} finally { + fs.closeSync(fd); +} ``` ### Create MPEG-TS chunks @@ -44,22 +43,21 @@ fs.open('/path/to/file', 'r', function(err, fd) { const fs = require('fs'); const VideoLib = require('node-video-lib'); -fs.open('/path/to/file', 'r', function(err, fd) { - try { - let movie = VideoLib.MovieParser.parse(fd); - let fragmentList = VideoLib.FragmentListBuilder.build(movie, 5); - for (let i = 0; i < fragmentList.count(); i++) { - let fragment = fragmentList.get(i); - let sampleBuffers = VideoLib.FragmentReader.readSamples(fragment, fd); - let buffer = VideoLib.HLSPacketizer.packetize(fragment, sampleBuffers); - // Now buffer contains MPEG-TS chunk - } - } catch (ex) { - console.error('Error:', ex); - } finally { - fs.closeSync(fd); +let fd = fs.openSync('/path/to/file', 'r'); +try { + let movie = VideoLib.MovieParser.parse(fd); + let fragmentList = VideoLib.FragmentListBuilder.build(movie, 5); + for (let i = 0; i < fragmentList.count(); i++) { + let fragment = fragmentList.get(i); + let sampleBuffers = VideoLib.FragmentReader.readSamples(fragment, fd); + let buffer = VideoLib.HLSPacketizer.packetize(fragment, sampleBuffers); + // Now buffer contains MPEG-TS chunk } -}); +} catch (ex) { + console.error('Error:', ex); +} finally { + fs.closeSync(fd); +} ``` ### Build MP4 file @@ -68,24 +66,22 @@ fs.open('/path/to/file', 'r', function(err, fd) { const fs = require('fs'); const VideoLib = require('node-video-lib'); -fs.open('/path/to/file', 'r', function(err, fd) { +let fd = fs.openSync('/path/to/file', 'r'); +try { + let movie = VideoLib.MovieParser.parse(fd); + let fw = fs.openSync('/path/to/output.mp4', 'w'); try { - let movie = VideoLib.MovieParser.parse(fd); - fs.open('/path/to/output.mp4', 'w', function(err, fw) { - try { - VideoLib.MP4Builder.build(movie, fd, fw); - } catch (ex) { - console.error('Error:', ex); - } finally { - fs.closeSync(fw); - } - } + VideoLib.MP4Builder.build(movie, fd, fw); } catch (ex) { console.error('Error:', ex); } finally { - fs.closeSync(fd); + fs.closeSync(fw); } -}); +} catch (ex) { + console.error('Error:', ex); +} finally { + fs.closeSync(fd); +} ``` ### Create index file @@ -94,26 +90,24 @@ fs.open('/path/to/file', 'r', function(err, fd) { const fs = require('fs'); const VideoLib = require('node-video-lib'); -fs.open('/path/to/file', 'r', function(err, fd) { +let fd = fs.openSync('/path/to/file', 'r'); +try { + let movie = VideoLib.MovieParser.parse(fd); + let fragmentList = VideoLib.FragmentListBuilder.build(movie, 5); + console.log('Duration:', fragmentList.relativeDuration()); + let fdi = fs.openSync('/path/to/index.idx', 'w'); try { - let movie = VideoLib.MovieParser.parse(fd); - let fragmentList = VideoLib.FragmentListBuilder.build(movie, 5); - console.log('Duration:', fragmentList.relativeDuration()); - fs.open('/path/to/index.idx', 'w', function(err, fdi) { - try { - VideoLib.FragmentListIndexer.index(fragmentList, fdi); - } catch (ex) { - console.error('Error:', ex); - } finally { - fs.closeSync(fdi); - } - }); + VideoLib.FragmentListIndexer.index(fragmentList, fdi); } catch (ex) { console.error('Error:', ex); } finally { - fs.closeSync(fd); + fs.closeSync(fdi); } -}); +} catch (ex) { + console.error('Error:', ex); +} finally { + fs.closeSync(fd); +} ``` ### Create MPEG-TS chunks using index file @@ -122,25 +116,23 @@ fs.open('/path/to/file', 'r', function(err, fd) { const fs = require('fs'); const VideoLib = require('node-video-lib'); -fs.open('/path/to/file', 'r', function(err, fd) { - fs.open('/path/to/index.idx', 'r', function(err, fdi) { - try { - let fragmentList = VideoLib.FragmentListIndexer.read(fdi); - console.log('Duration:', fragmentList.relativeDuration()); - for (let i = 0; i < fragmentList.count(); i++) { - let fragment = fragmentList.get(i); - let sampleBuffers = VideoLib.FragmentReader.readSamples(fragment, fd); - let buffer = VideoLib.HLSPacketizer.packetize(fragment, sampleBuffers); - // Now buffer contains MPEG-TS chunk - } - } catch (ex) { - console.error('Error:', ex); - } finally { - fs.closeSync(fd); - fs.closeSync(fdi); - } - }); -}); +let fd = fs.openSync('/path/to/file', 'r'); +let fdi = fs.openSync('/path/to/index.idx', 'r'); +try { + let fragmentList = VideoLib.FragmentListIndexer.read(fdi); + console.log('Duration:', fragmentList.relativeDuration()); + for (let i = 0; i < fragmentList.count(); i++) { + let fragment = fragmentList.get(i); + let sampleBuffers = VideoLib.FragmentReader.readSamples(fragment, fd); + let buffer = VideoLib.HLSPacketizer.packetize(fragment, sampleBuffers); + // Now buffer contains MPEG-TS chunk + } +} catch (ex) { + console.error('Error:', ex); +} finally { + fs.closeSync(fd); + fs.closeSync(fdi); +} ``` ## Classes diff --git a/lib/mp4/atoms/atom-edts.js b/lib/mp4/atoms/atom-edts.js new file mode 100644 index 0000000..2d796f5 --- /dev/null +++ b/lib/mp4/atoms/atom-edts.js @@ -0,0 +1,22 @@ +'use strict'; + +const ContainerAtom = require('../container-atom'); +const Utils = require('../utils'); + +const ATOM_CLASSES = { + elst: require('./atom-elst'), +}; + +class AtomEDTS extends ContainerAtom { + + type() { + return Utils.ATOM_EDTS; + } + + availableAtomClasses() { + return ATOM_CLASSES; + } + +} + +module.exports = AtomEDTS; diff --git a/lib/mp4/atoms/atom-elst.js b/lib/mp4/atoms/atom-elst.js new file mode 100644 index 0000000..c209113 --- /dev/null +++ b/lib/mp4/atoms/atom-elst.js @@ -0,0 +1,18 @@ +'use strict'; + +const SampleTableAtom = require('../sample-table-atom'); +const Utils = require('../utils'); + +class AtomELST extends SampleTableAtom { + + type() { + return Utils.ATOM_ELST; + } + + countMultiplier() { + return 3; + } + +} + +module.exports = AtomELST; diff --git a/lib/mp4/atoms/atom-trak.js b/lib/mp4/atoms/atom-trak.js index 950b7c4..15898bb 100644 --- a/lib/mp4/atoms/atom-trak.js +++ b/lib/mp4/atoms/atom-trak.js @@ -6,6 +6,7 @@ const Utils = require('../utils'); const ATOM_CLASSES = { tkhd: require('./atom-tkhd'), mdia: require('./atom-mdia'), + edts: require('./atom-edts'), }; class AtomTRAK extends ContainerAtom { diff --git a/lib/mp4/builder-impl.js b/lib/mp4/builder-impl.js index 0c57ef4..4ecf51f 100644 --- a/lib/mp4/builder-impl.js +++ b/lib/mp4/builder-impl.js @@ -137,6 +137,16 @@ class BuilderImpl { minfAtom.createAtom(Utils.ATOM_SMHD); } + // Edits + let edtsAtom = trakAtom.createAtom(Utils.ATOM_EDTS); + let elstAtom = edtsAtom.createAtom(Utils.ATOM_ELST); + elstAtom.entries = [track.duration * this.movie.timescale / track.timescale, 0, 1 << 16]; + if (track.samples.length > 0 && track.samples[0].timestamp > 0) { + // add an empty edit + let duration = track.samples[0].timestamp * this.movie.timescale / track.timescale; + elstAtom.entries.unshift(duration, -1, 1 << 16); + } + // Samples table let stblAtom = minfAtom.createAtom(Utils.ATOM_STBL); diff --git a/lib/mp4/parser-impl.js b/lib/mp4/parser-impl.js index b60935e..6dfa3eb 100644 --- a/lib/mp4/parser-impl.js +++ b/lib/mp4/parser-impl.js @@ -86,6 +86,8 @@ class ParserImpl { if (mvhdAtom) { this.movie.timescale = mvhdAtom.timescale; this.movie.duration = mvhdAtom.duration; + } else { + throw new Error('MVHD atom not found'); } } @@ -169,6 +171,16 @@ class ParserImpl { samplesPerChunk = samplesToChunk[1]; } + // check edit list table + let edtsAtom = trakAtom.getAtom(Utils.ATOM_EDTS); + if (edtsAtom !== null) { + let editEntries = ParserImpl._getEntries(edtsAtom, Utils.ATOM_ELST); + if (editEntries.length >= 3 && editEntries[1] === -1) { + // apply the first empty edit + currentTimestamp = editEntries[0] * track.timescale / this.movie.timescale; + } + } + // Build samples let samples = new Array(sampleSizes.length); let pos = 0; diff --git a/lib/mp4/sample-table-atom.js b/lib/mp4/sample-table-atom.js index 026be2b..7413687 100644 --- a/lib/mp4/sample-table-atom.js +++ b/lib/mp4/sample-table-atom.js @@ -2,6 +2,8 @@ const Atom = require('./atom'); +const UINT_MINUS_ONE = -1 >>> 0; + class SampleTableAtom extends Atom { constructor() { @@ -19,6 +21,9 @@ class SampleTableAtom extends Atom { this.entries = new Array(entryCount * this.countMultiplier()); for (let i = 0, l = this.entries.length; i < l; i++) { this.entries[i] = buffer.readUInt32BE(8 + 4 * i); + if (this.entries[i] === UINT_MINUS_ONE) { + this.entries[i] = -1; + } } } @@ -30,7 +35,11 @@ class SampleTableAtom extends Atom { buffer.writeUInt32BE((this.entries.length / this.countMultiplier()) << 0, offset + 12); // entries for (let i = 0, l = this.entries.length; i < l; i++) { - buffer.writeUInt32BE(this.entries[i], offset + 16 + 4 * i); + let val = this.entries[i]; + if (val === -1) { + val = UINT_MINUS_ONE; + } + buffer.writeUInt32BE(val, offset + 16 + 4 * i); } } diff --git a/lib/mp4/utils.js b/lib/mp4/utils.js index a053e36..a593f27 100644 --- a/lib/mp4/utils.js +++ b/lib/mp4/utils.js @@ -29,6 +29,8 @@ module.exports = { ATOM_ESDS: 'esds', ATOM_MDAT: 'mdat', ATOM_FTYP: 'ftyp', + ATOM_EDTS: 'edts', + ATOM_ELST: 'elst', TRACK_TYPE_VIDEO: 'vide', TRACK_TYPE_AUDIO: 'soun',