UNPKG

16.1 kBMarkdownView Raw
1# A Walk Through VHS
2
3Today we're going to take a walk through VHS. We'll start from a manifest URL and end with video playback.
4
5The purpose of this walk is not to see every piece of code, or define every module. Instead it's about seeing the most important parts of VHS. The goal is to make VHS more approachable.
6
7Lets start with a video tag:
8
9```html
10<video>
11 <source src="http://example.com/manifest.m3u8" type="application/x-mpegURL">
12</video>
13```
14
15The source, `manifest.m3u8`, is an HLS manifest. You can tell from the `.m3u8` extension and the `type`.
16
17Safari (and a few other browsers) will play that video natively, because Safari supports HLS content. However, other browsers don't support native playback of HLS and will fail to play the video.
18
19VHS provides the ability to play HLS (and DASH) content in browsers that don't support native HLS (and DASH) playback.
20
21Since VHS is a part of Video.js, let's set up a Video.js player for the `<video>`:
22
23```html
24<link href="//vjs.zencdn.net/7.10.2/video-js.min.css" rel="stylesheet">
25<script src="//vjs.zencdn.net/7.10.2/video.min.js"></script>
26
27<video-js id="myPlayer" class="video-js" data-setup='{}'>
28 <source src="http://example.com/manifest.m3u8" type="application/x-mpegURL">
29</video-js>
30```
31
32Video.js does a lot of things, but in the context of VHS, the important feature is a way to let VHS handle playback of the source. To do this, VHS is registered as a Video.js Source Handler. When a Video.js player is created and provided a `<source>`, Video.js goes through its list of registered Source Handlers, including VHS, to see if they're able to play that source.
33
34In this case, because it's an HLS source, VHS will tell Video.js "I can handle that!" From there, VHS is given the URL and it begins its process.
35
36## videojs-http-streaming.js
37
38`VhsSourceHandler` is defined at the [bottom of src/videojs-http-streaming.js](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/videojs-http-streaming.js#L1226-L1233).
39
40The function which Video.js calls to see if the `VhsSourceHandler` can handle the source is aptly named `canHandleSource`.
41
42Inside `canHandleSource`, VHS checks the source's `type`. In our case, it sees `application/x-mpegURL`, and, if we're running in a browser with MSE, then it says "I can handle it!" (It actually says "maybe," because in life there are few guarantees, and because the spec says to use "maybe.")
43
44### VhsSourceHandler
45
46Since VHS told Video.js that it can handle the source, Video.js passes the source to `VhsSourceHandler`'s `handleSource` function. That's where VHS really gets going. It creates a new `VhsHandler` object, merges some options, and performs initial setup. For instance, it creates listeners on some `tech` events.
47
48```mermaid
49flowchart TD
50 VhsSourceHandler --> VhsHandler
51```
52
53> :information_source: **What should be put in VhsHandler?**
54>
55> videojs-http-streaming.js is a good place for interfacing with Video.js and other plugins, isolating integrations from the rest of the code.
56>
57> Here are a couple of examples of what's done within videojs-http-streaming.js:
58> * most EME handling for DRM and setup of the [videojs-contrib-eme plugin](https://github.com/videojs/videojs-contrib-eme)
59> * mapping/handling of options passed down via Video.js
60
61## MasterPlaylistController
62
63One critical object that `VhsHandler`'s constructor creates is a new `MasterPlaylistController`.
64
65```mermaid
66flowchart TD
67 VhsSourceHandler --> VhsHandler
68 VhsHandler --> MasterPlaylistController
69```
70
71`MasterPlaylistController` is not a great name, and has grown in size to be a bit unwieldy, but it's the hub of VHS. Eventually, it should be broken into smaller pieces, but for now, it handles the creation and management of most of the other VHS modules. Its code can be found in [src/master-playlist-controller.js](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/master-playlist-controller.js).
72
73The best way to think of `MasterPlaylistController` is as Tron's Master Control Program, though hopefully it isn't as evil.
74
75`MasterPlaylistController` is a lot to say. So we often refer to it as MPC.
76
77If you need to find a place where different modules communicate, you will probably end up in MPC. Just about all of `VhsHandler` that doesn't interface with Video.js or other plugins, interfaces with MPC.
78
79MPC's [constructor](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/master-playlist-controller.js#L148) does a lot. Instead of listing all of the things it does, let's go step-by-step through the main ones, passing the source we had above.
80
81```html
82<video-js id="myPlayer" class="video-js" data-setup='{}'>
83 <source src="http://example.com/manifest.m3u8" type="application/x-mpegURL">
84</video-js>
85```
86
87Looking at the `<source>` tag, `VhsSourceHandler` already used the "type" to tell Video.js that it could handle the source. `VhsHandler` took the manifest URL, in this case "manifest.m3u8" and provided it to the constructor of MPC.
88
89The first thing that MPC must do is download that source, but it doesn't make the request itself. Instead, it creates [this.masterPlaylistLoader_](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/master-playlist-controller.js#L264-L266).
90
91```mermaid
92flowchart TD
93 VhsSourceHandler --> VhsHandler
94 VhsHandler --> MasterPlaylistController
95 MasterPlaylistController --> PlaylistLoader
96```
97
98`masterPlaylistLoader_` is an instance of either the [HLS PlaylistLoader](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/playlist-loader.js#L379) or the [DashPlaylistLoader](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/dash-playlist-loader.js#L259).
99
100The names betray their use. They load the playlist. The URL ("manifest.m3u8" here) is given, and the manifest/playlist is downloaded and parsed. If the content is live, the playlist loader also handles refreshing the manifest. For HLS, where manifests point to other manifests, the playlist loader requests those as well.
101
102As for parsing, for HLS, the manifest responses are parsed using [m3u8-parser](https://github.com/videojs/m3u8-parser). For DASH, the manifest response is parsed using [mpd-parser](https://github.com/videojs/mpd-parser). The output of these parsers is a JSON object that VHS understands. The main structure can be seen in the READMEs, e.g., [here](https://github.com/videojs/m3u8-parser#parsed-output).
103
104So what was once a URL in a `<source>` tag was requested and parsed into a JSON object like the following:
105
106```
107Manifest {
108 playlists: [
109 {
110 attributes: {},
111 Manifest
112 }
113 ],
114 mediaGroups: { ... },
115 segments: [ ... ],
116 ...
117}
118```
119
120Many properties are removed for simplicity. This is a top level manifest (often referred to as a master or main manifest), and within it there are playlists, each playlist being a Manifest itself. Since the JSON "schema" for main and media playlists is the same, you will see irrelevant properties within any given manifest object. For instance, you might see a `targetDuration` property on the main manifest object, though a main manifest doesn't have a target duration. You can ignore irrelevant properties. Eventually they should be cleaned up, and a proper schema defined for manifest objects.
121
122MPC will also use `masterPlaylistLoader_` to select which media playlist is active (e.g., the 720p rendition or the 480p rendition), so that `masterPlaylistLoader_` will only need to refresh that individual playlist if the stream is live.
123
124> :information_source: **Future Work**
125>
126> The playlist loaders are not the clearest modules. Work has been started on improvements to the loaders and how we use them: https://github.com/videojs/http-streaming/pull/1208
127>
128> That work makes them much easier to read, but will require changes throughout the rest of the code before the old PlaylistLoader and DashPlaylistLoader code can be removed.
129
130### Media Source Extensions
131
132The next thing MPC needs to do is set up a media source for [Media Source Extensions](https://www.w3.org/TR/media-source/). Specifically, it needs to create [this.mediaSource](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/master-playlist-controller.js#L210) and its associated [source buffers](https://github.com/videojs/http-streaming/blob/main/src/master-playlist-controller.js#L1818). These are where audio and video data will be appended, so that the browser has content to play. But those aren't used directly. Because source buffers can only handle one operation at a time, [this.sourceUpdater_](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/master-playlist-controller.js#L234) is created. `sourceUpdater_` is a queue for operations performed on the source buffers. That's pretty much it. So all of the MSE pieces for appending get wrapped up in `sourceUpdater_`.
133
134## Segment Loaders
135
136The SourceUpdater created for MSE above is passed to the segment loaders.
137
138[this.mainSegmentLoader_](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/master-playlist-controller.js#L271-L275) is used for muxed content (audio and video in one segment) and for audio or video only streams.
139
140[this.audioSegmentLoader_](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/master-playlist-controller.js#L278-L281) is used when the content is demuxed (audio and video in separate playlists).
141
142```mermaid
143flowchart TD
144 VhsSourceHandler --> VhsHandler
145 VhsHandler --> MasterPlaylistController
146 MasterPlaylistController --> PlaylistLoader
147 MasterPlaylistController --> SourceUpdater
148 MasterPlaylistController --> SegmentLoader
149```
150
151Besides options and the `sourceUpdater_` from MPC, the segment loaders are given a [playlist](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/segment-loader.js#L988). This playlist is a media playlist from the `masterPlaylistLoader_`. So looking back at our parsed manifest object:
152
153```
154Manifest {
155 playlists: [
156 {
157 attributes: {},
158 Manifest
159 }
160 ],
161 mediaGroups: { ... },
162 segments: [ ... ],
163 ...
164}
165```
166
167The media playlists were those objects found in the `playlists` array. Each segment loader is given one of those.
168
169Segment Loader uses the provided media playlist to determine which segment to download next. It performs this check when [monitorBuffer_](https://github.com/videojs/http-streaming/blob/main/src/segment-loader.js#L1300) is called, which ultimately runs [chooseNextRequest_](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/segment-loader.js#L1399). `chooseNextRequest_` looks at the buffer, the current time, and a few other properties to choose what segment to download from the `playlist`'s `segments` array.
170
171### Choosing Segments to Download
172
173VHS uses a strategy called `mediaIndex++` for choosing the next segment, see [here](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/segment-loader.js#L1442). This means that, if segment 3 was previously requested, segment 4 should be requested next, and segment 5 after that. Those segment numbers are determined by the HLS [#EXT-X-MEDIA-SEQUENCE tag](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-10#section-4.4.3.2).
174
175If there are no seeks or rendition changes, `chooseNextRequest_` will rely on the `mediaIndex++` strategy.
176
177If there are seeks or rendition changes, then `chooseNextRequest_` will look at segment timing values via the `SyncController` (created previously in MPC), the current time, and the buffer, to determine what the next segment should be, and what it's start time should be (to position it on the timeline).
178
179```mermaid
180flowchart TD
181 VhsSourceHandler --> VhsHandler
182 VhsHandler --> MasterPlaylistController
183 MasterPlaylistController --> PlaylistLoader
184 MasterPlaylistController --> SourceUpdater
185 MasterPlaylistController --> SegmentLoader
186 SegmentLoader --> SyncController
187```
188
189The `SyncController` has various [strategies](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/sync-controller.js#L16) for ensuring that different renditions, which can have different media sequence and segment timing values, can be positioned on the playback timeline successfully. (It is also be [used by MPC](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/master-playlist-controller.js#L1477) to establish a `seekable` range.)
190
191### Downloading and Appending Segments
192
193If the buffer is not full, and a segment was chosen, then `SegmentLoader` will download and append it. It does this via a [mediaSegmentRequest](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/segment-loader.js#L2489).
194
195
196```mermaid
197flowchart TD
198 VhsSourceHandler --> VhsHandler
199 VhsHandler --> MasterPlaylistController
200 MasterPlaylistController --> PlaylistLoader
201 MasterPlaylistController --> SourceUpdater
202 MasterPlaylistController --> SegmentLoader
203 SegmentLoader --> SyncController
204 SegmentLoader --> mediaSegmentRequest
205```
206
207`mediaSegmentRequest` takes a lot of arguments. Most are callbacks. These callbacks provide the data that `SegmentLoader` needs to append the segment. It includes the timing information of the segment, captions, and the segment data.
208
209When the `SegmentLoader` receives timing info events, it can update the source buffer's timestamp offset (via `SourceUpdater`).
210
211When the `SegmentLoader` receives segment data events, it can append the data to the source buffer (via `SourceUpdater`).
212
213```mermaid
214flowchart TD
215 VhsSourceHandler --> VhsHandler
216 VhsHandler --> MasterPlaylistController
217 MasterPlaylistController --> PlaylistLoader
218 MasterPlaylistController --> SourceUpdater
219 MasterPlaylistController --> SegmentLoader
220 SegmentLoader --> SyncController
221 SegmentLoader --> mediaSegmentRequest
222 SegmentLoader --> SourceUpdater
223```
224
225## mediaSegmentRequest
226
227We talked a bit about how `SegmentLoader` uses `mediaSegmentRequest`, but what does [mediaSegmentRequest](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/media-segment-request.js#L941) do?
228
229Besides downloading segments, `mediaSegmentRequest` [decrypts](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/media-segment-request.js#L621) AES encrypted segments, probes [MP4](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/media-segment-request.js#L171) and [TS](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/media-segment-request.js#L375) segments for timing info, and [transmuxes](https://github.com/videojs/http-streaming/blob/0964cb4827d9e80aa36f2fa29e35dad92ca84111/src/media-segment-request.js#L280) TS segments into MP4s using [mux.js](https://github.com/videojs/mux.js) so they can be appended to the source buffers.
230
231```mermaid
232flowchart TD
233 VhsSourceHandler --> VhsHandler
234 VhsHandler --> MasterPlaylistController
235 MasterPlaylistController --> PlaylistLoader
236 MasterPlaylistController --> SourceUpdater
237 MasterPlaylistController --> SegmentLoader
238 SegmentLoader --> SyncController
239 SegmentLoader --> mediaSegmentRequest
240 SegmentLoader --> SourceUpdater
241 mediaSegmentRequest --> mux.js
242```
243
244## Video playback begins
245
246The video can start playing as soon as there's enough audio and video (for muxed streams) in the buffer to move the playhead forwards. So playback may begin before the `SegmentLoader` completes its full cycle.
247
248But once `SegmentLoader` does finish, it starts the process again, looking for new content.
249
250There are other modules, and other functions of the code (e.g., blacklisting logic, ABR, etc.), but this is the most critical path of VHS, the one that allows video to play in the browser.