Source: recorder.js

export {config, mainDiv};
import * as buttonClass from './buttonClass.js';
import * as utils from './utils.js';

/**
 * This is the default configuration variable
 * @type {Object}
*/
let config = {
	position: "bottom-right",
	movable: true,
	debug: true,
	recordButtonColor: null,
	pauseButtonColor: null
};

/**
 * This variable is used to see if the button is dragged or clicked.
 * Default value: false;
 * @type {boolean}
*/
let isDragged = false;
/**
 * This variable is used to see if the menu is open or closed.
 * Default value: false;
 * @type {boolean}
*/
let isMenuOpen = false;

/**
 * Main div Object
 * @type {Object}
 */
let mainDiv;

/**
 * Record Button Object
 * @type {Button}
 */
let recordButton;
/**
 * Pause Button Object
 * @type {Button}
 */
let pauseButton;
/**
 * Download Button Object
 * @type {Button}
 */
let downButton;
/**
 * Check if pause Button has been created
 * Default value: false
 * @type {boolean}
 */
let isPauseButtonCreated = false;

/**
 * Rrweb events array.
 * Default value: [] (empty)
 * @type {array}
 */
let events = [];
/**
 * Contains audio parts of recording.
 * Each time a user set pause to the recording, a blob is written into this
 * array.
 * When the user stop the record, all audio are concatenated into one.
 * At the end of the recording, the array contains at least one element.
 * Default value: [] (empty)
 * @type {array}
 */
let audioParts = [];
/**
 * Rrweb recorder.
 * Allow us to stop it when needed
 * @type {Object}
 */
let isActive;
/**
 * interval to print logs
 * Default value: every 10 seconds
 * @type {function}
*/
let interval;

/**
 * Recorder Object
 * @type {Object}
 */
let recorder;
/**
 * Audio Stream.
 * Allow us to stop it when needed
 */
let recStream;
/**
 * encoding type is format encoding for sound.
 * Default value: "mp3";
 * @type {string}
 */
let encodingType = "mp3";

/**
 * Variable to check if all script used to record are loaded.
 * Default value: false
 * @type {boolean}
 */
let areRecordScriptsLoaded = false;

/**
 * A blob of all events, for downloading
 * @type {Blob}
 */
let eventBlob;
/**
 * A blob of mp3 recording, for downloading
 * @type {Blob}
 */
let soundBlob;
/**
 * The replay page text when downloading
 * @type {string}
 */
let textData;
/**
 * The js script in the replay page (for download)
 * @type {string}
 */
let jsData;
/**
 * The css data used in the replay page (for download)
 * @type {string}
 */
let cssData;

/**
 * Time of the record.
 * Used when the range bar is displayed
 * @type {string}
 */
let totalTime;
/**
 * Sliderbar range bar Object
 * @type {Object}
 */
let sliderBar;
/**
 * Range bar text indicator
 * @type {Object}
 */
let textTime;
/**
 * Replayer created during pause, to be able to recreate frames in real time
 * Default value: null
 * @type {Object}
 */
let pauseReplayer = null;
/**
 * Frame selected each time the user use the range bar
 * @type {integer}
 */
let arrayValue;
/**
 * Check if the user came back in the timeline of events.
 * Default value: false
 * @type {boolean}
 */

/**
 * Recorder status. Can be "PLAYING", "PAUSED" or "STOPPED"
 * Default value: null;
 * @type {string}
 */
let recorderState = null;

/**
 * A flag to detect if encoding is finished.
 * Avoid downloading archive before the audio recorder has finished
 * Default value: false.
 * @type {boolean}
 */
let encodingOver = false;

/**
 * The object of the gif button. This button is not clickable,
 * it is just showing a running gif.
 * @type {Object}
 */
let gifLoadingButton;

/**
 * Load different JS library and callback when fully loaded
 * @param {string} src The path of the script (can be relative or absolute)
 * @param {Function} cb Callback, function to launch when loaded
*/
function loadJS(src, cb, ordered) {
	"use strict";
	var tmp;
	var ref = document.getElementsByTagName("script")[0];
	var script = document.createElement("script");

	if (typeof(cb) === 'boolean') {
		tmp = ordered;
		ordered = cb;
		cb = tmp;
	}

	script.src = src;
	script.async = !ordered;
	ref.parentNode.insertBefore(script, ref);

	if (cb && typeof(cb) === "function") {
		script.onload = cb;
	}
	return script;
}

function loadScripts() {
	// We make sure this is not a drag, but a click
	if (!isDragged) {
		// We include Rrweb
		loadJS("./lib/rrweb/dist/rrweb.min.js", function() {
			loadJS("./lib/web-audio-recorder/lib/WebAudioRecorder.js", function() {
				// Once script has been loaded, launch the rest of the code
				areRecordScriptsLoaded = true;
				launchRecord();
			});
		});
	}
}

/**
 * Resume the record
 * This function is very similar to {@link launchRecord}, excepted we do not
 * need to check for permissions, they should be granted
 */
function resumeRecord(){
	if (!isDragged) {
		recorderState = "RECORDING";
		pauseButton.style.backgroundImage = "url('media/pause32.png')";
		document.getElementById('rrweb-pauseRecord').onclick = pauseRecord;

		if (isUserComingBack) {
			console.log("I split from 0 to " + arrayValue);
			events = events.slice(0, arrayValue);
		}

		recorder.startRecording();

		isActive = rrweb.record({
				emit(event) {
				// push event into the events array
				events.push(event);
			},
		});
		interval = setInterval(function () {utils.logger(events);}, 1000);
	}
}

/**
 * When the record is in pause make appear the range bar
 */
function pauseRecord() {
	let sliderDiv;

	if (!isDragged) {
		recorderState = "PAUSED";
		if (isActive)
			isActive();
		clearInterval(interval);

		pauseButton.style.backgroundImage = "url('media/resume32.png')";
		document.getElementById('rrweb-pauseRecord').onclick = resumeRecord;

		console.log("I set in pause");
		if (events.length > 2) {
			totalTime = computeTimeBetweenTwoFrames(events[events.length - 1], events[0]);
			// We stop audioRecorder
			recorder.finishRecording();

			sliderBar.style.background = 'linear-gradient(to right, #82CFD0 0%, #82CFD0 100%, #999999 100%, #999999 100%)';
		}
	}
}

/**
 * Launch the audio and screen record
 */
function launchRecord() {
	if (!isDragged) {
		recorderState = "RECORDING";
		console.log("Recording has started! ");
		navigator.mediaDevices.getUserMedia({audio: true, video: false}).then(function(stream) {
			utils.logger("getUserMedia() success, stream created, initializing WebAudioRecorder...");
			let audioContext = new AudioContext();
			//update the format
			utils.logger("Format: 2 channel " + encodingType + " @ " + audioContext.sampleRate / 1000 + "kHz");
			//assign to recStream for later use
			recStream = stream;

			/* use the stream */
			let input = audioContext.createMediaStreamSource(stream);

			if (areRecordScriptsLoaded) {
				recorder = new WebAudioRecorder(input, {
					workerDir: "./lib/web-audio-recorder/lib/",
					encoding: encodingType,
					numChannel: 2,
					onEncoderLoading: function(recorder, encodingType) {
						utils.logger("Loading " + encodingType + " encoder...");
					},
					onEncoderLoaded: function(recorder, encodingType) {
						utils.logger(encodingType + " encoder loaded");
					}
				});

				recorder.onComplete = function(recorder, blob) {
					utils.logger("Encoding complete");
					utils.logger(URL.createObjectURL(blob));
					audioParts.push(blob);
                    // If the recorder has been fully stopped, print the downloadButton
					if (recorderState == "STOPPED"){
						encodingOver = true;
						gifLoadingButton.hide();
						displayDownButton();
					}
				}

				recorder.setOptions({
					timeLimit:120,
					encodeAfterRecord: true,
					ogg: {quality: 0.5},
					mp3: {bitRate: 160}
				});

				recorder.startRecording();

				isActive = rrweb.record({
					emit(event) {
						// push event into the events array
						events.push(event);
					},
				});
				interval = setInterval(function () {utils.logger(events);}, 1000);

				// Update the style of record button and the onclick function.
				recordButton.style.backgroundColor = "white";
				recordButton.style.backgroundImage = "url('media/recording32.png')";
				recordButton.style.border = "1px solid black";
				document.getElementById('rrweb-recordButton').onclick = stopRecord;

				//Opening the menu to create
				openMenu();
			}
		});
	}
}

/**
 * This function stop the record of the screen and audio.
 */
function stopRecord() {
	// Restore the style and onclick of recordButton
	if (!isDragged) {
		recorderState = "STOPPED";
		recordButton.style.backgroundColor = "#d92027";
		recordButton.style.backgroundImage = "url('media/camera32.png')";
		recordButton.style.border = "none";
		document.getElementById('rrweb-recordButton').onclick = launchRecord;

		console.log("The recording has been stopped");
		// We stop Rrweb
		if (isActive)
			isActive();
		clearInterval(interval);

		//We close the menu
		openMenu();

		// We stop audioRecorder
		recorder.finishRecording();
		recStream.getAudioTracks()[0].stop();

		// Set the event as Blob
		eventBlob = new Blob([JSON.stringify(events)], {type: "application/json"});

		if (encodingOver == false) {
			//Display GIF loading
			gifLoadingButton = new buttonClass.Button(mainDiv, null, "rrweb-loadingDown", "Your download is almost ready !", 'media/loading32.gif', recordButton);
			gifLoadingButton.createChildButton();
			gifLoadingButton.setClickable(false);
			gifLoadingButton.show();
		}
	}
}

function displayDownButton() {
	//avoid concatenating if there is only one element
	if (audioParts.length > 1) {
		utils.logger("Loading ConcatenateBlobs");
		// We concatenate all audio parts.
		loadJS("./lib/concatenate-blob/ConcatenateBlobs.js", function () {
			ConcatenateBlobs(audioParts, 'audio/mpeg3', function(resultingBlob) {
				soundBlob = resultingBlob;
			});
				if (events.length > 2) {
				utils.changeMainDivSize(80, 0);
				utils.logger("I can download the page");
				downButton = new buttonClass.Button(mainDiv, downRecord, "rrweb-downRecord", "Download your record", 'media/down32.png', recordButton);
				downButton.createChildButton();
				downButton.show();
			}
		});
	}
	else {
		utils.logger("No need to load ConcatenateBlobs");
		soundBlob = audioParts[0];
		if (events.length > 2) {
			utils.changeMainDivSize(80, 0);
			utils.logger("I can download the page");
			downButton = new buttonClass.Button(mainDiv, downRecord, "rrweb-downRecord", "Download your record", 'media/down32.png', recordButton);
			downButton.createChildButton();
			downButton.show();
		}
	}
}

/**
 * Increase size of the mainDiv and make visible pause Button
 */
function openMenu() {
	if (isDragged == false) {
		if (!isMenuOpen) {
			utils.changeMainDivSize(80, 0);
			if (!isPauseButtonCreated) {
				pauseButton = new buttonClass.Button(mainDiv, pauseRecord, "rrweb-pauseRecord", "Pause the record", 'media/pause32.png', recordButton);
				isPauseButtonCreated = true;
				pauseButton.createChildButton();
			} else { pauseButton.show(); }
			isMenuOpen = true;
		} else {
			pauseButton.hide();
			utils.changeMainDivSize(-80, 0);
			isMenuOpen = false;
		}
	}
}

/**
 * Download the record automatically
 * @param {Object} data raw data of the zip file
 * @param {string} filename The name we want to give to the downloaded filename
 */
function saveAs(data, filename)
{
	// Create invisible link
	const a = document.createElement("a");
	a.style.display = "none";
	document.body.appendChild(a);

	// Set the HREF to a Blob representation of the data to be downloaded
	a.href = window.URL.createObjectURL(
		new Blob([data], { type: "application/zip" })
	);

	// Use download attribute to set set desired file name
	a.setAttribute("download", filename);

	// Trigger the download by simulating click
	a.click();

	// Cleanup
	window.URL.revokeObjectURL(a.href);
	document.body.removeChild(a);
}

/** This function read a server-local text file
 *  (does not work in localhost due to CORS request not being HTTPS)
 */
function readTextFile(file) {
	var rawFile = new XMLHttpRequest();
	var isFileValid = false;
	rawFile.open("GET", file, false);
	rawFile.onreadystatechange = function () {
		if (rawFile.readyState === 4) {
			if(rawFile.status === 200 || rawFile.status == 0) {
				isFileValid = true;
			}
		}
	}
	rawFile.send(null);
	if (isFileValid)
		return rawFile.responseText;
	else
		return "error";
}

/**
 * This function escape all special character that could break JSON parsing
 * This function is used when downloading record
 * @param {string} str String given containing all the events.
 * @return Return a well escaped string.
 */
function addslashes(str) {
	return str.replace(/\\/g, '\\\\').
		replace(/\u0008/g, '\\b').
		replace(/\t/g, '\\t').
		replace(/\n/g, '\\n').
		replace(/\f/g, '\\f').
		replace(/\r/g, '\\r').
		replace(/'/g, '\\\'').
		replace(/"/g, '\\"').
		replace(/\//g, '\\/');
}

/**
 * This function launch the download of the record.
 * It also put files in the zip for download
 */
function downRecord() {
	//Load Jszip (minified Because it load faster)
	loadJS("./lib/jszip/dist/jszip.js", function() {
		let zip = new JSZip();

		textData = readTextFile("./download/download.html");
        let textDataEnd = readTextFile("./download/download_end.html");
		jsData = readTextFile("./lib/rrweb/dist/rrweb.min.js");
		cssData = readTextFile("./lib/rrweb/dist/rrweb.min.css");
        // We add thoses files to the zip archive

		console.log(addslashes(JSON.stringify(events)));
		let addEventsToFile = "let events_string = \"" + addslashes(JSON.stringify(events)) + '";';

		textData += addEventsToFile + textDataEnd;
		utils.logger("Je mets les fichiers dans l'archive");
		zip.file("download.html", textData);
		zip.file("js/rrweb.min.js", jsData);
		zip.file("js/rrweb.min.css", cssData);
		zip.file("data/events.json", eventBlob);
		zip.file("data/sound.mp3", soundBlob);

		// Once archive has been generated, we can download it
		zip.generateAsync({type:"blob"})
		.then(function(content) {
			saveAs(content, "cours.zip");
		});
	});
}

/**
 * Set the button position according to user config
 * @param {Object} button The object to apply the style to
 */
function buttonPosition(button) {
	if (config.position.search("bottom") > -1)
		button.style.bottom = "50";
	if (config.position.search("top") > -1)
		button.style.top = "50";
	if (config.position.search("middle") > -1)
		button.style.top = "50";
	if (config.position.search("-right") > -1)
		button.style.right = "50";
	if (config.position.search("-left") > -1)
		button.style.left = "50";
}

function finishDragging() {
	isDragged = false;
}

/**
 * The goal of this function is to make an element movable with mouse
 * @param {string} element The element id
 */
function makeElementMovable(element) {
	//Make the button element draggable:
	dragElement(element);

	function dragElement(elmnt) {
        utils.logger("Make element draggable");
		var pos1 = 0, pos2 = 0, mouseX = 0, mouseY = 0;
		let isClick = true;
		if (document.getElementById(elmnt.id + "header")) {
			/* if present, the header is where you move the element from:*/
			document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
		} else {
			elmnt.onmousedown = dragMouseDown;
		}

		/**
		 * Change the behaviour when the mouse is down
		 * @parent {event} The event when triggered
		 */
		function dragMouseDown(e) {
			isDragged = true;
			e = e || window.event;
			e.preventDefault();
			// get the mouse cursor position at startup:
			mouseX = e.clientX;
			mouseY = e.clientY;
			document.onmouseup = closeDragElement;
			// call a function whenever the cursor moves:
			document.onmousemove = elementDrag;
		}

		/**
		 * Compute the new element position and put it at the right place
		 * @parent {event} The event when triggered
		 */
		function elementDrag(e) {
			// If this function is called, then this it not a "click"
			isClick = false;
			e = e || window.event;
			e.preventDefault();
			// calculate the new cursor position:
			pos1 = mouseX - e.clientX;
			pos2 = mouseY - e.clientY;
			mouseX = e.clientX;
			mouseY = e.clientY;
			// set the element's new position, only if not going out of the window:
			if (elmnt.offsetLeft - pos1 >= 0 && (elmnt.offsetLeft + 70) - pos1 < window.screen.width
				&& elmnt.offsetTop - pos2 >= 0 && (elmnt.offsetTop + 70) - pos2 < window.screen.height)
			{
				elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
				elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
			}
		}

		/**
		 * End the drag
		 * @parent {event} The event when triggered
		 */
		function closeDragElement() {
			/* stop moving when mouse button is released:*/
			document.onmouseup = null;
			document.onmousemove = null;
			// we add a "delay" to prevent misclick while dragging
			// ONLY if elementDrag is not called (Because the mouse is not moving)
			isClick ? finishDragging() : setTimeout(finishDragging, 300);
			isClick = true;
		}
	}
}

/**
 * Create base div containing recorder, pause, resume, stop and downloader
 * buttons.
 * @param {string} mainDivId Specify the div id
 * @return {Object} Return the object of the div, allowing to be modifiable
 */
function createBaseDiv(mainDivId) {
	var mainDiv = document.createElement("div");
	mainDiv.classList.add("rr-block");
	mainDiv.id = mainDivId;
	document.body.appendChild(mainDiv);
	return mainDiv;
}

/**
 * Load CSS file from path given
 * @param {string} path The path for the css file to load
 */
function loadCss(path) {
	var head = document.getElementsByTagName('head')[0];
	var link = document.createElement('link');
	link.rel = 'stylesheet';
	link.type = 'text/css';
	link.href = path;
	link.media = 'all';
	head.appendChild(link);
	utils.logger("Loaded Css !");
}

/**
 * When the page has finished Loading
 * @function window.onload
 */
window.onload = function() {
	utils.logger("Page has finished Loading, launching generic-rrweb-recorder");
	// We create a mainDiv in which wi will display all menu element as block
	mainDiv = createBaseDiv("rrweb-mainDivButton");

	// We load CSS
	loadCss("media/style.css");

	// We define a button that will launch recording
	recordButton = new buttonClass.Button(mainDiv, loadScripts, "rrweb-recordButton", "Start recording! ", 'media/camera32.png', null);
	recordButton.createMenuButton();

	buttonPosition(mainDiv);

	utils.logger("Main Button has been created");

	if (config.movable)
		makeElementMovable(mainDiv);
}