Video in your Web Application
Gone are the days when our web applications were limited to text content, enriched by some colors and fonts. Today’s websites must include rich video and audio content — in order to appeal to the modern user — within a couple of seconds.
YouTube can provide you all the video streaming solutions you need — at a minimal cost and extreme scale. However, techies like to make things on their own — for the fun — to learn and understand the technology that goes into building such applications. If you are one of them, do come along as we dig into the core concepts and nitty-gritties of what goes into displaying video on a website.
Foremost, we need to host our web application on some backend. And what is better than AWS S3/CloudFront for hosting static content? All the examples below will use the same.
The Primitive
Once we have the CDN setup, life is just a play of HTML, CSS and JavaScript running in your browser. The most primitive solution comes with the video tag in HTML5. All you need is a simple HTML file containing the video tag, referencing the MP4 file lying back on your CDN.
<!DOCTYPE html>
<html>
<body>
<video width="320" height="240" controls>
<source src="relative.path.to.the.video.mp4" type="video/mp4">
</video>
</body>
</html>
This will display the video on the page. It will ensure the width and height of the output is restricted, and also provide the necessary controls to let the user play around with it.
If you have a decent network connection and the video is small enough, it will work without any problem — leaving us wondering why do we need anything beyond this?
Smooth Streaming
To understand this, choose a large video — few hours long. Try the above approach to show this video on your website. This will take some time to load, even on a high-speed network.
That is because, by default, CloudFront treats your MP4 video as an ordinary file, and sends it across as an ordinary file. That means, the browser has to receive the entire MP4 file on the network before it can start playing the video. This can be a long time if the video is large.
Certainly not a good user experience, as your users will not stay on the website beyond a few seconds — certainly not if they see slow loading videos. CloudFront provides us a simple solution to this problem. That is smooth streaming. It is a simple binary configuration in the Behavior tab.
When we set it to yes, the files are delivered in smooth streams. That means, our browser can start playing the video as soon as it receives the first chunk of the data — it need not wait for the entire file.
Try making this change and you can see the amazing improvement in the latency.
Layered Content
There was some improvement, however we still have a few open problems. Think of the data moving out of AWS — that directly impacts your bill. If you have a four-hour long video — a few hundred MB, every user visiting the website will trigger a GET on that huge chunk of data. The user may move away after watching a few seconds, but you will have to pay for the entire size. Isn’t that wasteful?
Another problem is the availability of bandwidth. Some users have the luxury of a high-speed network connection. Others are busy travelers who land on your website while waiting on a crowded airport. You want to ensure optimal experience for both.
You can always break your long video into a playlist of smaller pieces. You can write a web component that senses the network speed and pulls individual pieces from the playlist and gives a seamless experience to the end-user. Such a component is not difficult to develop. In fact, you can try it out for the fun.
However, someone has done that job for us. M3U8 is a wonderful standard for doing just this. If we convert the Mp4 to M3U8, we can get the browser to manage all the details for us. M3U8 is a special format that maintains a playlist of individual files. Thus, we do not download the entire video at once. We pull small chunks — only one at a time.
However, the M3U8 player is not available out in most browsers. This is one of the rare features where Microsoft leads the game. Only IE Edge can play the M3U8 files, out of the box. All other browsers require external support.
One very good library is video.js. The code below shows how video.js is used to play the M3U8 files.
<html>
<head>
<link href="https://vjs.zencdn.net/8.6.0/video-js.css" rel="stylesheet" />
</head>
<body>
<video id="my-video"
class="video-js"
controls
preload="auto"
width="640"
height="264"
controls
data-setup="{}">
<source src="relative.path.to.m3u8" type="application/x-mpegURL" />
</video>
<script src="https://vjs.zencdn.net/8.6.0/video.min.js"></script>
</body>
</html>
Creating M3U8
Technology cannot eliminate complexity. It just transfers it from the user to developer. If M3U8 solves a lot of problems on the field, it is surely not so easy to create. In fact, unlike the mp4 or other video formats, M3U8 is not limited to one file. We have a chunk of files generated for one video. Let us see how we can create these M3U8 files.
Most of the complexity here is absorbed by AWS. We just invoke their services, and our job is done. We can use the AWS Media Converter for this purpose.
The code below can generate an M3U8 files out of the input MP4 file.
const mediaConvertClient = new MediaConvertClient({
endpoint: 'https://uniquename.mediaconvert.ap-south-1.amazonaws.com',
});
const params: CreateJobCommandInput = {
ClientRequestToken: nanoid(),
Role: "arn:aws:iam::1234567890:role/service-role/MediaConvert_Default_Role",
Settings: {
Inputs: [{
FileInput: "s3://source-s3-bucket/input-video.mp4",
}],
OutputGroups: [
{
CustomName: 'custom',
Name: 'name',
OutputGroupSettings: {
HlsGroupSettings: {
Destination: "s3://output-s3-bucket/output-name",
SegmentLength: 10,
MinSegmentLength: 0,
},
Type: 'HLS_GROUP_SETTINGS',
},
Outputs: [
{
VideoDescription: {
CodecSettings: {
Codec: 'H_264',
H264Settings: {
InterlaceMode: 'PROGRESSIVE',
NumberReferenceFrames: 3,
Syntax: 'DEFAULT',
Softness: 0,
GopClosedCadence: 1,
GopSize: 48,
Slices: 1,
GopBReference: 'DISABLED',
SlowPal: 'DISABLED',
SpatialAdaptiveQuantization: 'ENABLED',
TemporalAdaptiveQuantization: 'ENABLED',
FlickerAdaptiveQuantization: 'DISABLED',
EntropyEncoding: 'CABAC',
Bitrate: 4500000,
FramerateControl: 'SPECIFIED',
RateControlMode: 'CBR',
CodecProfile: 'HIGH',
Telecine: 'NONE',
MinIInterval: 0,
AdaptiveQuantization: 'HIGH',
CodecLevel: 'LEVEL_4_1',
FieldEncoding: 'PAFF',
SceneChangeDetect: 'ENABLED',
QualityTuningLevel: 'SINGLE_PASS_HQ',
FramerateConversionAlgorithm: 'DUPLICATE_DROP',
UnregisteredSeiTimecode: 'DISABLED',
GopSizeUnits: 'FRAMES',
ParControl: 'INITIALIZE_FROM_SOURCE',
NumberBFramesBetweenReferenceFrames: 3,
RepeatPps: 'DISABLED',
HrdBufferSize: 9000000,
HrdBufferInitialFillPercentage: 90,
FramerateNumerator: 24000,
FramerateDenominator: 1001,
},
},
},
ContainerSettings: {
Container: 'M3U8',
M3u8Settings: {},
},
Extension: 'm3u8',
NameModifier: nanoid(),
},
],
},
],
},
};
const response = await mediaConvertClient.send(new CreateJobCommand(params));
Run this code snippet on the console, with sufficient permissions (or in a Lambda function). It will create a new Job in the Media Convert, which will process the input file and generate the M3U8 file (along with a bunch of T1 files) in the output s3 bucket. You can view the progress of the job on the Media Converter console.