diff --git a/client/package.json b/client/package.json index 58ad417..e70821a 100644 --- a/client/package.json +++ b/client/package.json @@ -53,6 +53,7 @@ "formik": "^2.2.9", "history": "^5.3.0", "lodash": "^4.17.21", + "moment": "^2.29.4", "numeral": "^2.0.6", "prop-types": "^15.8.1", "react": "^18.2.0", @@ -60,6 +61,7 @@ "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", "react-hook-form": "^7.37.0", + "react-moment": "^1.1.3", "react-player": "^2.11.0", "react-router-dom": "^6.4.2", "react-scripts": "^5.0.1", @@ -69,7 +71,6 @@ "yup": "^0.32.11" }, "devDependencies": { - "serve": "^14.2.0", "@babel/core": "^7.19.3", "@babel/eslint-parser": "^7.19.1", "@svgr/webpack": "^6.5.0", @@ -83,7 +84,8 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.31.10", "eslint-plugin-react-hooks": "^4.6.0", - "prettier": "^2.7.1" + "prettier": "^2.7.1", + "serve": "^14.2.0" }, "overrides": { "@svgr/webpack": "^6.5.0" diff --git a/client/src/pages/ProductsPage.js b/client/src/pages/ProductsPage.js index f9568ff..b779597 100644 --- a/client/src/pages/ProductsPage.js +++ b/client/src/pages/ProductsPage.js @@ -3,7 +3,7 @@ import { useState } from 'react'; // @mui import { Container, Stack, Typography } from '@mui/material'; // components -import { ProductSort, ProductList, ProductCartWidget, ProductFilterSidebar } from '../sections/@dashboard/products'; +import { ProductSort, VideoList, ProductCartWidget, ProductFilterSidebar } from '../sections/@dashboard/products'; // mock import PRODUCTS from '../_mock/products'; @@ -42,7 +42,7 @@ export default function ProductsPage() { - + diff --git a/client/src/pages/VideosPage.js b/client/src/pages/VideosPage.js index 8ae4cd0..8fdbe96 100644 --- a/client/src/pages/VideosPage.js +++ b/client/src/pages/VideosPage.js @@ -8,7 +8,7 @@ import { Container, Stack, Typography } from '@mui/material'; // components import { ProductSort, - ProductList, + VideoList, ProductCartWidget, ProductFilterSidebar, } from '../sections/@dashboard/products'; @@ -66,7 +66,7 @@ export default function ProductsPage() { - + diff --git a/client/src/sections/@dashboard/products/ProductList.js b/client/src/sections/@dashboard/products/ProductList.js deleted file mode 100644 index 1494145..0000000 --- a/client/src/sections/@dashboard/products/ProductList.js +++ /dev/null @@ -1,22 +0,0 @@ -import PropTypes from 'prop-types'; -// @mui -import { Grid } from '@mui/material'; -import ShopProductCard from './ProductCard'; - -// ---------------------------------------------------------------------- - -ProductList.propTypes = { - products: PropTypes.array.isRequired, -}; - -export default function ProductList({ products = [], ...other }) { - return ( - - {products.map((product) => ( - - - - ))} - - ); -} diff --git a/client/src/sections/@dashboard/products/ProductCard.js b/client/src/sections/@dashboard/products/VideoCard.js similarity index 57% rename from client/src/sections/@dashboard/products/ProductCard.js rename to client/src/sections/@dashboard/products/VideoCard.js index 46a743f..244f9b8 100644 --- a/client/src/sections/@dashboard/products/ProductCard.js +++ b/client/src/sections/@dashboard/products/VideoCard.js @@ -4,6 +4,7 @@ import { Box, Card, Stack, Typography } from '@mui/material'; import { styled } from '@mui/material/styles'; import { Link } from "react-router-dom"; +import Moment from 'react-moment'; // utils // components @@ -21,17 +22,18 @@ const StyledProductImg = styled('img')({ // ---------------------------------------------------------------------- -ShopProductCard.propTypes = { - product: PropTypes.object, +VideoCard.propTypes = { + video: PropTypes.object, }; -export default function ShopProductCard({ video }) { +export default function VideoCard({ video }) { const { title: name, thumbnailUrl: cover, viewCount, - duration: status, - publishedAt, + duration, + status, + recordingDate, _id: id, } = video; @@ -39,6 +41,8 @@ export default function ShopProductCard({ video }) { console.log('clicked', video); }; + const videoDuration = duration ?? 0; + return ( @@ -61,20 +65,23 @@ export default function ShopProductCard({ video }) { - - - {name} - - - - - {publishedAt} - {viewCount} views - + + + + {name} + + + + {recordingDate} + + + + + {viewCount} views + + {videoDuration*1000} + + ); diff --git a/client/src/sections/@dashboard/products/VideoList.js b/client/src/sections/@dashboard/products/VideoList.js new file mode 100644 index 0000000..f4b19ca --- /dev/null +++ b/client/src/sections/@dashboard/products/VideoList.js @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types'; +// @mui +import { Grid } from '@mui/material'; +import VideoCard from './VideoCard'; + +// ---------------------------------------------------------------------- + +VideoList.propTypes = { + videos: PropTypes.array.isRequired, +}; + +export default function VideoList({ videos = [], ...other }) { + return ( + + {videos.map((video) => ( + + + + ))} + + ); +} diff --git a/client/src/sections/@dashboard/products/index.js b/client/src/sections/@dashboard/products/index.js index 43784e1..5a7e980 100644 --- a/client/src/sections/@dashboard/products/index.js +++ b/client/src/sections/@dashboard/products/index.js @@ -1,5 +1,5 @@ -export { default as ProductCard } from './ProductCard'; -export { default as ProductList } from './ProductList'; +export { default as VideoCard } from './VideoCard'; +export { default as VideoList } from './VideoList'; export { default as ProductSort } from './ProductSort'; export { default as ProductCartWidget } from './ProductCartWidget'; export { default as ProductFilterSidebar } from './ProductFilterSidebar'; diff --git a/client/yarn.lock b/client/yarn.lock index 02937b8..f64eac1 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -6681,6 +6681,11 @@ mkdirp@~0.5.1: dependencies: minimist "^1.2.6" +moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -7967,6 +7972,11 @@ react-is@^18.0.0, react-is@^18.2.0: resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-moment@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-1.1.3.tgz#829b21dfb279aa6db47ce4f1ac2555af17a1bcdc" + integrity sha512-8EPvlUL8u6EknPp1ISF5MQ3wx2OHJVXIP/iZc4wRh3iV3XozftZERDv9ANZeAtMlhNNQHdFoqcZHFUkBSTONfA== + react-player@^2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/react-player/-/react-player-2.11.0.tgz#9afc75314eb915238e8d6615b2891fbe7170aeaa" diff --git a/server/package-lock.json b/server/package-lock.json index 674161c..b169254 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -82,7 +82,127 @@ "integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==", "optional": true }, - "@hapi/hoek": { + "node_modules/@ffprobe-installer/darwin-arm64": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@ffprobe-installer/darwin-arm64/-/darwin-arm64-5.0.1.tgz", + "integrity": "sha512-vwNCNjokH8hfkbl6m95zICHwkSzhEvDC3GVBcUp5HX8+4wsX10SP3B+bGur7XUzTIZ4cQpgJmEIAx6TUwRepMg==", + "cpu": [ + "arm64" + ], + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@ffprobe-installer/darwin-x64": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@ffprobe-installer/darwin-x64/-/darwin-x64-5.1.0.tgz", + "integrity": "sha512-J+YGscZMpQclFg31O4cfVRGmDpkVsQ2fZujoUdMAAYcP0NtqpC49Hs3SWJpBdsGB4VeqOt5TTm1vSZQzs1NkhA==", + "cpu": [ + "x64" + ], + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@ffprobe-installer/ffprobe": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@ffprobe-installer/ffprobe/-/ffprobe-2.1.1.tgz", + "integrity": "sha512-v4nAFp70Gv7yyEvjvlGTOrYn1atjFi6Z9vpGpGvWAiCXL6vnL88gBeIyJ4hY01G/XOw1LVuq1vjUM3gQzxMmIA==", + "engines": { + "node": ">=14.21.2" + }, + "optionalDependencies": { + "@ffprobe-installer/darwin-arm64": "5.0.1", + "@ffprobe-installer/darwin-x64": "5.1.0", + "@ffprobe-installer/linux-arm": "5.1.0", + "@ffprobe-installer/linux-arm64": "5.1.0", + "@ffprobe-installer/linux-ia32": "5.1.0", + "@ffprobe-installer/linux-x64": "5.1.0", + "@ffprobe-installer/win32-ia32": "5.1.0", + "@ffprobe-installer/win32-x64": "5.1.0" + } + }, + "node_modules/@ffprobe-installer/linux-arm": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@ffprobe-installer/linux-arm/-/linux-arm-5.1.0.tgz", + "integrity": "sha512-y34AEive/M5/++RcuRJZciICYYRkmTh5gK6th7raydgfQhIYy/8AXAIGKqD5Hn855i6o/RpEIPjk7DWpEuwrdA==", + "cpu": [ + "arm" + ], + "hasInstallScript": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffprobe-installer/linux-arm64": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@ffprobe-installer/linux-arm64/-/linux-arm64-5.1.0.tgz", + "integrity": "sha512-u5b3r3/39eZmEV0X4OKUfp6gO3lOyVIpRO4WU+5Eb6omWYy+k0OkVbRffK4qJF1Uz8irwzyiDsmUZ6vE13/abQ==", + "cpu": [ + "arm64" + ], + "hasInstallScript": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffprobe-installer/linux-ia32": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@ffprobe-installer/linux-ia32/-/linux-ia32-5.1.0.tgz", + "integrity": "sha512-Uqk4sYYQxz0KQmEQ2xxbVu/KiDX7Pw6wRDpCYv0sW49GI3wpX2Umqi/Kmtr0tpCvxctVoCdf/U71EAxH2Lztdg==", + "cpu": [ + "ia32" + ], + "hasInstallScript": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffprobe-installer/linux-x64": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@ffprobe-installer/linux-x64/-/linux-x64-5.1.0.tgz", + "integrity": "sha512-r7cGOjNb8AMnKAEvz5f8N/WsWsre02LhAfDKZX0m5J0bsrYgs2HUlnnQiwjRCH9CYXYerjYqq592o/GXvxDS+Q==", + "cpu": [ + "x64" + ], + "hasInstallScript": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffprobe-installer/win32-ia32": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@ffprobe-installer/win32-ia32/-/win32-ia32-5.1.0.tgz", + "integrity": "sha512-5O3vOoNRxmut0/Nu9vSazTdSHasrr+zPT2B3Hm7kjmO3QVFcIfVImS6ReQnZeSy8JPJOqXts5kX5x/3KOX54XQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@ffprobe-installer/win32-x64": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@ffprobe-installer/win32-x64/-/win32-x64-5.1.0.tgz", + "integrity": "sha512-jMGYeAgkrdn4e2vvYt/qakgHRE3CPju4bn5TmdPfoAm1BlX1mY9cyMd8gf5vSzI8gH8Zq5WQAyAkmekX/8TSTg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" diff --git a/server/package.json b/server/package.json index 4e21224..cf9c433 100644 --- a/server/package.json +++ b/server/package.json @@ -24,6 +24,7 @@ "license": "ISC", "dependencies": { "@ffmpeg-installer/ffmpeg": "^1.1.0", + "@ffprobe-installer/ffprobe": "^2.1.1", "bcrypt": "^5.1.0", "bullmq": "^3.5.11", "compression": "^1.7.4", @@ -33,6 +34,7 @@ "express": "^4.18.2", "express-pino-logger": "^7.0.0", "express-rate-limit": "^6.7.0", + "ffprobe": "^1.1.2", "fluent-ffmpeg": "^2.1.2", "helmet": "^6.0.1", "joi": "^17.7.0", diff --git a/server/scripts/seed-data/video.js b/server/scripts/seed-data/video.js index acadfd1..75654ec 100644 --- a/server/scripts/seed-data/video.js +++ b/server/scripts/seed-data/video.js @@ -1,5 +1,6 @@ const { faker } = require('@faker-js/faker'); const { MongoManager } = require('../../src/modules/db/mongo'); +const { VIDEO_STATUS, VIDEO_VISIBILITIES } = require('../../src/modules/db/constant'); const getFakeVideosData = (x) => { const videos = []; @@ -7,7 +8,7 @@ const getFakeVideosData = (x) => { for (let i = 0; i < 1000; i++) { videos.push({ title: faker.lorem.sentence(5), - visibility: faker.helpers.arrayElement(['Public', 'Private', 'Unlisted']), + visibility: faker.helpers.arrayElement(Object.values(VIDEO_VISIBILITIES)), category: faker.helpers.arrayElement([ 'education', 'entertainment', @@ -18,7 +19,8 @@ const getFakeVideosData = (x) => { recordingDate: faker.date.past(), publishedAt: faker.date.past(), fileName: faker.lorem.sentence(5), - + status: faker.helpers.arrayElement(Object.values(VIDEO_STATUS)), + isDeleted : faker.datatype.boolean(), thumbnailUrl: faker.image.imageUrl(), duration: parseInt(faker.random.numeric()), viewCount: parseInt(faker.random.numeric()), diff --git a/server/src/modules/db/constant.js b/server/src/modules/db/constant.js new file mode 100644 index 0000000..6fc6065 --- /dev/null +++ b/server/src/modules/db/constant.js @@ -0,0 +1,18 @@ +const VIDEO_VISIBILITIES = { + PUBLIC : 'Public', + PRIVATE : 'Private', + UNLISTED : 'Unlisted', +}; + + +const VIDEO_STATUS = { + PENDING : "pending", + PROCESSED : "processed", + PUBLISHED : "published" +}; + + +module.exports = { + VIDEO_STATUS, + VIDEO_VISIBILITIES +} \ No newline at end of file diff --git a/server/src/modules/db/schemas/videos.js b/server/src/modules/db/schemas/videos.js index 9648e40..f88f349 100644 --- a/server/src/modules/db/schemas/videos.js +++ b/server/src/modules/db/schemas/videos.js @@ -1,6 +1,6 @@ const { baseSchema, ensureCollection } = require('./common'); -const VIDEO_VISIBILITIES = ['Public', 'Private', 'Unlisted']; +const { VIDEO_STATUS, VIDEO_VISIBILITIES } = require('../constant'); const name = 'videos'; @@ -20,6 +20,7 @@ const updateSchema = async (db) => { 'fileName', 'originalName', 'visibility', + 'status', 'recordingDate', 'videoLink', ...Object.keys(baseSchema), @@ -40,7 +41,7 @@ const updateSchema = async (db) => { description: 'must be an integer', }, visibility: { - enum: VIDEO_VISIBILITIES, + enum: Object.values(VIDEO_VISIBILITIES), description: 'can only be one of the enum values and is required', }, duration: { @@ -48,6 +49,10 @@ const updateSchema = async (db) => { minimum: 1, description: 'must be an integer', }, + status: { + enum: Object.values(VIDEO_STATUS), + description: "can only be one of the enum values and is required", + }, playlistId: { bsonType: 'objectId', description: 'must be an objectId and is required', @@ -127,6 +132,10 @@ const updateSchema = async (db) => { key: { viewCount: -1 }, name: 'custom_viewCount_index', }, + { + key: { status: -1 }, + name: "custom_status_index", + }, ]; await ensureCollection({ db, name, validator, indexes }); diff --git a/server/src/modules/models/video/controller.js b/server/src/modules/models/video/controller.js index 53cb58c..0f50cff 100644 --- a/server/src/modules/models/video/controller.js +++ b/server/src/modules/models/video/controller.js @@ -4,11 +4,13 @@ const { search, update, getById, + updateViewCount, deleteById, } = require('./service'); const { validate } = require('./request'); const { VIDEO_QUEUE_EVENTS: QUEUE_EVENTS } = require('../../queues/constants'); const { addQueueItem } = require('../../queues/queue'); +const { getVideoDurationAndResolution } = require('../../queues/video-processor'); const BASE_URL = `/api/videos`; @@ -29,7 +31,7 @@ const setupRoutes = (app) => { app.get(`${BASE_URL}/detail/:id`, async (req, res) => { console.log(`GET`, req.params); - const video = await getById(req.params.id); + const video = await updateViewCount(req.params.id); if (video instanceof Error) { return res.status(400).json(JSON.parse(video.message)); } @@ -133,14 +135,17 @@ const setupRoutes = (app) => { app.post(`${BASE_URL}/upload`, uploadProcessor, async (req, res) => { try { - console.log('POST upload', JSON.stringify(req.body)); + + const { videoDuration } = await getVideoDurationAndResolution(`./${req.file.path}`) + const dbPayload = { ...req.body, fileName: req.file.filename, - originalName: req.file.originalname, + originalName: req.file.originalname, recordingDate: new Date(), videoLink: req.file.path, - viewCount: 0, + viewCount:0, + duration:videoDuration }; console.log('dbPayload', dbPayload); // TODO: save the file info and get the id from the database diff --git a/server/src/modules/models/video/handler.js b/server/src/modules/models/video/handler.js index c8aa605..d275d8d 100644 --- a/server/src/modules/models/video/handler.js +++ b/server/src/modules/models/video/handler.js @@ -1,16 +1,19 @@ const eventEmitter = require('../../../event-manager').getInstance(); const { VIDEO_QUEUE_EVENTS } = require('../../queues/constants'); -const { updateHistory } = require('./service'); +const { updateHistory, update } = require('./service'); +const { VIDEO_STATUS } = require('../../db/constant') const setup = () => { // eventEmitter.on(VIDEO_QUEUE_EVENTS.VIDEO_UPLOADED, (data) => { // console.log('VIDEO_QUEUE_EVENTS.VIDEO_UPLOADED Event handler', data); // }); - console.log('registering video queue events'); + const SERVER_URL = process.env.SERVER_URL; + Object.values(VIDEO_QUEUE_EVENTS).forEach((eventName) => { + eventEmitter.on(eventName, async (data) => { - console.log(`models/video/handler.js - ${eventName}`, data); + if (eventName === VIDEO_QUEUE_EVENTS.VIDEO_PROCESSED) { await updateHistory(data.id, { history: { status: eventName, createdAt: new Date() }, @@ -18,13 +21,21 @@ const setup = () => { }); return; } + if (eventName === VIDEO_QUEUE_EVENTS.VIDEO_HLS_CONVERTED) { await updateHistory(data.id, { history: { status: eventName, createdAt: new Date() }, hlsPath: data.path, }); + + await update({ + _id: data.id, + status: VIDEO_STATUS.PUBLISHED + }); + return; } + if (eventName === VIDEO_QUEUE_EVENTS.VIDEO_THUMBNAIL_GENERATED) { await updateHistory(data.id, { history: { status: eventName, createdAt: new Date() }, diff --git a/server/src/modules/models/video/service.js b/server/src/modules/models/video/service.js index 8693e26..6033c47 100644 --- a/server/src/modules/models/video/service.js +++ b/server/src/modules/models/video/service.js @@ -1,9 +1,13 @@ const { ObjectId } = require('mongodb'); const { Video } = require('../../db/collections'); +const { VIDEO_STATUS } = require('../../db/constant') + + +// TODO: add logging const insert = async (document) => { try { - return await Video.insert(document); + return await Video.insert({status: VIDEO_STATUS.PENDING, ...document}); // assigning default satus for all new videos } catch (error) { return error; } @@ -13,7 +17,6 @@ const update = async (document) => { try { return await Video.update(document); } catch (error) { - console.error(error); return error; } }; @@ -34,6 +37,7 @@ const search = async (searchObject) => { category: 1, duration: 1, viewCount: 1, + status : 1 }; const sort = searchObject.sort || { viewCount: -1 }; @@ -81,6 +85,7 @@ const deleteById = async (id) => { } }; + module.exports = { insert, search, diff --git a/server/src/modules/queues/video-processor.js b/server/src/modules/queues/video-processor.js index 32f32db..b6592e6 100644 --- a/server/src/modules/queues/video-processor.js +++ b/server/src/modules/queues/video-processor.js @@ -1,8 +1,12 @@ /** execute function will take a filePath and run ffmpeg command to convert it to mp4 */ -const ffmpegInstaller = require("@ffmpeg-installer/ffmpeg"); const ffmpeg = require("fluent-ffmpeg"); + +const ffmpegInstaller = require("@ffmpeg-installer/ffmpeg"); ffmpeg.setFfmpegPath(ffmpegInstaller.path); -console.log(ffmpegInstaller.path, ffmpegInstaller.version); + +const ffprobeInstaller = require('@ffprobe-installer/ffprobe'); +ffmpeg.setFfprobePath(ffprobeInstaller.path); + const path = require("path"); const { VIDEO_QUEUE_EVENTS: QUEUE_EVENTS } = require("./constants"); const { addQueueItem } = require("./queue"); @@ -102,4 +106,31 @@ const processMp4ToHls = async (filePath, outputFolder, jobData) => { return; }; -module.exports = { processRawFileToMp4, processMp4ToHls, generateThumbnail }; +const getVideoDurationAndResolution = (filePath) => { + + // if any error occour return error + // else return video meta data + return new Promise((resolve,reject) => { + let videoDuration = 0 + let videoResolution = { + height : 0, + width : 0 + } + ffmpeg.ffprobe(filePath, function(err, metadata) { + if(!err){ + videoDuration = parseInt(metadata.format.duration); + videoResolution ={ + height : metadata.streams[0].coded_height, + width : metadata.streams[0].coded_width + } + }; + + resolve({videoDuration, videoResolution}); + }); + }) + + +} + + +module.exports = { processRawFileToMp4, processMp4ToHls, generateThumbnail, getVideoDurationAndResolution }; \ No newline at end of file