完成截图工具

This commit is contained in:
Harvey Zack committed 2018-12-05 14:45:16 +08:00
1 parent 0ba8efc2b9
commit 5078533ad8
14 files changed
+667 -305

No files matched your search

+77 -2
View File
@@ -1,3 +1,78 @@
# artplayer-plugin-danmu
Danmu plugin for ArtPlayer
# artplayer-tool-thumbnail
thumbnail tool for ArtPlayer
## Demo
[Checkout the demo](https://artplayer.org/thumbnail/) from Github Pages
## Install
Install with `npm`
```
$ npm install artplayer-tool-thumbnail
```
Or install with `yarn`
```
$ yarn add artplayer-tool-thumbnail
```
```js
import ArtplayerToolThumbnail from 'artplayer-tool-thumbnail';
```
Or umd builds are also available
```html
<script src="artplayer-tool-thumbnail"></script>
```
Will expose the global variable to `window.ArtplayerToolThumbnail`.
## Usage
```html
<input class="file" type="file">
```
```js
var thumbnail = new ArtplayerToolThumbnail({
fileInput: document.querySelector('.file'),
delay: 300,
number: 60,
width: 160,
height: 90,
column: 10,
});
thumbnail.on('file', file => {
console.log(file);
});
thumbnail.on('video', video => {
console.log(video);
thumbnail.start();
});
thumbnail.on('canvas', canvas => {
console.log(canvas);
});
thumbnail.on('update', (percentage, url) => {
console.log(percentage, url);
});
thumbnail.on('download', name => {
console.log(name);
});
thumbnail.on('done', () => {
console.log('done');
});
```
## License
MIT © [Harvey Zack](https://www.zhw-island.com/)
+125 -91
View File
@@ -1,48 +1,15 @@
import './index.scss';
import Emitter from 'tiny-emitter';
import { runPromisesInSeries, sleep, getFileName, clamp } from './utils';
function errorHandle(condition, msg) {
if (!condition) {
throw new Error(msg);
}
}
function runPromisesInSeries(ps) {
return ps.reduce((p, next) => p.then(next), Promise.resolve());
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class ArtplayerToolThumbnail {
constructor(option) {
if (option.fileInput) {
errorHandle(
option.fileInput.tagName === 'INPUT' && option.fileInput.type === 'file',
'This fileInput is not a file input',
);
}
if (option.videoElement) {
errorHandle(option.fileInput.tagName === 'VIDEO', 'This videoElement is not a video element');
}
if (option.callbackVideoUrl) {
errorHandle(typeof option.callbackVideoUrl === 'function', 'This callbackVideoUrl is not a function');
}
if (option.callbackThumbnailUrl) {
errorHandle(
typeof option.callbackThumbnailUrl === 'function',
'This callbackThumbnailUrl is not a function',
);
}
this.option = {
...ArtplayerToolThumbnail.DEFAULTS,
...option,
};
this.getVideoUrl();
class ArtplayerToolThumbnail extends Emitter {
constructor(option = {}) {
super();
this.processing = false;
this.option = {};
this.setup(Object.assign({}, ArtplayerToolThumbnail.DEFAULTS, option));
this.video = ArtplayerToolThumbnail.creatVideo();
this.inputChange = this.inputChange.bind(this);
this.option.fileInput.addEventListener('change', this.inputChange);
}
static get DEFAULTS() {
@@ -55,70 +22,137 @@ class ArtplayerToolThumbnail {
};
}
mergeOption(option = {}) {
this.option = {
...this.option,
...option,
};
return this.option;
setup(option = {}) {
this.option = Object.assign({}, this.option, option);
const { fileInput, delay, number, width, height, column } = this.option;
this.errorHandle(
fileInput.tagName === 'INPUT' && fileInput.type === 'file',
'The \'fileInput\' is not a usable file input like: <input type="file">',
);
['delay', 'number', 'width', 'height', 'column'].forEach(item => {
this.errorHandle(typeof this.option[item] === 'number', `The '${item}' is not a number`);
});
this.option.delay = clamp(delay, 100, 1000);
this.option.number = clamp(number, 10, 100);
this.option.width = clamp(width, 100, 500);
this.option.height = clamp(height, 100, 500);
this.option.column = clamp(column, 10, 100);
return this;
}
getVideoUrl() {
const fileType = ['video/mp4', 'video/ogg', 'video/webm'];
this.option.fileInput.addEventListener('change', () => {
const file = this.option.fileInput.files[0];
if (file) {
errorHandle(fileType.includes(file.type), `Only file types are supported: ${fileType.toString()}`);
const videoUrl = URL.createObjectURL(file);
this.videoUrl = videoUrl;
if (this.option.callbackVideoUrl) {
this.option.callbackVideoUrl(videoUrl);
}
}
static creatVideo() {
const video = document.createElement('video');
video.style.position = 'absolute';
video.style.top = '-9999px';
video.style.left = '-9999px';
video.muted = true;
video.controls = true;
document.body.appendChild(video);
return video;
}
inputChange() {
const { delay } = this.option;
const fileType = ['video/mp4', 'audio/ogg', 'video/webm'];
const file = this.option.fileInput.files[0];
if (file) {
this.errorHandle(
fileType.includes(file.type),
`Only file types are supported: ${fileType.toString()}, but got ${file.type}`,
);
const videoUrl = URL.createObjectURL(file);
this.videoUrl = videoUrl;
this.file = file;
this.emit('file', this.file);
this.video.src = videoUrl;
sleep(delay).then(() => {
this.emit('video', this.video);
});
}
}
start() {
this.errorHandle(this.file && this.video, 'Please select the video file first');
this.errorHandle(!this.processing, 'There is currently a task in progress, please wait a moment...');
const { width, height, number, delay } = this.option;
const screenshotDate = this.creatScreenshotDate();
const canvas = this.creatCanvas();
const context2D = canvas.getContext('2d');
this.emit('canvas', canvas);
const promiseList = screenshotDate.map((item, index) => () => {
this.video.currentTime = item.time;
return sleep(delay).then(() => {
context2D.drawImage(this.video, item.x, item.y, width, height);
canvas.toBlob(blob => {
this.thumbnailUrl = URL.createObjectURL(blob);
this.emit('update', (index + 1) / number, this.thumbnailUrl);
});
});
});
this.processing = true;
runPromisesInSeries(promiseList).then(() => {
sleep(delay).then(() => {
this.processing = false;
this.emit('done');
});
});
}
getThumbnailUrl(option = {}) {
this.mergeOption(option);
const { number, width, height, column, videoElement, callbackDone } = this.option;
const { duration } = videoElement;
creatScreenshotDate() {
const { number, width, height, column } = this.option;
const { duration } = this.video;
const timeGap = duration / number;
const timePoints = [timeGap];
while (timePoints.length < number) {
const last = timePoints[timePoints.length - 1];
timePoints.push(last + timeGap);
}
const screenshotDate = timePoints.map((item, index) => ({
return timePoints.map((item, index) => ({
time: item - timeGap / 2,
x: (index % column) * width,
y: Math.floor(index / column) * height,
}));
const canvas = document.createElement('canvas');
canvas.width = width * column;
canvas.height = Math.ceil(number / column) * height;
const promiseList = screenshotDate.map(item => this.getScreenshot(canvas, item));
runPromisesInSeries(promiseList).then(() => {
const thumbnailUrl = canvas.toDataURL('image/png');
this.thumbnailUrl = thumbnailUrl;
if (callbackDone) {
callbackDone(this.videoUrl, thumbnailUrl);
}
});
}
getScreenshot(canvas, item) {
return () => {
const context2D = canvas.getContext('2d');
const { width, height, videoElement, delay, callbackThumbnailUrl } = this.option;
videoElement.currentTime = item.time;
return sleep(delay).then(() => {
context2D.drawImage(videoElement, item.x, item.y, width, height);
const thumbnailUrl = canvas.toDataURL('image/png');
if (callbackThumbnailUrl) {
callbackThumbnailUrl(thumbnailUrl);
}
});
};
creatCanvas() {
const { number, width, height, column } = this.option;
const canvas = document.createElement('canvas');
const context2D = canvas.getContext('2d');
canvas.width = width * column;
canvas.height = Math.ceil(number / column) * height + 30;
context2D.fillStyle = 'black';
context2D.fillRect(0, 0, canvas.width, canvas.height);
context2D.font = '14px Georgia';
context2D.fillStyle = '#fff';
context2D.fillText(
`From: https://artplayer.org/thumbnail, Number: ${number}, Width: ${width}, Height: ${height}, Column: ${column}`,
10,
canvas.height - 12,
);
return canvas;
}
download() {
this.errorHandle(this.file && this.thumbnailUrl, 'Download does not seem to be ready');
this.errorHandle(!this.processing, 'There is currently a task in progress, please wait a moment...');
const elink = document.createElement('a');
const name = `${getFileName(this.file.name)}.png`;
elink.download = name;
elink.href = this.thumbnailUrl;
document.body.appendChild(elink);
elink.click();
document.body.removeChild(elink);
this.emit('download', name);
}
errorHandle(condition, msg) {
if (!condition) {
this.emit('error', msg);
throw new Error(msg);
}
}
}
@@ -1,3 +0,0 @@
.artplayer-tool-thumbnail {
display: flex;
}
@@ -0,0 +1,17 @@
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function runPromisesInSeries(ps) {
return ps.reduce((p, next) => p.then(next), Promise.resolve());
}
export function getFileName(name) {
const nameArray = name.split('.');
nameArray.pop();
return nameArray.join('.');
}
export function clamp(num, a, b) {
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
}