From aaa272eda5f927ed2bfa9bf7ae9627f853790690 Mon Sep 17 00:00:00 2001 From: Samir Date: Sat, 4 Nov 2017 00:34:49 +0600 Subject: [PATCH] unit test updated --- README.md | 9 +---- example/index.html | 34 ++++++---------- example/player/pcm-player.js | 58 +++++++++++++++++++++++++++ example/player/pcm_player.js | 9 ++--- karma.conf.js | 77 ++++++++++++++++++++++++++++++++++++ package.json | 19 ++++++--- rollup.config.js | 13 +++--- src/opus-to-pcm.js | 15 ++++++- src/utils/event.js | 39 ++++++++++++++++++ src/utils/ogg.js | 71 +++++++++++++++++++-------------- src/utils/opus-worker.js | 13 +++--- test/event.js | 31 +++++++++++++++ test/ogg.js | 34 ++++++++++++++++ 13 files changed, 334 insertions(+), 88 deletions(-) create mode 100644 example/player/pcm-player.js create mode 100644 karma.conf.js create mode 100644 src/utils/event.js create mode 100644 test/event.js create mode 100644 test/ogg.js diff --git a/README.md b/README.md index 503fe7d..fe4d5d3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1 @@ -git clone git@gitlab.ipvisionsoft.com:ipvision-web/liveplayer.git - - -cd liveplayer - -npm run build - -npm run start \ No newline at end of file +Coming soon \ No newline at end of file diff --git a/example/index.html b/example/index.html index 497f0bc..216c466 100644 --- a/example/index.html +++ b/example/index.html @@ -5,42 +5,30 @@ Opus to PCM -
- It should play audio if everying went well! +
+

It should play audio if everying went well!

+

Yea! recoreded audio is not in good qualtity though!

diff --git a/example/player/pcm-player.js b/example/player/pcm-player.js new file mode 100644 index 0000000..5d11718 --- /dev/null +++ b/example/player/pcm-player.js @@ -0,0 +1,58 @@ +function PCMPlayer() { + + this.samples = []; + this.flushingTime = 200; + this.createContext(); + this.startFlushing(); + this.flush = this.flush.bind(this); + this.interval = setInterval(this.flush, this.flushingTime); + + this.setConfig = function(sampleRate, channels) { + this.sampleRate = sampleRate; + this.channels = channels; + } + + this.createContext = function() { + this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + this.gainNode = this.audioCtx.createGain(); + this.gainNode.gain.value = 1; + this.gainNode.connect(this.audioCtx.destination); + }; + + this.stopFlushing = function() { + if (this.interval) { + clearInterval(this.interval); + } + } + + this.feed = function(data) { + let tmp = new Float32Array(this.samples.length + data.length); + tmp.set(this.samples, 0); + tmp.set(data, this.samples.length); + this.samples = tmp; + } + + this.flush = function() { + let bufferSource = this.audioCtx.createBufferSource(), + length = this.samples.length, + audioBuffer = this.audioCtx.createBuffer(this.channels, length, this.sampleRate), + audioData, + channel, + offset, + i; + + for (channel = 0; channel < this.channels; channel++) { + audioData = audioBuffer.getChannelData(channel); + offset = channel; + for (i = 0; i < length; i++) { + audioData[i] = this.samples[offset]; + offset += this.channels; + } + } + + bufferSource.buffer = audioBuffer; + bufferSource.connect(this.gainNode); + bufferSource.start(); + this.samples = []; + } +} \ No newline at end of file diff --git a/example/player/pcm_player.js b/example/player/pcm_player.js index 208c7b0..47bdd72 100644 --- a/example/player/pcm_player.js +++ b/example/player/pcm_player.js @@ -1,12 +1,9 @@ -function PCMPlayer() { +function PCMPlayer(channels, sampleRate) { this.samples = new Float32Array(); this.flushingTime = 200; - - this.setConfig = function(sampleRate, channels) { - this.sampleRate = sampleRate; - this.channels = channels; - }; + this.channels = channels; + this.sampleRate = sampleRate; this.createContext = function() { this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..9cc9073 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,77 @@ +// Karma configuration +// Generated on Fri Nov 03 2017 16:10:03 GMT+0600 (+06) + +module.exports = function(config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'], + + // list of files / patterns to load in the browser + files: [ + 'test/*.js' + ], + + // list of files to exclude + exclude: [ + ], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'test/*.js': ['rollup'] + }, + + rollupPreprocessor: { + plugins: [ + require('rollup-plugin-node-globals')(), + require('rollup-plugin-node-builtins')(), + require('rollup-plugin-babel')() + ], + format: 'iife', // Helps prevent naming collisions. + name: 'Decoder', // Required for 'iife' format. + sourcemap: 'inline' // Sensible for testing. + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity + }) +} diff --git a/package.json b/package.json index a508c9c..21f1641 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { - "name": "opsu-to-pcm", + "name": "opus-to-pcm", "version": "0.1.0", "description": "Decode raw opus packet to pcm without using any external library", "main": "dist/opus_to_pcm.js", "scripts": { "build": "rollup -c", "pro": "NODE_ENV=production rollup -c", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "karma start karma.conf.js" }, "repository": { "type": "git", @@ -16,13 +16,22 @@ "devDependencies": { "babel-plugin-external-helpers": "^6.22.0", "babel-preset-es2015": "^6.24.1", + "babel-register": "^6.26.0", + "karma": "^1.7.1", + "karma-chrome-launcher": "^2.2.0", + "karma-mocha": "^1.3.0", + "karma-rollup-preprocessor": "^5.0.1", + "mocha": "^4.0.1", "path": "^0.12.7", - "rollup": "^0.39.0", + "rollup": "^0.50.0", "rollup-plugin-babel": "^2.7.1", + "rollup-plugin-buble": "^0.16.0", "rollup-plugin-eslint": "^3.0.0", + "rollup-plugin-node-builtins": "^2.1.2", + "rollup-plugin-node-globals": "^1.1.0", + "rollup-plugin-node-resolve": "^3.0.0", "rollup-plugin-replace": "^1.1.1", "rollup-plugin-uglify": "^1.0.1" }, - "dependencies": { - } + "dependencies": {} } diff --git a/rollup.config.js b/rollup.config.js index 60c9b3c..528edfb 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -6,13 +6,14 @@ import eslint from 'rollup-plugin-eslint'; import replace from 'rollup-plugin-replace'; import uglify from 'rollup-plugin-uglify'; - export default { - entry: 'src/opus-to-pcm.js', - dest: 'dist/opus_to_pcm.js', - format: 'iife', - moduleName: 'Decoder', - sourceMap: false, //inline + input: 'src/opus-to-pcm.js', + output: { + file: 'dist/opus_to_pcm.js', + format: 'iife', + name: 'Decoder', + sourcemap: false, //inline + }, plugins: [ eslint(), babel({ diff --git a/src/opus-to-pcm.js b/src/opus-to-pcm.js index 1a31e7a..5dfd5a3 100644 --- a/src/opus-to-pcm.js +++ b/src/opus-to-pcm.js @@ -1,9 +1,11 @@ import { appendByteArray } from './utils/utils.js'; +import Event from './utils/event.js'; import Ogg from './utils/ogg.js'; import OpusWorker from './utils/opus-worker.js'; -export class OpusToPCM { +export class OpusToPCM extends Event { constructor(options) { + super('decoder'); window.MediaSource = window.MediaSource || window.WebKitMediaSource; let nativeSupport = !!(window.MediaSource && window.MediaSource.isTypeSupported('audio/webm; codecs=opus')); let defaults = { @@ -19,20 +21,29 @@ export class OpusToPCM { } else { this.decoder = null; } + + if (this.decoder) { + this.decoder.on('data', this.onData.bind(this)); + } } getSampleRate() { return this.decoder.getSampleRate(); } + onData(data) { + this.dispatch('decode', data); + } + decode(packet) { if (!this.decoder) { throw ('opps! no decoder is found to decode'); } - return this.decoder.decode(packet); + this.decoder.decode(packet); } destroy() { this.decoder.destroy(); + this.offAll(); } } diff --git a/src/utils/event.js b/src/utils/event.js new file mode 100644 index 0000000..d651ef4 --- /dev/null +++ b/src/utils/event.js @@ -0,0 +1,39 @@ +export default class Event { + constructor(type) { + this.listener = {}; + this.type = type | ''; + } + + on(event, fn) { + if (!this.listener[event]) { + this.listener[event] = []; + } + this.listener[event].push(fn); + return true; + } + + off(event, fn) { + if (this.listener[event]) { + var index = this.listener[event].indexOf(fn); + if (index > -1) { + this.listener[event].splice(index, 1); + } + return true; + } + return false; + } + + offAll() { + this.listener = {}; + } + + dispatch(event, data) { + if (this.listener[event]) { + this.listener[event].map((each) => { + each.apply(null, [data]); + }); + return true; + } + return false; + } +} diff --git a/src/utils/ogg.js b/src/utils/ogg.js index f2400cc..189dc28 100644 --- a/src/utils/ogg.js +++ b/src/utils/ogg.js @@ -1,15 +1,17 @@ +import Event from './event.js'; import { appendByteArray } from './utils.js'; -export default class Ogg { +export default class Ogg extends Event { constructor(channel) { - this.outSampleRate = 0; + super('ogg'); this.channel = channel; this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - this.resolver = null; + this.queue = []; + this.flushLimit = 20; /* the larger flush limit, the lesser clicking noise */ this.init(); } getSampleRate() { - return this.outSampleRate; + return this.audioCtx.sampleRate; } init() { @@ -42,19 +44,18 @@ export default class Ogg { dv.setUint16( 10, 0, true ); // pre-skip, don't need to skip any value dv.setUint32( 12, 8000, true ); // original sample rate, any valid sample e.g 8000 dv.setUint16( 16, 0, true ); // output gain - dv.setUint8( 18, 0, true ); // channel map 0 = mono or stereo + dv.setUint8( 18, 0, true ); // channel map 0 = one stream: mono or stereo return data; } getCommentHeader() { - let data = new Uint8Array(24), + let data = new Uint8Array(20), dv = new DataView(data.buffer); dv.setUint32( 0, 1937076303, true ); // Magic Signature 'Opus' dv.setUint32( 4, 1936154964, true ); // Magic Signature 'Tags' - dv.setUint32( 8, 8, true ); // Vendor Length - dv.setUint32( 12, 1919512167, true ); // Vendor name 'ring' - dv.setUint32( 16, 1818850917, true ); // Vendor name 'live' - dv.setUint32( 20, 0, true ); // User Comment List Length + dv.setUint32( 8, 4, true ); // Vendor Length + dv.setUint32( 12, 1633837924, true ); // Vendor name 'abcd' + dv.setUint32( 16, 0, true ); // User Comment List Length return data; } @@ -84,12 +85,19 @@ export default class Ogg { return page; } - getOGG(packet) { + getOGG() { let oggData = this.oggHeader, - segmentData; + packet, + segmentData, + headerType; + + while (this.queue.length) { + packet = this.queue.shift(); + headerType = this.queue.length == 0 ? 4 : 0; // for last packet, header type should be end of stream + segmentData = this.getPage(packet, headerType); + oggData = appendByteArray(oggData, segmentData); + } - segmentData = this.getPage(packet, 4); /* headerType - end of stream i.e 4 */ - oggData = appendByteArray(oggData, segmentData); this.pageIndex = 2; /* reseting pageIndex to 2 so we can re-use same header */ return oggData; } @@ -114,27 +122,28 @@ export default class Ogg { } decode(packet) { - let ogg = this.getOGG(packet); - return new Promise((resolve) => { - this.audioCtx.decodeAudioData(ogg.buffer, (audioBuffer) => { - let pcmFloat; - - if (!this.outSampleRate) { - this.outSampleRate = audioBuffer.sampleRate; - } - if (this.channel == 1) { - pcmFloat = audioBuffer.getChannelData(0); - } else { - pcmFloat = this.getMergedPCMData(audioBuffer); - } - resolve(pcmFloat); - }); + this.queue.push(packet); + if (this.queue.length >= this.flushLimit) { + this.process(); + } + } + + process() { + let ogg = this.getOGG(); + this.audioCtx.decodeAudioData(ogg.buffer, (audioBuffer) => { + let pcmFloat; + if (this.channel == 1) { + pcmFloat = audioBuffer.getChannelData(0); + } else { + pcmFloat = this.getMergedPCMData(audioBuffer); + } + this.dispatch('data', pcmFloat); }); } getMergedPCMData(audioBuffer) { let audioData, - result, + result = [], length, pcmFloat, offset = 0, @@ -148,6 +157,7 @@ export default class Ogg { length = result[0].length; pcmFloat = new Float32Array(this.channel * length); + i = 0; while(length > i) { for(j=0; j { - this.resolver = resolve; - }); } onMessage(event) { let data = event.data; - if (this.resolver) { - this.resolver(data.buffer); - } + this.dispatch('data', data.buffer); } destroy() { this.worker = null; + this.offAll(); } } diff --git a/test/event.js b/test/event.js new file mode 100644 index 0000000..1bec7e9 --- /dev/null +++ b/test/event.js @@ -0,0 +1,31 @@ +import {equal, deepEqual} from 'assert'; +import Event from '../src/utils/event.js'; + +var event = new Event('XYZ'); + +var callback = function() { +}; + +describe('event tests --', function() { + + beforeEach(function(){ + event.offAll(); + event.on('topic1',callback); + event.on('topic1',callback); + event.on('topic2',callback); + }); + + it('listener should be 2 for the topic1', function() { + equal(event.listener.topic1.length, 2); + }); + + it('event dispatcher should be return true for a successful dispatcher', function() { + equal(event.dispatch('topic1', true), true); + }); + + it('listener should be zero after removing all listeners', function() { + event.offAll(); + deepEqual(event.listener, {}); + }); +}); + diff --git a/test/ogg.js b/test/ogg.js new file mode 100644 index 0000000..3e0cf96 --- /dev/null +++ b/test/ogg.js @@ -0,0 +1,34 @@ +import {equal} from 'assert'; +import Ogg from '../src/utils/ogg.js'; +var channel = 1, + decoder = new Ogg(channel), + audioCtx = new (window.AudioContext || window.webkitAudioContext)(), + sampleRate = audioCtx.sampleRate; + + +describe('Ogg tests -- ', function() { + it('sample rate should be same system sample rate', function() { + equal(decoder.getSampleRate(), sampleRate); + }); + + it('magic Signature of ID header should be Opus Head', function() { + var idHeader = decoder.getIDHeader(); + var dv = new DataView(idHeader.buffer); + equal(dv.getUint32(0, true), 1937076303); + equal(dv.getUint32(4, true), 1684104520); + }); + + it('magic Signature of comment header should be Opus Tags', function() { + var commonHeader = decoder.getCommentHeader(); + var dv = new DataView(commonHeader.buffer); + equal(dv.getUint32(0, true), 1937076303); + equal(dv.getUint32(4, true), 1936154964); + }); + + it('page header should be started with OggS', function() { + var segmentData = new Uint8Array(20); + var page = decoder.getPage(segmentData, 4); + var dv = new DataView(page.buffer); + equal(dv.getUint32(0, true), 1399285583); + }); +}); \ No newline at end of file