Browse Source

Make it work with recordjs

File is now encoded to Opus, like in other apps
master
Igor Zhukov 7 years ago
parent
commit
0a2b5fbdfe
  1. 2
      app/index.html
  2. 2
      app/js/controllers.js
  3. 134
      app/js/directives.js
  4. 31
      app/less/mobile.less
  5. 2
      app/partials/mobile/im.html
  6. 18
      app/vendor/recorderjs/encoder_worker.js
  7. 242
      app/vendor/recorderjs/recorder.js
  8. 1
      app/vendor/recorderjs/recorder.min.js
  9. 2
      app/webogram.appcache

2
app/index.html

@ -78,6 +78,8 @@ @@ -78,6 +78,8 @@
<script type="text/javascript" src="vendor/ogv.js/ogv-decoder-audio-vorbis.js"></script>
<script type="text/javascript" src="vendor/ogv.js/ogv-support.js"></script>
<script type="text/javascript" src="vendor/recorderjs/recorder.js"></script>
<script type="text/javascript" src="js/lib/utils.js"></script>
<script type="text/javascript" src="js/lib/bin_utils.js"></script>
<script type="text/javascript" src="js/lib/tl_utils.js"></script>

2
app/js/controllers.js

@ -518,8 +518,6 @@ angular.module('myApp.controllers', ['myApp.i18n']) @@ -518,8 +518,6 @@ angular.module('myApp.controllers', ['myApp.i18n'])
skipped: false
}
$scope.voiceRecorder = { time : '', recording : null, processing : false };
$scope.openSettings = function () {
$modal.open({
templateUrl: templateUrl('settings_modal'),

134
app/js/directives.js

@ -1547,7 +1547,7 @@ angular.module('myApp.directives', ['myApp.filters']) @@ -1547,7 +1547,7 @@ angular.module('myApp.directives', ['myApp.filters'])
}
})
.directive('mySendForm', function (_, $q, $timeout, $compile, $modalStack, $http, $interpolate, Storage, AppStickersManager, AppDocsManager, ErrorService, AppInlineBotsManager, FileManager, shouldFocusOnInteraction) {
.directive('mySendForm', function (_, $q, $timeout, $interval, $window, $compile, $modalStack, $http, $interpolate, Storage, AppStickersManager, AppDocsManager, ErrorService, AppInlineBotsManager, FileManager, shouldFocusOnInteraction) {
return {
link: link,
scope: {
@ -1564,17 +1564,25 @@ angular.module('myApp.directives', ['myApp.filters']) @@ -1564,17 +1564,25 @@ angular.module('myApp.directives', ['myApp.filters'])
var fileSelects = $('input', element)
var dropbox = $('.im_send_dropbox_wrap', element)[0]
var messageFieldWrap = $('.im_send_field_wrap', element)[0]
var sendFieldPanel = $('.im_send_field_panel', element)[0]
var dragStarted
var dragTimeout
var submitBtn = $('.im_submit', element)[0]
var voiceRecord = $('.im_record', element);
var voiceRecordBtn = $('.im_record', element)[0]
var stickerImageCompiled = $compile('<a class="composer_sticker_btn" data-sticker="{{::document.id}}" my-load-sticker document="document" thumb="true" img-class="composer_sticker_image"></a>')
var cachedStickerImages = {}
var audioRecorder = null;
var audioPromise = null;
var audioStream = null;
var voiceRecorder = null
var voiceRecordSuccess = false
var voiceRecordSupported = Recorder.isRecordingSupported()
var voiceRecordDurationInterval = null
var voiceRecorderPromise = null
if (voiceRecordSupported) {
$(sendFieldPanel).addClass('im_record_supported')
}
$scope.voiceRecorder = {duration: 0, recording: false, processing: false}
var emojiTooltip = new EmojiTooltip(emojiButton, {
getStickers: function (callback) {
@ -1688,82 +1696,82 @@ angular.module('myApp.directives', ['myApp.filters']) @@ -1688,82 +1696,82 @@ angular.module('myApp.directives', ['myApp.filters'])
})
})
navigator.getUserMedia = ( navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
voiceRecord.on('touchstart', function(e) {
if ($scope.$parent.$parent.voiceRecorder.processing) { return; }
$(voiceRecordBtn).on('contextmenu', cancelEvent)
navigator.getUserMedia({audio : true}, function(stream){
var start = Date.now();
var touch = null;
audioPromise = null;
audioStream = stream;
audioRecorder = new MediaRecorder(stream);
var interval = setInterval(function(){
var time = (new Date());
$(voiceRecordBtn).on('touchstart', function(e) {
if ($scope.voiceRecorder.processing) {
return
}
time.setTime(Date.now() - start);
voiceRecorderPromise = null
$scope.$apply(function(){
$scope.$parent.$parent.voiceRecorder.time = (time.getMinutes() < 10 ? '0' : '') + time.getMinutes() + ':' + (time.getSeconds() < 10 ? '0' : '') + time.getSeconds();
});
}, 1000);
voiceRecorder = new Recorder({
monitorGain: 0,
numberOfChannels: 1,
bitRate: 64000,
encoderSampleRate: 48000,
encoderPath: 'vendor/recorderjs/encoder_worker.js'
})
$scope.$apply(function(){
$scope.$parent.$parent.voiceRecorder.time = '00:00';
$scope.$parent.$parent.voiceRecorder.recording = interval;
});
voiceRecorder.addEventListener('start', function(e) {
var startTime = tsNow(true)
audioRecorder.start();
voiceRecordSuccess = false
console.log('recording now!');
voiceRecordDurationInterval = $interval(function() {
$scope.voiceRecorder.duration = tsNow(true) - startTime
}, 1000)
}, function(e){
console.error(e);
});
});
$scope.$apply(function() {
$scope.voiceRecorder.recording = true
})
voiceRecord.on('click', function(){
if (audioPromise) {
$scope.$parent.$parent.voiceRecorder.processing = true;
console.warn(dT(), 'recording now!')
})
audioPromise.then(function(e) {
var blob = e.data;
voiceRecorder.addEventListener('streamReady', function(e) {
voiceRecorder.start()
})
console.log(blob);
$scope.draftMessage.files = [blob];
$scope.draftMessage.isMedia = true;
voiceRecorder.initStream()
$scope.$parent.$parent.voiceRecorder.processing = false;
$($window).one('touchend', function() {
var deferred = $q.defer()
voiceRecorder.addEventListener('dataAvailable', function(e) {
var blob = blobConstruct([e.detail], 'audio/ogg')
deferred.resolve(blob)
})
voiceRecorderPromise = deferred.promise
voiceRecorder.stop()
audioPromise = null;
});
}
});
$interval.cancel(voiceRecordDurationInterval)
$($window).on('touchend', function(){
if (audioStream && audioRecorder) {
audioPromise = new Promise(function(resolve) {
audioRecorder.ondataavailable = resolve;
});
$scope.$apply(function() {
$scope.voiceRecorder.recording = false
})
})
})
audioRecorder.stop();
audioStream.stop();
$(voiceRecordBtn).on('touchend', function(e) {
voiceRecordSuccess = true
$timeout(function () {
if (voiceRecorderPromise) {
$scope.voiceRecorder.processing = true
audioRecorder = null;
audioStream = null;
voiceRecorderPromise.then(function(blob) {
console.warn(dT(), 'got audio', blob)
$scope.draftMessage.files = [blob]
$scope.draftMessage.isMedia = true
clearInterval($scope.$parent.$parent.voiceRecorder.recording);
$scope.voiceRecorder.processing = false
$scope.$apply(function(){
$scope.$parent.$parent.voiceRecorder.recording = null;
});
}
});
voiceRecorderPromise = null
})
}
}, 100)
})
var sendOnEnter = true;
var sendOnEnter = true
function updateSendSettings () {
Storage.get('send_ctrlenter').then(function (sendOnCtrl) {
sendOnEnter = !sendOnCtrl

31
app/less/mobile.less

@ -1431,7 +1431,7 @@ a.im_message_fwd_author { @@ -1431,7 +1431,7 @@ a.im_message_fwd_author {
background-position: -12px -285px;
}
.im_attach, .im_record {
.im_attach {
cursor: pointer;
display: none;
overflow: hidden;
@ -1442,6 +1442,7 @@ a.im_message_fwd_author { @@ -1442,6 +1442,7 @@ a.im_message_fwd_author {
width: 50px;
height: 32px;
padding: 3px 13px 4px 16px;
right: 0;
&:active {
.icon-paperclip {
@ -1451,29 +1452,15 @@ a.im_message_fwd_author { @@ -1451,29 +1452,15 @@ a.im_message_fwd_author {
}
}
.non_ffos {
.im_attach {
right: 0;
}
}
.im_record {
right: 0;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.ffos {
.im_send_form_empty {
.im_send_field_wrap {
margin-right: 85px;
}
right: 0;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
.im_record {
display: block;
}
.im_record_supported .im_send_form_empty & {
display: block;
}
}

2
app/partials/mobile/im.html

@ -163,7 +163,7 @@ @@ -163,7 +163,7 @@
<div class="im_voice_recorder_wrap">
<div class="im_recorder_indicator"><i></i></div>
<div class="im_recorder_time">{{voiceRecorder.time}}</div>
<div class="im_recorder_time" ng-bind="voiceRecorder.duration | duration"></div>
<div class="im_recorder_label">
<i class="icon icon-back"></i>
<span my-i18n="im_voice_recorder_label"></span>

18
app/vendor/recorderjs/encoder_worker.js vendored

File diff suppressed because one or more lines are too long

242
app/vendor/recorderjs/recorder.js vendored

@ -0,0 +1,242 @@ @@ -0,0 +1,242 @@
"use strict";
var root = (typeof self === 'object' && self.self === self && self) || (typeof global === 'object' && global.global === global && global) || this;
(function( global ) {
var Recorder = function( config ){
var that = this;
if ( !Recorder.isRecordingSupported() ) {
throw new Error("Recording is not supported in this browser");
}
this.state = "inactive";
this.eventTarget = global.document.createDocumentFragment();
this.audioContext = new global.AudioContext();
this.monitorNode = this.audioContext.createGain();
this.config = config = config || {};
this.config.command = "init";
this.config.bufferLength = config.bufferLength || 4096;
this.config.monitorGain = config.monitorGain || 0;
this.config.numberOfChannels = config.numberOfChannels || 1;
this.config.originalSampleRate = this.audioContext.sampleRate;
this.config.encoderSampleRate = config.encoderSampleRate || 48000;
this.config.encoderPath = config.encoderPath || 'encoderWorker.min.js';
this.config.streamPages = config.streamPages || false;
this.config.leaveStreamOpen = config.leaveStreamOpen || false;
this.config.maxBuffersPerPage = config.maxBuffersPerPage || 40;
this.config.encoderApplication = config.encoderApplication || 2049;
this.config.encoderFrameSize = config.encoderFrameSize || 20;
this.config.resampleQuality = config.resampleQuality || 3;
this.config.streamOptions = config.streamOptions || {
optional: [],
mandatory: {
googEchoCancellation: false,
googAutoGainControl: false,
googNoiseSuppression: false,
googHighpassFilter: false
}
};
this.setMonitorGain( this.config.monitorGain );
this.scriptProcessorNode = this.audioContext.createScriptProcessor( this.config.bufferLength, this.config.numberOfChannels, this.config.numberOfChannels );
this.scriptProcessorNode.onaudioprocess = function( e ){
that.encodeBuffers( e.inputBuffer );
};
};
Recorder.isRecordingSupported = function(){
return global.AudioContext && global.navigator && ( global.navigator.getUserMedia || ( global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia ) );
};
Recorder.prototype.addEventListener = function( type, listener, useCapture ){
this.eventTarget.addEventListener( type, listener, useCapture );
};
Recorder.prototype.clearStream = function() {
if ( this.stream ) {
if ( this.stream.getTracks ) {
this.stream.getTracks().forEach(function ( track ) {
track.stop();
});
}
else {
this.stream.stop();
}
delete this.stream;
}
};
Recorder.prototype.encodeBuffers = function( inputBuffer ){
if ( this.state === "recording" ) {
var buffers = [];
for ( var i = 0; i < inputBuffer.numberOfChannels; i++ ) {
buffers[i] = inputBuffer.getChannelData(i);
}
this.encoder.postMessage({
command: "encode",
buffers: buffers
});
}
};
Recorder.prototype.initStream = function(){
var that = this;
var onStreamInit = function( stream ){
that.stream = stream;
that.sourceNode = that.audioContext.createMediaStreamSource( stream );
that.sourceNode.connect( that.scriptProcessorNode );
that.sourceNode.connect( that.monitorNode );
that.eventTarget.dispatchEvent( new global.Event( "streamReady" ) );
return stream;
}
var onStreamError = function( e ){
that.eventTarget.dispatchEvent( new global.ErrorEvent( "streamError", { error: e } ) );
}
var constraints = { audio : this.config.streamOptions };
if ( this.stream ) {
this.eventTarget.dispatchEvent( new global.Event( "streamReady" ) );
return global.Promise.resolve( this.stream );
}
if ( global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia ) {
return global.navigator.mediaDevices.getUserMedia( constraints ).then( onStreamInit, onStreamError );
}
if ( global.navigator.getUserMedia ) {
return new global.Promise( function( resolve, reject ) {
global.navigator.getUserMedia( constraints, resolve, reject );
}).then( onStreamInit, onStreamError );
}
};
Recorder.prototype.pause = function(){
if ( this.state === "recording" ){
this.state = "paused";
this.eventTarget.dispatchEvent( new global.Event( 'pause' ) );
}
};
Recorder.prototype.removeEventListener = function( type, listener, useCapture ){
this.eventTarget.removeEventListener( type, listener, useCapture );
};
Recorder.prototype.resume = function() {
if ( this.state === "paused" ) {
this.state = "recording";
this.eventTarget.dispatchEvent( new global.Event( 'resume' ) );
}
};
Recorder.prototype.setMonitorGain = function( gain ){
this.monitorNode.gain.value = gain;
};
Recorder.prototype.start = function(){
if ( this.state === "inactive" && this.stream ) {
var that = this;
this.encoder = new global.Worker( this.config.encoderPath );
if (this.config.streamPages){
this.encoder.addEventListener( "message", function( e ) {
that.streamPage( e.data );
});
}
else {
this.recordedPages = [];
this.totalLength = 0;
this.encoder.addEventListener( "message", function( e ) {
that.storePage( e.data );
});
}
// First buffer can contain old data. Don't encode it.
this.encodeBuffers = function(){
delete this.encodeBuffers;
};
this.state = "recording";
this.monitorNode.connect( this.audioContext.destination );
this.scriptProcessorNode.connect( this.audioContext.destination );
this.eventTarget.dispatchEvent( new global.Event( 'start' ) );
this.encoder.postMessage( this.config );
}
};
Recorder.prototype.stop = function(){
if ( this.state !== "inactive" ) {
this.state = "inactive";
this.monitorNode.disconnect();
this.scriptProcessorNode.disconnect();
if ( !this.config.leaveStreamOpen ) {
this.clearStream();
}
this.encoder.postMessage({ command: "done" });
}
};
Recorder.prototype.storePage = function( page ) {
if ( page === null ) {
var outputData = new Uint8Array( this.totalLength );
var outputIndex = 0;
for ( var i = 0; i < this.recordedPages.length; i++ ) {
outputData.set( this.recordedPages[i], outputIndex );
outputIndex += this.recordedPages[i].length;
}
this.eventTarget.dispatchEvent( new global.CustomEvent( 'dataAvailable', {
detail: outputData
}));
this.recordedPages = [];
this.eventTarget.dispatchEvent( new global.Event( 'stop' ) );
}
else {
this.recordedPages.push( page );
this.totalLength += page.length;
}
};
Recorder.prototype.streamPage = function( page ) {
if ( page === null ) {
this.eventTarget.dispatchEvent( new global.Event( 'stop' ) );
}
else {
this.eventTarget.dispatchEvent( new global.CustomEvent( 'dataAvailable', {
detail: page
}));
}
};
// Exports
global.Recorder = Recorder;
if ( typeof define === 'function' && define.amd ) {
define( [], function() {
return Recorder;
});
}
else if ( typeof module == 'object' && module.exports ) {
module.exports = Recorder;
}
})(root);

1
app/vendor/recorderjs/recorder.min.js vendored

@ -0,0 +1 @@ @@ -0,0 +1 @@
"use strict";var root="object"==typeof self&&self.self===self&&self||"object"==typeof global&&global.global===global&&global||this;!function(e){var t=function(n){var i=this;if(!t.isRecordingSupported())throw new Error("Recording is not supported in this browser");this.state="inactive",this.eventTarget=e.document.createDocumentFragment(),this.audioContext=new e.AudioContext,this.monitorNode=this.audioContext.createGain(),this.config=n=n||{},this.config.command="init",this.config.bufferLength=n.bufferLength||4096,this.config.monitorGain=n.monitorGain||0,this.config.numberOfChannels=n.numberOfChannels||1,this.config.originalSampleRate=this.audioContext.sampleRate,this.config.encoderSampleRate=n.encoderSampleRate||48e3,this.config.encoderPath=n.encoderPath||"encoderWorker.min.js",this.config.streamPages=n.streamPages||!1,this.config.leaveStreamOpen=n.leaveStreamOpen||!1,this.config.maxBuffersPerPage=n.maxBuffersPerPage||40,this.config.encoderApplication=n.encoderApplication||2049,this.config.encoderFrameSize=n.encoderFrameSize||20,this.config.resampleQuality=n.resampleQuality||3,this.config.streamOptions=n.streamOptions||{optional:[],mandatory:{googEchoCancellation:!1,googAutoGainControl:!1,googNoiseSuppression:!1,googHighpassFilter:!1}},this.setMonitorGain(this.config.monitorGain),this.scriptProcessorNode=this.audioContext.createScriptProcessor(this.config.bufferLength,this.config.numberOfChannels,this.config.numberOfChannels),this.scriptProcessorNode.onaudioprocess=function(e){i.encodeBuffers(e.inputBuffer)}};t.isRecordingSupported=function(){return e.AudioContext&&e.navigator&&(e.navigator.getUserMedia||e.navigator.mediaDevices&&e.navigator.mediaDevices.getUserMedia)},t.prototype.addEventListener=function(e,t,n){this.eventTarget.addEventListener(e,t,n)},t.prototype.clearStream=function(){this.stream&&(this.stream.getTracks?this.stream.getTracks().forEach(function(e){e.stop()}):this.stream.stop(),delete this.stream)},t.prototype.encodeBuffers=function(e){if("recording"===this.state){for(var t=[],n=0;n<e.numberOfChannels;n++)t[n]=e.getChannelData(n);this.encoder.postMessage({command:"encode",buffers:t})}},t.prototype.initStream=function(){var t=this,n=function(n){return t.stream=n,t.sourceNode=t.audioContext.createMediaStreamSource(n),t.sourceNode.connect(t.scriptProcessorNode),t.sourceNode.connect(t.monitorNode),t.eventTarget.dispatchEvent(new e.Event("streamReady")),n},i=function(n){t.eventTarget.dispatchEvent(new e.ErrorEvent("streamError",{error:n}))},o={audio:this.config.streamOptions};return this.stream?(this.eventTarget.dispatchEvent(new e.Event("streamReady")),e.Promise.resolve(this.stream)):e.navigator.mediaDevices&&e.navigator.mediaDevices.getUserMedia?e.navigator.mediaDevices.getUserMedia(o).then(n,i):e.navigator.getUserMedia?new e.Promise(function(t,n){e.navigator.getUserMedia(o,t,n)}).then(n,i):void 0},t.prototype.pause=function(){"recording"===this.state&&(this.state="paused",this.eventTarget.dispatchEvent(new e.Event("pause")))},t.prototype.removeEventListener=function(e,t,n){this.eventTarget.removeEventListener(e,t,n)},t.prototype.resume=function(){"paused"===this.state&&(this.state="recording",this.eventTarget.dispatchEvent(new e.Event("resume")))},t.prototype.setMonitorGain=function(e){this.monitorNode.gain.value=e},t.prototype.start=function(){if("inactive"===this.state&&this.stream){var t=this;this.encoder=new e.Worker(this.config.encoderPath),this.config.streamPages?this.encoder.addEventListener("message",function(e){t.streamPage(e.data)}):(this.recordedPages=[],this.totalLength=0,this.encoder.addEventListener("message",function(e){t.storePage(e.data)})),this.encodeBuffers=function(){delete this.encodeBuffers},this.state="recording",this.monitorNode.connect(this.audioContext.destination),this.scriptProcessorNode.connect(this.audioContext.destination),this.eventTarget.dispatchEvent(new e.Event("start")),this.encoder.postMessage(this.config)}},t.prototype.stop=function(){"inactive"!==this.state&&(this.state="inactive",this.monitorNode.disconnect(),this.scriptProcessorNode.disconnect(),this.config.leaveStreamOpen||this.clearStream(),this.encoder.postMessage({command:"done"}))},t.prototype.storePage=function(t){if(null===t){for(var n=new Uint8Array(this.totalLength),i=0,o=0;o<this.recordedPages.length;o++)n.set(this.recordedPages[o],i),i+=this.recordedPages[o].length;this.eventTarget.dispatchEvent(new e.CustomEvent("dataAvailable",{detail:n})),this.recordedPages=[],this.eventTarget.dispatchEvent(new e.Event("stop"))}else this.recordedPages.push(t),this.totalLength+=t.length},t.prototype.streamPage=function(t){null===t?this.eventTarget.dispatchEvent(new e.Event("stop")):this.eventTarget.dispatchEvent(new e.CustomEvent("dataAvailable",{detail:t}))},e.Recorder=t,"function"==typeof define&&define.amd?define([],function(){return t}):"object"==typeof module&&module.exports&&(module.exports=t)}(root);

2
app/webogram.appcache

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
CACHE MANIFEST
# 75
# 76
NETWORK:
*

Loading…
Cancel
Save