aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/documents/Documents.ts13
-rw-r--r--src/client/util/DragManager.ts39
-rw-r--r--src/client/views/PreviewCursor.tsx55
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx12
-rw-r--r--src/client/views/nodes/LoadingBox.tsx10
-rw-r--r--src/client/views/nodes/VideoBox.scss20
-rw-r--r--src/client/views/nodes/VideoBox.tsx212
-rw-r--r--src/server/ApiManagers/UploadManager.ts2
-rw-r--r--src/server/DashUploadUtils.ts48
-rw-r--r--src/server/SharedMediaTypes.ts38
11 files changed, 257 insertions, 194 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 8c3b91177..7111cb233 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,5 +1,4 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { files } from 'jszip';
import { action, runInAction } from 'mobx';
import { basename } from 'path';
import { DateField } from '../../fields/DateField';
@@ -1780,6 +1779,9 @@ export namespace DocUtils {
proto.lng = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection);
}
}
+ if (Upload.isVideoInformation(result)) {
+ proto['data-duration'] = result.duration;
+ }
generatedDocuments.push(doc);
}
}
@@ -1814,7 +1816,7 @@ export namespace DocUtils {
source: { name, type },
result,
} of await Networking.UploadYoutubeToServer(videoId)) {
- name && type && processFileupload(generatedDocuments, name, type, result, options);
+ name && processFileupload(generatedDocuments, name, type, result, options);
}
return generatedDocuments;
}
@@ -1826,7 +1828,12 @@ export namespace DocUtils {
source: { name, type },
result,
} = upfiles.lastElement();
- name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
+ if ((result as any).message) {
+ if (overwriteDoc) {
+ overwriteDoc.isLoading = false;
+ overwriteDoc.errorMessage = (result as any).message;
+ }
+ } else name && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
});
}
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index dfd916e92..cec158d23 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -382,29 +382,28 @@ export namespace DragManager {
}
}
const rect = ele.getBoundingClientRect();
- const rotWidth = (rot > 45 && rot < 135) || (rot > 215 && rot < 305) ? rect.height : rect.width; //rect.width * Math.cos((rot * Math.PI) / 180) + rect.height * Math.sin((rot * Math.PI) / 180);
- const scaling = rot ? rotWidth / ele.offsetWidth : rect.width / (ele.offsetWidth || rect.width);
+ const w = ele.offsetWidth || rect.width;
+ const h = ele.offsetHeight || rect.height;
+ const rotR = -(rot / 180) * Math.PI;
+ const tl = [0, 0];
+ const tr = [Math.cos(rotR) * w, Math.sin(-rotR) * w];
+ const bl = [Math.sin(rotR) * h, Math.cos(-rotR) * h];
+ const br = [Math.cos(rotR) * w + Math.sin(rotR) * h, Math.cos(-rotR) * h - Math.sin(rotR) * w];
+ const minx = Math.min(tl[0], tr[0], br[0], bl[0]);
+ const maxx = Math.max(tl[0], tr[0], br[0], bl[0]);
+ const miny = Math.min(tl[1], tr[1], br[1], bl[1]);
+ const maxy = Math.max(tl[1], tr[1], br[1], bl[1]);
+ const scaling = rect.width / (Math.abs(maxx - minx) || 1);
elesCont.left = Math.min(rect.left, elesCont.left);
elesCont.top = Math.min(rect.top, elesCont.top);
elesCont.right = Math.max(rect.right, elesCont.right);
elesCont.bottom = Math.max(rect.bottom, elesCont.bottom);
- const rotRad = (rot / 180) * Math.PI;
- xs.push(
- (rot > 90 && rot <= 270 ? rect.right : rect.left) + //
- (rot > 270 ? -scaling * (ele.offsetHeight * Math.sin(rotRad)) : 0) +
- (rot <= 90 || rot > 180 ? scaling * (ele.offsetHeight * Math.sin(rotRad)) : 0) +
- (options?.offsetX || 0)
- );
- ys.push(
- rect.top + //
- (rot > 180 ? -scaling * (ele.offsetWidth * Math.sin(rotRad)) : 0) +
- (rot >= 90 && rot < 270 ? -scaling * (ele.offsetHeight * Math.cos(rotRad)) : 0) +
- (options?.offsetY || 0)
- );
+ xs.push(((0 - minx) / (maxx - minx)) * rect.width + rect.left);
+ ys.push(((0 - miny) / (maxy - miny)) * rect.height + rect.top);
scalings.push(scaling);
Object.assign(dragElement.style, {
- opacity: '0',
+ opacity: '0.7',
position: 'absolute',
margin: '0',
top: '0',
@@ -415,9 +414,9 @@ export namespace DragManager {
borderRadius: getComputedStyle(ele).borderRadius,
zIndex: globalCssVariables.contextMenuZindex,
transformOrigin: '0 0',
- width: rot ? '' : `${rect.width / scaling}px`,
- height: rot ? '' : `${rect.height / scaling}px`,
- transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg)`,
+ width: '',
+ height: '',
+ transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg) scale(${scaling})`,
});
dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
@@ -431,8 +430,6 @@ export namespace DragManager {
[dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => (ele as any).style && ((ele as any).style.pointerEvents = 'none'));
dragDiv.appendChild(dragElement);
- scalings[scalings.length - 1] = rect.width / dragElement.getBoundingClientRect().width;
- setTimeout(() => (dragElement.style.opacity = '0.7'));
if (dragElement !== ele) {
const children = [Array.from(ele.children), Array.from(dragElement.children)];
while (children[0].length) {
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 68f5f072d..4c17d5a97 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -4,9 +4,9 @@ import 'normalize.css';
import * as React from 'react';
import { Doc } from '../../fields/Doc';
import { Cast, NumCast, StrCast } from '../../fields/Types';
-import { returnFalse } from '../../Utils';
+import { emptyFunction, returnFalse } from '../../Utils';
import { DocServer } from '../DocServer';
-import { Docs, DocUtils } from '../documents/Documents';
+import { Docs, DocumentOptions, DocUtils } from '../documents/Documents';
import { Transform } from '../util/Transform';
import { undoBatch, UndoManager } from '../util/UndoManager';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
@@ -16,15 +16,25 @@ import './PreviewCursor.scss';
export class PreviewCursor extends React.Component<{}> {
static _onKeyPress?: (e: KeyboardEvent) => void;
static _getTransform: () => Transform;
- static _addDocument: (doc: Doc | Doc[]) => void;
+ static _addDocument: (doc: Doc | Doc[]) => boolean;
static _addLiveTextDoc: (doc: Doc) => void;
static _nudge?: undefined | ((x: number, y: number) => boolean);
+ static _slowLoadDocuments?: (
+ files: File[] | string,
+ options: DocumentOptions,
+ generatedDocuments: Doc[],
+ text: string,
+ completed: ((doc: Doc[]) => void) | undefined,
+ clientX: number,
+ clientY: number,
+ addDocument: (doc: Doc | Doc[]) => boolean
+ ) => Promise<void>;
@observable static _clickPoint = [0, 0];
@observable public static Visible = false;
constructor(props: any) {
super(props);
document.addEventListener('keydown', this.onKeyPress);
- document.addEventListener('paste', this.paste);
+ document.addEventListener('paste', this.paste, true);
}
paste = async (e: ClipboardEvent) => {
@@ -38,20 +48,16 @@ export class PreviewCursor extends React.Component<{}> {
if (plain) {
// tests for youtube and makes video document
if (plain.indexOf('www.youtube.com/watch') !== -1) {
- const url = plain.replace('youtube.com/watch?v=', 'youtube.com/embed/');
- undoBatch(() =>
- PreviewCursor._addDocument(
- Docs.Create.VideoDocument(url, {
- title: url,
- _width: 400,
- _height: 315,
- _nativeWidth: 600,
- _nativeHeight: 472.5,
- x: newPoint[0],
- y: newPoint[1],
- })
- )
- )();
+ const batch = UndoManager.StartBatch('youtube upload');
+ const generatedDocuments: Doc[] = [];
+ const options = {
+ title: plain,
+ _width: 400,
+ _height: 315,
+ x: newPoint[0],
+ y: newPoint[1],
+ };
+ PreviewCursor._slowLoadDocuments?.(plain.split('v=')[1].split('&')[0], options, generatedDocuments, '', undefined, newPoint[0], newPoint[1], PreviewCursor._addDocument).then(batch.end);
} else if (re.test(plain)) {
const url = plain;
undoBatch(() =>
@@ -184,7 +190,17 @@ export class PreviewCursor extends React.Component<{}> {
addLiveText: (doc: Doc) => void,
getTransform: () => Transform,
addDocument: undefined | ((doc: Doc | Doc[]) => boolean),
- nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean)
+ nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean),
+ slowLoadDocuments: (
+ files: File[] | string,
+ options: DocumentOptions,
+ generatedDocuments: Doc[],
+ text: string,
+ completed: ((doc: Doc[]) => void) | undefined,
+ clientX: number,
+ clientY: number,
+ addDocument: (doc: Doc | Doc[]) => boolean
+ ) => Promise<void>
) {
this._clickPoint = [x, y];
this._onKeyPress = onKeyPress;
@@ -192,6 +208,7 @@ export class PreviewCursor extends React.Component<{}> {
this._getTransform = getTransform;
this._addDocument = addDocument || returnFalse;
this._nudge = nudge;
+ this._slowLoadDocuments = slowLoadDocuments;
this.Visible = true;
}
render() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0c4de681a..947bd8aaa 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -52,7 +52,6 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso
import './CollectionFreeFormView.scss';
import { MarqueeView } from './MarqueeView';
import React = require('react');
-import e = require('connect-flash');
export type collectionFreeformViewProps = {
annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
@@ -1858,6 +1857,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
ungroup={this.props.Document._isGroup ? this.promoteCollection : undefined}
nudge={this.isAnnotationOverlay || this.props.renderDepth > 0 ? undefined : this.nudge}
addDocTab={this.addDocTab}
+ slowLoadDocuments={this.slowLoadDocuments}
trySelectCluster={this.trySelectCluster}
activeDocuments={this.getActiveDocuments}
selectDocuments={this.selectDocuments}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 65a11cbcb..58a00bbac 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -40,6 +40,16 @@ interface MarqueeViewProps {
nudge?: (x: number, y: number, nudgeTime?: number) => boolean;
ungroup?: () => void;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
+ slowLoadDocuments: (
+ files: File[] | string,
+ options: DocumentOptions,
+ generatedDocuments: Doc[],
+ text: string,
+ completed: ((doc: Doc[]) => void) | undefined,
+ clientX: number,
+ clientY: number,
+ addDocument: (doc: Doc | Doc[]) => boolean
+ ) => Promise<void>;
}
export interface MarqueeViewBounds {
@@ -330,7 +340,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this._downY = y;
const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) {
- PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
+ PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge, this.props.slowLoadDocuments);
}
this.clearSelection();
}
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index 462ad425a..acf728ebb 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -1,12 +1,10 @@
import { observer } from 'mobx-react';
-import { ViewBoxAnnotatableComponent } from '../DocComponent';
-import { FieldView, FieldViewProps } from './FieldView';
import * as React from 'react';
-import './LoadingBox.scss';
import ReactLoading from 'react-loading';
import { StrCast } from '../../../fields/Types';
-import { computed } from 'mobx';
-import { Docs } from '../../documents/Documents';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { FieldView, FieldViewProps } from './FieldView';
+import './LoadingBox.scss';
/**
* LoadingBox Class represents a placeholder doc for documents that are currently
@@ -39,7 +37,7 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return (
<div className="loadingBoxContainer" style={{ background: this.rootDoc.isLoading ? '' : 'red' }}>
<div className="textContainer">
- <p className="headerText">{this.rootDoc.isLoading ? 'Loading:' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}</p>
+ <p className="headerText">{this.rootDoc.isLoading ? 'Loading (can take several minutes):' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}</p>
<span className="text">{StrCast(this.rootDoc.title)}</span>
{!this.rootDoc.isLoading ? null : <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />}
</div>
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index aa51714da..c2aee7a1b 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,4 +1,4 @@
-@import "../global/globalCssVariables.scss";
+@import '../global/globalCssVariables.scss';
.mini-viewer {
cursor: grab;
@@ -97,7 +97,7 @@
height: 40px;
padding: 0 10px 0 7px;
transition: opacity 0.3s;
- z-index: 100001;
+ z-index: 10001;
.timecode-controls {
display: flex;
@@ -114,7 +114,8 @@
}
}
- .toolbar-slider.volume, .toolbar-slider.zoom {
+ .toolbar-slider.volume,
+ .toolbar-slider.zoom {
width: 50px;
}
@@ -157,7 +158,8 @@
}
}
-.videoBox-content-fullScreen, .videoBox-content-fullScreen-interactive {
+.videoBox-content-fullScreen,
+.videoBox-content-fullScreen-interactive {
display: flex;
justify-content: center;
align-items: flex-end;
@@ -175,16 +177,16 @@ video::-webkit-media-controls {
display: none !important;
}
-input[type="range"] {
+input[type='range'] {
-webkit-appearance: none;
background: none;
}
-input[type="range"]:focus {
+input[type='range']:focus {
outline: none;
}
-input[type="range"]::-webkit-slider-runnable-track {
+input[type='range']::-webkit-slider-runnable-track {
width: 100%;
height: 10px;
cursor: pointer;
@@ -193,7 +195,7 @@ input[type="range"]::-webkit-slider-runnable-track {
border-radius: 10px;
}
-input[type="range"]::-webkit-slider-thumb {
+input[type='range']::-webkit-slider-thumb {
box-shadow: 0;
border: 0;
height: 12px;
@@ -203,4 +205,4 @@ input[type="range"]::-webkit-slider-thumb {
cursor: pointer;
-webkit-appearance: none;
margin-top: -1px;
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 6ff11258d..bfb8c1528 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -935,112 +935,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
);
};
- @computed get UIButtons() {
- const bounds = this.props.docViewPath().lastElement().getBounds();
- const width = (bounds?.right || 0) - (bounds?.left || 0);
- const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
- return (
- <>
- <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}>
- <FontAwesomeIcon icon={this._playing ? 'pause' : 'play'} />
- </div>
-
- {this.timeline && width > 150 && (
- <div className="timecode-controls">
- <div className="timecode-current">{formatTime(curTime)}</div>
-
- {this._fullScreen || (this.heightPercent === 100 && width > 200) ? (
- <div className="timeline-slider">
- <input
- type="range"
- step="0.1"
- min={this.timeline.clipStart}
- max={this.timeline.clipEnd}
- value={curTime}
- className="toolbar-slider time-progress"
- onPointerDown={action((e: React.PointerEvent) => {
- e.stopPropagation();
- this._scrubbing = true;
- })}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
- onPointerUp={action((e: React.PointerEvent) => {
- e.stopPropagation();
- this._scrubbing = false;
- })}
- />
- </div>
- ) : (
- <div>/</div>
- )}
-
- <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div>
- </div>
- )}
-
- <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}>
- <FontAwesomeIcon icon="expand" />
- </div>
-
- {!this._fullScreen && width > 300 && (
- <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}>
- <FontAwesomeIcon icon="eye" />
- </div>
- )}
-
- {!this._fullScreen && width > 300 && (
- <div className="videobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish trimming' : 'start trim'} onPointerDown={this.onClipPointerDown}>
- <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} />
- </div>
- )}
-
- <div
- className="videobox-button"
- title={this._muted ? 'unmute' : 'mute'}
- onPointerDown={e => {
- e.stopPropagation();
- this.toggleMute();
- }}>
- <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} />
- </div>
- {width > 300 && (
- <input
- type="range"
- style={{ width: `min(25%, 50px)` }}
- step="0.1"
- min="0"
- max="1"
- value={this._muted ? 0 : this._volume}
- className="toolbar-slider volume"
- onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
- />
- )}
-
- {!this._fullScreen && this.heightPercent !== 100 && width > 300 && (
- <>
- <div className="videobox-button" title="zoom">
- <FontAwesomeIcon icon="search-plus" />
- </div>
- <input
- type="range"
- step="0.1"
- min="1"
- max="5"
- value={this.timeline?._zoomFactor}
- className="toolbar-slider zoom"
- onPointerDown={(e: React.PointerEvent) => {
- e.stopPropagation();
- }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
- this.zoom(Number(e.target.value));
- }}
- />
- </>
- )}
- </>
- );
- }
-
// renders CollectionStackedTimeline
@computed get renderTimeline() {
return (
@@ -1149,6 +1043,112 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
</div>
);
}
+
+ @computed get UIButtons() {
+ const bounds = this.props.docViewPath().lastElement().getBounds();
+ const width = (bounds?.right || 0) - (bounds?.left || 0);
+ const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
+ return (
+ <>
+ <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}>
+ <FontAwesomeIcon icon={this._playing ? 'pause' : 'play'} />
+ </div>
+
+ {this.timeline && width > 150 && (
+ <div className="timecode-controls">
+ <div className="timecode-current">{formatTime(curTime)}</div>
+
+ {this._fullScreen || (this.heightPercent === 100 && width > 200) ? (
+ <div className="timeline-slider">
+ <input
+ type="range"
+ step="0.1"
+ min={this.timeline.clipStart}
+ max={this.timeline.clipEnd}
+ value={curTime}
+ className="toolbar-slider time-progress"
+ onPointerDown={action((e: React.PointerEvent) => {
+ e.stopPropagation();
+ this._scrubbing = true;
+ })}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
+ onPointerUp={action((e: React.PointerEvent) => {
+ e.stopPropagation();
+ this._scrubbing = false;
+ })}
+ />
+ </div>
+ ) : (
+ <div>/</div>
+ )}
+
+ <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div>
+ </div>
+ )}
+
+ <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}>
+ <FontAwesomeIcon icon="expand" />
+ </div>
+
+ {!this._fullScreen && width > 300 && (
+ <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}>
+ <FontAwesomeIcon icon="eye" />
+ </div>
+ )}
+
+ {!this._fullScreen && width > 300 && (
+ <div className="videobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish trimming' : 'start trim'} onPointerDown={this.onClipPointerDown}>
+ <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} />
+ </div>
+ )}
+
+ <div
+ className="videobox-button"
+ title={this._muted ? 'unmute' : 'mute'}
+ onPointerDown={e => {
+ e.stopPropagation();
+ this.toggleMute();
+ }}>
+ <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} />
+ </div>
+ {width > 300 && (
+ <input
+ type="range"
+ style={{ width: `min(25%, 50px)` }}
+ step="0.1"
+ min="0"
+ max="1"
+ value={this._muted ? 0 : this._volume}
+ className="toolbar-slider volume"
+ onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
+ />
+ )}
+
+ {!this._fullScreen && this.heightPercent !== 100 && width > 300 && (
+ <>
+ <div className="videobox-button" title="zoom">
+ <FontAwesomeIcon icon="search-plus" />
+ </div>
+ <input
+ type="range"
+ step="0.1"
+ min="1"
+ max="5"
+ value={this.timeline?._zoomFactor}
+ className="toolbar-slider zoom"
+ onPointerDown={(e: React.PointerEvent) => {
+ e.stopPropagation();
+ }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ this.zoom(Number(e.target.value));
+ }}
+ />
+ </>
+ )}
+ </>
+ );
+ }
}
VideoBox._nativeControls = false;
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 787e331c5..0b6e18743 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -86,7 +86,7 @@ export default class UploadManager extends ApiManager {
const videoId = JSON.parse(payload).videoId;
const results: Upload.FileResponse[] = [];
const result = await DashUploadUtils.uploadYoutube(videoId);
- result && !(result.result instanceof Error) && results.push(result);
+ result && results.push(result);
_success(res, results);
resolve();
});
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index ef7192ecc..28e26e51e 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -17,8 +17,6 @@ import { resolvedServerUrl } from './server_Initialization';
import { AcceptableMedia, Upload } from './SharedMediaTypes';
import request = require('request-promise');
import formidable = require('formidable');
-import { file } from 'jszip';
-import { csvParser } from './DataVizUtils';
const { exec } = require('child_process');
const parse = require('pdf-parse');
const ffmpeg = require('fluent-ffmpeg');
@@ -102,16 +100,41 @@ export namespace DashUploadUtils {
}
export function uploadYoutube(videoId: string): Promise<Upload.FileResponse> {
- console.log('UPLOAD ' + videoId);
return new Promise<Upload.FileResponse<Upload.FileInformation>>((res, rej) => {
- exec('youtube-dl -o ' + (videoId + '.mp4') + ' https://www.youtube.com/watch?v=' + videoId + ' -f "best[filesize<50M]"', (error: any, stdout: any, stderr: any) => {
- if (error) console.log(`error: ${error.message}`);
- else if (stderr) console.log(`stderr: ${stderr}`);
- else {
- console.log(`stdout: ${stdout}`);
- const data = { size: 0, path: videoId + '.mp4', name: videoId, type: 'video/mp4' };
- const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ''), mtime: null, length: 0, mime: '', toJson: () => undefined as any }) };
- res(MoveParsedFile(file, Directory.videos));
+ console.log('Uploading YouTube video: ' + videoId);
+ exec('youtube-dl -o ' + (videoId + '.mp4') + ' ' + videoId + ' -f "bestvideo[filesize<5M]+bestaudio/bestvideo+bestaudio"', (error: any, stdout: any, stderr: any) => {
+ if (error) {
+ console.log(`error: Error: ${error.message}`);
+ res({
+ source: {
+ size: 0,
+ path: videoId,
+ name: videoId,
+ type: '',
+ toJSON: () => ({ name: videoId, path: videoId }),
+ },
+ result: { name: 'failed youtube query', message: `Could not upload YouTube video (${videoId}). Error: ${error.message}` },
+ });
+ } else if (stderr) {
+ console.log(`stderr: StdError: ${stderr}`);
+ res({
+ source: {
+ size: 0,
+ path: videoId,
+ name: videoId,
+ type: '',
+ toJSON: () => ({ name: videoId, path: videoId }),
+ },
+ result: { name: 'failed youtube query', message: `Could not upload YouTube video (${videoId}). Error: ${stderr}` },
+ });
+ } else {
+ exec('youtube-dl -o ' + (videoId + '.mp4') + ' ' + videoId + ' --get-duration', (error: any, stdout: any, stderr: any) => {
+ const time = Array.from(stdout.trim().split(':')).reverse();
+ const duration = (time.length > 2 ? Number(time[2]) * 1000 * 60 : 0) + (time.length > 1 ? Number(time[1]) * 60 : 0) + (time.length > 0 ? Number(time[0]) : 0);
+ const data = { size: 0, path: videoId + '.mp4', name: videoId, type: 'video/mp4' };
+ const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ''), mtime: duration.toString(), mime: '', toJson: () => undefined as any }) };
+ res(MoveParsedFile(file, Directory.videos));
+ });
}
});
});
@@ -352,7 +375,7 @@ export namespace DashUploadUtils {
* @param suffix If the file doesn't have a suffix and you want to provide it one
* to appear in the new location
*/
- export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix: string | undefined = undefined, text?: string): Promise<Upload.FileResponse> {
+ export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix: string | undefined = undefined, text?: string, duration?: number): Promise<Upload.FileResponse> {
const { path: sourcePath } = file;
let name = path.basename(sourcePath);
suffix && (name += suffix);
@@ -368,6 +391,7 @@ export namespace DashUploadUtils {
agnostic: getAccessPaths(destination, name),
},
rawText: text,
+ duration,
},
});
});
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
index cde95526f..7db1c2dae 100644
--- a/src/server/SharedMediaTypes.ts
+++ b/src/server/SharedMediaTypes.ts
@@ -2,36 +2,45 @@ import { ExifData } from 'exif';
import { File } from 'formidable';
export namespace AcceptableMedia {
- export const gifs = [".gif"];
- export const pngs = [".png"];
- export const jpgs = [".jpg", ".jpeg"];
- export const webps = [".webp"];
- export const tiffs = [".tiff"];
+ export const gifs = ['.gif'];
+ export const pngs = ['.png'];
+ export const jpgs = ['.jpg', '.jpeg'];
+ export const webps = ['.webp'];
+ export const tiffs = ['.tiff'];
export const imageFormats = [...pngs, ...jpgs, ...gifs, ...webps, ...tiffs];
- export const videoFormats = [".mov", ".mp4", ".quicktime", ".mkv", ".x-matroska;codecs=avc1"];
- export const applicationFormats = [".pdf"];
- export const audioFormats = [".wav", ".mp3", ".mpeg", ".flac", ".au", ".aiff", ".m4a", ".webm"];
+ export const videoFormats = ['.mov', '.mp4', '.quicktime', '.mkv', '.x-matroska;codecs=avc1'];
+ export const applicationFormats = ['.pdf'];
+ export const audioFormats = ['.wav', '.mp3', '.mpeg', '.flac', '.au', '.aiff', '.m4a', '.webm'];
}
export namespace Upload {
-
export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation {
- return "nativeWidth" in uploadResponse;
+ return 'nativeWidth' in uploadResponse;
+ }
+
+ export function isVideoInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.VideoInformation {
+ return 'duration' in uploadResponse;
}
export interface FileInformation {
accessPaths: AccessPathInfo;
rawText?: string;
+ duration?: number;
}
- export type FileResponse<T extends FileInformation = FileInformation> = { source: File, result: T | Error };
+ export type FileResponse<T extends FileInformation = FileInformation> = { source: File; result: T | Error };
export type ImageInformation = FileInformation & InspectionResults;
+ export type VideoInformation = FileInformation & VideoResults;
+
export interface AccessPathInfo {
- [suffix: string]: { client: string, server: string };
+ [suffix: string]: { client: string; server: string };
}
+ export interface VideoResults {
+ duration: number;
+ }
export interface InspectionResults {
source: string;
requestable: string;
@@ -44,8 +53,7 @@ export namespace Upload {
}
export interface EnrichedExifData {
- data: ExifData & ExifData["gps"];
+ data: ExifData & ExifData['gps'];
error?: string;
}
-
-} \ No newline at end of file
+}