mirror of
https://github.com/moodle/moodle.git
synced 2025-08-11 03:46:42 +02:00
MDL-81111 tiny_recordrtc: add 'Pause' button for recording A/V
This commit is contained in:
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
|
@ -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.
|
* 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>
|
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
|
||||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
* @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
|
//# sourceMappingURL=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 <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"}
|
|
@ -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
|
//# sourceMappingURL=screen_recorder.min.js.map
|
File diff suppressed because one or more lines are too long
|
@ -25,7 +25,7 @@
|
||||||
import {getString, getStrings} from 'core/str';
|
import {getString, getStrings} from 'core/str';
|
||||||
import {component} from './common';
|
import {component} from './common';
|
||||||
import Pending from 'core/pending';
|
import Pending from 'core/pending';
|
||||||
import {getData} from './options';
|
import {getData, isPausingAllowed} from './options';
|
||||||
import uploadFile from 'editor_tiny/uploader';
|
import uploadFile from 'editor_tiny/uploader';
|
||||||
import {add as addToast} from 'core/toast';
|
import {add as addToast} from 'core/toast';
|
||||||
import * as ModalEvents from 'core/modal_events';
|
import * as ModalEvents from 'core/modal_events';
|
||||||
|
@ -40,6 +40,9 @@ import AlertModal from 'core/local/modal/alert';
|
||||||
export default class {
|
export default class {
|
||||||
|
|
||||||
stopRequested = false;
|
stopRequested = false;
|
||||||
|
buttonTimer = null;
|
||||||
|
pauseTime = null;
|
||||||
|
startTime = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the RecordRTC class
|
* Constructor for the RecordRTC class
|
||||||
|
@ -60,6 +63,7 @@ export default class {
|
||||||
this.modalRoot = modal.getRoot()[0];
|
this.modalRoot = modal.getRoot()[0];
|
||||||
this.startStopButton = this.modalRoot.querySelector('button[data-action="startstop"]');
|
this.startStopButton = this.modalRoot.querySelector('button[data-action="startstop"]');
|
||||||
this.uploadButton = this.modalRoot.querySelector('button[data-action="upload"]');
|
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.
|
// Disable the record button untilt he stream is acquired.
|
||||||
this.setRecordButtonState(false);
|
this.setRecordButtonState(false);
|
||||||
|
@ -233,6 +237,8 @@ export default class {
|
||||||
'maxfilesizehit',
|
'maxfilesizehit',
|
||||||
'maxfilesizehit_title',
|
'maxfilesizehit_title',
|
||||||
'uploadfailed',
|
'uploadfailed',
|
||||||
|
'pause',
|
||||||
|
'resume',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
prefetchTemplates([
|
prefetchTemplates([
|
||||||
|
@ -316,6 +322,17 @@ export default class {
|
||||||
container.classList.toggle('hide', !visible);
|
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.
|
* Enable the upload button.
|
||||||
*
|
*
|
||||||
|
@ -334,6 +351,20 @@ export default class {
|
||||||
const container = this.getButtonContainer('upload');
|
const container = this.getButtonContainer('upload');
|
||||||
container.classList.toggle('hide', !visible);
|
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.
|
* Handle failure to capture the User Media.
|
||||||
*
|
*
|
||||||
|
@ -375,7 +406,7 @@ export default class {
|
||||||
* @param {MouseEvent} event The click event
|
* @param {MouseEvent} event The click event
|
||||||
*/
|
*/
|
||||||
async outsideClickHandler(event) {
|
async outsideClickHandler(event) {
|
||||||
if (this.isRecording()) {
|
if (this.isRecording() || this.isPaused()) {
|
||||||
// The user is recording.
|
// The user is recording.
|
||||||
// Do not distract with a confirmation, just prevent closing.
|
// Do not distract with a confirmation, just prevent closing.
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -414,6 +445,10 @@ export default class {
|
||||||
if (action === 'upload') {
|
if (action === 'upload') {
|
||||||
this.uploadRecording();
|
this.uploadRecording();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action === 'pauseresume') {
|
||||||
|
this.handleRecordingPauseResumeRequested();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,13 +456,26 @@ export default class {
|
||||||
* Handle the click event for the recording start/stop button.
|
* Handle the click event for the recording start/stop button.
|
||||||
*/
|
*/
|
||||||
handleRecordingStartStopRequested() {
|
handleRecordingStartStopRequested() {
|
||||||
if (this.mediaRecorder?.state === 'recording') {
|
if (this.isRecording() || this.isPaused()) {
|
||||||
this.requestRecordingStop();
|
this.requestRecordingStop();
|
||||||
} else {
|
} else {
|
||||||
this.startRecording();
|
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.
|
* Handle the media stream after it has finished.
|
||||||
*/
|
*/
|
||||||
|
@ -442,14 +490,16 @@ export default class {
|
||||||
// Change the label to "Record again".
|
// Change the label to "Record again".
|
||||||
this.setRecordButtonTextFromString('recordagain');
|
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.
|
// Show upload button.
|
||||||
this.setUploadButtonVisibility(true);
|
this.setUploadButtonVisibility(true);
|
||||||
|
this.setPlayerState(true);
|
||||||
this.setUploadButtonState(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) {
|
static async display(editor) {
|
||||||
const ModalClass = this.getModalClass();
|
const ModalClass = this.getModalClass();
|
||||||
const modal = await ModalClass.create({
|
const modal = await ModalClass.create({
|
||||||
templateContext: {},
|
templateContext: {
|
||||||
|
isallowedpausing: isPausingAllowed(editor),
|
||||||
|
},
|
||||||
large: true,
|
large: true,
|
||||||
removeOnClose: true,
|
removeOnClose: true,
|
||||||
});
|
});
|
||||||
|
@ -578,7 +630,7 @@ export default class {
|
||||||
async setStopRecordingButton() {
|
async setStopRecordingButton() {
|
||||||
const {html, js} = await Templates.renderForPromise('tiny_recordrtc/timeremaining', this.getTimeRemaining());
|
const {html, js} = await Templates.renderForPromise('tiny_recordrtc/timeremaining', this.getTimeRemaining());
|
||||||
Templates.replaceNodeContents(this.startStopButton, html, js);
|
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);
|
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.
|
* Set the upload button text progress.
|
||||||
*
|
*
|
||||||
|
@ -626,6 +689,34 @@ export default class {
|
||||||
clearInterval(this.buttonTimer);
|
clearInterval(this.buttonTimer);
|
||||||
}
|
}
|
||||||
this.buttonTimer = null;
|
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.
|
* @returns {Object} The minutes and seconds remaining.
|
||||||
*/
|
*/
|
||||||
getTimeRemaining() {
|
getTimeRemaining() {
|
||||||
// All times are in milliseconds
|
// All times are in milliseconds.
|
||||||
const now = new Date().getTime();
|
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 remaining = Math.floor(this.getTimeLimit() - ((now - this.startTime) / 1000));
|
||||||
|
|
||||||
const formatter = new Intl.NumberFormat(navigator.language, {minimumIntegerDigits: 2});
|
const formatter = new Intl.NumberFormat(navigator.language, {minimumIntegerDigits: 2});
|
||||||
|
@ -666,6 +761,9 @@ export default class {
|
||||||
requestRecordingStop() {
|
requestRecordingStop() {
|
||||||
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
||||||
this.stopRequested = true;
|
this.stopRequested = true;
|
||||||
|
if (this.isPaused()) {
|
||||||
|
this.stopRecorder();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// There is no recording to stop, but the stream must still be cleaned up.
|
// There is no recording to stop, but the stream must still be cleaned up.
|
||||||
this.cleanupStream();
|
this.cleanupStream();
|
||||||
|
@ -673,6 +771,9 @@ export default class {
|
||||||
}
|
}
|
||||||
|
|
||||||
stopRecorder() {
|
stopRecorder() {
|
||||||
|
if (this.isPaused()) {
|
||||||
|
this.pauseTime = null;
|
||||||
|
}
|
||||||
this.mediaRecorder.stop();
|
this.mediaRecorder.stop();
|
||||||
|
|
||||||
// Unmute the player so that the audio is heard during playback.
|
// Unmute the player so that the audio is heard during playback.
|
||||||
|
@ -710,16 +811,39 @@ export default class {
|
||||||
*/
|
*/
|
||||||
handleStarted() {
|
handleStarted() {
|
||||||
this.startTime = new Date().getTime();
|
this.startTime = new Date().getTime();
|
||||||
|
if (isPausingAllowed(this.editor) && !this.isPaused()) {
|
||||||
|
this.setPauseButtonVisibility(true);
|
||||||
|
}
|
||||||
this.setStopRecordingButton();
|
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.
|
* Handle the mediaRecorder `dataavailable` event.
|
||||||
*
|
*
|
||||||
* @param {Event} event
|
* @param {Event} event
|
||||||
*/
|
*/
|
||||||
handleDataAvailable(event) {
|
handleDataAvailable(event) {
|
||||||
if (this.isRecording()) {
|
if (this.isRecording() || this.isPaused()) {
|
||||||
const newSize = this.data.blobSize + event.data.size;
|
const newSize = this.data.blobSize + event.data.size;
|
||||||
// Recording stops when either the maximum upload size is reached, or the time limit expires.
|
// Recording stops when either the maximum upload size is reached, or the time limit expires.
|
||||||
// The time limit is checked in the `updateButtonTime` function.
|
// The time limit is checked in the `updateButtonTime` function.
|
||||||
|
@ -756,6 +880,15 @@ export default class {
|
||||||
return this.mediaRecorder?.state === 'recording';
|
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.
|
* Whether any data has been recorded.
|
||||||
*
|
*
|
||||||
|
@ -771,7 +904,7 @@ export default class {
|
||||||
async startRecording() {
|
async startRecording() {
|
||||||
if (this.mediaRecorder) {
|
if (this.mediaRecorder) {
|
||||||
// Stop the existing recorder if it exists.
|
// Stop the existing recorder if it exists.
|
||||||
if (this.isRecording()) {
|
if (this.isRecording() || this.isPaused()) {
|
||||||
this.mediaRecorder.stop();
|
this.mediaRecorder.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -782,6 +915,7 @@ export default class {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setUploadButtonVisibility(false);
|
this.setUploadButtonVisibility(false);
|
||||||
|
this.setPlayerState(false);
|
||||||
if (!this.stream.active) {
|
if (!this.stream.active) {
|
||||||
await this.captureUserMedia();
|
await this.captureUserMedia();
|
||||||
}
|
}
|
||||||
|
@ -796,6 +930,8 @@ export default class {
|
||||||
this.mediaRecorder.addEventListener('dataavailable', this.handleDataAvailable.bind(this));
|
this.mediaRecorder.addEventListener('dataavailable', this.handleDataAvailable.bind(this));
|
||||||
this.mediaRecorder.addEventListener('stop', this.handleStopped.bind(this));
|
this.mediaRecorder.addEventListener('stop', this.handleStopped.bind(this));
|
||||||
this.mediaRecorder.addEventListener('start', this.handleStarted.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 = {
|
this.data = {
|
||||||
chunks: [],
|
chunks: [],
|
||||||
|
|
|
@ -28,6 +28,7 @@ const dataName = getPluginOptionName(pluginName, 'data');
|
||||||
const videoAllowedName = getPluginOptionName(pluginName, 'videoAllowed');
|
const videoAllowedName = getPluginOptionName(pluginName, 'videoAllowed');
|
||||||
const audioAllowedName = getPluginOptionName(pluginName, 'audioAllowed');
|
const audioAllowedName = getPluginOptionName(pluginName, 'audioAllowed');
|
||||||
const screenAllowedName = getPluginOptionName(pluginName, 'screenAllowed');
|
const screenAllowedName = getPluginOptionName(pluginName, 'screenAllowed');
|
||||||
|
const pausingAllowedName = getPluginOptionName(pluginName, 'pausingAllowed');
|
||||||
|
|
||||||
export const register = (editor) => {
|
export const register = (editor) => {
|
||||||
const registerOption = editor.options.register;
|
const registerOption = editor.options.register;
|
||||||
|
@ -50,6 +51,11 @@ export const register = (editor) => {
|
||||||
processor: 'boolean',
|
processor: 'boolean',
|
||||||
"default": false,
|
"default": false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerOption(pausingAllowedName, {
|
||||||
|
processor: 'boolean',
|
||||||
|
"default": false,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getData = (editor) => editor.options.get(dataName);
|
export const getData = (editor) => editor.options.get(dataName);
|
||||||
|
@ -77,3 +83,11 @@ export const isVideoAllowed = (editor) => editor.options.get(videoAllowedName);
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export const isScreenAllowed = (editor) => editor.options.get(screenAllowedName);
|
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);
|
||||||
|
|
|
@ -164,7 +164,7 @@ export default class Screen extends BaseClass {
|
||||||
* Callback that is called by the user clicking Stop screen sharing on the browser.
|
* Callback that is called by the user clicking Stop screen sharing on the browser.
|
||||||
*/
|
*/
|
||||||
handleStopScreenSharing() {
|
handleStopScreenSharing() {
|
||||||
if (this.isRecording()) {
|
if (this.isRecording() || this.isPaused()) {
|
||||||
this.requestRecordingStop();
|
this.requestRecordingStop();
|
||||||
this.cleanupStream();
|
this.cleanupStream();
|
||||||
} else {
|
} else {
|
||||||
|
@ -177,7 +177,7 @@ export default class Screen extends BaseClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRecordingStartStopRequested() {
|
handleRecordingStartStopRequested() {
|
||||||
if (this.isRecording()) {
|
if (this.isRecording() || this.isPaused()) {
|
||||||
this.requestRecordingStop();
|
this.requestRecordingStop();
|
||||||
this.cleanupStream();
|
this.cleanupStream();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -85,6 +85,7 @@ class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menu
|
||||||
$audioallowed = false;
|
$audioallowed = false;
|
||||||
$videoallowed = false;
|
$videoallowed = false;
|
||||||
$screenallowed = false;
|
$screenallowed = false;
|
||||||
|
$allowedpausing = (bool) get_config('tiny_recordrtc', 'allowedpausing');
|
||||||
foreach ($allowedtypes as $value) {
|
foreach ($allowedtypes as $value) {
|
||||||
switch ($value) {
|
switch ($value) {
|
||||||
case constants::TINYRECORDRTC_AUDIO_TYPE:
|
case constants::TINYRECORDRTC_AUDIO_TYPE:
|
||||||
|
@ -136,6 +137,7 @@ class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menu
|
||||||
'videoAllowed' => $videoallowed,
|
'videoAllowed' => $videoallowed,
|
||||||
'audioAllowed' => $audioallowed,
|
'audioAllowed' => $audioallowed,
|
||||||
'screenAllowed' => $screenallowed,
|
'screenAllowed' => $screenallowed,
|
||||||
|
'pausingAllowed' => $allowedpausing,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
$string['allowedpausing'] = 'Allow pausing';
|
||||||
$string['allowedtypes'] = 'Recording type';
|
$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['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';
|
$string['attachrecording'] = 'Attach recording';
|
||||||
|
@ -62,6 +63,7 @@ $string['nowebrtc_title'] = 'WebRTC not supported';
|
||||||
$string['onlyaudio'] = 'Audio';
|
$string['onlyaudio'] = 'Audio';
|
||||||
$string['onlyscreen'] = 'Screen';
|
$string['onlyscreen'] = 'Screen';
|
||||||
$string['onlyvideo'] = 'Video';
|
$string['onlyvideo'] = 'Video';
|
||||||
|
$string['pause'] = 'Pause';
|
||||||
$string['pluginname'] = 'RecordRTC';
|
$string['pluginname'] = 'RecordRTC';
|
||||||
$string['privacy:metadata'] = 'The RecordRTC plugin does not store any personal data.';
|
$string['privacy:metadata'] = 'The RecordRTC plugin does not store any personal data.';
|
||||||
$string['recordagain'] = 'Record again';
|
$string['recordagain'] = 'Record again';
|
||||||
|
@ -72,6 +74,7 @@ $string['recordinguploaded'] = 'Recording uploaded';
|
||||||
$string['recordrtc:recordaudio'] = 'Record audio in the text editor';
|
$string['recordrtc:recordaudio'] = 'Record audio in the text editor';
|
||||||
$string['recordrtc:recordscreen'] = 'Record screen in the text editor';
|
$string['recordrtc:recordscreen'] = 'Record screen in the text editor';
|
||||||
$string['recordrtc:recordvideo'] = 'Record video in the text editor';
|
$string['recordrtc:recordvideo'] = 'Record video in the text editor';
|
||||||
|
$string['resume'] = 'Resume';
|
||||||
$string['screenbitrate'] = 'Screen bitrate';
|
$string['screenbitrate'] = 'Screen bitrate';
|
||||||
$string['screenbitrate_desc'] = 'Quality of Screen recording (larger number means higher quality).';
|
$string['screenbitrate_desc'] = 'Quality of Screen recording (larger number means higher quality).';
|
||||||
$string['screenbuttontitle'] = 'Record screen';
|
$string['screenbuttontitle'] = 'Record screen';
|
||||||
|
|
|
@ -129,4 +129,14 @@ if ($ADMIN->fulltree) {
|
||||||
$default = '1280,720';
|
$default = '1280,720';
|
||||||
$setting = new admin_setting_configselect('tiny_recordrtc/screensize', $name, $desc, $default, $options);
|
$setting = new admin_setting_configselect('tiny_recordrtc/screensize', $name, $desc, $default, $options);
|
||||||
$settings->add($setting);
|
$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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
Example context (json):
|
Example context (json):
|
||||||
{
|
{
|
||||||
|
"isallowedpausing": true
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
{{!
|
{{!
|
||||||
|
@ -53,10 +54,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div data-purpose="start-stop-container" class="row">
|
<div data-purpose="start-stop-container" class="row">
|
||||||
<div class="col-1"></div>
|
<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">
|
<button class="btn btn-lg btn-outline-danger btn-block" data-action="startstop">
|
||||||
{{#str}} startrecording, tiny_recordrtc {{/str}}
|
{{#str}} startrecording, tiny_recordrtc {{/str}}
|
||||||
</button>
|
</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>
|
||||||
<div class="col-1"></div>
|
<div class="col-1"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
Example context (json):
|
Example context (json):
|
||||||
{
|
{
|
||||||
|
"isallowedpausing": true
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
{{< core/modal }}
|
{{< core/modal }}
|
||||||
|
@ -47,10 +48,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div data-purpose="start-stop-container" class="row">
|
<div data-purpose="start-stop-container" class="row">
|
||||||
<div class="col-1"></div>
|
<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">
|
<button class="btn btn-lg btn-outline-danger btn-block" data-action="startstop">
|
||||||
{{#str}} startrecording, tiny_recordrtc {{/str}}
|
{{#str}} startrecording, tiny_recordrtc {{/str}}
|
||||||
</button>
|
</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>
|
||||||
<div class="col-1"></div>
|
<div class="col-1"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
Example context (json):
|
Example context (json):
|
||||||
{
|
{
|
||||||
|
"isallowedpausing": true
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
{{!
|
{{!
|
||||||
|
@ -53,10 +54,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div data-purpose="start-stop-container" class="row">
|
<div data-purpose="start-stop-container" class="row">
|
||||||
<div class="col-1"></div>
|
<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">
|
<button class="btn btn-lg btn-outline-danger btn-block" data-action="startstop">
|
||||||
{{#str}} startrecording, tiny_recordrtc {{/str}}
|
{{#str}} startrecording, tiny_recordrtc {{/str}}
|
||||||
</button>
|
</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>
|
||||||
<div class="col-1"></div>
|
<div class="col-1"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,6 +24,6 @@
|
||||||
|
|
||||||
defined('MOODLE_INTERNAL') || die();
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
$plugin->version = 2024042400;
|
$plugin->version = 2024053100;
|
||||||
$plugin->requires = 2024041600;
|
$plugin->requires = 2024041600;
|
||||||
$plugin->component = 'tiny_recordrtc';
|
$plugin->component = 'tiny_recordrtc';
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue