unit test updated

pull/14/head
Samir 7 years ago
parent f65222fc73
commit aaa272eda5

@ -1,8 +1 @@
git clone git@gitlab.ipvisionsoft.com:ipvision-web/liveplayer.git
cd liveplayer
npm run build
npm run start
Coming soon

@ -5,42 +5,30 @@
<title>Opus to PCM</title>
</head>
<body>
<div id="container" style="width: 400px; margin: 0 auto; font-size: 30px;">
It should play audio if everying went well!
<div id="container" style="width: 400px; margin: 0 auto;">
<h2>It should play audio if everying went well!</h2>
<p>Yea! recoreded audio is not in good qualtity though! </p>
</div>
<script>
window.onload = function() {
var orgSampleRate = 8000,
channels = 1,
configFlag = false,
socketURL = 'ws://localhost:8080';
var channels = 1,
socketURL = 'ws://localhost:8080';
var decoder = new Decoder.OpusToPCM({
channels: channels
});
var player = new PCMPlayer();
decoder.on('decode', function(pcmData) {
player.feed(pcmData);
});
var player = new PCMPlayer(channels, decoder.getSampleRate());
var ws = new WebSocket(socketURL);
ws.binaryType = 'arraybuffer';
ws.addEventListener('message',function(event) {
var data = new Uint8Array(event.data);
decoder.decode(data).then(function(pcmData) {
/*
Sample rate in opus is not fixed.
It can vary depending on hardware rate. But
we need sample rate for playing PCM data using audio context,
so lets set sampelRate here returned by decoder.
*/
if (!configFlag) {
var samppleRate = decoder.getSampleRate();
player.setConfig(samppleRate, channels);
configFlag = true;
}
player.feed(pcmData);
});
decoder.decode(data);
});
}
</script>

@ -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 = [];
}
}

@ -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)();

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

@ -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": {}
}

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

@ -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();
}
}

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

@ -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.channel; j++) {
pcmFloat[offset++] = result[j][i];
@ -161,5 +171,6 @@ export default class Ogg {
this.oggHeader = null;
this.audioCtx = null;
this.checksumTable = null;
this.offAll();
}
}

@ -1,7 +1,8 @@
export default class OpusWorker {
import Event from './event.js';
export default class OpusWorker extends Event {
constructor(channels) {
super('worker');
this.worker = new Worker('libopus/opus.min.js');
this.resolver = null;
this.worker.addEventListener('message', this.onMessage.bind(this));
this.worker.postMessage({
type: 'init',
@ -22,18 +23,14 @@ export default class OpusWorker {
buffer: packet
};
this.worker.postMessage(workerData);
return new Promise((resolve) => {
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();
}
}

@ -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, {});
});
});

@ -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);
});
});
Loading…
Cancel
Save