UNPKG

mp4frag

Version:

A parser that reads piped data from ffmpeg containing a fragmented mp4 and splits it into an initialization segment and media segments. Designed for streaming live video relayed from cctv cameras.

225 lines (166 loc) 9.31 kB
# mp4frag ###### [![build](https://github.com/kevinGodell/mp4frag/actions/workflows/node.js.yml/badge.svg)](https://github.com/kevinGodell/mp4frag/actions/workflows/node.js.yml) [![Build status](https://ci.appveyor.com/api/projects/status/n9emuydmqgf845v0/branch/master?svg=true)](https://ci.appveyor.com/project/kevinGodell/mp4frag/branch/master) [![GitHub issues](https://img.shields.io/github/issues/kevinGodell/mp4frag.svg)](https://github.com/kevinGodell/mp4frag/issues) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/kevinGodell/mp4frag/master/LICENSE) [![npm](https://img.shields.io/npm/dt/mp4frag.svg?style=flat-square)](https://www.npmjs.com/package/mp4frag) A parser that reads piped data from ffmpeg containing a fragmented mp4 and splits it into an initialization segment and media segments. Designed for streaming live video relayed from cctv cameras. ***You must use the correct output args with ffmpeg to create a compatible fragmented mp4 format similar to the following real world examples:*** * `ffmpeg -loglevel quiet -rtsp_transport tcp -i rtsp://192.168.1.21:554/user=admin_password=pass_channel=0_stream=1.sdp?real_stream -reset_timestamps 1 -an -c:v copy -f mp4 -movflags +frag_every_frame+empty_moov+default_base_moof -min_frag_duration 500000 pipe:1` * `ffmpeg -loglevel quiet -rtsp_transport tcp -i rtsp://192.168.1.18:554/user=admin&password=pass&channel=1&stream=1.sdp -reset_timestamps 1 -an -c:v copy -f mp4 -movflags +frag_keyframe+empty_moov+default_base_moof pipe:1` ### [Documents](https://kevingodell.github.io/mp4frag/) generated by jsdocs. ### Interesting projects using mp4frag: * [Shinobi - Simple CCTV and NVR Solution](https://shinobi.video/) * [Live Video Experience (LiVE)](https://video-experience.live/) * [ffmpeg-streamer](https://github.com/kevinGodell/ffmpeg-streamer) * [node-red-contrib-mp4frag](https://github.com/kevinGodell/node-red-contrib-mp4frag) ### Known Limitations: * only supports fragmented mp4 video encoded with h.264, h.265, and aac # Changes v0.6.1 => v0.7.0 * dropping support for node.js < 14 * experimental buffer pool support added # Changes v0.6.0 => v0.6.1 * readableObjectMode can be set in [constructor](https://kevingodell.github.io/mp4frag/Mp4Frag.html#Mp4Frag) (defaults to false) * piping and data event outputs segment buffer by default # Changes v0.5.4 => v0.6.0 * dropping support for node.js < 10 * h.265 codec parsing * [keyframe](https://kevingodell.github.io/mp4frag/Mp4Frag.html#keyframe) now returns a boolean # Changes v0.5.2 => v0.5.4 * [getSegmentObject](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegmentObject) internal improvements # Changes v0.5.1 => v0.5.2 ### Property getters added * [totalDuration](https://kevingodell.github.io/mp4frag/Mp4Frag.html#totalDuration) * [totalByteLength](https://kevingodell.github.io/mp4frag/Mp4Frag.html#totalByteLength) # Changes v0.5.0 => v0.5.1 * better media segment duration handling # Changes v0.4.1 => v0.5.0 ### Method signature change * removed methods: getBuffer, getSegment, getSegmentList, getSegmentObjectList # Changes v0.4.0 => v0.4.1 ### Method signature change * methods that retrieve segment buffer can now accept a stopping `count` parameter * affects: [getBuffer](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getBuffer), [getSegment](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegment), [getSegmentList](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegmentList), [getSegmentObject](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegmentObject), [getSegmentObjectList](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegmentObjectList) # Changes v0.3.0 => v0.4.0 ### SegmentObject changed * keyframe property added to segmentObject `{ segment, sequence, duration, timestamp, keyframe }` * segment contains a keyframe if `keyframe >= 0` ### Convenience methods added * [getBuffer](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getBuffer) * [getSegment](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegment) * [getSegmentList](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegmentList) * [getSegmentObject](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegmentObject) * [getSegmentObjectList](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegmentObjectList) # Changes v0.2.0 => v0.3.0 ### Constructor options changed ***---> BREAKING <---*** * `hlsBase` => `hlsPlaylistBase` _string_, accepts `_`, `a-z`, and `A-Z` * `hlsSize` => `hlsPlaylistSize` _integer_, ranges from `2` to `20`, defaults to `4` * `hlsInit` => `hlsPlaylistInit` _boolean_, defaults to `true` * `bufferListSize` => `segmentCount` _integer_, ranges from `2` to `30`, defaults to `2` ### Segment event changed ***---> BREAKING <---*** ```js mp4frag.on('segment', data => { console.log(data); }); ``` * previously, data was a Buffer. * currently, data is a segmentObject structured as `{ segment, sequence, duration, timestamp }` # Options for a new instance of Mp4Frag #### segmentCount: integer (2 - 30), *setting this value will store specified number of media segments in the buffer* * will be ignored if setting `hlsPlaylistBase` ```javascript const mp4frag = new Mp4Frag({segmentCount: 3}); ``` #### hlsPlaylistBase: string (`_`, `a-z`, and `A-Z`), *setting this will generate a live fmp4 HLS m3u8 playlist* #### hlsPlaylistSize: integer (2 - 20), *setting this will determine the number of segments in the fmp4 HLS m3u8 playlist* ```javascript const mp4frag = new Mp4Frag({hlsPlaylistSize: 4, hlsPlaylistBase: 'my_String'}); ``` # Possible usage examples ## Example 1: *Generate a live fmp4 HLS m3u8 playlist with ffmpeg* ```javascript const { spawn } = require('child_process'); const Mp4Frag = require('mp4frag'); const mp4frag = new Mp4Frag({hlsPlaylistSize: 3, hlsPlaylistBase: 'back_yard'}); const ffmpeg = spawn( 'ffmpeg', ['-loglevel', 'quiet', '-probesize', '64', '-analyzeduration', '100000', '-reorder_queue_size', '5', '-rtsp_transport', 'tcp', '-i', 'rtsp://216.4.116.29:554/axis-media/media.3gp', '-an', '-c:v', 'copy', '-f', 'mp4', '-movflags', '+frag_keyframe+empty_moov+default_base_moof', '-metadata', 'title="ip 216.4.116.29"', '-reset_timestamps', '1', 'pipe:1'], { stdio: ['ignore', 'pipe', 'inherit'] } ); ffmpeg.stdio[1].pipe(mp4frag); ``` * **m3u8 playlist will now be available via `mp4frag.m3u8` and can be served to a client browser via express** * **segments in playlist can be accessed by sequence number via `mp4frag.getSegmentObject(6)`, with `6` being the current sequence number** #### Generated m3u8 playlist will look like the following example pulled from my live feed ``` #EXTM3U #EXT-X-VERSION:7 #EXT-X-ALLOW-CACHE:NO #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:6 #EXT-X-MAP:URI="init-back_yard.mp4" #EXTINF:4.780000, back_yard6.m4s #EXTINF:5.439000, back_yard7.m4s #EXTINF:4.269000, back_yard8.m4s ``` #### Setting up server routes to respond to http requests for playing live HLS feed ```javascript app.get('/back_yard.m3u8', (req, res) => { if (mp4frag.m3u8) { res.writeHead(200, {'Content-Type': 'application/vnd.apple.mpegurl'}); res.end(mp4frag.m3u8); } else { res.sendStatus(503); } }); app.get('/init-back_yard.mp4', (req, res) => { if (mp4frag.initialization) { res.writeHead(200, {'Content-Type': 'video/mp4'}); res.end(mp4frag.initialization); } else { res.sendStatus(503); } }); app.get('/back_yard:id.m4s', (req, res) => { const segmentObject = mp4frag.getSegmentObject(req.params.id); if (segmentObject) { res.writeHead(200, {'Content-Type': 'video/mp4'}); res.end(segmentObject.segment); } else { res.sendStatus(503); } }); ``` ## Example 2: *Create a buffer of past video to store for later recording* ```javascript const { spawn } = require('child_process'); const Mp4Frag = require('mp4frag'); // 3 past segments will be held in memory for later access via mp4frag.segmentObjects // if each segment has a duration of 2 seconds, then buffer will contain 6 seconds of video const mp4frag = new Mp4Frag({segmentCount: 3}); const ffmpeg = spawn( 'ffmpeg', ['-loglevel', 'quiet', '-probesize', '64', '-analyzeduration', '100000', '-reorder_queue_size', '5', '-rtsp_transport', 'tcp', '-i', 'rtsp://131.95.3.162:554/axis-media/media.3gp', '-an', '-c:v', 'copy', '-f', 'mp4', '-movflags', '+frag_keyframe+empty_moov+default_base_moof', '-metadata', 'title="ip 131.95.3.162"', '-reset_timestamps', '1', 'pipe:1'], { stdio: ['ignore', 'pipe', 'inherit'] } ); ffmpeg.stdio[1].pipe(mp4frag); ``` ##### Moments later, a triggering event occurs such as motion detection, and we need to record buffered video from before the event occurred: ```javascript const fs = require('fs'); const { initialization, segmentObjects } = mp4frag; if (initialization && segmentObjects) { const fileName = `${Date.now()}.mp4`; const writeStream = fs.createWriteStream(fileName); // write the initialization fragment writeStream.write(initialization); // write the media segments segmentObjects.forEach(({segment}) => { writeStream.write(segment); }); // end writeStream.end(); } ```