Socket
Socket
Sign inDemoInstall

video-quality-tools

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

video-quality-tools - npm Package Compare versions

Comparing version 1.0.0 to 1.1.0

CHANGELOG.md

2

package.json
{
"name": "video-quality-tools",
"version": "1.0.0",
"version": "1.1.0",
"description": "Set of tools to evaluate video stream quality.",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -120,3 +120,3 @@ # Video Quality Tools module - helps to measure live stream characteristics by RTMP/HLS/DASH streams

```json
```
{ videos:

@@ -218,3 +218,3 @@ [ { index: 1,

run `framesMonitor.listen()` method. After that `framesMonitor` starts emitting `frame` event as soon as ffprobe
decodes frame from the stream. It emits only video frames at now.
decodes frame from the stream. It emits video and audio frames.

@@ -255,3 +255,4 @@ ```javascript

This event is generated on each video frame decoded by ffprobe. The structure of the frame object is the following:
This event is generated on each video and audio frame decoded by ffprobe.
The structure of the frame object is the following:

@@ -263,6 +264,14 @@ ```

pkt_size: 3332,
width: 640,
height: 480,
pict_type: 'P' }
```
or
```
{ media_type: 'audio',
key_frame: 1,
pkt_pts_time: 'N/A',
pkt_size: 20 }
```
## `exit` event

@@ -364,3 +373,15 @@

max: 1525.95703125 },
fps: { mean: 30, min: 30, max: 30 } }
fps: {
mean: 30,
min: 30,
max: 30 },
gopDuration: {
mean: 2,
min: 1.9,
max: 2.1 },
displayAspectRatio: '16:9',
width: 1280,
height: 720,
hasAudioStream: true
}
```

@@ -380,8 +401,15 @@

For the full GOPs `processFrames` calculates min/max/mean values of bitrates (in kbit/s) and framerates and returns
them in `payload` field. The result of the check for the similarity of GOP structures for the collected GOPs is returned in
`areAllGopsIdentical` field.
For the full GOPs `processFrames` calculates min/max/mean values of bitrates (in kbit/s), framerates and GOP duration
(in seconds) and returns them in `payload` field. The result of the check for the similarity of GOP structures for
the collected GOPs is returned in `areAllGopsIdentical` field. Fields `width`, `height` and `displayAspectRatio`
are taken from data from first frame of the first collected GOP. Value of `hasAudioStream` reflects presence of
audio frames.
For display aspect ratio calculation method `processFrames::calculateDisplayAspectRatio` use list of
[current video aspect ratio standards](https://en.wikipedia.org/wiki/Aspect_ratio_(image))
with approximation error of frames width and height ratio. If ratio hasn't a reflection in aspect ratio standards then
[GCD algorithm](https://en.wikipedia.org/wiki/Greatest_common_divisor) is used.
`processFrames` may throw `Errors.GopNotFoundError`.
Also, you may extend the metrics. Check `src/processFrames.js` to find common functions.

@@ -272,7 +272,5 @@ 'use strict';

errorLevel,
'-select_streams',
'v:0',
'-show_frames',
'-show_entries',
'frame=pkt_size,pkt_pts_time,media_type,pict_type,key_frame',
'frame=pkt_size,pkt_pts_time,media_type,pict_type,key_frame,width,height',
'-i',

@@ -279,0 +277,0 @@ `${this._url} timeout=${timeoutInSec}`

@@ -7,2 +7,19 @@ 'use strict';

const AR_CALCULATION_PRECISION = 0.01;
const SQUARE_AR_COEFFICIENT = 1;
const SQUARE_AR = '1:1';
const TRADITIONAL_TV_AR_COEFFICIENT = 1.333;
const TRADITIONAL_TV_AR = '4:3';
const HD_VIDEO_AR_COEFFICIENT = 1.777;
const HD_VIDEO_AR = '16:9';
const UNIVISIUM_AR_COEFFICIENT = 2;
const UNIVISIUM_AR = '18:9';
const WIDESCREEN_AR_COEFFICIENT = 2.33;
const WIDESCREEN_AR = '21:9';
function processFrames(frames) {

@@ -20,6 +37,45 @@ if (!Array.isArray(frames)) {

const areAllGopsIdentical = processFrames.areAllGopsIdentical(gops);
const bitrate = processFrames.calculateBitrate(gops);
const fps = processFrames.calculateFps(gops);
let areAllGopsIdentical = true;
const hasAudioStream = processFrames.hasAudioFrames(frames);
const baseGopSize = gops[0].frames.length;
const bitrates = [];
const fpsList = [];
const gopDurations = [];
gops.forEach(gop => {
areAllGopsIdentical = areAllGopsIdentical && baseGopSize === gop.frames.length;
const accumulatedPktSize = processFrames.accumulatePktSize(gop);
const gopDuration = processFrames.gopDurationInSec(gop);
const gopBitrate = processFrames.toKbs(accumulatedPktSize / gopDuration);
bitrates.push(gopBitrate);
const gopFps = gop.frames.length / gopDuration;
fpsList.push(gopFps);
gopDurations.push(gopDuration);
});
const bitrate = {
mean: _.mean(bitrates),
min : Math.min(...bitrates),
max : Math.max(...bitrates)
};
const fps = {
mean: _.mean(fpsList),
min : Math.min(...fpsList),
max : Math.max(...fpsList)
};
const gopDuration = {
mean: _.mean(gopDurations),
min: Math.min(...gopDurations),
max: Math.max(...gopDurations)
};
const width = gops[0].frames[0].width;
const height = gops[0].frames[0].height;
const displayAspectRatio = calculateDisplayAspectRatio(width, height);
return {

@@ -29,3 +85,8 @@ payload : {

bitrate,
fps
fps,
gopDuration,
displayAspectRatio,
width,
height,
hasAudioStream
},

@@ -36,10 +97,14 @@ remainedFrames: remainedFrames

processFrames.identifyGops = identifyGops;
processFrames.calculateBitrate = calculateBitrate;
processFrames.calculateFps = calculateFps;
processFrames.filterVideoFrames = filterVideoFrames;
processFrames.gopDurationInSec = gopDurationInSec;
processFrames.toKbs = toKbs;
processFrames.accumulatePktSize = accumulatePktSize;
processFrames.areAllGopsIdentical = areAllGopsIdentical;
processFrames.identifyGops = identifyGops;
processFrames.calculateBitrate = calculateBitrate;
processFrames.calculateFps = calculateFps;
processFrames.calculateGopDuration = calculateGopDuration;
processFrames.filterVideoFrames = filterVideoFrames;
processFrames.hasAudioFrames = hasAudioFrames;
processFrames.gopDurationInSec = gopDurationInSec;
processFrames.toKbs = toKbs;
processFrames.accumulatePktSize = accumulatePktSize;
processFrames.areAllGopsIdentical = areAllGopsIdentical;
processFrames.findGcd = findGcd;
processFrames.calculateDisplayAspectRatio = calculateDisplayAspectRatio;

@@ -192,2 +257,54 @@ module.exports = processFrames;

function calculateGopDuration(gops) {
const gopsDurations = [];
gops.forEach(gop => {
const gopDurationInSec = processFrames.gopDurationInSec(gop);
gopsDurations.push(gopDurationInSec);
});
return {
mean: _.mean(gopsDurations),
min: Math.min(...gopsDurations),
max: Math.max(...gopsDurations)
};
}
function calculateDisplayAspectRatio(width, height) {
if (!_.isInteger(width) || width <= 0) {
throw new TypeError('"width" must be a positive integer');
}
if (!_.isInteger(height) || height <= 0) {
throw new TypeError('"height" must be a positive integer');
}
const arCoefficient = width / height;
if (Math.abs(arCoefficient - SQUARE_AR_COEFFICIENT) <= AR_CALCULATION_PRECISION) {
return SQUARE_AR;
}
if (Math.abs(arCoefficient - TRADITIONAL_TV_AR_COEFFICIENT) <= AR_CALCULATION_PRECISION) {
return TRADITIONAL_TV_AR;
}
if (Math.abs(arCoefficient - HD_VIDEO_AR_COEFFICIENT) <= AR_CALCULATION_PRECISION) {
return HD_VIDEO_AR;
}
if (Math.abs(arCoefficient - UNIVISIUM_AR_COEFFICIENT) <= AR_CALCULATION_PRECISION) {
return UNIVISIUM_AR;
}
if (Math.abs(arCoefficient - WIDESCREEN_AR_COEFFICIENT) <= AR_CALCULATION_PRECISION) {
return WIDESCREEN_AR;
}
const gcd = findGcd(width, height);
return `${width / gcd}:${height / gcd}`;
}
function areAllGopsIdentical(gops) {

@@ -201,4 +318,20 @@ return gops.every(gop => _.isEqual(gops[0].frames.length, gop.frames.length));

function hasAudioFrames(frames) {
return frames.some(frame => frame.media_type === 'audio');
}
function toKbs(val) {
return val * 8 / 1024;
}
function findGcd(a, b) {
if (a === 0 && b === 0) {
return 0;
}
if (b === 0) {
return a;
}
return findGcd(b, a % b);
}

@@ -9,2 +9,3 @@ 'use strict';

const Errors = require('./Errors/');
const processFrames = require('./processFrames');

@@ -133,3 +134,10 @@ const DAR_OR_SAR_NA = 'N/A';

video.sample_aspect_ratio = '1:1';
video.display_aspect_ratio = this._calculateDisplayAspectRatio(video.width, video.height);
try {
video.display_aspect_ratio = processFrames.calculateDisplayAspectRatio(video.width, video.height);
} catch (err) {
throw new Errors.StreamsInfoError(
'Can not calculate aspect ratio due to invalid video resolution',
{width: video.width, height: video.height, url: this._url}
);
}
}

@@ -140,28 +148,4 @@

}
_calculateDisplayAspectRatio(width, height) {
if (!_.isInteger(width) || !_.isInteger(height) || width <= 0 || height <= 0) {
throw new Errors.StreamsInfoError(
'Can not calculate aspect rate due to invalid video resolution',
{width, height, url: this._url}
);
}
const gcd = this._findGcd(width, height);
return `${width / gcd}:${height / gcd}`;
}
_findGcd(a, b) {
if (a === 0 && b === 0) {
return 0;
}
if (b === 0) {
return a;
}
return this._findGcd(b, a % b);
}
}
module.exports = StreamsInfo;

@@ -108,3 +108,3 @@ 'use strict';

const expectedPFramesCount = 240;
const expectedAudioFramesCount = 0;
const expectedAudioFramesCount = 431;

@@ -124,11 +124,15 @@ const onFrame = {I: spyOnIFrame, P: spyOnPFrame};

framesMonitor.on('exit', reason => {
assert.instanceOf(reason, ExitReasons.NormalExit);
assert.strictEqual(reason.payload.code, expectedReturnCode);
try {
assert.instanceOf(reason, ExitReasons.NormalExit);
assert.strictEqual(reason.payload.code, expectedReturnCode);
assert.strictEqual(spyOnAudioFrame.callCount, expectedAudioFramesCount);
assert.strictEqual(spyOnAudioFrame.callCount, expectedAudioFramesCount);
assert.strictEqual(spyOnIFrame.callCount, expectedIFramesCount);
assert.strictEqual(spyOnPFrame.callCount, expectedPFramesCount);
assert.strictEqual(spyOnIFrame.callCount, expectedIFramesCount);
assert.strictEqual(spyOnPFrame.callCount, expectedPFramesCount);
done();
done();
} catch (err) {
done(err);
}
});

@@ -135,0 +139,0 @@ });

@@ -45,2 +45,6 @@ 'use strict';

const expectedReturnCode = 0;
const expectedWidth = 854;
const expectedHeight = 480;
const expectedAspectRatio = '16:9';
const expectAudio = true;

@@ -74,2 +78,6 @@ const frames = [];

const expectedMinGop = 0.16599999999999993;
const expectedMaxGop = 0.16700000000000004;
const expectedMeanGop = 0.16666101694915256;
const expectedRemainedFrames = [

@@ -87,4 +95,9 @@ {key_frame: 1, pict_type: 'I'},

areAllGopsIdentical: true,
fps : {mean: expectedMeanFps, min: expectedMinFps, max: expectedMaxFps},
bitrate : {mean: expectedMeanBitrate, min: expectedMinBitrate, max: expectedMaxBitrate}
fps: {mean: expectedMeanFps, min: expectedMinFps, max: expectedMaxFps},
bitrate: {mean: expectedMeanBitrate, min: expectedMinBitrate, max: expectedMaxBitrate},
gopDuration: {mean: expectedMeanGop, min: expectedMinGop, max: expectedMaxGop},
aspectRatio: expectedAspectRatio,
height: expectedHeight,
width: expectedWidth,
hasAudioStream: expectAudio
});

@@ -104,2 +117,6 @@

const expectedReturnCode = 0;
const expectedWidth = 854;
const expectedHeight = 480;
const expectedAspectRatio = '16:9';
const expectAudio = true;

@@ -133,2 +150,6 @@ const frames = [];

const expectedMinGop = 1.534;
const expectedMaxGop = 5.000000000000001;
const expectedMeanGop = 3.2113333333333336;
const expectedRemainedFrames = [

@@ -152,4 +173,9 @@ {key_frame: 1, pict_type: 'I'},

areAllGopsIdentical: false,
fps : {mean: expectedMeanFps, min: expectedMinFps, max: expectedMaxFps},
bitrate : {mean: expectedMeanBitrate, min: expectedMinBitrate, max: expectedMaxBitrate}
fps: {mean: expectedMeanFps, min: expectedMinFps, max: expectedMaxFps},
bitrate: {mean: expectedMeanBitrate, min: expectedMinBitrate, max: expectedMaxBitrate},
gopDuration: {mean: expectedMeanGop, min: expectedMinGop, max: expectedMaxGop},
aspectRatio: expectedAspectRatio,
width: expectedWidth,
height: expectedHeight,
hasAudioStream: expectAudio
});

@@ -156,0 +182,0 @@

@@ -14,7 +14,5 @@ 'use strict';

errorLevel,
'-select_streams',
'v:0',
'-show_frames',
'-show_entries',
'frame=pkt_size,pkt_pts_time,media_type,pict_type,key_frame',
'frame=pkt_size,pkt_pts_time,media_type,pict_type,key_frame,width,height',
'-i',

@@ -21,0 +19,0 @@ `${url} timeout=${timeoutInSec}`

@@ -62,25 +62,23 @@ 'use strict';

const frames1 = [
{pkt_size: 1, pkt_pts_time: 1, media_type: 'audio', key_frame: 1},
{pkt_size: 1, pkt_pts_time: 11, media_type: 'video', key_frame: 1},
{pkt_size: 3, pkt_pts_time: 13, media_type: 'video', key_frame: 0},
{pkt_size: 3, pkt_pts_time: 3, media_type: 'audio', key_frame: 1},
{pkt_size: 5, pkt_pts_time: 15, media_type: 'video', key_frame: 1},
{pkt_size: 7, pkt_pts_time: 17, media_type: 'video', key_frame: 0},
{pkt_size: 9, pkt_pts_time: 19, media_type: 'video', key_frame: 1}
{width: 640, height: 480, pkt_size: 1, pkt_pts_time: 11, media_type: 'video', key_frame: 1},
{width: 640, height: 480, pkt_size: 3, pkt_pts_time: 13, media_type: 'video', key_frame: 0},
{width: 640, height: 480, pkt_size: 5, pkt_pts_time: 15, media_type: 'video', key_frame: 1},
{width: 640, height: 480, pkt_size: 7, pkt_pts_time: 17, media_type: 'video', key_frame: 0},
{width: 640, height: 480, pkt_size: 9, pkt_pts_time: 19, media_type: 'video', key_frame: 1}
];
const frames2 = [
{pkt_size: 1, pkt_pts_time: 1, media_type: 'audio', key_frame: 1},
{pkt_size: 1, pkt_pts_time: 11, media_type: 'video', key_frame: 0},
{pkt_size: 2, pkt_pts_time: 13, media_type: 'video', key_frame: 0},
{pkt_size: 3, pkt_pts_time: 15, media_type: 'video', key_frame: 1},
{pkt_size: 3, pkt_pts_time: 15, media_type: 'audio', key_frame: 1},
{pkt_size: 4, pkt_pts_time: 17, media_type: 'video', key_frame: 0},
{pkt_size: 5, pkt_pts_time: 19, media_type: 'video', key_frame: 0},
{pkt_size: 6, pkt_pts_time: 21, media_type: 'video', key_frame: 1},
{pkt_size: 7, pkt_pts_time: 23, media_type: 'video', key_frame: 0},
{pkt_size: 3, pkt_pts_time: 15, media_type: 'audio', key_frame: 1},
{pkt_size: 8, pkt_pts_time: 25, media_type: 'video', key_frame: 0},
{pkt_size: 9, pkt_pts_time: 27, media_type: 'video', key_frame: 1},
{pkt_size: 10, pkt_pts_time: 29, media_type: 'video', key_frame: 0}
{width: 854, height: 480, pkt_size: 1, pkt_pts_time: 1, media_type: 'audio', key_frame: 1},
{width: 854, height: 480, pkt_size: 1, pkt_pts_time: 11, media_type: 'video', key_frame: 0},
{width: 854, height: 480, pkt_size: 2, pkt_pts_time: 13, media_type: 'video', key_frame: 0},
{width: 854, height: 480, pkt_size: 3, pkt_pts_time: 15, media_type: 'video', key_frame: 1},
{width: 854, height: 480, pkt_size: 3, pkt_pts_time: 15, media_type: 'audio', key_frame: 1},
{width: 854, height: 480, pkt_size: 4, pkt_pts_time: 17, media_type: 'video', key_frame: 0},
{width: 854, height: 480, pkt_size: 5, pkt_pts_time: 19, media_type: 'video', key_frame: 0},
{width: 854, height: 480, pkt_size: 6, pkt_pts_time: 21, media_type: 'video', key_frame: 1},
{width: 854, height: 480, pkt_size: 7, pkt_pts_time: 23, media_type: 'video', key_frame: 0},
{width: 854, height: 480, pkt_size: 3, pkt_pts_time: 15, media_type: 'audio', key_frame: 1},
{width: 854, height: 480, pkt_size: 8, pkt_pts_time: 25, media_type: 'video', key_frame: 0},
{width: 854, height: 480, pkt_size: 9, pkt_pts_time: 27, media_type: 'video', key_frame: 1},
{width: 854, height: 480, pkt_size: 10, pkt_pts_time: 29, media_type: 'video', key_frame: 0}
];

@@ -95,3 +93,3 @@

const expectedRemainedFrames1 = [
{pkt_size: 9, pkt_pts_time: 19, media_type: 'video', key_frame: 1}
{pkt_size: 9, pkt_pts_time: 19, media_type: 'video', key_frame: 1, width: 640, height: 480}
];

@@ -106,4 +104,4 @@

const expectedRemainedFrames2 = [
{pkt_size: 9, pkt_pts_time: 27, media_type: 'video', key_frame: 1},
{pkt_size: 10, pkt_pts_time: 29, media_type: 'video', key_frame: 0}
{pkt_size: 9, pkt_pts_time: 27, media_type: 'video', key_frame: 1, width: 854, height: 480},
{pkt_size: 10, pkt_pts_time: 29, media_type: 'video', key_frame: 0, width: 854, height: 480}
];

@@ -114,2 +112,23 @@

const expectedGopDuration1 = {
min: 15 - 11,
max: 19 - 15,
mean: (15 - 11 + 19 - 15) / 2
};
const expectedGopDuration2 = {
min: 21 - 15,
max: 27 - 21,
mean: (21 - 15 + 27 - 21) / 2
};
const expectedAspectRatio1 = '4:3';
const expectedAspectRatio2 = '16:9';
const expectedWidth1 = 640;
const expectedHeight1 = 480;
const expectedWidth2 = 854;
const expectedHeight2 = 480;
const expectAudio1 = false;
const expectAudio2 = true;
let res1 = processFrames(frames1);

@@ -120,3 +139,8 @@

bitrate : expectedBitrate1,
fps : expectedFps1
fps : expectedFps1,
gopDuration : expectedGopDuration1,
displayAspectRatio : expectedAspectRatio1,
width : expectedWidth1,
height : expectedHeight1,
hasAudioStream : expectAudio1
});

@@ -131,3 +155,8 @@

bitrate : expectedBitrate2,
fps : expectedFps2
fps : expectedFps2,
gopDuration : expectedGopDuration2,
displayAspectRatio : expectedAspectRatio2,
width : expectedWidth2,
height : expectedHeight2,
hasAudioStream : expectAudio2
});

@@ -138,2 +167,52 @@

it('must detect that GOPs is not identical ', () => {
const frames = [
{width: 640, height: 480, pkt_size: 1, pkt_pts_time: 11, media_type: 'video', key_frame: 1},
{width: 640, height: 480, pkt_size: 3, pkt_pts_time: 13, media_type: 'video', key_frame: 0},
{width: 640, height: 480, pkt_size: 5, pkt_pts_time: 15, media_type: 'video', key_frame: 1},
{width: 640, height: 480, pkt_size: 6, pkt_pts_time: 17, media_type: 'video', key_frame: 0},
{width: 640, height: 480, pkt_size: 9, pkt_pts_time: 19, media_type: 'video', key_frame: 0},
{width: 640, height: 480, pkt_size: 11, pkt_pts_time: 21, media_type: 'video', key_frame: 1}
];
const expectedBitrate = {
min: processFrames.toKbs((1 + 3) / (15 - 11)),
max: processFrames.toKbs((5 + 6 + 9) / (21 - 15)),
mean: processFrames.toKbs(((1 + 3) / (15 - 11) + (5 + 6 + 9) / (21 - 15)) / 2)
};
const expectedRemainedFrames = [
{pkt_size: 11, pkt_pts_time: 21, media_type: 'video', key_frame: 1, width: 640, height: 480}
];
const expectedFps = {min: 0.5, max: 0.5, mean: 0.5};
const expectedGopDuration = {
min: 15 - 11,
max: 21 - 15,
mean: (15 - 11 + 21 - 15) / 2
};
const expectedAspectRatio = '4:3';
const expectedWidth = 640;
const expectedHeight = 480;
const expectAudio = false;
const expectAreAllGopsIdentical = false;
let res = processFrames(frames);
assert.deepEqual(res.payload, {
areAllGopsIdentical: expectAreAllGopsIdentical,
bitrate: expectedBitrate,
fps: expectedFps,
gopDuration: expectedGopDuration,
displayAspectRatio: expectedAspectRatio,
width: expectedWidth,
height: expectedHeight,
hasAudioStream: expectAudio
});
assert.deepEqual(res.remainedFrames, expectedRemainedFrames);
});
});

@@ -38,3 +38,3 @@ const invalidParams = [

data : [{sample_aspect_ratio: '200:100', display_aspect_ratio: '0:1', width: 20, height: 10}],
res : [{sample_aspect_ratio: '1:1', display_aspect_ratio: '2:1', width: 20, height: 10}]
res : [{sample_aspect_ratio: '1:1', display_aspect_ratio: '18:9', width: 20, height: 10}]
},

@@ -49,3 +49,3 @@ {

data : [{sample_aspect_ratio: '200:100', display_aspect_ratio: 'N/A', width: 20, height: 10}],
res : [{sample_aspect_ratio: '1:1', display_aspect_ratio: '2:1', width: 20, height: 10}]
res : [{sample_aspect_ratio: '1:1', display_aspect_ratio: '18:9', width: 20, height: 10}]
}

@@ -52,0 +52,0 @@ ];

@@ -21,3 +21,3 @@ 'use strict';

dataDriven(invalidParams, function () {
const expectedErrorMessage = 'Can not calculate aspect rate due to invalid video resolution';
const expectedErrorMessage = 'Can not calculate aspect ratio due to invalid video resolution';
const expectedErrorClass = StreamsInfoError;

@@ -24,0 +24,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc