Remove various fallbacks and workarounds from the Ugoira Javascript player for legacy browser issues. * Remove fallbacks for URL, Blob, and ArrayBuffer (these have been supported in most browsers for years). * Remove workaround for iOS <6 not supporting Blob URLs. * Remove workaround for a bug in Safari's handling of the Range HTTP header. This was fixed in 2015. * Remove support for playing a list of image URLs instead of playing images from the .zip file (this was never used). * Remove support for limiting loadahead of images (it was hardcoded to always use unlimited loadahead anyways).
378 lines
12 KiB
JavaScript
378 lines
12 KiB
JavaScript
// Source: https://github.com/pixiv/zip_player
|
|
|
|
function ZipImagePlayer(options) {
|
|
this.op = options;
|
|
if (!URL) {
|
|
this._error("No URL support");
|
|
}
|
|
if (!Blob) {
|
|
this._error("No Blob support");
|
|
}
|
|
if (!Uint8Array) {
|
|
this._error("No Uint8Array support");
|
|
}
|
|
if (!DataView) {
|
|
this._error("No DataView support");
|
|
}
|
|
if (!ArrayBuffer) {
|
|
this._error("No ArrayBuffer support");
|
|
}
|
|
if (!this.op.source) {
|
|
this._error("No source URL provided");
|
|
}
|
|
this._loadingState = 0;
|
|
this._dead = false;
|
|
this._context = options.canvas.getContext("2d");
|
|
this._files = [];
|
|
this._frameCount = this.op.metadata.frames.length;
|
|
this._debugLog("Frame count: " + this._frameCount);
|
|
this._frame = 0;
|
|
this._loadFrame = 0;
|
|
this._frameImages = [];
|
|
this._paused = false;
|
|
this._startLoad();
|
|
if (this.op.autoStart) {
|
|
this.play();
|
|
} else {
|
|
this._paused = true;
|
|
}
|
|
}
|
|
|
|
ZipImagePlayer.prototype = {
|
|
_trailerBytes: 30000,
|
|
_failed: false,
|
|
_mkerr: function(msg) {
|
|
var _this = this;
|
|
return function() {
|
|
_this._error(msg);
|
|
}
|
|
},
|
|
_error: function(msg) {
|
|
this._failed = true;
|
|
throw Error("ZipImagePlayer error: " + msg);
|
|
},
|
|
_debugLog: function(msg) {
|
|
if (this.op.debug) {
|
|
console.log(msg);
|
|
}
|
|
},
|
|
_load: function(offset, length, callback) {
|
|
var _this = this;
|
|
// Unfortunately JQuery doesn't support ArrayBuffer XHR
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.addEventListener("load", function(ev) {
|
|
if (_this._dead) {
|
|
return;
|
|
}
|
|
_this._debugLog("Load: " + offset + " " + length + " status=" +
|
|
xhr.status);
|
|
if (xhr.status == 200) {
|
|
_this._debugLog("Range disabled or unsupported, complete load");
|
|
offset = 0;
|
|
length = xhr.response.byteLength;
|
|
_this._len = length;
|
|
_this._buf = xhr.response;
|
|
_this._bytes = new Uint8Array(_this._buf);
|
|
} else {
|
|
if (xhr.status != 206) {
|
|
_this._error("Unexpected HTTP status " + xhr.status);
|
|
}
|
|
if (xhr.response.byteLength != length) {
|
|
_this._error("Unexpected length " +
|
|
xhr.response.byteLength +
|
|
" (expected " + length + ")");
|
|
}
|
|
_this._bytes.set(new Uint8Array(xhr.response), offset);
|
|
}
|
|
if (callback) {
|
|
callback.apply(_this, [offset, length]);
|
|
}
|
|
}, false);
|
|
xhr.addEventListener("error", this._mkerr("Fetch failed"), false);
|
|
xhr.open("GET", this.op.source);
|
|
xhr.responseType = "arraybuffer";
|
|
if (offset != null && length != null) {
|
|
var end = offset + length;
|
|
xhr.setRequestHeader("Range", "bytes=" + offset + "-" + (end - 1));
|
|
}
|
|
/*this._debugLog("Load: " + offset + " " + length);*/
|
|
xhr.send();
|
|
},
|
|
_startLoad: function() {
|
|
var _this = this;
|
|
$.ajax({
|
|
url: this.op.source,
|
|
type: "HEAD"
|
|
}).done(function(data, status, xhr) {
|
|
if (_this._dead) {
|
|
return;
|
|
}
|
|
_this._pHead = 0;
|
|
_this._pNextHead = 0;
|
|
_this._pFetch = 0;
|
|
var len = parseInt(xhr.getResponseHeader("Content-Length"));
|
|
if (!len) {
|
|
_this._debugLog("HEAD request failed: invalid file length.");
|
|
_this._debugLog("Falling back to full file mode.");
|
|
_this._load(null, null, function(off, len) {
|
|
_this._pTail = 0;
|
|
_this._pHead = len;
|
|
_this._findCentralDirectory();
|
|
});
|
|
return;
|
|
}
|
|
_this._debugLog("Len: " + len);
|
|
_this._len = len;
|
|
_this._buf = new ArrayBuffer(len);
|
|
_this._bytes = new Uint8Array(_this._buf);
|
|
var off = len - _this._trailerBytes;
|
|
if (off < 0) {
|
|
off = 0;
|
|
}
|
|
_this._pTail = len;
|
|
_this._load(off, len - off, function(off, len) {
|
|
_this._pTail = off;
|
|
_this._findCentralDirectory();
|
|
});
|
|
}).fail(this._mkerr("Length fetch failed"));
|
|
},
|
|
_findCentralDirectory: function() {
|
|
// No support for ZIP file comment
|
|
var dv = new DataView(this._buf, this._len - 22, 22);
|
|
if (dv.getUint32(0, true) != 0x06054b50) {
|
|
this._error("End of Central Directory signature not found");
|
|
}
|
|
var cd_count = dv.getUint16(10, true);
|
|
var cd_size = dv.getUint32(12, true);
|
|
var cd_off = dv.getUint32(16, true);
|
|
if (cd_off < this._pTail) {
|
|
this._load(cd_off, this._pTail - cd_off, function() {
|
|
this._pTail = cd_off;
|
|
this._readCentralDirectory(cd_off, cd_size, cd_count);
|
|
});
|
|
} else {
|
|
this._readCentralDirectory(cd_off, cd_size, cd_count);
|
|
}
|
|
},
|
|
_readCentralDirectory: function(offset, size, count) {
|
|
var dv = new DataView(this._buf, offset, size);
|
|
var p = 0;
|
|
for (var i = 0; i < count; i++ ) {
|
|
if (dv.getUint32(p, true) != 0x02014b50) {
|
|
this._error("Invalid Central Directory signature");
|
|
}
|
|
var compMethod = dv.getUint16(p + 10, true);
|
|
var uncompSize = dv.getUint32(p + 24, true);
|
|
var nameLen = dv.getUint16(p + 28, true);
|
|
var extraLen = dv.getUint16(p + 30, true);
|
|
var cmtLen = dv.getUint16(p + 32, true);
|
|
var off = dv.getUint32(p + 42, true);
|
|
if (compMethod != 0) {
|
|
this._error("Unsupported compression method");
|
|
}
|
|
p += 46 + nameLen + extraLen + cmtLen;
|
|
this._files[i] = {off: off, len: uncompSize};
|
|
}
|
|
// Two outstanding fetches at any given time.
|
|
// Note: the implementation does not support more than two.
|
|
if (this._pHead >= this._pTail) {
|
|
this._pHead = this._len;
|
|
$(this).triggerHandler("loadProgress", [this._pHead / this._len]);
|
|
this._loadNextFrame();
|
|
} else {
|
|
this._loadNextChunk();
|
|
this._loadNextChunk();
|
|
}
|
|
},
|
|
_loadNextChunk: function() {
|
|
if (this._pFetch >= this._pTail) {
|
|
return;
|
|
}
|
|
var off = this._pFetch;
|
|
var len = this.op.chunkSize;
|
|
if (this._pFetch + len > this._pTail) {
|
|
len = this._pTail - this._pFetch;
|
|
}
|
|
this._pFetch += len;
|
|
this._load(off, len, function() {
|
|
if (off == this._pHead) {
|
|
if (this._pNextHead) {
|
|
this._pHead = this._pNextHead;
|
|
this._pNextHead = 0;
|
|
} else {
|
|
this._pHead = off + len;
|
|
}
|
|
if (this._pHead >= this._pTail) {
|
|
this._pHead = this._len;
|
|
}
|
|
/*this._debugLog("New pHead: " + this._pHead);*/
|
|
$(this).triggerHandler("loadProgress",
|
|
[this._pHead / this._len]);
|
|
this._loadNextFrame();
|
|
} else {
|
|
this._pNextHead = off + len;
|
|
}
|
|
this._loadNextChunk();
|
|
});
|
|
},
|
|
_fileDataStart: function(offset) {
|
|
var dv = new DataView(this._buf, offset, 30);
|
|
var nameLen = dv.getUint16(26, true);
|
|
var extraLen = dv.getUint16(28, true);
|
|
return offset + 30 + nameLen + extraLen;
|
|
},
|
|
_isFileAvailable: function(index) {
|
|
var info = this._files[index];
|
|
if (!info) {
|
|
this._error("File " + index + " not found in ZIP");
|
|
}
|
|
if (this._pHead < (info.off + 30)) {
|
|
return false;
|
|
}
|
|
return this._pHead >= (this._fileDataStart(info.off) + info.len);
|
|
},
|
|
_loadNextFrame: function() {
|
|
if (this._dead) {
|
|
return;
|
|
}
|
|
var frame = this._loadFrame;
|
|
if (frame >= this._frameCount) {
|
|
return;
|
|
}
|
|
if (!this._isFileAvailable(frame)) {
|
|
return;
|
|
}
|
|
this._loadFrame += 1;
|
|
var off = this._fileDataStart(this._files[frame].off);
|
|
var end = off + this._files[frame].len;
|
|
var slice = this._buf.slice(off, end);
|
|
var blob = new Blob([slice]);
|
|
/*_this._debugLog("Loading " + meta.file + " to frame " + frame);*/
|
|
var url = URL.createObjectURL(blob);
|
|
this._loadImage(frame, url);
|
|
},
|
|
_loadImage: function(frame, url) {
|
|
var _this = this;
|
|
var image = new Image();
|
|
image.addEventListener('load', function() {
|
|
_this._debugLog(`Loaded frame ${frame}`);
|
|
URL.revokeObjectURL(url);
|
|
if (_this._dead) {
|
|
return;
|
|
}
|
|
_this._frameImages[frame] = image;
|
|
$(_this).triggerHandler("frameLoaded", frame);
|
|
if (_this._loadingState == 0) {
|
|
_this._displayFrame.apply(_this);
|
|
}
|
|
if (frame >= (_this._frameCount - 1)) {
|
|
_this._setLoadingState(2);
|
|
_this._buf = null;
|
|
_this._bytes = null;
|
|
} else {
|
|
_this._loadNextFrame();
|
|
}
|
|
});
|
|
image.src = url;
|
|
},
|
|
_setLoadingState: function(state) {
|
|
if (this._loadingState != state) {
|
|
this._loadingState = state;
|
|
$(this).triggerHandler("loadingStateChanged", [state]);
|
|
}
|
|
},
|
|
_displayFrame: function() {
|
|
if (this._dead) {
|
|
return;
|
|
}
|
|
var _this = this;
|
|
var delay = this.op.metadata.frames[this._frame];
|
|
this._debugLog("Displaying frame: " + this._frame);
|
|
var image = this._frameImages[this._frame];
|
|
if (!image) {
|
|
this._debugLog("Image not available!");
|
|
this._setLoadingState(0);
|
|
return;
|
|
}
|
|
if (this._loadingState != 2) {
|
|
this._setLoadingState(1);
|
|
}
|
|
if (this.op.autosize) {
|
|
if (this._context.canvas.width != image.width || this._context.canvas.height != image.height) {
|
|
// make the canvas autosize itself according to the images drawn on it
|
|
// should set it once, since we don't have variable sized frames
|
|
this._context.canvas.width = image.width;
|
|
this._context.canvas.height = image.height;
|
|
}
|
|
};
|
|
this._context.clearRect(0, 0, this.op.canvas.width,
|
|
this.op.canvas.height);
|
|
this._context.drawImage(image, 0, 0);
|
|
$(this).triggerHandler("frame", this._frame);
|
|
if (!this._paused) {
|
|
this._timer = setTimeout(function() {
|
|
_this._timer = null;
|
|
_this._nextFrame.apply(_this);
|
|
}, delay);
|
|
}
|
|
},
|
|
_nextFrame: function(frame) {
|
|
if (this._frame >= (this._frameCount - 1)) {
|
|
if (this.op.loop) {
|
|
this._frame = 0;
|
|
} else {
|
|
this.pause();
|
|
return;
|
|
}
|
|
} else {
|
|
this._frame += 1;
|
|
}
|
|
this._displayFrame();
|
|
},
|
|
play: function() {
|
|
if (this._dead) {
|
|
return;
|
|
}
|
|
if (this._paused) {
|
|
$(this).triggerHandler("play", [this._frame]);
|
|
this._paused = false;
|
|
this._displayFrame();
|
|
}
|
|
},
|
|
pause: function() {
|
|
if (this._dead) {
|
|
return;
|
|
}
|
|
if (!this._paused) {
|
|
if (this._timer) {
|
|
clearTimeout(this._timer);
|
|
}
|
|
this._paused = true;
|
|
$(this).triggerHandler("pause", [this._frame]);
|
|
}
|
|
},
|
|
rewind: function() {
|
|
if (this._dead) {
|
|
return;
|
|
}
|
|
this._frame = 0;
|
|
if (this._timer) {
|
|
clearTimeout(this._timer);
|
|
}
|
|
this._displayFrame();
|
|
},
|
|
stop: function() {
|
|
this._debugLog("Stopped!");
|
|
this._dead = true;
|
|
if (this._timer) {
|
|
clearTimeout(this._timer);
|
|
}
|
|
this._frameImages = null;
|
|
this._buf = null;
|
|
this._bytes = null;
|
|
$(this).triggerHandler("stop");
|
|
},
|
|
}
|
|
|
|
export { ZipImagePlayer };
|