chris-rudmin / opus-recorder Goto Github PK
View Code? Open in Web Editor NEWThis project forked from mattdiamond/recorderjs
A library for encoding and decoding web audio as OggOpus.
License: Other
This project forked from mattdiamond/recorderjs
A library for encoding and decoding web audio as OggOpus.
License: Other
The first time an instance of Recorder is created, the user is asked for access to their microphone. In chrome this adds a red icon to the tab. There isn't any way seemingly to end the stream.
In my application I'm creating a new Recorder instance each time I want to record, is this an incorrect way to do things? I've added this.
recorder.addEventListener 'stop', =>
if recorder && recorder.stream
recorder.stream.stop()
It would be nice if I could de-initialise the stream as an intended feature of the library.
Apologies in advance for posting this as an issue.
We are using Recorderjs to encode to opus and the library is working great. We have a need to merge opus encoded files(identically encoded) on server in nodejs like so: file1.ogg + file2.ogg + ... fileN.ogg = final.ogg
The work would be paid but I would prefer final work be open source and in your name.
email: [email protected]
Thanks,
Ted Patrick
I have incorporated recorder.js into a Meteor project. Everything works fine (thanks!) but for some reason the first time I do a recording, it fires dataAvailable after only 130 bytes across both Chrome and FF (haven't tested on anything else yet). The second recording behaves as expected. Any reason why this might be happening?
Maybe I'm doing something wrong, but whenever I have streamPages set to true so that it is outputting chunks all of the blobs / typedarray data is unplayable.
Is it something I may be misunderstanding? Any help would be greatly appreciated. Great work :)
QUESTION
If a user makes a recorder initialization (init.addEventListener("click", function () {...}
- this event is happened) but does NOT start recording and then leaves a webpage with audio recording functionality, should I call recorder.stop()
when the page is left to stop the audio recording processes or they are stopped automatically in some way?
TESTIMONIAL
Thank you very much from my children for your package that has helped to develop my app. Using it I record and share with them fairy tales, they like them very much.
Have a nice day!
It looks like this is the only fork of Recorderjs that is in active development. Thanks so much for taking up the mantle!
What's the chance of publishing a new npm package?
I'm noticing that when setting the sample rate to 44100 (for example) the audio does not record. It seems that setting to 48000, 24000 and 12000 all work. Any thoughts on what might be going on?
Otherwise, everything is working perfectly. Thanks for putting this out there. Really phenomenal contribution to RecorderJS. I recorded over 2-hours with this and saving took only a few seconds to process and was around 15MB ๐
Hi, I can't make the example work on Chrome, is it supposed to work on Chrome 43?
Is there any trick?
Hi,
Any reason not to update the latest Opus and Speex?
LibOpus new stable is 1.1.4 (currently using 1.1.2)
Speex new stable is 1.2.0 (currently using 1.2rc3)
I think it might be a good idea to add some audio files to the project as tests, and run encode & decode on them.
this.worker.postMessage( page, [page.buffer] );;
postMessage(object, transferableList)
and postMessage(object)
To test:
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
if (ab.byteLength) {
alert('Transferables are not supported in your browser!');
} else {
// Transferables are supported.
}
Source: https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast
Hey Chris,
I'm loving the work you've done here.
I've played around with changing the target bitrate of the encoder, which seems to be working reasonably well. Right now my modifications to initCodec
look something like this:
OggOpus.prototype.initCodec = function() {
this.encoder = _opus_encoder_create( this.outputSampleRate, this.numberOfChannels, this.encoderApplication, allocate(4, 'i32', ALLOC_STACK) );
// here we go
var targetBitrate = 192000;
var bitrateLocation = _malloc(4);
HEAP32[bitrateLocation >>> 2] = targetBitrate;
_opus_encoder_ctl(
this.encoder,
4002, // this translates to 'OPUS_SET_BITRATE_REQUEST'
bitrateLocation)
;
_free(bitrateLocation);
// all done
this.encoderBufferIndex = 0;
this.encoderSamplesPerChannelPerPacket = this.outputSampleRate * this.encoderFrameSize / 1000;
this.encoderBufferLength = this.encoderSamplesPerChannelPerPacket * this.numberOfChannels;
this.encoderBufferPointer = _malloc( this.encoderBufferLength * 4 );
this.encoderBuffer = HEAPF32.subarray( this.encoderBufferPointer >> 2, (this.encoderBufferPointer >> 2) + this.encoderBufferLength );
this.encoderOutputMaxLength = 4000;
this.encoderOutputPointer = _malloc( this.encoderOutputMaxLength );
this.encoderOutputBuffer = HEAPU8.subarray( this.encoderOutputPointer, this.encoderOutputPointer + this.encoderOutputMaxLength );
};
At higher bitrates, however, this seems to be producing files that opusinfo
identifies some problems with. For example, after setting targetBitrate = 192000
above:
lappy486:Desktop theoffice$ opusinfo 192k.ogg
Processing file "192k.ogg"...
New logical stream (#1, serial: 00000539): type opus
Encoded with Recorder
WARNING: Sample count ahead of granule (364800>363840) in stream 1
WARNING: Sample count ahead of granule (606720>605760) in stream 1
WARNING: Sample count ahead of granule (842880>841920) in stream 1
WARNING: Sample count ahead of granule (1562880>1561920) in stream 1
WARNING: Sample count ahead of granule (1801920>1800960) in stream 1
WARNING: Sample count ahead of granule (1922880>1921920) in stream 1
WARNING: Sample count ahead of granule (2164800>2163840) in stream 1
WARNING: Sample count ahead of granule (2409600>2408640) in stream 1
WARNING: Sample count ahead of granule (2526720>2525760) in stream 1
WARNING: Sample count ahead of granule (3010560>3009600) in stream 1
WARNING: Sample count ahead of granule (3130560>3129600) in stream 1
WARNING: Sample count ahead of granule (3248640>3247680) in stream 1
WARNING: Sample count ahead of granule (3493440>3492480) in stream 1
WARNING: Sample count ahead of granule (3615360>3614400) in stream 1
WARNING: Sample count ahead of granule (3737280>3736320) in stream 1
WARNING: Sample count ahead of granule (3859200>3858240) in stream 1
WARNING: Sample count ahead of granule (3978240>3977280) in stream 1
WARNING: Sample count ahead of granule (4100160>4099200) in stream 1
WARNING: Sample count ahead of granule (4344960>4344000) in stream 1
WARNING: Sample count ahead of granule (4589760>4588800) in stream 1
WARNING: Sample count ahead of granule (4710720>4709760) in stream 1
WARNING: Sample count ahead of granule (4955520>4954560) in stream 1
WARNING: Sample count ahead of granule (5200320>5199360) in stream 1
WARNING: Sample count ahead of granule (5445120>5444160) in stream 1
WARNING: Sample count ahead of granule (5689920>5688960) in stream 1
WARNING: Sample count ahead of granule (5934720>5933760) in stream 1
WARNING: Sample count ahead of granule (6179520>6178560) in stream 1
WARNING: Sample count ahead of granule (6300480>6299520) in stream 1
WARNING: Sample count ahead of granule (6545280>6544320) in stream 1
WARNING: Sample count ahead of granule (6790080>6789120) in stream 1
WARNING: Sample count ahead of granule (7154880>7153920) in stream 1
WARNING: Sample count ahead of granule (7624320>7623360) in stream 1
WARNING: Sample count ahead of granule (7977600>7976640) in stream 1
WARNING: Sample count ahead of granule (8326080>8325120) in stream 1
WARNING: Sample count ahead of granule (8566080>8565120) in stream 1
WARNING: Sample count ahead of granule (8686080>8685120) in stream 1
WARNING: Sample count ahead of granule (8930880>8929920) in stream 1
WARNING: Sample count ahead of granule (9783360>9782400) in stream 1
WARNING: Sample count ahead of granule (10272000>10271040) in stream 1
WARNING: Sample count ahead of granule (10756800>10755840) in stream 1
WARNING: Sample count ahead of granule (10878720>10877760) in stream 1
WARNING: Sample count ahead of granule (10998720>10997760) in stream 1
WARNING: Sample count ahead of granule (11243520>11242560) in stream 1
WARNING: Sample count ahead of granule (11364480>11363520) in stream 1
WARNING: Sample count ahead of granule (11485440>11484480) in stream 1
WARNING: Sample count ahead of granule (11607360>11606400) in stream 1
WARNING: Sample count ahead of granule (11727360>11726400) in stream 1
WARNING: Sample count ahead of granule (11846400>11845440) in stream 1
WARNING: Sample count ahead of granule (12320640>12319680) in stream 1
Opus stream 1:
Pre-skip: 3840
Playback gain: 0 dB
Channels: 1
Original sample rate: 44100Hz
Packet duration: 20.0ms (max), 20.0ms (avg), 20.0ms (min)
Page duration: 2560.0ms (max), 2492.4ms (avg), 40.0ms (min)
Total data length: 6203394 bytes (overhead: 0.466%)
Playback length: 4m:16.639s
Average bitrate: 193.4 kb/s, w/o overhead: 192.5 kb/s
Logical stream 1 ended
I've poked around a bit further, and noticed this snippet in OggOpus.prototype.generatePage
:
// Number of samples upto and including this page at 48000Hz, into 64 bits
pageBufferView.setUint32( 6, granulePosition, true );
if ( granulePosition > 4294967296 || granulePosition < 0 ) {
pageBufferView.setUint32( 10, Math.floor( granulePosition/4294967296 ), true );
}
Is it possible that these are related? I'm a bit out of my depth here and trying to figure out whether there are other changes that need to be made to support varying the encoder bitrate. I'd love to hear any thoughts you have on this.
Is it possible to use lower sample rates like 8000Hz?
I know you have forced the use of 48000Hz sample rate, but is there any way I can make it convert to .ogg at 8000Hz sample rate??
Thanks so much for this great library! One question: Would it be possible to stream the encoded audio data to a server as it's being recorded? If so, any guidance would be appreciated.
Hey Chris,
I'm wondering whether you've ever seen an issue where audio data seems to get lost in a file without getting encoded. I'm not quite able to duplicate, but I have a hunch that it's related to underpowered/overtaxed systems. It doesn't seem to affect the overall file-length, but sections where there should be audio captured have silence instead. opusinfo
doesn't seem to reveal anything off with the files produced at the other end.
Anyway, I've been looking into this and figured I'd check to see whether it's something you've experienced.
When I click on the record button it's showing following error:
ReferenceError: _malloc is not defined
I didn't change anything. Just testing with the example files. Any suggestion please?
In unsupported browsers (like latest Safari) the way that AudioContext is defined causes page load errors. In Safari, the error is ReferenceError: Can't find variable: AudioContext.
To reproduce, use Safari to navigate to a page that loads recorder.js. Check the javascript console and you will see the ReferenceError.
Hi,
I need to compare the encoding to that of lib-opus-tools that runs on files.
Do you have a working encoder.html example where the input is a wav/pcm file instead of the microphone?
I tried creating a working example, but was unsuccessful.
Thx
Perhaps replace custom ogg implementation with an emscripten compiled version of libogg
Hey Chris, I really love the work you did here with the opus encoder. I was looking at doing something similar for actionscript. One question I have is, why do you use the _speex_resampler_process_interleaved_float when encoding opus? For my purposes I don't really need more than 1 channel, am I right to assume this might not be necessary if you only have 1 channel?
if ( this.resampleBufferIndex === this.resampleBufferLength ) {
_speex_resampler_process_interleaved_float( this.resampler, this.resampleBufferPointer, this.resampleSamplesPerChannelPointer, this.encoderBufferPointer, this.encoderSamplesPerChannelPointer );
var packetLength = _opus_encode_float( this.encoder, this.encoderBufferPointer, this.encoderSamplesPerChannel, this.encoderOutputPointer, this.encoderOutputMaxLength );
this.segmentPacket( packetLength );
this.resampleBufferIndex = 0;
}
There is huge memory leak in this: at least you don't terminate Workers and creating new each time.
Hi,
Any benchmarks have been done on mobile devices? especially mobile devices that are not top of the line?
If there's a need, I can add support for _opus_encode_fixed
in addition to _opus_encode_float
.
Resampler, encoder and decoder would benefit greatly from unit tests.
can it used to be a web radio station publisher?
i have seen "streamPages" config option, it will output Uint8Array data, i can broadcast these data to other client side with socket.io, but i have no idea about how to make these steamPages played on the other client side.
Hi Chris,
I'm using your library in my application to encode voice recordings from a microphone/headset combo. One of the recordings has some strange looping/skipping artifacts in it. The code used to init the recorder is:
var MONITOR_GAIN = 0;
var NUMBER_CHANNELS = 1;
var ENCODER_SAMPLE_RATE = 48000;
var BIT_RATE = 64000;
var recorder = new Recorder({
monitorGain: MONITOR_GAIN,
numberOfChannels: NUMBER_CHANNELS,
bitRate: BIT_RATE,
encoderSampleRate: ENCODER_SAMPLE_RATE,
encoderPath: '/js/recorder/oggopusEncoder.js'
});
The code does a recorder.initStream(), followed by recorder.start() and recorder.stop()
I've recorded several audio files and only one had a problem.
This was done on Chrome Version 47.0.2526.80 m on Windows 7. The machine is under fairly high CPU load and sometimes the gui becomes unresponsive. Might this have anything to do with the problem?
The actual ogg file can be found at https://github.com/afonso-arantes/issue It's the only file in the repository but happens to be named 1449866949-gQIPhRPDyA3Cn2vyW8vs.ogg
Playing it inside the github site as raw doesn't seem to work, but it works if you download it using git or wget.
Thank you so much for your help.
Hello,
I apologize if this is turns out not to be a real issue. I tried to use the example.html in Chrome but when I pressed the 'init' button, nothing happen.
In firefox, the getusermedia bring up a pop up and asking permission to access mic. That is expected behavior.
Anything I need to do to make this work in Chrome? Thank you in advance.
Could we get this on npm? If you're not familiar with how to do that, here are some helpful instructions
The project provides license details for the source it reuses; however, there doesn't seem to be a license for project itself. You'd expect a copyright notice with the author's name to cover the updates.
Hi,
I use your library successfully. It works really well. My goal is to achieve a complete recorder/player.
I would like to insert an ogg file in another ogg file at a specific position. And this in the browser in javascript. So the user will be able to take an existing recording, and to insert a new recording in it. Do you think it is possible ?
All the best,
Vivien TOURNEL
I'm working on at a bit of code which stores generated audio files in IndexedDB, but also uses the Web Audio API as well.
As it stands your version of Recorderjs is epic and works amazingly well, but because Chrome's implementation doesn't support Opus in decodeAudioData
, when I pass an ogg file in it fails to decode. I can see from the emscriptened libopus that the decode functionality is there, but that Recorderjs doesn't have a corresponding decode
call. Is there any chance you can add it? I'd do a PR, but I'm not quite sure how to work with the emscriptened code, I'm afraid :-/
Hello @chris-rudmin ,
I am the member of cdnjs project.
We want to host this library.
But there is a question want to ask.
I think the main file is recorder.js
.
Is necessary to add other dependent js files?
Thanks for your help!
Add documentation on how to use oggopusDecoder
Looks like Chrome's changed something in v50 (currently in beta) that is causing the navigator.getUserMedia
call in recorder.js to throw an error. (Specifically, a TrackStartError
.)
It seems that this due to the parameters passed in the call โ the call, as is, is
navigator.getUserMedia(
{ audio : this.config.streamOptions },
However changing it to this
navigator.getUserMedia(
{ audio : true },
resolves the issue.
I'm not sure what the implications of dropping the various options (googAutoGainControl
, etc) are, so I haven't done this up as a PR, but I wanted to give you a heads up.
Why this is not implemented using Web Audio API and encoding in the ScriptProcessorNode?
Hello, I'm testing the example on Chrome mobile Android 5.0, it records ok, but I can't play the recorded file.
According to this doc http://developer.android.com/guide/appendix/media-formats.html .ogg files can be played on android.
Do you have any idea?
Thank you
Seems like not all browsers support OGG so it would be good to get access to WAV blob to display it in a player while sending compressed OGG to the server. Is it possible? Thanks
Hello Chris,
This is a question.
First of all, thank you very much for your package. It's really great and helpful, in particular, because it converts the output to the OGG
format which is very compact. Also, your package is fast and reliable.
I've analyzed some of the most popular packages for sound recording and found that yours is one of the best or even the best one. Now I'm developing an app that is based on your package. But recently I've understood that some of potential users of my app will visit it via mobile devices and mobile browsers.
According to this list OGG Opus
is supported by mobile browsers very weak. So, I need to provide such users with audio files in some other format than OGG Opus
.
I've analyzed the issue #28. The approach described there is interesting but wav
files are relatively large in comparision with OGG
files and require much more traffic. There could be another way to solve the problem, namely, to convert OGG
to the mp3
format (which has great support on the mobile devices) on the server. Now I'm considering converting all recorded OGG
files into mp3
on the server using the ffmpeg
library. As a result, I'll store only mp3
files in the DB.
But before to implement this approach I've decided to ask your opinion. What do you think about this way? Maybe, there are other approaches that are more efficient?
Thank you in advance.
By the way, I've considered a client-side converting OGG
to mp3
but tests indicated that this process is very long and requires too much resources to be done. So I gave up that idea.
Hello,
I'm trying to send the encoded ogg-opus files to a node server for decoding using the package node-opus which also handles the ogg container using node-ogg.
Unfortunately node crashes. The output of the format event seems also to be weird because it's showing always 44100Hz, that is not supported by opus, independent from my chosen encoder settings on the client.
After some investigation I noticed that the libopus version of node-opus was pretty old (1.1 Beta).
So I replaced it using the current 1.1.2 build. Now it doesn't crash anymore but there is still no decoded data. And the weird 44100Hz are still shown as format.
If you use the opusinfo command line tool you can also see those 44100Hz. By the way opusdec tool plays the ogg-opus file successfully but it also shows several errors before ("Error playing audio").
It can be easily tested using your encoder example (recording some microphone input to a blob/file) and the stream-to-alsa example from node-opus (but using output to a file stream instead of alsa).
Well, the browser as well as VLC can play those files (besides there is no progress bar available) so it can be expected that it is rather a problem of node-opus. But I had a look in its source code and it is really a very thin layer to the native libraries. Anyway, I also submitted an issue there.
To make things easier, I attached a problematic sample recorded by this fork of RecorderJS:
test-file.ogg.txt
(The file has to be renamed before because of the file type restrictions on GitHub)
Thanks,
Robbson
hi - i have recently hired a programmer to combine the opus-recorder.js with
https://github.com/binaryjs/binaryjs
so the uploading would happen while the recorder is running. i am unfamiliar with forks, especially combining two existing githubs. any suggestions or advice? i am considering making the combination of the two open-sourced and any advice would be very much appreciated.
mark at edwardsmark(dot)com.
bower.json
currently doesn't include the worker and opus lib in the main
I want to decode ogg and play it.
but I only got noise :(
what's wrong with my code or decoder?
function decodeOgg(blob) {
chunks = [];
var worker = new Worker('oggopusDecoder.js');
worker.postMessage((new Recorder).config); // uses same config as for recording
worker.addEventListener("error", function(e) {
console.log(e);
});
worker.addEventListener("message", function(e) {
console.log(e.data);
chunks += e.data;
});
var reader = new FileReader();
reader.onload = function(event) {
worker.postMessage({
command: "decode",
pages: new Uint8Array(this.result)
});
};
reader.readAsArrayBuffer(blob);
setTimeout( function() {
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var source = audioCtx.createBufferSource();
audioBuffer = audioCtx.createBuffer( 1, chunks.length, audioCtx.sampleRate );
audioBuffer.getChannelData( 0 ).set( chunks );
source.buffer = audioBuffer;
source.connect(audioCtx.destination);
source.start();
}, 5000);
}
Hi,
I'm streaming from recorderjs to node-opus.
On the server when I read the header file it seems like it is still 44100.I also tried to run opusinfo, and still it is 44100.
Do I miss anything?
this is how I use it on the client:
var rec = new Recorderjs.Recorder({
encoderPath:'libs/recorderjs/encoderWorker.min.js',
streamPages:true,
encoderSampleRate:48000
});
On the server, using node-opus I do:
var oggDecoder = new ogg.Decoder();
var opusDecoder=null;
var wavWriter = null;
var fsOpus = fs.createWriteStream(opusFile);
oggDecoder.on('stream',function (oggStream) {
opusDecoder = new opus.Decoder();
opusDecoder.on('format', function (format) {
console.log(format);
wavWriter = new wav.FileWriter(pcmFile, {
channels: format.channels,
sampleRate: format.sampleRate,
bitDepth: format.bitDepth
});
opusDecoder.pipe(wavWriter);
});
oggStream.pipe(opusDecoder);
})
stream.pipe(oggDecoder);
stream.pipe(fsOpus);
Nice piece of work!
I did noticed the case 'doneRecoding' in recorderWorker.js throw when e.data.recordOpus is false. Seems instanceof fail, because importScripts( 'oggopus.js' ) has not run and the type has not loaded.The behaviour is a bit annoying in a Chrome's debugger, because it halts the worker thread. Here's the snippet:
case 'doneRecording':
if ( this.recorder instanceof OggOpus ) {
this.recorder.encodeFinalFrame();
}
break;
case 'init':
if ( e.data.recordOpus ) {
importScripts( 'oggopus.js' );
this.recorder = new OggOpus( e.data );
}
else {
importScripts( 'wavepcm.js' );
this.recorder = new WavePCM( e.data );
}
break;
Hi,
Environment: iPhone 6 with iOS 10.2.1
Browsers: Safari, mobile Chrome
Recorder.isRecordingSupported()
returns false
and I got an error Recording features are not supported in your browser
:
On the Apple laptop and PCs this functionality works fine.
Trying to understand what may be the reason, but still without any result. Will be very grateful for any help or hint.
Hi Chris,
Thanks for taking the time to develop and share this amazing codebase, it has really helped in our JS implementation of a web-based recording app.
We're thinking of using a Flash fallback for browsers that don't support getUserMedia() (stand up Safari!) which is working, but we're ending up with WAV files which is not ideal. What I'd like to do is pipe the Flash ByteArray data in realtime to recorderjs so that an .ogg can be made on the fly. In your opinion, would this be possible?
Thanks again, your library has been invaluable!
I'm currently using opus streaming.
new Recorder
recordOpus:
stream: true
On dataAvailable I'm piping information to the server, but I haven't found a way to detect that all pages are finished and the stream should be closed. After the existing stop event there are still one and sometimes two dataAvailable events that occur.
It would be really helpful if there were a dataFinished event or similar but I wasn't able to pinpoint a good place to put it.
Hi, there.
I tested a known-working page using your library on Chrome 46 and it worked. I then upgraded Chrome to 47 and tested again and got this exception:
Uncaught TypeError: this.stream.stop is not a functionRecorder.clearStream
@ recorder.js:51Recorder.stop
@ recorder.js:186(anonymous function)
@ audio:461m.event.dispatch
@ jquery.js:4r.handle @ jquery.js:4
Happens on Chrome 47.0.2526.106 on both Windows and Linux, both 64 bit. The same code still works fine on Firefox on Windows and Linux.
Does the fork use the same license as mattsdiamond's original code and can I use it for comercial projects?
Sorry just found the License...
Hi, Chris.
First off, thanks for your work on this. I think it will be very helpful for a project I am working on. My JS abilities are pretty terrible, but I'm trying to upload a recorded .ogg file as a blob to a server (running PHP). I am listening for the dataAvailable
event and trying to append e.detail
to a form, which I am hoping to POST through Ajax. I am finding that the resulting .ogg file is always the same size (130k) and never seems to have any actual captured audio when I try to play it back. The actual recording portion seems to be working (I am prompted for permissions to the mic, etc...). I am sure I am missing something obvious and I hope you can point it out to me.
Here my simple listener setup:
recorder.addEventListener( "dataAvailable", function(e) {
var formData = new FormData();
formData.append('audio', e.detail, 'blob.ogg');
console.log('about to send');
$.ajax({
type: "POST",
url: '/url',
data: formData,
success: function(data) {
console.log('success!');
},
processData: false,
contentType: false
});
});
Thanks for your help on this.
For now error is just thrown.
https://github.com/chris-rudmin/Recorderjs/blob/master/recorder.js#L164
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.