You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
315 lines
12 KiB
315 lines
12 KiB
<!DOCTYPE html>
|
|
<html>
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
<title>Page Title</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
</head>
|
|
|
|
<body>
|
|
|
|
|
|
<script>
|
|
var Groover = (function() {
|
|
// ensure webp is supported
|
|
function canEncode() {
|
|
var canvas = document.createElement("canvas");
|
|
canvas.width = 8;
|
|
canvas.height = 8;
|
|
return canvas.toDataURL("image/webp", 0.1).indexOf("image/webp") > -1;
|
|
}
|
|
if (!canEncode()) {
|
|
return undefined;
|
|
}
|
|
var webmData = null;
|
|
var clusterTimecode = 0;
|
|
var clusterCounter = 0;
|
|
var CLUSTER_MAX_DURATION = 30000;
|
|
var frameNumber = 0;
|
|
var width;
|
|
var height;
|
|
var frameDelay;
|
|
var quality;
|
|
var name;
|
|
const videoMimeType = "video/webm"; // the only one.
|
|
const frameMimeType = 'image/webp'; // can be no other
|
|
const S = String.fromCharCode;
|
|
const dataTypes = {
|
|
object: function(data) {
|
|
return toBlob(data);
|
|
},
|
|
number: function(data) {
|
|
return stream.num(data);
|
|
},
|
|
string: function(data) {
|
|
return stream.str(data);
|
|
},
|
|
array: function(data) {
|
|
return data;
|
|
},
|
|
double2Str: function(num) {
|
|
var c = new Uint8Array((new Float64Array([num])).buffer);
|
|
return S(c[7]) + S(c[6]) + S(c[5]) + S(c[4]) + S(c[3]) + S(c[2]) + S(c[1]) + S(c[0]);
|
|
}
|
|
};
|
|
|
|
const stream = {
|
|
num: function(num) { // writes int
|
|
var parts = [];
|
|
while (num > 0) {
|
|
parts.push(num & 0xff);
|
|
num = num >> 8;
|
|
}
|
|
return new Uint8Array(parts.reverse());
|
|
},
|
|
str: function(str) { // writes string
|
|
var i, len, arr;
|
|
len = str.length;
|
|
arr = new Uint8Array(len);
|
|
for (i = 0; i < len; i++) {
|
|
arr[i] = str.charCodeAt(i);
|
|
}
|
|
return arr;
|
|
},
|
|
compInt: function(num) { // could not find full details so bit of a guess
|
|
if (num < 128) { // number is prefixed with a bit (1000 is on byte 0100 two, 0010 three and so on)
|
|
num += 0x80;
|
|
return new Uint8Array([num]);
|
|
} else
|
|
if (num < 0x4000) {
|
|
num += 0x4000;
|
|
return new Uint8Array([num >> 8, num])
|
|
} else
|
|
if (num < 0x200000) {
|
|
num += 0x200000;
|
|
return new Uint8Array([num >> 16, num >> 8, num])
|
|
} else
|
|
if (num < 0x10000000) {
|
|
num += 0x10000000;
|
|
return new Uint8Array([num >> 24, num >> 16, num >> 8, num])
|
|
}
|
|
}
|
|
}
|
|
const ids = { // header names and values
|
|
videoData: 0x1a45dfa3,
|
|
Version: 0x4286,
|
|
ReadVersion: 0x42f7,
|
|
MaxIDLength: 0x42f2,
|
|
MaxSizeLength: 0x42f3,
|
|
DocType: 0x4282,
|
|
DocTypeVersion: 0x4287,
|
|
DocTypeReadVersion: 0x4285,
|
|
Segment: 0x18538067,
|
|
Info: 0x1549a966,
|
|
TimecodeScale: 0x2ad7b1,
|
|
MuxingApp: 0x4d80,
|
|
WritingApp: 0x5741,
|
|
Duration: 0x4489,
|
|
Tracks: 0x1654ae6b,
|
|
TrackEntry: 0xae,
|
|
TrackNumber: 0xd7,
|
|
TrackUID: 0x63c5,
|
|
FlagLacing: 0x9c,
|
|
Language: 0x22b59c,
|
|
CodecID: 0x86,
|
|
CodecName: 0x258688,
|
|
TrackType: 0x83,
|
|
Video: 0xe0,
|
|
PixelWidth: 0xb0,
|
|
PixelHeight: 0xba,
|
|
Cluster: 0x1f43b675,
|
|
Timecode: 0xe7,
|
|
Frame: 0xa3,
|
|
Keyframe: 0x9d012a,
|
|
FrameBlock: 0x81,
|
|
};
|
|
const keyframeD64Header = '\x9d\x01\x2a'; //VP8 keyframe header 0x9d012a
|
|
const videoDataPos = 1; // data pos of frame data header
|
|
const defaultDelay = dataTypes.double2Str(1000 / 25);
|
|
const header = [ // structure of webM header/chunks what ever they are called.
|
|
ids.videoData, [
|
|
ids.Version, 1,
|
|
ids.ReadVersion, 1,
|
|
ids.MaxIDLength, 4,
|
|
ids.MaxSizeLength, 8,
|
|
ids.DocType, 'webm',
|
|
ids.DocTypeVersion, 2,
|
|
ids.DocTypeReadVersion, 2
|
|
],
|
|
ids.Segment, [
|
|
ids.Info, [
|
|
ids.TimecodeScale, 1000000,
|
|
ids.MuxingApp, 'Groover',
|
|
ids.WritingApp, 'Groover',
|
|
ids.Duration, 0
|
|
],
|
|
ids.Tracks, [
|
|
ids.TrackEntry, [
|
|
ids.TrackNumber, 1,
|
|
ids.TrackUID, 1,
|
|
ids.FlagLacing, 0, // always o
|
|
ids.Language, 'und', // undefined I think this means
|
|
ids.CodecID, 'V_VP8', // These I think must not change
|
|
ids.CodecName, 'VP8', // These I think must not change
|
|
ids.TrackType, 1,
|
|
ids.Video, [
|
|
ids.PixelWidth, 0,
|
|
ids.PixelHeight, 0
|
|
]
|
|
]
|
|
]
|
|
]
|
|
];
|
|
|
|
function getHeader() {
|
|
header[3][2][3] = name;
|
|
header[3][2][5] = name;
|
|
header[3][2][7] = dataTypes.double2Str(frameDelay);
|
|
header[3][3][1][15][1] = width;
|
|
header[3][3][1][15][3] = height;
|
|
|
|
function create(dat) {
|
|
var i, kv, data;
|
|
data = [];
|
|
for (i = 0; i < dat.length; i += 2) {
|
|
kv = {
|
|
i: dat[i]
|
|
};
|
|
if (Array.isArray(dat[i + 1])) {
|
|
kv.d = create(dat[i + 1]);
|
|
} else {
|
|
kv.d = dat[i + 1];
|
|
}
|
|
data.push(kv);
|
|
}
|
|
return data;
|
|
}
|
|
return create(header);
|
|
}
|
|
|
|
function addCluster() {
|
|
webmData[videoDataPos].d.push({
|
|
i: ids.Cluster,
|
|
d: [{
|
|
i: ids.Timecode,
|
|
d: Math.round(clusterTimecode)
|
|
}]
|
|
}); // Fixed bug with Round
|
|
clusterCounter = 0;
|
|
}
|
|
|
|
function addFrame(frame) {
|
|
var VP8, kfS, riff;
|
|
riff = getWebPChunks(atob(frame.toDataURL(frameMimeType, quality).slice(23)));
|
|
VP8 = riff.RIFF[0].WEBP[0];
|
|
kfS = VP8.indexOf(keyframeD64Header) + 3;
|
|
frame = {
|
|
width: ((VP8.charCodeAt(kfS + 1) << 8) | VP8.charCodeAt(kfS)) & 0x3FFF,
|
|
height: ((VP8.charCodeAt(kfS + 3) << 8) | VP8.charCodeAt(kfS + 2)) & 0x3FFF,
|
|
data: VP8,
|
|
riff: riff
|
|
};
|
|
if (clusterCounter > CLUSTER_MAX_DURATION) {
|
|
addCluster();
|
|
}
|
|
webmData[videoDataPos].d[webmData[videoDataPos].d.length - 1].d.push({
|
|
i: ids.Frame,
|
|
d: S(ids.FrameBlock) + S(Math.round(clusterCounter) >> 8) + S(Math.round(clusterCounter) & 0xff) + S(128) + frame.data.slice(4),
|
|
});
|
|
clusterCounter += frameDelay;
|
|
clusterTimecode += frameDelay;
|
|
webmData[videoDataPos].d[0].d[3].d = dataTypes.double2Str(clusterTimecode);
|
|
}
|
|
|
|
function startEncoding() {
|
|
frameNumber = clusterCounter = clusterTimecode = 0;
|
|
webmData = getHeader();
|
|
addCluster();
|
|
}
|
|
|
|
function toBlob(vidData) {
|
|
var data, i, vData, len;
|
|
vData = [];
|
|
for (i = 0; i < vidData.length; i++) {
|
|
data = dataTypes[typeof vidData[i].d](vidData[i].d);
|
|
len = data.size || data.byteLength || data.length;
|
|
vData.push(stream.num(vidData[i].i));
|
|
vData.push(stream.compInt(len));
|
|
vData.push(data)
|
|
}
|
|
return new Blob(vData, {
|
|
type: videoMimeType
|
|
});
|
|
}
|
|
|
|
function getWebPChunks(str) {
|
|
var offset, chunks, id, len, data;
|
|
offset = 0;
|
|
chunks = {};
|
|
while (offset < str.length) {
|
|
id = str.substr(offset, 4);
|
|
// value will have top bit on (bit 32) so not simply a bitwise operation
|
|
// Warning little endian (Will not work on big endian systems)
|
|
len = new Uint32Array(
|
|
new Uint8Array([
|
|
str.charCodeAt(offset + 7),
|
|
str.charCodeAt(offset + 6),
|
|
str.charCodeAt(offset + 5),
|
|
str.charCodeAt(offset + 4)
|
|
]).buffer)[0];
|
|
id = str.substr(offset, 4);
|
|
chunks[id] = chunks[id] === undefined ? [] : chunks[id];
|
|
if (id === 'RIFF' || id === 'LIST') {
|
|
chunks[id].push(getWebPChunks(str.substr(offset + 8, len)));
|
|
offset += 8 + len;
|
|
} else if (id === 'WEBP') {
|
|
chunks[id].push(str.substr(offset + 8));
|
|
break;
|
|
} else {
|
|
chunks[id].push(str.substr(offset + 4));
|
|
break;
|
|
}
|
|
}
|
|
return chunks;
|
|
}
|
|
|
|
function Encoder(fps, _quality = 0.8, _name = "Groover") {
|
|
this.fps = fps;
|
|
this.quality = quality = _quality;
|
|
this.frameDelay = frameDelay = 1000 / fps;
|
|
this.frame = 0;
|
|
this.width = width = null;
|
|
this.timecode = 0;
|
|
this.name = name = _name;
|
|
}
|
|
Encoder.prototype = {
|
|
addFrame: function(frame) {
|
|
if ('canvas' in frame) {
|
|
frame = frame.canvas;
|
|
}
|
|
if (width === null) {
|
|
this.width = width = frame.width,
|
|
this.height = height = frame.height
|
|
startEncoding();
|
|
} else
|
|
if (width !== frame.width || height !== frame.height) {
|
|
throw RangeError("Frame size error. Frames must be the same size.");
|
|
}
|
|
addFrame(frame);
|
|
this.frame += 1;
|
|
this.timecode = clusterTimecode;
|
|
},
|
|
toBlob: function() {
|
|
return toBlob(webmData);
|
|
}
|
|
}
|
|
return {
|
|
Video: Encoder,
|
|
}
|
|
})()
|
|
</script>
|
|
</body>
|
|
|
|
</html> |