diff options
| author | bobzel <zzzman@gmail.com> | 2023-04-05 22:44:03 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2023-04-05 22:44:03 -0400 |
| commit | 9b41da1af16b982ee8ac2fc09f2f8b5d67eac9fb (patch) | |
| tree | bc3f57cd5b31fd453d272c925f6d5b728ab63bae /src/client/views/nodes/VideoBox.tsx | |
| parent | 9dae453967183b294bf4f7444b948023a1d52d39 (diff) | |
| parent | 8f7e99641f84ad15f34ba9e4a60b664ac93d2e5d (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.tsx | 448 |
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; |
