Lucid Projects

Blog - Day to day mumblings...
message roulette

message roulette

04 Nov 2020 - Jake Sherwood

tvLand message roulette

message roulette

For the LiveWeb this week I explored the MediaRecorder constructor.

MediaRecorder let’s you record “media” webcam, audio, screen etc, and work with it in the browser it was recorded in or save to a file system.

I thought it would be fun to create a “message roulette.”

Everyone records a ten second video and when done you are shown a “roulette” button. Which when pressed will cycle through stills of everyone participating and randomly stop on a message. You can spin again to hear another message or record a new one.

First I needed to have stills in addition to the video, building off the example code, I passed my video stream into a canvas and initially tried to just use our old dataToURL method. That saved files but I was unable to see them in preview or the browser.

After some searching I ended up saving the canvas as a Blob just like the MediaRecorder, with the canvas.toBlob method.

With that I still needed to do a bit of post work on the image to have it saved in a readable format. Luckily this stackoverflow had a good way of stripping off some the base64 meta data and allowed me to save the images as readable image files. I then emit them to the server for storage.

function startStillRecord(streamToRecord) {
	canvas.width = myVideo.videoWidth;
	canvas.height = myVideo.videoHeight;
	ctx.drawImage(myVideo, 0, 0, canvas.width, canvas.height);

	let newImgSrc, url;

	canvas.toBlob(function (blob) {
		newImg = document.createElement('img');
		newImg.id = "newImg";
		url = URL.createObjectURL(blob);

		newImg.src = url;
		document.body.appendChild(newImg);
		newImgSrc = document.getElementById("newImg").src;

	});

	let myStill = canvas.toDataURL('image/png');
	myImage.src = myStill;

	var jpegFile64 = myStill.replace(/^data:image\/(png|jpeg);base64,/, "");
	var jpegBlob = base64ToBlob(jpegFile64, 'image/jpeg');

	socket.emit('still', jpegBlob);

}

// https://stackoverflow.com/questions/50537735/convert-blob-to-image-file
function base64ToBlob(base64, mime) {
	mime = mime || '';
	var sliceSize = 1024;
	var byteChars = window.atob(base64);
	var byteArrays = [];

	for (var offset = 0, len = byteChars.length; offset < len; offset += sliceSize) {
		var slice = byteChars.slice(offset, offset + sliceSize);

		var byteNumbers = new Array(slice.length);
		for (var i = 0; i < slice.length; i++) {
			byteNumbers[i] = slice.charCodeAt(i);
		}

		var byteArray = new Uint8Array(byteNumbers);

		byteArrays.push(byteArray);
	}

	return new Blob(byteArrays, {
		type: mime
	});
}

I needed my still images and videos to be named the same (later I create the video msg elem from the stopped image name) so I set the name of the still to a variable (tempFilename) and use that for the video name as well.

// Save Still
 socket.on('still', function(data){
	 console.log(data);
	 // save in stills dir
	 let stillDir = "/public/stills/";
	 let filename = Date.now() + "_" + Math.random();
	 tempFilename = filename;
	 console.log(filename);
	 fs.writeFile(__dirname + stillDir + filename + '.png', data, function(err){
		 if (err) console.log(err);
		 console.log("image saved!")
	 });
 });		

 // Save Recording
 socket.on('video', function(data){
	 console.log(data);
	 //save in vidoes dir
	 let vidDir = "/public/videos/";
	 // use same name set from still above
	 let filename = tempFilename;
	 console.log(tempFilename);
	 fs.writeFile(__dirname + vidDir + filename + '.webm', data, function(err){
		 if (err) console.log(err);
		 console.log("video saved!")
	 });
 });		

To get at my image files from the server I needed to expose the /stills/ dir. To do this I used the serve-index npm package. I’m not sure if this is the most secure way of doing this, but it served my purpose and this directory didn’t have much sensitive data.


message roulette record

I also wanted to give the user a better idea of when they were being recorded. So I found a count down button example here. It used jQuery, I modified it to just be vanilla js.


record button count down

I also needed to dynamically create the DOM element(s) to rotate in the the roulette spinner. I used a simple AJAX script modified for my needs.

// ajax call to load imgs from dir from here 
imgCount = 0;
$.ajax({
	url: folder,
	success: function (data) {
		$(data).find("a").attr("href", function (i, val) {
			if (val.match(/\.(jpe?g|png|gif)$/)) {
				// $("body").append("<img src='" + folder + val + "'>");
				$(".roulette-inner").append("<img id='" + imgCount + "'src='" + val +
					"' style='display:block'>");
				imgCount++
			}
		});
		console.log(imgCount);


	},
	complete: function () {

		myNewHeight = imgHeight * imgCount;
		console.log("myNewHeight " + myNewHeight);
		
		//roulette(imgCount);	
		//roulSettings();
	}

});

I wrapped that in a function call which dynamically created my content when the roulette button is clicked.

To make the roulette functionality I found this example - roulette.js. It is written using jQuery which I have been trying to get away from, but after a little bit of trying to move to vanilla js it was having a bunch of problems. I ended up just leaving as jQuery because I actually needed it for the AJAX call anyway.

The one main problem I had, that I wasn’t able to get sorted was caused by loading the content dynamically. The demo used the height of the DOM element(s) that are going to spin to set various thresholds. Creating the content dynamically and then trying to calculate those heights with js didn’t seem to work. The only way I could stop it from spinning of to infinity was by setting a hard coded height.


roulette to infinity

Still going…

roulette still going inspector gif

Hardcoding a totalHeight dimension made it work but is not exactly the way I want. If there are too few submissions the roulette looks weird. If there are too many you may never see them.

To hack it to stop on the correct value even if it’s “higher” than the height set, I am doing a final css transform when the spin finishes.

Then I take the image name and created my video element from that.

let myResetHeight = imgHeight * stopImg;
let myElem = document.getElementsByClassName('roulette-inner')[0];
// set to final height of selected stopImg
// ** THIS IS THE HACKERY PART **
myElem.style.transform = 'translate(0px, -' + myResetHeight + 'px';

let myMovieElem = document.getElementById(stopImg).src;
let myMovieSrc = myMovieElem.split('/stills/');
// console.log(myMovieSrc[1]);	
let myMovie = myMovieSrc[1].split('.png');
// console.log(myMovie);

let myMovieUrl = "videos/" + myMovie[0] + ".webm";

myMovieMsg = document.createElement('video');
myMovieMsg.src = myMovieUrl;
myMovieMsg.id = 'myMovieMsg' + stopImg;
myMovieMsg.classList.add('movieMsgClass');
myMovieMsg.setAttribute("controls","controls")   

let rouletteDiv = document.getElementById('rouletteDiv');

setTimeout( function(){
	rouletteDiv.prepend(myMovieMsg);

}, 1500)


roulette spin


roulette play

Github code here.

categories: liveweb

join me on this crazy ride. enter your email:

contact [at] jakesherwood [dot] com

contact me if you'd like to work together