diff options
Diffstat (limited to 'src/client/views/nodes/VideoBox.tsx')
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 1627 |
1 files changed, 866 insertions, 761 deletions
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 680c5f2a9..e833c7e30 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -6,8 +6,9 @@ import { basename } from "path"; import * as rp from 'request-promise'; import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc"; import { InkTool } from "../../../fields/InkField"; +import { List } from "../../../fields/List"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { AudioField, ImageField, RecordingField, VideoField } from "../../../fields/URLField"; +import { AudioField, ImageField, VideoField } from "../../../fields/URLField"; import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; @@ -33,7 +34,6 @@ import { RecordingBox } from "./RecordingBox"; import { ReplayMovements } from "../../util/ReplayMovements"; const path = require('path'); - /** * VideoBox * Main component: VideoBox.tsx @@ -77,6 +77,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp static _youtubeIframeCounter: number = 0; static heightPercent = 80; // height of video relative to videoBox when timeline is open + static numThumbnails = 20; private _disposers: { [name: string]: IReactionDisposer } = {}; private _youtubePlayer: YT.Player | undefined = undefined; private _videoRef: HTMLVideoElement | null = null; // <video> ref @@ -87,6 +88,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _playRegionTimer: any = null; // timeout for playback + private _controlsFadeTimer: any = null; // timeout for controls fade + @observable _stackedTimeline: any; // CollectionStackedTimeline ref @observable static _nativeControls: boolean; // default html controls @observable _marqueeing: number[] | undefined; // coords for marquee selection @@ -100,6 +103,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable _finished: boolean = false; // has playback reached end of clip @observable _volume: number = 1; @observable _muted: boolean = false; + @observable _controlsTransform?: { X: number, Y: number }; + @observable _controlsVisible: boolean = true; + @observable _scrubbing: boolean = false; @computed get links() { return DocListCast(this.dataDoc.links); } @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height @@ -121,815 +127,914 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } // returns the presentation data if it exists, null otherwise - @computed get presentation(): Presentation | null { - const data = this.dataDoc[this.fieldKey + '-presentation']; - return data ? JSON.parse(data) : null; - } - + @computed get presentation() { + const data = this.dataDoc[this.fieldKey + '-presentation']; + return data ? JSON.parse(data) : null; + } - @computed private get timeline() { return this._stackedTimeline; } - private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline - public get player(): HTMLVideoElement | null { return this._videoRef; } + @computed private get timeline() { return this._stackedTimeline; } + private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline + public get player(): HTMLVideoElement | null { return this._videoRef; } componentDidMount() { - this._playing = 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.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(0); - - // if presentation data exists, pass it to ReplayMovments - if (this.presentation != null) { - ReplayMovements.Instance.setVideoBox(this); - } - } - - componentWillUnmount() { + } + this.player && this.setPlayheadTime(0); + document.addEventListener("keydown", this.keyEvents, true); + + if (this.presentation) { + ReplayMovements.Instance.setVideoBox(this); + } + } + + componentWillUnmount() { this.removeCurrentlyPlaying(); this.Pause(); - Object.keys(this._disposers).forEach(d => this._disposers[d]?.()); - - // tell ReplayMovements that this VideoBox is no longer active - if (this.presentation != null) { - ReplayMovements.Instance.removeVideoBox(); - } - } + Object.keys(this._disposers).forEach(d => this._disposers[d]?.()); + document.removeEventListener("keydown", this.keyEvents, true); + if (this.presentation) { + ReplayMovements.Instance.removeVideoBox(); + } + } + + // handles key events, when timeline scrubs fade controls + @action + keyEvents = (e: KeyboardEvent) => { + if ( + // need to include range inputs because after dragging time slider it becomes target element + !(e.target instanceof HTMLInputElement && !(e.target.type === "range")) && + this.props.isSelected(true) + ) { + switch (e.key) { + case "ArrowLeft": + case "ArrowRight": + clearTimeout(this._controlsFadeTimer); + this._scrubbing = true; + this._controlsFadeTimer = setTimeout(action(() => this._scrubbing = false), 500); + e.stopPropagation(); + break; + } + } + } - // plays video + // plays video @action public Play = (update: boolean = true) => { - // BUGFIXES: only run if video is not already playing & dont set isplaying to true until that async call - if (this._playing) return; - // requires a promise for native videoplayer to be set to true - if (!this.player) this._playing = true; - const eleTime = this.player?.currentTime || 0; - if (this.timeline) { - let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime; - - if (this._finished) { - // restarts video if reached end on previous play - this._finished = false; - console.log('trim start', this.timeline.trimStart); - start = this.timeline.trimStart; - } - - try { - 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); - } + if (this._playRegionTimer) return; + + this._playing = true; + const eleTime = this.player?.currentTime || 0; + if (this.timeline) { + let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime; + + if (this._finished) { + // restarts video if reached end on previous play + this._finished = false; + start = this.timeline.trimStart; } - this.updateTimecode(); - } - // goes to time - @action public Seek(time: number) { try { - this._youtubePlayer?.seekTo(Math.round(time), true); + 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 Seek Exception:", e); - } - this.player && (this.player.currentTime = time); - this._audioPlayer && (this._audioPlayer.currentTime = time); - // TODO: revisit this and clean it - console.log('seek', time); - if ((this.player?.currentTime || -1) < this.rawDuration) { - this._finished = false; + console.log("Video Play Exception:", e); } - } + } + this.updateTimecode(); + } + + // 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 + if ((this.player?.currentTime || -1) < this.rawDuration) { + this._finished = false; + } + } - // pauses video + // pauses video @action public Pause = (update: boolean = true) => { - if (!this._playing) return; - this._playing = false; - this.removeCurrentlyPlaying(); - 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) clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play - } - - // toggles video full screen - @action public FullScreen = () => { - if (document.fullscreenElement === this._contentRef) { - this._fullScreen = false; - this.player && this._contentRef && document.exitFullscreen(); - } - else { - this._fullScreen = true; - this.player && this._contentRef && this._contentRef.requestFullscreen(); + this._playing = false; + this.removeCurrentlyPlaying(); + 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) { + clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play + } + this._playRegionTimer = undefined; + } - } - try { - this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add"); - } catch (e) { - console.log("Video FullScreen Exception:", e); - } - } + // toggles video full screen + @action public FullScreen = () => { + if (document.fullscreenElement === this._contentRef) { + this._fullScreen = false; + this.player && this._contentRef && document.exitFullscreen(); + } + else { + this._fullScreen = true; + this.player && this._contentRef && this._contentRef.requestFullscreen(); + } + try { + this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add"); + } catch (e) { + console.log("Video FullScreen Exception:", e); + } + } + // fades out controls in fullscreen after mouse stops moving + @action controlsFade = (e: PointerEvent) => { + e.stopPropagation(); + if (!this._scrubbing) { + clearTimeout(this._controlsFadeTimer); + this._controlsVisible = true; + this._controlsFadeTimer = setTimeout(action(() => this._controlsVisible = false), 3000); + } + } - // creates and links snapshot photo of current video frame - @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => { - const width = NumCast(this.layoutDoc._width); - const canvas = document.createElement('canvas'); - canvas.width = 640; - canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1); - const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions - if (ctx) { - this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); - } - if (!this._videoRef) { - const b = Docs.Create.LabelDocument({ - x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y, 1), - _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(), - _isLinkButton: true - }); - this.props.addDocument?.(b); - DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot"); - Networking.PostToServer("/youtubeScreenshot", { - id: this.youtubeVideoId, - timecode: this.layoutDoc._currentTimecode - }).then(response => { - const resolved = response?.accessPaths?.agnostic?.client; - if (resolved) { - this.props.removeDocument?.(b); - this.createRealSummaryLink(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.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)); - } - } - - updateIcon = () => { - const makeIcon = (returnedfilename: string) => { - this.dataDoc.icon = new ImageField(returnedfilename); - this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym](); - this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym](); - }; - this.Snapshot(undefined, undefined, makeIcon); - } - - // creates link for snapshot - createRealSummaryLink = (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, { - _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc), - x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _isLinkButton: true, - _width: 150, _height: height / width * 150, title: "--snapshot" + NumCast(this.layoutDoc._currentTimecode) + " image-" + // drag controls around window in fulls screen + @action controlsDrag = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + const eleStyle = getComputedStyle(e.target as Element); + this._controlsTransform = { X: parseInt(eleStyle.left), Y: parseInt(eleStyle.top) }; + + setupMoveUpEvents(e.target, + e, + action((e, down, delta) => { + if (this._controlsTransform) { + this._controlsTransform.X = Math.max(0, Math.min(delta[0] + this._controlsTransform.X, window.innerWidth)); + this._controlsTransform.Y = Math.max(0, Math.min(delta[1] + this._controlsTransform.Y, window.innerHeight)); + } + return false; + }), + emptyFunction, + emptyFunction) + } + + + // creates and links snapshot photo of current video frame + @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => { + const width = NumCast(this.layoutDoc._width); + const canvas = document.createElement('canvas'); + canvas.width = 640; + canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1); + const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions + if (ctx) { + this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); + } + + if (!this._videoRef) { + const b = Docs.Create.LabelDocument({ + x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y, 1), + _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(), + _isLinkButton: true }); - 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"); - 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)); - } - - - getAnchor = () => { - 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; - } - - - // 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; - if (Number.isFinite(this.player!.duration)) { - this.rawDuration = this.player!.duration; - } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + "-duration"]); - }); - - - // updates video time - @action - updateTimecode = (e? : any) => { - // BUGFIX: if seeking when paused, no longer finished - // ok this is constantly fired all the time - if (e && !this._playing && this.player && this.player.paused) { - // console.log('updateTimecode: seeking when paused', e); - this.player.currentTime = e.target.currentTime; - this._finished = false; - } - this.player && (this.layoutDoc._currentTimecode = this.player.currentTime); - try { - this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.()); - } catch (e) { - console.log("Video Timecode Exception:", e); - } - } - - - // sets video element ref - @action - setVideoRef = (vref: HTMLVideoElement | null) => { - this._videoRef = vref; - if (vref) { - this._videoRef!.ontimeupdate = this.updateTimecode; - // @ts-ignore - // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); - this._disposers.reactionDisposer?.(); - this._disposers.reactionDisposer = reaction(() => NumCast(this.layoutDoc._currentTimecode), - time => { - !this._playing && (vref.currentTime = time); - // console.log("vref time = " + vref.currentTime) - }, { fireImmediately: true }); - } - } - - // set ref for div that wraps video and controls for fullscreen - @action - setContentRef = (cref: HTMLDivElement | null) => { - this._contentRef = cref; - if (cref) { - cref.onfullscreenchange = action((e) => this._fullScreen = (document.fullscreenElement === cref)); - } - } - - - // context menu - specificContextMenu = (e: React.MouseEvent): void => { - const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); - if (field) { - const url = field.url.href; - const subitems: ContextMenuProps[] = []; - subitems.push({ description: "Full Screen", event: this.FullScreen, icon: "expand" }); - subitems.push({ description: "Take Snapshot", event: this.Snapshot, icon: "expand-arrows-alt" }); - this.rootDoc.type === DocumentType.SCREENSHOT && subitems.push({ - description: "Screen Capture", event: (async () => { - runInAction(() => this._screenCapture = !this._screenCapture); - this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); - }), icon: "expand-arrows-alt" - }); - subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? "" : "Don't") + " follow links when encountered", event: () => this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks, icon: "expand-arrows-alt" }); - subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, 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" }); - subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" }); - // if the videobox was turned from a recording box - if (this.dataDoc[this.fieldKey + "-recorded"] === true) { - subitems.push({ - description: "Recreate recording", event: () => { - this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey); - // delete assoicated video data - this.dataDoc[this.props.fieldKey] = ""; - this.dataDoc[this.fieldKey + "-duration"] = ""; - // delete assoicated presentation data - this.dataDoc[this.fieldKey + "-presentation"] = ""; - }, icon: "expand-arrows-alt" - }); - - } - ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); - } - } - - - // ref for updating time - setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e; - - // renders the video and audio - @computed get content() { - const field = Cast(this.dataDoc[this.fieldKey], VideoField); - const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; - const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; - return !field ? <div key="loading">Loading</div> : - <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}> - <div className={classname} ref={this.setContentRef} onPointerDown={(e) => this._fullScreen && e.stopPropagation()}> - {this.uIButtons} - <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}} - onCanPlay={this.videoLoad} - controls={VideoBox._nativeControls} - onPlay={() => this.Play()} - onSeeked={this.updateTimecode} - onPause={() => this.Pause() } - onClick={e => e.preventDefault()}> - <source src={field.url.href} type="video/mp4" /> - Not supported. - </video> - {!this.audiopath || this.audiopath === field.url.href ? (null) : - <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}> - <source src={this.audiopath} type="audio/mpeg" /> - Not supported. - </audio>} - </div> - </div>; - } - - - @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); + this.props.addDocument?.(b); + DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot"); + Networking.PostToServer("/youtubeScreenshot", { + id: this.youtubeVideoId, + timecode: this.layoutDoc._currentTimecode + }).then(response => { + const resolved = response?.accessPaths?.agnostic?.client; + if (resolved) { + this.props.removeDocument?.(b); + this.createRealSummaryLink(resolved); + } }); - const onYoutubePlayerReady = (event: any) => { - this._disposers.reactionDisposer?.(); - this._disposers.youtubeReactionDisposer?.(); - this._disposers.reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode))); - this._disposers.youtubeReactionDisposer = reaction( - () => CurrentUserUtils.SelectedTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, - (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 = () => { - console.log('onPlayDown', this._playing); - this._playing ? this.Pause() : this.Play(); - } - - // for fullscreen button - onFullDown = (e: React.PointerEvent) => { - this.FullScreen(); - e.stopPropagation(); - e.preventDefault(); - } - - // for snapshot button - onSnapshotDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, (e) => { - this.Snapshot(e.clientX, e.clientY); - return true; - }, emptyFunction, () => this.Snapshot()); - } - - // for show/hide timeline button, transitions between show/hide - @action - onTimelineHdlDown = (e: React.PointerEvent) => { - this._clicking = true; - setupMoveUpEvents(this, e, - action(encodeURIComponent => { - this._clicking = false; - if (this.props.isContentActive()) { - // 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)); - - this.layoutDoc._timelineHeightPercent = 80; - } - return false; - }), emptyFunction, - () => { - this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent; - setTimeout(action(() => this._clicking = false), 500); - }, this.props.isContentActive(), this.props.isContentActive()); - } - - - // removes video from currently playing display - @action - removeCurrentlyPlaying = () => { - if (CollectionStackedTimeline.CurrentlyPlaying) { - const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc); - index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1); - } - } + } 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.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)); + } + } + + updateIcon = () => { + const makeIcon = (returnedfilename: string) => { + this.dataDoc.icon = new ImageField(returnedfilename); + this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym](); + this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym](); + }; + this.Snapshot(undefined, undefined, makeIcon); + } + + // creates link for snapshot + createRealSummaryLink = (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, { + _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc), + x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _isLinkButton: true, + _width: 150, _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"); + 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)); + } + + + getAnchor = () => { + 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; + } + + + // 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; + if (Number.isFinite(this.player!.duration)) { + this.rawDuration = this.player!.duration; + } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + "-duration"]); + }); - // adds video to currently playing display - @action - addCurrentlyPlaying = () => { - if (!CollectionStackedTimeline.CurrentlyPlaying) { - CollectionStackedTimeline.CurrentlyPlaying = []; + + // updates video time + @action + updateTimecode = () => { + this.player && (this.layoutDoc._currentTimecode = this.player.currentTime); + try { + this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.()); + } catch (e) { + console.log("Video Timecode Exception:", e); + } + } + + + // extracts video thumbnails and saves them as field of doc + getVideoThumbnails = () => { + const video = document.createElement('video'); + const thumbnailPromises: Promise<any>[] = []; + + video.onloadedmetadata = () => { + video.currentTime = 0; + }; + + video.onseeked = () => { + const canvas = document.createElement('canvas'); + canvas.height = video.videoHeight; + canvas.width = video.videoWidth; + const ctx = canvas.getContext('2d'); + ctx?.drawImage(video, 0, 0, canvas.width, canvas.height); + const imgUrl = canvas.toDataURL(); + const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, ""); + const encodedFilename = encodeURIComponent("thumbnail" + retitled + "_" + video.currentTime.toString().replace(/\./, "_")); + const filename = basename(encodedFilename); + thumbnailPromises.push(VideoBox.convertDataUri(imgUrl, filename)); + const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1); + if (newTime < video.duration) { + video.currentTime = newTime; } - if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) { - CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc); + else { + Promise.all(thumbnailPromises).then(thumbnails => { this.dataDoc.thumbnails = new List<string>(thumbnails); }); } - } - - - @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._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 { - const docs = doc instanceof Doc ? [doc] : doc; - const curTime = NumCast(this.layoutDoc._currentTimecode); - docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1); - return this.addDocument(doc); - } - + } + + const field = Cast(this.dataDoc[this.fieldKey], VideoField); + field && (video.src = field.url.href); + } + + + // sets video element ref @action - private setPlaying = (b: boolean) => { - this._playing = b; + setVideoRef = (vref: HTMLVideoElement | null) => { + this._videoRef = vref; + if (vref) { + this._videoRef!.ontimeupdate = this.updateTimecode; + // @ts-ignore + // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); + this._disposers.reactionDisposer?.(); + this._disposers.reactionDisposer = reaction(() => NumCast(this.layoutDoc._currentTimecode), + time => !this._playing && (vref.currentTime = time), { fireImmediately: true }); + + (!this.dataDoc.thumbnails || this.dataDoc.thumbnails.length != VideoBox.numThumbnails) && this.getVideoThumbnails(); + } + } + + // set ref for div that wraps video and controls for fullscreen + @action + setContentRef = (cref: HTMLDivElement | null) => { + this._contentRef = cref; + if (cref) { + cref.onfullscreenchange = action((e) => { + this._fullScreen = (document.fullscreenElement === cref); + this._controlsVisible = true; + this._scrubbing = false; + clearTimeout(this._controlsFadeTimer); + if (this._fullScreen) { + document.addEventListener('pointermove', this.controlsFade); + } + else { + document.removeEventListener('pointermove', this.controlsFade); + } + }); + } } - // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range - @action - playFrom = async (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => { - clearTimeout(this._playRegionTimer); - if (Number.isNaN(this.player?.duration)) { - setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); + // context menu + specificContextMenu = (e: React.MouseEvent): void => { + const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); + if (field) { + const url = field.url.href; + const subitems: ContextMenuProps[] = []; + subitems.push({ description: "Full Screen", event: this.FullScreen, icon: "expand" }); + subitems.push({ description: "Take Snapshot", event: this.Snapshot, icon: "expand-arrows-alt" }); + this.rootDoc.type === DocumentType.SCREENSHOT && subitems.push({ + description: "Screen Capture", event: (async () => { + runInAction(() => this._screenCapture = !this._screenCapture); + this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); + }), icon: "expand-arrows-alt" + }); + subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? "" : "Don't") + " follow links when encountered", event: () => this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks, icon: "expand-arrows-alt" }); + subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, 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" }); + subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" }); + // if the videobox was turned from a recording box + if (this.dataDoc[this.fieldKey + "-recorded"] === true) { + subitems.push({ + description: "Recreate recording", event: () => { + this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey); + // delete assoicated video data + this.dataDoc[this.props.fieldKey] = ""; + this.dataDoc[this.fieldKey + "-duration"] = ""; + // delete assoicated presentation data + this.dataDoc[this.fieldKey + "-presentation"] = ""; + }, icon: "expand-arrows-alt" + }); } - else if (this.player) { - // trimBounds override requested playback bounds - const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration); - const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds); - const playRegionDuration = end - start; - // checks if times are within clip range - if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.rawDuration)) { - this.player.currentTime = start; - this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds); - // BUGFIX: video.play is async, so we need to wait for it to finish - console.log('playFrom'); - this.player.play().then(() => { - this._audioPlayer?.play(); - this.setPlaying(true); - this.addCurrentlyPlaying(); - this._playRegionTimer = setTimeout( - () => { - // need to keep track of if end of clip is reached so on next play, clip restarts - if (fullPlay) { - this._finished = true; - // if (this.player) { - // this.player.currentTime = 0; - // this.updateTimecode(); - // } - - } - // removes from currently playing if playback has reached end of range marker - else this.removeCurrentlyPlaying(); - this.Pause(); - }, playRegionDuration * 1000); - }); - } else { - this.Pause(); - } + ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); + } + } + + + // ref for updating time + setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e; + + // renders the video and audio + @computed get content() { + const field = Cast(this.dataDoc[this.fieldKey], VideoField); + const interactive = CurrentUserUtils.ActiveTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; + const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; + const opacity = this._scrubbing ? 0.3 : (this._controlsVisible ? 1 : 0); + 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={classname} ref={this.setContentRef} onPointerDown={(e) => this._fullScreen && e.stopPropagation()}> + {this._fullScreen && <div className="videoBox-ui" onPointerDown={this.controlsDrag} + style={{ left: this._controlsTransform && this._controlsTransform.X, top: this._controlsTransform && this._controlsTransform.Y, visibility: this._controlsVisible || this._scrubbing ? 'visible' : 'hidden', opacity: opacity }}> + {this.UIButtons} + </div>} + <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}} + onCanPlay={this.videoLoad} + controls={VideoBox._nativeControls} + onPlay={() => this.Play()} + onSeeked={this.updateTimecode} + onPause={() => this.Pause()} + onClick={this._fullScreen ? () => this.playing() ? this.Pause() : this.Play() : e => e.preventDefault()}> + <source src={field.url.href} type="video/mp4" /> + Not supported. + </video> + {!this.audiopath || this.audiopath === field.url.href ? (null) : + <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}> + <source src={this.audiopath} type="audio/mpeg" /> + Not supported. + </audio>} + </div> + </div>; + } + + + @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; } - } - - - // ends trim, hides trim controls and displays new clip - @undoBatch - finishTrim = action(() => { - this.Pause(); - this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this.player!.currentTime), this.timeline?.trimStart || 0)); - this.timeline?.StopTrimming(); - }); - - // displays trim controls to start trimming clip - startTrim = (scope: TrimScope) => { - this.Pause(); - this.timeline?.StartTrimming(scope); - } - - // for trim button, double click displays full clip, single displays curr trim bounds - onClipPointerDown = (e: React.PointerEvent) => { - // if timeline isn't shown, show first then trim - this.heightPercent >= 100 && this.onTimelineHdlDown(e); - this.timeline && setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => { - if (doubleTap) { - this.startTrim(TrimScope.All); - } else if (this.timeline) { + 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._currentTimecode, () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode))); + this._disposers.youtubeReactionDisposer = reaction( + () => CurrentUserUtils.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, + (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(); + + // for fullscreen button + onFullDown = (e: React.PointerEvent) => { + this.FullScreen(); + e.stopPropagation(); + e.preventDefault(); + } + + // for snapshot button + onSnapshotDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, (e) => { + this.Snapshot(e.clientX, e.clientY); + return true; + }, emptyFunction, () => this.Snapshot()); + } + + // for show/hide timeline button, transitions between show/hide + @action + onTimelineHdlDown = (e: React.PointerEvent) => { + this._clicking = true; + setupMoveUpEvents(this, e, + action(encodeURIComponent => { + this._clicking = false; + if (this.props.isContentActive()) { + // 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)); + + this.layoutDoc._timelineHeightPercent = 80; + } + return false; + }), emptyFunction, + () => { + this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent; + setTimeout(action(() => this._clicking = false), 500); + }, this.props.isContentActive(), this.props.isContentActive()); + } + + + // removes video from currently playing display + @action + removeCurrentlyPlaying = () => { + if (CollectionStackedTimeline.CurrentlyPlaying) { + const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc); + index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1); + } + } + + // adds video to currently playing display + @action + addCurrentlyPlaying = () => { + if (!CollectionStackedTimeline.CurrentlyPlaying) { + CollectionStackedTimeline.CurrentlyPlaying = []; + } + if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) { + CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc); + } + } + + + @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._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 { + const docs = doc instanceof Doc ? [doc] : doc; + const curTime = NumCast(this.layoutDoc._currentTimecode); + docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1); + return this.addDocument(doc); + } + + + // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range + @action + playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => { + clearTimeout(this._playRegionTimer); + this._playRegionTimer = undefined; + if (Number.isNaN(this.player?.duration)) { + setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); + } + else if (this.player) { + // trimBounds override requested playback bounds + const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration); + const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds); + const playRegionDuration = end - start; + // checks if times are within clip range + if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.rawDuration)) { + this.player.currentTime = start; + this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds); + this.player.play(); + this._audioPlayer?.play(); + this._playing = true; + this.addCurrentlyPlaying(); + this._playRegionTimer = setTimeout( + () => { + // need to keep track of if end of clip is reached so on next play, clip restarts + if (fullPlay) this._finished = true; + // removes from currently playing if playback has reached end of range marker + else this.removeCurrentlyPlaying(); this.Pause(); - this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip); - } - })); - } - - - // for volume slider sets volume - @action - setVolume = (volume: number) => { - if (this.player) { - this._volume = volume; - this.player.volume = volume; - if (this._muted) { - this.toggleMute(); - } - } - } - - // toggles video mute - @action - toggleMute = () => { - if (this.player) { - this._muted = !this._muted; - this.player.muted = this._muted; + }, playRegionDuration * 1000); + } else { + this.Pause(); } - } + } + } - // 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%" }; + // ends trim, hides trim controls and displays new clip + @undoBatch + finishTrim = action(() => { + this.Pause(); + this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this.player!.currentTime), this.timeline?.trimStart || 0)); + this.timeline?.StopTrimming(); + }); + + // displays trim controls to start trimming clip + startTrim = (scope: TrimScope) => { + this.Pause(); + this.timeline?.StartTrimming(scope); + } + + // for trim button, double click displays full clip, single displays curr trim bounds + onClipPointerDown = (e: React.PointerEvent) => { + // if timeline isn't shown, show first then trim + this.heightPercent >= 100 && this.onTimelineHdlDown(e); + this.timeline && setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => { + if (doubleTap) { + this.startTrim(TrimScope.All); + } else if (this.timeline) { + this.Pause(); + this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip); } - else { - return { width: "100%" }; + })); + } + + + // for volume slider sets volume + @action + setVolume = (volume: number) => { + if (this.player) { + this._volume = volume; + this.player.volume = volume; + if (this._muted) { + this.toggleMute(); } - } + } + } + + // toggles video mute + @action + toggleMute = () => { + if (this.player) { + this._muted = !this._muted; + this.player.muted = this._muted; + } + } + + + // 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%" }; + } + } + + + // for zoom slider, sets timeline waveform zoom + zoom = (zoom: number) => { + this.timeline?.setZoom(zoom); + } + + + // plays link + playLink = (doc: Doc) => { + const startTime = Math.max(0, (this._stackedTimeline?.anchorStart(doc) || 0)); + const endTime = this.timeline?.anchorEnd(doc); + if (startTime !== undefined) { + if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + else this.Seek(startTime); + } + } + + + // 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(CurrentUserUtils.ActiveTool)) { + setupMoveUpEvents(this, e, action(e => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this._marqueeing = [e.clientX, e.clientY]; + return true; + }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false); + } + } + + // ends marquee selection + @action + finishMarquee = () => { + this._marqueeing = undefined; + this.props.select(true); + } + + timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive)); + + timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight()); + + setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time; + + timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100; + + playing = () => this._playing; + + contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; + scaling = () => this.props.scaling?.() || 1; - // for zoom slider, sets timeline waveform zoom - zoom = (zoom: number) => { - this.timeline?.setZoom(zoom); - } + panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100; + panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100; + screenToLocalTransform = () => { + 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]; + + timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`]; + + + // 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 opacity = this._scrubbing ? 0.3 : (this._controlsVisible ? 1 : 0); + return this._fullScreen || (right - left) < 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}}> + {this.UIButtons} + </div> + </div> + } - // plays link - playLink = (doc: Doc) => { - const startTime = Math.max(0, (this._stackedTimeline?.anchorStart(doc) || 0)); - const endTime = this.timeline?.anchorEnd(doc); - if (startTime !== undefined) { - if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); - else this.Seek(startTime); + @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> + } - // 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(CurrentUserUtils.SelectedTool)) { - setupMoveUpEvents(this, e, action(e => { - MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this._marqueeing = [e.clientX, e.clientY]; - return true; - }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false); + { + !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> } - } - - // ends marquee selection - @action - finishMarquee = () => { - this._marqueeing = undefined; - this.props.select(true); - } - - timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive)); - - timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight()); - - setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time; - - timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100; - - playing = () => this._playing; - - 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) || 1) : this.props.PanelHeight() * this.heightPercent / 100; - - screenToLocalTransform = () => { - 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]; - - timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`]; - - - // renders video controls - @computed get uIButtons() { - const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0); - return <div className="videoBox-ui" style={this._fullScreen || this.heightPercent === 100 ? { fontSize: "40px", minWidth: "80%" } : {}}> - <div className="videobox-button" - title={this._playing ? "play" : "pause"} - onPointerDown={this.onPlayDown}> - <FontAwesomeIcon icon={this._playing ? "pause" : "play"} /> - </div> - - {this.timeline && <div className="timecode-controls"> - <div className="timecode-current"> - {formatTime(curTime)} - </div> - - {this._fullScreen || this.heightPercent === 100 ? - <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={(e: React.PointerEvent) => { e.stopPropagation(); }} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))} - /> - </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 && <div className="videobox-button" - title={"show timeline"} - onPointerDown={this.onTimelineHdlDown}> - <FontAwesomeIcon icon="eye" /> - </div>} - - {!this._fullScreen && <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 show-slider" - title={this._muted ? "unmute" : "mute"} - onPointerDown={(e) => { e.stopPropagation(); this.toggleMute(); }}> - <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} /> - </div> - <input type="range" 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 && - <> - <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)); }} - /> - </>} - </div>; - } - // renders CollectionStackedTimeline - @computed get renderTimeline() { - return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}> - <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props} - fieldKey={this.annotationKey} - dictationKey={this.fieldKey + "-dictation"} - mediaPath={this.audiopath} + <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}%` }}> + <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props} + fieldKey={this.annotationKey} + dictationKey={this.fieldKey + "-dictation"} + mediaPath={this.audiopath} + renderDepth={this.props.renderDepth + 1} + startTag={"_timecodeToShow" /* videoStart */} + endTag={"_timecodeToHide" /* videoEnd */} + bringToFront={emptyFunction} + CollectionView={undefined} + playFrom={this.playFrom} + setTime={this.setPlayheadTime} + playing={this.playing} + isAnyChildContentActive={this.isAnyChildContentActive} + whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged} + moveDocument={this.moveDocument} + addDocument={this.addDocument} + removeDocument={this.removeDocument} + ScreenToLocalTransform={this.timelineScreenToLocal} + Play={this.Play} + Pause={this.Pause} + playLink={this.playLink} + PanelHeight={this.timelineHeight} + rawDuration={this.rawDuration} + /> + </div>; + } + + // renders annotation layer + @computed get annotationLayer() { + return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />; + } + + savedAnnotations = () => this._savedAnnotations; + render() { + const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); + const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad; + return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} + style={{ + pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined, + borderRadius, + overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined + }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}> + <div className="videoBox-viewer" onPointerDown={this.marqueeDown} > + <div style={{ + position: "absolute", transition: this.transition, + width: this.panelWidth(), + height: this.panelHeight(), + top: 0, + left: (this.props.PanelWidth() - this.panelWidth()) / 2 + }}> + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} renderDepth={this.props.renderDepth + 1} - startTag={"_timecodeToShow" /* videoStart */} - endTag={"_timecodeToHide" /* videoEnd */} - bringToFront={emptyFunction} + fieldKey={this.annotationKey} CollectionView={undefined} - playFrom={this.playFrom} - setTime={this.setPlayheadTime} - playing={this.playing} - isAnyChildContentActive={this.isAnyChildContentActive} - whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged} - moveDocument={this.moveDocument} - addDocument={this.addDocument} + isAnnotationOverlay={true} + annotationLayerHostsContent={true} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + ScreenToLocalTransform={this.screenToLocalTransform} + docFilters={this.timelineDocFilter} + select={emptyFunction} + scaling={returnOne} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} - ScreenToLocalTransform={this.timelineScreenToLocal} - Play={this.Play} - Pause={this.Pause} - playLink={this.playLink} - PanelHeight={this.timelineHeight} - rawDuration={this.rawDuration} - /> - </div>; - } - - // renders annotation layer - @computed get annotationLayer() { - return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />; - } - - savedAnnotations = () => this._savedAnnotations; - render() { - const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); - const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad; - return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} - style={{ - pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined, - borderRadius, - overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined - }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}> - <div className="videoBox-viewer" onPointerDown={this.marqueeDown} > - <div style={{ - position: "absolute", transition: this.transition, - width: this.panelWidth(), - height: this.panelHeight(), - top: 0, - left: (this.props.PanelWidth() - this.panelWidth()) / 2 - }}> - <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - renderDepth={this.props.renderDepth + 1} - fieldKey={this.annotationKey} - CollectionView={undefined} - isAnnotationOverlay={true} - annotationLayerHostsContent={true} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - ScreenToLocalTransform={this.screenToLocalTransform} - docFilters={this.timelineDocFilter} - select={emptyFunction} - scaling={returnOne} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocWithTimecode}> - {this.contentFunc} - </CollectionFreeFormView> - </div> - {this.annotationLayer} - {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - <MarqueeAnnotator - rootDoc={this.rootDoc} - scrollTop={0} - down={this._marqueeing} - scaling={this.marqueeFitScaling} - docView={this.props.docViewPath().slice(-1)[0]} - containerOffset={this.marqueeOffset} - addDocument={this.addDocWithTimecode} - finishMarquee={this.finishMarquee} - savedAnnotations={this.savedAnnotations} - annotationLayer={this._annotationLayer.current} - mainCont={this._mainCont.current} - />} - {this.renderTimeline} - </div> - </div >); - } + moveDocument={this.moveDocument} + addDocument={this.addDocWithTimecode}> + {this.contentFunc} + </CollectionFreeFormView> + </div> + {this.annotationLayer} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : + <MarqueeAnnotator + rootDoc={this.rootDoc} + scrollTop={0} + down={this._marqueeing} + scaling={this.marqueeFitScaling} + docView={this.props.docViewPath().slice(-1)[0]} + containerOffset={this.marqueeOffset} + addDocument={this.addDocWithTimecode} + finishMarquee={this.finishMarquee} + savedAnnotations={this.savedAnnotations} + annotationLayer={this._annotationLayer.current} + mainCont={this._mainCont.current} + />} + {this.renderTimeline} + </div> + </div >); + } } VideoBox._nativeControls = false;
\ No newline at end of file |