Skip to content

Instantly share code, notes, and snippets.

@leizongmin
Last active June 20, 2017 05:59
Show Gist options
  • Save leizongmin/81b481e9ee72cba6e0507e863e6f134c to your computer and use it in GitHub Desktop.
Save leizongmin/81b481e9ee72cba6e0507e863e6f134c to your computer and use it in GitHub Desktop.
HTML5本地视频截图演示
<!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