diff options
author | usodhi <61431818+usodhi@users.noreply.github.com> | 2021-02-01 18:42:38 -0500 |
---|---|---|
committer | usodhi <61431818+usodhi@users.noreply.github.com> | 2021-02-01 18:42:38 -0500 |
commit | b160a00f80c4855e190d75f97482b5b019e77437 (patch) | |
tree | 4fd150d45a5c49b9d5572edcb663956c0b29dd60 /src/client/views/nodes/VideoBox.tsx | |
parent | 888fb3b3933e7aa48e9ac3abe85536328fcca336 (diff) | |
parent | 41bb365dd4f787aec2262dcb07508e0de3837e10 (diff) |
more merge
Diffstat (limited to 'src/client/views/nodes/VideoBox.tsx')
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 149 |
1 files changed, 77 insertions, 72 deletions
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index bfac7dc1c..8a1cefbd9 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -7,7 +7,7 @@ import { Dictionary } from "typescript-collections"; import { Doc, DocListCast } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { InkTool } from "../../../fields/InkField"; -import { createSchema, makeInterface } from "../../../fields/Schema"; +import { makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; import { emptyFunction, formatTime, OmitKeys, returnOne, setupMoveUpEvents, Utils } from "../../../Utils"; @@ -29,11 +29,8 @@ import { LinkDocPreview } from "./LinkDocPreview"; import "./VideoBox.scss"; const path = require('path'); -export const timeSchema = createSchema({ - _currentTimecode: "number", // the current time of a video or other linear, time-based document. Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first -}); -type VideoDocument = makeInterface<[typeof documentSchema, typeof timeSchema]>; -const VideoDocument = makeInterface(documentSchema, timeSchema); +type VideoDocument = makeInterface<[typeof documentSchema]>; +const VideoDocument = makeInterface(documentSchema); @observer export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) { @@ -55,16 +52,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @observable _marqueeing: number[] | undefined; @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); @observable _screenCapture = false; - @observable _visible: boolean = false; + @observable _clicking = false; @observable _forceCreateYouTubeIFrame = false; @observable _playTimer?: NodeJS.Timeout = undefined; @observable _fullScreen = false; @observable _playing = false; @computed get links() { return DocListCast(this.dataDoc.links); } - @computed get heightPercent() { return this.layoutDoc._timelineShow ? NumCast(this.layoutDoc._videoTimelineHeightPercent, VideoBox.heightPercent) : 100; } + @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } @computed get duration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); } - @computed get anchorDocs() { return DocListCast(this.dataDoc[this.annotationKey + "-timeline"]).concat(DocListCast(this.dataDoc[this.annotationKey])); } + private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } public get player(): HTMLVideoElement | null { return this._videoRef; } constructor(props: Readonly<FieldViewProps>) { @@ -72,11 +69,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD VideoBox.Instance = this; } - anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart))); - anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal))); - getAnchor = () => { - return this._stackedTimeline.current?.createAnchor(Cast(this.layoutDoc._currentTimecode, "number", null)) || this.rootDoc; + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey + "-timeline", "videoStart", "videoEnd", Cast(this.layoutDoc._currentTimecode, "number", null)) || this.rootDoc; } choosePath(url: string) { @@ -91,13 +85,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this.dataDoc[this.fieldKey + "-duration"] = this.player!.duration; } - static keyEventsWrapper = (e: KeyboardEvent) => { - VideoBox.Instance._stackedTimeline.current?.keyEvents(e); - } - @action public Play = (update: boolean = true) => { - document.removeEventListener("keydown", VideoBox.keyEventsWrapper, true); - document.addEventListener("keydown", VideoBox.keyEventsWrapper, true); this._playing = true; try { update && this.player?.play(); @@ -179,7 +167,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD //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 filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.rootDoc.title).replace(/\..*$/, "") + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"))); + const retitled = StrCast(this.rootDoc.title).replace(/[ -\.]/g, ""); + const filename = path.basename(encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"))); VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && this.createRealSummaryLink(returnedFilename)); } @@ -191,14 +180,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const height = this.layoutDoc._height || 0; const imageSummary = Docs.Create.ImageDocument(url, { _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc), - x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), + x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), isLinkButton: true, _width: 150, _height: height / width * 150, title: "--snapshot" + (this.layoutDoc._currentTimecode || 0) + " image-" }); Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc)); Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc)); - imageSummary.isLinkButton = true; this.props.addDocument?.(imageSummary); - DocUtils.MakeLink({ doc: imageSummary }, { doc: this.rootDoc }, "video snapshot"); + DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot"); } @action @@ -253,7 +241,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD componentWillUnmount() { this.Pause(); Object.keys(this._disposers).forEach(d => this._disposers[d]?.()); - document.removeEventListener("keydown", VideoBox.keyEventsWrapper, true); } @action @@ -310,8 +297,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const field = Cast(this.dataDoc[this.fieldKey], VideoField); const interactive = Doc.GetSelectedTool() !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; - return !field ? <div>Loading</div> : - <div className="container" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }}> + return !field ? <div key="loading">Loading</div> : + <div className="container" key="container" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }}> <div className={`${style}`} style={{ width: "100%", height: "100%", left: "0px" }}> <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }} @@ -380,16 +367,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD <span>{"" + formatTime(curTime)}</span> <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> </div>, - <div className="videoBox-snapshot" key="snap" onPointerDown={this.onSnapshot} > + <div className="videoBox-snapshot" key="snap" onClick={this.onSnapshot} > <FontAwesomeIcon icon="camera" size="lg" /> </div>, - <div className="timeline-button" key="timeline-button" onPointerDown={action(e => this.layoutDoc._timelineShow = !this.layoutDoc._timelineShow)} - style={{ - transform: `scale(${this.scaling()})`, - right: this.scaling() * 10 - 10, - bottom: this.scaling() * 10 - 10 - }}> - <FontAwesomeIcon icon={this.layoutDoc._timelineShow ? "eye-slash" : "eye"} style={{ width: "100%" }} /> + <div className="videoBox-timelineButton" key="timeline" onPointerDown={this.onTimelineHdlDown} style={{ bottom: `${100 - this.heightPercent}%` }}> + <FontAwesomeIcon icon={"eye"} size="lg" /> </div>, VideoBox._showControls ? (null) : [ // <div className="control-background"> @@ -411,18 +393,37 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD e.preventDefault(); } - onSnapshot = (e: React.PointerEvent) => { + onSnapshot = (e: React.MouseEvent) => { this.Snapshot(); e.stopPropagation(); e.preventDefault(); } + onTimelineHdlDown = action((e: React.PointerEvent) => { + this._clicking = true; + setupMoveUpEvents(this, e, + action((e: PointerEvent) => { + this._clicking = false; + if (this.active()) { + const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY); + this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100)); + } + return false; + }), emptyFunction, + () => { + this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent; + setTimeout(action(() => this._clicking = false), 500); + }, this.active(), this.active()); + }); + onResetDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, (e: PointerEvent) => { - this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333)); - e.stopImmediatePropagation(); - return false; - }, emptyFunction, + setupMoveUpEvents(this, e, + (e: PointerEvent) => { + this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333)); + e.stopImmediatePropagation(); + return false; + }, + emptyFunction, (e: PointerEvent) => this.layoutDoc._currentTimecode = 0); } @@ -441,7 +442,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD addDocWithTimecode(doc: Doc | Doc[]): boolean { const docs = doc instanceof Doc ? [doc] : doc; const curTime = NumCast(this.layoutDoc._currentTimecode); - docs.forEach(doc => doc._timecodeToShow = curTime); + docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1); return this.addDocument(doc); } @@ -473,62 +474,54 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } playLink = (doc: Doc) => { - const startTime = NumCast(doc.anchorStartTime, NumCast(doc._timecodeToShow)); - const endTime = NumCast(doc.anchorEndTime, NumCast(doc._timecodeToHide, null)); + const startTime = this._stackedTimeline.current?.anchorStart(doc) || 0; + const endTime = this._stackedTimeline.current?.anchorEnd(doc); if (startTime !== undefined) { if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); else this.Seek(startTime); } } - // returns the timeline + playing = () => this._playing; + isActiveChild = () => this._isChildActive; + timelineWhenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(runInAction(() => this._isChildActive = isActive)); + timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight()); + setAnchorTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time; + timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100; @computed get renderTimeline() { - return <div style={{ width: "100%", height: `${100 - this.heightPercent}%`, position: "absolute" }}> - <CollectionStackedTimeline ref={this._stackedTimeline} - Document={this.props.Document} + return <div style={{ width: "100%", transition: this.transition, height: `${100 - this.heightPercent}%`, position: "absolute" }}> + <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props} fieldKey={this.annotationKey} renderDepth={this.props.renderDepth + 1} - parentActive={this.props.parentActive} + startTag={"videoStart"} + endTag={"videoEnd"} + fieldKeySuffix={"-timeline"} focus={emptyFunction} - styleProvider={this.props.styleProvider} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} - searchFilterDocs={this.props.searchFilterDocs} - rootSelected={this.props.rootSelected} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} bringToFront={emptyFunction} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - ContainingCollectionView={this.props.ContainingCollectionView} CollectionView={undefined} duration={this.duration} playFrom={this.playFrom} - setTime={(time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time} - playing={() => this._playing} - select={this.props.select} - isSelected={this.props.isSelected} - whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))} + setTime={this.setAnchorTime} + playing={this.playing} + whenActiveChanged={this.timelineWhenActiveChanged} removeDocument={this.removeDocument} - ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight())} - isChildActive={() => this._isChildActive} + ScreenToLocalTransform={this.timelineScreenToLocal} + isChildActive={this.isActiveChild} Play={this.Play} Pause={this.Pause} active={this.active} playLink={this.playLink} - PanelWidth={this.props.PanelWidth} - PanelHeight={() => this.props.PanelHeight() * (100 - this.heightPercent) / 100} + PanelHeight={this.timelineHeight} /> </div>; } - contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; - @computed get annotationLayer() { - return <div className="imageBox-annotationLayer" style={{ height: `${this.heightPercent}%` }} ref={this._annotationLayer} />; + return <div className="imageBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />; } marqueeDown = action((e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.active(true)) this._marqueeing = [e.clientX, e.clientY]; + if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.active(true)) this._marqueeing = [e.clientX, e.clientY]; }); finishMarquee = action(() => { @@ -536,6 +529,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this.props.select(true); }); + contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; scaling = () => this.props.scaling?.() || 1; panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100; panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / Doc.NativeAspect(this.rootDoc) : this.props.PanelHeight() * this.heightPercent / 100; @@ -543,6 +537,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent); } + marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100; + marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0]; render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); @@ -553,7 +549,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD borderRadius }} > <div className="videoBox-viewer" onPointerDown={this.marqueeDown} > - <div style={{ position: "absolute", width: this.panelWidth(), height: this.panelHeight(), top: 0, left: `${(100 - this.heightPercent) / 2}%` }}> + <div style={{ position: "absolute", transition: this.transition, width: this.panelWidth(), height: this.panelHeight(), top: 0, left: `${(100 - this.heightPercent) / 2}%` }}> <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} fieldKey={this.annotationKey} isAnnotationOverlay={true} @@ -577,7 +573,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD {this.annotationLayer} {this.renderTimeline} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocWithTimecode} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} + <MarqueeAnnotator + rootDoc={this.rootDoc} + down={this._marqueeing} + scaling={this.marqueeFitScaling} + containerOffset={this.marqueeOffset} + addDocument={this.addDocWithTimecode} + finishMarquee={this.finishMarquee} + savedAnnotations={this._savedAnnotations} + annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} + />} </div> </div >); } |