906e1f23 by David LaPalomento

WIP: playlist-loader

1 parent 58952618
1 /**
2 * A state machine that manages the loading, caching, and updating of
3 * M3U8 playlists.
4 */
5 (function(window) {
6 'use strict';
7 var
8
9 /* XXX COPIED REMOVE ME */
10 /**
11 * Constructs a new URI by interpreting a path relative to another
12 * URI.
13 * @param basePath {string} a relative or absolute URI
14 * @param path {string} a path part to combine with the base
15 * @return {string} a URI that is equivalent to composing `base`
16 * with `path`
17 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
18 */
19 resolveUrl = function(basePath, path) {
20 // use the base element to get the browser to handle URI resolution
21 var
22 oldBase = document.querySelector('base'),
23 docHead = document.querySelector('head'),
24 a = document.createElement('a'),
25 base = oldBase,
26 oldHref,
27 result;
28
29 // prep the document
30 if (oldBase) {
31 oldHref = oldBase.href;
32 } else {
33 base = docHead.appendChild(document.createElement('base'));
34 }
35
36 base.href = basePath;
37 a.href = path;
38 result = a.href;
39
40 // clean up
41 if (oldBase) {
42 oldBase.href = oldHref;
43 } else {
44 docHead.removeChild(base);
45 }
46 return result;
47 },
48
49 PlaylistLoader = function(url) {
50 var
51 loader = this,
52 request;
53 if (!url) {
54 throw new Error('A non-empty playlist URL is required');
55 }
56 loader.state = 'HAVE_NOTHING';
57 request = new window.XMLHttpRequest();
58 request.open('GET', url);
59 request.onreadystatechange = function() {
60 var parser = new videojs.m3u8.Parser();
61 parser.push(this.responseText);
62
63 if (parser.manifest.playlists) {
64 loader.master = parser.manifest;
65 } else {
66 // infer a master playlist if none was previously requested
67 loader.master = {
68 playlists: [parser.manifest]
69 };
70 }
71 loader.state = 'HAVE_MASTER';
72 return;
73 };
74 request.send(null);
75 };
76
77 window.videojs.hls.PlaylistLoader = PlaylistLoader;
78 })(window);
1 (function(window) {
2 'use strict';
3 var
4 oldXhr,
5 requests,
6 videojs = window.videojs;
7
8 module('Playlist Loader', {
9 setup: function() {
10 oldXhr = window.XMLHttpRequest;
11 requests = [];
12
13 window.XMLHttpRequest = function() {
14 this.open = function(method, url) {
15 this.method = method;
16 this.url = url;
17 };
18 this.send = function() {
19 requests.push(this);
20 };
21 this.respond = function(response) {
22 this.responseText = response;
23 this.readyState = 4;
24 this.onreadystatechange();
25 };
26 };
27 this.send = function() {
28 };
29 },
30 teardown: function() {
31 window.XMLHttpRequest = oldXhr;
32 }
33 });
34
35 test('throws if the playlist url is empty or undefined', function() {
36 throws(function() {
37 videojs.hls.PlaylistLoader();
38 }, 'requires an argument');
39 throws(function() {
40 videojs.hls.PlaylistLoader('');
41 }, 'does not accept the empty string');
42 });
43
44 test('starts without any metadata', function() {
45 var loader = new videojs.hls.PlaylistLoader('master.m3u8');
46 strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet');
47 });
48
49 test('requests the initial playlist immediately', function() {
50 var loader = new videojs.hls.PlaylistLoader('master.m3u8');
51 strictEqual(requests.length, 1, 'made a request');
52 strictEqual(requests[0].url, 'master.m3u8', 'requested the initial playlist');
53 });
54
55 test('moves to HAVE_MASTER after loading a master playlist', function() {
56 var loader = new videojs.hls.PlaylistLoader('master.m3u8');
57 requests.pop().respond('#EXTM3U\n' +
58 '#EXT-X-STREAM-INF:\n' +
59 'media.m3u8\n');
60 ok(loader.master, 'the master playlist is available');
61 strictEqual(loader.state, 'HAVE_MASTER', 'the state is correct');
62 });
63
64 test('jumps to HAVE_METADATA when initialized with a media playlist', function() {
65 var loader = new videojs.hls.PlaylistLoader('media.m3u8');
66 requests.pop().respond('#EXTM3U\n' +
67 '#EXTINF:10,\n' +
68 '0.ts\n' +
69 '#EXT-X-ENDLIST\n');
70 ok(loader.master, 'infers a master playlist');
71 ok(loader.media, 'sets the media playlist');
72 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
73 strictEqual(0, requests.length, 'no more requests are made');
74 });
75
76 test('jumps to HAVE_METADATA when initialized with a live media playlist', function() {
77 var loader = new videojs.hls.PlaylistLoader('media.m3u8');
78 requests.pop().respond('#EXTM3U\n' +
79 '#EXTINF:10,\n' +
80 '0.ts\n');
81 ok(loader.master, 'infers a master playlist');
82 ok(loader.media, 'sets the media playlist');
83 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
84 });
85
86 test('moves to HAVE_METADATA after loading a media playlist', function() {
87 var loader = new videojs.hls.PlaylistLoader('master.m3u8');
88 requests.pop().respond('#EXTM3U\n' +
89 '#EXT-X-STREAM-INF:\n' +
90 'media.m3u8\n' +
91 'alt.m3u8\n');
92 strictEqual(requests.length, 1, 'requests the media playlist');
93 strictEqual(requests[0].method, 'GET', 'GETs the media playlist');
94 strictEqual(requests[0].url, 'media.m3u8', 'requests the first playlist');
95
96 requests.pop().response('#EXTM3U\n' +
97 '#EXTINF:10,\n' +
98 '0.ts\n');
99 ok(loader.master, 'sets the master playlist');
100 ok(loader.media, 'sets the media playlist');
101 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
102 });
103
104 test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() {
105 var loader = new videojs.hls.PlaylistLoader('live.m3u8');
106 requests.pop().response('#EXTM3U\n' +
107 '#EXTINF:10,\n' +
108 '0.ts\n');
109 loader.refreshMedia();
110 strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct');
111 strictEqual(requests.length, 1, 'requested playlist');
112 strictEqual(requests[0].url, 'live.m3u8', 'refreshes the media playlist');
113 });
114
115 test('returns to HAVE_METADATA after refreshing the playlist', function() {
116 var loader = new videojs.hls.PlaylistLoader('live.m3u8');
117 requests.pop().response('#EXTM3U\n' +
118 '#EXTINF:10,\n' +
119 '0.ts\n');
120 loader.refreshMedia();
121 requests.pop().response('#EXTM3U\n' +
122 '#EXTINF:10,\n' +
123 '1.ts\n');
124 strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct');
125 });
126 })(window);
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
28 <!-- M3U8 --> 28 <!-- M3U8 -->
29 <script src="../src/stream.js"></script> 29 <script src="../src/stream.js"></script>
30 <script src="../src/m3u8/m3u8-parser.js"></script> 30 <script src="../src/m3u8/m3u8-parser.js"></script>
31 <script src="../src/playlist-loader.js"></script>
31 <!-- M3U8 TEST DATA --> 32 <!-- M3U8 TEST DATA -->
32 <script src="../tmp/manifests.js"></script> 33 <script src="../tmp/manifests.js"></script>
33 <script src="../tmp/expected.js"></script> 34 <script src="../tmp/expected.js"></script>
...@@ -51,6 +52,7 @@ ...@@ -51,6 +52,7 @@
51 <script src="exp-golomb_test.js"></script> 52 <script src="exp-golomb_test.js"></script>
52 <script src="flv-tag_test.js"></script> 53 <script src="flv-tag_test.js"></script>
53 <script src="m3u8_test.js"></script> 54 <script src="m3u8_test.js"></script>
55 <script src="playlist-loader_test.js"></script>
54 </head> 56 </head>
55 <body> 57 <body>
56 <div id="qunit"></div> 58 <div id="qunit"></div>
......