From 25106d06de8d549a427a3184fa73cda7a9ec88ef Mon Sep 17 00:00:00 2001 From: Oleh Poberezhets Date: Fri, 7 Sep 2018 18:43:35 +0300 Subject: [PATCH 1/8] New functionality implementation --- package.json | 2 +- src/FramesMonitor.js | 4 +- src/processFrames.js | 135 ++++++++++++++++-- tests/Functional/FramesMonitor/listen.test.js | 18 ++- tests/Functional/processFrames.test.js | 34 ++++- .../_runShowFramesProcess.test.js | 4 +- .../Unit/processFrames/processFrames.test.js | 79 ++++++---- 7 files changed, 221 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index aa2d59b..116712f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "video-quality-tools", - "version": "1.0.0", + "version": "1.1.0", "description": "Set of tools to evaluate video stream quality.", "main": "index.js", "engines": { diff --git a/src/FramesMonitor.js b/src/FramesMonitor.js index 39571b0..db11bdb 100644 --- a/src/FramesMonitor.js +++ b/src/FramesMonitor.js @@ -269,11 +269,9 @@ class FramesMonitor extends EventEmitter { '-hide_banner', '-v', 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', `${this._url} timeout=${timeoutInSec}` ] diff --git a/src/processFrames.js b/src/processFrames.js index 7b82b78..8c9a3fb 100644 --- a/src/processFrames.js +++ b/src/processFrames.js @@ -4,11 +4,29 @@ const _ = require('lodash'); const Errors = require('./Errors'); +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.37; +const WIDESCREEN_AR = '21:9'; + function processFrames(frames) { if (!Array.isArray(frames)) { throw new TypeError('process method is supposed to accept an array of frames.'); } + const audioFrames = processFrames.filterAudioFrames(frames); const videoFrames = processFrames.filterVideoFrames(frames); const {gops, remainedFrames} = processFrames.identifyGops(videoFrames); @@ -16,28 +34,75 @@ function processFrames(frames) { throw new Errors.GopNotFoundError('Can not find any gop for these frames', {frames}); } - const areAllGopsIdentical = processFrames.areAllGopsIdentical(gops); - const bitrate = processFrames.calculateBitrate(gops); - const fps = processFrames.calculateFps(gops); + let areAllGopsIdentical = true; + const hasAudioStream = audioFrames.length > 0; + const baseGopSize = gops[0].frames.length; + const bitrates = []; + const fpsList = []; + const gopsDurations = []; + + gops.forEach(gop => { + areAllGopsIdentical = areAllGopsIdentical ? baseGopSize === gop.frames.length : false; + 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); + + gopsDurations.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(gopsDurations), + min: Math.min(...gopsDurations), + max: Math.max(...gopsDurations) + }; + + const width = gops[0].frames[0].width; + const height = gops[0].frames[0].height; + const aspectRatio = calculateAspectRatio(width, height); return { payload : { areAllGopsIdentical, bitrate, - fps + fps, + gopDuration, + aspectRatio, + width, + height, + hasAudioStream }, 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.calculateAspectRatio = calculateAspectRatio; +processFrames.filterVideoFrames = filterVideoFrames; +processFrames.filterAudioFrames = filterAudioFrames; +processFrames.gopDurationInSec = gopDurationInSec; +processFrames.toKbs = toKbs; +processFrames.accumulatePktSize = accumulatePktSize; +processFrames.areAllGopsIdentical = areAllGopsIdentical; module.exports = processFrames; @@ -185,6 +250,48 @@ function calculateFps(gops) { }; } +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 calculateAspectRatio(width, height) { + 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; + } + + return `${width}:${height}`; +} + function areAllGopsIdentical(gops) { return gops.every(gop => _.isEqual(gops[0].frames.length, gop.frames.length)); } @@ -193,6 +300,10 @@ function filterVideoFrames(frames) { return frames.filter(frame => frame.media_type === 'video'); } +function filterAudioFrames(frames) { + return frames.filter(frame => frame.media_type === 'audio'); +} + function toKbs(val) { return val * 8 / 1024; } diff --git a/tests/Functional/FramesMonitor/listen.test.js b/tests/Functional/FramesMonitor/listen.test.js index 6887e16..4f20119 100644 --- a/tests/Functional/FramesMonitor/listen.test.js +++ b/tests/Functional/FramesMonitor/listen.test.js @@ -105,7 +105,7 @@ describe('FramesMonitor::listen, fetch frames from active stream', () => { const expectedReturnCode = 0; const expectedIFramesCount = 60; const expectedPFramesCount = 240; - const expectedAudioFramesCount = 0; + const expectedAudioFramesCount = 431; const onFrame = {I: spyOnIFrame, P: spyOnPFrame}; @@ -120,15 +120,19 @@ describe('FramesMonitor::listen, fetch frames from active stream', () => { }); 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); + } }); }); }); diff --git a/tests/Functional/processFrames.test.js b/tests/Functional/processFrames.test.js index 40c4b22..ba5e712 100644 --- a/tests/Functional/processFrames.test.js +++ b/tests/Functional/processFrames.test.js @@ -42,6 +42,10 @@ describe('processFrames functional tests', () => { it('must return correct frames info for the stream with fixed gop', async () => { const expectedReturnCode = 0; + const expectedWidth = 854; + const expectedHeight = 480; + const expectedAspectRatio = '16:9'; + const expectAudio = true; const frames = []; @@ -70,6 +74,10 @@ describe('processFrames functional tests', () => { const expectedMaxBitrate = 3691.1709337349394; const expectedMeanBitrate = 1018.2511085012945; + const expectedMinGop = 0.16599999999999993; + const expectedMaxGop = 0.16700000000000004; + const expectedMeanGop = 0.16666101694915256; + const expectedRemainedFrames = [ {key_frame: 1, pict_type: 'I'}, {key_frame: 0, pict_type: 'P'}, @@ -82,8 +90,13 @@ describe('processFrames functional tests', () => { assert.deepEqual(payload, { 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 }); assert.deepEqual( @@ -98,6 +111,10 @@ describe('processFrames functional tests', () => { it('must return correct frames info for the stream with open gop', async () => { const expectedReturnCode = 0; + const expectedWidth = 854; + const expectedHeight = 480; + const expectedAspectRatio = '16:9'; + const expectAudio = true; const frames = []; @@ -126,6 +143,10 @@ describe('processFrames functional tests', () => { const expectedMaxBitrate = 1435.5312499999998; const expectedMeanBitrate = 1066.2038272718032; + const expectedMinGop = 1.534; + const expectedMaxGop = 5.000000000000001; + const expectedMeanGop = 3.2113333333333336; + const expectedRemainedFrames = [ {key_frame: 1, pict_type: 'I'}, {key_frame: 0, pict_type: 'P'}, @@ -144,8 +165,13 @@ describe('processFrames functional tests', () => { assert.deepEqual(payload, { 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 }); assert.deepEqual( diff --git a/tests/Unit/FramesMonitor/_runShowFramesProcess.test.js b/tests/Unit/FramesMonitor/_runShowFramesProcess.test.js index f2c6cee..2eeb999 100644 --- a/tests/Unit/FramesMonitor/_runShowFramesProcess.test.js +++ b/tests/Unit/FramesMonitor/_runShowFramesProcess.test.js @@ -11,11 +11,9 @@ function getSpawnArguments(url, timeoutInSec, errorLevel) { '-hide_banner', '-v', 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', `${url} timeout=${timeoutInSec}` ]; diff --git a/tests/Unit/processFrames/processFrames.test.js b/tests/Unit/processFrames/processFrames.test.js index 4b3b190..7d86463 100644 --- a/tests/Unit/processFrames/processFrames.test.js +++ b/tests/Unit/processFrames/processFrames.test.js @@ -59,29 +59,27 @@ describe('processFrames', () => { it('must return correct info just fine', () => { 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} ]; const expectedBitrate1 = { @@ -91,7 +89,7 @@ describe('processFrames', () => { }; 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} ]; const expectedBitrate2 = { @@ -101,19 +99,45 @@ describe('processFrames', () => { }; 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} ]; const expectedFps1 = {min: 0.5, max: 0.5, mean: 0.5}; const expectedFps2 = {min: 0.5, max: 0.5, mean: 0.5}; + 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); assert.deepEqual(res1.payload, { areAllGopsIdentical: true, bitrate : expectedBitrate1, - fps : expectedFps1 + fps : expectedFps1, + gopDuration : expectedGopDuration1, + aspectRatio : expectedAspectRatio1, + width : expectedWidth1, + height : expectedHeight1, + hasAudioStream : expectAudio1 }); assert.deepEqual(res1.remainedFrames, expectedRemainedFrames1); @@ -123,7 +147,12 @@ describe('processFrames', () => { assert.deepEqual(res2.payload, { areAllGopsIdentical: true, bitrate : expectedBitrate2, - fps : expectedFps2 + fps : expectedFps2, + gopDuration : expectedGopDuration2, + aspectRatio : expectedAspectRatio2, + width : expectedWidth2, + height : expectedHeight2, + hasAudioStream : expectAudio2 }); assert.deepEqual(res2.remainedFrames, expectedRemainedFrames2); From 9856aa4bf44d26fda1b84ea672046c916ab255f8 Mon Sep 17 00:00:00 2001 From: Oleh Poberezhets Date: Mon, 10 Sep 2018 09:00:32 +0300 Subject: [PATCH 2/8] fix WIDESCREEN_AR_COEFFICIENT --- src/processFrames.js | 2 +- .../calculateAspectRatio.test.js | 85 +++++++++++++++++++ .../calculateGopDuration.test.js | 32 +++++++ .../processFrames/filterAudioFrames.test.js | 34 ++++++++ 4 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/processFrames/calculateAspectRatio.test.js create mode 100644 tests/Unit/processFrames/calculateGopDuration.test.js create mode 100644 tests/Unit/processFrames/filterAudioFrames.test.js diff --git a/src/processFrames.js b/src/processFrames.js index 8c9a3fb..51c4b2b 100644 --- a/src/processFrames.js +++ b/src/processFrames.js @@ -18,7 +18,7 @@ const HD_VIDEO_AR = '16:9'; const UNIVISIUM_AR_COEFFICIENT = 2; const UNIVISIUM_AR = '18:9'; -const WIDESCREEN_AR_COEFFICIENT = 2.37; +const WIDESCREEN_AR_COEFFICIENT = 2.33; const WIDESCREEN_AR = '21:9'; function processFrames(frames) { diff --git a/tests/Unit/processFrames/calculateAspectRatio.test.js b/tests/Unit/processFrames/calculateAspectRatio.test.js new file mode 100644 index 0000000..0d126d3 --- /dev/null +++ b/tests/Unit/processFrames/calculateAspectRatio.test.js @@ -0,0 +1,85 @@ +'use strict'; + +const {assert} = require('chai'); + +const processFrames = require('src/processFrames'); + +describe('processFrames.calculateAspectRatio', () => { + + it('must correct calculate aspectRatio for frame 640x640', () => { + const expectedAspectRatio = '1:1'; + + const width = 640; + const height = 640; + + const aspectRatio = processFrames.calculateAspectRatio(width, height); + + assert.strictEqual(aspectRatio, expectedAspectRatio); + }); + + it('must correct calculate aspectRatio for frame 640x480', () => { + const expectedAspectRatio = '4:3'; + + const width = 640; + const height = 480; + + const aspectRatio = processFrames.calculateAspectRatio(width, height); + + assert.strictEqual(aspectRatio, expectedAspectRatio); + }); + + it('must correct calculate aspectRatio for frame 854x480', () => { + const expectedAspectRatio = '16:9'; + + const width = 854; + const height = 480; + + const aspectRatio = processFrames.calculateAspectRatio(width, height); + + assert.strictEqual(aspectRatio, expectedAspectRatio); + }); + + it('must correct calculate aspectRatio for frame 1280x720', () => { + const expectedAspectRatio = '16:9'; + + const width = 1280; + const height = 720; + + const aspectRatio = processFrames.calculateAspectRatio(width, height); + + assert.strictEqual(aspectRatio, expectedAspectRatio); + }); + + it('must correct calculate aspectRatio for frame 1440x720', () => { + const expectedAspectRatio = '18:9'; + + const width = 1440; + const height = 720; + + const aspectRatio = processFrames.calculateAspectRatio(width, height); + + assert.strictEqual(aspectRatio, expectedAspectRatio); + }); + + it('must correct calculate aspectRatio for frame 1680x720', () => { + const expectedAspectRatio = '21:9'; + + const width = 1680; + const height = 720; + + const aspectRatio = processFrames.calculateAspectRatio(width, height); + + assert.strictEqual(aspectRatio, expectedAspectRatio); + }); + + it('aspectRatio for frame with not default correlation', () => { + const expectedAspectRatio = '1000:720'; + + const width = 1000; + const height = 720; + + const aspectRatio = processFrames.calculateAspectRatio(width, height); + + assert.strictEqual(aspectRatio, expectedAspectRatio); + }); +}); diff --git a/tests/Unit/processFrames/calculateGopDuration.test.js b/tests/Unit/processFrames/calculateGopDuration.test.js new file mode 100644 index 0000000..3cafed4 --- /dev/null +++ b/tests/Unit/processFrames/calculateGopDuration.test.js @@ -0,0 +1,32 @@ +'use strict'; + +const {assert} = require('chai'); + +const processFrames = require('src/processFrames'); + +describe('processFrames.calculateGopDuration', () => { + + it('must correct calculate min, max and average gopDuration for gops', () => { + const expectedGopDuration = { + min : 1, + max : 6, + mean: 3.5 + }; + + const gops = [ + { + startTime: 1, + endTime : 2 + }, + { + startTime: 3, + endTime : 9 + } + ]; + + const gopDuration = processFrames.calculateGopDuration(gops); + + assert.deepEqual(gopDuration, expectedGopDuration); + }); + +}); diff --git a/tests/Unit/processFrames/filterAudioFrames.test.js b/tests/Unit/processFrames/filterAudioFrames.test.js new file mode 100644 index 0000000..bd6f29d --- /dev/null +++ b/tests/Unit/processFrames/filterAudioFrames.test.js @@ -0,0 +1,34 @@ +'use strict'; + +const {assert} = require('chai'); + +const processFrames = require('src/processFrames'); + +describe('processFrames.filterAudioFrames', () => { + + it('must corret filter audio frames', () => { + const expectedResult = [ + {media_type: 'audio'} + ]; + + let frames = [ + {media_type: 'video', width: 1}, + {media_type: 'audio'}, + {media_type: 'data'}, + {media_type: 'video', width: 2} + ]; + + const videoFrames = processFrames.filterAudioFrames(frames); + + assert.deepEqual(videoFrames, expectedResult); + }); + + it('must corret filter empty array of frames', () => { + const expectedResult = []; + + const videoFrames = processFrames.filterAudioFrames([]); + + assert.deepEqual(videoFrames, expectedResult); + }); + +}); From 5f23d5ea17721f8c91471d39e63aa271840bbc19 Mon Sep 17 00:00:00 2001 From: Oleh Poberezhets Date: Mon, 10 Sep 2018 09:27:45 +0300 Subject: [PATCH 3/8] update Readme.md --- README.md | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 71b6806..df793d6 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ structure as the `ffprobe -show_streams` output has. You may find a typical outp `videos` and `audios` may be an empty array if there are no appropriate streams in the live stream. -```json +``` { videos: [ { index: 1, codec_name: 'h264', @@ -214,7 +214,7 @@ do it pretty often). If ffprobe doesn't exit after `exitProcessGuardTimeoutInMs` After creation of the `FramesMonitor` instance, you may start listening live stream data. To do so, just 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. ```javascript const {FramesMonitor, processFrames, ExitReasons} = require('video-quality-tools'); @@ -250,16 +250,25 @@ try { ## `frame` event -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: ``` { media_type: 'video', key_frame: 0, pkt_pts_time: 3530.279, 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 @@ -357,7 +366,19 @@ There is an output for the example above: { mean: 1494.9075520833333, min: 1440.27734375, 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 }, + aspectRatio: '16:9', + width: 1280, + height: 720, + hasAudioStream: true +} ``` In given example the frames are collected in `frames` array and than use `processFrames` function for sets of 300 frames @@ -372,9 +393,10 @@ If there are more than 2 key frames, `processFrames` uses full GOPs to track fps in the last GOP that was not finished. It's important to remember the `remainedFrames` output and push a new frame to the `remainedFrames` array when it arrives. -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 `aspectRatio` adjust on +data from first frame of the first collected GOP. Value of `hasAudioStream` reflects a presence of audio frames. `processFrames` may throw `Errors.GopNotFoundError`. From eeaacc4bedf45446af23462e4c3cee64e4ccab13 Mon Sep 17 00:00:00 2001 From: Oleh Poberezhets Date: Mon, 10 Sep 2018 10:25:54 +0300 Subject: [PATCH 4/8] add Changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5f0182d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +## video-quality-tools v1.1.0 + +* **processFrames**: + + Added new fields `gopDuration`, `aspectRatio`, `width`, `height`, `hasAudioStream` to the result of + _processFrames_ execution . + + Add new methods to _processFrames_: `calculateGopDuration`, `calculateAspectRatio`, `filterAudioFrames`. + +* **FramesMonitor** + + FramesMonitor fetch video and audio frames from the stream now. + + Added `width` and `height` info to video frames. \ No newline at end of file From 6375e9749d617e82851d9d65f0cf1dab19928a67 Mon Sep 17 00:00:00 2001 From: Oleh Poberezhets Date: Mon, 10 Sep 2018 10:39:16 +0300 Subject: [PATCH 5/8] edit Changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f0182d..b614cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,6 @@ * **FramesMonitor** - FramesMonitor fetch video and audio frames from the stream now. + FramesMonitor fetches video and audio frames from the stream now. Added `width` and `height` info to video frames. \ No newline at end of file From 1dd255c70ab6a22cc2e1d860c1aed1ea54548e1e Mon Sep 17 00:00:00 2001 From: Oleh Poberezhets Date: Mon, 10 Sep 2018 11:45:03 +0300 Subject: [PATCH 6/8] add test on processFrames --- src/processFrames.js | 2 +- .../Unit/processFrames/processFrames.test.js | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/processFrames.js b/src/processFrames.js index 51c4b2b..84b5df3 100644 --- a/src/processFrames.js +++ b/src/processFrames.js @@ -42,7 +42,7 @@ function processFrames(frames) { const gopsDurations = []; gops.forEach(gop => { - areAllGopsIdentical = areAllGopsIdentical ? baseGopSize === gop.frames.length : false; + areAllGopsIdentical = areAllGopsIdentical ? baseGopSize === gop.frames.length : false; const accumulatedPktSize = processFrames.accumulatePktSize(gop); const gopDuration = processFrames.gopDurationInSec(gop); diff --git a/tests/Unit/processFrames/processFrames.test.js b/tests/Unit/processFrames/processFrames.test.js index 7d86463..4cea4fa 100644 --- a/tests/Unit/processFrames/processFrames.test.js +++ b/tests/Unit/processFrames/processFrames.test.js @@ -158,4 +158,55 @@ describe('processFrames', () => { assert.deepEqual(res2.remainedFrames, expectedRemainedFrames2); }); + 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, + aspectRatio: expectedAspectRatio, + width: expectedWidth, + height: expectedHeight, + hasAudioStream: expectAudio + }); + + assert.deepEqual(res.remainedFrames, expectedRemainedFrames); + }); + }); From 786f42d378cbfe411a6b61f6dd8d91b9a389daad Mon Sep 17 00:00:00 2001 From: Oleh Poberezhets Date: Tue, 11 Sep 2018 13:22:25 +0300 Subject: [PATCH 7/8] move calculateDisplayAspectRatio method to processFrames --- src/StreamsInfo.js | 34 ++------ src/processFrames.js | 70 +++++++++------ .../StreamsInfo/_adjustAspectRatio.data.js | 4 +- .../StreamsInfo/_adjustAspectRatio.test.js | 2 +- .../_calculateDisplayAspectRatio.test.js | 39 --------- .../calculateAspectRatio.test.js | 85 ------------------- .../calculateDisplayAspectRatio.data.js} | 54 ++++++++++-- .../calculateDisplayAspectRatio.test.js | 31 +++++++ .../processFrames/filterAudioFrames.test.js | 34 -------- .../findGCD.test.js} | 13 +-- .../Unit/processFrames/hasAudioFrames.test.js | 38 +++++++++ .../Unit/processFrames/processFrames.test.js | 1 - 12 files changed, 178 insertions(+), 227 deletions(-) delete mode 100644 tests/Unit/StreamsInfo/_calculateDisplayAspectRatio.test.js delete mode 100644 tests/Unit/processFrames/calculateAspectRatio.test.js rename tests/Unit/{StreamsInfo/_calculateDisplayAspectRatio.data.js => processFrames/calculateDisplayAspectRatio.data.js} (59%) create mode 100644 tests/Unit/processFrames/calculateDisplayAspectRatio.test.js delete mode 100644 tests/Unit/processFrames/filterAudioFrames.test.js rename tests/Unit/{StreamsInfo/_findGCD.test.js => processFrames/findGCD.test.js} (66%) create mode 100644 tests/Unit/processFrames/hasAudioFrames.test.js diff --git a/src/StreamsInfo.js b/src/StreamsInfo.js index 83e6fd0..1107bf7 100644 --- a/src/StreamsInfo.js +++ b/src/StreamsInfo.js @@ -6,6 +6,7 @@ const {exec} = require('child_process'); const {promisify} = require('util'); const Errors = require('./Errors/'); +const processFrames = require('./processFrames'); const DAR_OR_SAR_NA = 'N/A'; const DAR_OR_SAR_01 = '0:1'; @@ -129,36 +130,19 @@ class StreamsInfo { video.display_aspect_ratio === DAR_OR_SAR_NA ) { 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} + ); + } } return video; }); } - - _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; diff --git a/src/processFrames.js b/src/processFrames.js index 84b5df3..74dff09 100644 --- a/src/processFrames.js +++ b/src/processFrames.js @@ -26,7 +26,6 @@ function processFrames(frames) { throw new TypeError('process method is supposed to accept an array of frames.'); } - const audioFrames = processFrames.filterAudioFrames(frames); const videoFrames = processFrames.filterVideoFrames(frames); const {gops, remainedFrames} = processFrames.identifyGops(videoFrames); @@ -35,14 +34,14 @@ function processFrames(frames) { } let areAllGopsIdentical = true; - const hasAudioStream = audioFrames.length > 0; + const hasAudioStream = processFrames.hasAudioFrames(frames); const baseGopSize = gops[0].frames.length; const bitrates = []; const fpsList = []; - const gopsDurations = []; + const gopDurations = []; gops.forEach(gop => { - areAllGopsIdentical = areAllGopsIdentical ? baseGopSize === gop.frames.length : false; + areAllGopsIdentical = areAllGopsIdentical && baseGopSize === gop.frames.length; const accumulatedPktSize = processFrames.accumulatePktSize(gop); const gopDuration = processFrames.gopDurationInSec(gop); @@ -52,7 +51,7 @@ function processFrames(frames) { const gopFps = gop.frames.length / gopDuration; fpsList.push(gopFps); - gopsDurations.push(gopDuration); + gopDurations.push(gopDuration); }); const bitrate = { @@ -68,14 +67,14 @@ function processFrames(frames) { }; const gopDuration = { - mean: _.mean(gopsDurations), - min: Math.min(...gopsDurations), - max: Math.max(...gopsDurations) + 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 aspectRatio = calculateAspectRatio(width, height); + const aspectRatio = calculateDisplayAspectRatio(width, height); return { payload : { @@ -92,17 +91,18 @@ function processFrames(frames) { }; } -processFrames.identifyGops = identifyGops; -processFrames.calculateBitrate = calculateBitrate; -processFrames.calculateFps = calculateFps; -processFrames.calculateGopDuration = calculateGopDuration; -processFrames.calculateAspectRatio = calculateAspectRatio; -processFrames.filterVideoFrames = filterVideoFrames; -processFrames.filterAudioFrames = filterAudioFrames; -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; module.exports = processFrames; @@ -266,7 +266,15 @@ function calculateGopDuration(gops) { }; } -function calculateAspectRatio(width, height) { +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) { @@ -289,7 +297,9 @@ function calculateAspectRatio(width, height) { return WIDESCREEN_AR; } - return `${width}:${height}`; + const gcd = findGcd(width, height); + + return `${width / gcd}:${height / gcd}`; } function areAllGopsIdentical(gops) { @@ -300,10 +310,22 @@ function filterVideoFrames(frames) { return frames.filter(frame => frame.media_type === 'video'); } -function filterAudioFrames(frames) { - return frames.filter(frame => frame.media_type === 'audio'); +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); +} diff --git a/tests/Unit/StreamsInfo/_adjustAspectRatio.data.js b/tests/Unit/StreamsInfo/_adjustAspectRatio.data.js index e6c30c9..4647e6b 100644 --- a/tests/Unit/StreamsInfo/_adjustAspectRatio.data.js +++ b/tests/Unit/StreamsInfo/_adjustAspectRatio.data.js @@ -35,7 +35,7 @@ const validParams = [ { description: 'display_aspect_ratio param is invalid', 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}] }, { description: 'sample_aspect_ratio param is invalid', @@ -45,7 +45,7 @@ const validParams = [ { description: 'display_aspect_ratio param is invalid', 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}] } ]; diff --git a/tests/Unit/StreamsInfo/_adjustAspectRatio.test.js b/tests/Unit/StreamsInfo/_adjustAspectRatio.test.js index 04e3087..dd2fa26 100644 --- a/tests/Unit/StreamsInfo/_adjustAspectRatio.test.js +++ b/tests/Unit/StreamsInfo/_adjustAspectRatio.test.js @@ -18,7 +18,7 @@ describe('StreamsInfo::_adjustAspectRatio', () => { }, correctUrl); 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; it('{description}', function (ctx) { diff --git a/tests/Unit/StreamsInfo/_calculateDisplayAspectRatio.test.js b/tests/Unit/StreamsInfo/_calculateDisplayAspectRatio.test.js deleted file mode 100644 index d4b0f02..0000000 --- a/tests/Unit/StreamsInfo/_calculateDisplayAspectRatio.test.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -const {assert} = require('chai'); -const dataDriven = require('data-driven'); - -const {StreamsInfoError} = require('src/Errors'); - -const {correctPath, correctUrl, StreamsInfo} = require('./Helpers/'); - -const {invalidParams, validParams} = require('./_calculateDisplayAspectRatio.data'); - -describe('StreamsInfo::_calculateDisplayAspectRatio', () => { - - const streamsInfo = new StreamsInfo({ - ffprobePath : correctPath, - timeoutInSec: 1 - }, correctUrl); - - dataDriven(invalidParams, function () { - it('width or height must be a positive integers, but {description} was passed', function (ctx) { - const expectedErrorMsg = 'Can not calculate aspect rate due to invalid video resolution'; - const expectedErrorClass = StreamsInfoError; - - assert.throws(() => { - streamsInfo._calculateDisplayAspectRatio(ctx.width, ctx.height); - }, expectedErrorClass, expectedErrorMsg); - }); - }); - - dataDriven(validParams, function () { - it('calculate display aspect ratio for correct input {aspectRate}', (ctx) => { - const expected = ctx.aspectRate; - - const result = streamsInfo._calculateDisplayAspectRatio(ctx.width, ctx.height); - - assert.strictEqual(result, expected); - }); - }); -}); diff --git a/tests/Unit/processFrames/calculateAspectRatio.test.js b/tests/Unit/processFrames/calculateAspectRatio.test.js deleted file mode 100644 index 0d126d3..0000000 --- a/tests/Unit/processFrames/calculateAspectRatio.test.js +++ /dev/null @@ -1,85 +0,0 @@ -'use strict'; - -const {assert} = require('chai'); - -const processFrames = require('src/processFrames'); - -describe('processFrames.calculateAspectRatio', () => { - - it('must correct calculate aspectRatio for frame 640x640', () => { - const expectedAspectRatio = '1:1'; - - const width = 640; - const height = 640; - - const aspectRatio = processFrames.calculateAspectRatio(width, height); - - assert.strictEqual(aspectRatio, expectedAspectRatio); - }); - - it('must correct calculate aspectRatio for frame 640x480', () => { - const expectedAspectRatio = '4:3'; - - const width = 640; - const height = 480; - - const aspectRatio = processFrames.calculateAspectRatio(width, height); - - assert.strictEqual(aspectRatio, expectedAspectRatio); - }); - - it('must correct calculate aspectRatio for frame 854x480', () => { - const expectedAspectRatio = '16:9'; - - const width = 854; - const height = 480; - - const aspectRatio = processFrames.calculateAspectRatio(width, height); - - assert.strictEqual(aspectRatio, expectedAspectRatio); - }); - - it('must correct calculate aspectRatio for frame 1280x720', () => { - const expectedAspectRatio = '16:9'; - - const width = 1280; - const height = 720; - - const aspectRatio = processFrames.calculateAspectRatio(width, height); - - assert.strictEqual(aspectRatio, expectedAspectRatio); - }); - - it('must correct calculate aspectRatio for frame 1440x720', () => { - const expectedAspectRatio = '18:9'; - - const width = 1440; - const height = 720; - - const aspectRatio = processFrames.calculateAspectRatio(width, height); - - assert.strictEqual(aspectRatio, expectedAspectRatio); - }); - - it('must correct calculate aspectRatio for frame 1680x720', () => { - const expectedAspectRatio = '21:9'; - - const width = 1680; - const height = 720; - - const aspectRatio = processFrames.calculateAspectRatio(width, height); - - assert.strictEqual(aspectRatio, expectedAspectRatio); - }); - - it('aspectRatio for frame with not default correlation', () => { - const expectedAspectRatio = '1000:720'; - - const width = 1000; - const height = 720; - - const aspectRatio = processFrames.calculateAspectRatio(width, height); - - assert.strictEqual(aspectRatio, expectedAspectRatio); - }); -}); diff --git a/tests/Unit/StreamsInfo/_calculateDisplayAspectRatio.data.js b/tests/Unit/processFrames/calculateDisplayAspectRatio.data.js similarity index 59% rename from tests/Unit/StreamsInfo/_calculateDisplayAspectRatio.data.js rename to tests/Unit/processFrames/calculateDisplayAspectRatio.data.js index 5fe19be..1167211 100644 --- a/tests/Unit/StreamsInfo/_calculateDisplayAspectRatio.data.js +++ b/tests/Unit/processFrames/calculateDisplayAspectRatio.data.js @@ -45,37 +45,77 @@ const validParams = [ { width : 1, height : 1, - aspectRate: '1:1' + aspectRatio: '1:1' }, { width : 10, height : 1, - aspectRate: '10:1' + aspectRatio: '10:1' }, { width : 1, height : 10, - aspectRate: '1:10' + aspectRatio: '1:10' }, { width : 13, height : 7, - aspectRate: '13:7' + aspectRatio: '13:7' }, { width : 7, height : 13, - aspectRate: '7:13' + aspectRatio: '7:13' }, { width : 10, height : 5, - aspectRate: '2:1' + aspectRatio: '18:9' }, { width : 5, height : 10, - aspectRate: '1:2' + aspectRatio: '1:2' + }, + { + width : 640, + height : 480, + aspectRatio: '4:3' + }, + { + width : 854, + height : 480, + aspectRatio: '16:9' + }, + { + width : 1280, + height : 720, + aspectRatio: '16:9' + }, + { + width : 1284, + height : 720, + aspectRatio: '16:9' + }, + { + width : 1275, + height : 720, + aspectRatio: '16:9' + }, + { + width : 1440, + height : 720, + aspectRatio: '18:9' + }, + { + width : 1680, + height : 720, + aspectRatio: '21:9' + }, + { + width : 1000, + height : 720, + aspectRatio: '25:18' } ]; diff --git a/tests/Unit/processFrames/calculateDisplayAspectRatio.test.js b/tests/Unit/processFrames/calculateDisplayAspectRatio.test.js new file mode 100644 index 0000000..e3cd1df --- /dev/null +++ b/tests/Unit/processFrames/calculateDisplayAspectRatio.test.js @@ -0,0 +1,31 @@ +'use strict'; + +const {assert} = require('chai'); +const dataDriven = require('data-driven'); + +const processFrames = require('src/processFrames'); +const {invalidParams, validParams} = require('./calculateDisplayAspectRatio.data'); + +describe('processFrames.calculateDisplayAspectRatio', () => { + + dataDriven(invalidParams, function () { + it('width and height must be a positive integers, but {description} was passed', function (ctx) { + const expectedErrorMsg = /must be a positive integer/; + const expectedErrorClass = TypeError; + + assert.throws(() => { + processFrames.calculateDisplayAspectRatio(ctx.width, ctx.height); + }, expectedErrorClass, expectedErrorMsg); + }); + }); + + dataDriven(validParams, function () { + it('calculate display aspect ratio for correct input {aspectRatio}', (ctx) => { + const expected = ctx.aspectRatio; + + const result = processFrames.calculateDisplayAspectRatio(ctx.width, ctx.height); + + assert.strictEqual(result, expected); + }); + }); +}); diff --git a/tests/Unit/processFrames/filterAudioFrames.test.js b/tests/Unit/processFrames/filterAudioFrames.test.js deleted file mode 100644 index bd6f29d..0000000 --- a/tests/Unit/processFrames/filterAudioFrames.test.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const {assert} = require('chai'); - -const processFrames = require('src/processFrames'); - -describe('processFrames.filterAudioFrames', () => { - - it('must corret filter audio frames', () => { - const expectedResult = [ - {media_type: 'audio'} - ]; - - let frames = [ - {media_type: 'video', width: 1}, - {media_type: 'audio'}, - {media_type: 'data'}, - {media_type: 'video', width: 2} - ]; - - const videoFrames = processFrames.filterAudioFrames(frames); - - assert.deepEqual(videoFrames, expectedResult); - }); - - it('must corret filter empty array of frames', () => { - const expectedResult = []; - - const videoFrames = processFrames.filterAudioFrames([]); - - assert.deepEqual(videoFrames, expectedResult); - }); - -}); diff --git a/tests/Unit/StreamsInfo/_findGCD.test.js b/tests/Unit/processFrames/findGCD.test.js similarity index 66% rename from tests/Unit/StreamsInfo/_findGCD.test.js rename to tests/Unit/processFrames/findGCD.test.js index 255c608..2cc7197 100644 --- a/tests/Unit/StreamsInfo/_findGCD.test.js +++ b/tests/Unit/processFrames/findGCD.test.js @@ -3,15 +3,9 @@ const {assert} = require('chai'); const dataDriven = require('data-driven'); -const {correctPath, correctUrl, StreamsInfo} = require('./Helpers/'); - -describe('StreamsInfo::_findGcd', () => { - - const streamsInfo = new StreamsInfo({ - ffprobePath : correctPath, - timeoutInSec: 1 - }, correctUrl); +const processFrames = require('src/processFrames'); +describe('findGcd', () => { const data = [ {a: 0, b: 0, answer: 0}, {a: 1, b: 0, answer: 1}, @@ -21,13 +15,14 @@ describe('StreamsInfo::_findGcd', () => { {a: 7, b: 13, answer: 1}, {a: 56, b: 98, answer: 14}, {a: 98, b: 56, answer: 14}, + {a: 1280, b: 720, answer: 80}, ]; dataDriven(data, function () { it('for ({a}, {b})', function (ctx) { const expectation = ctx.answer; - const result = streamsInfo._findGcd(ctx.a, ctx.b); + const result = processFrames.findGcd(ctx.a, ctx.b); assert.strictEqual(result, expectation); }); diff --git a/tests/Unit/processFrames/hasAudioFrames.test.js b/tests/Unit/processFrames/hasAudioFrames.test.js new file mode 100644 index 0000000..4d2e6d2 --- /dev/null +++ b/tests/Unit/processFrames/hasAudioFrames.test.js @@ -0,0 +1,38 @@ +'use strict'; + +const {assert} = require('chai'); + +const processFrames = require('src/processFrames'); + +describe('processFrames.hasAudioFrames', () => { + + it('must detect the audio frames existence', () => { + const expectedResult = true; + + const frames = [ + {media_type: 'video', width: 1}, + {media_type: 'audio'}, + {media_type: 'data'}, + {media_type: 'video', width: 2} + ]; + + const hasAudioFrames = processFrames.hasAudioFrames(frames); + + assert.deepEqual(hasAudioFrames, expectedResult); + }); + + it('must detect the audio frames absence', () => { + const expectedResult = false; + + const frames = [ + {media_type: 'video', width: 1}, + {media_type: 'data'}, + {media_type: 'video', width: 2} + ]; + + const hasAudioFrames = processFrames.hasAudioFrames(frames); + + assert.deepEqual(hasAudioFrames, expectedResult); + }); + +}); diff --git a/tests/Unit/processFrames/processFrames.test.js b/tests/Unit/processFrames/processFrames.test.js index 4cea4fa..75063d8 100644 --- a/tests/Unit/processFrames/processFrames.test.js +++ b/tests/Unit/processFrames/processFrames.test.js @@ -178,7 +178,6 @@ describe('processFrames', () => { {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 = { From 9f94a65e1fac9ab3bfba46a30c1d3f26e926058b Mon Sep 17 00:00:00 2001 From: Oleh Poberezhets Date: Tue, 11 Sep 2018 14:40:20 +0300 Subject: [PATCH 8/8] update release info --- CHANGELOG.md | 4 ++-- README.md | 12 +++++++++--- src/processFrames.js | 4 ++-- tests/Unit/processFrames/processFrames.test.js | 6 +++--- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b614cf9..d947139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,10 @@ * **processFrames**: - Added new fields `gopDuration`, `aspectRatio`, `width`, `height`, `hasAudioStream` to the result of + Added new fields `gopDuration`, `displayAspectRatio`, `width`, `height`, `hasAudioStream` to the result of _processFrames_ execution . - Add new methods to _processFrames_: `calculateGopDuration`, `calculateAspectRatio`, `filterAudioFrames`. + Add new methods to _processFrames_: `calculateGopDuration`, `calculateDisplayAspectRatio`, `hasAudioFrames`. * **FramesMonitor** diff --git a/README.md b/README.md index df793d6..59d5f2e 100644 --- a/README.md +++ b/README.md @@ -374,7 +374,7 @@ There is an output for the example above: mean: 2, min: 1.9, max: 2.1 }, - aspectRatio: '16:9', + displayAspectRatio: '16:9', width: 1280, height: 720, hasAudioStream: true @@ -395,8 +395,14 @@ the `remainedFrames` array when it arrives. 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 `aspectRatio` adjust on -data from first frame of the first collected GOP. Value of `hasAudioStream` reflects a presence of audio frames. +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`. diff --git a/src/processFrames.js b/src/processFrames.js index 74dff09..0494bef 100644 --- a/src/processFrames.js +++ b/src/processFrames.js @@ -74,7 +74,7 @@ function processFrames(frames) { const width = gops[0].frames[0].width; const height = gops[0].frames[0].height; - const aspectRatio = calculateDisplayAspectRatio(width, height); + const displayAspectRatio = calculateDisplayAspectRatio(width, height); return { payload : { @@ -82,7 +82,7 @@ function processFrames(frames) { bitrate, fps, gopDuration, - aspectRatio, + displayAspectRatio, width, height, hasAudioStream diff --git a/tests/Unit/processFrames/processFrames.test.js b/tests/Unit/processFrames/processFrames.test.js index 75063d8..825302b 100644 --- a/tests/Unit/processFrames/processFrames.test.js +++ b/tests/Unit/processFrames/processFrames.test.js @@ -134,7 +134,7 @@ describe('processFrames', () => { bitrate : expectedBitrate1, fps : expectedFps1, gopDuration : expectedGopDuration1, - aspectRatio : expectedAspectRatio1, + displayAspectRatio : expectedAspectRatio1, width : expectedWidth1, height : expectedHeight1, hasAudioStream : expectAudio1 @@ -149,7 +149,7 @@ describe('processFrames', () => { bitrate : expectedBitrate2, fps : expectedFps2, gopDuration : expectedGopDuration2, - aspectRatio : expectedAspectRatio2, + displayAspectRatio : expectedAspectRatio2, width : expectedWidth2, height : expectedHeight2, hasAudioStream : expectAudio2 @@ -199,7 +199,7 @@ describe('processFrames', () => { bitrate: expectedBitrate, fps: expectedFps, gopDuration: expectedGopDuration, - aspectRatio: expectedAspectRatio, + displayAspectRatio: expectedAspectRatio, width: expectedWidth, height: expectedHeight, hasAudioStream: expectAudio