From 81ed57c77fbb01ad2dc144bd6b2101fe2c2babdb Mon Sep 17 00:00:00 2001 From: Utkarsh Verma Date: Fri, 29 Apr 2022 09:32:47 +0530 Subject: [PATCH] Initial commit --- LICENSE | 20 ++++++++ index.js | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 10 ++++ 3 files changed, 162 insertions(+) create mode 100644 LICENSE create mode 100644 index.js create mode 100644 package.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..36abd50 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) Utkarsh Verma + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..a6ea61d --- /dev/null +++ b/index.js @@ -0,0 +1,132 @@ +const videoFrames = async options => { + + let index = 0; + let error = false; + let frames = []; + let interval; + let seekResolve; + let currentTime = 0; + let extractOffsets = false; + + const isNumber = n => { + return +n + '' === n + ''; + }; + + const isTimestamp = timestamp => { + return isNumber(timestamp) && +timestamp >= 0 && +timestamp <= video.duration; + }; + + const fallbackToDefault = (property, defaultValue) => { + options[property] = options.hasOwnProperty(property) ? options[property] : defaultValue; + }; + + // Buffer Video Element + const video = document.createElement('video'); + video.src = options.url; + video.crossOrigin = 'anonymous'; + video.onseeked = async () => { + if(seekResolve) + seekResolve(); + }; + video.onerror = () => { + error = true; + }; + + while((video.duration === Infinity || isNaN(video.duration)) && video.readyState < 2 && !error) { + await new Promise(r => setTimeout(r, 100)); + video.currentTime = 10000000 * Math.random(); + } + + // Set options to default values if not set + fallbackToDefault('format', 'image/png'); + fallbackToDefault('offsets', []); + fallbackToDefault('startTime', 0); + fallbackToDefault('endTime', video.duration); + fallbackToDefault('count', 1); + + // Filter out invalid offsets + if(options.offsets.constructor !== Array) + options.offsets = []; + else + options.offsets = options.offsets.filter(offset => { + return isTimestamp(offset); + }); + + if(options.offsets.length !== 0) + extractOffsets = true; + + // Check if start and end times are valid + if(!isTimestamp(options.startTime)) + options.startTime = 0; + + if(!isTimestamp(options.endTime)) + options.endTime = video.duration; + + if(options.startTime >= options.endTime) { + options.startTime = options.endTime; + options.count = 1; + } + + // Convert count value to a positive integer (floor() or 0 if string) + options.count = Math.abs(~~options.count); + + // Starting at startTime + interval and ending at endTime - interval + interval = (options.endTime - options.startTime) / (options.count + 1); + + // Set Width and Height + let isWidthSet = options.hasOwnProperty('width'); + let isHeightSet = options.hasOwnProperty('height'); + let videoDimensionRatio = video.videoWidth / video.videoHeight; + + // Reset Width and Height if not valid + if(isWidthSet && !isNumber(options.width)) + isWidthSet = false; + + if(isHeightSet && !isNumber(options.height)) + isHeightSet = false; + + if(!isWidthSet && !isHeightSet) { + // Both Width and Height not set + options.width = 128; + options.height = options.width / videoDimensionRatio; + + } else if(isWidthSet && !isHeightSet) { + // Width set but Height not set + options.height = options.width / videoDimensionRatio; + + } else if(!isWidthSet && isHeightSet) { + // Height set but Width not set + options.width = options.height * videoDimensionRatio; + } + + // Float values + options.width = +options.width; + options.height = +options.height; + + // Buffer Canvas Element + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.width = options.width; + canvas.height = options.height; + + return new Promise(async resolve => { + + if(error) + resolve([]); + + while( + (extractOffsets && index < options.offsets.length) || + (!extractOffsets && index < options.count) + ) { + video.currentTime = extractOffsets ? options.offsets[index] : options.startTime + (index + 1) * interval; + await new Promise(r => seekResolve = r); + context.clearRect(0, 0, canvas.width, canvas.height); + context.drawImage(video, 0, 0, canvas.width, canvas.height); + frames.push(canvas.toDataURL(options.format)); + index ++; + } + resolve(frames); + }); +}; + +module.exports = { videoFrames }; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..254d2c2 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "video-frames", + "version": "0.1.0", + "private": false, + "description": "Client side video frames extraction as base64 encoded images", + "author": "Utkarsh Verma", + "keywords": ["video", "frame", "client-side", "base64"], + "main": "index.js", + "license": "MIT" +}