From 9c7a238c2a04b53e1d8d6b8c4f6e1c0692b3c4ad Mon Sep 17 00:00:00 2001 From: laineyhm Date: Wed, 7 Sep 2022 14:10:06 +0700 Subject: [PATCH] working media recorder object Using Media-Recorder for recording. play/pause bug Fixed error with play/plause. Still debugging playback Another work-in-progress commit. Rearranged some Committing to save progress; debugging sound-player Playback is working, but NaN still shows up at the beginning sometimes Put the scope applications back in Audio source updates immediately. Playback and slider working. Tweaks to slider; Practicing uploads More tweaks to the slider Fix bug (remove out-of-place console log) With the added ffmpeg dependency in the php server, converts .ogg recordings to .wav (<6.25 seconds) or .mp3 (>6.25 seconds) for use with Flex cleaned up code Conversions for FLEx implemented; mp3/wav based on audio duration. Cleaned up comments. Fixed php function error. Removing unused experimental listeners, html attributes, else statements, and dependencies Small cleanup Adding @types/dom-mediacapture-record for MediaRecorder types, among other suggested edits Saving the recorded/uploaded file as well as the converted one. Allowing for FLAC and OGG uploads. Sanitizing shell args. Took out code that was removing duplicate files with different formats Sanitizing shell args in all places; making code Prettier Apply suggestions from code review Co-authored-by: Christopher Hirt --- docker/base-php/Dockerfile | 3 +- package-lock.json | 51 ++-- package.json | 3 +- .../Lexicon/Command/LexUploadCommands.php | 67 +++++- .../audio-recorder.component.ts | 226 ++++++++---------- .../shared/sound-player.component.html | 3 +- .../bellows/shared/sound-player.component.ts | 32 ++- .../editor/field/dc-audio.component.ts | 3 +- 8 files changed, 211 insertions(+), 177 deletions(-) diff --git a/docker/base-php/Dockerfile b/docker/base-php/Dockerfile index e209bdc4b3..e2d38b65c9 100644 --- a/docker/base-php/Dockerfile +++ b/docker/base-php/Dockerfile @@ -4,7 +4,8 @@ FROM php:7.3.28-apache # p7zip-full - used by LF application for unzipping lexicon uploads # unzip - used by LF application for unzipping lexicon uploads # curl - used by LF application -RUN apt-get update && apt-get -y install p7zip-full unzip curl tini && rm -rf /var/lib/apt/lists/* +# ffmpeg - used by LF audio upload method +RUN apt-get update && apt-get -y install p7zip-full unzip curl tini ffmpeg && rm -rf /var/lib/apt/lists/* # see https://github.com/mlocati/docker-php-extension-installer # PHP extensions required by the LF application diff --git a/package-lock.json b/package-lock.json index 830f37d133..7906c988a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "font-awesome": "^4.7.0", "intl-tel-input": "9.2.7", "jquery": "^3.6.0", - "lamejs": "^1.2.0", "localforage": "^1.7.1", "ng-drag-to-reorder": "^1.0.8", "ng-file-upload": "12.0.4", @@ -30,6 +29,7 @@ "npm-run-all": "^4.1.5", "oclazyload": "^1.1.0", "offline-js": "0.7.11", + "webm-fix-duration": "^1.0.1", "zxcvbn": "^4.4.2" }, "devDependencies": { @@ -43,6 +43,7 @@ "@types/bootstrap": "^3.3.35", "@types/core-js": "^0.9.42", "@types/deep-diff": "^1.0.1", + "@types/dom-mediacapture-record": "^1.0.11", "@types/intl-tel-input": "0.0.7", "@types/jasmine": "^2.8.4", "@types/jasminewd2": "^2.0.3", @@ -2156,6 +2157,12 @@ "integrity": "sha512-cZIq2GFcPmW0/M7dtLuphyoU8f3zpTcBgV+wkFFJ0CK0lwRVGGLaBSJZ98qs4LjtLimPq1Bb2VJnhGn6SEE4IA==", "dev": true }, + "node_modules/@types/dom-mediacapture-record": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.11.tgz", + "integrity": "sha512-ODVOH95x08arZhbQOjH3no7Iye64akdO+55nM+IGtTzpu2ACKr9CQTrI//CCVieIjlI/eL+rK1hQjMycxIgylQ==", + "dev": true + }, "node_modules/@types/eslint": { "version": "7.2.7", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.7.tgz", @@ -6121,14 +6128,6 @@ "immediate": "~3.0.5" } }, - "node_modules/lamejs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/lamejs/-/lamejs-1.2.0.tgz", - "integrity": "sha1-Aln4PbRmYUGntnG4yqY2nZUXfQg=", - "dependencies": { - "use-strict": "1.0.1" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -9910,11 +9909,6 @@ "node": ">=8.9.0" } }, - "node_modules/use-strict": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/use-strict/-/use-strict-1.0.1.tgz", - "integrity": "sha1-C7gNlPSaSgUZK4Sox9NOlfGn46A=" - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -10155,6 +10149,11 @@ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", "dev": true }, + "node_modules/webm-fix-duration": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/webm-fix-duration/-/webm-fix-duration-1.0.1.tgz", + "integrity": "sha512-KaL2yq5BQ1ngRlWVpwQ0bI8INW3ZMzrbPfz8L50GvZHhReCeG5+zKqzk4BcpK6vEdMRHSxoNHn/ixiDqVdKZTw==" + }, "node_modules/webpack": { "version": "5.27.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.27.1.tgz", @@ -12633,6 +12632,12 @@ "integrity": "sha512-cZIq2GFcPmW0/M7dtLuphyoU8f3zpTcBgV+wkFFJ0CK0lwRVGGLaBSJZ98qs4LjtLimPq1Bb2VJnhGn6SEE4IA==", "dev": true }, + "@types/dom-mediacapture-record": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.11.tgz", + "integrity": "sha512-ODVOH95x08arZhbQOjH3no7Iye64akdO+55nM+IGtTzpu2ACKr9CQTrI//CCVieIjlI/eL+rK1hQjMycxIgylQ==", + "dev": true + }, "@types/eslint": { "version": "7.2.7", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.7.tgz", @@ -15785,14 +15790,6 @@ } } }, - "lamejs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/lamejs/-/lamejs-1.2.0.tgz", - "integrity": "sha1-Aln4PbRmYUGntnG4yqY2nZUXfQg=", - "requires": { - "use-strict": "1.0.1" - } - }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -18713,11 +18710,6 @@ } } }, - "use-strict": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/use-strict/-/use-strict-1.0.1.tgz", - "integrity": "sha1-C7gNlPSaSgUZK4Sox9NOlfGn46A=" - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -18909,6 +18901,11 @@ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", "dev": true }, + "webm-fix-duration": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/webm-fix-duration/-/webm-fix-duration-1.0.1.tgz", + "integrity": "sha512-KaL2yq5BQ1ngRlWVpwQ0bI8INW3ZMzrbPfz8L50GvZHhReCeG5+zKqzk4BcpK6vEdMRHSxoNHn/ixiDqVdKZTw==" + }, "webpack": { "version": "5.27.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.27.1.tgz", diff --git a/package.json b/package.json index ed57f4e0f0..09e5039960 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "font-awesome": "^4.7.0", "intl-tel-input": "9.2.7", "jquery": "^3.6.0", - "lamejs": "^1.2.0", "localforage": "^1.7.1", "ng-drag-to-reorder": "^1.0.8", "ng-file-upload": "12.0.4", @@ -45,6 +44,7 @@ "npm-run-all": "^4.1.5", "oclazyload": "^1.1.0", "offline-js": "0.7.11", + "webm-fix-duration": "^1.0.1", "zxcvbn": "^4.4.2" }, "devDependencies": { @@ -58,6 +58,7 @@ "@types/bootstrap": "^3.3.35", "@types/core-js": "^0.9.42", "@types/deep-diff": "^1.0.1", + "@types/dom-mediacapture-record": "^1.0.11", "@types/intl-tel-input": "0.0.7", "@types/jasmine": "^2.8.4", "@types/jasminewd2": "^2.0.3", diff --git a/src/Api/Model/Languageforge/Lexicon/Command/LexUploadCommands.php b/src/Api/Model/Languageforge/Lexicon/Command/LexUploadCommands.php index e5ac7e68b6..6558796400 100644 --- a/src/Api/Model/Languageforge/Lexicon/Command/LexUploadCommands.php +++ b/src/Api/Model/Languageforge/Lexicon/Command/LexUploadCommands.php @@ -18,6 +18,7 @@ class LexUploadCommands ".lift" ); + /** * Upload an audio file * @@ -53,9 +54,11 @@ public static function uploadAudioFile($projectId, $mediaType, $tmpFilePath) $allowedTypes = array( "application/octet-stream", - // allow m4a audio uploads, which curiously has a mime type of video/mp4 + // allow m4a audio uploads, which curiously has a mime type of video/mp4, audio/m4a, audio/mp4, or audio/x-m4a "video/mp4", - + "audio/m4a", + "audio/mp4", + "audio/x-m4a", "audio/mpeg", "audio/x-mpeg", "audio/mp3", @@ -66,27 +69,70 @@ public static function uploadAudioFile($projectId, $mediaType, $tmpFilePath) "audio/x-mpg", "audio/x-mpegaudio", "audio/x-wav", - "audio/wav" + "audio/wav", + "audio/flac", + "audio/x-flac", + "audio/ogg", + "audio/webm", + // allow Google Chrome to handle MediaRecorder recordings as video/webm MimeType + "video/webm" ); $allowedExtensions = array( ".mp3", ".mpa", ".mpg", ".m4a", - ".wav" + ".wav", + ".ogg", + ".flac", + ".webm", ); $response = new UploadResponse(); + if (in_array(strtolower($fileType), $allowedTypes) && in_array(strtolower($fileExt), $allowedExtensions)) { // make the folders if they don't exist $project->createAssetsFolders(); $folderPath = $project->getAudioFolderPath(); - // move uploaded file from tmp location to assets + // move uploaded/recorded file from tmp location to assets $filePath = self::mediaFilePath($folderPath, $fileNamePrefix, $fileName); $moveOk = copy($tmpFilePath, $filePath); - @unlink($tmpFilePath); + + // convert audio file to mp3 or wav format if necessary + // FLEx only supports mp3 or wav format as of 2022-09 + + if (strcmp(strtolower($fileExt), ".mp3") !== 0 && strcmp(strtolower($fileExt), ".wav") !== 0) { + //First, find the duration of the file + $sanitizedTmpFilePath = escapeshellarg($tmpFilePath); + $ffprobeCommand = `ffprobe -i $sanitizedTmpFilePath -show_entries format=duration -v quiet -of csv="p=0" 2> /dev/null`; + $audioDuration = floatval($ffprobeCommand); + + // Convert to .wav if the result will be less than 1 MB at 165Kb/s (recording is shorter than 6 seconds) + // and .mp3 otherwise (recording is longer than 6 seconds) + $extensionlessTmpFilePath = substr($sanitizedTmpFilePath, 0, strrpos($sanitizedTmpFilePath, strtolower($fileExt))); + $extensionlessFileName = substr($fileName, 0, strrpos($fileName, strtolower($fileExt))); + + if($audioDuration < 6){ + `ffmpeg -i $sanitizedTmpFilePath -b:a 165K -maxrate 165K -bufsize 80K $extensionlessTmpFilePath.wav 2> /dev/null')`; + `mv $sanitizedTmpFilePath $extensionlessTmpFilePath.wav 2> /dev/null`; + $fileName = $extensionlessFileName . '.wav'; + $tmpFilePath = $extensionlessTmpFilePath. '.wav'; + } + else{ + `ffmpeg -i $sanitizedTmpFilePath -b:a 165K -maxrate 165K -bufsize 80K $extensionlessTmpFilePath.mp3 2> /dev/null')`; + `mv $sanitizedTmpFilePath $extensionlessTmpFilePath.mp3 2> /dev/null`; + $fileName = $extensionlessFileName . '.mp3'; + $tmpFilePath = $extensionlessTmpFilePath . '.mp3'; + } + + + // move uploaded file from tmp location to assets + $filePath = self::mediaFilePath($folderPath, $fileNamePrefix, $fileName); + $moveOk = copy($tmpFilePath, $filePath); + @unlink($tmpFilePath); + } // construct server response if ($moveOk && $tmpFilePath) { @@ -95,10 +141,11 @@ public static function uploadAudioFile($projectId, $mediaType, $tmpFilePath) $data->fileName = $fileNamePrefix . '_' . $fileName; $response->result = true; - if (array_key_exists('previousFilename', $_POST)) { - $previousFilename = $_POST['previousFilename']; - self::deleteMediaFile($projectId, $mediaType, $previousFilename); - } + //Uncomment to ensure that only one format for each audio file is stored in the assets. We want to keep up to two formats right now (09-2022): the original and if needed, a FLEx-compatible one + // if (array_key_exists('previousFilename', $_POST)) { + // $previousFilename = $_POST['previousFilename']; + // self::deleteMediaFile($projectId, $mediaType, $previousFilename); + // } } else { $data = new ErrorResult(); $data->errorType = 'UserMessage'; diff --git a/src/angular-app/bellows/shared/audio-recorder/audio-recorder.component.ts b/src/angular-app/bellows/shared/audio-recorder/audio-recorder.component.ts index d77ad9dd45..ac268dab40 100644 --- a/src/angular-app/bellows/shared/audio-recorder/audio-recorder.component.ts +++ b/src/angular-app/bellows/shared/audio-recorder/audio-recorder.component.ts @@ -1,25 +1,99 @@ -import * as angular from 'angular'; -import * as lamejs from 'lamejs'; - -declare var webkitAudioContext: { - new(): AudioContext; -}; +import { webmFixDuration } from "webm-fix-duration"; +import * as angular from "angular"; export class AudioRecorderController implements angular.IController { + static $inject = ["$interval", "$scope"]; - static $inject = ['$interval', '$scope']; - constructor(private $interval: angular.IIntervalService, private $scope: angular.IScope) {} - + mediaRecorder: MediaRecorder; + chunks: string[] = []; isRecording = false; hasRecorded = false; + recordingStartTime: Date; audioSrc: string; blob: Blob; recordingTime: string; errorMessage: string; - stopMediaStream: () => void; callback: (blob: Blob) => void; + durationInMilliseconds: number; interval: angular.IPromise; + constructor( + private $interval: angular.IIntervalService, + private $scope: angular.IScope + ) {} + + private startRecording() { + this.recordingTime = "0:00"; + + navigator.mediaDevices.getUserMedia({ audio: true }).then( + (stream) => { + this.mediaRecorder = new MediaRecorder(stream); + + this.$scope.$apply(() => { + this.hasRecorded = true; + this.errorMessage = null; + this.isRecording = true; + }); + + this.mediaRecorder.addEventListener( + "dataavailable", + async (e: { data: any }) => { + this.chunks.push(e.data); + var roughBlob = new Blob(this.chunks, { + type: "audio/webm; codecs=opus", + }); + //In some browsers (Chrome, Edge, ...) navigator.mediaDevices.getUserMedia with MediaRecorder creates WEBM files without duration metadata //2022-09 + //webmFixDuration appends missing duration metadata to a WEBM file blob. + this.blob = await webmFixDuration( + roughBlob, + this.durationInMilliseconds, + "audio/webm; codecs=opus" + ); + this.chunks = []; + this.audioSrc = window.URL.createObjectURL(this.blob); + this.$scope.$digest(); + } + ); + + this.recordingStartTime = new Date(); + + this.interval = this.$interval(() => { + const seconds = Math.floor( + (new Date().getTime() - this.recordingStartTime.getTime()) / 1000 + ); + this.recordingTime = + Math.floor(seconds / 60) + + ":" + + (seconds % 60 < 10 ? "0" : "") + + (seconds % 60); + }, 1000); + + this.mediaRecorder.start(); + }, + (err) => { + this.$scope.$apply(() => { + this.errorMessage = "Unable to record audio from your microphone."; + this.isRecording = false; + this.hasRecorded = false; + }); + + console.error(err); + } + ); + } + + private stopRecording() { + this.durationInMilliseconds = Math.floor( + new Date().getTime() - this.recordingStartTime.getTime() + ); + + this.mediaRecorder.stop(); + + if (this.interval) { + this.$interval.cancel(this.interval); + } + } + toggleRecording() { if (this.isRecording) this.stopRecording(); else this.startRecording(); @@ -27,7 +101,9 @@ export class AudioRecorderController implements angular.IController { } close() { - this.stopRecording(); + if (this.isRecording) { + this.stopRecording(); + } this.callback(null); } @@ -36,132 +112,26 @@ export class AudioRecorderController implements angular.IController { } recordingSupported() { - return navigator.mediaDevices && navigator.mediaDevices.enumerateDevices && navigator.mediaDevices.getUserMedia && - ((window as any).AudioContext || (window as any).webkitAudioContext); + return ( + navigator.mediaDevices && + navigator.mediaDevices.enumerateDevices && + navigator.mediaDevices.getUserMedia && + ((window as any).AudioContext || (window as any).webkitAudioContext) + ); } $onDestroy() { - this.stopRecording(); - } - - private startRecording() { - this.recordingTime = '0:00'; - - navigator.mediaDevices.getUserMedia({audio: true}).then(stream => { - - this.$scope.$apply(() => { - this.hasRecorded = true; - this.errorMessage = null; - this.isRecording = true; - }); - - const recordingStartTime = new Date(); - // webkit prefix required for Safari - const context = (window as any).AudioContext ? new AudioContext() : new webkitAudioContext(); - const bufferSize = 0; - const channels = 1; - const processor = context.createScriptProcessor(bufferSize, channels, channels); - context.createMediaStreamSource(stream).connect(processor); - processor.connect(context.destination); - const sampleRate = context.sampleRate; - const bitrate = 128; - const mp3Encoder = new MP3Encoder(channels, sampleRate, bitrate); - - function handleAudioData(event: AudioProcessingEvent) { - mp3Encoder.appendData(event.inputBuffer.getChannelData(0)); - } - processor.addEventListener('audioprocess', handleAudioData); - - mp3Encoder.onMp3Blob(blob => { - this.blob = blob; - this.audioSrc = URL.createObjectURL(blob); - }); - - this.interval = this.$interval(() => { - const seconds = Math.floor((new Date().getTime() - recordingStartTime.getTime()) / 1000); - this.recordingTime = Math.floor(seconds / 60) + ':' + (seconds % 60 < 10 ? '0' : '') + seconds % 60; - }, 1000); - - this.stopMediaStream = () => { - processor.removeEventListener('audioprocess', handleAudioData); - mp3Encoder.end(); - stream.getAudioTracks()[0].stop(); - }; - - }, err => { - this.$scope.$apply(() => { - this.errorMessage = 'Unable to record audio from your microphone.'; - this.isRecording = false; - this.hasRecorded = false; - }); - console.error(err); - }); - } - - private stopRecording() { - if (this.interval) this.$interval.cancel(this.interval); - if (this.stopMediaStream) this.stopMediaStream(); - } - -} - -export class MP3Encoder { - - constructor(private channels: number, private sampleRate: number, private bitrate: number) {} - - buffer: Float32Array[] = []; - mp3BlobListeners: Array<(data: Blob) => void> = []; - - lame = new lamejs.Mp3Encoder(this.channels, this.sampleRate, this.bitrate); - - appendData(data: Float32Array) { - // copy the data, because Chromium 66.0.3359.139 overwrites it (FF 60.0.1 does not) - this.buffer.push(data.slice(0, data.length)); - } - - end() { - // Flatten the buffer array while converting it from Float32 to Int16 - const pcmData = new Int16Array(this.buffer.length * this.buffer[0].length); - - // max and min 16-bit values - const max = 0x7FFF; - const min = 0x8000; - let index = 0; - for (let i = 0, len1 = this.buffer.length; i < len1; ++i) { - const chunk = this.buffer[i]; - for (let j = 0, len2 = chunk.length; j < len2; ++j) { - pcmData[index] = chunk[j] < 0 ? chunk[j] * min : chunk[j] * max; - ++index; - } - } - - const blockSize = 1152; - const mp3Data: Int8Array[] = []; - - let encoded: Int8Array; - for (let i = 0; i < pcmData.length; i += blockSize) { - const chunk = pcmData.subarray(i, i + blockSize); - // This line takes the most time of this function, about 90% - encoded = this.lame.encodeBuffer(chunk); - if (chunk.length > 0) mp3Data.push(new Int8Array(encoded)); + if (this.isRecording) { + this.stopRecording(); } - - encoded = this.lame.flush(); - if (encoded.length > 0) mp3Data.push(new Int8Array(encoded)); - const blob = new Blob(mp3Data, {type: 'audio/mp3'}); - - this.mp3BlobListeners.forEach(cb => cb(blob)); - } - - onMp3Blob(cb: (data: Blob) => void) { - this.mp3BlobListeners.push(cb); } } export const AudioRecorderComponent: angular.IComponentOptions = { bindings: { - callback: '<' // TODO probably change to > or <, not sure which + callback: "<", }, controller: AudioRecorderController, - templateUrl: '/angular-app/bellows/shared/audio-recorder/audio-recorder.component.html' + templateUrl: + "/angular-app/bellows/shared/audio-recorder/audio-recorder.component.html", }; diff --git a/src/angular-app/bellows/shared/sound-player.component.html b/src/angular-app/bellows/shared/sound-player.component.html index f20786653c..fd49deec1a 100644 --- a/src/angular-app/bellows/shared/sound-player.component.html +++ b/src/angular-app/bellows/shared/sound-player.component.html @@ -6,5 +6,6 @@ {{$ctrl.currentTime()}} / {{$ctrl.duration()}} - + + diff --git a/src/angular-app/bellows/shared/sound-player.component.ts b/src/angular-app/bellows/shared/sound-player.component.ts index 30b938f0d6..cdf1f3285d 100644 --- a/src/angular-app/bellows/shared/sound-player.component.ts +++ b/src/angular-app/bellows/shared/sound-player.component.ts @@ -4,15 +4,19 @@ export class SoundController implements angular.IController { puiUrl: string; audioElement = document.createElement('audio'); + playing = false; private isUserMovingSlider: boolean = false; private slider: HTMLInputElement; static $inject = ['$scope', '$element']; - constructor(private $scope: angular.IScope, private $element: angular.IRootElementService) { } + + constructor(private $scope: angular.IScope, private $element: angular.IRootElementService) {} $onInit(): void { + + this.slider = this.$element.find('.seek-slider').get(0) as HTMLInputElement; this.audioElement.addEventListener('ended', () => { @@ -21,13 +25,9 @@ export class SoundController implements angular.IController { this.togglePlayback(); } - this.audioElement.currentTime = 0; }); }); - this.audioElement.addEventListener('loadedmetadata', () => { - this.$scope.$digest(); - }); const previousFormattedTime: string = null; this.audioElement.addEventListener('timeupdate', () => { @@ -65,21 +65,37 @@ export class SoundController implements angular.IController { } $onDestroy(): void { - this.audioElement.pause(); + if (!this.audioElement.paused){ + this.audioElement.pause(); + } } iconClass(): string { return this.playing ? 'fa-pause' : 'fa-play'; } + + async playAudio() { + try{ + let loadedAudioPlayer = await this.audioElement.play(); + return loadedAudioPlayer; + } catch (e) { + + } + } + togglePlayback(): void { this.playing = !this.playing; if (this.playing) { - this.audioElement.play(); + this.audioElement.currentTime = 0; + this.playAudio(); } else { - this.audioElement.pause(); + if(!this.audioElement.paused){ + this.audioElement.pause(); + } } + } currentTimeInSeconds(): number { diff --git a/src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts b/src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts index 90e42f3b52..657cf1d233 100644 --- a/src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts +++ b/src/angular-app/languageforge/lexicon/editor/field/dc-audio.component.ts @@ -1,3 +1,4 @@ + import * as angular from 'angular'; import { format, addMinutes } from 'date-fns'; @@ -145,7 +146,7 @@ export class FieldAudioController implements angular.IController { audioRecorderCallback = (blob: Blob) => { if (blob) { const date = new Date(); - const fileName = 'recording_' + format(addMinutes(date, date.getTimezoneOffset()), 'yyyy_MM_dd_HH_mm_ss') + '.mp3'; + const fileName = 'recording_' + format(addMinutes(date, date.getTimezoneOffset()), 'yyyy_MM_dd_HH_mm_ss') + '.webm'; const file = new File([blob], fileName); this.uploadAudio(file); }