From 0208b9cae28cc4f1d3b061d7838d9be0e924099b Mon Sep 17 00:00:00 2001 From: "tai.letan" Date: Fri, 21 Jun 2024 09:22:17 +0700 Subject: [PATCH] MDL-81111 tiny_recordrtc: add 'Pause' button for recording A/V --- .../recordrtc/amd/build/base_recorder.min.js | 2 +- .../amd/build/base_recorder.min.js.map | 2 +- .../recordrtc/amd/build/options.min.js | 4 +- .../recordrtc/amd/build/options.min.js.map | 2 +- .../amd/build/screen_recorder.min.js | 2 +- .../amd/build/screen_recorder.min.js.map | 2 +- .../recordrtc/amd/src/base_recorder.js | 164 ++++++++++++++++-- .../tiny/plugins/recordrtc/amd/src/options.js | 14 ++ .../recordrtc/amd/src/screen_recorder.js | 4 +- .../plugins/recordrtc/classes/plugininfo.php | 2 + .../recordrtc/lang/en/tiny_recordrtc.php | 3 + .../tiny/plugins/recordrtc/settings.php | 10 ++ .../templates/audio_recorder.mustache | 8 +- .../templates/screen_recorder.mustache | 8 +- .../templates/video_recorder.mustache | 8 +- lib/editor/tiny/plugins/recordrtc/version.php | 2 +- 16 files changed, 210 insertions(+), 27 deletions(-) diff --git a/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js b/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js index 08586307376..a08c57003f0 100644 --- a/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js +++ b/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js @@ -1,3 +1,3 @@ -define("tiny_recordrtc/base_recorder",["exports","core/str","./common","core/pending","./options","editor_tiny/uploader","core/toast","core/modal_events","core/templates","core/notification","core/prefetch","core/local/modal/alert"],(function(_exports,_str,_common,_pending,_options,_uploader,_toast,ModalEvents,Templates,_notification,_prefetch,_alert){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_pending=_interopRequireDefault(_pending),_uploader=_interopRequireDefault(_uploader),ModalEvents=_interopRequireWildcard(ModalEvents),Templates=_interopRequireWildcard(Templates),_alert=_interopRequireDefault(_alert);return _exports.default=class{constructor(editor,modal){var obj,key,value;value=!1,(key="stopRequested")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,this.ready=!1,this.checkAndWarnAboutBrowserCompatibility()&&(this.editor=editor,this.config=(0,_options.getData)(editor).params,this.modal=modal,this.modalRoot=modal.getRoot()[0],this.startStopButton=this.modalRoot.querySelector('button[data-action="startstop"]'),this.uploadButton=this.modalRoot.querySelector('button[data-action="upload"]'),this.setRecordButtonState(!1),this.player=this.configurePlayer(),this.registerEventListeners(),this.ready=!0,this.captureUserMedia(),this.prefetchContent())}isReady(){return this.ready}configurePlayer(){throw new Error("configurePlayer() must be implemented in ".concat(this.constructor.name))}getSupportedTypes(){throw new Error("getSupportedTypes() must be implemented in ".concat(this.constructor.name))}getRecordingOptions(){throw new Error("getRecordingOptions() must be implemented in ".concat(this.constructor.name))}getFileName(prefix){throw new Error("getFileName() must be implemented in ".concat(this.constructor.name))}getMediaConstraints(){throw new Error("getMediaConstraints() must be implemented in ".concat(this.constructor.name))}playOnCapture(){return!1}getTimeLimit(){throw new Error("getTimeLimit() must be implemented in ".concat(this.constructor.name))}getEmbedTemplateName(){throw new Error("getEmbedTemplateName() must be implemented in ".concat(this.constructor.name))}static getModalClass(){throw new Error("getModalClass() must be implemented in ".concat(this.constructor.name))}getParsedRecordingOptions(){const compatTypes=this.getSupportedTypes().reduce(((result,type)=>(result.push(type),result.push(type.replace("=",":")),result)),[]).filter((type=>window.MediaRecorder.isTypeSupported(type))),options=this.getRecordingOptions();return 0!==compatTypes.length&&(options.mimeType=compatTypes[0]),window.console.info("Selected codec ".concat(options.mimeType," from ").concat(compatTypes.length," options."),compatTypes),options}async captureUserMedia(){try{const stream=await navigator.mediaDevices.getUserMedia(this.getMediaConstraints());this.handleCaptureSuccess(stream)}catch(error){this.handleCaptureFailure(error)}}prefetchContent(){(0,_prefetch.prefetchStrings)(_common.component,["uploading","recordagain_title","recordagain_desc","discard_title","discard_desc","confirm_yes","recordinguploaded","maxfilesizehit","maxfilesizehit_title","uploadfailed"]),(0,_prefetch.prefetchTemplates)([this.getEmbedTemplateName(),"tiny_recordrtc/timeremaining"])}async displayAlert(title,content){const pendingPromise=new _pending.default("core/confirm:alert"),modal=await _alert.default.create({title:title,body:content,removeOnClose:!0});return modal.show(),pendingPromise.resolve(),modal}handleCaptureSuccess(stream){this.player.srcObject=stream,this.playOnCapture()&&(this.player.muted=!0,this.player.play()),this.stream=stream,this.setupPlayerSource(),this.setRecordButtonState(!0)}setupPlayerSource(){this.player.srcObject||(this.player.srcObject=this.stream,this.player.muted=!0,this.player.play())}setRecordButtonState(enabled){this.startStopButton.disabled=!enabled}setRecordButtonVisibility(visible){this.getButtonContainer("start-stop").classList.toggle("hide",!visible)}setUploadButtonState(enabled){this.uploadButton.disabled=!enabled}setUploadButtonVisibility(visible){this.getButtonContainer("upload").classList.toggle("hide",!visible)}handleCaptureFailure(error){var subject="gum".concat(error.name.replace("Error","").toLowerCase());this.displayAlert((0,_str.getString)("".concat(subject,"_title"),_common.component),(0,_str.getString)(subject,_common.component))}close(){this.modal.hide()}registerEventListeners(){this.modalRoot.addEventListener("click",this.handleModalClick.bind(this)),this.modal.getRoot().on(ModalEvents.outsideClick,this.outsideClickHandler.bind(this)),this.modal.getRoot().on(ModalEvents.hidden,(()=>{this.cleanupStream(),this.requestRecordingStop()}))}async outsideClickHandler(event){if(this.isRecording())event.preventDefault();else if(this.hasData()){event.preventDefault();try{await(0,_notification.saveCancelPromise)(await(0,_str.getString)("discard_title",_common.component),await(0,_str.getString)("discard_desc",_common.component),await(0,_str.getString)("confirm_yes",_common.component)),this.modal.hide()}catch(error){}}}handleModalClick(event){const button=event.target.closest("button");if(button&&button.dataset.action){const action=button.dataset.action;"startstop"===action&&this.handleRecordingStartStopRequested(),"upload"===action&&this.uploadRecording()}}handleRecordingStartStopRequested(){var _this$mediaRecorder;"recording"===(null===(_this$mediaRecorder=this.mediaRecorder)||void 0===_this$mediaRecorder?void 0:_this$mediaRecorder.state)?this.requestRecordingStop():this.startRecording()}async onMediaStopped(){var _this$getButtonContai;this.blob=new Blob(this.data.chunks,{type:this.mediaRecorder.mimeType}),this.player.srcObject=null,this.player.src=URL.createObjectURL(this.blob),this.setRecordButtonTextFromString("recordagain"),this.player.muted=!1,this.player.controls=!0,null===(_this$getButtonContai=this.getButtonContainer("player"))||void 0===_this$getButtonContai||_this$getButtonContai.classList.toggle("hide",!1),this.setUploadButtonVisibility(!0),this.setUploadButtonState(!0)}async uploadRecording(){if(0===this.data.chunks.length)return void this.displayAlert("norecordingfound");const fileName=this.getFileName((1e3*Math.random()).toString().replace(".",""));try{this.setRecordButtonVisibility(!1),this.setUploadButtonState(!1);const fileURL=await(0,_uploader.default)(this.editor,"media",this.blob,fileName,(progress=>{this.setUploadButtonTextProgress(progress)}));this.insertMedia(fileURL),this.close(),(0,_toast.add)(await(0,_str.getString)("recordinguploaded",_common.component))}catch(error){this.setUploadButtonState(!0),(0,_toast.add)(await(0,_str.getString)("uploadfailed",_common.component,{error:error}),{type:"error"})}}getButtonContainer(purpose){return this.modalRoot.querySelector('[data-purpose="'.concat(purpose,'-container"]'))}static isBrowserCompatible(){return this.checkSecure()&&this.hasUserMedia()}static async display(editor){const ModalClass=this.getModalClass(),modal=await ModalClass.create({templateContext:{},large:!0,removeOnClose:!0});return new this(editor,modal).isReady()&&modal.show(),modal}checkAndWarnAboutBrowserCompatibility(){return this.constructor.checkSecure()?!!this.constructor.hasUserMedia||((0,_str.getStrings)(["nowebrtc_title","nowebrtc"].map((key=>({key:key,component:_common.component})))).then((_ref2=>{let[title,message]=_ref2;return(0,_toast.add)(message,{title:title,type:"error"})})).catch(),!1):((0,_str.getStrings)(["insecurealert_title","insecurealert"].map((key=>({key:key,component:_common.component})))).then((_ref=>{let[title,message]=_ref;return(0,_toast.add)(message,{title:title,type:"error"})})).catch(),!1)}static hasUserMedia(){return navigator.mediaDevices&&window.MediaRecorder}static checkSecure(){return window.isSecureContext}async setStopRecordingButton(){const{html:html,js:js}=await Templates.renderForPromise("tiny_recordrtc/timeremaining",this.getTimeRemaining());Templates.replaceNodeContents(this.startStopButton,html,js),this.buttonTimer=setInterval(this.updateRecordButtonTime.bind(this),500)}updateRecordButtonTime(){const{remaining:remaining,minutes:minutes,seconds:seconds}=this.getTimeRemaining();remaining<0?this.requestRecordingStop():(this.startStopButton.querySelector('[data-type="minutes"]').textContent=minutes,this.startStopButton.querySelector('[data-type="seconds"]').textContent=seconds)}async setRecordButtonTextFromString(string){this.startStopButton.textContent=await(0,_str.getString)(string,_common.component)}async setUploadButtonTextProgress(progress){this.uploadButton.textContent=await(0,_str.getString)("uploading",_common.component,{progress:Math.round(100*progress)/100})}async resetUploadButtonText(){this.uploadButton.textContent=await(0,_str.getString)("upload",_common.component)}clearButtonTimer(){this.buttonTimer&&clearInterval(this.buttonTimer),this.buttonTimer=null}getTimeRemaining(){const now=(new Date).getTime(),remaining=Math.floor(this.getTimeLimit()-(now-this.startTime)/1e3),formatter=new Intl.NumberFormat(navigator.language,{minimumIntegerDigits:2}),seconds=formatter.format(remaining%60);return{remaining:remaining,minutes:formatter.format(Math.floor((remaining-seconds)/60)),seconds:seconds}}getMaxUploadSize(){return this.config.maxrecsize}requestRecordingStop(){this.mediaRecorder&&"inactive"!==this.mediaRecorder.state?this.stopRequested=!0:this.cleanupStream()}stopRecorder(){this.mediaRecorder.stop(),this.player.muted=!1}cleanupStream(){this.stream&&this.stream.getTracks().filter((track=>"ended"!==track.readyState)).forEach((track=>track.stop()))}handleStopped(){this.onMediaStopped(),this.clearButtonTimer()}handleStarted(){this.startTime=(new Date).getTime(),this.setStopRecordingButton()}handleDataAvailable(event){if(this.isRecording()){const newSize=this.data.blobSize+event.data.size;newSize>=this.getMaxUploadSize()?(this.stopRecorder(),this.displayFileLimitHitMessage()):(this.data.chunks.push(event.data),this.data.blobSize=newSize,this.stopRequested&&this.stopRecorder())}}async displayFileLimitHitMessage(){(0,_toast.add)(await(0,_str.getString)("maxfilesizehit",_common.component),{title:await(0,_str.getString)("maxfilesizehit_title",_common.component),type:"error"})}isRecording(){var _this$mediaRecorder2;return"recording"===(null===(_this$mediaRecorder2=this.mediaRecorder)||void 0===_this$mediaRecorder2?void 0:_this$mediaRecorder2.state)}hasData(){var _this$data;return!(null===(_this$data=this.data)||void 0===_this$data||!_this$data.blobSize)}async startRecording(){if(this.mediaRecorder){if(this.isRecording()&&this.mediaRecorder.stop(),this.hasData()){if(!await this.recordAgainConfirmation())return;this.setUploadButtonVisibility(!1),this.stream.active||await this.captureUserMedia()}this.mediaRecorder=null}this.mediaRecorder=new MediaRecorder(this.stream,this.getParsedRecordingOptions()),this.mediaRecorder.addEventListener("dataavailable",this.handleDataAvailable.bind(this)),this.mediaRecorder.addEventListener("stop",this.handleStopped.bind(this)),this.mediaRecorder.addEventListener("start",this.handleStarted.bind(this)),this.data={chunks:[],blobSize:0},this.setupPlayerSource(),this.stopRequested=!1,this.mediaRecorder.start(50)}async recordAgainConfirmation(){try{return await(0,_notification.saveCancelPromise)(await(0,_str.getString)("recordagain_title",_common.component),await(0,_str.getString)("recordagain_desc",_common.component),await(0,_str.getString)("confirm_yes",_common.component)),!0}catch{return!1}}async insertMedia(source){const{html:html}=await Templates.renderForPromise(this.getEmbedTemplateName(),this.getEmbedTemplateContext({source:source}));this.editor.insertContent(html)}getEmbedTemplateContext(templateContext){return templateContext}},_exports.default})); +define("tiny_recordrtc/base_recorder",["exports","core/str","./common","core/pending","./options","editor_tiny/uploader","core/toast","core/modal_events","core/templates","core/notification","core/prefetch","core/local/modal/alert"],(function(_exports,_str,_common,_pending,_options,_uploader,_toast,ModalEvents,Templates,_notification,_prefetch,_alert){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_pending=_interopRequireDefault(_pending),_uploader=_interopRequireDefault(_uploader),ModalEvents=_interopRequireWildcard(ModalEvents),Templates=_interopRequireWildcard(Templates),_alert=_interopRequireDefault(_alert);return _exports.default=class{constructor(editor,modal){_defineProperty(this,"stopRequested",!1),_defineProperty(this,"buttonTimer",null),_defineProperty(this,"pauseTime",null),_defineProperty(this,"startTime",null),this.ready=!1,this.checkAndWarnAboutBrowserCompatibility()&&(this.editor=editor,this.config=(0,_options.getData)(editor).params,this.modal=modal,this.modalRoot=modal.getRoot()[0],this.startStopButton=this.modalRoot.querySelector('button[data-action="startstop"]'),this.uploadButton=this.modalRoot.querySelector('button[data-action="upload"]'),this.pauseResumeButton=this.modalRoot.querySelector('button[data-action="pauseresume"]'),this.setRecordButtonState(!1),this.player=this.configurePlayer(),this.registerEventListeners(),this.ready=!0,this.captureUserMedia(),this.prefetchContent())}isReady(){return this.ready}configurePlayer(){throw new Error("configurePlayer() must be implemented in ".concat(this.constructor.name))}getSupportedTypes(){throw new Error("getSupportedTypes() must be implemented in ".concat(this.constructor.name))}getRecordingOptions(){throw new Error("getRecordingOptions() must be implemented in ".concat(this.constructor.name))}getFileName(prefix){throw new Error("getFileName() must be implemented in ".concat(this.constructor.name))}getMediaConstraints(){throw new Error("getMediaConstraints() must be implemented in ".concat(this.constructor.name))}playOnCapture(){return!1}getTimeLimit(){throw new Error("getTimeLimit() must be implemented in ".concat(this.constructor.name))}getEmbedTemplateName(){throw new Error("getEmbedTemplateName() must be implemented in ".concat(this.constructor.name))}static getModalClass(){throw new Error("getModalClass() must be implemented in ".concat(this.constructor.name))}getParsedRecordingOptions(){const compatTypes=this.getSupportedTypes().reduce(((result,type)=>(result.push(type),result.push(type.replace("=",":")),result)),[]).filter((type=>window.MediaRecorder.isTypeSupported(type))),options=this.getRecordingOptions();return 0!==compatTypes.length&&(options.mimeType=compatTypes[0]),window.console.info("Selected codec ".concat(options.mimeType," from ").concat(compatTypes.length," options."),compatTypes),options}async captureUserMedia(){try{const stream=await navigator.mediaDevices.getUserMedia(this.getMediaConstraints());this.handleCaptureSuccess(stream)}catch(error){this.handleCaptureFailure(error)}}prefetchContent(){(0,_prefetch.prefetchStrings)(_common.component,["uploading","recordagain_title","recordagain_desc","discard_title","discard_desc","confirm_yes","recordinguploaded","maxfilesizehit","maxfilesizehit_title","uploadfailed","pause","resume"]),(0,_prefetch.prefetchTemplates)([this.getEmbedTemplateName(),"tiny_recordrtc/timeremaining"])}async displayAlert(title,content){const pendingPromise=new _pending.default("core/confirm:alert"),modal=await _alert.default.create({title:title,body:content,removeOnClose:!0});return modal.show(),pendingPromise.resolve(),modal}handleCaptureSuccess(stream){this.player.srcObject=stream,this.playOnCapture()&&(this.player.muted=!0,this.player.play()),this.stream=stream,this.setupPlayerSource(),this.setRecordButtonState(!0)}setupPlayerSource(){this.player.srcObject||(this.player.srcObject=this.stream,this.player.muted=!0,this.player.play())}setRecordButtonState(enabled){this.startStopButton.disabled=!enabled}setRecordButtonVisibility(visible){this.getButtonContainer("start-stop").classList.toggle("hide",!visible)}setPauseButtonVisibility(visible){this.pauseResumeButton&&this.pauseResumeButton.classList.toggle("hidden",!visible)}setUploadButtonState(enabled){this.uploadButton.disabled=!enabled}setUploadButtonVisibility(visible){this.getButtonContainer("upload").classList.toggle("hide",!visible)}setPlayerState(state){var _this$getButtonContai;this.player.muted=!state,this.player.controls=state,null===(_this$getButtonContai=this.getButtonContainer("player"))||void 0===_this$getButtonContai||_this$getButtonContai.classList.toggle("hide",!state)}handleCaptureFailure(error){var subject="gum".concat(error.name.replace("Error","").toLowerCase());this.displayAlert((0,_str.getString)("".concat(subject,"_title"),_common.component),(0,_str.getString)(subject,_common.component))}close(){this.modal.hide()}registerEventListeners(){this.modalRoot.addEventListener("click",this.handleModalClick.bind(this)),this.modal.getRoot().on(ModalEvents.outsideClick,this.outsideClickHandler.bind(this)),this.modal.getRoot().on(ModalEvents.hidden,(()=>{this.cleanupStream(),this.requestRecordingStop()}))}async outsideClickHandler(event){if(this.isRecording()||this.isPaused())event.preventDefault();else if(this.hasData()){event.preventDefault();try{await(0,_notification.saveCancelPromise)(await(0,_str.getString)("discard_title",_common.component),await(0,_str.getString)("discard_desc",_common.component),await(0,_str.getString)("confirm_yes",_common.component)),this.modal.hide()}catch(error){}}}handleModalClick(event){const button=event.target.closest("button");if(button&&button.dataset.action){const action=button.dataset.action;"startstop"===action&&this.handleRecordingStartStopRequested(),"upload"===action&&this.uploadRecording(),"pauseresume"===action&&this.handleRecordingPauseResumeRequested()}}handleRecordingStartStopRequested(){this.isRecording()||this.isPaused()?this.requestRecordingStop():this.startRecording()}handleRecordingPauseResumeRequested(){this.isRecording()?this.mediaRecorder.pause():this.isPaused()&&this.mediaRecorder.resume()}async onMediaStopped(){this.blob=new Blob(this.data.chunks,{type:this.mediaRecorder.mimeType}),this.player.srcObject=null,this.player.src=URL.createObjectURL(this.blob),this.setRecordButtonTextFromString("recordagain"),this.setUploadButtonVisibility(!0),this.setPlayerState(!0),this.setUploadButtonState(!0),this.setPauseButtonVisibility(!1),"inactive"===this.mediaRecorder.state&&this.setPauseButtonTextFromString("pause")}async uploadRecording(){if(0===this.data.chunks.length)return void this.displayAlert("norecordingfound");const fileName=this.getFileName((1e3*Math.random()).toString().replace(".",""));try{this.setRecordButtonVisibility(!1),this.setUploadButtonState(!1);const fileURL=await(0,_uploader.default)(this.editor,"media",this.blob,fileName,(progress=>{this.setUploadButtonTextProgress(progress)}));this.insertMedia(fileURL),this.close(),(0,_toast.add)(await(0,_str.getString)("recordinguploaded",_common.component))}catch(error){this.setUploadButtonState(!0),(0,_toast.add)(await(0,_str.getString)("uploadfailed",_common.component,{error:error}),{type:"error"})}}getButtonContainer(purpose){return this.modalRoot.querySelector('[data-purpose="'.concat(purpose,'-container"]'))}static isBrowserCompatible(){return this.checkSecure()&&this.hasUserMedia()}static async display(editor){const ModalClass=this.getModalClass(),modal=await ModalClass.create({templateContext:{isallowedpausing:(0,_options.isPausingAllowed)(editor)},large:!0,removeOnClose:!0});return new this(editor,modal).isReady()&&modal.show(),modal}checkAndWarnAboutBrowserCompatibility(){return this.constructor.checkSecure()?!!this.constructor.hasUserMedia||((0,_str.getStrings)(["nowebrtc_title","nowebrtc"].map((key=>({key:key,component:_common.component})))).then((_ref2=>{let[title,message]=_ref2;return(0,_toast.add)(message,{title:title,type:"error"})})).catch(),!1):((0,_str.getStrings)(["insecurealert_title","insecurealert"].map((key=>({key:key,component:_common.component})))).then((_ref=>{let[title,message]=_ref;return(0,_toast.add)(message,{title:title,type:"error"})})).catch(),!1)}static hasUserMedia(){return navigator.mediaDevices&&window.MediaRecorder}static checkSecure(){return window.isSecureContext}async setStopRecordingButton(){const{html:html,js:js}=await Templates.renderForPromise("tiny_recordrtc/timeremaining",this.getTimeRemaining());Templates.replaceNodeContents(this.startStopButton,html,js),this.startButtonTimer()}updateRecordButtonTime(){const{remaining:remaining,minutes:minutes,seconds:seconds}=this.getTimeRemaining();remaining<0?this.requestRecordingStop():(this.startStopButton.querySelector('[data-type="minutes"]').textContent=minutes,this.startStopButton.querySelector('[data-type="seconds"]').textContent=seconds)}async setRecordButtonTextFromString(string){this.startStopButton.textContent=await(0,_str.getString)(string,_common.component)}async setPauseButtonTextFromString(string){this.pauseResumeButton&&(this.pauseResumeButton.textContent=await(0,_str.getString)(string,_common.component))}async setUploadButtonTextProgress(progress){this.uploadButton.textContent=await(0,_str.getString)("uploading",_common.component,{progress:Math.round(100*progress)/100})}async resetUploadButtonText(){this.uploadButton.textContent=await(0,_str.getString)("upload",_common.component)}clearButtonTimer(){this.buttonTimer&&clearInterval(this.buttonTimer),this.buttonTimer=null,this.pauseTime=null,this.startTime=null}pauseButtonTimer(){this.pauseTime=(new Date).getTime(),this.buttonTimer&&clearInterval(this.buttonTimer)}startButtonTimer(){if(null!==this.pauseTime){const pauseDuration=(new Date).getTime()-this.pauseTime;this.startTime+=pauseDuration,this.pauseTime=null}this.buttonTimer=setInterval(this.updateRecordButtonTime.bind(this),500)}getTimeRemaining(){let now=(new Date).getTime();null!==this.pauseTime&&(now=this.pauseTime);const remaining=Math.floor(this.getTimeLimit()-(now-this.startTime)/1e3),formatter=new Intl.NumberFormat(navigator.language,{minimumIntegerDigits:2}),seconds=formatter.format(remaining%60);return{remaining:remaining,minutes:formatter.format(Math.floor((remaining-seconds)/60)),seconds:seconds}}getMaxUploadSize(){return this.config.maxrecsize}requestRecordingStop(){this.mediaRecorder&&"inactive"!==this.mediaRecorder.state?(this.stopRequested=!0,this.isPaused()&&this.stopRecorder()):this.cleanupStream()}stopRecorder(){this.isPaused()&&(this.pauseTime=null),this.mediaRecorder.stop(),this.player.muted=!1}cleanupStream(){this.stream&&this.stream.getTracks().filter((track=>"ended"!==track.readyState)).forEach((track=>track.stop()))}handleStopped(){this.onMediaStopped(),this.clearButtonTimer()}handleStarted(){this.startTime=(new Date).getTime(),(0,_options.isPausingAllowed)(this.editor)&&!this.isPaused()&&this.setPauseButtonVisibility(!0),this.setStopRecordingButton()}handlePaused(){this.pauseButtonTimer(),this.setPauseButtonTextFromString("resume")}handleResume(){this.startButtonTimer(),this.setPauseButtonTextFromString("pause")}handleDataAvailable(event){if(this.isRecording()||this.isPaused()){const newSize=this.data.blobSize+event.data.size;newSize>=this.getMaxUploadSize()?(this.stopRecorder(),this.displayFileLimitHitMessage()):(this.data.chunks.push(event.data),this.data.blobSize=newSize,this.stopRequested&&this.stopRecorder())}}async displayFileLimitHitMessage(){(0,_toast.add)(await(0,_str.getString)("maxfilesizehit",_common.component),{title:await(0,_str.getString)("maxfilesizehit_title",_common.component),type:"error"})}isRecording(){var _this$mediaRecorder;return"recording"===(null===(_this$mediaRecorder=this.mediaRecorder)||void 0===_this$mediaRecorder?void 0:_this$mediaRecorder.state)}isPaused(){var _this$mediaRecorder2;return"paused"===(null===(_this$mediaRecorder2=this.mediaRecorder)||void 0===_this$mediaRecorder2?void 0:_this$mediaRecorder2.state)}hasData(){var _this$data;return!(null===(_this$data=this.data)||void 0===_this$data||!_this$data.blobSize)}async startRecording(){if(this.mediaRecorder){if((this.isRecording()||this.isPaused())&&this.mediaRecorder.stop(),this.hasData()){if(!await this.recordAgainConfirmation())return;this.setUploadButtonVisibility(!1),this.setPlayerState(!1),this.stream.active||await this.captureUserMedia()}this.mediaRecorder=null}this.mediaRecorder=new MediaRecorder(this.stream,this.getParsedRecordingOptions()),this.mediaRecorder.addEventListener("dataavailable",this.handleDataAvailable.bind(this)),this.mediaRecorder.addEventListener("stop",this.handleStopped.bind(this)),this.mediaRecorder.addEventListener("start",this.handleStarted.bind(this)),this.mediaRecorder.addEventListener("pause",this.handlePaused.bind(this)),this.mediaRecorder.addEventListener("resume",this.handleResume.bind(this)),this.data={chunks:[],blobSize:0},this.setupPlayerSource(),this.stopRequested=!1,this.mediaRecorder.start(50)}async recordAgainConfirmation(){try{return await(0,_notification.saveCancelPromise)(await(0,_str.getString)("recordagain_title",_common.component),await(0,_str.getString)("recordagain_desc",_common.component),await(0,_str.getString)("confirm_yes",_common.component)),!0}catch{return!1}}async insertMedia(source){const{html:html}=await Templates.renderForPromise(this.getEmbedTemplateName(),this.getEmbedTemplateContext({source:source}));this.editor.insertContent(html)}getEmbedTemplateContext(templateContext){return templateContext}},_exports.default})); //# sourceMappingURL=base_recorder.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js.map b/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js.map index a9dbd3cebb0..9eb02d83955 100644 --- a/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js.map +++ b/lib/editor/tiny/plugins/recordrtc/amd/build/base_recorder.min.js.map @@ -1 +1 @@ -{"version":3,"file":"base_recorder.min.js","sources":["../src/base_recorder.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n//\n\n/**\n * Tiny Record RTC type.\n *\n * @module tiny_recordrtc/base_recorder\n * @copyright 2022 Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getString, getStrings} from 'core/str';\nimport {component} from './common';\nimport Pending from 'core/pending';\nimport {getData} from './options';\nimport uploadFile from 'editor_tiny/uploader';\nimport {add as addToast} from 'core/toast';\nimport * as ModalEvents from 'core/modal_events';\nimport * as Templates from 'core/templates';\nimport {saveCancelPromise} from 'core/notification';\nimport {prefetchStrings, prefetchTemplates} from 'core/prefetch';\nimport AlertModal from 'core/local/modal/alert';\n\n/**\n * The RecordRTC base class for audio, video, and any other future types\n */\nexport default class {\n\n stopRequested = false;\n\n /**\n * Constructor for the RecordRTC class\n *\n * @param {TinyMCE} editor The Editor to which the content will be inserted\n * @param {Modal} modal The Moodle Modal that contains the interface used for recording\n */\n constructor(editor, modal) {\n this.ready = false;\n\n if (!this.checkAndWarnAboutBrowserCompatibility()) {\n return;\n }\n\n this.editor = editor;\n this.config = getData(editor).params;\n this.modal = modal;\n this.modalRoot = modal.getRoot()[0];\n this.startStopButton = this.modalRoot.querySelector('button[data-action=\"startstop\"]');\n this.uploadButton = this.modalRoot.querySelector('button[data-action=\"upload\"]');\n\n // Disable the record button untilt he stream is acquired.\n this.setRecordButtonState(false);\n\n this.player = this.configurePlayer();\n this.registerEventListeners();\n this.ready = true;\n\n this.captureUserMedia();\n this.prefetchContent();\n }\n\n /**\n * Check whether the browser is compatible.\n *\n * @returns {boolean}\n */\n isReady() {\n return this.ready;\n }\n\n // Disable eslint's valid-jsdoc rule as the following methods are abstract and mnust be overridden by the child class.\n\n /* eslint-disable valid-jsdoc, no-unused-vars */\n\n /**\n * Get the Player element for this type.\n *\n * @returns {HTMLElement} The player element, typically an audio or video tag.\n */\n configurePlayer() {\n throw new Error(`configurePlayer() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get the list of supported mimetypes for this recorder.\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/isTypeSupported}\n *\n * @returns {string[]} The list of supported mimetypes.\n */\n getSupportedTypes() {\n throw new Error(`getSupportedTypes() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get any recording options passed into the MediaRecorder.\n * Please note that the mimeType will be fetched from {@link getSupportedTypes()}.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder#options}\n * @returns {Object}\n */\n getRecordingOptions() {\n throw new Error(`getRecordingOptions() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get a filename for the generated file.\n *\n * Typically this function will take a prefix and add a type-specific suffix such as the extension to it.\n *\n * @param {string} prefix The prefix for the filename generated by the recorder.\n * @returns {string}\n */\n getFileName(prefix) {\n throw new Error(`getFileName() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get a list of constraints as required by the getUserMedia() function.\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#constraints}\n *\n * @returns {Object}\n */\n getMediaConstraints() {\n throw new Error(`getMediaConstraints() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Whether to start playing the recording as it is captured.\n * @returns {boolean} Whether to start playing the recording as it is captured.\n */\n playOnCapture() {\n return false;\n }\n\n /**\n * Get the time limit for this recording type.\n *\n * @returns {number} The time limit in seconds.\n */\n getTimeLimit() {\n throw new Error(`getTimeLimit() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get the name of the template used when embedding the URL in the editor content.\n *\n * @returns {string}\n */\n getEmbedTemplateName() {\n throw new Error(`getEmbedTemplateName() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Fetch the Class of the Modal to be displayed.\n *\n * @returns {Modal}\n */\n static getModalClass() {\n throw new Error(`getModalClass() must be implemented in ${this.constructor.name}`);\n }\n\n /* eslint-enable valid-jsdoc, no-unused-vars */\n\n /**\n * Get the options for the MediaRecorder.\n *\n * @returns {object} The options for the MediaRecorder instance.\n */\n getParsedRecordingOptions() {\n const requestedTypes = this.getSupportedTypes();\n const possibleTypes = requestedTypes.reduce((result, type) => {\n result.push(type);\n // Safari seems to use codecs: instead of codecs=.\n // It is safe to add both, so we do, but we want them to remain in order.\n result.push(type.replace('=', ':'));\n return result;\n }, []);\n\n const compatTypes = possibleTypes.filter((type) => window.MediaRecorder.isTypeSupported(type));\n\n const options = this.getRecordingOptions();\n if (compatTypes.length !== 0) {\n options.mimeType = compatTypes[0];\n }\n window.console.info(\n `Selected codec ${options.mimeType} from ${compatTypes.length} options.`,\n compatTypes,\n );\n\n return options;\n }\n\n /**\n * Start capturing the User Media and handle success or failure of the capture.\n */\n async captureUserMedia() {\n try {\n const stream = await navigator.mediaDevices.getUserMedia(this.getMediaConstraints());\n this.handleCaptureSuccess(stream);\n } catch (error) {\n this.handleCaptureFailure(error);\n }\n }\n\n /**\n * Prefetch some of the content that will be used in the UI.\n *\n * Note: not all of the strings used are pre-fetched.\n * Some of the strings will be fetched because their template is used.\n */\n prefetchContent() {\n prefetchStrings(component, [\n 'uploading',\n 'recordagain_title',\n 'recordagain_desc',\n 'discard_title',\n 'discard_desc',\n 'confirm_yes',\n 'recordinguploaded',\n 'maxfilesizehit',\n 'maxfilesizehit_title',\n 'uploadfailed',\n ]);\n\n prefetchTemplates([\n this.getEmbedTemplateName(),\n 'tiny_recordrtc/timeremaining',\n ]);\n }\n\n /**\n * Display an error message to the user.\n *\n * @param {Promise} title The error title\n * @param {Promise} content The error message\n * @returns {Promise}\n */\n async displayAlert(title, content) {\n const pendingPromise = new Pending('core/confirm:alert');\n const modal = await AlertModal.create({\n title: title,\n body: content,\n removeOnClose: true,\n });\n\n modal.show();\n pendingPromise.resolve();\n\n return modal;\n }\n\n /**\n * Handle successful capture of the User Media.\n *\n * @param {MediaStream} stream The stream as captured by the User Media.\n */\n handleCaptureSuccess(stream) {\n // Set audio player source to microphone stream.\n this.player.srcObject = stream;\n\n if (this.playOnCapture()) {\n // Mute audio, distracting while recording.\n this.player.muted = true;\n\n this.player.play();\n }\n\n this.stream = stream;\n this.setupPlayerSource();\n this.setRecordButtonState(true);\n }\n\n /**\n * Setup the player to use the stream as a source.\n */\n setupPlayerSource() {\n if (!this.player.srcObject) {\n this.player.srcObject = this.stream;\n\n // Mute audio, distracting while recording.\n this.player.muted = true;\n\n this.player.play();\n }\n }\n\n /**\n * Enable the record button.\n *\n * @param {boolean|null} enabled Set the button state\n */\n setRecordButtonState(enabled) {\n this.startStopButton.disabled = !enabled;\n }\n\n /**\n * Configure button visibility for the record button.\n *\n * @param {boolean} visible Set the visibility of the button.\n */\n setRecordButtonVisibility(visible) {\n const container = this.getButtonContainer('start-stop');\n container.classList.toggle('hide', !visible);\n }\n\n /**\n * Enable the upload button.\n *\n * @param {boolean|null} enabled Set the button state\n */\n setUploadButtonState(enabled) {\n this.uploadButton.disabled = !enabled;\n }\n\n /**\n * Configure button visibility for the upload button.\n *\n * @param {boolean} visible Set the visibility of the button.\n */\n setUploadButtonVisibility(visible) {\n const container = this.getButtonContainer('upload');\n container.classList.toggle('hide', !visible);\n }\n /**\n * Handle failure to capture the User Media.\n *\n * @param {Error} error\n */\n handleCaptureFailure(error) {\n // Changes 'CertainError' -> 'gumcertain' to match language string names.\n var subject = `gum${error.name.replace('Error', '').toLowerCase()}`;\n this.displayAlert(\n getString(`${subject}_title`, component),\n getString(subject, component)\n );\n }\n\n /**\n * Close the modal and stop recording.\n */\n close() {\n // Closing the modal will destroy it and remove it from the DOM.\n // It will also stop the recording via the hidden Modal Event.\n this.modal.hide();\n }\n\n /**\n * Register event listeners for the modal.\n */\n registerEventListeners() {\n this.modalRoot.addEventListener('click', this.handleModalClick.bind(this));\n this.modal.getRoot().on(ModalEvents.outsideClick, this.outsideClickHandler.bind(this));\n this.modal.getRoot().on(ModalEvents.hidden, () => {\n this.cleanupStream();\n this.requestRecordingStop();\n });\n }\n\n /**\n * Prevent the Modal from closing when recording is on process.\n *\n * @param {MouseEvent} event The click event\n */\n async outsideClickHandler(event) {\n if (this.isRecording()) {\n // The user is recording.\n // Do not distract with a confirmation, just prevent closing.\n event.preventDefault();\n } else if (this.hasData()) {\n // If there is a blobsize then there is data that may be lost.\n // Ask the user to confirm they want to close the modal.\n // We prevent default here, and then close the modal if they confirm.\n event.preventDefault();\n\n try {\n await saveCancelPromise(\n await getString(\"discard_title\", component),\n await getString(\"discard_desc\", component),\n await getString(\"confirm_yes\", component),\n );\n this.modal.hide();\n } catch (error) {\n // Do nothing, the modal will not close.\n }\n }\n }\n\n /**\n * Handle a click within the Modal.\n *\n * @param {MouseEvent} event The click event\n */\n handleModalClick(event) {\n const button = event.target.closest('button');\n if (button && button.dataset.action) {\n const action = button.dataset.action;\n if (action === 'startstop') {\n this.handleRecordingStartStopRequested();\n }\n\n if (action === 'upload') {\n this.uploadRecording();\n }\n }\n }\n\n /**\n * Handle the click event for the recording start/stop button.\n */\n handleRecordingStartStopRequested() {\n if (this.mediaRecorder?.state === 'recording') {\n this.requestRecordingStop();\n } else {\n this.startRecording();\n }\n }\n\n /**\n * Handle the media stream after it has finished.\n */\n async onMediaStopped() {\n // Set source of audio player.\n this.blob = new Blob(this.data.chunks, {\n type: this.mediaRecorder.mimeType\n });\n this.player.srcObject = null;\n this.player.src = URL.createObjectURL(this.blob);\n\n // Change the label to \"Record again\".\n this.setRecordButtonTextFromString('recordagain');\n\n // Show audio player with controls enabled, and unmute.\n this.player.muted = false;\n this.player.controls = true;\n this.getButtonContainer('player')?.classList.toggle('hide', false);\n\n // Show upload button.\n this.setUploadButtonVisibility(true);\n this.setUploadButtonState(true);\n }\n\n /**\n * Upload the recording and insert it into the editor content.\n */\n async uploadRecording() {\n // Trigger error if no recording has been made.\n if (this.data.chunks.length === 0) {\n this.displayAlert('norecordingfound');\n return;\n }\n\n const fileName = this.getFileName((Math.random() * 1000).toString().replace('.', ''));\n\n // Upload recording to server.\n try {\n // Once uploading starts, do not allow any further changes to the recording.\n this.setRecordButtonVisibility(false);\n\n // Disable the upload button.\n this.setUploadButtonState(false);\n\n // Upload the recording.\n const fileURL = await uploadFile(this.editor, 'media', this.blob, fileName, (progress) => {\n this.setUploadButtonTextProgress(progress);\n });\n this.insertMedia(fileURL);\n this.close();\n addToast(await getString('recordinguploaded', component));\n } catch (error) {\n // Show a toast and unhide the button.\n this.setUploadButtonState(true);\n\n addToast(await getString('uploadfailed', component, {error}), {\n type: 'error',\n });\n\n }\n }\n\n /**\n * Helper to get the container that a button is in.\n *\n * @param {string} purpose The button purpose\n * @returns {HTMLElement}\n */\n getButtonContainer(purpose) {\n return this.modalRoot.querySelector(`[data-purpose=\"${purpose}-container\"]`);\n }\n\n /**\n * Check whether the browser is compatible with capturing media.\n *\n * @returns {boolean}\n */\n static isBrowserCompatible() {\n return this.checkSecure() && this.hasUserMedia();\n }\n\n static async display(editor) {\n const ModalClass = this.getModalClass();\n const modal = await ModalClass.create({\n templateContext: {},\n large: true,\n removeOnClose: true,\n });\n\n // Set up the VideoRecorder.\n const recorder = new this(editor, modal);\n if (recorder.isReady()) {\n modal.show();\n }\n return modal;\n }\n\n /**\n * Check whether the browser is compatible with capturing media, and display a warning if not.\n *\n * @returns {boolean}\n */\n checkAndWarnAboutBrowserCompatibility() {\n if (!this.constructor.checkSecure()) {\n getStrings(['insecurealert_title', 'insecurealert'].map((key) => ({key, component})))\n .then(([title, message]) => addToast(message, {title, type: 'error'}))\n .catch();\n return false;\n }\n\n if (!this.constructor.hasUserMedia) {\n getStrings(['nowebrtc_title', 'nowebrtc'].map((key) => ({key, component})))\n .then(([title, message]) => addToast(message, {title, type: 'error'}))\n .catch();\n return false;\n }\n\n return true;\n }\n\n /**\n * Check whether the browser supports WebRTC.\n *\n * @returns {boolean}\n */\n static hasUserMedia() {\n return (navigator.mediaDevices && window.MediaRecorder);\n }\n\n /**\n * Check whether the hostname is either hosted over SSL, or from a valid localhost hostname.\n *\n * The UserMedia API can only be used in secure contexts as noted.\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#privacy_and_security}\n *\n * @returns {boolean} Whether the plugin can be loaded.\n */\n static checkSecure() {\n // Note: We can now use window.isSecureContext.\n // https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#feature_detection\n // https://developer.mozilla.org/en-US/docs/Web/API/isSecureContext\n return window.isSecureContext;\n }\n\n /**\n * Update the content of the stop recording button timer.\n */\n async setStopRecordingButton() {\n const {html, js} = await Templates.renderForPromise('tiny_recordrtc/timeremaining', this.getTimeRemaining());\n Templates.replaceNodeContents(this.startStopButton, html, js);\n this.buttonTimer = setInterval(this.updateRecordButtonTime.bind(this), 500);\n }\n\n /**\n * Update the time on the stop recording button.\n */\n updateRecordButtonTime() {\n const {remaining, minutes, seconds} = this.getTimeRemaining();\n if (remaining < 0) {\n this.requestRecordingStop();\n } else {\n this.startStopButton.querySelector('[data-type=\"minutes\"]').textContent = minutes;\n this.startStopButton.querySelector('[data-type=\"seconds\"]').textContent = seconds;\n }\n }\n\n /**\n * Set the text of the record button using a language string.\n *\n * @param {string} string The string identifier\n */\n async setRecordButtonTextFromString(string) {\n this.startStopButton.textContent = await getString(string, component);\n }\n\n /**\n * Set the upload button text progress.\n *\n * @param {number} progress The progress\n */\n async setUploadButtonTextProgress(progress) {\n this.uploadButton.textContent = await getString('uploading', component, {\n progress: Math.round(progress * 100) / 100,\n });\n }\n\n async resetUploadButtonText() {\n this.uploadButton.textContent = await getString('upload', component);\n }\n\n /**\n * Clear the timer for the stop recording button.\n */\n clearButtonTimer() {\n if (this.buttonTimer) {\n clearInterval(this.buttonTimer);\n }\n this.buttonTimer = null;\n }\n\n /**\n * Get the time remaining for the recording.\n *\n * @returns {Object} The minutes and seconds remaining.\n */\n getTimeRemaining() {\n // All times are in milliseconds\n const now = new Date().getTime();\n const remaining = Math.floor(this.getTimeLimit() - ((now - this.startTime) / 1000));\n\n const formatter = new Intl.NumberFormat(navigator.language, {minimumIntegerDigits: 2});\n const seconds = formatter.format(remaining % 60);\n const minutes = formatter.format(Math.floor((remaining - seconds) / 60));\n return {\n remaining,\n minutes,\n seconds,\n };\n }\n\n /**\n * Get the maximum file size that can be uploaded.\n *\n * @returns {number} The max byte size\n */\n getMaxUploadSize() {\n return this.config.maxrecsize;\n }\n\n /**\n * Stop the recording.\n * Please note that this should only stop the recording.\n * Anything related to processing the recording should be handled by the\n * mediaRecorder's stopped event handler which is processed after it has stopped.\n */\n requestRecordingStop() {\n if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {\n this.stopRequested = true;\n } else {\n // There is no recording to stop, but the stream must still be cleaned up.\n this.cleanupStream();\n }\n }\n\n stopRecorder() {\n this.mediaRecorder.stop();\n\n // Unmute the player so that the audio is heard during playback.\n this.player.muted = false;\n }\n\n /**\n * Clean up the stream.\n *\n * This involves stopping any track which is still active.\n */\n cleanupStream() {\n if (this.stream) {\n this.stream.getTracks()\n .filter((track) => track.readyState !== 'ended')\n .forEach((track) => track.stop());\n }\n }\n\n /**\n * Handle the mediaRecorder `stop` event.\n */\n handleStopped() {\n // Handle the stream data.\n this.onMediaStopped();\n\n // Clear the button timer.\n this.clearButtonTimer();\n }\n\n /**\n * Handle the mediaRecorder `start` event.\n *\n * This event is called when the recording starts.\n */\n handleStarted() {\n this.startTime = new Date().getTime();\n this.setStopRecordingButton();\n }\n\n /**\n * Handle the mediaRecorder `dataavailable` event.\n *\n * @param {Event} event\n */\n handleDataAvailable(event) {\n if (this.isRecording()) {\n const newSize = this.data.blobSize + event.data.size;\n // Recording stops when either the maximum upload size is reached, or the time limit expires.\n // The time limit is checked in the `updateButtonTime` function.\n if (newSize >= this.getMaxUploadSize()) {\n this.stopRecorder();\n this.displayFileLimitHitMessage();\n } else {\n // Push recording slice to array.\n this.data.chunks.push(event.data);\n\n // Size of all recorded data so far.\n this.data.blobSize = newSize;\n\n if (this.stopRequested) {\n this.stopRecorder();\n }\n }\n }\n }\n\n async displayFileLimitHitMessage() {\n addToast(await getString('maxfilesizehit', component), {\n title: await getString('maxfilesizehit_title', component),\n type: 'error',\n });\n }\n\n /**\n * Check whether the recording is in progress.\n *\n * @returns {boolean}\n */\n isRecording() {\n return this.mediaRecorder?.state === 'recording';\n }\n\n /**\n * Whether any data has been recorded.\n *\n * @returns {boolean}\n */\n hasData() {\n return !!this.data?.blobSize;\n }\n\n /**\n * Start the recording\n */\n async startRecording() {\n if (this.mediaRecorder) {\n // Stop the existing recorder if it exists.\n if (this.isRecording()) {\n this.mediaRecorder.stop();\n }\n\n if (this.hasData()) {\n const resetRecording = await this.recordAgainConfirmation();\n if (!resetRecording) {\n // User cancelled at the confirmation to reset the data, so exit early.\n return;\n }\n this.setUploadButtonVisibility(false);\n if (!this.stream.active) {\n await this.captureUserMedia();\n }\n }\n\n this.mediaRecorder = null;\n }\n\n // The options for the recording codecs and bitrates.\n this.mediaRecorder = new MediaRecorder(this.stream, this.getParsedRecordingOptions());\n\n this.mediaRecorder.addEventListener('dataavailable', this.handleDataAvailable.bind(this));\n this.mediaRecorder.addEventListener('stop', this.handleStopped.bind(this));\n this.mediaRecorder.addEventListener('start', this.handleStarted.bind(this));\n\n this.data = {\n chunks: [],\n blobSize: 0\n };\n this.setupPlayerSource();\n this.stopRequested = false;\n\n // Capture in 50ms chunks.\n this.mediaRecorder.start(50);\n }\n\n /**\n * Confirm whether the user wants to reset the existing recoring.\n *\n * @returns {Promise} Whether the user confirmed the reset.\n */\n async recordAgainConfirmation() {\n try {\n await saveCancelPromise(\n await getString(\"recordagain_title\", component),\n await getString(\"recordagain_desc\", component),\n await getString(\"confirm_yes\", component)\n );\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Insert the HTML to embed the recording into the editor content.\n *\n * @param {string} source The URL to view the media.\n */\n async insertMedia(source) {\n const {html} = await Templates.renderForPromise(\n this.getEmbedTemplateName(),\n this.getEmbedTemplateContext({\n source,\n })\n );\n this.editor.insertContent(html);\n }\n\n /**\n * Add or modify the template parameters for the specified type.\n *\n * @param {Object} templateContext The Tempalte context to use\n * @returns {Object} The finalised template context\n */\n getEmbedTemplateContext(templateContext) {\n return templateContext;\n }\n}\n"],"names":["constructor","editor","modal","ready","this","checkAndWarnAboutBrowserCompatibility","config","params","modalRoot","getRoot","startStopButton","querySelector","uploadButton","setRecordButtonState","player","configurePlayer","registerEventListeners","captureUserMedia","prefetchContent","isReady","Error","name","getSupportedTypes","getRecordingOptions","getFileName","prefix","getMediaConstraints","playOnCapture","getTimeLimit","getEmbedTemplateName","getParsedRecordingOptions","compatTypes","reduce","result","type","push","replace","filter","window","MediaRecorder","isTypeSupported","options","length","mimeType","console","info","stream","navigator","mediaDevices","getUserMedia","handleCaptureSuccess","error","handleCaptureFailure","component","title","content","pendingPromise","Pending","AlertModal","create","body","removeOnClose","show","resolve","srcObject","muted","play","setupPlayerSource","enabled","disabled","setRecordButtonVisibility","visible","getButtonContainer","classList","toggle","setUploadButtonState","setUploadButtonVisibility","subject","toLowerCase","displayAlert","close","hide","addEventListener","handleModalClick","bind","on","ModalEvents","outsideClick","outsideClickHandler","hidden","cleanupStream","requestRecordingStop","event","isRecording","preventDefault","hasData","button","target","closest","dataset","action","handleRecordingStartStopRequested","uploadRecording","mediaRecorder","state","startRecording","blob","Blob","data","chunks","src","URL","createObjectURL","setRecordButtonTextFromString","controls","fileName","Math","random","toString","fileURL","progress","setUploadButtonTextProgress","insertMedia","purpose","checkSecure","hasUserMedia","ModalClass","getModalClass","templateContext","large","map","key","then","_ref2","message","catch","_ref","isSecureContext","html","js","Templates","renderForPromise","getTimeRemaining","replaceNodeContents","buttonTimer","setInterval","updateRecordButtonTime","remaining","minutes","seconds","textContent","string","round","clearButtonTimer","clearInterval","now","Date","getTime","floor","startTime","formatter","Intl","NumberFormat","language","minimumIntegerDigits","format","getMaxUploadSize","maxrecsize","stopRequested","stopRecorder","stop","getTracks","track","readyState","forEach","handleStopped","onMediaStopped","handleStarted","setStopRecordingButton","handleDataAvailable","newSize","blobSize","size","displayFileLimitHitMessage","_this$data","recordAgainConfirmation","active","start","source","getEmbedTemplateContext","insertContent"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"base_recorder.min.js","sources":["../src/base_recorder.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n//\n\n/**\n * Tiny Record RTC type.\n *\n * @module tiny_recordrtc/base_recorder\n * @copyright 2022 Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getString, getStrings} from 'core/str';\nimport {component} from './common';\nimport Pending from 'core/pending';\nimport {getData, isPausingAllowed} from './options';\nimport uploadFile from 'editor_tiny/uploader';\nimport {add as addToast} from 'core/toast';\nimport * as ModalEvents from 'core/modal_events';\nimport * as Templates from 'core/templates';\nimport {saveCancelPromise} from 'core/notification';\nimport {prefetchStrings, prefetchTemplates} from 'core/prefetch';\nimport AlertModal from 'core/local/modal/alert';\n\n/**\n * The RecordRTC base class for audio, video, and any other future types\n */\nexport default class {\n\n stopRequested = false;\n buttonTimer = null;\n pauseTime = null;\n startTime = null;\n\n /**\n * Constructor for the RecordRTC class\n *\n * @param {TinyMCE} editor The Editor to which the content will be inserted\n * @param {Modal} modal The Moodle Modal that contains the interface used for recording\n */\n constructor(editor, modal) {\n this.ready = false;\n\n if (!this.checkAndWarnAboutBrowserCompatibility()) {\n return;\n }\n\n this.editor = editor;\n this.config = getData(editor).params;\n this.modal = modal;\n this.modalRoot = modal.getRoot()[0];\n this.startStopButton = this.modalRoot.querySelector('button[data-action=\"startstop\"]');\n this.uploadButton = this.modalRoot.querySelector('button[data-action=\"upload\"]');\n this.pauseResumeButton = this.modalRoot.querySelector('button[data-action=\"pauseresume\"]');\n\n // Disable the record button untilt he stream is acquired.\n this.setRecordButtonState(false);\n\n this.player = this.configurePlayer();\n this.registerEventListeners();\n this.ready = true;\n\n this.captureUserMedia();\n this.prefetchContent();\n }\n\n /**\n * Check whether the browser is compatible.\n *\n * @returns {boolean}\n */\n isReady() {\n return this.ready;\n }\n\n // Disable eslint's valid-jsdoc rule as the following methods are abstract and mnust be overridden by the child class.\n\n /* eslint-disable valid-jsdoc, no-unused-vars */\n\n /**\n * Get the Player element for this type.\n *\n * @returns {HTMLElement} The player element, typically an audio or video tag.\n */\n configurePlayer() {\n throw new Error(`configurePlayer() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get the list of supported mimetypes for this recorder.\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/isTypeSupported}\n *\n * @returns {string[]} The list of supported mimetypes.\n */\n getSupportedTypes() {\n throw new Error(`getSupportedTypes() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get any recording options passed into the MediaRecorder.\n * Please note that the mimeType will be fetched from {@link getSupportedTypes()}.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder#options}\n * @returns {Object}\n */\n getRecordingOptions() {\n throw new Error(`getRecordingOptions() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get a filename for the generated file.\n *\n * Typically this function will take a prefix and add a type-specific suffix such as the extension to it.\n *\n * @param {string} prefix The prefix for the filename generated by the recorder.\n * @returns {string}\n */\n getFileName(prefix) {\n throw new Error(`getFileName() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get a list of constraints as required by the getUserMedia() function.\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#constraints}\n *\n * @returns {Object}\n */\n getMediaConstraints() {\n throw new Error(`getMediaConstraints() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Whether to start playing the recording as it is captured.\n * @returns {boolean} Whether to start playing the recording as it is captured.\n */\n playOnCapture() {\n return false;\n }\n\n /**\n * Get the time limit for this recording type.\n *\n * @returns {number} The time limit in seconds.\n */\n getTimeLimit() {\n throw new Error(`getTimeLimit() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get the name of the template used when embedding the URL in the editor content.\n *\n * @returns {string}\n */\n getEmbedTemplateName() {\n throw new Error(`getEmbedTemplateName() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Fetch the Class of the Modal to be displayed.\n *\n * @returns {Modal}\n */\n static getModalClass() {\n throw new Error(`getModalClass() must be implemented in ${this.constructor.name}`);\n }\n\n /* eslint-enable valid-jsdoc, no-unused-vars */\n\n /**\n * Get the options for the MediaRecorder.\n *\n * @returns {object} The options for the MediaRecorder instance.\n */\n getParsedRecordingOptions() {\n const requestedTypes = this.getSupportedTypes();\n const possibleTypes = requestedTypes.reduce((result, type) => {\n result.push(type);\n // Safari seems to use codecs: instead of codecs=.\n // It is safe to add both, so we do, but we want them to remain in order.\n result.push(type.replace('=', ':'));\n return result;\n }, []);\n\n const compatTypes = possibleTypes.filter((type) => window.MediaRecorder.isTypeSupported(type));\n\n const options = this.getRecordingOptions();\n if (compatTypes.length !== 0) {\n options.mimeType = compatTypes[0];\n }\n window.console.info(\n `Selected codec ${options.mimeType} from ${compatTypes.length} options.`,\n compatTypes,\n );\n\n return options;\n }\n\n /**\n * Start capturing the User Media and handle success or failure of the capture.\n */\n async captureUserMedia() {\n try {\n const stream = await navigator.mediaDevices.getUserMedia(this.getMediaConstraints());\n this.handleCaptureSuccess(stream);\n } catch (error) {\n this.handleCaptureFailure(error);\n }\n }\n\n /**\n * Prefetch some of the content that will be used in the UI.\n *\n * Note: not all of the strings used are pre-fetched.\n * Some of the strings will be fetched because their template is used.\n */\n prefetchContent() {\n prefetchStrings(component, [\n 'uploading',\n 'recordagain_title',\n 'recordagain_desc',\n 'discard_title',\n 'discard_desc',\n 'confirm_yes',\n 'recordinguploaded',\n 'maxfilesizehit',\n 'maxfilesizehit_title',\n 'uploadfailed',\n 'pause',\n 'resume',\n ]);\n\n prefetchTemplates([\n this.getEmbedTemplateName(),\n 'tiny_recordrtc/timeremaining',\n ]);\n }\n\n /**\n * Display an error message to the user.\n *\n * @param {Promise} title The error title\n * @param {Promise} content The error message\n * @returns {Promise}\n */\n async displayAlert(title, content) {\n const pendingPromise = new Pending('core/confirm:alert');\n const modal = await AlertModal.create({\n title: title,\n body: content,\n removeOnClose: true,\n });\n\n modal.show();\n pendingPromise.resolve();\n\n return modal;\n }\n\n /**\n * Handle successful capture of the User Media.\n *\n * @param {MediaStream} stream The stream as captured by the User Media.\n */\n handleCaptureSuccess(stream) {\n // Set audio player source to microphone stream.\n this.player.srcObject = stream;\n\n if (this.playOnCapture()) {\n // Mute audio, distracting while recording.\n this.player.muted = true;\n\n this.player.play();\n }\n\n this.stream = stream;\n this.setupPlayerSource();\n this.setRecordButtonState(true);\n }\n\n /**\n * Setup the player to use the stream as a source.\n */\n setupPlayerSource() {\n if (!this.player.srcObject) {\n this.player.srcObject = this.stream;\n\n // Mute audio, distracting while recording.\n this.player.muted = true;\n\n this.player.play();\n }\n }\n\n /**\n * Enable the record button.\n *\n * @param {boolean|null} enabled Set the button state\n */\n setRecordButtonState(enabled) {\n this.startStopButton.disabled = !enabled;\n }\n\n /**\n * Configure button visibility for the record button.\n *\n * @param {boolean} visible Set the visibility of the button.\n */\n setRecordButtonVisibility(visible) {\n const container = this.getButtonContainer('start-stop');\n container.classList.toggle('hide', !visible);\n }\n\n /**\n * Configure button visibility for the pause button.\n *\n * @param {boolean} visible Set the visibility of the button.\n */\n setPauseButtonVisibility(visible) {\n if (this.pauseResumeButton) {\n this.pauseResumeButton.classList.toggle('hidden', !visible);\n }\n }\n\n /**\n * Enable the upload button.\n *\n * @param {boolean|null} enabled Set the button state\n */\n setUploadButtonState(enabled) {\n this.uploadButton.disabled = !enabled;\n }\n\n /**\n * Configure button visibility for the upload button.\n *\n * @param {boolean} visible Set the visibility of the button.\n */\n setUploadButtonVisibility(visible) {\n const container = this.getButtonContainer('upload');\n container.classList.toggle('hide', !visible);\n }\n\n /**\n * Sets the state of the audio player, including visibility, muting, and controls.\n *\n * @param {boolean} state A boolean indicating the audio player state.\n */\n setPlayerState(state) {\n // Mute or unmute the audio player and show or hide controls.\n this.player.muted = !state;\n this.player.controls = state;\n // Toggle the 'hide' class on the player button container based on state.\n this.getButtonContainer('player')?.classList.toggle('hide', !state);\n }\n\n /**\n * Handle failure to capture the User Media.\n *\n * @param {Error} error\n */\n handleCaptureFailure(error) {\n // Changes 'CertainError' -> 'gumcertain' to match language string names.\n var subject = `gum${error.name.replace('Error', '').toLowerCase()}`;\n this.displayAlert(\n getString(`${subject}_title`, component),\n getString(subject, component)\n );\n }\n\n /**\n * Close the modal and stop recording.\n */\n close() {\n // Closing the modal will destroy it and remove it from the DOM.\n // It will also stop the recording via the hidden Modal Event.\n this.modal.hide();\n }\n\n /**\n * Register event listeners for the modal.\n */\n registerEventListeners() {\n this.modalRoot.addEventListener('click', this.handleModalClick.bind(this));\n this.modal.getRoot().on(ModalEvents.outsideClick, this.outsideClickHandler.bind(this));\n this.modal.getRoot().on(ModalEvents.hidden, () => {\n this.cleanupStream();\n this.requestRecordingStop();\n });\n }\n\n /**\n * Prevent the Modal from closing when recording is on process.\n *\n * @param {MouseEvent} event The click event\n */\n async outsideClickHandler(event) {\n if (this.isRecording() || this.isPaused()) {\n // The user is recording.\n // Do not distract with a confirmation, just prevent closing.\n event.preventDefault();\n } else if (this.hasData()) {\n // If there is a blobsize then there is data that may be lost.\n // Ask the user to confirm they want to close the modal.\n // We prevent default here, and then close the modal if they confirm.\n event.preventDefault();\n\n try {\n await saveCancelPromise(\n await getString(\"discard_title\", component),\n await getString(\"discard_desc\", component),\n await getString(\"confirm_yes\", component),\n );\n this.modal.hide();\n } catch (error) {\n // Do nothing, the modal will not close.\n }\n }\n }\n\n /**\n * Handle a click within the Modal.\n *\n * @param {MouseEvent} event The click event\n */\n handleModalClick(event) {\n const button = event.target.closest('button');\n if (button && button.dataset.action) {\n const action = button.dataset.action;\n if (action === 'startstop') {\n this.handleRecordingStartStopRequested();\n }\n\n if (action === 'upload') {\n this.uploadRecording();\n }\n\n if (action === 'pauseresume') {\n this.handleRecordingPauseResumeRequested();\n }\n }\n }\n\n /**\n * Handle the click event for the recording start/stop button.\n */\n handleRecordingStartStopRequested() {\n if (this.isRecording() || this.isPaused()) {\n this.requestRecordingStop();\n } else {\n this.startRecording();\n }\n }\n\n /**\n * Handle the click event for the recording pause/resume button.\n */\n handleRecordingPauseResumeRequested() {\n if (this.isRecording()) {\n // Pause recording.\n this.mediaRecorder.pause();\n } else if (this.isPaused()) {\n // Resume recording.\n this.mediaRecorder.resume();\n }\n }\n\n /**\n * Handle the media stream after it has finished.\n */\n async onMediaStopped() {\n // Set source of audio player.\n this.blob = new Blob(this.data.chunks, {\n type: this.mediaRecorder.mimeType\n });\n this.player.srcObject = null;\n this.player.src = URL.createObjectURL(this.blob);\n\n // Change the label to \"Record again\".\n this.setRecordButtonTextFromString('recordagain');\n\n // Show upload button.\n this.setUploadButtonVisibility(true);\n this.setPlayerState(true);\n this.setUploadButtonState(true);\n\n // Hide the pause button.\n this.setPauseButtonVisibility(false);\n if (this.mediaRecorder.state === 'inactive') {\n this.setPauseButtonTextFromString('pause');\n }\n }\n\n /**\n * Upload the recording and insert it into the editor content.\n */\n async uploadRecording() {\n // Trigger error if no recording has been made.\n if (this.data.chunks.length === 0) {\n this.displayAlert('norecordingfound');\n return;\n }\n\n const fileName = this.getFileName((Math.random() * 1000).toString().replace('.', ''));\n\n // Upload recording to server.\n try {\n // Once uploading starts, do not allow any further changes to the recording.\n this.setRecordButtonVisibility(false);\n\n // Disable the upload button.\n this.setUploadButtonState(false);\n\n // Upload the recording.\n const fileURL = await uploadFile(this.editor, 'media', this.blob, fileName, (progress) => {\n this.setUploadButtonTextProgress(progress);\n });\n this.insertMedia(fileURL);\n this.close();\n addToast(await getString('recordinguploaded', component));\n } catch (error) {\n // Show a toast and unhide the button.\n this.setUploadButtonState(true);\n\n addToast(await getString('uploadfailed', component, {error}), {\n type: 'error',\n });\n\n }\n }\n\n /**\n * Helper to get the container that a button is in.\n *\n * @param {string} purpose The button purpose\n * @returns {HTMLElement}\n */\n getButtonContainer(purpose) {\n return this.modalRoot.querySelector(`[data-purpose=\"${purpose}-container\"]`);\n }\n\n /**\n * Check whether the browser is compatible with capturing media.\n *\n * @returns {boolean}\n */\n static isBrowserCompatible() {\n return this.checkSecure() && this.hasUserMedia();\n }\n\n static async display(editor) {\n const ModalClass = this.getModalClass();\n const modal = await ModalClass.create({\n templateContext: {\n isallowedpausing: isPausingAllowed(editor),\n },\n large: true,\n removeOnClose: true,\n });\n\n // Set up the VideoRecorder.\n const recorder = new this(editor, modal);\n if (recorder.isReady()) {\n modal.show();\n }\n return modal;\n }\n\n /**\n * Check whether the browser is compatible with capturing media, and display a warning if not.\n *\n * @returns {boolean}\n */\n checkAndWarnAboutBrowserCompatibility() {\n if (!this.constructor.checkSecure()) {\n getStrings(['insecurealert_title', 'insecurealert'].map((key) => ({key, component})))\n .then(([title, message]) => addToast(message, {title, type: 'error'}))\n .catch();\n return false;\n }\n\n if (!this.constructor.hasUserMedia) {\n getStrings(['nowebrtc_title', 'nowebrtc'].map((key) => ({key, component})))\n .then(([title, message]) => addToast(message, {title, type: 'error'}))\n .catch();\n return false;\n }\n\n return true;\n }\n\n /**\n * Check whether the browser supports WebRTC.\n *\n * @returns {boolean}\n */\n static hasUserMedia() {\n return (navigator.mediaDevices && window.MediaRecorder);\n }\n\n /**\n * Check whether the hostname is either hosted over SSL, or from a valid localhost hostname.\n *\n * The UserMedia API can only be used in secure contexts as noted.\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#privacy_and_security}\n *\n * @returns {boolean} Whether the plugin can be loaded.\n */\n static checkSecure() {\n // Note: We can now use window.isSecureContext.\n // https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#feature_detection\n // https://developer.mozilla.org/en-US/docs/Web/API/isSecureContext\n return window.isSecureContext;\n }\n\n /**\n * Update the content of the stop recording button timer.\n */\n async setStopRecordingButton() {\n const {html, js} = await Templates.renderForPromise('tiny_recordrtc/timeremaining', this.getTimeRemaining());\n Templates.replaceNodeContents(this.startStopButton, html, js);\n this.startButtonTimer();\n }\n\n /**\n * Update the time on the stop recording button.\n */\n updateRecordButtonTime() {\n const {remaining, minutes, seconds} = this.getTimeRemaining();\n if (remaining < 0) {\n this.requestRecordingStop();\n } else {\n this.startStopButton.querySelector('[data-type=\"minutes\"]').textContent = minutes;\n this.startStopButton.querySelector('[data-type=\"seconds\"]').textContent = seconds;\n }\n }\n\n /**\n * Set the text of the record button using a language string.\n *\n * @param {string} string The string identifier\n */\n async setRecordButtonTextFromString(string) {\n this.startStopButton.textContent = await getString(string, component);\n }\n\n /**\n * Set the text of the pause button using a language string.\n *\n * @param {string} string The string identifier\n */\n async setPauseButtonTextFromString(string) {\n if (this.pauseResumeButton) {\n this.pauseResumeButton.textContent = await getString(string, component);\n }\n }\n\n /**\n * Set the upload button text progress.\n *\n * @param {number} progress The progress\n */\n async setUploadButtonTextProgress(progress) {\n this.uploadButton.textContent = await getString('uploading', component, {\n progress: Math.round(progress * 100) / 100,\n });\n }\n\n async resetUploadButtonText() {\n this.uploadButton.textContent = await getString('upload', component);\n }\n\n /**\n * Clear the timer for the stop recording button.\n */\n clearButtonTimer() {\n if (this.buttonTimer) {\n clearInterval(this.buttonTimer);\n }\n this.buttonTimer = null;\n this.pauseTime = null;\n this.startTime = null;\n }\n\n /**\n * Pause the timer for the stop recording button.\n */\n pauseButtonTimer() {\n // Stop the countdown timer.\n this.pauseTime = new Date().getTime(); // Store pause time.\n if (this.buttonTimer) {\n clearInterval(this.buttonTimer);\n }\n }\n\n /**\n * Start the timer for the start recording button.\n * If the recording was paused, the timer will resume from the pause time.\n */\n startButtonTimer() {\n if (this.pauseTime !== null) {\n // Resume from pause.\n const pauseDuration = new Date().getTime() - this.pauseTime;\n // Adjust start time by pause duration.\n this.startTime += pauseDuration;\n this.pauseTime = null;\n }\n this.buttonTimer = setInterval(this.updateRecordButtonTime.bind(this), 500);\n }\n\n /**\n * Get the time remaining for the recording.\n *\n * @returns {Object} The minutes and seconds remaining.\n */\n getTimeRemaining() {\n // All times are in milliseconds.\n let now = new Date().getTime();\n if (this.pauseTime !== null) {\n // If paused, use pauseTime instead of current time.\n now = this.pauseTime;\n }\n const remaining = Math.floor(this.getTimeLimit() - ((now - this.startTime) / 1000));\n\n const formatter = new Intl.NumberFormat(navigator.language, {minimumIntegerDigits: 2});\n const seconds = formatter.format(remaining % 60);\n const minutes = formatter.format(Math.floor((remaining - seconds) / 60));\n return {\n remaining,\n minutes,\n seconds,\n };\n }\n\n /**\n * Get the maximum file size that can be uploaded.\n *\n * @returns {number} The max byte size\n */\n getMaxUploadSize() {\n return this.config.maxrecsize;\n }\n\n /**\n * Stop the recording.\n * Please note that this should only stop the recording.\n * Anything related to processing the recording should be handled by the\n * mediaRecorder's stopped event handler which is processed after it has stopped.\n */\n requestRecordingStop() {\n if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {\n this.stopRequested = true;\n if (this.isPaused()) {\n this.stopRecorder();\n }\n } else {\n // There is no recording to stop, but the stream must still be cleaned up.\n this.cleanupStream();\n }\n }\n\n stopRecorder() {\n if (this.isPaused()) {\n this.pauseTime = null;\n }\n this.mediaRecorder.stop();\n\n // Unmute the player so that the audio is heard during playback.\n this.player.muted = false;\n }\n\n /**\n * Clean up the stream.\n *\n * This involves stopping any track which is still active.\n */\n cleanupStream() {\n if (this.stream) {\n this.stream.getTracks()\n .filter((track) => track.readyState !== 'ended')\n .forEach((track) => track.stop());\n }\n }\n\n /**\n * Handle the mediaRecorder `stop` event.\n */\n handleStopped() {\n // Handle the stream data.\n this.onMediaStopped();\n\n // Clear the button timer.\n this.clearButtonTimer();\n }\n\n /**\n * Handle the mediaRecorder `start` event.\n *\n * This event is called when the recording starts.\n */\n handleStarted() {\n this.startTime = new Date().getTime();\n if (isPausingAllowed(this.editor) && !this.isPaused()) {\n this.setPauseButtonVisibility(true);\n }\n this.setStopRecordingButton();\n }\n\n /**\n * Handle the mediaRecorder `pause` event.\n *\n * This event is called when the recording pauses.\n */\n handlePaused() {\n this.pauseButtonTimer();\n this.setPauseButtonTextFromString('resume');\n }\n\n /**\n * Handle the mediaRecorder `resume` event.\n *\n * This event is called when the recording resumes.\n */\n handleResume() {\n this.startButtonTimer();\n this.setPauseButtonTextFromString('pause');\n }\n\n /**\n * Handle the mediaRecorder `dataavailable` event.\n *\n * @param {Event} event\n */\n handleDataAvailable(event) {\n if (this.isRecording() || this.isPaused()) {\n const newSize = this.data.blobSize + event.data.size;\n // Recording stops when either the maximum upload size is reached, or the time limit expires.\n // The time limit is checked in the `updateButtonTime` function.\n if (newSize >= this.getMaxUploadSize()) {\n this.stopRecorder();\n this.displayFileLimitHitMessage();\n } else {\n // Push recording slice to array.\n this.data.chunks.push(event.data);\n\n // Size of all recorded data so far.\n this.data.blobSize = newSize;\n\n if (this.stopRequested) {\n this.stopRecorder();\n }\n }\n }\n }\n\n async displayFileLimitHitMessage() {\n addToast(await getString('maxfilesizehit', component), {\n title: await getString('maxfilesizehit_title', component),\n type: 'error',\n });\n }\n\n /**\n * Check whether the recording is in progress.\n *\n * @returns {boolean}\n */\n isRecording() {\n return this.mediaRecorder?.state === 'recording';\n }\n\n /**\n * Check whether the recording is paused.\n *\n * @returns {boolean}\n */\n isPaused() {\n return this.mediaRecorder?.state === 'paused';\n }\n\n /**\n * Whether any data has been recorded.\n *\n * @returns {boolean}\n */\n hasData() {\n return !!this.data?.blobSize;\n }\n\n /**\n * Start the recording\n */\n async startRecording() {\n if (this.mediaRecorder) {\n // Stop the existing recorder if it exists.\n if (this.isRecording() || this.isPaused()) {\n this.mediaRecorder.stop();\n }\n\n if (this.hasData()) {\n const resetRecording = await this.recordAgainConfirmation();\n if (!resetRecording) {\n // User cancelled at the confirmation to reset the data, so exit early.\n return;\n }\n this.setUploadButtonVisibility(false);\n this.setPlayerState(false);\n if (!this.stream.active) {\n await this.captureUserMedia();\n }\n }\n\n this.mediaRecorder = null;\n }\n\n // The options for the recording codecs and bitrates.\n this.mediaRecorder = new MediaRecorder(this.stream, this.getParsedRecordingOptions());\n\n this.mediaRecorder.addEventListener('dataavailable', this.handleDataAvailable.bind(this));\n this.mediaRecorder.addEventListener('stop', this.handleStopped.bind(this));\n this.mediaRecorder.addEventListener('start', this.handleStarted.bind(this));\n this.mediaRecorder.addEventListener('pause', this.handlePaused.bind(this));\n this.mediaRecorder.addEventListener('resume', this.handleResume.bind(this));\n\n this.data = {\n chunks: [],\n blobSize: 0\n };\n this.setupPlayerSource();\n this.stopRequested = false;\n\n // Capture in 50ms chunks.\n this.mediaRecorder.start(50);\n }\n\n /**\n * Confirm whether the user wants to reset the existing recoring.\n *\n * @returns {Promise} Whether the user confirmed the reset.\n */\n async recordAgainConfirmation() {\n try {\n await saveCancelPromise(\n await getString(\"recordagain_title\", component),\n await getString(\"recordagain_desc\", component),\n await getString(\"confirm_yes\", component)\n );\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Insert the HTML to embed the recording into the editor content.\n *\n * @param {string} source The URL to view the media.\n */\n async insertMedia(source) {\n const {html} = await Templates.renderForPromise(\n this.getEmbedTemplateName(),\n this.getEmbedTemplateContext({\n source,\n })\n );\n this.editor.insertContent(html);\n }\n\n /**\n * Add or modify the template parameters for the specified type.\n *\n * @param {Object} templateContext The Tempalte context to use\n * @returns {Object} The finalised template context\n */\n getEmbedTemplateContext(templateContext) {\n return templateContext;\n }\n}\n"],"names":["constructor","editor","modal","ready","this","checkAndWarnAboutBrowserCompatibility","config","params","modalRoot","getRoot","startStopButton","querySelector","uploadButton","pauseResumeButton","setRecordButtonState","player","configurePlayer","registerEventListeners","captureUserMedia","prefetchContent","isReady","Error","name","getSupportedTypes","getRecordingOptions","getFileName","prefix","getMediaConstraints","playOnCapture","getTimeLimit","getEmbedTemplateName","getParsedRecordingOptions","compatTypes","reduce","result","type","push","replace","filter","window","MediaRecorder","isTypeSupported","options","length","mimeType","console","info","stream","navigator","mediaDevices","getUserMedia","handleCaptureSuccess","error","handleCaptureFailure","component","title","content","pendingPromise","Pending","AlertModal","create","body","removeOnClose","show","resolve","srcObject","muted","play","setupPlayerSource","enabled","disabled","setRecordButtonVisibility","visible","getButtonContainer","classList","toggle","setPauseButtonVisibility","setUploadButtonState","setUploadButtonVisibility","setPlayerState","state","controls","subject","toLowerCase","displayAlert","close","hide","addEventListener","handleModalClick","bind","on","ModalEvents","outsideClick","outsideClickHandler","hidden","cleanupStream","requestRecordingStop","event","isRecording","isPaused","preventDefault","hasData","button","target","closest","dataset","action","handleRecordingStartStopRequested","uploadRecording","handleRecordingPauseResumeRequested","startRecording","mediaRecorder","pause","resume","blob","Blob","data","chunks","src","URL","createObjectURL","setRecordButtonTextFromString","setPauseButtonTextFromString","fileName","Math","random","toString","fileURL","progress","setUploadButtonTextProgress","insertMedia","purpose","checkSecure","hasUserMedia","ModalClass","getModalClass","templateContext","isallowedpausing","large","map","key","then","_ref2","message","catch","_ref","isSecureContext","html","js","Templates","renderForPromise","getTimeRemaining","replaceNodeContents","startButtonTimer","updateRecordButtonTime","remaining","minutes","seconds","textContent","string","round","clearButtonTimer","buttonTimer","clearInterval","pauseTime","startTime","pauseButtonTimer","Date","getTime","pauseDuration","setInterval","now","floor","formatter","Intl","NumberFormat","language","minimumIntegerDigits","format","getMaxUploadSize","maxrecsize","stopRequested","stopRecorder","stop","getTracks","track","readyState","forEach","handleStopped","onMediaStopped","handleStarted","setStopRecordingButton","handlePaused","handleResume","handleDataAvailable","newSize","blobSize","size","displayFileLimitHitMessage","_this$data","recordAgainConfirmation","active","start","source","getEmbedTemplateContext","insertContent"],"mappings":"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"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/recordrtc/amd/build/options.min.js b/lib/editor/tiny/plugins/recordrtc/amd/build/options.min.js index cdee95028bb..01505c3737a 100644 --- a/lib/editor/tiny/plugins/recordrtc/amd/build/options.min.js +++ b/lib/editor/tiny/plugins/recordrtc/amd/build/options.min.js @@ -1,4 +1,4 @@ -define("tiny_recordrtc/options",["exports","./common","editor_tiny/options"],(function(_exports,_common,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.isVideoAllowed=_exports.isScreenAllowed=_exports.isAudioAllowed=_exports.getData=void 0; +define("tiny_recordrtc/options",["exports","./common","editor_tiny/options"],(function(_exports,_common,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.isVideoAllowed=_exports.isScreenAllowed=_exports.isPausingAllowed=_exports.isAudioAllowed=_exports.getData=void 0; /** * Options helper for Tiny Record RTC plugin. * @@ -6,6 +6,6 @@ define("tiny_recordrtc/options",["exports","./common","editor_tiny/options"],(fu * @copyright 2022, Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const dataName=(0,_options.getPluginOptionName)(_common.pluginName,"data"),videoAllowedName=(0,_options.getPluginOptionName)(_common.pluginName,"videoAllowed"),audioAllowedName=(0,_options.getPluginOptionName)(_common.pluginName,"audioAllowed"),screenAllowedName=(0,_options.getPluginOptionName)(_common.pluginName,"screenAllowed");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(dataName,{processor:"object"}),registerOption(videoAllowedName,{processor:"boolean",default:!1}),registerOption(audioAllowedName,{processor:"boolean",default:!1}),registerOption(screenAllowedName,{processor:"boolean",default:!1})};_exports.getData=editor=>editor.options.get(dataName);_exports.isAudioAllowed=editor=>editor.options.get(audioAllowedName);_exports.isVideoAllowed=editor=>editor.options.get(videoAllowedName);_exports.isScreenAllowed=editor=>editor.options.get(screenAllowedName)})); +const dataName=(0,_options.getPluginOptionName)(_common.pluginName,"data"),videoAllowedName=(0,_options.getPluginOptionName)(_common.pluginName,"videoAllowed"),audioAllowedName=(0,_options.getPluginOptionName)(_common.pluginName,"audioAllowed"),screenAllowedName=(0,_options.getPluginOptionName)(_common.pluginName,"screenAllowed"),pausingAllowedName=(0,_options.getPluginOptionName)(_common.pluginName,"pausingAllowed");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(dataName,{processor:"object"}),registerOption(videoAllowedName,{processor:"boolean",default:!1}),registerOption(audioAllowedName,{processor:"boolean",default:!1}),registerOption(screenAllowedName,{processor:"boolean",default:!1}),registerOption(pausingAllowedName,{processor:"boolean",default:!1})};_exports.getData=editor=>editor.options.get(dataName);_exports.isAudioAllowed=editor=>editor.options.get(audioAllowedName);_exports.isVideoAllowed=editor=>editor.options.get(videoAllowedName);_exports.isScreenAllowed=editor=>editor.options.get(screenAllowedName);_exports.isPausingAllowed=editor=>editor.options.get(pausingAllowedName)})); //# sourceMappingURL=options.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/recordrtc/amd/build/options.min.js.map b/lib/editor/tiny/plugins/recordrtc/amd/build/options.min.js.map index 2ec39ef3184..b720b51ccbc 100644 --- a/lib/editor/tiny/plugins/recordrtc/amd/build/options.min.js.map +++ b/lib/editor/tiny/plugins/recordrtc/amd/build/options.min.js.map @@ -1 +1 @@ -{"version":3,"file":"options.min.js","sources":["../src/options.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Options helper for Tiny Record RTC plugin.\n *\n * @module tiny_recordrtc/options\n * @copyright 2022, Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {pluginName} from './common';\nimport {getPluginOptionName} from 'editor_tiny/options';\n\nconst dataName = getPluginOptionName(pluginName, 'data');\nconst videoAllowedName = getPluginOptionName(pluginName, 'videoAllowed');\nconst audioAllowedName = getPluginOptionName(pluginName, 'audioAllowed');\nconst screenAllowedName = getPluginOptionName(pluginName, 'screenAllowed');\n\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n\n registerOption(dataName, {\n processor: 'object',\n });\n\n registerOption(videoAllowedName, {\n processor: 'boolean',\n \"default\": false,\n });\n\n registerOption(audioAllowedName, {\n processor: 'boolean',\n \"default\": false,\n });\n\n registerOption(screenAllowedName, {\n processor: 'boolean',\n \"default\": false,\n });\n};\n\nexport const getData = (editor) => editor.options.get(dataName);\n\n/**\n * Whether video may be recorded in this instance.\n *\n * @param {TinyMCE} editor\n * @returns {boolean}\n */\nexport const isAudioAllowed = (editor) => editor.options.get(audioAllowedName);\n\n/**\n * Whether audio may be recorded in this instance.\n *\n * @param {TinyMCE} editor\n * @returns {boolean}\n */\nexport const isVideoAllowed = (editor) => editor.options.get(videoAllowedName);\n\n/**\n * Whether screen may be recorded in this instance.\n *\n * @param {TinyMCE} editor\n * @returns {boolean}\n */\nexport const isScreenAllowed = (editor) => editor.options.get(screenAllowedName);\n"],"names":["dataName","pluginName","videoAllowedName","audioAllowedName","screenAllowedName","editor","registerOption","options","register","processor","get"],"mappings":";;;;;;;;MA0BMA,UAAW,gCAAoBC,mBAAY,QAC3CC,kBAAmB,gCAAoBD,mBAAY,gBACnDE,kBAAmB,gCAAoBF,mBAAY,gBACnDG,mBAAoB,gCAAoBH,mBAAY,mCAEjCI,eACfC,eAAiBD,OAAOE,QAAQC,SAEtCF,eAAeN,SAAU,CACrBS,UAAW,WAGfH,eAAeJ,iBAAkB,CAC7BO,UAAW,mBACA,IAGfH,eAAeH,iBAAkB,CAC7BM,UAAW,mBACA,IAGfH,eAAeF,kBAAmB,CAC9BK,UAAW,mBACA,sBAIKJ,QAAWA,OAAOE,QAAQG,IAAIV,kCAQvBK,QAAWA,OAAOE,QAAQG,IAAIP,0CAQ9BE,QAAWA,OAAOE,QAAQG,IAAIR,2CAQ7BG,QAAWA,OAAOE,QAAQG,IAAIN"} \ No newline at end of file +{"version":3,"file":"options.min.js","sources":["../src/options.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Options helper for Tiny Record RTC plugin.\n *\n * @module tiny_recordrtc/options\n * @copyright 2022, Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {pluginName} from './common';\nimport {getPluginOptionName} from 'editor_tiny/options';\n\nconst dataName = getPluginOptionName(pluginName, 'data');\nconst videoAllowedName = getPluginOptionName(pluginName, 'videoAllowed');\nconst audioAllowedName = getPluginOptionName(pluginName, 'audioAllowed');\nconst screenAllowedName = getPluginOptionName(pluginName, 'screenAllowed');\nconst pausingAllowedName = getPluginOptionName(pluginName, 'pausingAllowed');\n\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n\n registerOption(dataName, {\n processor: 'object',\n });\n\n registerOption(videoAllowedName, {\n processor: 'boolean',\n \"default\": false,\n });\n\n registerOption(audioAllowedName, {\n processor: 'boolean',\n \"default\": false,\n });\n\n registerOption(screenAllowedName, {\n processor: 'boolean',\n \"default\": false,\n });\n\n registerOption(pausingAllowedName, {\n processor: 'boolean',\n \"default\": false,\n });\n};\n\nexport const getData = (editor) => editor.options.get(dataName);\n\n/**\n * Whether video may be recorded in this instance.\n *\n * @param {TinyMCE} editor\n * @returns {boolean}\n */\nexport const isAudioAllowed = (editor) => editor.options.get(audioAllowedName);\n\n/**\n * Whether audio may be recorded in this instance.\n *\n * @param {TinyMCE} editor\n * @returns {boolean}\n */\nexport const isVideoAllowed = (editor) => editor.options.get(videoAllowedName);\n\n/**\n * Whether screen may be recorded in this instance.\n *\n * @param {TinyMCE} editor\n * @returns {boolean}\n */\nexport const isScreenAllowed = (editor) => editor.options.get(screenAllowedName);\n\n/**\n * Whether pausing is allowed in this instance.\n *\n * @param {TinyMCE} editor\n * @returns {boolean}\n */\nexport const isPausingAllowed = (editor) => editor.options.get(pausingAllowedName);\n"],"names":["dataName","pluginName","videoAllowedName","audioAllowedName","screenAllowedName","pausingAllowedName","editor","registerOption","options","register","processor","get"],"mappings":";;;;;;;;MA0BMA,UAAW,gCAAoBC,mBAAY,QAC3CC,kBAAmB,gCAAoBD,mBAAY,gBACnDE,kBAAmB,gCAAoBF,mBAAY,gBACnDG,mBAAoB,gCAAoBH,mBAAY,iBACpDI,oBAAqB,gCAAoBJ,mBAAY,oCAElCK,eACfC,eAAiBD,OAAOE,QAAQC,SAEtCF,eAAeP,SAAU,CACrBU,UAAW,WAGfH,eAAeL,iBAAkB,CAC7BQ,UAAW,mBACA,IAGfH,eAAeJ,iBAAkB,CAC7BO,UAAW,mBACA,IAGfH,eAAeH,kBAAmB,CAC9BM,UAAW,mBACA,IAGfH,eAAeF,mBAAoB,CAC/BK,UAAW,mBACA,sBAIKJ,QAAWA,OAAOE,QAAQG,IAAIX,kCAQvBM,QAAWA,OAAOE,QAAQG,IAAIR,0CAQ9BG,QAAWA,OAAOE,QAAQG,IAAIT,2CAQ7BI,QAAWA,OAAOE,QAAQG,IAAIP,6CAQ7BE,QAAWA,OAAOE,QAAQG,IAAIN"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/recordrtc/amd/build/screen_recorder.min.js b/lib/editor/tiny/plugins/recordrtc/amd/build/screen_recorder.min.js index 216450ff110..d1f7dbf134b 100644 --- a/lib/editor/tiny/plugins/recordrtc/amd/build/screen_recorder.min.js +++ b/lib/editor/tiny/plugins/recordrtc/amd/build/screen_recorder.min.js @@ -1,3 +1,3 @@ -define("tiny_recordrtc/screen_recorder",["exports","./base_recorder","tiny_recordrtc/modal","tiny_recordrtc/common","core/str"],(function(_exports,_base_recorder,_modal,_common,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_base_recorder=_interopRequireDefault(_base_recorder),_modal=_interopRequireDefault(_modal);class Screen extends _base_recorder.default{configurePlayer(){return this.modalRoot.querySelector("video")}getSupportedTypes(){return["video/webm;codecs=vp9,opus","video/webm;codecs=vp8,opus","video/mp4;codecs=h264,opus","video/mp4;codecs=h264,wav","video/mp4;codecs=v9,opus"]}getRecordingOptions(){return{videoBitsPerSecond:parseInt(this.config.screenbitrate),videoWidth:parseInt(this.config.videoscreenwidth),videoHeight:parseInt(this.config.videoscreenheight)}}getMediaConstraints(){return{audio:!0,systemAudio:"exclude",video:{displaySurface:"monitor",frameRate:{ideal:24},width:{max:parseInt(this.config.videoscreenwidth)},height:{max:parseInt(this.config.videoscreenheight)}}}}playOnCapture(){return!0}getRecordingType(){return"screen"}getTimeLimit(){return this.config.screentimelimit}getEmbedTemplateName(){return"tiny_recordrtc/embed_screen"}getFileName(prefix){return"".concat(prefix,"-video.").concat(this.getFileExtension())}getFileExtension(){return window.MediaRecorder.isTypeSupported("audio/webm")?"webm":window.MediaRecorder.isTypeSupported("audio/mp4")?"mp4":(window.console.warn("Unknown file type for MediaRecorder API"),"")}async captureUserMedia(){const audioPromise=navigator.mediaDevices.getUserMedia({audio:!0}),screenPromise=navigator.mediaDevices.getDisplayMedia(this.getMediaConstraints());await Promise.allSettled([audioPromise,screenPromise]).then(this.combineAudioAndScreenRecording.bind(this))}combineAudioAndScreenRecording(results){const[audioData,screenData]=results;if("fulfilled"!==screenData.status)return void this.handleCaptureFailure(screenData.reason);const screenStream=screenData.value;if(screenStream.getVideoTracks()[0].addEventListener("ended",this.handleStopScreenSharing.bind(this)),"fulfilled"!==audioData.status)return void this.handleCaptureSuccess(screenStream);const audioStream=audioData.value,composedStream=new MediaStream;screenStream.getTracks().forEach((function(track){"video"===track.kind?composedStream.addTrack(track):track.stop()})),audioStream.getAudioTracks().forEach((function(micTrack){composedStream.addTrack(micTrack)})),this.handleCaptureSuccess(composedStream)}handleStopScreenSharing(){this.isRecording()?(this.requestRecordingStop(),this.cleanupStream()):(this.setRecordButtonState(!1),this.displayAlert((0,_str.getString)("screensharingstopped_title",_common.component),(0,_str.getString)("screensharingstopped",_common.component)))}handleRecordingStartStopRequested(){this.isRecording()?(this.requestRecordingStop(),this.cleanupStream()):this.startRecording()}static getModalClass(){var _class;return _defineProperty(_class=class extends _modal.default{},"TYPE","".concat(_common.component,"/screen_recorder")),_defineProperty(_class,"TEMPLATE","".concat(_common.component,"/screen_recorder")),_class}}return _exports.default=Screen,_exports.default})); +define("tiny_recordrtc/screen_recorder",["exports","./base_recorder","tiny_recordrtc/modal","tiny_recordrtc/common","core/str"],(function(_exports,_base_recorder,_modal,_common,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_base_recorder=_interopRequireDefault(_base_recorder),_modal=_interopRequireDefault(_modal);class Screen extends _base_recorder.default{configurePlayer(){return this.modalRoot.querySelector("video")}getSupportedTypes(){return["video/webm;codecs=vp9,opus","video/webm;codecs=vp8,opus","video/mp4;codecs=h264,opus","video/mp4;codecs=h264,wav","video/mp4;codecs=v9,opus"]}getRecordingOptions(){return{videoBitsPerSecond:parseInt(this.config.screenbitrate),videoWidth:parseInt(this.config.videoscreenwidth),videoHeight:parseInt(this.config.videoscreenheight)}}getMediaConstraints(){return{audio:!0,systemAudio:"exclude",video:{displaySurface:"monitor",frameRate:{ideal:24},width:{max:parseInt(this.config.videoscreenwidth)},height:{max:parseInt(this.config.videoscreenheight)}}}}playOnCapture(){return!0}getRecordingType(){return"screen"}getTimeLimit(){return this.config.screentimelimit}getEmbedTemplateName(){return"tiny_recordrtc/embed_screen"}getFileName(prefix){return"".concat(prefix,"-video.").concat(this.getFileExtension())}getFileExtension(){return window.MediaRecorder.isTypeSupported("audio/webm")?"webm":window.MediaRecorder.isTypeSupported("audio/mp4")?"mp4":(window.console.warn("Unknown file type for MediaRecorder API"),"")}async captureUserMedia(){const audioPromise=navigator.mediaDevices.getUserMedia({audio:!0}),screenPromise=navigator.mediaDevices.getDisplayMedia(this.getMediaConstraints());await Promise.allSettled([audioPromise,screenPromise]).then(this.combineAudioAndScreenRecording.bind(this))}combineAudioAndScreenRecording(results){const[audioData,screenData]=results;if("fulfilled"!==screenData.status)return void this.handleCaptureFailure(screenData.reason);const screenStream=screenData.value;if(screenStream.getVideoTracks()[0].addEventListener("ended",this.handleStopScreenSharing.bind(this)),"fulfilled"!==audioData.status)return void this.handleCaptureSuccess(screenStream);const audioStream=audioData.value,composedStream=new MediaStream;screenStream.getTracks().forEach((function(track){"video"===track.kind?composedStream.addTrack(track):track.stop()})),audioStream.getAudioTracks().forEach((function(micTrack){composedStream.addTrack(micTrack)})),this.handleCaptureSuccess(composedStream)}handleStopScreenSharing(){this.isRecording()||this.isPaused()?(this.requestRecordingStop(),this.cleanupStream()):(this.setRecordButtonState(!1),this.displayAlert((0,_str.getString)("screensharingstopped_title",_common.component),(0,_str.getString)("screensharingstopped",_common.component)))}handleRecordingStartStopRequested(){this.isRecording()||this.isPaused()?(this.requestRecordingStop(),this.cleanupStream()):this.startRecording()}static getModalClass(){var _class;return _defineProperty(_class=class extends _modal.default{},"TYPE","".concat(_common.component,"/screen_recorder")),_defineProperty(_class,"TEMPLATE","".concat(_common.component,"/screen_recorder")),_class}}return _exports.default=Screen,_exports.default})); //# sourceMappingURL=screen_recorder.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/recordrtc/amd/build/screen_recorder.min.js.map b/lib/editor/tiny/plugins/recordrtc/amd/build/screen_recorder.min.js.map index 2f4db9eacb3..22804ddc684 100644 --- a/lib/editor/tiny/plugins/recordrtc/amd/build/screen_recorder.min.js.map +++ b/lib/editor/tiny/plugins/recordrtc/amd/build/screen_recorder.min.js.map @@ -1 +1 @@ -{"version":3,"file":"screen_recorder.min.js","sources":["../src/screen_recorder.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny Record RTC - Screen recorder configuration.\n *\n * @module tiny_recordrtc/screen_recorder\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BaseClass from './base_recorder';\nimport Modal from 'tiny_recordrtc/modal';\nimport {component} from 'tiny_recordrtc/common';\nimport {getString} from 'core/str';\n\nexport default class Screen extends BaseClass {\n configurePlayer() {\n return this.modalRoot.querySelector('video');\n }\n\n getSupportedTypes() {\n return [\n // Support webm as a preference.\n // This container supports both vp9, and vp8.\n // It does not support AVC1/h264 at all.\n // It is supported by Chromium, and Firefox browsers, but not Safari.\n 'video/webm;codecs=vp9,opus',\n 'video/webm;codecs=vp8,opus',\n\n // Fall back to mp4 if webm is not available.\n // The mp4 container supports v9, and h264 but neither of these are supported for recording on other\n // browsers.\n // In addition to this, we can record in v9, but VideoJS does not support a mp4 container with v9 codec\n // for playback. We leave it as a final option as a just-in-case.\n 'video/mp4;codecs=h264,opus',\n 'video/mp4;codecs=h264,wav',\n 'video/mp4;codecs=v9,opus',\n ];\n\n }\n\n getRecordingOptions() {\n return {\n videoBitsPerSecond: parseInt(this.config.screenbitrate),\n videoWidth: parseInt(this.config.videoscreenwidth),\n videoHeight: parseInt(this.config.videoscreenheight),\n };\n }\n\n getMediaConstraints() {\n return {\n audio: true,\n systemAudio: 'exclude',\n video: {\n displaySurface: 'monitor',\n frameRate: {ideal: 24},\n width: {\n max: parseInt(this.config.videoscreenwidth),\n },\n height: {\n max: parseInt(this.config.videoscreenheight),\n },\n },\n };\n }\n\n playOnCapture() {\n // Play the recording back on capture.\n return true;\n }\n\n getRecordingType() {\n return 'screen';\n }\n\n getTimeLimit() {\n return this.config.screentimelimit;\n }\n\n getEmbedTemplateName() {\n return 'tiny_recordrtc/embed_screen';\n }\n\n getFileName(prefix) {\n return `${prefix}-video.${this.getFileExtension()}`;\n }\n\n getFileExtension() {\n if (window.MediaRecorder.isTypeSupported('audio/webm')) {\n return 'webm';\n } else if (window.MediaRecorder.isTypeSupported('audio/mp4')) {\n return 'mp4';\n }\n\n window.console.warn(`Unknown file type for MediaRecorder API`);\n return '';\n }\n\n async captureUserMedia() {\n // Screen recording requires both audio and the screen, and we need to get them both together.\n const audioPromise = navigator.mediaDevices.getUserMedia({audio: true});\n const screenPromise = navigator.mediaDevices.getDisplayMedia(this.getMediaConstraints());\n // If the audioPromise is \"rejected\" (indicating that the user does not want to share their voice),\n // we will proceed to record their screen without audio.\n // Therefore, we will use Promise.allSettled instead of Promise.all.\n await Promise.allSettled([audioPromise, screenPromise]).then(this.combineAudioAndScreenRecording.bind(this));\n }\n\n /**\n * For starting screen recording, once we have both audio and video, combine them.\n *\n * @param {Object[]} results from the above Promise.allSettled call.\n */\n combineAudioAndScreenRecording(results) {\n const [audioData, screenData] = results;\n if (screenData.status !== 'fulfilled') {\n // If the user does not grant screen permission show warning popup.\n this.handleCaptureFailure(screenData.reason);\n return;\n }\n\n const screenStream = screenData.value;\n // Prepare to handle if the user clicks the browser's \"Stop Sharing Screen\" button.\n screenStream.getVideoTracks()[0].addEventListener('ended', this.handleStopScreenSharing.bind(this));\n\n // Handle microphone.\n if (audioData.status !== 'fulfilled') {\n // We could not get audio. In this case, we just continue without audio.\n this.handleCaptureSuccess(screenStream);\n return;\n }\n const audioStream = audioData.value;\n // Merge the video track from the media stream with the audio track from the microphone stream\n // and stop any unnecessary tracks to ensure that the recorded video has microphone sound.\n const composedStream = new MediaStream();\n screenStream.getTracks().forEach(function(track) {\n if (track.kind === 'video') {\n composedStream.addTrack(track);\n } else {\n track.stop();\n }\n });\n audioStream.getAudioTracks().forEach(function(micTrack) {\n composedStream.addTrack(micTrack);\n });\n\n this.handleCaptureSuccess(composedStream);\n }\n\n /**\n * Callback that is called by the user clicking Stop screen sharing on the browser.\n */\n handleStopScreenSharing() {\n if (this.isRecording()) {\n this.requestRecordingStop();\n this.cleanupStream();\n } else {\n this.setRecordButtonState(false);\n this.displayAlert(\n getString('screensharingstopped_title', component),\n getString('screensharingstopped', component)\n );\n }\n }\n\n handleRecordingStartStopRequested() {\n if (this.isRecording()) {\n this.requestRecordingStop();\n this.cleanupStream();\n } else {\n this.startRecording();\n }\n }\n\n static getModalClass() {\n return class extends Modal {\n static TYPE = `${component}/screen_recorder`;\n static TEMPLATE = `${component}/screen_recorder`;\n };\n }\n}\n"],"names":["Screen","BaseClass","configurePlayer","this","modalRoot","querySelector","getSupportedTypes","getRecordingOptions","videoBitsPerSecond","parseInt","config","screenbitrate","videoWidth","videoscreenwidth","videoHeight","videoscreenheight","getMediaConstraints","audio","systemAudio","video","displaySurface","frameRate","ideal","width","max","height","playOnCapture","getRecordingType","getTimeLimit","screentimelimit","getEmbedTemplateName","getFileName","prefix","getFileExtension","window","MediaRecorder","isTypeSupported","console","warn","audioPromise","navigator","mediaDevices","getUserMedia","screenPromise","getDisplayMedia","Promise","allSettled","then","combineAudioAndScreenRecording","bind","results","audioData","screenData","status","handleCaptureFailure","reason","screenStream","value","getVideoTracks","addEventListener","handleStopScreenSharing","handleCaptureSuccess","audioStream","composedStream","MediaStream","getTracks","forEach","track","kind","addTrack","stop","getAudioTracks","micTrack","isRecording","requestRecordingStop","cleanupStream","setRecordButtonState","displayAlert","component","handleRecordingStartStopRequested","startRecording","Modal"],"mappings":"+lBA4BqBA,eAAeC,uBAChCC,yBACWC,KAAKC,UAAUC,cAAc,SAGxCC,0BACW,CAKH,6BACA,6BAOA,6BACA,4BACA,4BAKRC,4BACW,CACHC,mBAAoBC,SAASN,KAAKO,OAAOC,eACzCC,WAAYH,SAASN,KAAKO,OAAOG,kBACjCC,YAAaL,SAASN,KAAKO,OAAOK,oBAI1CC,4BACW,CACHC,OAAO,EACPC,YAAa,UACbC,MAAO,CACHC,eAAgB,UAChBC,UAAW,CAACC,MAAO,IACnBC,MAAO,CACHC,IAAKf,SAASN,KAAKO,OAAOG,mBAE9BY,OAAQ,CACJD,IAAKf,SAASN,KAAKO,OAAOK,sBAM1CW,uBAEW,EAGXC,yBACW,SAGXC,sBACWzB,KAAKO,OAAOmB,gBAGvBC,6BACW,8BAGXC,YAAYC,wBACEA,yBAAgB7B,KAAK8B,oBAGnCA,0BACQC,OAAOC,cAAcC,gBAAgB,cAC9B,OACAF,OAAOC,cAAcC,gBAAgB,aACrC,OAGXF,OAAOG,QAAQC,gDACR,mCAKDC,aAAeC,UAAUC,aAAaC,aAAa,CAACzB,OAAO,IAC3D0B,cAAgBH,UAAUC,aAAaG,gBAAgBzC,KAAKa,6BAI5D6B,QAAQC,WAAW,CAACP,aAAcI,gBAAgBI,KAAK5C,KAAK6C,+BAA+BC,KAAK9C,OAQ1G6C,+BAA+BE,eACpBC,UAAWC,YAAcF,WACN,cAAtBE,WAAWC,wBAENC,qBAAqBF,WAAWG,cAInCC,aAAeJ,WAAWK,SAEhCD,aAAaE,iBAAiB,GAAGC,iBAAiB,QAASxD,KAAKyD,wBAAwBX,KAAK9C,OAGpE,cAArBgD,UAAUE,wBAELQ,qBAAqBL,oBAGxBM,YAAcX,UAAUM,MAGxBM,eAAiB,IAAIC,YAC3BR,aAAaS,YAAYC,SAAQ,SAASC,OACnB,UAAfA,MAAMC,KACNL,eAAeM,SAASF,OAExBA,MAAMG,UAGdR,YAAYS,iBAAiBL,SAAQ,SAASM,UAC1CT,eAAeM,SAASG,kBAGvBX,qBAAqBE,gBAM9BH,0BACQzD,KAAKsE,oBACAC,4BACAC,uBAEAC,sBAAqB,QACrBC,cACD,kBAAU,6BAA8BC,oBACxC,kBAAU,uBAAwBA,qBAK9CC,oCACQ5E,KAAKsE,oBACAC,4BACAC,sBAEAK,iFAKF,cAAcC,kCACAH,mFACIA"} \ No newline at end of file +{"version":3,"file":"screen_recorder.min.js","sources":["../src/screen_recorder.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny Record RTC - Screen recorder configuration.\n *\n * @module tiny_recordrtc/screen_recorder\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BaseClass from './base_recorder';\nimport Modal from 'tiny_recordrtc/modal';\nimport {component} from 'tiny_recordrtc/common';\nimport {getString} from 'core/str';\n\nexport default class Screen extends BaseClass {\n configurePlayer() {\n return this.modalRoot.querySelector('video');\n }\n\n getSupportedTypes() {\n return [\n // Support webm as a preference.\n // This container supports both vp9, and vp8.\n // It does not support AVC1/h264 at all.\n // It is supported by Chromium, and Firefox browsers, but not Safari.\n 'video/webm;codecs=vp9,opus',\n 'video/webm;codecs=vp8,opus',\n\n // Fall back to mp4 if webm is not available.\n // The mp4 container supports v9, and h264 but neither of these are supported for recording on other\n // browsers.\n // In addition to this, we can record in v9, but VideoJS does not support a mp4 container with v9 codec\n // for playback. We leave it as a final option as a just-in-case.\n 'video/mp4;codecs=h264,opus',\n 'video/mp4;codecs=h264,wav',\n 'video/mp4;codecs=v9,opus',\n ];\n\n }\n\n getRecordingOptions() {\n return {\n videoBitsPerSecond: parseInt(this.config.screenbitrate),\n videoWidth: parseInt(this.config.videoscreenwidth),\n videoHeight: parseInt(this.config.videoscreenheight),\n };\n }\n\n getMediaConstraints() {\n return {\n audio: true,\n systemAudio: 'exclude',\n video: {\n displaySurface: 'monitor',\n frameRate: {ideal: 24},\n width: {\n max: parseInt(this.config.videoscreenwidth),\n },\n height: {\n max: parseInt(this.config.videoscreenheight),\n },\n },\n };\n }\n\n playOnCapture() {\n // Play the recording back on capture.\n return true;\n }\n\n getRecordingType() {\n return 'screen';\n }\n\n getTimeLimit() {\n return this.config.screentimelimit;\n }\n\n getEmbedTemplateName() {\n return 'tiny_recordrtc/embed_screen';\n }\n\n getFileName(prefix) {\n return `${prefix}-video.${this.getFileExtension()}`;\n }\n\n getFileExtension() {\n if (window.MediaRecorder.isTypeSupported('audio/webm')) {\n return 'webm';\n } else if (window.MediaRecorder.isTypeSupported('audio/mp4')) {\n return 'mp4';\n }\n\n window.console.warn(`Unknown file type for MediaRecorder API`);\n return '';\n }\n\n async captureUserMedia() {\n // Screen recording requires both audio and the screen, and we need to get them both together.\n const audioPromise = navigator.mediaDevices.getUserMedia({audio: true});\n const screenPromise = navigator.mediaDevices.getDisplayMedia(this.getMediaConstraints());\n // If the audioPromise is \"rejected\" (indicating that the user does not want to share their voice),\n // we will proceed to record their screen without audio.\n // Therefore, we will use Promise.allSettled instead of Promise.all.\n await Promise.allSettled([audioPromise, screenPromise]).then(this.combineAudioAndScreenRecording.bind(this));\n }\n\n /**\n * For starting screen recording, once we have both audio and video, combine them.\n *\n * @param {Object[]} results from the above Promise.allSettled call.\n */\n combineAudioAndScreenRecording(results) {\n const [audioData, screenData] = results;\n if (screenData.status !== 'fulfilled') {\n // If the user does not grant screen permission show warning popup.\n this.handleCaptureFailure(screenData.reason);\n return;\n }\n\n const screenStream = screenData.value;\n // Prepare to handle if the user clicks the browser's \"Stop Sharing Screen\" button.\n screenStream.getVideoTracks()[0].addEventListener('ended', this.handleStopScreenSharing.bind(this));\n\n // Handle microphone.\n if (audioData.status !== 'fulfilled') {\n // We could not get audio. In this case, we just continue without audio.\n this.handleCaptureSuccess(screenStream);\n return;\n }\n const audioStream = audioData.value;\n // Merge the video track from the media stream with the audio track from the microphone stream\n // and stop any unnecessary tracks to ensure that the recorded video has microphone sound.\n const composedStream = new MediaStream();\n screenStream.getTracks().forEach(function(track) {\n if (track.kind === 'video') {\n composedStream.addTrack(track);\n } else {\n track.stop();\n }\n });\n audioStream.getAudioTracks().forEach(function(micTrack) {\n composedStream.addTrack(micTrack);\n });\n\n this.handleCaptureSuccess(composedStream);\n }\n\n /**\n * Callback that is called by the user clicking Stop screen sharing on the browser.\n */\n handleStopScreenSharing() {\n if (this.isRecording() || this.isPaused()) {\n this.requestRecordingStop();\n this.cleanupStream();\n } else {\n this.setRecordButtonState(false);\n this.displayAlert(\n getString('screensharingstopped_title', component),\n getString('screensharingstopped', component)\n );\n }\n }\n\n handleRecordingStartStopRequested() {\n if (this.isRecording() || this.isPaused()) {\n this.requestRecordingStop();\n this.cleanupStream();\n } else {\n this.startRecording();\n }\n }\n\n static getModalClass() {\n return class extends Modal {\n static TYPE = `${component}/screen_recorder`;\n static TEMPLATE = `${component}/screen_recorder`;\n };\n }\n}\n"],"names":["Screen","BaseClass","configurePlayer","this","modalRoot","querySelector","getSupportedTypes","getRecordingOptions","videoBitsPerSecond","parseInt","config","screenbitrate","videoWidth","videoscreenwidth","videoHeight","videoscreenheight","getMediaConstraints","audio","systemAudio","video","displaySurface","frameRate","ideal","width","max","height","playOnCapture","getRecordingType","getTimeLimit","screentimelimit","getEmbedTemplateName","getFileName","prefix","getFileExtension","window","MediaRecorder","isTypeSupported","console","warn","audioPromise","navigator","mediaDevices","getUserMedia","screenPromise","getDisplayMedia","Promise","allSettled","then","combineAudioAndScreenRecording","bind","results","audioData","screenData","status","handleCaptureFailure","reason","screenStream","value","getVideoTracks","addEventListener","handleStopScreenSharing","handleCaptureSuccess","audioStream","composedStream","MediaStream","getTracks","forEach","track","kind","addTrack","stop","getAudioTracks","micTrack","isRecording","isPaused","requestRecordingStop","cleanupStream","setRecordButtonState","displayAlert","component","handleRecordingStartStopRequested","startRecording","Modal"],"mappings":"+lBA4BqBA,eAAeC,uBAChCC,yBACWC,KAAKC,UAAUC,cAAc,SAGxCC,0BACW,CAKH,6BACA,6BAOA,6BACA,4BACA,4BAKRC,4BACW,CACHC,mBAAoBC,SAASN,KAAKO,OAAOC,eACzCC,WAAYH,SAASN,KAAKO,OAAOG,kBACjCC,YAAaL,SAASN,KAAKO,OAAOK,oBAI1CC,4BACW,CACHC,OAAO,EACPC,YAAa,UACbC,MAAO,CACHC,eAAgB,UAChBC,UAAW,CAACC,MAAO,IACnBC,MAAO,CACHC,IAAKf,SAASN,KAAKO,OAAOG,mBAE9BY,OAAQ,CACJD,IAAKf,SAASN,KAAKO,OAAOK,sBAM1CW,uBAEW,EAGXC,yBACW,SAGXC,sBACWzB,KAAKO,OAAOmB,gBAGvBC,6BACW,8BAGXC,YAAYC,wBACEA,yBAAgB7B,KAAK8B,oBAGnCA,0BACQC,OAAOC,cAAcC,gBAAgB,cAC9B,OACAF,OAAOC,cAAcC,gBAAgB,aACrC,OAGXF,OAAOG,QAAQC,gDACR,mCAKDC,aAAeC,UAAUC,aAAaC,aAAa,CAACzB,OAAO,IAC3D0B,cAAgBH,UAAUC,aAAaG,gBAAgBzC,KAAKa,6BAI5D6B,QAAQC,WAAW,CAACP,aAAcI,gBAAgBI,KAAK5C,KAAK6C,+BAA+BC,KAAK9C,OAQ1G6C,+BAA+BE,eACpBC,UAAWC,YAAcF,WACN,cAAtBE,WAAWC,wBAENC,qBAAqBF,WAAWG,cAInCC,aAAeJ,WAAWK,SAEhCD,aAAaE,iBAAiB,GAAGC,iBAAiB,QAASxD,KAAKyD,wBAAwBX,KAAK9C,OAGpE,cAArBgD,UAAUE,wBAELQ,qBAAqBL,oBAGxBM,YAAcX,UAAUM,MAGxBM,eAAiB,IAAIC,YAC3BR,aAAaS,YAAYC,SAAQ,SAASC,OACnB,UAAfA,MAAMC,KACNL,eAAeM,SAASF,OAExBA,MAAMG,UAGdR,YAAYS,iBAAiBL,SAAQ,SAASM,UAC1CT,eAAeM,SAASG,kBAGvBX,qBAAqBE,gBAM9BH,0BACQzD,KAAKsE,eAAiBtE,KAAKuE,iBACtBC,4BACAC,uBAEAC,sBAAqB,QACrBC,cACD,kBAAU,6BAA8BC,oBACxC,kBAAU,uBAAwBA,qBAK9CC,oCACQ7E,KAAKsE,eAAiBtE,KAAKuE,iBACtBC,4BACAC,sBAEAK,iFAKF,cAAcC,kCACAH,mFACIA"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/recordrtc/amd/src/base_recorder.js b/lib/editor/tiny/plugins/recordrtc/amd/src/base_recorder.js index 9350671b347..b773dd9f715 100644 --- a/lib/editor/tiny/plugins/recordrtc/amd/src/base_recorder.js +++ b/lib/editor/tiny/plugins/recordrtc/amd/src/base_recorder.js @@ -25,7 +25,7 @@ import {getString, getStrings} from 'core/str'; import {component} from './common'; import Pending from 'core/pending'; -import {getData} from './options'; +import {getData, isPausingAllowed} from './options'; import uploadFile from 'editor_tiny/uploader'; import {add as addToast} from 'core/toast'; import * as ModalEvents from 'core/modal_events'; @@ -40,6 +40,9 @@ import AlertModal from 'core/local/modal/alert'; export default class { stopRequested = false; + buttonTimer = null; + pauseTime = null; + startTime = null; /** * Constructor for the RecordRTC class @@ -60,6 +63,7 @@ export default class { this.modalRoot = modal.getRoot()[0]; this.startStopButton = this.modalRoot.querySelector('button[data-action="startstop"]'); this.uploadButton = this.modalRoot.querySelector('button[data-action="upload"]'); + this.pauseResumeButton = this.modalRoot.querySelector('button[data-action="pauseresume"]'); // Disable the record button untilt he stream is acquired. this.setRecordButtonState(false); @@ -233,6 +237,8 @@ export default class { 'maxfilesizehit', 'maxfilesizehit_title', 'uploadfailed', + 'pause', + 'resume', ]); prefetchTemplates([ @@ -316,6 +322,17 @@ export default class { container.classList.toggle('hide', !visible); } + /** + * Configure button visibility for the pause button. + * + * @param {boolean} visible Set the visibility of the button. + */ + setPauseButtonVisibility(visible) { + if (this.pauseResumeButton) { + this.pauseResumeButton.classList.toggle('hidden', !visible); + } + } + /** * Enable the upload button. * @@ -334,6 +351,20 @@ export default class { const container = this.getButtonContainer('upload'); container.classList.toggle('hide', !visible); } + + /** + * Sets the state of the audio player, including visibility, muting, and controls. + * + * @param {boolean} state A boolean indicating the audio player state. + */ + setPlayerState(state) { + // Mute or unmute the audio player and show or hide controls. + this.player.muted = !state; + this.player.controls = state; + // Toggle the 'hide' class on the player button container based on state. + this.getButtonContainer('player')?.classList.toggle('hide', !state); + } + /** * Handle failure to capture the User Media. * @@ -375,7 +406,7 @@ export default class { * @param {MouseEvent} event The click event */ async outsideClickHandler(event) { - if (this.isRecording()) { + if (this.isRecording() || this.isPaused()) { // The user is recording. // Do not distract with a confirmation, just prevent closing. event.preventDefault(); @@ -414,6 +445,10 @@ export default class { if (action === 'upload') { this.uploadRecording(); } + + if (action === 'pauseresume') { + this.handleRecordingPauseResumeRequested(); + } } } @@ -421,13 +456,26 @@ export default class { * Handle the click event for the recording start/stop button. */ handleRecordingStartStopRequested() { - if (this.mediaRecorder?.state === 'recording') { + if (this.isRecording() || this.isPaused()) { this.requestRecordingStop(); } else { this.startRecording(); } } + /** + * Handle the click event for the recording pause/resume button. + */ + handleRecordingPauseResumeRequested() { + if (this.isRecording()) { + // Pause recording. + this.mediaRecorder.pause(); + } else if (this.isPaused()) { + // Resume recording. + this.mediaRecorder.resume(); + } + } + /** * Handle the media stream after it has finished. */ @@ -442,14 +490,16 @@ export default class { // Change the label to "Record again". this.setRecordButtonTextFromString('recordagain'); - // Show audio player with controls enabled, and unmute. - this.player.muted = false; - this.player.controls = true; - this.getButtonContainer('player')?.classList.toggle('hide', false); - // Show upload button. this.setUploadButtonVisibility(true); + this.setPlayerState(true); this.setUploadButtonState(true); + + // Hide the pause button. + this.setPauseButtonVisibility(false); + if (this.mediaRecorder.state === 'inactive') { + this.setPauseButtonTextFromString('pause'); + } } /** @@ -512,7 +562,9 @@ export default class { static async display(editor) { const ModalClass = this.getModalClass(); const modal = await ModalClass.create({ - templateContext: {}, + templateContext: { + isallowedpausing: isPausingAllowed(editor), + }, large: true, removeOnClose: true, }); @@ -578,7 +630,7 @@ export default class { async setStopRecordingButton() { const {html, js} = await Templates.renderForPromise('tiny_recordrtc/timeremaining', this.getTimeRemaining()); Templates.replaceNodeContents(this.startStopButton, html, js); - this.buttonTimer = setInterval(this.updateRecordButtonTime.bind(this), 500); + this.startButtonTimer(); } /** @@ -603,6 +655,17 @@ export default class { this.startStopButton.textContent = await getString(string, component); } + /** + * Set the text of the pause button using a language string. + * + * @param {string} string The string identifier + */ + async setPauseButtonTextFromString(string) { + if (this.pauseResumeButton) { + this.pauseResumeButton.textContent = await getString(string, component); + } + } + /** * Set the upload button text progress. * @@ -626,6 +689,34 @@ export default class { clearInterval(this.buttonTimer); } this.buttonTimer = null; + this.pauseTime = null; + this.startTime = null; + } + + /** + * Pause the timer for the stop recording button. + */ + pauseButtonTimer() { + // Stop the countdown timer. + this.pauseTime = new Date().getTime(); // Store pause time. + if (this.buttonTimer) { + clearInterval(this.buttonTimer); + } + } + + /** + * Start the timer for the start recording button. + * If the recording was paused, the timer will resume from the pause time. + */ + startButtonTimer() { + if (this.pauseTime !== null) { + // Resume from pause. + const pauseDuration = new Date().getTime() - this.pauseTime; + // Adjust start time by pause duration. + this.startTime += pauseDuration; + this.pauseTime = null; + } + this.buttonTimer = setInterval(this.updateRecordButtonTime.bind(this), 500); } /** @@ -634,8 +725,12 @@ export default class { * @returns {Object} The minutes and seconds remaining. */ getTimeRemaining() { - // All times are in milliseconds - const now = new Date().getTime(); + // All times are in milliseconds. + let now = new Date().getTime(); + if (this.pauseTime !== null) { + // If paused, use pauseTime instead of current time. + now = this.pauseTime; + } const remaining = Math.floor(this.getTimeLimit() - ((now - this.startTime) / 1000)); const formatter = new Intl.NumberFormat(navigator.language, {minimumIntegerDigits: 2}); @@ -666,6 +761,9 @@ export default class { requestRecordingStop() { if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') { this.stopRequested = true; + if (this.isPaused()) { + this.stopRecorder(); + } } else { // There is no recording to stop, but the stream must still be cleaned up. this.cleanupStream(); @@ -673,6 +771,9 @@ export default class { } stopRecorder() { + if (this.isPaused()) { + this.pauseTime = null; + } this.mediaRecorder.stop(); // Unmute the player so that the audio is heard during playback. @@ -710,16 +811,39 @@ export default class { */ handleStarted() { this.startTime = new Date().getTime(); + if (isPausingAllowed(this.editor) && !this.isPaused()) { + this.setPauseButtonVisibility(true); + } this.setStopRecordingButton(); } + /** + * Handle the mediaRecorder `pause` event. + * + * This event is called when the recording pauses. + */ + handlePaused() { + this.pauseButtonTimer(); + this.setPauseButtonTextFromString('resume'); + } + + /** + * Handle the mediaRecorder `resume` event. + * + * This event is called when the recording resumes. + */ + handleResume() { + this.startButtonTimer(); + this.setPauseButtonTextFromString('pause'); + } + /** * Handle the mediaRecorder `dataavailable` event. * * @param {Event} event */ handleDataAvailable(event) { - if (this.isRecording()) { + if (this.isRecording() || this.isPaused()) { const newSize = this.data.blobSize + event.data.size; // Recording stops when either the maximum upload size is reached, or the time limit expires. // The time limit is checked in the `updateButtonTime` function. @@ -756,6 +880,15 @@ export default class { return this.mediaRecorder?.state === 'recording'; } + /** + * Check whether the recording is paused. + * + * @returns {boolean} + */ + isPaused() { + return this.mediaRecorder?.state === 'paused'; + } + /** * Whether any data has been recorded. * @@ -771,7 +904,7 @@ export default class { async startRecording() { if (this.mediaRecorder) { // Stop the existing recorder if it exists. - if (this.isRecording()) { + if (this.isRecording() || this.isPaused()) { this.mediaRecorder.stop(); } @@ -782,6 +915,7 @@ export default class { return; } this.setUploadButtonVisibility(false); + this.setPlayerState(false); if (!this.stream.active) { await this.captureUserMedia(); } @@ -796,6 +930,8 @@ export default class { this.mediaRecorder.addEventListener('dataavailable', this.handleDataAvailable.bind(this)); this.mediaRecorder.addEventListener('stop', this.handleStopped.bind(this)); this.mediaRecorder.addEventListener('start', this.handleStarted.bind(this)); + this.mediaRecorder.addEventListener('pause', this.handlePaused.bind(this)); + this.mediaRecorder.addEventListener('resume', this.handleResume.bind(this)); this.data = { chunks: [], diff --git a/lib/editor/tiny/plugins/recordrtc/amd/src/options.js b/lib/editor/tiny/plugins/recordrtc/amd/src/options.js index 631d7c93bac..0f790997ef8 100644 --- a/lib/editor/tiny/plugins/recordrtc/amd/src/options.js +++ b/lib/editor/tiny/plugins/recordrtc/amd/src/options.js @@ -28,6 +28,7 @@ const dataName = getPluginOptionName(pluginName, 'data'); const videoAllowedName = getPluginOptionName(pluginName, 'videoAllowed'); const audioAllowedName = getPluginOptionName(pluginName, 'audioAllowed'); const screenAllowedName = getPluginOptionName(pluginName, 'screenAllowed'); +const pausingAllowedName = getPluginOptionName(pluginName, 'pausingAllowed'); export const register = (editor) => { const registerOption = editor.options.register; @@ -50,6 +51,11 @@ export const register = (editor) => { processor: 'boolean', "default": false, }); + + registerOption(pausingAllowedName, { + processor: 'boolean', + "default": false, + }); }; export const getData = (editor) => editor.options.get(dataName); @@ -77,3 +83,11 @@ export const isVideoAllowed = (editor) => editor.options.get(videoAllowedName); * @returns {boolean} */ export const isScreenAllowed = (editor) => editor.options.get(screenAllowedName); + +/** + * Whether pausing is allowed in this instance. + * + * @param {TinyMCE} editor + * @returns {boolean} + */ +export const isPausingAllowed = (editor) => editor.options.get(pausingAllowedName); diff --git a/lib/editor/tiny/plugins/recordrtc/amd/src/screen_recorder.js b/lib/editor/tiny/plugins/recordrtc/amd/src/screen_recorder.js index dddb30fca38..e6d083a4e0e 100644 --- a/lib/editor/tiny/plugins/recordrtc/amd/src/screen_recorder.js +++ b/lib/editor/tiny/plugins/recordrtc/amd/src/screen_recorder.js @@ -164,7 +164,7 @@ export default class Screen extends BaseClass { * Callback that is called by the user clicking Stop screen sharing on the browser. */ handleStopScreenSharing() { - if (this.isRecording()) { + if (this.isRecording() || this.isPaused()) { this.requestRecordingStop(); this.cleanupStream(); } else { @@ -177,7 +177,7 @@ export default class Screen extends BaseClass { } handleRecordingStartStopRequested() { - if (this.isRecording()) { + if (this.isRecording() || this.isPaused()) { this.requestRecordingStop(); this.cleanupStream(); } else { diff --git a/lib/editor/tiny/plugins/recordrtc/classes/plugininfo.php b/lib/editor/tiny/plugins/recordrtc/classes/plugininfo.php index 7ffd338016b..1691c66d36c 100644 --- a/lib/editor/tiny/plugins/recordrtc/classes/plugininfo.php +++ b/lib/editor/tiny/plugins/recordrtc/classes/plugininfo.php @@ -85,6 +85,7 @@ class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menu $audioallowed = false; $videoallowed = false; $screenallowed = false; + $allowedpausing = (bool) get_config('tiny_recordrtc', 'allowedpausing'); foreach ($allowedtypes as $value) { switch ($value) { case constants::TINYRECORDRTC_AUDIO_TYPE: @@ -136,6 +137,7 @@ class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menu 'videoAllowed' => $videoallowed, 'audioAllowed' => $audioallowed, 'screenAllowed' => $screenallowed, + 'pausingAllowed' => $allowedpausing, ]; } } diff --git a/lib/editor/tiny/plugins/recordrtc/lang/en/tiny_recordrtc.php b/lib/editor/tiny/plugins/recordrtc/lang/en/tiny_recordrtc.php index b8bc643bceb..b27b62aab81 100644 --- a/lib/editor/tiny/plugins/recordrtc/lang/en/tiny_recordrtc.php +++ b/lib/editor/tiny/plugins/recordrtc/lang/en/tiny_recordrtc.php @@ -22,6 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['allowedpausing'] = 'Allow pausing'; $string['allowedtypes'] = 'Recording type'; $string['allowedtypes_desc'] = 'Which types of recording can be made in the TinyMCE editor? In addition to this setting, there are capabilities which control access to recording options.'; $string['attachrecording'] = 'Attach recording'; @@ -62,6 +63,7 @@ $string['nowebrtc_title'] = 'WebRTC not supported'; $string['onlyaudio'] = 'Audio'; $string['onlyscreen'] = 'Screen'; $string['onlyvideo'] = 'Video'; +$string['pause'] = 'Pause'; $string['pluginname'] = 'RecordRTC'; $string['privacy:metadata'] = 'The RecordRTC plugin does not store any personal data.'; $string['recordagain'] = 'Record again'; @@ -72,6 +74,7 @@ $string['recordinguploaded'] = 'Recording uploaded'; $string['recordrtc:recordaudio'] = 'Record audio in the text editor'; $string['recordrtc:recordscreen'] = 'Record screen in the text editor'; $string['recordrtc:recordvideo'] = 'Record video in the text editor'; +$string['resume'] = 'Resume'; $string['screenbitrate'] = 'Screen bitrate'; $string['screenbitrate_desc'] = 'Quality of Screen recording (larger number means higher quality).'; $string['screenbuttontitle'] = 'Record screen'; diff --git a/lib/editor/tiny/plugins/recordrtc/settings.php b/lib/editor/tiny/plugins/recordrtc/settings.php index 5ede2a5aced..b96e7b1166d 100644 --- a/lib/editor/tiny/plugins/recordrtc/settings.php +++ b/lib/editor/tiny/plugins/recordrtc/settings.php @@ -129,4 +129,14 @@ if ($ADMIN->fulltree) { $default = '1280,720'; $setting = new admin_setting_configselect('tiny_recordrtc/screensize', $name, $desc, $default, $options); $settings->add($setting); + + // Pausing allowed. + $options = [ + '1' => new lang_string('yes'), + '0' => new lang_string('no'), + ]; + + $name = get_string('allowedpausing', 'tiny_recordrtc'); + $setting = new admin_setting_configselect('tiny_recordrtc/allowedpausing', $name, '', 0, $options); + $settings->add($setting); } diff --git a/lib/editor/tiny/plugins/recordrtc/templates/audio_recorder.mustache b/lib/editor/tiny/plugins/recordrtc/templates/audio_recorder.mustache index 950a11f7417..6b80531fe63 100644 --- a/lib/editor/tiny/plugins/recordrtc/templates/audio_recorder.mustache +++ b/lib/editor/tiny/plugins/recordrtc/templates/audio_recorder.mustache @@ -21,6 +21,7 @@ Example context (json): { + "isallowedpausing": true } }} {{! @@ -53,10 +54,15 @@
-
+
+ {{#isallowedpausing}} + + {{/isallowedpausing}}
diff --git a/lib/editor/tiny/plugins/recordrtc/templates/screen_recorder.mustache b/lib/editor/tiny/plugins/recordrtc/templates/screen_recorder.mustache index dadd10ab72e..fd340292987 100644 --- a/lib/editor/tiny/plugins/recordrtc/templates/screen_recorder.mustache +++ b/lib/editor/tiny/plugins/recordrtc/templates/screen_recorder.mustache @@ -21,6 +21,7 @@ Example context (json): { + "isallowedpausing": true } }} {{< core/modal }} @@ -47,10 +48,15 @@
-
+
+ {{#isallowedpausing}} + + {{/isallowedpausing}}
diff --git a/lib/editor/tiny/plugins/recordrtc/templates/video_recorder.mustache b/lib/editor/tiny/plugins/recordrtc/templates/video_recorder.mustache index 0c80b8e1ef4..2e2b00b9b29 100644 --- a/lib/editor/tiny/plugins/recordrtc/templates/video_recorder.mustache +++ b/lib/editor/tiny/plugins/recordrtc/templates/video_recorder.mustache @@ -21,6 +21,7 @@ Example context (json): { + "isallowedpausing": true } }} {{! @@ -53,10 +54,15 @@
-
+
+ {{#isallowedpausing}} + + {{/isallowedpausing}}
diff --git a/lib/editor/tiny/plugins/recordrtc/version.php b/lib/editor/tiny/plugins/recordrtc/version.php index 10d0922a14d..e83c9049480 100644 --- a/lib/editor/tiny/plugins/recordrtc/version.php +++ b/lib/editor/tiny/plugins/recordrtc/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024042400; +$plugin->version = 2024053100; $plugin->requires = 2024041600; $plugin->component = 'tiny_recordrtc';