HLS & DASH Video Streaming
Overview
We all have used media platforms likes YouTube, Netflix, PrimeVideo, DailyMotion etc. Multi-media industry has evolved over the period of time and has been one of the main area of technical advancement. Multi-media solutions, platforms and technology has moved at rapid pace in last decade or so. Considering the adoption rate for newer technologies, media domain stands first and should be followed by ecommerce etc. With everyone getting access to high speed internet, demand for consuming media content particularly videos has rapidly increased.
There have been many solutions to tackle this problem. Flash
, HLS
and DASH
are some of the techniques which are famous. Though flash
is not used much these days, but it served its purpose when needed :stuck_out_tongue_winking_eye: In this article our focus would be mainly on HLS
& DASH
technique. I won't go much in depth of technical implementation as we can get best knowledge from official documentation and wiki links. Instead, I would rather focus on how to implement a simple video streaming server using these 2 technologies and consume it in chrome browser.
Multi-media is very huge and advance topic. It is not possible to cover even surfaces of it. We will stick to the basic requirement of server implementation.
HLS & DASH
Index file i.e .mpd
or .m3u8
file here contains the definition of the other files and format which are sent over the network. This index file is what is sent at the very beginning of the request
to the client. Then the specific client which have the capability to understand, decode and operate takes the control over. Don't interchange HLS
and DASH
, they are 2 different approaches and have their respective significance.
In short, non-technical basic steps are followed:
- Generate index files
- Send over network
- Capable client reads this file
- Try to read chuks which are specified in index file
- Plays videos and provide other options available.
More information here:
MIME Type and Extension
Note that these files have their special mime type
.
For HLS:
.m3u8
index file extension, it isapplication/x-mpegURL
.ts
media segment, it isvideo/MP2T
For DASH:
.mpd
index file, it isapplication/dash+xml
.m4s
media segment, it isvideo/mp4
Generating Video sequences and its index file
For our use-case we are considering streaming a static content i.e we will create segments from main file and distribute it from our server. To create hls index file and segments run following command
For HLS:
1ffmpeg -i main.mp4
2 -profile:v baseline -level 3.0 -s 640x360
3 -start_number 0 -hls_time 10 -hls_list_size 0
4 -f hls main_hls_output/main.m3u8
For DASH:
1ffmpeg -i main.mp4
2 -profile:v baseline -level 3.0 -s 640x360
3 -start_number 0 -hls_time 10 -hls_list_size 0
4 -f dash main_mpd_output/main.mpd
This command assumes that you have ffmpeg
in classpath
Serving File from server
Once we have created sequences and its index file. Let's share them over network.
For simple serving purpose we are using micronaut
java web framework.
Let's create:
- Controller for handling request for (index, segment and small config)
- A file manager to deal with index and segment files
Content for HlsController.java
is as follows:
1@Controller("/hls")
2public class HlsController {
3
4 @Value("${config.video.hls.folder}")
5 private String folder;
6
7 @Value("${config.video.hls.index}")
8 private String index;
9
10 private FileManager fileManager;
11
12 @Inject
13 public HlsController(FileManager fileManager) {
14 this.fileManager = fileManager;
15 }
16
17 @PostConstruct
18 public void initFields() {
19 fileManager.init(folder, index);
20 }
21
22 @Get("index.m3u8")
23 @Produces("application/x-mpegURL")
24 public File index() {
25 return fileManager.getIndex();
26 }
27
28 @Get(uri = "/{filename}")
29 @Produces("video/MP2T")
30 public File getFile(String filename) {
31 return fileManager.getFile(filename);
32 }
33
34 @Get("/config")
35 @Produces(MediaType.APPLICATION_JSON)
36 public Map<String, String> config() {
37 final HashMap<String, String> config = new HashMap<>();
38 config.put("folder", fileManager.getFolder().getAbsolutePath());
39 config.put("index", fileManager.getIndex().getAbsolutePath());
40 return config;
41 }
42
43 @Get
44 @Produces(MediaType.APPLICATION_JSON)
45 public Map<String, Object> list() {
46 final HashMap<String, Object> response = new HashMap<>();
47 response.put("type", "HLS");
48 response.put("files", fileManager.list());
49 return response;
50 }
51}
Content for DashController.java
is as follows:
1@Controller("/dash")
2public class DashController {
3
4 @Value("${config.video.dash.folder}")
5 private String folder;
6
7 @Value("${config.video.dash.index}")
8 private String index;
9 private FileManager fileManager;
10
11 @Inject
12 public DashController(FileManager fileManager) {
13 this.fileManager = fileManager;
14 }
15
16 @PostConstruct
17 public void postInit() {
18 this.fileManager.init(folder, index);
19 }
20
21 @Get("index.mpd")
22 @Produces("application/dash+xml")
23 public File index() {
24 return fileManager.getIndex();
25 }
26
27 @Get(uri = "/{filename}")
28 @Produces("video/mp4")
29 public File getFile(String filename) {
30 return fileManager.getFile(filename);
31 }
32
33 @Get("/config")
34 @Produces(MediaType.APPLICATION_JSON)
35 public Map<String, String> config() {
36 final HashMap<String, String> config = new HashMap<>();
37 config.put("folder", fileManager.getFolder().getAbsolutePath());
38 config.put("index", fileManager.getIndex().getAbsolutePath());
39 return config;
40 }
41
42 @Get
43 @Produces(MediaType.APPLICATION_JSON)
44 public Map<String, Object> list() {
45 final HashMap<String, Object> response = new HashMap<>();
46 response.put("type", "DASH");
47 response.put("files", fileManager.list());
48 return response;
49 }
50}
Our FileManager.java
per instance for each Controller
is as follows:
1@Prototype
2public class FileManager {
3
4 @Getter
5 private File folder;
6
7 @Getter
8 private File index;
9
10 public void init(String folder, String index) {
11 this.folder = new File(folder);
12 this.index = new File(this.folder, index);
13 }
14
15 public File getFile(String filename) {
16 return new File(this.folder, filename);
17 }
18
19 public List<String> list() {
20 return folder != null ?
21 Stream.of(folder.list()).collect(Collectors.toList()) :
22 Collections.emptyList();
23 }
24
25}
So far, we have created controller, a file manager and serving these from following urls:
- http://localhost:8080/dash/
- http://localhost:8080/hls/
Config
For controller to access config folder and file values, we need to provide it in application.yml
file, here are the file contents:
1---
2micronaut:
3 application:
4 name: Video Server
5---
6
7config:
8 video:
9 dash:
10 folder: D:\silentsudo\video_streaming\main_mpd_output\
11 index: main.mpd
12 hls:
13 folder: D:\silentsudo\video_streaming\main_hls_output\
14 index: main.m3u8
main_mpd_output
or main_hls_output
folder is the place where our generated index file and segment are present(generated from ffmpeg
command above). If this folder is empty then we will no be able to see video stream happening.
Test with chrome browser and extension
Once we have our server up, running and serving all files in proper content type. Let's test it out.
For simple testing, install Chrome Plugin https://chrome.google.com/webstore/detail/native-mpeg-dash-%2B-hls-pl/cjfbmleiaobegagekpmlhmaadepdeedn
and paste
this link http://localhost:8080/hls/main.m3u8
in address bar and hit enter.
You should be able to see video stream playing. Alternative to see what is going on in background press ctrl+shit+i
in chrome browser to open developer mode, switch to network tab and reload page. You should be able to see files http://localhost:8080/dash/mainX.ts
being downloaded in the order specified in .m3u8
file.
Enjoy :movie_camera:
comments powered by Disqus