From 8e0b274fa0fe0a1e8cfa0d3f9d147c3e6a639d38 Mon Sep 17 00:00:00 2001 From: Michael Foiani Date: Thu, 2 Jun 2022 11:27:17 -0400 Subject: ffmpeg code added, but there is an error with sharedArrayBuffer --- .../views/nodes/RecordingBox/RecordingView.tsx | 82 ++++++++++++++++------ 1 file changed, 59 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 87716e9cc..5cfb03414 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -7,12 +7,13 @@ import { FaCheckCircle } from 'react-icons/fa'; import { IconContext } from "react-icons"; import { Networking } from '../../../Network'; import { Upload } from '../../../../server/SharedMediaTypes'; +import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg'; import { RecordingApi } from '../../../util/RecordingApi'; interface MediaSegment { - videoChunks: any[], - endTime: number + videoChunks: any[], + endTime: number } interface IRecordingViewProps { @@ -31,7 +32,8 @@ export function RecordingView(props: IRecordingViewProps) { const [playing, setPlaying] = useState(false); const [progress, setProgress] = useState(0); - const [videos, setVideos] = useState([]); + const [videos, setVideos] = useState([]); + // const [order, setOrder] = useState([]); const videoRecorder = useRef(null); const videoElementRef = useRef(null); @@ -52,28 +54,52 @@ export function RecordingView(props: IRecordingViewProps) { } } - useEffect(() => { + useEffect(() => { + + console.log('in finish useEffect') - if (finished) { + if (finished) { + // load ffmpe + (async () => { + console.log('crossOriginIsolated', crossOriginIsolated) props.setDuration(recordingTimer * 100) - let allVideoChunks: any = [] - videos.forEach((vid) => { - console.log(vid.videoChunks) - allVideoChunks = allVideoChunks.concat(vid.videoChunks) - }) - - const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() }); - - Networking.UploadFilesToServer(videoFile) - .then((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"); - } - }) + console.log('Loading ffmpeg-core.js'); + const ffmpeg = createFFmpeg({ log: true }); + await ffmpeg.load(); + console.log('ffmpeg-core.js loaded'); + + let allVideoChunks: any = []; + const inputPaths: string[] = []; + // write each segment into it's indexed file + videos.forEach(async (vid, i) => { + const vidName = `segvideo${i}.mkv` + inputPaths.push(vidName) + const videoFile = new File(vid.videoChunks, vidName, { type: allVideoChunks[0].type, lastModified: Date.now() }); + ffmpeg.FS('writeFile', vidName, await fetchFile(videoFile)); + }) + ffmpeg.FS('writeFile', 'order.txt', inputPaths.join('\n')); + + console.log('concat') + await ffmpeg.run('-f', 'concat', '-safe', '0', '-i', 'order.txt', 'ouput.mp4'); + + const { buffer } = ffmpeg.FS('readFile', 'output.mp4'); + const concatVideo = new File([buffer], 'concat.mp4', { type: "video/mp4" }); + + const data = await Networking.UploadFilesToServer(concatVideo) + const result = data[0].result + if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox + props.setResult(result, trackScreen) + } else { + alert("video conversion failed"); + } + + // delete all files in MEMFS + inputPaths.forEach(path => ffmpeg.FS('unlink', path)); + ffmpeg.FS('unlink', 'order.txt'); + ffmpeg.FS('unlink', 'output.mp4'); + })(); + } @@ -84,7 +110,7 @@ export function RecordingView(props: IRecordingViewProps) { if (!navigator.mediaDevices) { console.log('This browser does not support getUserMedia.') } - console.log('This device has the correct media devices.') + console.log('This device has the correct media devices.') }, []) useEffect(() => { @@ -222,6 +248,16 @@ export function RecordingView(props: IRecordingViewProps) { const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000); return toTwoDigit(minutes) + " : " + toTwoDigit(seconds); } + + const doTranscode = async () => { + + // console.log('Start transcoding'); + // ffmpeg.FS('writeFile', 'test.avi', await fetchFile('/flame.avi')); + // await ffmpeg.run('-i', 'test.avi', 'test.mp4'); + // console.log('Complete transcoding'); + // const data = ffmpeg.FS('readFile', 'test.mp4'); + // console.log(URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }))); + }; return (
-- cgit v1.2.3-70-g09d2 From fee544d218768cb02ef3cfc0df722ac563fb78d2 Mon Sep 17 00:00:00 2001 From: Michael Foiani Date: Thu, 2 Jun 2022 14:19:04 -0400 Subject: switch to fluent ffmpeg --- .../views/nodes/RecordingBox/RecordingView.tsx | 88 +++++++++++++--------- 1 file changed, 53 insertions(+), 35 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 5cfb03414..994111bd4 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -7,7 +7,7 @@ import { FaCheckCircle } from 'react-icons/fa'; import { IconContext } from "react-icons"; import { Networking } from '../../../Network'; import { Upload } from '../../../../server/SharedMediaTypes'; -import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg'; +const ffmpeg = require("fluent-ffmpeg"); import { RecordingApi } from '../../../util/RecordingApi'; @@ -59,45 +59,63 @@ export function RecordingView(props: IRecordingViewProps) { console.log('in finish useEffect') if (finished) { - // load ffmpe (async () => { - console.log('crossOriginIsolated', crossOriginIsolated) - props.setDuration(recordingTimer * 100) + const inputPaths: string[] = []; + videos.forEach(async (vid, i) => { + const videoFile = new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }); + const { name } = videoFile; + ffmpeg(name) + inputPaths.push(name) + }); - console.log('Loading ffmpeg-core.js'); - const ffmpeg = createFFmpeg({ log: true }); - await ffmpeg.load(); - console.log('ffmpeg-core.js loaded'); - let allVideoChunks: any = []; - const inputPaths: string[] = []; - // write each segment into it's indexed file - videos.forEach(async (vid, i) => { - const vidName = `segvideo${i}.mkv` - inputPaths.push(vidName) - const videoFile = new File(vid.videoChunks, vidName, { type: allVideoChunks[0].type, lastModified: Date.now() }); - ffmpeg.FS('writeFile', vidName, await fetchFile(videoFile)); - }) - ffmpeg.FS('writeFile', 'order.txt', inputPaths.join('\n')); - console.log('concat') - await ffmpeg.run('-f', 'concat', '-safe', '0', '-i', 'order.txt', 'ouput.mp4'); - - const { buffer } = ffmpeg.FS('readFile', 'output.mp4'); - const concatVideo = new File([buffer], 'concat.mp4', { type: "video/mp4" }); - - const data = await Networking.UploadFilesToServer(concatVideo) - const result = data[0].result - if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox - props.setResult(result, trackScreen) - } else { - alert("video conversion failed"); - } + + if (format.includes("x-matroska")) { + await new Promise(res => ffmpeg(file.path) + .videoCodec("copy") // this will copy the data instead of reencode it + .save(file.path.replace(".mkv", ".mp4")) + .on('end', res)); + file.path = file.path.replace(".mkv", ".mp4"); + format = ".mp4"; + } + // console.log('crossOriginIsolated', crossOriginIsolated) + // props.setDuration(recordingTimer * 100) + + // console.log('Loading ffmpeg-core.js'); + // const ffmpeg = createFFmpeg({ log: true }); + // await ffmpeg.load(); + // console.log('ffmpeg-core.js loaded'); + + // let allVideoChunks: any = []; + // const inputPaths: string[] = []; + // // write each segment into it's indexed file + // videos.forEach(async (vid, i) => { + // const vidName = `segvideo${i}.mkv` + // inputPaths.push(vidName) + // const videoFile = new File(vid.videoChunks, vidName, { type: allVideoChunks[0].type, lastModified: Date.now() }); + // ffmpeg.FS('writeFile', vidName, await fetchFile(videoFile)); + // // }) + // ffmpeg.FS('writeFile', 'order.txt', inputPaths.join('\n')); + + // console.log('concat') + // await ffmpeg.run('-f', 'concat', '-safe', '0', '-i', 'order.txt', 'ouput.mp4'); + + // const { buffer } = ffmpeg.FS('readFile', 'output.mp4'); + // const concatVideo = new File([buffer], 'concat.mp4', { type: "video/mp4" }); + + // const data = await Networking.UploadFilesToServer(concatVideo) + // const result = data[0].result + // if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox + // props.setResult(result, trackScreen) + // } else { + // alert("video conversion failed"); + // } - // delete all files in MEMFS - inputPaths.forEach(path => ffmpeg.FS('unlink', path)); - ffmpeg.FS('unlink', 'order.txt'); - ffmpeg.FS('unlink', 'output.mp4'); + // // delete all files in MEMFS + // inputPaths.forEach(path => ffmpeg.FS('unlink', path)); + // ffmpeg.FS('unlink', 'order.txt'); + // ffmpeg.FS('unlink', 'output.mp4'); })(); } -- cgit v1.2.3-70-g09d2 From a08eff4abbcdb1ae78b4b27f0171c4046486219c Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 3 Jun 2022 12:25:31 -0400 Subject: ffmpeg testing --- .../views/nodes/RecordingBox/RecordingView.tsx | 50 ++++++++++++++++------ src/server/DashUploadUtils.ts | 17 ++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 994111bd4..3fd062483 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -7,9 +7,9 @@ import { FaCheckCircle } from 'react-icons/fa'; import { IconContext } from "react-icons"; import { Networking } from '../../../Network'; import { Upload } from '../../../../server/SharedMediaTypes'; -const ffmpeg = require("fluent-ffmpeg"); import { RecordingApi } from '../../../util/RecordingApi'; +import { DashUploadUtils } from '../../../../server/DashUploadUtils'; interface MediaSegment { videoChunks: any[], @@ -61,24 +61,50 @@ export function RecordingView(props: IRecordingViewProps) { if (finished) { (async () => { const inputPaths: string[] = []; + const videoFiles: File[] = [] videos.forEach(async (vid, i) => { const videoFile = new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }); + videoFiles.push(videoFile); + const { name } = videoFile; - ffmpeg(name) inputPaths.push(name) }); - - + + + + // const inputListName = 'order.txt'; + // fs.writeFileSync(inputListName, inputPaths.join('\n')); + + // var merge = ffmpeg(); + // merge.input(inputListName) + // .inputOptions(['-f concat', '-safe 0']) + // .outputOptions('-c copy') + // .save('output.mp4') + + // fs.unlinkSync(inputListName); + + const combined = await DashUploadUtils.combineSegments(videoFiles, inputPaths) + console.log('combined', combined) + + // const outputFile = new File(['output.mp4'], 'output.mp4', { type: 'video/mp4', lastModified: Date.now() }); + + const data = await Networking.UploadFilesToServer(combined) + const result = data[0].result + if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox + props.setResult(result, trackScreen) + } else { + alert("video conversion failed"); + } - if (format.includes("x-matroska")) { - await new Promise(res => ffmpeg(file.path) - .videoCodec("copy") // this will copy the data instead of reencode it - .save(file.path.replace(".mkv", ".mp4")) - .on('end', res)); - file.path = file.path.replace(".mkv", ".mp4"); - format = ".mp4"; - } + // if (format.includes("x-matroska")) { + // await new Promise(res => ffmpeg(file.path) + // .videoCodec("copy") // this will copy the data instead of reencode it + // .save(file.path.replace(".mkv", ".mp4")) + // .on('end', res)); + // file.path = file.path.replace(".mkv", ".mp4"); + // format = ".mp4"; + // } // console.log('crossOriginIsolated', crossOriginIsolated) // props.setDuration(recordingTimer * 100) diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 552ab57a5..0c4f87905 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -17,9 +17,11 @@ 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"); +const fs = require("fs"); const requestImageSize = require("../client/util/request-image-size"); export enum SizeSuffix { @@ -60,6 +62,21 @@ export namespace DashUploadUtils { const type = "content-type"; 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') + .on("error", reject) + .on("end", () => { + fs.unlinkSync(inputListName); + filePtr[0].path = 'output.mp4'; + resolve(filePtr[0]); + }); + }); + } export function uploadYoutube(videoId: string): Promise { console.log("UPLOAD " + videoId); -- cgit v1.2.3-70-g09d2 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 ++++++++- .../views/nodes/RecordingBox/ProgressBar.tsx | 80 ++++++++++++++++++---- .../views/nodes/RecordingBox/RecordingView.tsx | 34 ++++----- 3 files changed, 116 insertions(+), 31 deletions(-) (limited to 'src') 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; +} diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx index 82d5e1f04..3e71239ce 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -1,14 +1,26 @@ +import { isInteger } from 'lodash'; import * as React from 'react'; -import { useEffect } from "react" +import { useEffect, useState, useCallback, useRef } from "react" import "./ProgressBar.scss" +import { MediaSegment } from './RecordingView'; interface ProgressBarProps { - progress: number, - marks: number[], + videos: MediaSegment[], } export function ProgressBar(props: ProgressBarProps) { + const progressBarRef = useRef(null) + + // array for the order of video segments + const [segments, setSegments] = useState([]); + + // const totalTime = useMemo(() => props.videos.lastElement().endTime, [props.videos]) + + const totalTime = () => props.videos.lastElement().endTime + + + // const handleClick = (e: React.MouseEvent) => { // let progressbar = document.getElementById('progressbar')! // let bounds = progressbar!.getBoundingClientRect(); @@ -26,20 +38,62 @@ export function ProgressBar(props: ProgressBarProps) { // } // } // } + + const onPointerDown = (e: React.PointerEvent) =>{ + // e.persist() + // console.log('event', e) + // don't move the videobox element + e.stopPropagation() + + 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() + + 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`; + } + + 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.removeEventListener('pointermove', updateSegmentOrder), { once: true } + }) + } return ( -
-
+ {/*
- {props.marks.map((mark) => { - return
- })} -
+ >
*/} + {props.videos.map((vid, i) =>
)} +
) } \ 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 3fd062483..2420c126f 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -11,9 +11,10 @@ import { Upload } from '../../../../server/SharedMediaTypes'; import { RecordingApi } from '../../../util/RecordingApi'; import { DashUploadUtils } from '../../../../server/DashUploadUtils'; -interface MediaSegment { +export interface MediaSegment { videoChunks: any[], - endTime: number + endTime: number, + startTime: number } interface IRecordingViewProps { @@ -70,6 +71,8 @@ export function RecordingView(props: IRecordingViewProps) { inputPaths.push(name) }); + console.log(videoFiles) + // const inputListName = 'order.txt'; @@ -83,18 +86,18 @@ export function RecordingView(props: IRecordingViewProps) { // fs.unlinkSync(inputListName); - const combined = await DashUploadUtils.combineSegments(videoFiles, inputPaths) - console.log('combined', combined) + // const combined = await DashUploadUtils.combineSegments(videoFiles, inputPaths) + // console.log('combined', combined) // const outputFile = new File(['output.mp4'], 'output.mp4', { type: 'video/mp4', lastModified: Date.now() }); - const data = await Networking.UploadFilesToServer(combined) - 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 data = await Networking.UploadFilesToServer(combined) + // const result = data[0].result + // if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox + // props.setResult(result, trackScreen) + // } else { + // alert("video conversion failed"); + // } // if (format.includes("x-matroska")) { @@ -215,7 +218,7 @@ export function RecordingView(props: IRecordingViewProps) { // if we have a last portion if (videoChunks.length > 1) { // append the current portion to the video pieces - setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }]) + setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current, startTime: videos?.lastElement()?.endTime || 0 }]) } // reset the temporary chunks @@ -228,7 +231,7 @@ export function RecordingView(props: IRecordingViewProps) { // recording paused videoRecorder.current.onpause = (event: any) => { // append the current portion to the video pieces - setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }]) + setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current, startTime: videos?.lastElement()?.endTime || 0 }]) // reset the temporary chunks videoChunks = [] @@ -346,9 +349,8 @@ export function RecordingView(props: IRecordingViewProps) { - elt.endTime / MAXTIME * 100)} + -- 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') 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') 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') 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 f7966035392675e8670914f1df81cda9084c2cbe Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 8 Jun 2022 14:24:09 -0400 Subject: Refactor code, but there is a bug. May go back. --- .../views/nodes/RecordingBox/ProgressBar.tsx | 147 +++++++++++++-------- 1 file changed, 91 insertions(+), 56 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx index 34a771367..fbb5e8a8b 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -2,7 +2,7 @@ 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" +import { useEffect, useState, useCallback, useRef, useMemo } from "react" import "./ProgressBar.scss" import { MediaSegment } from './RecordingView'; @@ -11,33 +11,52 @@ interface ProgressBarProps { setVideos, } +interface SegmentBox { + endTime: number, + startTime: number, + order: number, +} +interface CurrentHover { + index: number, + minX: number, + maxX: number +} + export function ProgressBar(props: ProgressBarProps) { const progressBarRef = useRef(null) // array for the order of video segments const [segments, setSegments] = useState([]); - const [ordered, setOrdered] = useState([]); + const [ordered, setOrdered] = useState([]); - const [clicked, setClicked] = useState(-1); + const [dragged, setDragged] = useState(-1); // const totalTime = useMemo(() => props.videos.lastElement().endTime, [props.videos]) const totalTime = () => props.videos.lastElement().endTime + // const memoTotal = useMemo(totalTime, [props.videos]) useEffect(() => { - const segmentsJSX = ordered.map((vid, i) => -
{vid.order}
); + const segmentsJSX = ordered.map((seg, i) => +
{seg.order}
); setSegments(segmentsJSX) - }, [clicked, ordered]) + }, [dragged, ordered]) useEffect(() => { - setOrdered(props.videos.map((vid, order) => { - //const { endTime, startTime } = vid - // TODO: not tranfer the blobs around - return { ...vid, order }; - })) + const order = props.videos.length + if (order) { + const { endTime, startTime } = props.videos.lastElement(); + setOrdered(prevOrdered => { + return [...prevOrdered, { endTime, startTime , order }]; + }); + } + // }props.videos.map((vid, order) => { + // //const { endTime, startTime } = vid + // // TODO: not tranfer the blobs around + // return { ...vid, order }; + // })) }, [props.videos]); @@ -60,12 +79,10 @@ 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 updateLastHover = (segId: number): CurrentHover | null => { + // get the segId of the segment that will become the new bounding area const rect = progressBarRef.current?.children[segId].getBoundingClientRect() - console.log(rect) + if (rect == null) return null return { index: segId, minX: rect.x, @@ -73,51 +90,46 @@ export function ProgressBar(props: ProgressBarProps) { } } - const onPointerDown = (e: React.PointerEvent) =>{ - // e.persist() - // console.log('event', e) + const onPointerDown = (e: React.PointerEvent) => { + console.log('pointer down') // don't move the videobox element e.stopPropagation() - const progressBar = progressBarRef.current - if (progressBar == null) return - console.log('progressbar', progressBar) - - // progressBar.addEventListener('pointermove', updateSegmentOrder) - - + // get the segment the user clicked on to be dragged const clickedSegment = e.target as HTMLDivElement & EventTarget + // get the profess bar ro add event listeners + // don't do anything if null + const progressBar = progressBarRef.current + if (progressBar == null || clickedSegment.id === progressBar.id) return + progressBar.setPointerCapture(e.pointerId) + const rect = clickedSegment.getBoundingClientRect() - setClicked(parseInt(clickedSegment.id.split('-')[1])) - let lastHover = { - index: parseInt(clickedSegment.id.split('-')[1]), + // id for segment is like 'segment-1' or 'segment-10', + // so this works to get the id + const segId = parseInt(clickedSegment.id.split('-')[1]) + // set the selected segment to be the one dragged + setDragged(segId) + + // this is the logic for storing the lower X bound and upper X bound + // to know whether a swap is needed between two segments + let lastHover: CurrentHover = { + index: segId, minX: rect.x, maxX: rect.x + rect.width, } - // clickedSegment.style.backgroundColor = `inherit`; - const dot = document.createElement("div") - dot.classList.add("segment-selected") - dot.style.transitionDuration = '0s'; - 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() - dot.style.left = `${event.clientX - rect.width/2}px`; - dot.style.top = `${event.clientY - rect.height/2}px`; - } + // create the div element that tracks the cursor + const detchedSegment = document.createElement("div") + initDeatchSegment(detchedSegment, rect); - const updateSegmentOrder = (event: PointerEvent): void => { + function updateSegmentOrder(event: PointerEvent): void{ event.stopPropagation(); - dragSegment(event) + followCursor(event, detchedSegment, rect) const oldIndex = lastHover.index - const swapSegments = (newIndex) => { + const swapSegments = (newIndex: number) => { if (newIndex == null) return setOrdered(prevOrdered => { const cpy = [...prevOrdered] @@ -125,32 +137,55 @@ export function ProgressBar(props: ProgressBarProps) { cpy[newIndex] = prevOrdered[oldIndex] return cpy }) - setClicked(newIndex) + setDragged(newIndex) } const curX = event.clientX; if (curX < lastHover.minX && lastHover.index > 0) { swapSegments(lastHover.index - 1) - lastHover = updateLastHover(lastHover.index - 1) + lastHover = updateLastHover(lastHover.index - 1) ?? lastHover } else if (curX > lastHover.maxX && lastHover.index < segments.length - 1) { swapSegments(lastHover.index + 1) - lastHover = updateLastHover(lastHover.index + 1) + lastHover = updateLastHover(lastHover.index + 1) ?? lastHover } } - progressBar.setPointerCapture(e.pointerId) progressBar.addEventListener('pointermove', updateSegmentOrder) - progressBar.addEventListener('pointerup', (event) => { - progressBar.removeEventListener('pointermove', updateSegmentOrder), { once: true } + progressBar.addEventListener('pointerup', () => { + 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(); - }) + console.log('removed dot') + detchedSegment.remove() + setDragged(-1); + }, { once: true }) + } + + + const initDeatchSegment = (dot: HTMLDivElement, rect: DOMRect) => { + dot.classList.add("segment-selected") + dot.style.transitionDuration = '0s'; + dot.style.position = 'absolute'; + dot.style.zIndex = '999'; + dot.style.width = `${rect.width}px`; + dot.style.height = `${rect.height}px`; + dot.style.left = `${rect.x}px`; + dot.style.top = `${rect.y}px`; + 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() + const { width, height } = rect; + dot.style.left = `${event.clientX - width/2}px`; + dot.style.top = `${event.clientY - height/2}px`; + } + return (
-- cgit v1.2.3-70-g09d2 From 2b416e0b836af692e0ce7f121e25e167919f3681 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 8 Jun 2022 15:15:47 -0400 Subject: found a hacky way to stop the bug with the mouse. touch devices seems to be working in a weird way tho --- .../views/nodes/RecordingBox/ProgressBar.tsx | 68 +++++++++++++--------- 1 file changed, 41 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx index fbb5e8a8b..3314da355 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -8,7 +8,7 @@ import { MediaSegment } from './RecordingView'; interface ProgressBarProps { videos: MediaSegment[], - setVideos, + setVideos: React.Dispatch>, } interface SegmentBox { @@ -102,7 +102,8 @@ export function ProgressBar(props: ProgressBarProps) { // don't do anything if null const progressBar = progressBarRef.current if (progressBar == null || clickedSegment.id === progressBar.id) return - progressBar.setPointerCapture(e.pointerId) + const ptrId = e.pointerId; + progressBar.setPointerCapture(ptrId) const rect = clickedSegment.getBoundingClientRect() // id for segment is like 'segment-1' or 'segment-10', @@ -123,46 +124,58 @@ export function ProgressBar(props: ProgressBarProps) { const detchedSegment = document.createElement("div") initDeatchSegment(detchedSegment, rect); - function updateSegmentOrder(event: PointerEvent): void{ + const updateSegmentOrder = (event: PointerEvent): void => { event.stopPropagation(); + event.preventDefault(); - followCursor(event, detchedSegment, rect) - - const oldIndex = lastHover.index - const swapSegments = (newIndex: number) => { - if (newIndex == null) return - setOrdered(prevOrdered => { - const cpy = [...prevOrdered] - cpy[oldIndex] = cpy[newIndex] - cpy[newIndex] = prevOrdered[oldIndex] - return cpy - }) - setDragged(newIndex) + // 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; } + followCursor(event, detchedSegment, rect) + const curX = event.clientX; if (curX < lastHover.minX && lastHover.index > 0) { - swapSegments(lastHover.index - 1) + swapSegments(lastHover.index, lastHover.index - 1) lastHover = updateLastHover(lastHover.index - 1) ?? lastHover } else if (curX > lastHover.maxX && lastHover.index < segments.length - 1) { - swapSegments(lastHover.index + 1) + swapSegments(lastHover.index, lastHover.index + 1) lastHover = updateLastHover(lastHover.index + 1) ?? lastHover } - } - - progressBar.addEventListener('pointermove', updateSegmentOrder) - progressBar.addEventListener('pointerup', () => { - progressBar.removeEventListener('pointermove', updateSegmentOrder), { once : true } - // clickedSegment.style.position = clickedSegment.style.zIndex = 'inherit'; - // progressBar.removeEventListener('pointermove', updateSegmentOrder), { once: true } - // clickedSegment.style.backgroundColor = 'red'; - console.log('removed dot') + } + + const placeSegmentandCleanup = (event?: PointerEvent): void => { + event?.stopPropagation(); + event?.preventDefault(); + // remove the update event listener for pointermove + progressBar.removeEventListener('pointermove', updateSegmentOrder), { once: true } + // remove the floating segment from the DOM detchedSegment.remove() + // dragged is -1 is equiv to nothing being dragged, so the normal state + // so this will place the segment in it's location and update the segment bar setDragged(-1); - }, { once: true }) + } + + + progressBar.addEventListener('pointermove', updateSegmentOrder) + progressBar.addEventListener('pointerup', placeSegmentandCleanup, { once: true }) } + const swapSegments = (oldIndex: number, newIndex: number) => { + if (newIndex == null) return + setOrdered(prevOrdered => { + const cpy = [...prevOrdered] + cpy[oldIndex] = cpy[newIndex] + cpy[newIndex] = prevOrdered[oldIndex] + return cpy + }) + setDragged(newIndex) + } + const initDeatchSegment = (dot: HTMLDivElement, rect: DOMRect) => { dot.classList.add("segment-selected") @@ -173,6 +186,7 @@ export function ProgressBar(props: ProgressBarProps) { dot.style.height = `${rect.height}px`; dot.style.left = `${rect.x}px`; dot.style.top = `${rect.y}px`; + dot.draggable = false; document.body.append(dot) } const cleanupDetachSegment = (dot: HTMLDivElement) => { -- cgit v1.2.3-70-g09d2 From bc6aa7b8e7c9e43901f500d58acb0ebb6450b0a5 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 8 Jun 2022 15:38:40 -0400 Subject: got basic ordering to work for the videos that go to the server --- .../views/nodes/RecordingBox/ProgressBar.tsx | 7 +++++- .../views/nodes/RecordingBox/RecordingView.tsx | 25 ++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx index 3314da355..a91656cbc 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -9,6 +9,7 @@ import { MediaSegment } from './RecordingView'; interface ProgressBarProps { videos: MediaSegment[], setVideos: React.Dispatch>, + orderVideos: boolean, } interface SegmentBox { @@ -46,7 +47,7 @@ export function ProgressBar(props: ProgressBarProps) { useEffect(() => { const order = props.videos.length - if (order) { + if (order && !props.orderVideos) { const { endTime, startTime } = props.videos.lastElement(); setOrdered(prevOrdered => { return [...prevOrdered, { endTime, startTime , order }]; @@ -59,6 +60,10 @@ export function ProgressBar(props: ProgressBarProps) { // })) }, [props.videos]); + useEffect(() => { + props.setVideos(vids => ordered.map((seg) => vids[seg.order - 1])); + }, [props.orderVideos]); + // const handleClick = (e: React.MouseEvent) => { diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index aea7f56b5..a5c2dc85c 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -34,7 +34,7 @@ export function RecordingView(props: IRecordingViewProps) { const [progress, setProgress] = useState(0); const [videos, setVideos] = useState([]); - // const [order, setOrder] = useState([]); + const [orderVideos, setOrderVideos] = useState(false); const videoRecorder = useRef(null); const videoElementRef = useRef(null); @@ -57,13 +57,14 @@ export function RecordingView(props: IRecordingViewProps) { useEffect(() => { - console.log('in finish useEffect') + console.log('in videos useEffect') if (finished) { (async () => { const inputPaths: string[] = []; const videoFiles: File[] = [] - videos.forEach(async (vid, i) => { + videos.forEach(async (vid, i) => { + console.log(vid) const videoFile = new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }); videoFiles.push(videoFile); @@ -71,7 +72,7 @@ export function RecordingView(props: IRecordingViewProps) { inputPaths.push(name) }) - console.log(videoFiles) + console.log(inputPaths) const data = await Networking.UploadSegmentsAndConcatenate(videoFiles) console.log('data', data) @@ -158,6 +159,17 @@ export function RecordingView(props: IRecordingViewProps) { } + }, [videos]) + + useEffect(() => { + + console.log('in finish useEffect') + + if (finished) { + setOrderVideos(true); + } + + }, [finished]) useEffect(() => { @@ -360,8 +372,9 @@ export function RecordingView(props: IRecordingViewProps) {
-- cgit v1.2.3-70-g09d2 From 48c60bd982734676f972514f7074be6121e7c5df Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 8 Jun 2022 18:24:53 -0400 Subject: big commit. FINALLY got the combining segments to work using ffmpeg using a different workflow. small ui changes as well. --- src/client/Network.ts | 15 +-- .../views/nodes/RecordingBox/ProgressBar.tsx | 20 +++- .../views/nodes/RecordingBox/RecordingBox.tsx | 7 +- .../views/nodes/RecordingBox/RecordingView.tsx | 126 ++++----------------- src/server/ApiManagers/UploadManager.ts | 23 +--- src/server/DashUploadUtils.ts | 59 +++------- 6 files changed, 60 insertions(+), 190 deletions(-) (limited to 'src') diff --git a/src/client/Network.ts b/src/client/Network.ts index 8c1f31488..b26f2458d 100644 --- a/src/client/Network.ts +++ b/src/client/Network.ts @@ -35,20 +35,7 @@ export namespace Networking { const response = await fetch("/uploadFormData", parameters); return response.json(); } - - 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)); - const parameters = { - method: 'POST', - body: formData - }; - const response = await fetch("/uploadVideosandConcatenate", parameters); - return response.json(); - } - + export async function UploadYoutubeToServer(videoId: string): Promise[]> { const parameters = { method: 'POST', diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx index a91656cbc..effc3d8a8 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -53,11 +53,6 @@ export function ProgressBar(props: ProgressBarProps) { return [...prevOrdered, { endTime, startTime , order }]; }); } - // }props.videos.map((vid, order) => { - // //const { endTime, startTime } = vid - // // TODO: not tranfer the blobs around - // return { ...vid, order }; - // })) }, [props.videos]); useEffect(() => { @@ -96,7 +91,6 @@ export function ProgressBar(props: ProgressBarProps) { } const onPointerDown = (e: React.PointerEvent) => { - console.log('pointer down') // don't move the videobox element e.stopPropagation() @@ -107,6 +101,20 @@ export function ProgressBar(props: ProgressBarProps) { // don't do anything if null const progressBar = progressBarRef.current if (progressBar == null || clickedSegment.id === progressBar.id) return + + console.log('pointer down') + // if holding shift key, let's remove that segment + if (e.shiftKey) { + // TODO: fix this + const segId = parseInt(clickedSegment.id.split('-')[1]) + const o = ordered[segId].order + setOrdered(prevOrdered => { + return prevOrdered.filter((seg, i) => i !== segId) + }) + + return + } + const ptrId = e.pointerId; progressBar.setPointerCapture(ptrId) diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 68f3b3ad4..bd4af0a75 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -30,7 +30,7 @@ export class RecordingBox extends ViewBoxBaseComponent() { Doc.SetNativeHeight(this.dataDoc, 720); } - @observable result: Upload.FileInformation | undefined = undefined + @observable result: Upload.AccessPathInfo | undefined = undefined @observable videoDuration: number | undefined = undefined @action @@ -39,13 +39,14 @@ export class RecordingBox extends ViewBoxBaseComponent() { } @action - setResult = (info: Upload.FileInformation, trackScreen: boolean) => { + setResult = (info: Upload.AccessPathInfo, trackScreen: boolean) => { + if (info == null) return this.result = info this.dataDoc.type = DocumentType.VID; this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration; this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey); - this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client); + this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client); this.dataDoc[this.fieldKey + "-recorded"] = true; // stringify the presenation and store it if (trackScreen) { diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index a5c2dc85c..8c8728fc3 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -18,7 +18,7 @@ export interface MediaSegment { } interface IRecordingViewProps { - setResult: (info: Upload.FileInformation, trackScreen: boolean) => void + setResult: (info: Upload.AccessPathInfo, trackScreen: boolean) => void setDuration: (seconds: number) => void id: string } @@ -57,7 +57,7 @@ export function RecordingView(props: IRecordingViewProps) { useEffect(() => { - console.log('in videos useEffect') + // console.log('in videos useEffect', finished) if (finished) { (async () => { @@ -71,89 +71,18 @@ export function RecordingView(props: IRecordingViewProps) { const { name } = videoFile; inputPaths.push(name) }) - - console.log(inputPaths) - 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 serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles)) + .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server) - // const inputListName = 'order.txt'; - // fs.writeFileSync(inputListName, inputPaths.join('\n')); - // var merge = ffmpeg(); - // merge.input(inputListName) - // .inputOptions(['-f concat', '-safe 0']) - // .outputOptions('-c copy') - // .save('output.mp4') - - // fs.unlinkSync(inputListName); - - // const combined = await DashUploadUtils.combineSegments(videoFiles, inputPaths) - // console.log('combined', combined) - - // const outputFile = new File(['output.mp4'], 'output.mp4', { type: 'video/mp4', lastModified: Date.now() }); - - // const data = await Networking.UploadFilesToServer(combined) - // const result = data[0].result - // if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox - // props.setResult(result, trackScreen) - // } else { - // alert("video conversion failed"); - // } - - - // if (format.includes("x-matroska")) { - // await new Promise(res => ffmpeg(file.path) - // .videoCodec("copy") // this will copy the data instead of reencode it - // .save(file.path.replace(".mkv", ".mp4")) - // .on('end', res)); - // file.path = file.path.replace(".mkv", ".mp4"); - // format = ".mp4"; - // } - // console.log('crossOriginIsolated', crossOriginIsolated) - // props.setDuration(recordingTimer * 100) - - // console.log('Loading ffmpeg-core.js'); - // const ffmpeg = createFFmpeg({ log: true }); - // await ffmpeg.load(); - // console.log('ffmpeg-core.js loaded'); - - // let allVideoChunks: any = []; - // const inputPaths: string[] = []; - // // write each segment into it's indexed file - // videos.forEach(async (vid, i) => { - // const vidName = `segvideo${i}.mkv` - // inputPaths.push(vidName) - // const videoFile = new File(vid.videoChunks, vidName, { type: allVideoChunks[0].type, lastModified: Date.now() }); - // ffmpeg.FS('writeFile', vidName, await fetchFile(videoFile)); - // // }) - // ffmpeg.FS('writeFile', 'order.txt', inputPaths.join('\n')); - - // console.log('concat') - // await ffmpeg.run('-f', 'concat', '-safe', '0', '-i', 'order.txt', 'ouput.mp4'); - - // const { buffer } = ffmpeg.FS('readFile', 'output.mp4'); - // const concatVideo = new File([buffer], 'concat.mp4', { type: "video/mp4" }); - - // const data = await Networking.UploadFilesToServer(concatVideo) - // const result = data[0].result - // if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox - // props.setResult(result, trackScreen) - // } else { - // alert("video conversion failed"); - // } - - // // delete all files in MEMFS - // inputPaths.forEach(path => ffmpeg.FS('unlink', path)); - // ffmpeg.FS('unlink', 'order.txt'); - // ffmpeg.FS('unlink', 'output.mp4'); + const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths) + console.log(result) + if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox + props.setResult(result, trackScreen) + } else { + alert("video conversion failed"); + } + })(); } @@ -162,8 +91,6 @@ export function RecordingView(props: IRecordingViewProps) { }, [videos]) useEffect(() => { - - console.log('in finish useEffect') if (finished) { setOrderVideos(true); @@ -177,7 +104,7 @@ export function RecordingView(props: IRecordingViewProps) { if (!navigator.mediaDevices) { console.log('This browser does not support getUserMedia.') } - console.log('This device has the correct media devices.') + // console.log('This device has the correct media devices.') }, []) useEffect(() => { @@ -244,7 +171,6 @@ export function RecordingView(props: IRecordingViewProps) { // reset the temporary chunks videoChunks = [] setRecording(false); - setFinished(true); trackScreen && RecordingApi.Instance.pause(); } @@ -269,8 +195,10 @@ export function RecordingView(props: IRecordingViewProps) { } - const stop = () => { - if (videoRecorder.current) { + const stop = (e: React.MouseEvent) => { + e.stopPropagation() + if (videoRecorder.current) { + setFinished(true); if (videoRecorder.current.state !== "inactive") { videoRecorder.current.stop(); // recorder.current.stream.getTracks().forEach((track: any) => track.stop()) @@ -278,19 +206,21 @@ export function RecordingView(props: IRecordingViewProps) { } } - const pause = () => { + const pause = (e: React.MouseEvent) => { + e.stopPropagation() if (videoRecorder.current) { if (videoRecorder.current.state === "recording") { - videoRecorder.current.pause(); + videoRecorder.current.stop(); } } } - const startOrResume = () => { + const startOrResume = (e: React.MouseEvent) => { + e.stopPropagation() if (!videoRecorder.current || videoRecorder.current.state === "inactive") { record(); } else if (videoRecorder.current.state === "paused") { - videoRecorder.current.resume(); + videoRecorder.current.start(); } } @@ -315,16 +245,6 @@ export function RecordingView(props: IRecordingViewProps) { const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000); return toTwoDigit(minutes) + " : " + toTwoDigit(seconds); } - - const doTranscode = async () => { - - // console.log('Start transcoding'); - // ffmpeg.FS('writeFile', 'test.avi', await fetchFile('/flame.avi')); - // await ffmpeg.run('-i', 'test.avi', 'test.mp4'); - // console.log('Complete transcoding'); - // const data = ffmpeg.FS('readFile', 'test.mp4'); - // console.log(URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }))); - }; return (
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 398b007b5..0ee0a34df 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -44,25 +44,10 @@ export default class UploadManager extends ApiManager { 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 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(); - }); - }); + subscription: "/concatVideos", + secureHandler: async ({ req, res }) => { + // req.body contains the array of server paths to the videos + _success(res, await DashUploadUtils.concatVideos(req.body)); } }); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 148b0df65..be30c115d 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -63,21 +63,20 @@ export namespace DashUploadUtils { const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr - export async function concatenateVideos(filePaths: string[]): Promise { + export async function concatVideos(filePaths: string[]): 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, inputListName); // make a list of paths to create the ordered text file for ffmpeg - const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n'); + const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n'); + // write the text file to the file system writeFile(textFilePath, filePathsText, (err) => console.log(err)); - console.log(filePathsText) + console.log('fileTextPaths', filePathsText) // make output file name based on timestamp - const outputFileName = `output-${Utils.GenerateGuid()}.mkv`; - // create the output file path in the parsed_file directory - const outputFilePath = path.join(filesDirectory, outputFileName); + const outputFileName = `output-${Utils.GenerateGuid()}.mp4`; + // create the output file path in the videos directory + const outputFilePath = path.join(pathToDirectory(Directory.videos), outputFileName); // concatenate the videos await new Promise((resolve, reject) => { @@ -92,45 +91,15 @@ export namespace DashUploadUtils { .on("end", resolve); }) - // delete concat.txt from the file system - unlinkSync(textFilePath); - - // read the output file from the file system - // 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() + // delete concat.txt from the file system + unlinkSync(textFilePath); + // delete the old segment videos from the server + filePaths.forEach(filePath => unlinkSync(filePath)); - // 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 }) + // return the path(s) to the output file + return { + accessPaths: getAccessPaths(Directory.videos, outputFileName) } - - // 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 { -- cgit v1.2.3-70-g09d2 From bb8d6b6ad83bf20cdaf0f9a4b6665b3c1371cb95 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 9 Jun 2022 10:40:34 -0400 Subject: got the removing on the shift key to work. --- .../views/nodes/RecordingBox/ProgressBar.tsx | 72 +++++++++++++--------- .../views/nodes/RecordingBox/RecordingView.tsx | 9 ++- 2 files changed, 52 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx index effc3d8a8..7ceae6696 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -33,51 +33,71 @@ export function ProgressBar(props: ProgressBarProps) { const [dragged, setDragged] = useState(-1); + const [totalTime, setTotalTime] = useState(0); + + // this holds the index of the videoc segment to be removed + const [removed, setRemoved] = useState(-1); + // const totalTime = useMemo(() => props.videos.lastElement().endTime, [props.videos]) - const totalTime = () => props.videos.lastElement().endTime + // const totalTime = () => props.videos.lastElement().endTime // const memoTotal = useMemo(totalTime, [props.videos]) useEffect(() => { const segmentsJSX = ordered.map((seg, i) => -
{seg.order}
); +
{seg.order}
); setSegments(segmentsJSX) }, [dragged, ordered]) useEffect(() => { + // this useEffect fired when the videos are being rearragned to the order + // in this case, do nothing. + if (props.orderVideos) return; + const order = props.videos.length - if (order && !props.orderVideos) { + // in this case, a new video is added + if (order && 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 + else if (order < ordered.length) { + // update the total time + setTotalTime(prevTime => prevTime - (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); + } }, [props.videos]); useEffect(() => { props.setVideos(vids => ordered.map((seg) => vids[seg.order - 1])); }, [props.orderVideos]); + useEffect(() => { + if (removed === -1) return; + console.log('removed', removed) - // const handleClick = (e: React.MouseEvent) => { - // let progressbar = document.getElementById('progressbar')! - // let bounds = progressbar!.getBoundingClientRect(); - // let x = e.clientX - bounds.left; - // let percent = x / progressbar.clientWidth * 100 - - // for (let i = 0; i < props.marks.length; i++) { - // let start = i == 0 ? 0 : props.marks[i-1]; - // if (percent > start && percent < props.marks[i]) { - // props.playSegment(i) - // // console.log(i) - // // console.log(percent) - // // console.log(props.marks[i]) - // break - // } - // } - // } + // 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)); + }, [removed]); const updateLastHover = (segId: number): CurrentHover | null => { // get the segId of the segment that will become the new bounding area @@ -102,16 +122,12 @@ export function ProgressBar(props: ProgressBarProps) { const progressBar = progressBarRef.current if (progressBar == null || clickedSegment.id === progressBar.id) return - console.log('pointer down') // if holding shift key, let's remove that segment + // TODO: think of a way to accomodate touch -> maybe if poiuntermove isn't fired after x secs? if (e.shiftKey) { - // TODO: fix this - const segId = parseInt(clickedSegment.id.split('-')[1]) - const o = ordered[segId].order - setOrdered(prevOrdered => { - return prevOrdered.filter((seg, i) => i !== segId) - }) - + const segId = parseInt(clickedSegment.id.split('-')[1]); + console.log('removing segment', segId) + setRemoved(segId); return } diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 8c8728fc3..68d85855c 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -55,6 +55,7 @@ export function RecordingView(props: IRecordingViewProps) { } } + useEffect(() => { // console.log('in videos useEffect', finished) @@ -90,6 +91,12 @@ export function RecordingView(props: IRecordingViewProps) { }, [videos]) + + // make useEffect for progess and log when it fires + useEffect(() => { + console.log('progress', progress) + }, [progress]) + useEffect(() => { if (finished) { @@ -198,7 +205,7 @@ export function RecordingView(props: IRecordingViewProps) { const stop = (e: React.MouseEvent) => { e.stopPropagation() if (videoRecorder.current) { - setFinished(true); + setFinished(true); if (videoRecorder.current.state !== "inactive") { videoRecorder.current.stop(); // recorder.current.stream.getTracks().forEach((track: any) => track.stop()) -- cgit v1.2.3-70-g09d2 From 28ec1d48ee9e8f4d8d82337c2116ac8027ed653d Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 9 Jun 2022 11:08:34 -0400 Subject: got the progress bar to move while recording, but it is a tad slow --- .../views/nodes/RecordingBox/ProgressBar.tsx | 35 +++++++++++++++++++--- .../views/nodes/RecordingBox/RecordingView.tsx | 8 ++--- 2 files changed, 33 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx index 7ceae6696..ce231ba62 100644 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx @@ -10,6 +10,8 @@ interface ProgressBarProps { videos: MediaSegment[], setVideos: React.Dispatch>, orderVideos: boolean, + progress: number, + recording: boolean, } interface SegmentBox { @@ -33,7 +35,11 @@ export function ProgressBar(props: ProgressBarProps) { const [dragged, setDragged] = useState(-1); - const [totalTime, setTotalTime] = useState(0); + // 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); @@ -43,12 +49,31 @@ export function ProgressBar(props: ProgressBarProps) { // 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 recording", props.recording) + }, [props.recording]) + + + useEffect(() => { + console.log('rendering new segments') + const totalTime = props.progress*1000 - totalRemovedTime const segmentsJSX = ordered.map((seg, i) =>
{seg.order}
); setSegments(segmentsJSX) - }, [dragged, ordered]) + }, [dragged, ordered, props.progress]) + useEffect(() => { // this useEffect fired when the videos are being rearragned to the order @@ -60,7 +85,7 @@ export function ProgressBar(props: ProgressBarProps) { if (order && order > ordered.length) { const { endTime, startTime } = props.videos.lastElement(); // update total time - setTotalTime(prevTime => prevTime + (endTime - startTime)); + // setTotalTime(prevTime => prevTime + (endTime - startTime)); // add the new segment to the ordered array setOrdered(prevOrdered => { return [...prevOrdered, { endTime, startTime , order }]; @@ -70,7 +95,9 @@ export function ProgressBar(props: ProgressBarProps) { // in this case, a segment is removed else if (order < ordered.length) { // update the total time - setTotalTime(prevTime => prevTime - (ordered[removed].endTime - ordered[removed].startTime)); + // 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; diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 68d85855c..40117c41c 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -91,12 +91,6 @@ export function RecordingView(props: IRecordingViewProps) { }, [videos]) - - // make useEffect for progess and log when it fires - useEffect(() => { - console.log('progress', progress) - }, [progress]) - useEffect(() => { if (finished) { @@ -302,6 +296,8 @@ export function RecordingView(props: IRecordingViewProps) { videos={videos} setVideos={setVideos} orderVideos={orderVideos} + progress={progress} + recording={recording} // playSegment={playSegment} />
-- 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') 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') 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 (