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 @@
- 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