Creating an adaptive loading of media content

Denis Shilov
Quick Code
Published in
6 min readDec 18, 2019

Hello, guys! I’m currently developing a website for my project. On the website I need to show a lot of gifs, each weighs well. If everything is shown at once, page loads too long. At the same time it should definitely contain gifs at the very beginning, so content cannot be loaded after loading of a page.

If you are interested in solution for this problem, then this article will be helpful.

Actually the problem

As I have said before, there are TOO MANY gifs on the site. You can actually check them on reface.tech

Gifs on the webpage

As soon as I developed the beta version of the landing page, there was a problem with loading: for some users it loaded for a very long time. It is certainly a disattractive thing.

It was necessary to solve it somehow. At that tim media content was already loslessly compressed through compressor.io, so I had nothing to do with it. Therefore, it was necessary to compress with losses and give out gifs with lower quality.

But giving out bad media content to users with good Internet is some kind of blasphemy. Therefore, I decided to somehow determine the speed of the Internet of a client, and to slip the appropriate quality depending on the speed.

Making a rough sketch

I will have an array with a description of the media content which will actually be displayed.

Example:

[
{
"large":
{
"size": 0.6211118698120117,
"url":"gifs/control/large/control_1.gif"
},
"middle":
{
"size":0.5330495834350586,
"url":"gifs/control/middle/control_1.gif"
},
"small":
{
"size":0.4901447296142578,
"url":"gifs/control/small/control_1.gif"
}
}
]

We have in element an url and file size (for each quality).

We’ll just:

  1. Go through the array (when the page loads)
  2. Compute the total size of the loaded content for each quality
  3. Check which of them (starting from the best) can be loaded in 4 seconds (a period which fits my requirements)

Then we should:

  1. To write a script which will automatically output such array
  2. To write a thing which will output an internet connection speed

Writing a thing that checks speed

It will be relatively simple. Type in the address bar of your browser eu.httpbin.org/stream-bytes/51200. Then your browser will download a file which length is 51200 bytes. Paste it into “public” directory (to measure speed to a hosting).

Now we need to check how much the file downloads. Let’s file a simple function for this, which will return the speed in megabytes per second.

async checkDownloadSpeed(baseUrl, fileSizeInBytes) {
return new Promise((resolve, _) => {
let startTime = new Date().getTime();
return axios.get(baseUrl).then( response => {
const endTime = new Date().getTime();
const duration = (endTime - startTime) / 1000;
const bytesPerSecond = (fileSizeInBytes / duration);
const megabytesPerSecond = (bytesPerSecond / 1000 / 1000);
resolve(megabytesPerSecond);
});
}).catch(error => {
throw new Error(error);
});
}

So we have just set the time of beginning of downloading, the time of finishing the downloading, measure the difference, and, as we know the size of file, just divide.

Now let’s write a function which will do something with this speed:

async getNetworkDownloadSpeed() {
const baseUrl = process.env.PUBLIC_URL + ‘/51200’;
const fileSize = 51200;
const speed = await this.checkDownloadSpeed(baseUrl, fileSize);
console.log(“Network speed: “ + speed);
if (speed.mbps === “Infinity”) {
SpeedMeasure.speed = 1;
}
else {
SpeedMeasure.speed = speed * 5;
}
}

Actually there exists a problem with this code: an exact speed of internet connection is not determined because of the small size of dowloaded file. But we cannot download something bigger, so we just multiply determined speed by 5. Actually it will be even less than the right speed.

Now let’s write a function, which will set the quality depending on the speed:

static getResolution(gifsArray) {
let totalSizeLevel1 = 0;
let totalSizeLevel2 = 0;
let totalSizeLevel3 = 0;
for (let i = 0; i < gifsArray.length; i++) {
for (let a = 0; a < gifsArray[i].length; a++) {
let element = gifsArray[i][a];
totalSizeLevel1 += element.small.size;
totalSizeLevel2 += element.middle.size;
totalSizeLevel3 += element.large.size;
}
}
if (isNaN(SpeedMeasure.speed)) {
SpeedMeasure.speed = 1;
}
let timeLevel1 = totalSizeLevel1 / SpeedMeasure.speed;
let timeLevel2 = totalSizeLevel2 / SpeedMeasure.speed;
let timeLevel3 = totalSizeLevel3 / SpeedMeasure.speed;
if (timeLevel3 < APPROPRIATE_TIME_LIMIT) {
return "large";
}
else if (timeLevel2 < APPROPRIATE_TIME_LIMIT) {
return "middle";
}
else {
return "small";
}
}

Since the function that counts the speed is asynchronous, SpeedMeasure.speed can be NaN. By default, we believe that the connection speed is 1 megabyte per second. When the function counts the speed, we just re-render the container.

We pass an array of arrays to the getResolution function. Why? Because if we have several containers with gifs on the page, it is more convenient for us to transfer the corresponding content to each of them as an array, but it is necessary to consider the speed of loading for all at once.

An example of usage

Here is an example of usage (React):

async runFunction() {
let speedMeasure = new SpeedMeasure();
await speedMeasure.getNetworkDownloadSpeed();
this.forceUpdate()
}
componentDidMount() {
this.runFunction();
}
render() {
let quality = SpeedMeasure.getResolution([Control.getControlArray(),Health.getHealthArray()]);
return (
<div className="app">
<Presentation />
<Control quality={quality} />
<Health quality={quality} />
</div>
);
}

So when everything will be downloaded and the speed will be measured, container will be rerendered.

Inside the container (inside Control, for example) I just take a corresponding gif from an array (by index), and then I just get an object by “quality” key and by key “url” I get a link. Actually simple.

Writing a Python script:

Now I need to somehow compress gifs and output an array with description of content.

At first let’s write a script for compressing gifs. We will use gifsicle. For gifs of “middle” quality the compression rate will be 80 (out of 200), for “small” will be 160.

import osGIFS_DIR = "/home/mixeden/Документы/Landingv2/"
COMPRESSOR_DIR = "/home/mixeden/Документы/gifsicle-static"
NOT_OPTIMIZED_DIR = "not_optimized"
OPTIMIZED_DIR = "optimized"
GIF_RESIZED_DIR = "gif_not_optimized_resized"
GIF_COMPRESSED_DIR = "gif_compressed"
COMPRESSION_TYPE = ["middle", "small"]
for (root, dirs, files) in os.walk(GIFS_DIR, topdown=True):
if len(files) > 0 and GIF_RESIZED_DIR in root:
for file in files:
path = root + "/" + file
for compression in COMPRESSION_TYPE:
final_path = path.replace(GIF_RESIZED_DIR, GIF_COMPRESSED_DIR + "/" + compression + "/" + OPTIMIZED_DIR)
print(path, final_path)
if compression == COMPRESSION_TYPE[0]:
rate = 80
else:
rate = 160
os.system("echo 0 > " + final_path)
os.system(COMPRESSOR_DIR + " -O3 --lossy={} -o {} {}".format(rate, final_path, path))

Just for you to understand the structure for my file system, here is a description:

  1. NOT_OPTIMIZED_DIR — unoptimized gifs
  2. GIF_RESIZED_DIR — unoptimized gifs, but resized according to the size of containers of pages
  3. GIF_COMPRESSED_DIR — compressed gifs
  4. Inside the directories there are folders with names of categories of gifs. Inside the folder of category there are folders «large», «middle» и «small» (according to types of qualities).

In script we just go through the directory with gifs and we compress each file with an appropriate command.

Now let’s write a script which generates an array with information.

import json import osGIFS_DIR = "/home/mixeden/Документы/Landingv2/"
COMPRESSOR_DIR = "/home/mixeden/Документы/gifsicle-static"
NOT_OPTIMIZED_DIR = "not_optimized"
OPTIMIZED_DIR = "optimized"
GIF_RESIZED_DIR = "gif_not_optimized_resized"
GIF_COMPRESSED_DIR = "gif_compressed"
COMPRESSION_TYPE = ["large", "middle", "small"]
OUTPUT = {}
for (root, dirs, files) in os.walk(GIFS_DIR, topdown=True):
if len(files) > 0 and GIF_COMPRESSED_DIR in root and NOT_OPTIMIZED_DIR not in root:
files.sort()
type = root.split(GIFS_DIR)[1].split(GIF_COMPRESSED_DIR)[0].replace("/", "")
print(type)
if type not in OUTPUT:
OUTPUT[type] = []
if len(OUTPUT[type]) == 0:
for file in files:
OUTPUT[type].append(
{
"large": {
"url": "",
"size": 0
},
"middle": {
"url": "",
"size": 0
},
"small": {
"url": "",
"size": 0
}
})
for file in files:
full_path = root + "/" + file
bytes_size = os.path.getsize(full_path)
kilobytes_size = bytes_size / 1000
megabytes_size = kilobytes_size / 1000
index = int(file.split("_")[1].replace(".gif", "")) - 1

for typer in COMPRESSION_TYPE:
if typer in root:
local_type = typer

new_url = "gifs/" + full_path.replace(GIFS_DIR, "").replace("/" + GIF_COMPRESSED_DIR, "").replace("/" + OPTIMIZED_DIR, "")
OUTPUT[type][index][local_type]['url'] = new_url
OUTPUT[type][index][local_type]['size'] = megabytes_size

print(OUTPUT)
print(json.dumps(OUTPUT, indent=4, sort_keys=True))

Here we go through the folders, determine the size of the each file, find out the type of compression and put the information in the array. And then we output this array to the console (and then we can copy it).

Conclusion

I hope this article will help you in some way. I wish you a pleasant coding, guys.

--

--