aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/VideoBox.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2024-01-04 13:11:22 -0500
committerbobzel <zzzman@gmail.com>2024-01-04 13:11:22 -0500
commitae27dd1689ae1716591aab094e6d41f3a0160fef (patch)
tree09735c798abde4fd0b8f234c48375bd6cb1112a4 /src/client/views/nodes/VideoBox.tsx
parentebf32ac65d35053f847fb2cf60f915eb29d6fdd5 (diff)
fixed ffmpeg for uploading videos. fixed getView() to take focus options so that if a sidebar or timeline has to be opened, the didMove flag can be set properly. added video snapshot frame to top menu. fixed using '^' to stop and start a selection region on timelines.
Diffstat (limited to 'src/client/views/nodes/VideoBox.tsx')
-rw-r--r--src/client/views/nodes/VideoBox.tsx158
1 files changed, 20 insertions, 138 deletions
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index ce73d9f37..fb42286af 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,9 +1,9 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { basename } from 'path';
import * as React from 'react';
-import { Doc, StrListCast } from '../../../fields/Doc';
+import { Doc, Opt, StrListCast } from '../../../fields/Doc';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
@@ -12,23 +12,20 @@ import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
-import { Networking } from '../../Network';
import { DocumentManager } from '../../util/DocumentManager';
import { FollowLinkScript } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ReplayMovements } from '../../util/ReplayMovements';
-import { SelectionManager } from '../../util/SelectionManager';
-import { SnappingManager } from '../../util/SnappingManager';
import { undoBatch } from '../../util/UndoManager';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
-import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView';
+import { DocFocusOptions, DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { RecordingBox } from './RecordingBox';
import { PinProps, PresBox } from './trails';
@@ -40,7 +37,7 @@ import './VideoBox.scss';
* Supporting Components: CollectionStackedTimeline
*
* VideoBox is a node that supports the playback of video files in Dash.
- * When a video file or YouTube video is importeed into Dash, it is immediately rendered as a VideoBox document.
+ * When a video file is importeed into Dash, it is immediately rendered as a VideoBox document.
* CollectionStackedTimline handles AudioBox and VideoBox shared behavior, but VideoBox handles playing, pausing, etc because it contains <video> element
* User can trim video: nondestructive, just sets new bounds for playback and rendering timeline
* Like images, users can zoom and pan and it has an overlay layer allowing for annotations on top of the video at different times
@@ -51,16 +48,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(VideoBox, fieldKey);
}
- static _youtubeIframeCounter: number = 0;
static heightPercent = 80; // height of video relative to videoBox when timeline is open
static numThumbnails = 20;
private unmounting = false;
private _disposers: { [name: string]: IReactionDisposer } = {};
- private _youtubePlayer: YT.Player | undefined = undefined;
private _videoRef: HTMLVideoElement | null = null; // <video> ref
private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen
- private _youtubeIframeId: number = -1;
- private _youtubeContentCreated = false;
private _audioPlayer: HTMLAudioElement | null = null;
private _marqueeref = React.createRef<MarqueeAnnotator>();
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
@@ -74,12 +67,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._props.setContentView?.(this);
}
- @observable _stackedTimeline: any = undefined; // CollectionStackedTimeline ref
- @observable static _nativeControls: boolean = false; // default html controls
+ @observable _stackedTimeline: CollectionStackedTimeline | undefined = undefined; // CollectionStackedTimeline ref
@observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
@observable _screenCapture = false;
@observable _clicking = false; // used for transition between showing/hiding timeline
- @observable _forceCreateYouTubeIFrame = false;
@observable _playTimer?: NodeJS.Timeout = undefined;
@observable _fullScreen = false;
@observable _playing = false;
@@ -99,11 +90,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "_duration"]); }
@observable rawDuration: number = 0;
- @computed get youtubeVideoId() {
- const field = Cast(this.dataDoc[this._props.fieldKey], VideoField);
- return field && field.url.href.indexOf('youtube') !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split('/')) : '';
- }
-
// returns the path of the audio file
@computed get audiopath() {
const field = Cast(this.Document[this._props.fieldKey + '_audio'], AudioField, null);
@@ -130,17 +116,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
componentDidMount() {
this.unmounting = false;
this._props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
- if (this.youtubeVideoId) {
- const youtubeaspect = 400 / 315;
- const nativeWidth = Doc.NativeWidth(this.layoutDoc);
- const nativeHeight = Doc.NativeHeight(this.layoutDoc);
- if (!nativeWidth || !nativeHeight) {
- if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
- Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
- this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
- }
- }
- this.player && this.setPlayheadTime(this.timeline.clipStart || 0);
+ this.player && this.setPlayheadTime(this.timeline?.clipStart || 0);
document.addEventListener('keydown', this.keyEvents, true);
if (this.presentation) {
@@ -201,8 +177,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
update && this.player && this.playFrom(start, undefined, true);
update && this._audioPlayer?.play();
- update && this._youtubePlayer?.playVideo();
- this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
} catch (e) {
console.log('Video Play Exception:', e);
}
@@ -212,11 +186,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// goes to time
@action public Seek(time: number) {
- try {
- this._youtubePlayer?.seekTo(Math.round(time), true);
- } catch (e) {
- console.log('Video Seek Exception:', e);
- }
this.player && (this.player.currentTime = time);
this._audioPlayer && (this._audioPlayer.currentTime = time);
// TODO: revisit this and clean it
@@ -242,13 +211,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
try {
update && this.player?.pause();
update && this._audioPlayer?.pause();
- update && this._youtubePlayer?.pauseVideo();
- this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
- this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true);
} catch (e) {
console.log('Video Pause Exception:', e);
}
- this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
this._playTimer = undefined;
this.updateTimecode();
if (!this._finished) {
@@ -270,11 +235,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._fullScreen = true;
this.player && this._contentRef && this._contentRef.requestFullscreen();
}
- try {
- this._youtubePlayer && this._props.addDocTab(this.Document, OpenWhere.add);
- } catch (e) {
- console.log('Video FullScreen Exception:', e);
- }
};
// fades out controls in fullscreen after mouse stops moving
@@ -334,22 +294,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
});
this._props.addDocument?.(b);
DocUtils.MakeLink(b, this.Document, { link_relationship: 'video snapshot' });
- Networking.PostToServer('/youtubeScreenshot', {
- id: this.youtubeVideoId,
- timecode: this.layoutDoc._layout_currentTimecode,
- }).then(response => {
- const resolved = response?.accessPaths?.agnostic?.client;
- if (resolved) {
- this._props.removeDocument?.(b);
- this.createSnapshotLink(resolved);
- }
- });
} else {
//convert to desired file format
const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
// if you want to preview the captured image,
const retitled = StrCast(this.Document.title).replace(/[ -\.:]/g, '');
- const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString().replace(/\./, '_'));
+ const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[\.\/\?\=]/g, '_'));
const filename = basename(encodedFilename);
Utils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
}
@@ -416,16 +366,18 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@action
updateTimecode = () => {
!this.unmounting && this.player && (this.layoutDoc._layout_currentTimecode = this.player.currentTime);
- try {
- this._youtubePlayer && (this.layoutDoc._layout_currentTimecode = this._youtubePlayer.getCurrentTime?.());
- } catch (e) {
- console.log('Video Timecode Exception:', e);
- }
};
- // getView = async (doc: Doc) => {
- // return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
- // };
+ getView = (doc: Doc, options: DocFocusOptions) => {
+ if (this._stackedTimeline?.makeDocUnfiltered(doc)) {
+ if (this.heightPercent === 100) {
+ this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent;
+ options.didMove = true;
+ }
+ return this._stackedTimeline.getView(doc, options);
+ }
+ return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+ };
// extracts video thumbnails and saves them as field of doc
getVideoThumbnails = () => {
@@ -518,7 +470,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
icon: 'expand-arrows-alt',
});
subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : 'Auto play') + ' anchors onClick', event: () => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors), icon: 'expand-arrows-alt' });
- // subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
// subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" });
// subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" });
// subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" });
@@ -581,7 +532,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
ref={this.setVideoRef}
style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.layoutDoc._freeform_scale)})`, transformOrigin: 'top left' } : {}}
onCanPlay={this.videoLoad}
- controls={VideoBox._nativeControls}
+ controls={false}
onPlay={() => this.Play()}
onSeeked={this.updateTimecode}
onPause={() => this.Pause()}
@@ -600,54 +551,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
);
}
- @action youtubeIframeLoaded = (e: any) => {
- if (!this._youtubeContentCreated) {
- this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
- return;
- } else this._youtubeContentCreated = false;
-
- this.loadYouTube(e.target);
- };
-
- loadYouTube = (iframe: any) => {
- let started = true;
- const onYoutubePlayerStateChange = (event: any) =>
- runInAction(() => {
- if (started && event.data === YT.PlayerState.PLAYING) {
- started = false;
- this._youtubePlayer?.unMute();
- //this.Pause();
- return;
- }
- if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
- if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
- });
- const onYoutubePlayerReady = (event: any) => {
- this._disposers.reactionDisposer?.();
- this._disposers.youtubeReactionDisposer?.();
- this._disposers.reactionDisposer = reaction(
- () => this.layoutDoc._layout_currentTimecode,
- () => !this._playing && this.Seek(NumCast(this.layoutDoc._layout_currentTimecode))
- );
- this._disposers.youtubeReactionDisposer = reaction(
- () => Doc.ActiveTool === InkTool.None && this._props.isSelected() && !SnappingManager.IsDragging && !SnappingManager.IsResizing,
- interactive => (iframe.style.pointerEvents = interactive ? 'all' : 'none'),
- { fireImmediately: true }
- );
- };
- if (typeof YT === undefined) setTimeout(() => this.loadYouTube(iframe), 100);
- else {
- (YT as any)?.ready(() => {
- this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, {
- events: {
- onReady: this._props.dontRegisterView ? undefined : onYoutubePlayerReady,
- onStateChange: this._props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
- },
- });
- });
- }
- };
-
// for play button
onPlayDown = () => (this._playing ? this.Pause() : this.Play());
@@ -723,25 +626,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
};
- @computed get youtubeContent() {
- this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
- this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
- const classname = 'videoBox-content-YouTube' + (this._fullScreen ? '-fullScreen' : '');
- const start = untracked(() => Math.round(NumCast(this.layoutDoc._layout_currentTimecode)));
- return (
- <iframe
- key={this._youtubeIframeId}
- id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
- onPointerLeave={this.updateTimecode}
- onLoad={this.youtubeIframeLoaded}
- className={classname}
- width={Doc.NativeWidth(this.layoutDoc) || 640}
- height={Doc.NativeHeight(this.layoutDoc) || 390}
- src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`}
- />
- );
- }
-
// for annotating, adds doc with time info
@action.bound
addDocWithTimecode(doc: Doc | Doc[]): boolean {
@@ -1090,7 +974,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
addDocument={this.addDocWithTimecode}>
- {this.youtubeVideoId ? this.youtubeContent : this.content}
+ {this.content}
</CollectionFreeFormView>
</div>
{this.annotationLayer}
@@ -1225,5 +1109,3 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
);
}
}
-
-VideoBox._nativeControls = false;