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
Markdown
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`
* [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
* 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
* dropping support for node.js < 10
* h.265 codec parsing
* [keyframe](https://kevingodell.github.io/mp4frag/Mp4Frag.html#keyframe) now returns a boolean
* [getSegmentObject](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegmentObject) internal improvements
* [totalDuration](https://kevingodell.github.io/mp4frag/Mp4Frag.html#totalDuration)
* [totalByteLength](https://kevingodell.github.io/mp4frag/Mp4Frag.html#totalByteLength)
* better media segment duration handling
* removed methods: getBuffer, getSegment, getSegmentList, getSegmentObjectList
* methods that retrieve segment buffer can now accept a stopping `count` parameter
* affects: [getBuffer](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getBuffer),
[](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegment), [getSegmentList](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegmentList),
[](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegmentObject),
[](https://kevingodell.github.io/mp4frag/Mp4Frag.html#getSegmentObjectList)
* keyframe property added to segmentObject `{ segment, sequence, duration, timestamp, keyframe }`
* segment contains a keyframe if `keyframe >= 0`
* [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)
* `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`
```js
mp4frag.on('segment', data => {
console.log(data);
});
```
* previously, data was a Buffer.
* currently, data is a segmentObject structured as `{ segment, sequence, duration, timestamp }`
* will be ignored if setting `hlsPlaylistBase`
```javascript
const mp4frag = new Mp4Frag({segmentCount: 3});
```
```javascript
const mp4frag = new Mp4Frag({hlsPlaylistSize: 4, hlsPlaylistBase: 'my_String'});
```
```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**
```
back_yard6.m4s
back_yard7.m4s
back_yard8.m4s
```
```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);
}
});
```
```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);
```
```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();
}
```