aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/VideoBox.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2023-04-05 22:44:03 -0400
committerbobzel <zzzman@gmail.com>2023-04-05 22:44:03 -0400
commit9b41da1af16b982ee8ac2fc09f2f8b5d67eac9fb (patch)
treebc3f57cd5b31fd453d272c925f6d5b728ab63bae /src/client/views/nodes/VideoBox.tsx
parent9dae453967183b294bf4f7444b948023a1d52d39 (diff)
parent8f7e99641f84ad15f34ba9e4a60b664ac93d2e5d (diff)
Merge branch 'master' into data-visualization-view-naafi
Diffstat (limited to 'src/client/views/nodes/VideoBox.tsx')
-rw-r--r--src/client/views/nodes/VideoBox.tsx448
1 files changed, 259 insertions, 189 deletions
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index a3d501153..e14ad4b05 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -3,17 +3,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx';
import { observer } from 'mobx-react';
import { basename } from 'path';
-import * as rp from 'request-promise';
-import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc';
+import { Doc, HeightSym, WidthSym } from '../../../fields/Doc';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
+import { ObjectField } from '../../../fields/ObjectField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+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 { LinkManager } from '../../util/LinkManager';
import { ReplayMovements } from '../../util/ReplayMovements';
import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
@@ -27,8 +28,10 @@ import { DocumentDecorations } from '../DocumentDecorations';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
+import { OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { RecordingBox } from './RecordingBox';
+import { PinProps, PresBox } from './trails';
import './VideoBox.scss';
const path = require('path');
@@ -49,30 +52,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(VideoBox, fieldKey);
}
- /**
- * Uploads an image buffer to the server and stores with specified filename. by default the image
- * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
- * @param imageUri the bytes of the image
- * @param returnedFilename the base filename to store the image on the server
- * @param nosuffix optionally suppress creating multiple resolution images
- */
- public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
- try {
- const posting = Utils.prepend('/uploadURI');
- const returnedUri = await rp.post(posting, {
- body: {
- uri: imageUri,
- name: returnedFilename,
- nosuffix,
- replaceRootFilename,
- },
- json: true,
- });
- return returnedUri;
- } catch (e) {
- console.log('VideoBox :' + e);
- }
- }
static _youtubeIframeCounter: number = 0;
static heightPercent = 80; // height of video relative to videoBox when timeline is open
@@ -107,7 +86,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@observable _scrubbing: boolean = false;
@computed get links() {
- return DocListCast(this.dataDoc.links);
+ return LinkManager.Links(this.dataDoc);
}
@computed get heightPercent() {
return NumCast(this.layoutDoc._timelineHeightPercent, 100);
@@ -240,10 +219,20 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
}
+ _keepCurrentlyPlaying = false; // flag to prevent document when paused from being removed from global 'currentlyPlaying' list
+ IsPlaying = () => this._playing;
+ TogglePause = () => {
+ if (!this._playing) this.Play();
+ else {
+ this._keepCurrentlyPlaying = true;
+ this.pause();
+ setTimeout(() => (this._keepCurrentlyPlaying = false));
+ }
+ };
+
// pauses video
- @action public Pause = (update: boolean = true) => {
+ @action public pause = (update: boolean = true) => {
this._playing = false;
- this.removeCurrentlyPlaying();
try {
update && this.player?.pause();
update && this._audioPlayer?.pause();
@@ -261,6 +250,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
this._playRegionTimer = undefined;
};
+ @action Pause = (update: boolean = true) => {
+ this.pause(update);
+ !this._keepCurrentlyPlaying && this.removeCurrentlyPlaying();
+ };
// toggles video full screen
@action public FullScreen = () => {
@@ -272,7 +265,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.player && this._contentRef && this._contentRef.requestFullscreen();
}
try {
- this._youtubePlayer && this.props.addDocTab(this.rootDoc, 'add');
+ this._youtubePlayer && this.props.addDocTab(this.rootDoc, OpenWhere.add);
} catch (e) {
console.log('Video FullScreen Exception:', e);
}
@@ -334,7 +327,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
_isLinkButton: true,
});
this.props.addDocument?.(b);
- DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, 'video snapshot');
+ DocUtils.MakeLink(b, this.rootDoc, { linkRelationship: 'video snapshot' });
Networking.PostToServer('/youtubeScreenshot', {
id: this.youtubeVideoId,
timecode: this.layoutDoc._currentTimecode,
@@ -342,7 +335,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const resolved = response?.accessPaths?.agnostic?.client;
if (resolved) {
this.props.removeDocument?.(b);
- this.createRealSummaryLink(resolved);
+ this.createSnapshotLink(resolved);
}
});
} else {
@@ -352,7 +345,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, '_'));
const filename = basename(encodedFilename);
- VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
+ Utils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
}
};
@@ -366,11 +359,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
};
// creates link for snapshot
- createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
+ createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => {
const url = !imagePath.startsWith('/') ? Utils.CorsProxy(imagePath) : imagePath;
const width = NumCast(this.layoutDoc._width) || 1;
const height = NumCast(this.layoutDoc._height);
- const imageSummary = Docs.Create.ImageDocument(url, {
+ const imageSnapshot = Docs.Create.ImageDocument(url, {
_nativeWidth: Doc.NativeWidth(this.layoutDoc),
_nativeHeight: Doc.NativeHeight(this.layoutDoc),
x: NumCast(this.layoutDoc.x) + width,
@@ -380,28 +373,34 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
_height: (height / width) * 150,
title: '--snapshot' + NumCast(this.layoutDoc._currentTimecode) + ' image-',
});
- Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
- Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
- this.props.addDocument?.(imageSummary);
- const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, 'video snapshot');
+ Doc.SetNativeWidth(Doc.GetProto(imageSnapshot), Doc.NativeWidth(this.layoutDoc));
+ Doc.SetNativeHeight(Doc.GetProto(imageSnapshot), Doc.NativeHeight(this.layoutDoc));
+ this.props.addDocument?.(imageSnapshot);
+ const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { linkRelationship: 'video snapshot' });
link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
- setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, 'move', true));
+ setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true));
};
- getAnchor = () => {
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
const timecode = Cast(this.layoutDoc._currentTimecode, 'number', null);
- const marquee = AnchorMenu.Instance.GetAnchor?.();
- return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow' /* videoStart */, '_timecodeToHide' /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
+ const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
+ if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent';
+ const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.rootDoc;
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc);
+ return anchor;
};
// sets video info on load
videoLoad = action(() => {
- const aspect = this.player!.videoWidth / this.player!.videoHeight;
- Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
- Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
- this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
+ const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1);
+ if (aspect && !this.isCropped) {
+ Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
+ Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
+ this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
+ }
if (Number.isFinite(this.player!.duration)) {
this.rawDuration = this.player!.duration;
+ this.dataDoc[this.fieldKey + '-duration'] = this.rawDuration;
} else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']);
});
@@ -416,6 +415,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
};
+ // getView = async (doc: Doc) => {
+ // return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+ // };
+
// extracts video thumbnails and saves them as field of doc
getVideoThumbnails = () => {
if (this.dataDoc.thumbnails !== undefined) return;
@@ -432,7 +435,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
canvas.getContext('2d')?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, 100, 100);
const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_'));
- thumbnailPromises?.push(VideoBox.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
+ thumbnailPromises?.push(Utils.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1);
if (newTime < video.duration) {
video.currentTime = newTime;
@@ -549,7 +552,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return !field ? (
<div key="loading">Loading</div>
) : (
- <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'pointer' }}>
+ <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'default' }}>
<div className={classname} ref={this.setContentRef} onPointerDown={e => this._fullScreen && e.stopPropagation()}>
{this._fullScreen && (
<div
@@ -568,7 +571,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
key="video"
autoPlay={this._screenCapture}
ref={this.setVideoRef}
- style={this._fullScreen ? this.fullScreenSize() : {}}
+ style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._viewScale)})`, transformOrigin: 'top left' } : {}}
onCanPlay={this.videoLoad}
controls={VideoBox._nativeControls}
onPlay={() => this.Play()}
@@ -691,23 +694,24 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
);
};
- // removes video from currently playing display
+ // removes from currently playing display
@action
removeCurrentlyPlaying = () => {
- if (CollectionStackedTimeline.CurrentlyPlaying) {
- const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
+ const docView = this.props.DocumentView?.();
+ if (CollectionStackedTimeline.CurrentlyPlaying && docView) {
+ const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView);
index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
}
};
-
- // adds video to currently playing display
+ // adds doc to currently playing display
@action
addCurrentlyPlaying = () => {
+ const docView = this.props.DocumentView?.();
if (!CollectionStackedTimeline.CurrentlyPlaying) {
CollectionStackedTimeline.CurrentlyPlaying = [];
}
- if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
- CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
+ if (docView && CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView) === -1) {
+ CollectionStackedTimeline.CurrentlyPlaying.push(docView);
}
};
@@ -831,16 +835,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// stretches vertically or horizontally depending on video orientation so video fits full screen
fullScreenSize() {
if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
- return { height: '100%' };
- } else {
- return { width: '100%' };
+ //prettier-ignore
+ return ({ height: '100%' });
}
+ //prettier-ignore
+ return ({ width: '100%' });
}
// for zoom slider, sets timeline waveform zoom
- zoom = (zoom: number) => {
- this.timeline?.setZoom(zoom);
- };
+ zoom = (zoom: number) => this.timeline?.setZoom(zoom);
// plays link
playLink = (doc: Doc) => {
@@ -854,7 +857,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// starts marquee selection
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._viewScale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
setupMoveUpEvents(
this,
e,
@@ -891,8 +894,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
playing = () => this._playing;
- contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
-
scaling = () => this.props.NativeDimScaling?.() || 1;
panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100;
@@ -913,137 +914,43 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// renders video controls
componentUI = (boundsLeft: number, boundsTop: number) => {
- const bounds = this.props.docViewPath().lastElement().getBounds();
- const left = bounds?.left || 0;
- const right = bounds?.right || 0;
- const top = bounds?.top || 0;
- const height = (bounds?.bottom || 0) - top;
- const width = Math.max(right - left, 100);
- const uiHeight = Math.max(25, Math.min(50, height / 10));
- const uiMargin = Math.min(10, height / 20);
- const vidHeight = (height * this.heightPercent) / 100;
- const yPos = top + vidHeight - uiHeight - uiMargin;
- const xPos = uiHeight / vidHeight > 0.4 ? right + 10 : left + 10;
+ const xf = this.props.ScreenToLocalTransform().inverse();
+ const height = this.props.PanelHeight();
+ const vidHeight = (height * this.heightPercent) / 100 / this.scaling();
+ const vidWidth = this.props.PanelWidth() / this.scaling();
+ const uiHeight = 25;
+ const uiMargin = 10;
+ const yBot = xf.transformPoint(0, vidHeight)[1];
+ // prettier-ignore
+ const yMid = (xf.transformPoint(0, 0)[1] +
+ xf.transformPoint(0, height / this.scaling())[1]) / 2;
+ const xPos = xf.transformPoint(vidWidth / 2, 0)[0];
+ const xRight = xf.transformPoint(vidWidth, 0)[0];
const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0;
- return this._fullScreen || right - left < 50 ? null : (
+ return this._fullScreen || this.isCropped || (xRight - xPos) * 2 < 50 ? null : (
<div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
- <div className="videoBox-ui" style={{ left: xPos, top: yPos, height: uiHeight, width: width - 20, transition: this._clicking ? 'top 0.5s' : '', opacity: opacity }}>
+ <div
+ className="videoBox-ui"
+ style={{
+ transformOrigin: 'top left',
+ transform: `rotate(${NumCast(this.rootDoc.rotation)}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`,
+ left: xPos,
+ top: yMid,
+ height: uiHeight,
+ width: (xRight - xPos) * 2 - 20,
+ transition: this._clicking ? 'top 0.5s' : '',
+ opacity,
+ }}>
{this.UIButtons}
</div>
</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));
- }}
- />
- </>
- )}
- </>
- );
- }
-
// renders CollectionStackedTimeline
@computed get renderTimeline() {
return (
- <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
+ <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%`, display: this.heightPercent === 100 ? 'none' : '' }}>
<CollectionStackedTimeline
ref={action((r: any) => (this._stackedTimeline = r))}
{...this.props}
@@ -1073,12 +980,63 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
</div>
);
}
+ @computed get isCropped() {
+ return this.dataDoc.videoCrop; // bcz: hack to identify a cropped video
+ }
// renders annotation layer
@computed get annotationLayer() {
return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
}
+ crop = (region: Doc | undefined, addCrop?: boolean) => {
+ if (!region) return;
+ const cropping = Doc.MakeCopy(region, true);
+ Doc.GetProto(region).backgroundColor = 'transparent';
+ Doc.GetProto(region).lockedPosition = true;
+ Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
+ Doc.GetProto(region).followLinkToggle = true;
+ region._timecodeToHide = NumCast(region._timecodeToShow) + 0.0001;
+ this.addDocument(region);
+ const anchx = NumCast(cropping.x);
+ const anchy = NumCast(cropping.y);
+ const anchw = NumCast(cropping._width);
+ const anchh = NumCast(cropping._height);
+ const viewScale = NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']) / anchw;
+ cropping.title = 'crop: ' + this.rootDoc.title;
+ cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width);
+ cropping.y = NumCast(this.rootDoc.y);
+ cropping._width = anchw * (this.props.NativeDimScaling?.() || 1);
+ cropping._height = anchh * (this.props.NativeDimScaling?.() || 1);
+ cropping.timecodeToHide = undefined;
+ cropping.timecodeToShow = undefined;
+ cropping.isLinkButton = undefined;
+ const croppingProto = Doc.GetProto(cropping);
+ croppingProto.annotationOn = undefined;
+ croppingProto.isPrototype = true;
+ croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
+ croppingProto.type = DocumentType.VID;
+ croppingProto.layout = VideoBox.LayoutString('data');
+ croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField);
+ croppingProto['data-nativeWidth'] = anchw;
+ croppingProto['data-nativeHeight'] = anchh;
+ croppingProto.videoCrop = true;
+ croppingProto.currentTimecode = this.layoutDoc._currentTimecode;
+ croppingProto.viewScale = viewScale;
+ croppingProto.viewScaleMin = viewScale;
+ croppingProto.panX = anchx / viewScale;
+ croppingProto.panY = anchy / viewScale;
+ croppingProto.panXMin = anchx / viewScale;
+ croppingProto.panXMax = anchw / viewScale;
+ croppingProto.panYMin = anchy / viewScale;
+ croppingProto.panYMax = anchh / viewScale;
+ if (addCrop) {
+ DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' });
+ }
+ this.props.bringToFront(cropping);
+ return cropping;
+ };
+
savedAnnotations = () => this._savedAnnotations;
render() {
const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
@@ -1108,7 +1066,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
left: (this.props.PanelWidth() - this.panelWidth()) / 2,
}}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
renderDepth={this.props.renderDepth + 1}
fieldKey={this.annotationKey}
CollectionView={undefined}
@@ -1116,6 +1077,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
annotationLayerHostsContent={true}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
+ isAnyChildContentActive={returnFalse}
ScreenToLocalTransform={this.screenToLocalTransform}
docFilters={this.timelineDocFilter}
select={emptyFunction}
@@ -1124,7 +1086,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
addDocument={this.addDocWithTimecode}>
- {this.contentFunc}
+ {this.youtubeVideoId ? this.youtubeContent : this.content}
</CollectionFreeFormView>
</div>
{this.annotationLayer}
@@ -1139,8 +1101,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
addDocument={this.addDocWithTimecode}
finishMarquee={this.finishMarquee}
savedAnnotations={this.savedAnnotations}
+ selectionText={returnEmptyString}
annotationLayer={this._annotationLayer.current}
mainCont={this._mainCont.current}
+ anchorMenuCrop={this.crop}
/>
)}
{this.renderTimeline}
@@ -1148,6 +1112,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;