Video Matrix

Displaying multiple videos tiled on one screen

Wouldn't it be cool if we could display several video streams at the same time? Traditional set top boxes and streaming sticks have limited bandwidth, processing and video decoding capability. Senza lets you take advantage of a powerful browser running in the cloud and stream anything you want to the cloud connector, no matter how complex.

For this demo, we'll make a mosaic that displays multiple copies of a video. Using the arrow keys, you'll be able to adjust the number of videos presented using up/down, and the time offset between the videos using left/right.

HTML and CSS

We can start with this HTML. Since we'll be building the page up dynamically, all we need is an empty div tag.

<!DOCTYPE html>
<html>
<head>
    <title>Mosaic</title>
    <meta charset="UTF-8">
    <style>
    </style>
</head>
<body>
<div id="main">
</div>
</body>
<script>
</script>
</html>

Here's a bit of CSS to make the page TV-sized:

body {  
  margin: 0px;  
  background-color: black;  
  overflow: hidden;  
}

main {  
  width: 1920px;  
  height: 1080px;  
  left: 0px;  
  bottom: 0px;  
  overflow: hidden;  
  position: relative;  
}

Constants and variables

You can configure the app with some constants:

  • The videoUrl of any video you like
  • The videoLength in seconds

We'll create the following variables to store the current state:

  • A one-dimensional array of all the videos
  • The size of the matrix, i.e. the number of rows and columns
  • The number of seconds separating the playback time of consecutive videos
  • Whether the video is playing
const videoUrl = "video/gaia.mp4";  
const videoLength = 18.0;  
const width = 1920;  
const height = 1080;  
const main = document.getElementById("main");

let videos = \[];  
let number = 4;  
let seconds = 0;  
let playing = true;

Creating videos

We'll tile the screen with a mosaic of videos in a grid. We'll have a total of (number * number) videos.

function createVideos() {  
  let videoWidth = width / size;  
  let videoHeight = height / size;

  main.innerHTML = "";  
  videos = \[];

  for (let i = 0; i \< size \* size; i++) {  
    let row = Math.floor(i / size);  
    let col = i % size;
    
    let video = document.createElement("video");
		video.width = videoWidth;
		video.height = videoHeight;
		video.style.top = (row * videoHeight) + "px";
		video.style.left = (col * videoWidth) + "px";
		video.autoplay = "autoplay";
		video.loop = "loop";
 
		let source = document.createElement("source");
		source.src = videoUrl;
		source.type = "video/mp4";

		video.appendChild(source);
		main.appendChild(video);
		videos.push(video);
  }
}

createVideos();

This will create several video tags, set their size and position appropriately, and add them to the videos array.

For example, if we have a matrix of size 4 then the last video tag (the one in the bottom right) will be rendered like this:

<video width="480" height="270" autoplay="" loop="" style="top: 810px; left: 1440px;">
  <source src="video/gaia.mp4" type="video/mp4">
</video>

Try viewing the page now and you should see 16 copies of your video playing!

Changing the size

We'll add support for changing the size of the matrix dynamically. We'll support sizes between 1 and 4. (Displaying 100 videos would be computationally intensive even for a powerful browser).

Add a key event listener like this, and define some empty functions to handle each of the keystrokes.

document.addEventListener("keydown", function(event) {  
  switch (event.key) {  
    case "ArrowUp": up(); break;  
    case "ArrowDown": down(); break;  
    case "ArrowLeft": left(); break;  
    case "ArrowRight": right(); break;  
    case "Enter": enter(); break;  
    default: return;  
  }  
  event.preventDefault();  
});

We can implement the up and down functions like this. They'll simply increment or decrement the size, ensure that it's within our bounds, and then regenerate the videos. Note that in our createVideos() implementation it clears the videos array and the contents of the main div so we can call it repeatedly.

const minSize = 1;  
const maxSize = 5;

function up() {  
  size++;  
  if (size > maxSize) size = maxSize;  
  createVideos();  
}

function down() {  
  size--;  
  if (size < minSize) size = minSize;  
  createVideos();  
}

Now if you reload the page, you'll find that you can use the up and down arrow keys to change the size.

Pausing and playing

We can use the enter key to pause or play the video.

First, remove the autoplay attribute from the code that builds the video tag. We'll take explicit control of whether or not the video is playing.

The updatePlaying() function will call play() or pause() on all the videos in the array depending upon whether video should be playing.

The enter() function will toggle the play state and call updatePlaying().

Make sure to also add a call to updatePlaying() at the end of the createVideos() function so that videos will have the correct state when they are created or recreated.

function updatePlaying() {  
  videos.forEach((video) => playing ? video.play() : video.pause());  
}

function enter() {  
  playing = !playing;  
  updatePlaying();  
}

Adjusting timing

Currently, all the videos should play the same thing at the same time. Wouldn't it be better if we could use our matrix to watch different parts of the video?

We'll use the seconds variable to store the offset between consecutive videos. For example, if the offset is 1, then videos will start playing from 0s, 1s, 2s, etc.

To give the user fine-grained control, we'll let them use the left/right arrow keys to adjust the offset in increments of a tenth of a second. It makes a cool effect if all the videos are just slightly offset.

The adjustTiming() function will iterate over all the videos and stagger the current time by the offset. It will use the current time of the first video as a starting point, so this will let you vary the offset as videos are playing in a pleasing way. The tenth of a second offset is subtle for the second video but adds up the more videos you have. Just hit the arrow keys multiple times as needed.

Don't forget to add a call to adjustTiming() at the end of the createVideos() function as well.

function adjustTiming() {  
  let firstTime = videos[0].currentTime;  
  for (let i = 0; i \< size _ size; i++) {  
    let time = firstTime + (seconds _ i);  
    while (time \< 0) time += videoLength;  
    videos[i].currentTime = time % videoLength;  
  }  
}

function left() {  
  seconds -= 0.05;  
  if (seconds \< -10) seconds = -10;  
  adjustTiming();  
}

function right() {  
  seconds += 0.05;  
  if (seconds > 10) seconds = 10;  
  adjustTiming();  
}

Conclusion

That's it! Now you've got a versatile video mosaic app that lets you adjust the size of the matrix, play/pause the video, and adjust the time offset between videos. Try doing that on a little HDMI stick!

Now that you can display multiple videos on one screen, next you can learn how to display one video across multiple screens.


What’s Next