mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-28 10:31:20 -08:00
完成截图工具
This commit is contained in:
1 parent
0ba8efc2b9
commit
5078533ad8
14 files changed
+667
-305
No files matched your search
@@ -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/)
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user