MDL-81111 tiny_recordrtc: add 'Pause' button for recording A/V

This commit is contained in:
tai.letan 2024-06-21 09:22:17 +07:00
parent ad7fc69c25
commit 0208b9cae2
16 changed files with 210 additions and 27 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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 <stevani@hotmail.com.au>
* @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

View file

@ -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 <http://www.gnu.org/licenses/>.\n\n/**\n * Options helper for Tiny Record RTC plugin.\n *\n * @module tiny_recordrtc/options\n * @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>\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"}
{"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 <http://www.gnu.org/licenses/>.\n\n/**\n * Options helper for Tiny Record RTC plugin.\n *\n * @module tiny_recordrtc/options\n * @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>\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"}

View file

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

File diff suppressed because one or more lines are too long

View file

@ -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: [],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,6 +21,7 @@
Example context (json):
{
"isallowedpausing": true
}
}}
{{!
@ -53,10 +54,15 @@
</div>
<div data-purpose="start-stop-container" class="row">
<div class="col-1"></div>
<div class="col-10">
<div class="col-10 d-inline-flex w-100">
<button class="btn btn-lg btn-outline-danger btn-block" data-action="startstop">
{{#str}} startrecording, tiny_recordrtc {{/str}}
</button>
{{#isallowedpausing}}
<button class="btn btn-lg btn-outline-primary ml-3 hidden" data-action="pauseresume">
{{#str}} pause, tiny_recordrtc {{/str}}
</button>
{{/isallowedpausing}}
</div>
<div class="col-1"></div>
</div>

View file

@ -21,6 +21,7 @@
Example context (json):
{
"isallowedpausing": true
}
}}
{{< core/modal }}
@ -47,10 +48,15 @@
</div>
<div data-purpose="start-stop-container" class="row">
<div class="col-1"></div>
<div class="col-10">
<div class="col-10 d-inline-flex w-100">
<button class="btn btn-lg btn-outline-danger btn-block" data-action="startstop">
{{#str}} startrecording, tiny_recordrtc {{/str}}
</button>
{{#isallowedpausing}}
<button class="btn btn-lg btn-outline-primary ml-3 hidden" data-action="pauseresume">
{{#str}} pause, tiny_recordrtc {{/str}}
</button>
{{/isallowedpausing}}
</div>
<div class="col-1"></div>
</div>

View file

@ -21,6 +21,7 @@
Example context (json):
{
"isallowedpausing": true
}
}}
{{!
@ -53,10 +54,15 @@
</div>
<div data-purpose="start-stop-container" class="row">
<div class="col-1"></div>
<div class="col-10">
<div class="col-10 d-inline-flex w-100">
<button class="btn btn-lg btn-outline-danger btn-block" data-action="startstop">
{{#str}} startrecording, tiny_recordrtc {{/str}}
</button>
{{#isallowedpausing}}
<button class="btn btn-lg btn-outline-primary ml-3 hidden" data-action="pauseresume">
{{#str}} pause, tiny_recordrtc {{/str}}
</button>
{{/isallowedpausing}}
</div>
<div class="col-1"></div>
</div>

View file

@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042400;
$plugin->version = 2024053100;
$plugin->requires = 2024041600;
$plugin->component = 'tiny_recordrtc';