import Emitter from 'tiny-emitter'; import { runPromisesInSeries, sleep, getFileName, clamp } from './utils'; 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.ondrop = this.ondrop.bind(this); this.option.fileInput.addEventListener('change', this.inputChange); this.option.fileInput.addEventListener('dragover', ArtplayerToolThumbnail.ondragover); this.option.fileInput.addEventListener('drop', ArtplayerToolThumbnail.ondrop); } static get DEFAULTS() { return { delay: 300, number: 60, width: 160, height: 90, column: 10, }; } static ondragover(e) { e.preventDefault(); } ondrop(e) { e.preventDefault(); const file = e.dataTransfer.files[0]; this.loadVideo(file); } setup(option = {}) { this.option = Object.assign({}, this.option, option); const { fileInput, delay, number, width, height, column } = this.option; this.errorHandle(fileInput instanceof Element, "The 'fileInput' is not a Element"); if (!(fileInput.tagName === 'INPUT' && fileInput.type === 'file')) { fileInput.style.position = 'relative'; const newFileInput = document.createElement('input'); newFileInput.type = 'file'; newFileInput.style.position = 'absolute'; newFileInput.style.width = '100%'; newFileInput.style.height = '100%'; newFileInput.style.left = '0'; newFileInput.style.top = '0'; newFileInput.style.right = '0'; newFileInput.style.bottom = '0'; newFileInput.style.opacity = '0'; fileInput.appendChild(newFileInput); this.option.fileInput = newFileInput; } ['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, 10, 1000); this.option.number = clamp(number, 10, 1000); this.option.width = clamp(width, 10, 1000); this.option.height = clamp(height, 10, 1000); this.option.column = clamp(column, 1, 1000); return this; } 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 file = this.option.fileInput.files[0]; this.loadVideo(file); } loadVideo(file) { const { delay } = this.option; if (file) { const canPlayType = this.video.canPlayType(file.type); this.errorHandle( canPlayType === 'maybe' || canPlayType === 'probably', `Playback of this file format is not supported: ${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); }) .catch(err => { this.emit('error', err.message); throw err; }); } } start() { const { width, height, number, delay } = this.option; this.density = number / this.video.duration; 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...'); this.errorHandle(this.density <= 1, `The preview density cannot be greater than 1, but got ${this.density}`); 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 new Promise(resolve => { sleep(delay) .then(() => { context2D.drawImage(this.video, item.x, item.y, width, height); canvas.toBlob(blob => { if (this.thumbnailUrl) { URL.revokeObjectURL(this.thumbnailUrl); } this.thumbnailUrl = URL.createObjectURL(blob); this.emit('update', this.thumbnailUrl, (index + 1) / number); resolve(); }); }) .catch(err => { throw err; }); }); }); this.processing = true; return runPromisesInSeries(promiseList) .then(() => sleep(delay * 2) .then(() => { this.processing = false; this.emit('done'); }) .catch(err => { this.processing = false; this.emit('error', err.message); throw err; }), ) .catch(err => { this.processing = false; this.emit('error', err.message); throw err; }); } 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); } return timePoints.map((item, index) => ({ time: item - timeGap / 2, x: (index % column) * width, y: Math.floor(index / column) * height, })); } 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/, Number: ${number}, Width: ${width}, Height: ${height}, Column: ${column}`, 10, canvas.height - 11, ); return canvas; } download() { this.errorHandle( this.file && this.thumbnailUrl, 'Download does not seem to be ready, please create preview first', ); 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); return this; } errorHandle(condition, msg) { if (!condition) { this.emit('error', msg); throw new Error(msg); } } destroy() { this.option.fileInput.removeEventListener('change', this.inputChange); this.option.fileInput.removeEventListener('dragover', ArtplayerToolThumbnail.ondragover); this.option.fileInput.removeEventListener('drop', ArtplayerToolThumbnail.ondrop); document.body.removeChild(this.video); if (this.videoUrl) { URL.revokeObjectURL(this.videoUrl); } if (this.thumbnailUrl) { URL.revokeObjectURL(this.thumbnailUrl); } this.emit('destroy'); } } export default ArtplayerToolThumbnail;