forked from renzhezhilu/webp2jpg-online
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a59e8ee
commit f78fd0c
Showing
8 changed files
with
653 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
.DS_Store | ||
node_modules | ||
/dist | ||
/test | ||
|
||
/psd | ||
|
||
# local env files | ||
|
Large diffs are not rendered by default.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
|
||
<body> | ||
|
||
<p>要使用的视频:</p> | ||
|
||
<video id="video1" controls width="270" autoplay> | ||
<source src="mov_bbb.mp4" type='video/mp4'> | ||
</video> | ||
|
||
<p>画布(每 20 毫秒,代码就会绘制视频的当前帧):</p> | ||
|
||
<canvas id="myCanvas" width="270" height="135" style="border:1px solid #d3d3d3;"> | ||
Your browser does not support the HTML5 canvas tag. | ||
</canvas> | ||
|
||
<canvas id="ccccc" width="270" height="135" style="border:1px solid #d3d3d3;"> | ||
Your browser does not support the HTML5 canvas tag. | ||
</canvas> | ||
|
||
|
||
<img id="img" src="./g.gif" alt=""> | ||
|
||
<script> | ||
// var v = document.getElementById("video1"); | ||
// var c = document.getElementById("myCanvas"); | ||
// ctx = c.getContext('2d'); | ||
|
||
// v.addEventListener('play', function() { | ||
// var i = window.setInterval(function() { | ||
// ctx.drawImage(v, 0, 0, 270, 135) | ||
// }, 20); | ||
// }, false); | ||
// v.addEventListener('pause', function() { | ||
// window.clearInterval(i); | ||
// }, false); | ||
// v.addEventListener('ended', function() { | ||
// clearInterval(i); | ||
// }, false); | ||
|
||
// let xhr = new XMLHttpRequest(); | ||
// xhr.open('get', './g.gif'); | ||
// xhr.setRequestHeader("Content-Type", "image/gif"); | ||
// xhr.send(); | ||
// xhr.onreadystatechange = function() { | ||
// if (xhr.readyState === 4) { | ||
// if (xhr.status === 200) { | ||
// console.log('response', xhr.response); | ||
// } | ||
// } | ||
// } | ||
|
||
const img = document.getElementById("img"); | ||
const ccc = document.getElementById("ccccc"); | ||
const ctx22 = ccc.getContext('2d'); | ||
render(); | ||
|
||
function render() { | ||
ctx22.drawImage(img, 0, 0); | ||
window.requestAnimationFrame(render); | ||
} | ||
</script> | ||
|
||
</body> | ||
|
||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,315 @@ | ||
<!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> |
Oops, something went wrong.