From f4a384b194f1267d1a5a22cedf2a06edcd6e1b26 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 7 Jun 2022 17:26:53 -0400 Subject: started working with the pointer events to drag - very laggy, may try something more simple for now --- .../views/nodes/RecordingBox/ProgressBar.scss | 33 ++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) (limited to 'src/client/views/nodes/RecordingBox/ProgressBar.scss') diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss index a493b0b89..5d27c8a7d 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.scss +++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss @@ -1,11 +1,14 @@ .progressbar { + touch-action: none; + + position: absolute; display: flex; justify-content: flex-start; bottom: 10px; width: 80%; - height: 5px; + height: 20px; background-color: gray; &.done { @@ -23,4 +26,30 @@ z-index: 3; pointer-events: none; } -} \ No newline at end of file +} + +.segment { + border: 3px solid black; + background-color: red; + margin: 1px; + padding: 0; + cursor: pointer; + transition-duration: .25s; + + text-align: center; +} + +.segment:first-child { + margin-left: 2px; +} +.segment:last-child { + margin-right: 2px; +} + +.segment:hover { + margin: 0px; + border: 4px solid red; + background-color: black; + min-width: 10px; + border-radius: 2px; +} -- cgit v1.2.3-70-g09d2 From ac76c4836c61cc6564367f35e14014bb9489257b Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 7 Jun 2022 19:22:37 -0400 Subject: got basic ui for segmentation to work - W overall --- .../views/nodes/RecordingBox/ProgressBar.scss | 2 +- .../views/nodes/RecordingBox/ProgressBar.tsx | 113 ++++++++++++++++----- .../views/nodes/RecordingBox/RecordingView.tsx | 1 + 3 files changed, 91 insertions(+), 25 deletions(-) (limited to 'src/client/views/nodes/RecordingBox/ProgressBar.scss') diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss index 5d27c8a7d..67f96033a 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.scss +++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss @@ -8,7 +8,7 @@ justify-content: flex-start; bottom: 10px; width: 80%; - height: 20px; + height: 30px; background-color: gray; &.done { diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx index 3e71239ce..b17d27d2e 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -1,3 +1,5 @@ +import { resolveTxt } from 'dns'; +import { videointelligence } from 'googleapis/build/src/apis/videointelligence'; import { isInteger } from 'lodash'; import * as React from 'react'; import { useEffect, useState, useCallback, useRef } from "react" @@ -6,6 +8,7 @@ import { MediaSegment } from './RecordingView'; interface ProgressBarProps { videos: MediaSegment[], + setVideos, } export function ProgressBar(props: ProgressBarProps) { @@ -13,12 +16,30 @@ export function ProgressBar(props: ProgressBarProps) { const progressBarRef = useRef(null) // array for the order of video segments - const [segments, setSegments] = useState([]); + const [segments, setSegments] = useState([]); + const [ordered, setOrdered] = useState([]); + + const [clicked, setClicked] = useState(-1); // const totalTime = useMemo(() => props.videos.lastElement().endTime, [props.videos]) const totalTime = () => props.videos.lastElement().endTime + useEffect(() => { + const segmentsJSX = ordered.map((vid, i) => +
{vid.order}
); + + setSegments(segmentsJSX) + }, [clicked, ordered]) + + useEffect(() => { + setOrdered(props.videos.map((vid, order) => { + //const { endTime, startTime } = vid + // TODO: not tranfer the blobs around + return { ...vid, order }; + })) + }, [props.videos]); + // const handleClick = (e: React.MouseEvent) => { @@ -39,6 +60,19 @@ export function ProgressBar(props: ProgressBarProps) { // } // } + const updateLastHover = (index) => { + // id for segment is like 'segment-1' or 'segment-10' + const segId = index + console.log(segId) + const rect = progressBarRef.current?.children[segId].getBoundingClientRect() + console.log(rect) + return { + index: segId, + minX: rect.x, + maxX: rect.x + rect.width, + } + } + const onPointerDown = (e: React.PointerEvent) =>{ // e.persist() // console.log('event', e) @@ -48,41 +82,72 @@ export function ProgressBar(props: ProgressBarProps) { const progressBar = progressBarRef.current if (progressBar == null) return console.log('progressbar', progressBar) - - const updateSegmentOrder = (event: PointerEvent): void => { - // event.stopPropagation() - - // const hoveredSegment = event.target as HTMLDivElement & EventTarget - // console.log('updateSegmentOrder called', hoveredSegment.id === clickedSegment.id) - // if (hoveredSegment.id === clickedSegment.id) return; - - console.log('updateSegmentOrder : target', event.target.id) - } // progressBar.addEventListener('pointermove', updateSegmentOrder) const clickedSegment = e.target as HTMLDivElement & EventTarget - console.log('segment', clickedSegment.getBoundingClientRect()) const rect = clickedSegment.getBoundingClientRect() - + setClicked(parseInt(clickedSegment.id.split('-')[1])) + let lastHover = { + index: parseInt(clickedSegment.id.split('-')[1]), + minX: rect.x, + maxX: rect.x + rect.width, + } + + // clickedSegment.style.backgroundColor = `inherit`; + const dot = document.createElement("div") + dot.classList.add("segment") + dot.style.position = 'absolute'; + dot.style.zIndex = '999'; + dot.style.width = `${rect.width}px`; + dot.style.height = `${rect.height}px`; + document.body.append(dot) const dragSegment = (event: PointerEvent): void => { // event.stopPropagation() - clickedSegment.style.position = 'absolute'; - clickedSegment.style.zIndex = '999'; - clickedSegment.style.width = `${rect.width}px`; - clickedSegment.style.height = `${rect.height}px`; - clickedSegment.style.left = `${event.offsetX - rect.width/2}px`; - clickedSegment.style.top = `${event.offsetY - rect.height/2}px`; + dot.style.left = `${event.clientX - rect.width/2}px`; + dot.style.top = `${event.clientY - rect.height/2}px`; + } + + const updateSegmentOrder = (event: PointerEvent): void => { + event.stopPropagation(); + + dragSegment(event) + + const oldIndex = lastHover.index + const swapSegments = (newIndex) => { + if (newIndex == null) return + setOrdered(prevOrdered => { + const cpy = [...prevOrdered] + cpy[oldIndex] = cpy[newIndex] + cpy[newIndex] = prevOrdered[oldIndex] + return cpy + }) + setClicked(newIndex) + } + + const curX = event.clientX; + if (curX < lastHover.minX && lastHover.index > 0) { + swapSegments(lastHover.index - 1) + lastHover = updateLastHover(lastHover.index - 1) + } + else if (curX > lastHover.maxX && lastHover.index < segments.length - 1) { + swapSegments(lastHover.index + 1) + lastHover = updateLastHover(lastHover.index + 1) + } } progressBar.setPointerCapture(e.pointerId) - progressBar.addEventListener('pointermove', dragSegment) - progressBar.addEventListener('pointerup', () => { - progressBar.removeEventListener('pointermove', dragSegment), { once: true } - clickedSegment.style.position = clickedSegment.style.zIndex = 'inherit'; + progressBar.addEventListener('pointermove', updateSegmentOrder) + progressBar.addEventListener('pointerup', (event) => { + progressBar.removeEventListener('pointermove', updateSegmentOrder), { once: true } + // clickedSegment.style.position = clickedSegment.style.zIndex = 'inherit'; // progressBar.removeEventListener('pointermove', updateSegmentOrder), { once: true } + // clickedSegment.style.backgroundColor = 'red'; + setClicked(-1); + // updateSegmentOrder(event); + dot.remove(); }) } @@ -93,7 +158,7 @@ export function ProgressBar(props: ProgressBarProps) { style={{ width: `${props.progress}%` }} // onClick={handleClick} > */} - {props.videos.map((vid, i) =>
)} + {segments} ) } \ No newline at end of file diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 2420c126f..640e4f5f2 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -351,6 +351,7 @@ export function RecordingView(props: IRecordingViewProps) { -- cgit v1.2.3-70-g09d2 From c656abad84ac56c3067a48ca6d34d567e3fcdd88 Mon Sep 17 00:00:00 2001 From: Michael Foiani Date: Wed, 8 Jun 2022 00:05:34 -0400 Subject: Took stab at ffmpeg in server. did some reworking of recordingview css. fix smoothness bug for the moving segment that follow cursor. --- src/client/Network.ts | 15 +++++++- .../views/nodes/RecordingBox/ProgressBar.scss | 33 +++++++++++++++--- .../views/nodes/RecordingBox/ProgressBar.tsx | 9 ++--- .../views/nodes/RecordingBox/RecordingBox.tsx | 4 +-- .../views/nodes/RecordingBox/RecordingView.scss | 29 ++++++++++------ .../views/nodes/RecordingBox/RecordingView.tsx | 23 +++++++++---- src/server/ApiManagers/UploadManager.ts | 28 +++++++++++++-- src/server/DashUploadUtils.ts | 40 ++++++++++++++-------- 8 files changed, 136 insertions(+), 45 deletions(-) (limited to 'src/client/views/nodes/RecordingBox/ProgressBar.scss') diff --git a/src/client/Network.ts b/src/client/Network.ts index 1255e5ce0..2c6d9d711 100644 --- a/src/client/Network.ts +++ b/src/client/Network.ts @@ -19,7 +19,6 @@ export namespace Networking { } export async function UploadFilesToServer(files: File | File[]): Promise[]> { - console.log(files) const formData = new FormData(); if (Array.isArray(files)) { if (!files.length) { @@ -36,6 +35,19 @@ export namespace Networking { const response = await fetch("/uploadFormData", parameters); return response.json(); } + + export async function UploadSegmentsAndConcatenate(files: File | File[]): Promise[]> { + console.log(files) + const formData = new FormData(); + if (!Array.isArray(files) || !files.length) return []; + files.forEach(file => formData.append(Utils.GenerateGuid(), file)); + const parameters = { + method: 'POST', + body: formData + }; + const response = await fetch("/uploadVideosandConcatenate", parameters); + return response.json(); + } export async function UploadYoutubeToServer(videoId: string): Promise[]> { const parameters = { @@ -46,5 +58,6 @@ export namespace Networking { const response = await fetch("/uploadYoutubeVideo", parameters); return response.json(); } + } \ No newline at end of file diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss index 67f96033a..d387468c6 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.scss +++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss @@ -1,13 +1,17 @@ .progressbar { touch-action: none; + vertical-align: middle; + text-align: center; + + align-items: center; position: absolute; display: flex; justify-content: flex-start; - bottom: 10px; - width: 80%; + bottom: 2px; + width: 99%; height: 30px; background-color: gray; @@ -34,11 +38,24 @@ margin: 1px; padding: 0; cursor: pointer; - transition-duration: .25s; + transition-duration: .5s; + user-select: none; + vertical-align: middle; text-align: center; } +.segment-hide { + background-color: inherit; + text-align: center; + vertical-align: middle; + user-select: none; + // /* Hide the text. */ + // text-indent: 100%; + // white-space: nowrap; + // overflow: hidden; +} + .segment:first-child { margin-left: 2px; } @@ -47,9 +64,17 @@ } .segment:hover { + background-color: white; +} + +.segment:hover, .segment-selected { margin: 0px; border: 4px solid red; - background-color: black; min-width: 10px; border-radius: 2px; } + +.segment-selected { + border: 4px solid grey; + background-color: red; +} diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx index b17d27d2e..34a771367 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -27,7 +27,7 @@ export function ProgressBar(props: ProgressBarProps) { useEffect(() => { const segmentsJSX = ordered.map((vid, i) => -
{vid.order}
); +
{vid.order}
); setSegments(segmentsJSX) }, [clicked, ordered]) @@ -98,7 +98,8 @@ export function ProgressBar(props: ProgressBarProps) { // clickedSegment.style.backgroundColor = `inherit`; const dot = document.createElement("div") - dot.classList.add("segment") + dot.classList.add("segment-selected") + dot.style.transitionDuration = '0s'; dot.style.position = 'absolute'; dot.style.zIndex = '999'; dot.style.width = `${rect.width}px`; @@ -106,8 +107,8 @@ export function ProgressBar(props: ProgressBarProps) { document.body.append(dot) const dragSegment = (event: PointerEvent): void => { // event.stopPropagation() - dot.style.left = `${event.clientX - rect.width/2}px`; - dot.style.top = `${event.clientY - rect.height/2}px`; + dot.style.left = `${event.clientX - rect.width/2}px`; + dot.style.top = `${event.clientY - rect.height/2}px`; } const updateSegmentOrder = (event: PointerEvent): void => { diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 159271223..68f3b3ad4 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -54,9 +54,9 @@ export class RecordingBox extends ViewBoxBaseComponent() { } render() { - // console.log("Proto[Is]: ", this.rootDoc.proto?.[Id]) + console.log("Proto[Is]: ", this.rootDoc.proto?.[Id]) return
- {!this.result && } + {!this.result && }
; } } diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss index 9b2f6d070..0b7b1918f 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.scss +++ b/src/client/views/nodes/RecordingBox/RecordingView.scss @@ -33,7 +33,7 @@ button { } .video-wrapper:hover .controls { - bottom: 30px; + bottom: 34.5px; transform: translateY(0%); opacity: 100%; } @@ -43,8 +43,8 @@ button { align-items: center; justify-content: space-evenly; position: absolute; - padding: 14px; - width: 100%; + // padding: 14px; + //width: 100%; max-width: 500px; // max-height: 20%; flex-wrap: wrap; @@ -56,7 +56,14 @@ button { // transform: translateY(150%); transition: all 0.3s ease-in-out; // opacity: 0%; - bottom: 30px; + bottom: 34.5px; + height: 60px; + right: 2px; + // bottom: -150px; +} + +.controls:active { + bottom: 40px; // bottom: -150px; } @@ -127,9 +134,8 @@ button { .controls-inner-container { display: flex; flex-direction: row; - justify-content: center; - width: 100%; - + align-content: center; + position: relative; } .record-button-wrapper { @@ -180,14 +186,14 @@ button { height: 100%; display: flex; flex-direction: row; - align-items: center; - position: absolute; + align-content: center; + position: relative; top: 0; bottom: 0; &.video-edit-wrapper { - right: 50% - 15; + // right: 50% - 15; .track-screen { font-weight: 200; @@ -197,10 +203,11 @@ button { &.track-screen-wrapper { - right: 50% - 30; + // right: 50% - 30; .track-screen { font-weight: 200; + color: aqua; } } diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 640e4f5f2..1e9ce22f1 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -71,13 +71,21 @@ export function RecordingView(props: IRecordingViewProps) { inputPaths.push(name) }); - console.log(videoFiles) + console.log(videoFiles) + + const data = await Networking.UploadSegmentsAndConcatenate(videoFiles) + console.log('data', data) + const result = data[0].result + if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox + props.setResult(result, trackScreen) + } else { + alert("video conversion failed"); + } // const inputListName = 'order.txt'; // fs.writeFileSync(inputListName, inputPaths.join('\n')); - // var merge = ffmpeg(); // merge.input(inputListName) // .inputOptions(['-f concat', '-safe 0']) @@ -345,16 +353,17 @@ export function RecordingView(props: IRecordingViewProps) { Track Screen - )} - - + )} - + + + + - ) } \ No newline at end of file diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 634548154..faf36c6e5 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -40,7 +40,31 @@ export function clientPathToFile(directory: Directory, filename: string) { export default class UploadManager extends ApiManager { - protected initialize(register: Registration): void { + protected initialize(register: Registration): void { + + register({ + method: Method.POST, + subscription: "/uploadVideosAndConcatenate", + secureHandler: async ({ req, res }) => { + const form = new formidable.IncomingForm(); + form.keepExtensions = true; + form.uploadDir = pathToDirectory(Directory.parsed_files); + return new Promise(resolve => { + form.parse(req, async (_err, _fields, files) => { + const result: Upload.FileResponse[] = []; + for (const key in files) { + const f = files[key]; + if (Array.isArray(f)) { + const result = await DashUploadUtils.concatenateVideos(f); + console.log('concatenated', result); + result && !(result.result instanceof Error) && _success(res, result); + } + } + resolve(); + }); + }); + } + }); register({ method: Method.POST, @@ -50,7 +74,7 @@ export default class UploadManager extends ApiManager { form.keepExtensions = true; form.uploadDir = pathToDirectory(Directory.parsed_files); return new Promise(resolve => { - form.parse(req, async (_err, _fields, files) => { + form.parse(req, async (_err, _fields, files) => { const results: Upload.FileResponse[] = []; for (const key in files) { const f = files[key]; diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 0c4f87905..6a7c8543d 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -1,9 +1,9 @@ import { green, red } from 'colors'; import { ExifImage } from 'exif'; +import * as exifr from 'exifr'; import { File } from 'formidable'; import { createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs'; import * as path from 'path'; -import * as exifr from 'exifr'; import { basename } from "path"; import * as sharp from 'sharp'; import { Stream } from 'stream'; @@ -17,7 +17,6 @@ import { resolvedServerUrl } from "./server_Initialization"; import { AcceptableMedia, Upload } from './SharedMediaTypes'; import request = require('request-promise'); import formidable = require('formidable'); -import { output } from '../../webpack.config'; const { exec } = require("child_process"); const parse = require('pdf-parse'); const ffmpeg = require("fluent-ffmpeg"); @@ -63,19 +62,31 @@ export namespace DashUploadUtils { const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr - export async function combineSegments(filePtr: File[], inputPaths: string[]): Promise { - const inputListName = 'order.txt'; - - return new Promise((resolve, reject) => { - fs.writeFileSync(inputListName, inputPaths.join('\n')); - ffmpeg(inputListName).inputOptions(['-f concat', '-safe 0']).outputOptions('-c copy').save('output.mp4') + export async function concatenateVideos(videoFiles: File[]): Promise { + // make a list of paths to create the ordered text file for ffmpeg + const filePaths = videoFiles.map(file => file.path); + // write the text file to the file system + const inputListName = 'concat.txt'; + const textFilePath = path.join(filesDirectory, "concat.txt"); + writeFile(textFilePath, filePaths.join("\n"), (err) => console.log(err)); + + + // make output file name based on timestamp + const outputFileName = `output-${Utils.GenerateGuid()}.mp4`; + await new Promise((resolve, reject) => { + ffmpeg(inputListName).inputOptions(['-f concat', '-safe 0']).outputOptions('-c copy').save(outputFileName) .on("error", reject) - .on("end", () => { - fs.unlinkSync(inputListName); - filePtr[0].path = 'output.mp4'; - resolve(filePtr[0]); - }); - }); + .on("end", resolve); + }) + + // delete concat.txt from the file system + unlinkSync(textFilePath); + + // read the output file from the file system + const outputFile = fs.readFileSync(outputFileName); + console.log('outputFile', outputFile); + // move only the output file to the videos directory + return MoveParsedFile(outputFile, Directory.videos) } export function uploadYoutube(videoId: string): Promise { @@ -237,6 +248,7 @@ export namespace DashUploadUtils { } let resolvedUrl: string; /** + * * At this point, we want to take whatever url we have and make sure it's requestable. * Anything that's hosted by some other website already is, but if the url is a local file url * (locates the file on this server machine), we have to resolve the client side url by cutting out the -- cgit v1.2.3-70-g09d2 From d570ab0d8ea08363c651caf8ee67c43c5a5823a3 Mon Sep 17 00:00:00 2001 From: Michael Foiani Date: Wed, 8 Jun 2022 02:49:39 -0400 Subject: got it to run, but not concatenating - only first video --- src/client/Network.ts | 4 +- .../views/nodes/RecordingBox/ProgressBar.scss | 1 - .../views/nodes/RecordingBox/RecordingView.tsx | 2 +- src/server/ApiManagers/UploadManager.ts | 20 +++---- src/server/DashUploadUtils.ts | 67 ++++++++++++++++++---- 5 files changed, 69 insertions(+), 25 deletions(-) (limited to 'src/client/views/nodes/RecordingBox/ProgressBar.scss') diff --git a/src/client/Network.ts b/src/client/Network.ts index 2c6d9d711..8c1f31488 100644 --- a/src/client/Network.ts +++ b/src/client/Network.ts @@ -36,8 +36,8 @@ export namespace Networking { return response.json(); } - export async function UploadSegmentsAndConcatenate(files: File | File[]): Promise[]> { - console.log(files) + export async function UploadSegmentsAndConcatenate(files: File[]): Promise[]> { + console.log("network.ts : uploading segments and concatenating", files); const formData = new FormData(); if (!Array.isArray(files) || !files.length) return []; files.forEach(file => formData.append(Utils.GenerateGuid(), file)); diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss index d387468c6..c7a190566 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.scss +++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss @@ -70,7 +70,6 @@ .segment:hover, .segment-selected { margin: 0px; border: 4px solid red; - min-width: 10px; border-radius: 2px; } diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 1e9ce22f1..aea7f56b5 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -69,7 +69,7 @@ export function RecordingView(props: IRecordingViewProps) { const { name } = videoFile; inputPaths.push(name) - }); + }) console.log(videoFiles) diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index faf36c6e5..398b007b5 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -44,22 +44,22 @@ export default class UploadManager extends ApiManager { register({ method: Method.POST, - subscription: "/uploadVideosAndConcatenate", + subscription: "/uploadVideosandConcatenate", secureHandler: async ({ req, res }) => { const form = new formidable.IncomingForm(); form.keepExtensions = true; form.uploadDir = pathToDirectory(Directory.parsed_files); return new Promise(resolve => { form.parse(req, async (_err, _fields, files) => { - const result: Upload.FileResponse[] = []; - for (const key in files) { - const f = files[key]; - if (Array.isArray(f)) { - const result = await DashUploadUtils.concatenateVideos(f); - console.log('concatenated', result); - result && !(result.result instanceof Error) && _success(res, result); - } - } + const results: Upload.FileResponse[] = []; + + // create an array of all the file paths + const filePaths: string[] = Object.keys(files).map(key => files[key].path); + console.log("uploading files", Array.isArray(filePaths)); + const result = await DashUploadUtils.concatenateVideos(filePaths); + console.log('concatenated', result); + result && !(result.result instanceof Error) && results.push(result); + _success(res, results) resolve(); }); }); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 6a7c8543d..148b0df65 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -17,6 +17,7 @@ import { resolvedServerUrl } from "./server_Initialization"; import { AcceptableMedia, Upload } from './SharedMediaTypes'; import request = require('request-promise'); import formidable = require('formidable'); +import { file } from 'jszip'; const { exec } = require("child_process"); const parse = require('pdf-parse'); const ffmpeg = require("fluent-ffmpeg"); @@ -62,19 +63,31 @@ export namespace DashUploadUtils { const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr - export async function concatenateVideos(videoFiles: File[]): Promise { + export async function concatenateVideos(filePaths: string[]): Promise { // make a list of paths to create the ordered text file for ffmpeg - const filePaths = videoFiles.map(file => file.path); + //const filePaths = videoFiles.map(file => file.path); // write the text file to the file system const inputListName = 'concat.txt'; - const textFilePath = path.join(filesDirectory, "concat.txt"); - writeFile(textFilePath, filePaths.join("\n"), (err) => console.log(err)); - + const textFilePath = path.join(filesDirectory, inputListName); + // make a list of paths to create the ordered text file for ffmpeg + const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n'); + writeFile(textFilePath, filePathsText, (err) => console.log(err)); + console.log(filePathsText) // make output file name based on timestamp - const outputFileName = `output-${Utils.GenerateGuid()}.mp4`; + const outputFileName = `output-${Utils.GenerateGuid()}.mkv`; + // create the output file path in the parsed_file directory + const outputFilePath = path.join(filesDirectory, outputFileName); + + // concatenate the videos await new Promise((resolve, reject) => { - ffmpeg(inputListName).inputOptions(['-f concat', '-safe 0']).outputOptions('-c copy').save(outputFileName) + console.log('concatenating videos'); + var merge = ffmpeg(); + merge.input(textFilePath) + .inputOptions(['-f concat', '-safe 0']) + .outputOptions('-c copy') + //.videoCodec("copy") + .save(outputFilePath) .on("error", reject) .on("end", resolve); }) @@ -83,10 +96,41 @@ export namespace DashUploadUtils { unlinkSync(textFilePath); // read the output file from the file system - const outputFile = fs.readFileSync(outputFileName); - console.log('outputFile', outputFile); - // move only the output file to the videos directory - return MoveParsedFile(outputFile, Directory.videos) + // const outputFile = fs.readFileSync(outputFilePath); + + // make a new blob object with the output file buffer + // const outputFileBlob = new Blob([outputFile.buffer], { type: 'x-matroska/mkv' }); + + // TODO: make with toJSON() + + // make a data object + const outputFileObject: formidable.File = { + size: 0, + name: outputFileName, + path: outputFilePath, + // size: outputFileBlob.size, + type: 'video/x-matroska;codecs=avc1,opus', + lastModifiedDate: new Date(), + + toJSON: () => ({ ...outputFileObject, filename: outputFilePath.replace(/.*\//, ""), mtime: null, length: 0, mime: "", toJson: () => undefined as any }) + } + + // const file = { ...outputFileObject, toJSON: () => ({ ...outputFileObject, filename: outputFilePath.replace(/.*\//, ""), mtime: null, length: 0, mime: "", toJson: () => undefined as any }) }; + + // this will convert it to mp4 and save it to the server + //return await MoveParsedFile(outputFileObject, Directory.videos); + + return await upload(outputFileObject); + + // // return only the output (first) file to the videos directory + // return { + // source: file, result: { + // accessPaths: { + // agnostic: getAccessPaths(Directory.videos, outputFileName) + // }, + // rawText: undefined + // } + // } } export function uploadYoutube(videoId: string): Promise { @@ -122,6 +166,7 @@ export namespace DashUploadUtils { } case "video": if (format.includes("x-matroska")) { + console.log("case video"); await new Promise(res => ffmpeg(file.path) .videoCodec("copy") // this will copy the data instead of reencode it .save(file.path.replace(".mkv", ".mp4")) -- cgit v1.2.3-70-g09d2 From e9ae7c0482f1c9409d905bc6474bfd06abb01743 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 9 Jun 2022 11:54:02 -0400 Subject: got the progress to be smooth, but missing final animation --- .../views/nodes/RecordingBox/ProgressBar.scss | 4 ++ .../views/nodes/RecordingBox/ProgressBar.tsx | 52 ++++++++++++++++------ 2 files changed, 42 insertions(+), 14 deletions(-) (limited to 'src/client/views/nodes/RecordingBox/ProgressBar.scss') diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss index c7a190566..3ed7cee3e 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.scss +++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss @@ -45,6 +45,10 @@ text-align: center; } +.no-transition { + transition-duration: 0s; +} + .segment-hide { background-color: inherit; text-align: center; diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx index ce231ba62..54bfbc792 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -44,35 +44,57 @@ export function ProgressBar(props: ProgressBarProps) { // this holds the index of the videoc segment to be removed const [removed, setRemoved] = useState(-1); + // const totalTime = useMemo(() => props.progress*1000 - totalRemovedTime, [props.progress, totalRemovedTime]); + // const totalTime = useMemo(() => props.videos.lastElement().endTime, [props.videos]) // const totalTime = () => props.videos.lastElement().endTime // const memoTotal = useMemo(totalTime, [props.videos]) // useEffect that updates total time based on progress - useEffect(() => { - console.log("useEffect progress", props.progress, 'totalRemovedTime', totalRemovedTime) - // setTotalTime(prev => { - // // progress is in seconds, prev is in deciseconds? - // const toAdd = props.progress * 10 - prev - // return prev + toAdd - // }) - // setTotalTime(prev => props.progress * 10) - }, [props.progress]) + // useEffect(() => { + // console.log("useEffect progress", props.progress, 'totalRemovedTime', totalRemovedTime) + // // setTotalTime(prev => { + // // // progress is in seconds, prev is in deciseconds? + // // const toAdd = props.progress * 10 - prev + // // return prev + toAdd + // // }) + // // setTotalTime(prev => props.progress * 10) + // }, [props.progress]) useEffect(() => { console.log("useEffect recording", props.recording) + segments.forEach((seg, i) => { + const htmlId = seg.props.id; + const segmentHtml = document.getElementById(htmlId); + segmentHtml?.classList.toggle('no-transition', props.recording); + console.log(segmentHtml) + }); }, [props.recording]) useEffect(() => { - console.log('rendering new segments') - const totalTime = props.progress*1000 - totalRemovedTime + console.log('useEffect dragged, ordered') + const totalTime = props.progress * 1000 - totalRemovedTime; const segmentsJSX = ordered.map((seg, i) => -
{seg.order}
); +
{seg.order}
); setSegments(segmentsJSX) - }, [dragged, ordered, props.progress]) + }, [dragged, ordered]); + + // to imporve performance, we only want to update the width, not re-render the whole thing + useEffect(() => { + // console.log('updating width on progress useEffect') + const totalTime = props.progress * 1000 - totalRemovedTime; + segments.forEach((seg, i) => { + const htmlId = seg.props.id; + const segmentHtml = document.getElementById(htmlId); + segmentHtml?.style.width = `${((ordered[i].endTime - ordered[i].startTime) / totalTime) * 100}%`; + + // console.log('updating width on progress useEffect', htmlId, segmentHtml, segmentHtml?.style.width) + }); + }, [props.progress]); + useEffect(() => { @@ -96,9 +118,11 @@ export function ProgressBar(props: ProgressBarProps) { else if (order < ordered.length) { // update the total time // setTotalTime(prevTime => prevTime - (ordered[removed].endTime - ordered[removed].startTime)); + // update total removed time setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime)); - //remove the segment from the ordered array and decrement the order of the remaining segments + + // remove the segment from the ordered array and decrement the order of the remaining segments // if they are greater than the removed segment's order const removedOrder = ordered[removed].order; setOrdered(prevOrdered => -- cgit v1.2.3-70-g09d2 From 9024621c15962f3cdfccee6fc35294b44d4cf1ee Mon Sep 17 00:00:00 2001 From: Michael Foiani Date: Thu, 9 Jun 2022 16:27:27 -0400 Subject: fix undo, add disabled ui while recording, create expanding-segment to fill the space while recording --- .../views/nodes/RecordingBox/ProgressBar.scss | 25 +++- .../views/nodes/RecordingBox/ProgressBar.tsx | 158 ++++++++++----------- .../views/nodes/RecordingBox/RecordingView.tsx | 40 +++--- 3 files changed, 122 insertions(+), 101 deletions(-) (limited to 'src/client/views/nodes/RecordingBox/ProgressBar.scss') diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss index 3ed7cee3e..1cf60b82b 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.scss +++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss @@ -45,8 +45,31 @@ text-align: center; } -.no-transition { +// .no-transition { +// transition-duration: 0s; +// } + +.segment-expanding { +border-color: red; + background-color: white; + transition-duration: 0s; + opacity: .75; + pointer-events: none; +} + +.segment-expanding:hover { + background-color: inherit; + cursor: not-allowed; +} + +.segment-disabled { + pointer-events: none; + opacity: 0.5; transition-duration: 0s; + /* Hide the text. */ + text-indent: 100%; + white-space: nowrap; + overflow: hidden; } .segment-hide { diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx index 54bfbc792..a00a4643d 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -1,8 +1,9 @@ +import { disable } from 'colors'; import { resolveTxt } from 'dns'; import { videointelligence } from 'googleapis/build/src/apis/videointelligence'; import { isInteger } from 'lodash'; import * as React from 'react'; -import { useEffect, useState, useCallback, useRef, useMemo } from "react" +import { useEffect, useState, useCallback, useRef } from "react" import "./ProgressBar.scss" import { MediaSegment } from './RecordingView'; @@ -11,7 +12,8 @@ interface ProgressBarProps { setVideos: React.Dispatch>, orderVideos: boolean, progress: number, - recording: boolean, + recording: boolean, + doUndo: boolean, } interface SegmentBox { @@ -31,68 +33,73 @@ export function ProgressBar(props: ProgressBarProps) { // array for the order of video segments const [segments, setSegments] = useState([]); - const [ordered, setOrdered] = useState([]); + const [ordered, setOrdered] = useState([]); + + const [undoStack, setUndoStack] = useState([]); const [dragged, setDragged] = useState(-1); - // total length of the video, in seconds*100 - // const [totalTime, setTotalTime] = useState(0); - // length of the time removed from the video, in seconds*100 const [totalRemovedTime, setTotalRemovedTime] = useState(0); // this holds the index of the videoc segment to be removed const [removed, setRemoved] = useState(-1); - - // const totalTime = useMemo(() => props.progress*1000 - totalRemovedTime, [props.progress, totalRemovedTime]); - - // const totalTime = useMemo(() => props.videos.lastElement().endTime, [props.videos]) - - // const totalTime = () => props.videos.lastElement().endTime - // const memoTotal = useMemo(totalTime, [props.videos]) - - // useEffect that updates total time based on progress - // useEffect(() => { - // console.log("useEffect progress", props.progress, 'totalRemovedTime', totalRemovedTime) - // // setTotalTime(prev => { - // // // progress is in seconds, prev is in deciseconds? - // // const toAdd = props.progress * 10 - prev - // // return prev + toAdd - // // }) - // // setTotalTime(prev => props.progress * 10) - // }, [props.progress]) + + + // abstracted for other uses - brings back the most recently deleted segment + const handleUndo = () => { + // get the last element from the undo if it exists + if (undoStack.length === 0) return; + // get and remove the last element from the undo stack + const last = undoStack.lastElement(); + setUndoStack(prevUndo => prevUndo.slice(0, -1)); + + // update the removed time and place element back into ordered + setTotalRemovedTime(prevRemoved => prevRemoved - (last.endTime - last.startTime)); + setOrdered(prevOrdered => [...prevOrdered, last]); + } + useEffect(() => handleUndo(), [props.doUndo]) useEffect(() => { - console.log("useEffect recording", props.recording) - segments.forEach((seg, i) => { - const htmlId = seg.props.id; - const segmentHtml = document.getElementById(htmlId); - segmentHtml?.classList.toggle('no-transition', props.recording); - console.log(segmentHtml) - }); + console.log("useEffect recording", props.recording) + // get segments segment's html using it's id -> make them appeared disabled (or enabled) + segments.forEach((seg) => document.getElementById(seg.props.id)?.classList.toggle('segment-disabled', props.recording)); + + if (props.recording) + setSegments(prevSegments => [...prevSegments,
{props.videos.length + 1}
]); }, [props.recording]) useEffect(() => { - console.log('useEffect dragged, ordered') const totalTime = props.progress * 1000 - totalRemovedTime; const segmentsJSX = ordered.map((seg, i) => -
{seg.order}
); +
{seg.order + 1}
); setSegments(segmentsJSX) }, [dragged, ordered]); // to imporve performance, we only want to update the width, not re-render the whole thing - useEffect(() => { - // console.log('updating width on progress useEffect') - const totalTime = props.progress * 1000 - totalRemovedTime; - segments.forEach((seg, i) => { - const htmlId = seg.props.id; - const segmentHtml = document.getElementById(htmlId); - segmentHtml?.style.width = `${((ordered[i].endTime - ordered[i].startTime) / totalTime) * 100}%`; + useEffect(() => { + if (!props.recording) return + const totalTime = props.progress * 1000 - totalRemovedTime; + let remainingTime = totalTime; + segments.forEach((seg, i) => { + // for the last segment, we need to set that directly + if (i === segments.length - 1) return; + // update remaining time + remainingTime -= (ordered[i].endTime - ordered[i].startTime); + + // update the width for this segment + const htmlId = seg.props.id; + const segmentHtml = document.getElementById(htmlId); + if (segmentHtml) segmentHtml.style.width = `${((ordered[i].endTime - ordered[i].startTime) / totalTime) * 100}%`; + }); - // console.log('updating width on progress useEffect', htmlId, segmentHtml, segmentHtml?.style.width) - }); + // update the width of the expanding segment using the remaining time + const segExapandHtml = document.getElementById('segment-expanding'); + if (segExapandHtml) + segExapandHtml.style.width = ordered.length === 0 ? '100%' : `${(remainingTime / totalTime) * 100}%`; + }, [props.progress]); @@ -102,52 +109,36 @@ export function ProgressBar(props: ProgressBarProps) { // in this case, do nothing. if (props.orderVideos) return; - const order = props.videos.length - // in this case, a new video is added - if (order && order > ordered.length) { + const order = props.videos.length - 1; + // in this case, a new video is added -> push it onto ordered + if (order >= ordered.length) { const { endTime, startTime } = props.videos.lastElement(); - // update total time - // setTotalTime(prevTime => prevTime + (endTime - startTime)); - // add the new segment to the ordered array setOrdered(prevOrdered => { return [...prevOrdered, { endTime, startTime , order }]; }); } - // in this case, a segment is removed + // in this case, a video is removed else if (order < ordered.length) { - // update the total time - // setTotalTime(prevTime => prevTime - (ordered[removed].endTime - ordered[removed].startTime)); - - // update total removed time - setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime)); - - // remove the segment from the ordered array and decrement the order of the remaining segments - // if they are greater than the removed segment's order - const removedOrder = ordered[removed].order; - setOrdered(prevOrdered => - prevOrdered.reduce((acc, seg, i) => { - (i !== removed) && acc.push({ ...seg, order: seg.order > removedOrder ? seg.order - 1 : seg.order }); - return acc; - },[] as SegmentBox[]) - ); - // reset to default/nullish state - setRemoved(-1); + console.error('warning: video removed from parent'); } }, [props.videos]); useEffect(() => { - props.setVideos(vids => ordered.map((seg) => vids[seg.order - 1])); + props.setVideos(vids => ordered.map((seg) => vids[seg.order])); }, [props.orderVideos]); useEffect(() => { if (removed === -1) return; - - console.log('removed', removed) - - // update the videos array -> this will fire the useEffect above, where we update the orders & totalTime - const index = ordered[removed].order - 1; - props.setVideos(vids => vids.filter((vid, i) => i !== index)); + // update total removed time + setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime)); + + // put the element on the undo stack + setUndoStack(prevUndo => [...prevUndo, ordered[removed]]); + // remove the segment from the array + setOrdered(prevOrdered => prevOrdered.filter((seg, i) => i !== removed)); + // reset to default/nullish state + setRemoved(-1); }, [removed]); const updateLastHover = (segId: number): CurrentHover | null => { @@ -163,7 +154,10 @@ export function ProgressBar(props: ProgressBarProps) { const onPointerDown = (e: React.PointerEvent) => { // don't move the videobox element - e.stopPropagation() + e.stopPropagation() + + // if recording, do nothing + if (props.recording) return; // get the segment the user clicked on to be dragged const clickedSegment = e.target as HTMLDivElement & EventTarget @@ -177,10 +171,15 @@ export function ProgressBar(props: ProgressBarProps) { // TODO: think of a way to accomodate touch -> maybe if poiuntermove isn't fired after x secs? if (e.shiftKey) { const segId = parseInt(clickedSegment.id.split('-')[1]); - console.log('removing segment', segId) setRemoved(segId); return } + + if (e.ctrlKey) { + handleUndo(); + return; + // todo: implement undo stack + } const ptrId = e.pointerId; progressBar.setPointerCapture(ptrId) @@ -209,7 +208,6 @@ export function ProgressBar(props: ProgressBarProps) { event.preventDefault(); // this fixes a bug where pointerup doesn't fire while cursor is upped while being dragged - console.log('update cursor', progressBar.hasPointerCapture(ptrId), ptrId) if (!progressBar.hasPointerCapture(ptrId)) { placeSegmentandCleanup(); return; @@ -269,9 +267,6 @@ export function ProgressBar(props: ProgressBarProps) { dot.draggable = false; document.body.append(dot) } - const cleanupDetachSegment = (dot: HTMLDivElement) => { - dot.remove() - } const followCursor = (event: PointerEvent, dot: HTMLDivElement, rect: DOMRect): void => { // event.stopPropagation() // const { width, height } = dot.getBoundingClientRect() @@ -283,11 +278,6 @@ export function ProgressBar(props: ProgressBarProps) { return (
- {/*
*/} {segments}
) diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 40117c41c..e131420c3 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -31,7 +31,9 @@ export function RecordingView(props: IRecordingViewProps) { const recordingTimerRef = useRef(0); const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second const [playing, setPlaying] = useState(false); - const [progress, setProgress] = useState(0); + const [progress, setProgress] = useState(0); + + const [doUndo, setDoUndo] = useState(false); const [videos, setVideos] = useState([]); const [orderVideos, setOrderVideos] = useState(false); @@ -225,11 +227,15 @@ export function RecordingView(props: IRecordingViewProps) { } } - const clearPrevious = () => { - const numVideos = videos.length - setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime) - setVideoProgressHelper(numVideos == 1 ? 0 : videos[numVideos - 2].endTime) - setVideos(videos.filter((_, idx) => idx !== numVideos - 1)); + const undoPrevious = (e: React.MouseEvent) => { + e.stopPropagation(); + console.log('undo previous', doUndo) + setDoUndo(prev => !prev); + // const numVideos = videos.length + // setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime) + // setVideoProgressHelper(numVideos == 1 ? 0 : videos[numVideos - 2].endTime) + // setVideos(videos.filter((_, idx) => idx !== numVideos - 1)); + } const handleOnTimeUpdate = () => { @@ -247,7 +253,8 @@ export function RecordingView(props: IRecordingViewProps) { return toTwoDigit(minutes) + " : " + toTwoDigit(seconds); } - return ( + // TODO: have the undo button only appear if there is something to undo + return (