vpstream 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vpstream/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """On-demand video streaming from a VPS."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ try:
8
+ __version__ = version("vpstream")
9
+ except PackageNotFoundError: # running from a source tree without an install
10
+ __version__ = "0.0.0+unknown"
vpstream/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from vpstream.cli import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Files served to browsers: pages, scripts and the bundled hls.js."""
@@ -0,0 +1,54 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>vpstream capability test</title>
7
+ <style>
8
+ html, body { margin: 0; background: #111; color: #eee; font: 22px/1.4 sans-serif; }
9
+ main { max-width: 1100px; margin: 0 auto; padding: 16px; }
10
+ h1 { font-size: 30px; margin: 8px 0 4px; }
11
+ p { margin: 6px 0; }
12
+ .muted { color: #aaa; }
13
+ button { font: inherit; font-size: 24px; padding: 14px 26px; margin: 6px 10px 6px 0;
14
+ background: #2a2a2a; color: #fff; border: 3px solid #555; border-radius: 10px; }
15
+ button:focus { outline: none; border-color: #ffd400; background: #3a3a10; }
16
+ button[disabled] { opacity: .4; }
17
+ #stage { font-size: 26px; margin: 12px 0; min-height: 1.4em; }
18
+ #player { position: relative; width: 640px; max-width: 100%; background: #000;
19
+ margin: 10px 0; height: 360px; }
20
+ #player video { width: 100%; height: 100%; display: block; }
21
+ #overlay { position: absolute; left: 0; right: 0; top: 6%; text-align: center;
22
+ pointer-events: none; }
23
+ #overlay span { background: rgba(0,0,0,.75); color: #ffd400; padding: 4px 12px;
24
+ font-size: 26px; border-radius: 6px; }
25
+ #ask { margin: 10px 0; }
26
+ #ask p { font-size: 24px; }
27
+ table { border-collapse: collapse; width: 100%; font-size: 18px; margin: 8px 0 18px; }
28
+ td, th { border-bottom: 1px solid #333; padding: 4px 8px; text-align: left; }
29
+ .pass { color: #6f6; } .fail { color: #f66; } .skip { color: #aaa; }
30
+ #keys { font-family: monospace; font-size: 18px; }
31
+ </style>
32
+ </head>
33
+ <body>
34
+ <main>
35
+ <h1>vpstream capability test</h1>
36
+ <p class="muted">Use the arrow keys to move and OK to press. Results are sent to the server as each part finishes.</p>
37
+ <div>
38
+ <button id="start">Start all tests</button>
39
+ <button id="speedonly">Speed test only</button>
40
+ </div>
41
+ <div id="stage">Loading…</div>
42
+ <div id="player"><div id="overlay"></div></div>
43
+ <div id="ask"></div>
44
+ <div id="controls"></div>
45
+ <p id="keys">Remote keys: press every button on the remote (arrows, OK, Back, Play/Pause, colors). Last key: none</p>
46
+ <h2>Results</h2>
47
+ <table id="results"><thead><tr><th>Test</th><th>Result</th><th>Detail</th></tr></thead><tbody></tbody></table>
48
+ <h2>Browser</h2>
49
+ <table id="env"><tbody></tbody></table>
50
+ </main>
51
+ <script src="/caps/hls.min.js"></script>
52
+ <script src="/caps/caps.js"></script>
53
+ </body>
54
+ </html>
@@ -0,0 +1,559 @@
1
+ /* vpstream capability test. Plain ES5 so older TV browsers can run it. */
2
+ (function () {
3
+ "use strict";
4
+
5
+ var PLAY_TIMEOUT = 12000;
6
+ var AUDIO_START_LIMIT = 30000;
7
+ var MEDIA = "/caps/media/";
8
+ var S = {
9
+ session: String(Date.now()) + "-" + String(Math.random()).slice(2, 8),
10
+ stage: "",
11
+ env: {},
12
+ support: [],
13
+ playback: [],
14
+ subtitles: null,
15
+ audio: null,
16
+ speed: null,
17
+ keys: []
18
+ };
19
+ var manifest = null;
20
+ var busy = false;
21
+
22
+ var HLS_TYPE = "application/vnd.apple.mpegurl";
23
+
24
+ function $(id) { return document.getElementById(id); }
25
+ function setStage(t) { $("stage").textContent = t; }
26
+
27
+ function mediaSource() {
28
+ return window.MediaSource || window.ManagedMediaSource || window.WebKitMediaSource;
29
+ }
30
+ function hlsJsOk() { return !!(window.Hls && Hls.isSupported()); }
31
+ function nativeHls(v) { return (v || document.createElement("video")).canPlayType(HLS_TYPE); }
32
+ function stop(v) { try { v.pause(); } catch (e) { } }
33
+ function clear(id) { $(id).innerHTML = ""; }
34
+
35
+ function send(stage) {
36
+ S.stage = stage;
37
+ try {
38
+ var x = new XMLHttpRequest();
39
+ x.open("POST", "/caps/report", true);
40
+ x.setRequestHeader("Content-Type", "application/json");
41
+ x.send(JSON.stringify(S));
42
+ } catch (e) { /* the page still shows results */ }
43
+ }
44
+
45
+ function getJSON(url, cb) {
46
+ var x = new XMLHttpRequest();
47
+ x.open("GET", url, true);
48
+ x.onload = function () {
49
+ try { cb(null, JSON.parse(x.responseText)); } catch (e) { cb(e); }
50
+ };
51
+ x.onerror = function () { cb(new Error("network error")); };
52
+ x.send();
53
+ }
54
+
55
+ function addRow(table, name, result, detail) {
56
+ var tr = document.createElement("tr");
57
+ var a = document.createElement("td"); a.textContent = name;
58
+ var b = document.createElement("td"); b.textContent = result; b.className = result;
59
+ var c = document.createElement("td"); c.textContent = detail || "";
60
+ tr.appendChild(a); tr.appendChild(b); tr.appendChild(c);
61
+ $(table).getElementsByTagName("tbody")[0].appendChild(tr);
62
+ }
63
+
64
+ function clearTable(table) { $(table).getElementsByTagName("tbody")[0].innerHTML = ""; }
65
+
66
+ function record(name, result, detail) {
67
+ S.playback.push({ name: name, result: result, detail: detail || "" });
68
+ addRow("results", name, result, detail);
69
+ }
70
+
71
+ /* ---------- focus navigation for TV remotes ---------- */
72
+
73
+ function buttons() {
74
+ var all = document.getElementsByTagName("button"), out = [];
75
+ for (var i = 0; i < all.length; i++) {
76
+ if (!all[i].disabled && all[i].offsetParent !== null) out.push(all[i]);
77
+ }
78
+ return out;
79
+ }
80
+
81
+ function moveFocus(step) {
82
+ var list = buttons();
83
+ if (!list.length) return;
84
+ var i = list.indexOf(document.activeElement);
85
+ i = i < 0 ? 0 : (i + step + list.length) % list.length;
86
+ list[i].focus();
87
+ }
88
+
89
+ function keyName(e) {
90
+ return (e.key || "?") + "/" + (e.keyCode || e.which || 0);
91
+ }
92
+
93
+ function noteKey(k) {
94
+ if (S.keys.indexOf(k) < 0) S.keys.push(k);
95
+ $("keys").textContent = "Remote keys seen: " + S.keys.join(" ");
96
+ }
97
+
98
+ document.addEventListener("keydown", function (e) {
99
+ noteKey(keyName(e));
100
+ var code = e.keyCode;
101
+ if (code === 37 || code === 38) { moveFocus(-1); e.preventDefault(); }
102
+ if (code === 39 || code === 40) { moveFocus(1); e.preventDefault(); }
103
+ });
104
+
105
+ // Keep Back on this page and record it as a key.
106
+ if (window.history && history.pushState) {
107
+ history.pushState({ caps: 1 }, "");
108
+ window.addEventListener("popstate", function () {
109
+ noteKey("Back(popstate)");
110
+ history.pushState({ caps: 1 }, "");
111
+ });
112
+ }
113
+
114
+ /* ---------- browser environment and codec support ---------- */
115
+
116
+ function envInfo() {
117
+ var v = document.createElement("video");
118
+ var env = {
119
+ userAgent: navigator.userAgent,
120
+ screen: screen.width + "x" + screen.height + " @" + (window.devicePixelRatio || 1),
121
+ viewport: window.innerWidth + "x" + window.innerHeight,
122
+ colorDepth: screen.colorDepth,
123
+ hdrDisplay: !!(window.matchMedia && matchMedia("(dynamic-range: high)").matches),
124
+ mediaSource: !!window.MediaSource,
125
+ managedMediaSource: !!window.ManagedMediaSource,
126
+ hlsJsSupported: hlsJsOk(),
127
+ hlsJsVersion: window.Hls ? Hls.version : "",
128
+ nativeHls: nativeHls(v) || "no",
129
+ audioTracksApi: "audioTracks" in v,
130
+ textTracksApi: "textTracks" in v,
131
+ playbackQualityApi: typeof v.getVideoPlaybackQuality === "function",
132
+ cookies: navigator.cookieEnabled,
133
+ fetchApi: typeof window.fetch === "function",
134
+ promise: typeof window.Promise === "function"
135
+ };
136
+ try { new Function("var f = (a) => a; class A {}"); env.es2015 = true; }
137
+ catch (e) { env.es2015 = false; }
138
+ S.env = env;
139
+ for (var k in env) {
140
+ if (Object.prototype.hasOwnProperty.call(env, k)) addRow("env", k, "", String(env[k]));
141
+ }
142
+ }
143
+
144
+ function supportInfo() {
145
+ var v = document.createElement("video");
146
+ var MS = mediaSource();
147
+ var types = [
148
+ ["H.264 High 8-bit", 'video/mp4; codecs="avc1.640028"'],
149
+ ["H.264 High10 10-bit", 'video/mp4; codecs="avc1.6e0028"'],
150
+ ["HEVC Main (hvc1)", 'video/mp4; codecs="hvc1.1.6.L120.90"'],
151
+ ["HEVC Main (hev1)", 'video/mp4; codecs="hev1.1.6.L120.90"'],
152
+ ["HEVC Main10", 'video/mp4; codecs="hvc1.2.4.L120.90"'],
153
+ ["HEVC Main10 4K", 'video/mp4; codecs="hvc1.2.4.L153.90"'],
154
+ ["AV1 8-bit", 'video/mp4; codecs="av01.0.08M.08"'],
155
+ ["AV1 10-bit", 'video/mp4; codecs="av01.0.08M.10"'],
156
+ ["AV1 10-bit 4K", 'video/mp4; codecs="av01.0.12M.10"'],
157
+ ["VP9", 'video/webm; codecs="vp9"'],
158
+ ["AAC", 'audio/mp4; codecs="mp4a.40.2"'],
159
+ ["AC-3", 'audio/mp4; codecs="ac-3"'],
160
+ ["E-AC-3", 'audio/mp4; codecs="ec-3"'],
161
+ ["Opus", 'audio/mp4; codecs="opus"'],
162
+ ["FLAC", 'audio/mp4; codecs="flac"']
163
+ ];
164
+ for (var i = 0; i < types.length; i++) {
165
+ var native = v.canPlayType(types[i][1]) || "no";
166
+ var mse = "no MSE";
167
+ if (MS && MS.isTypeSupported) mse = MS.isTypeSupported(types[i][1]) ? "yes" : "no";
168
+ S.support.push({ name: types[i][0], type: types[i][1], canPlayType: native, mse: mse });
169
+ addRow("results", "Support: " + types[i][0], native !== "no" || mse === "yes" ? "pass" : "fail",
170
+ "canPlayType=" + native + ", MSE=" + mse);
171
+ }
172
+ }
173
+
174
+ /* ---------- playback tests ---------- */
175
+
176
+ function freshVideo(muted) {
177
+ var box = $("player");
178
+ var old = box.getElementsByTagName("video");
179
+ while (old.length) {
180
+ stop(old[0]);
181
+ try { old[0].removeAttribute("src"); old[0].load(); } catch (e) { }
182
+ box.removeChild(old[0]);
183
+ }
184
+ var v = document.createElement("video");
185
+ v.muted = !!muted;
186
+ v.setAttribute("playsinline", "");
187
+ v.preload = "auto";
188
+ box.insertBefore(v, $("overlay"));
189
+ return v;
190
+ }
191
+
192
+ function tryPlay(v, onBlocked) {
193
+ var p;
194
+ try { p = v.play(); } catch (e) { if (onBlocked) onBlocked(e); return; }
195
+ if (p && p.then) p.then(null, function (e) { if (onBlocked) onBlocked(e); });
196
+ }
197
+
198
+ function decodedFrames(v) {
199
+ if (typeof v.getVideoPlaybackQuality === "function") {
200
+ return v.getVideoPlaybackQuality().totalVideoFrames;
201
+ }
202
+ if (typeof v.webkitDecodedFrameCount === "number") return v.webkitDecodedFrameCount;
203
+ return -1;
204
+ }
205
+
206
+ /* setup(video, fail) attaches the source and returns a cleanup function. */
207
+ function playTest(name, setup, done) {
208
+ setStage("Playing: " + name);
209
+ var v = freshVideo(true);
210
+ var finished = false, timer = null, poll = null, cleanup = null;
211
+ function finish(result, detail) {
212
+ if (finished) return;
213
+ finished = true;
214
+ clearTimeout(timer); clearInterval(poll);
215
+ try { if (cleanup) cleanup(); } catch (e) { }
216
+ stop(v);
217
+ record(name, result, detail);
218
+ setTimeout(done, 300);
219
+ }
220
+ v.addEventListener("error", function () {
221
+ var code = v.error ? v.error.code : "?";
222
+ finish("fail", "media error " + code + (v.error && v.error.message ? ": " + v.error.message : ""));
223
+ });
224
+ cleanup = setup(v, function (why) { finish("fail", why); });
225
+ tryPlay(v, function (e) { /* muted play may still be refused; the timeout reports it */
226
+ if (!finished) setStage("Playing: " + name + " (play() refused: " + (e && e.name) + ")");
227
+ });
228
+ poll = setInterval(function () {
229
+ var frames = decodedFrames(v);
230
+ if (v.currentTime >= 1.0 && v.videoWidth > 0 && frames !== 0) {
231
+ finish("pass", v.videoWidth + "x" + v.videoHeight + (frames > 0 ? ", " + frames + " frames" : ""));
232
+ }
233
+ }, 250);
234
+ timer = setTimeout(function () {
235
+ finish("fail", "timeout: readyState=" + v.readyState + " networkState=" + v.networkState +
236
+ " time=" + v.currentTime.toFixed(2) + " width=" + v.videoWidth + " frames=" + decodedFrames(v));
237
+ }, PLAY_TIMEOUT);
238
+ }
239
+
240
+ function srcSetup(url) {
241
+ return function (v) { v.src = url; return null; };
242
+ }
243
+
244
+ function hlsSetup(url) {
245
+ return function (v, fail) {
246
+ var hls = new Hls({ enableWorker: true });
247
+ hls.on(Hls.Events.ERROR, function (ev, data) {
248
+ if (data && data.fatal) fail("hls.js " + data.type + ": " + data.details);
249
+ });
250
+ hls.loadSource(url);
251
+ hls.attachMedia(v);
252
+ return function () { hls.destroy(); };
253
+ };
254
+ }
255
+
256
+ function playbackQueue() {
257
+ var q = [];
258
+ var hlsOk = hlsJsOk();
259
+ var native = !!nativeHls();
260
+ var clips = manifest.clips || [];
261
+ for (var i = 0; i < clips.length; i++) {
262
+ var c = clips[i];
263
+ if (!c.mp4) { q.push({ skip: c.label, why: c.error || "not generated" }); continue; }
264
+ q.push({ name: c.label + " MP4", setup: srcSetup(MEDIA + c.mp4) });
265
+ if (c.hls && hlsOk) q.push({ name: c.label + " HLS hls.js", setup: hlsSetup(MEDIA + c.hls) });
266
+ if (c.hls && native) q.push({ name: c.label + " HLS native", setup: srcSetup(MEDIA + c.hls) });
267
+ }
268
+ var ts = manifest.extras && manifest.extras.ts;
269
+ if (ts && hlsOk) q.push({ name: "H.264 HLS (TS) hls.js", setup: hlsSetup(MEDIA + ts) });
270
+ if (ts && native) q.push({ name: "H.264 HLS (TS) native", setup: srcSetup(MEDIA + ts) });
271
+ if (!hlsOk && !native) q.push({ skip: "HLS", why: "no MSE for hls.js and no native HLS" });
272
+ return q;
273
+ }
274
+
275
+ function runQueue(q, done) {
276
+ if (!q.length) { done(); return; }
277
+ var t = q.shift();
278
+ if (t.skip) { record(t.skip, "skip", t.why); runQueue(q, done); return; }
279
+ playTest(t.name, t.setup, function () { runQueue(q, done); });
280
+ }
281
+
282
+ /* ---------- questions answered with the remote ---------- */
283
+
284
+ function ask(question, answers, cb) {
285
+ var box = $("ask");
286
+ box.innerHTML = "";
287
+ var p = document.createElement("p");
288
+ p.textContent = question;
289
+ box.appendChild(p);
290
+ var first = null;
291
+ for (var i = 0; i < answers.length; i++) {
292
+ (function (label) {
293
+ var b = document.createElement("button");
294
+ b.textContent = label;
295
+ b.onclick = function () { box.innerHTML = ""; cb(label); };
296
+ box.appendChild(b);
297
+ if (!first) first = b;
298
+ })(answers[i]);
299
+ }
300
+ if (first) first.focus();
301
+ }
302
+
303
+ /* Buttons that must outlive the questions, which replace everything in #ask. */
304
+ function addControl(label, onclick) {
305
+ var b = document.createElement("button");
306
+ b.textContent = label;
307
+ b.onclick = onclick;
308
+ $("controls").appendChild(b);
309
+ return b;
310
+ }
311
+
312
+ /* ---------- subtitles: native WebVTT track and our own overlay ---------- */
313
+
314
+ function parseTime(t) {
315
+ var p = t.trim().split(":"), s = 0;
316
+ for (var i = 0; i < p.length; i++) s = s * 60 + parseFloat(p[i].replace(",", "."));
317
+ return s;
318
+ }
319
+
320
+ function parseVtt(text) {
321
+ var cues = [], lines = text.replace(/\r/g, "").split("\n");
322
+ for (var i = 0; i < lines.length; i++) {
323
+ if (lines[i].indexOf("-->") > 0) {
324
+ var parts = lines[i].split("-->");
325
+ var start = parseTime(parts[0]), end = parseTime(parts[1].trim().split(" ")[0]);
326
+ var body = [];
327
+ for (i++; i < lines.length && lines[i] !== ""; i++) body.push(lines[i]);
328
+ cues.push({ start: start, end: end, text: body.join("\n") });
329
+ }
330
+ }
331
+ return cues;
332
+ }
333
+
334
+ function subtitleTest(done) {
335
+ var long = manifest.extras && manifest.extras.long;
336
+ if (!long) { S.subtitles = { result: "skip", why: "no 20 s clip" }; done(); return; }
337
+ setStage("Subtitles: a native track at the bottom, our overlay at the top");
338
+ var x = new XMLHttpRequest();
339
+ x.open("GET", MEDIA + manifest.vtt, true);
340
+ x.onload = function () {
341
+ var cues = parseVtt(x.responseText);
342
+ var res = { parsedCues: cues.length, overlaySeen: [], nativeCues: null };
343
+ var v = freshVideo(true);
344
+ var tr = document.createElement("track");
345
+ tr.kind = "subtitles"; tr.srclang = "en"; tr.label = "English"; tr["default"] = true;
346
+ tr.src = MEDIA + manifest.vtt;
347
+ v.appendChild(tr);
348
+ v.src = MEDIA + long;
349
+ if (v.textTracks && v.textTracks[0]) v.textTracks[0].mode = "showing";
350
+ var ov = $("overlay");
351
+ var timer = setInterval(function () {
352
+ var t = v.currentTime, txt = "";
353
+ for (var i = 0; i < cues.length; i++) {
354
+ if (t >= cues[i].start && t < cues[i].end) { txt = cues[i].text; break; }
355
+ }
356
+ clear("overlay");
357
+ if (txt) {
358
+ var s = document.createElement("span"); s.textContent = txt; ov.appendChild(s);
359
+ if (res.overlaySeen.indexOf(txt) < 0) res.overlaySeen.push(txt);
360
+ }
361
+ }, 150);
362
+ tryPlay(v);
363
+ setTimeout(function () {
364
+ if (v.textTracks && v.textTracks[0] && v.textTracks[0].cues) {
365
+ res.nativeCues = v.textTracks[0].cues.length;
366
+ }
367
+ ask("Do you see a yellow 'Subtitle line N' at the top (our overlay)?", ["Yes", "No"], function (a) {
368
+ res.overlayVisible = a;
369
+ ask("Do you see 'Subtitle line N' at the bottom (the browser's own subtitles)?", ["Yes", "No"], function (b) {
370
+ res.nativeVisible = b;
371
+ clearInterval(timer); clear("overlay");
372
+ stop(v);
373
+ res.result = res.overlayVisible === "Yes" ? "pass" : "fail";
374
+ S.subtitles = res;
375
+ record("Subtitles overlay", res.result, "cues seen: " + res.overlaySeen.length);
376
+ record("Subtitles native track", res.nativeVisible === "Yes" ? "pass" : "fail",
377
+ "cues loaded: " + res.nativeCues);
378
+ done();
379
+ });
380
+ });
381
+ }, 4500);
382
+ };
383
+ x.onerror = function () { S.subtitles = { result: "fail", why: "could not load VTT" }; done(); };
384
+ x.send();
385
+ }
386
+
387
+ /* ---------- audio track switching ---------- */
388
+
389
+ function audioTest(done) {
390
+ var url = manifest.extras && manifest.extras.multi_audio;
391
+ if (!url) { S.audio = { result: "skip", why: "no multi-audio clip" }; done(); return; }
392
+ url = MEDIA + url;
393
+ var res = { method: "", tracks: [], switched: false, events: [] };
394
+ var v = freshVideo(false);
395
+ var hls = null, current = 0;
396
+ function names() {
397
+ if (hls) {
398
+ var out = [];
399
+ for (var i = 0; i < hls.audioTracks.length; i++) {
400
+ out.push(hls.audioTracks[i].name + " (" + hls.audioTracks[i].lang + ")");
401
+ }
402
+ return out;
403
+ }
404
+ if (v.audioTracks) {
405
+ var n = [];
406
+ for (var j = 0; j < v.audioTracks.length; j++) n.push(v.audioTracks[j].label || v.audioTracks[j].language || ("track " + j));
407
+ return n;
408
+ }
409
+ return [];
410
+ }
411
+ function select(i) {
412
+ current = i;
413
+ if (hls) { hls.audioTrack = i; return; }
414
+ if (v.audioTracks) {
415
+ for (var j = 0; j < v.audioTracks.length; j++) v.audioTracks[j].enabled = (j === i);
416
+ }
417
+ }
418
+ if (hlsJsOk()) {
419
+ res.method = "hls.js";
420
+ hls = new Hls();
421
+ hls.on(Hls.Events.AUDIO_TRACK_SWITCHED, function (ev, d) {
422
+ res.switched = true; res.events.push("switched to " + d.id);
423
+ setStage("Audio: now playing track " + (d.id + 1) + " of " + hls.audioTracks.length);
424
+ });
425
+ hls.on(Hls.Events.ERROR, function (ev, d) { if (d && d.fatal) res.events.push("fatal " + d.details); });
426
+ hls.loadSource(url);
427
+ hls.attachMedia(v);
428
+ } else if (nativeHls(v)) {
429
+ res.method = "native";
430
+ v.src = url;
431
+ } else {
432
+ S.audio = { result: "skip", why: "no way to play HLS" };
433
+ done(); return;
434
+ }
435
+ function finish(a) {
436
+ clear("controls");
437
+ res.heardSwitch = a;
438
+ if (hls) hls.destroy();
439
+ stop(v);
440
+ res.result = a === "Yes" ? "pass" : "fail";
441
+ S.audio = res;
442
+ record("Audio track switch (" + res.method + ")", res.result,
443
+ "tracks: " + res.tracks.length + ", event: " + res.switched + ", heard: " + a);
444
+ done();
445
+ }
446
+ // The switch is timed from when sound starts, so a refused play() can still pass.
447
+ var started = false;
448
+ function begin() {
449
+ if (started) return;
450
+ started = true;
451
+ clear("controls");
452
+ setStage("Audio: you should hear a low tone (440 Hz, English)");
453
+ setTimeout(function () {
454
+ res.tracks = names();
455
+ setStage("Audio: switching to track 2 (higher tone, Japanese). Tracks: " + res.tracks.join(", "));
456
+ select(1);
457
+ setTimeout(function () {
458
+ ask("Did the tone get higher? (Switch audio flips between the tracks.)",
459
+ ["Yes", "No", "No sound at all"], finish);
460
+ addControl("Switch audio", function () { select(current === 0 ? 1 : 0); });
461
+ }, 3000);
462
+ }, 4000);
463
+ }
464
+ v.addEventListener("playing", begin);
465
+ setTimeout(begin, AUDIO_START_LIMIT); // never started: ask anyway
466
+ tryPlay(v, function (e) {
467
+ res.events.push("play() refused: " + (e && e.name));
468
+ setStage("Audio: press Play to start the sound");
469
+ addControl("Play", function () { tryPlay(v); }).focus();
470
+ });
471
+ }
472
+
473
+ /* ---------- download speed ---------- */
474
+
475
+ function speedTest(done) {
476
+ var sizes = [2, 8, 32, 64, 128], runs = [];
477
+ setStage("Speed: downloading from the server…");
478
+ function next() {
479
+ if (!sizes.length) return finish();
480
+ var mb = sizes.shift();
481
+ var x = new XMLHttpRequest();
482
+ var t0 = Date.now();
483
+ x.open("GET", "/caps/speed?mb=" + mb + "&r=" + Math.random(), true);
484
+ x.responseType = "arraybuffer";
485
+ x.timeout = 120000;
486
+ x.onload = function () {
487
+ var secs = (Date.now() - t0) / 1000;
488
+ var bytes = x.response ? x.response.byteLength : mb * 1048576;
489
+ var mbps = bytes * 8 / secs / 1e6;
490
+ runs.push({ mb: mb, seconds: secs, mbps: Math.round(mbps * 10) / 10 });
491
+ setStage("Speed: " + mb + " MB in " + secs.toFixed(1) + " s = " + mbps.toFixed(1) + " Mbit/s");
492
+ if (secs > 8) return finish();
493
+ next();
494
+ };
495
+ x.onerror = x.ontimeout = function () { runs.push({ mb: mb, error: true }); finish(); };
496
+ x.send();
497
+ }
498
+ function finish() {
499
+ var best = 0;
500
+ for (var i = 0; i < runs.length; i++) if (runs[i].mbps > best && runs[i].mb >= 8) best = runs[i].mbps;
501
+ if (!best) for (var j = 0; j < runs.length; j++) if (runs[j].mbps > best) best = runs[j].mbps;
502
+ S.speed = { best_mbps: best, runs: runs };
503
+ record("Download speed", best ? "pass" : "fail", best + " Mbit/s (best run of 8 MB or more)");
504
+ done();
505
+ }
506
+ next();
507
+ }
508
+
509
+ /* ---------- wiring ---------- */
510
+
511
+ function lock(on) {
512
+ busy = on;
513
+ $("start").disabled = on;
514
+ $("speedonly").disabled = on;
515
+ }
516
+
517
+ function runAll() {
518
+ if (busy || !manifest) return;
519
+ lock(true);
520
+ S.playback = []; S.support = []; S.subtitles = null; S.audio = null; S.speed = null;
521
+ clearTable("results");
522
+ supportInfo();
523
+ send("support");
524
+ runQueue(playbackQueue(), function () {
525
+ send("playback");
526
+ subtitleTest(function () {
527
+ send("subtitles");
528
+ audioTest(function () {
529
+ send("audio");
530
+ speedTest(function () {
531
+ freshVideo(true);
532
+ send("done");
533
+ setStage("Done. Results were sent to the server. You can keep pressing remote keys.");
534
+ lock(false);
535
+ $("start").focus();
536
+ });
537
+ });
538
+ });
539
+ });
540
+ }
541
+
542
+ $("start").onclick = runAll;
543
+ $("speedonly").onclick = function () {
544
+ if (busy) return;
545
+ lock(true);
546
+ speedTest(function () { send("speed"); setStage("Speed test done."); lock(false); });
547
+ };
548
+
549
+ envInfo();
550
+ send("env");
551
+ getJSON("/caps/clips.json", function (err, data) {
552
+ if (err) { setStage("Could not load the clip list: " + err.message); return; }
553
+ manifest = data;
554
+ var missing = [];
555
+ for (var i = 0; i < data.clips.length; i++) if (!data.clips[i].mp4) missing.push(data.clips[i].label);
556
+ setStage("Ready. Press Start all tests." + (missing.length ? " Not generated: " + missing.join(", ") : ""));
557
+ $("start").focus();
558
+ });
559
+ })();
@@ -0,0 +1,28 @@
1
+ Copyright (c) 2017 Dailymotion (http://www.dailymotion.com)
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
14
+
15
+ src/remux/mp4-generator.js and src/demux/exp-golomb.ts implementation in this project
16
+ are derived from the HLS library for video.js (https://github.com/videojs/videojs-contrib-hls)
17
+
18
+ That work is also covered by the Apache 2 License, following copyright:
19
+ Copyright (c) 2013-2015 Brightcove
20
+
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28
+ THE SOFTWARE.
@@ -0,0 +1 @@
1
+ 1.7.4