diff --git a/public/src/js/abstractfile.js b/public/src/js/abstractfile.js index 62694f5..ef44771 100644 --- a/public/src/js/abstractfile.js +++ b/public/src/js/abstractfile.js @@ -47,21 +47,33 @@ class RemoteFile{ } } arrayBuffer(){ - return loader.ajax(this.url, request => { - request.responseType = "arraybuffer" - }) + var name = this.name.toLowerCase() + if(name.endsWith(".ogg") || name.endsWith(".png") || name.endsWith(".gif") || name.endsWith(".tja") || name.endsWith(".osu")){ + return RangeFetcher.fetchArrayBuffer(this.url, {concurrency: 4, chunkSize: 1048576}).catch(() => loader.ajax(this.url, request => { request.responseType = "arraybuffer" })) + } + return loader.ajax(this.url, request => { request.responseType = "arraybuffer" }) } read(encoding){ if(encoding){ + var name = this.name.toLowerCase() + if(name.endsWith(".tja") || name.endsWith(".osu")){ + return RangeFetcher.fetchText(this.url, {concurrency: 4, chunkSize: 262144}).catch(() => this.blob().then(blob => readFile(blob, false, encoding))) + } return this.blob().then(blob => readFile(blob, false, encoding)) }else{ + var name = this.name.toLowerCase() + if(name.endsWith(".tja") || name.endsWith(".osu")){ + return RangeFetcher.fetchText(this.url, {concurrency: 4, chunkSize: 262144}).catch(() => loader.ajax(this.url)) + } return loader.ajax(this.url) } } blob(){ - return loader.ajax(this.url, request => { - request.responseType = "blob" - }) + var name = this.name.toLowerCase() + if(name.endsWith(".ogg") || name.endsWith(".png") || name.endsWith(".gif")){ + return RangeFetcher.fetchBlob(this.url, {concurrency: 4, chunkSize: 1048576}).catch(() => loader.ajax(this.url, request => { request.responseType = "blob" })) + } + return loader.ajax(this.url, request => { request.responseType = "blob" }) } } class LocalFile{ diff --git a/public/src/js/assets.js b/public/src/js/assets.js index 24d4232..5fef59b 100644 --- a/public/src/js/assets.js +++ b/public/src/js/assets.js @@ -39,6 +39,7 @@ var assets = { "abstractfile.js", "idb.js", "plugins.js", + "rangefetcher.js", "search.js" ], "css": [ diff --git a/public/src/js/loader.js b/public/src/js/loader.js index f12b33d..7f128fd 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -23,8 +23,8 @@ class Loader{ Promise.all(promises).then(this.run.bind(this)) } - run(){ - this.promises = [] + run(){ + this.promises = [] this.loaderDiv = document.querySelector("#loader") this.loaderPercentage = document.querySelector("#loader .percentage") this.loaderProgress = document.querySelector("#loader .progress") @@ -35,12 +35,10 @@ class Loader{ this.addPromise(this.loadScript(gameConfig.custom_js), gameConfig.custom_js) } var oggSupport = new Audio().canPlayType("audio/ogg;codecs=vorbis") - if(!oggSupport){ - assets.js.push("lib/oggmented-wasm.js") - } - assets.js.forEach(name => { - this.addPromise(this.loadScript("src/js/" + name), "src/js/" + name) - }) + if(!oggSupport){ + assets.js.push("lib/oggmented-wasm.js") + } + this.addPromise(this.prefetchAndLoadScripts(assets.js), "src/js/*") var pageVersion = versionLink.href var index = pageVersion.lastIndexOf("/") @@ -399,6 +397,26 @@ class Loader{ }, e => this.errorMsg(e)) }) } + prefetchAndLoadScripts(names){ + var urls = names.map(name => "src/js/" + name + this.queryString) + var fetches = urls.map(url => this.ajax(url, r => { r.responseType = "arraybuffer" })) + return Promise.all(fetches).then(buffers => { + var chain = Promise.resolve() + for(let i=0;i { + var blob = new Blob([buffers[i]], {type: "application/javascript"}) + var u = URL.createObjectURL(blob) + return this.loadScript(u).then(() => { URL.revokeObjectURL(u) }) + }) + } + return chain + }).catch(() => { + names.forEach(name => { + this.addPromise(this.loadScript("src/js/" + name), "src/js/" + name) + }) + return Promise.resolve() + }) + } addPromise(promise, url){ this.promises.push(promise) promise.then(this.assetLoaded.bind(this), response => { diff --git a/public/src/js/rangefetcher.js b/public/src/js/rangefetcher.js new file mode 100644 index 0000000..12c06f4 --- /dev/null +++ b/public/src/js/rangefetcher.js @@ -0,0 +1,106 @@ +var RangeFetcher = { + _head: function(url){ + return new Promise(function(resolve){ + var r = new XMLHttpRequest() + r.open("HEAD", url) + r.onreadystatechange = function(){ + if(r.readyState === 4){ + var len = parseInt(r.getResponseHeader("Content-Length")) || null + var ranges = r.getResponseHeader("Accept-Ranges") || "" + resolve({length: len, acceptRanges: /bytes/i.test(ranges)}) + } + } + r.onerror = function(){ resolve({length: null, acceptRanges: false}) } + r.send() + }) + }, + _getRange: function(url, start, end, responseType){ + return new Promise(function(resolve, reject){ + var r = new XMLHttpRequest() + r.open("GET", url) + r.responseType = responseType || "arraybuffer" + r.setRequestHeader("Range", "bytes=" + start + "-" + end) + r.onload = function(){ + if(r.status === 206 || r.status === 200){ + resolve(r.response) + }else{ + reject(r.status) + } + } + r.onerror = function(){ reject("network") } + r.send() + }) + }, + _getAll: function(url, responseType){ + return new Promise(function(resolve, reject){ + var r = new XMLHttpRequest() + r.open("GET", url) + r.responseType = responseType || "arraybuffer" + r.onload = function(){ + if(r.status === 200){ + resolve(r.response) + }else{ + reject(r.status) + } + } + r.onerror = function(){ reject("network") } + r.send() + }) + }, + _runConcurrent: function(tasks, limit){ + return new Promise(function(resolve, reject){ + var i = 0 + var running = 0 + var results = [] + function next(){ + while(running < limit && i < tasks.length){ + var idx = i++ + running++ + tasks[idx]().then(function(res){ + results[idx] = res + running-- + if(results.length === tasks.length && results.every(function(v){return v !== undefined})){ resolve(results) } + else{ next() } + }, function(e){ reject(e) }) + } + } + next() + }) + }, + fetchArrayBuffer: function(url, opts){ + opts = opts || {} + var concurrency = opts.concurrency || 4 + var minChunk = opts.chunkSize || 1048576 + var self = this + return this._head(url).then(function(info){ + if(!info.length || !info.acceptRanges || info.length < minChunk){ + return self._getAll(url, "arraybuffer") + } + var total = info.length + var parts = Math.ceil(total / minChunk) + parts = Math.max(1, Math.min(parts, 16)) + var tasks = [] + for(var p=0; p