Last active
June 20, 2017 05:59
-
-
Save leizongmin/81b481e9ee72cba6e0507e863e6f134c to your computer and use it in GitHub Desktop.
HTML5本地视频截图演示
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
<!doctype html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>本地视频截图演示</title> | |
<style> | |
#videoInfo { | |
padding: 20px; | |
background: antiquewhite; | |
font-family: monospace; | |
font-size: 16px; | |
word-break: break-all; | |
} | |
#imageList img { | |
max-height: 200px; | |
margin: 20px; | |
} | |
#oneImage { | |
padding: 20px; | |
background: antiquewhite; | |
} | |
</style> | |
</head> | |
<body> | |
<h3>请选择一个视频文件</h3> | |
<input type="file" id="inputFile" accept="*" /> | |
<button onclick="processVideo.cancel()">取消</button> | |
<hr> | |
<h3>视频基本信息</h3> | |
<div id="videoInfo"></div> | |
<h3>序列帧图片(视频大于30秒截取16张,小于30秒截取8张)</h3> | |
<div id="imageList"></div> | |
<h3>序列帧合并成一张图片</h3> | |
<div id="oneImage"></div> | |
</body> | |
</html> | |
<script> | |
// --------------------------------------------------------------------------- | |
const processVideo = {}; | |
processVideo.$video = null; | |
processVideo.$canvas = null; | |
processVideo.$context = null; | |
processVideo.$cancelTaskTimeout = 10000; | |
processVideo.$cancelTaskCheckInterval = 500; | |
processVideo.$cancelTaskTid = null; | |
processVideo.$isCanceled = false; | |
processVideo.$isProcessing = false; | |
processVideo.$debug = function () { | |
console.log.apply(console, arguments); | |
}; | |
/** | |
* 初始化 | |
*/ | |
processVideo.init = function () { | |
const video = document.createElement('video'); | |
const canvas = document.createElement('canvas'); | |
const context = canvas.getContext('2d'); | |
processVideo.$video = video; | |
processVideo.$canvas = canvas; | |
processVideo.$context = context; | |
processVideo.$debug('已初始化'); | |
}; | |
/** | |
* 获取视频基本信息 | |
*/ | |
processVideo.getInfo = function (url) { | |
return new Promise((resolve, reject) => { | |
processVideo.$debug('获取视频基本信息...'); | |
processVideo.$isCanceled = false; | |
processVideo.$isProcessing = true; | |
processVideo.$video.src = url; | |
function onError(e) { | |
const err = new Error('读取视频失败'); | |
err.event = e; | |
processVideo.$debug(err); | |
callback(err); | |
} | |
function onLoad(e) { | |
processVideo.$debug('已加载视频...'); | |
const info = {}; | |
info.duration = processVideo.$video.duration; | |
info.width = processVideo.$video.videoWidth; | |
info.height = processVideo.$video.videoHeight; | |
callback(null, info); | |
} | |
function callback(err, ret) { | |
processVideo.$video.removeEventListener('error', onError); | |
processVideo.$video.removeEventListener('loadeddata', onLoad); | |
processVideo.$debug('获取视频基本信息结束'); | |
if (err) return reject(err); | |
resolve(ret); | |
}; | |
processVideo.$video.addEventListener('error', onError, { once: true }); | |
processVideo.$video.addEventListener('loadeddata', onLoad, { once: true }); | |
}); | |
}; | |
/** | |
* 开始处理视频 | |
* | |
* @param url | |
* @param options { width, height, interval, total } | |
*/ | |
processVideo.start = function (url, options) { | |
return new Promise((resolve, reject) => { | |
options = Object.assign({}, options || {}); | |
function callback(err, ret) { | |
processVideo.$debug('任务已结束'); | |
processVideo.$isProcessing = false; | |
if (err) return reject(err); | |
resolve(ret); | |
} | |
processVideo.$debug('开始任务...'); | |
const timestamp = Date.now(); | |
return processVideo.getInfo(url).then(info => { | |
async function renderAllFrames(info) { | |
for (let i = 0; i < info.duration && info.list.length < options.total; i += options.interval) { | |
if (processVideo.$isCanceled) { | |
break; | |
} | |
const data = await renderFrame(i); | |
info.list.push(data); | |
} | |
} | |
function renderFrame(seek) { | |
return new Promise((resolve, reject) => { | |
const cb = (e) => { | |
processVideo.$context.drawImage(processVideo.$video, 0, 0, info.width, info.height, 0, 0, options.width, options.height); | |
const data = processVideo.$canvas.toDataURL('image/jpeg'); | |
if (data.indexOf('data:image/jpeg') !== 0) { | |
processVideo.$debug(e); | |
const msg = '无法正确获取视频帧图像:seek=' + seek; | |
processVideo.$debug(msg); | |
const err = new Error(msg); | |
err.event = e; | |
return reject(err); | |
} | |
resolve(data); | |
}; | |
processVideo.$video.addEventListener('seeked', cb, { once: true }); | |
processVideo.$video.currentTime = seek; | |
processVideo.$debug('渲染位置:%s', seek); | |
}); | |
} | |
info.list = []; | |
info.options = options; | |
processVideo.$isCanceled = false; | |
processVideo.$isProcessing = true; | |
// 计算目标截图的宽高 | |
if (options.width) { | |
if (!options.height) { | |
options.height = Math.floor(options.width / (info.width / info.height)); | |
} | |
} else if (options.height) { | |
options.width = Math.floor(options.height * (info.width / info.height)); | |
} else { | |
options.width = info.width; | |
options.height = info.height; | |
} | |
// 计算截图时间间隔和总数 | |
if (!options.interval) { | |
if (options.total) { | |
options.interval = Number((info.duration / options.total).toFixed(3)); | |
} else { | |
options.interval = 1; | |
} | |
} | |
options.total = options.total || Math.floor(info.duration / options.interval); | |
// 设置画布宽高 | |
processVideo.$canvas.width = options.width; | |
processVideo.$canvas.height = options.height; | |
// 开始渲染所有帧 | |
renderAllFrames(info) | |
.then(() => { | |
info.spentTime = Date.now() - timestamp; | |
processVideo.$isProcessing = false; | |
callback(null, info); | |
}) | |
.catch(callback); | |
}).catch(callback); | |
}); | |
}; | |
/** | |
* 取消任务 | |
*/ | |
processVideo.cancel = function () { | |
return new Promise((resolve, reject) => { | |
if (processVideo.$isProcessing) { | |
processVideo.$debug('正在取消任务...'); | |
processVideo.$isCanceled = true; | |
let i = 0; | |
processVideo.$cancelTaskTid = setInterval(() => { | |
if (!processVideo.$isProcessing) { | |
clearInterval(processVideo.$cancelTaskTid); | |
processVideo.$debug('任务已取消'); | |
resolve(); | |
} | |
i += processVideo.$cancelTaskCheckInterval; | |
if (i >= processVideo.$cancelTaskTimeout) { | |
clearInterval(processVideo.$cancelTaskTid); | |
reject(new Error('取消任务超时')); | |
} | |
}, processVideo.$cancelTaskCheckInterval); | |
} else { | |
processVideo.$debug('没有正在进行的任务'); | |
resolve(); | |
} | |
}); | |
}; | |
// --------------------------------------------------------------------------- | |
/** | |
* 平铺合并多张图片 | |
* | |
* @param list | |
* @param options { borderWidth, borderColor } | |
*/ | |
async function combineImages(list, options) { | |
options = Object.assign({}, options || {}); | |
options.borderWidth = options.borderWidth || 0; | |
options.borderColor = options.borderColor || '#000'; | |
function loadImage(data) { | |
return new Promise((resolve, reject) => { | |
const img = document.createElement('img'); | |
img.src = data; | |
img.onload = (e) => { | |
resolve(img); | |
}; | |
img.onerror = (e) => { | |
const err = new Error('加载图片失败'); | |
err.event = e; | |
reject(); | |
} | |
}); | |
} | |
let height = 0; | |
let width = 0; | |
const canvas = document.createElement('canvas'); | |
const context = canvas.getContext('2d'); | |
const images = []; | |
for (let i = 0; i < list.length; i++) { | |
const img = await loadImage(list[i]); | |
images.push({ | |
x: width, | |
y: 0, | |
img: img, | |
}); | |
height = Math.max(height, img.height); | |
width += img.width; | |
} | |
canvas.width = width; | |
canvas.height = height; | |
images.forEach((item, i) => { | |
context.drawImage(item.img, item.x, item.y); | |
if (options.borderWidth > 0 && i > 0) { | |
context.fillStyle = options.borderColor; | |
context.fillRect(item.x, item.y, options.borderWidth, height); | |
} | |
}); | |
return canvas.toDataURL('image/jpeg'); | |
} | |
// --------------------------------------------------------------------------- | |
const inputFile = document.getElementById('inputFile'); | |
const videoInfo = document.getElementById('videoInfo'); | |
const imageList = document.getElementById('imageList'); | |
const oneImage = document.getElementById('oneImage'); | |
inputFile.addEventListener('change', (e) => { | |
const file = inputFile.files[0]; | |
if (!file) { | |
return alert('没有选择文件'); | |
} | |
if (file.type.indexOf('video/') !== 0) { | |
return alert('请选择视频文件'); | |
} | |
const url = URL.createObjectURL(file); | |
let total = 8; | |
videoInfo.innerHTML = '正在处理视频,请稍候...'; | |
processVideo.cancel() | |
.then(() => { | |
return processVideo.getInfo(url).then(info => { | |
if (info.duration >= 30) { | |
total = 16; | |
} | |
}); | |
}) | |
.then(() => { | |
return processVideo.start(url, { height: 100, total: total }).then(showResult); | |
}) | |
.catch(err => alert(err)); | |
}); | |
function showResult(info) { | |
imageList.innerHTML = ''; | |
info.list.forEach(data => { | |
const img = document.createElement('img'); | |
img.src = data; | |
imageList.appendChild(img); | |
}); | |
combineImages(info.list, { borderWidth: 1, borderColor: '#0000ff' }).then(data => { | |
const img = document.createElement('img'); | |
img.src = data; | |
oneImage.innerHTML = ''; | |
oneImage.appendChild(img); | |
delete info.list; | |
videoInfo.innerHTML = JSON.stringify(info); | |
}); | |
} | |
processVideo.init(); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment