diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 16 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.scss | 138 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 355 |
3 files changed, 487 insertions, 22 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index c3c876b75..77777ff76 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -24,11 +24,11 @@ import { SnappingManager } from "../../util/SnappingManager"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; -import "./AudioBox.scss"; import { DocumentView } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; +import "./AudioBox.scss"; declare class MediaRecorder { // whatever MediaRecorder has @@ -264,6 +264,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); funcs.push({ description: (this.layoutDoc.hideMarkers ? "Don't hide" : "Hide") + " range markers", event: () => this.layoutDoc.hideMarkers = !this.layoutDoc.hideMarkers, icon: "expand-arrows-alt" }); funcs.push({ description: (this.layoutDoc.playOnClick ? "Don't play" : "Play") + " markers onClick", event: () => this.layoutDoc.playOnClick = !this.layoutDoc.playOnClick, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " markers onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @@ -364,15 +365,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD setupMoveUpEvents(this, e, action(e => { this._markerEnd = toTimeline(e.clientX - rect.x); + return false; + }), + action((e, movement) => { + this._markerEnd = toTimeline(e.clientX - rect.x); if (this._markerEnd < this._markerStart) { const tmp = this._markerStart; this._markerStart = this._markerEnd; this._markerEnd = tmp; } - return false; - }), - action((e, movement) => { - AudioBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, toTimeline(e.clientX - rect.x)); + AudioBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, this._markerEnd); AudioBox.SelectingRegion = undefined; }), e => { @@ -456,7 +458,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD @computed get selectionContainer() { return AudioBox.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{ - left: `${NumCast(this._markerStart) / this.audioDuration * 100}%`, + left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.audioDuration * 100}%`, width: `${Math.abs(this._markerStart - this._markerEnd) / this.audioDuration * 100}%`, height: "100%", top: "0%" }} />; } @@ -534,7 +536,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD const timelineContentHeight = (this.props.PanelHeight() * AudioBox.heightPercent / 100) * AudioBox.heightPercent / 100; // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline) const overlaps: { audioStart: number, audioEnd: number, level: number }[] = []; const drawMarkers = this.markerDocs.map((m, i) => ({ level: this.getLevel(m, overlaps), marker: m })); - const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 1; + const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; return <div className="audiobox-container" onContextMenu={this.specificContextMenu} onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined} diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 07e8e0951..76edda847 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -10,6 +10,135 @@ .inkingCanvas-paths-markers { opacity : 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround } + + .audiobox-timeline { + position: absolute; + height: 20%; + width: 100%; + bottom: 0px; + background: white; + border: gray solid 1px; + border-radius: 3px; + z-index: 1000; + overflow: hidden; + left: 0px; + + .audiobox-current { + width: 1px; + height: 100%; + background-color: red; + position: absolute; + top: 0px; + } + + .audiobox-container { + position: absolute; + width: 10px; + top: 2.5%; + height: 0px; + background: lightblue; + border-radius: 5px; + // box-shadow: black 2px 2px 1px; + opacity: 0.3; + z-index: 500; + border-style: solid; + border-color: darkblue; + border-width: 1px; + } + + .audiobox-marker-container, + .audiobox-marker-minicontainer { + position: absolute; + width: 10px; + height: 10px; + top: 2.5%; + background: gray; + border-radius: 50%; + box-shadow: black 2px 2px 1px; + overflow: visible; + cursor: pointer; + + .audiobox-marker { + position: relative; + height: 100%; + // height: calc(100% - 15px); + width: 100%; + //margin-top: 15px; + } + + .audio-marker:hover { + border: orange 2px solid; + } + } + + .audiobox-marker-timeline, + .audiobox-marker-minicontainer { + position: absolute; + width: 10px; + height: 90%; + top: 2.5%; + background: gray; + border-radius: 5px; + box-shadow: black 2px 2px 1px; + opacity: 0.3; + + .audiobox-marker { + position: relative; + height: calc(100% - 15px); + margin-top: 15px; + } + + .audio-marker:hover { + border: orange 2px solid; + } + + .resizer { + position: absolute; + right: 0; + cursor: ew-resize; + height: 100%; + width: 2px; + z-index: 100; + } + + .click { + position: relative; + height: 100%; + width: 100%; + z-index: 100; + } + + .left-resizer { + position: absolute; + left: 0; + cursor: ew-resize; + height: 100%; + width: 2px; + z-index: 100; + } + + // .contentFittingDocumentView-previewDoc { + // width: 100% !important; + // transform: none !important; + // } + } + + .audiobox-marker-container1:hover, + .audiobox-marker-minicontainer:hover { + opacity: 0.8; + } + + .audiobox-marker-minicontainer { + width: 5px; + border-radius: 1px; + + .audiobox-marker { + position: relative; + height: calc(100% - 8px); + margin-top: 8px; + } + } + } } .videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen, @@ -49,6 +178,15 @@ transform-origin: left top; pointer-events:all; } + +// .timeline-button { +// position: absolute; +// bottom: 35px; +// right: 235px; +// color: lightgrey; +// width: 20px; + +// } .videoBox-play { width: 25px; height: 20px; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index c2c159e4b..67e8d74b3 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -3,12 +3,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; -import { Doc } from "../../../fields/Doc"; +import { Doc, Opt, DocListCast } from "../../../fields/Doc"; import { InkTool } from "../../../fields/InkField"; import { createSchema, makeInterface } from "../../../fields/Schema"; import { Cast, StrCast, NumCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; -import { Utils, emptyFunction, returnOne, returnZero, OmitKeys } from "../../../Utils"; +import { Utils, emptyFunction, returnOne, returnZero, OmitKeys, setupMoveUpEvents, returnFalse, returnTrue, formatTime } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { ContextMenu } from "../ContextMenu"; @@ -21,10 +21,15 @@ import { documentSchema } from "../../../fields/documentSchemas"; import { Networking } from "../../Network"; import { SnappingManager } from "../../util/SnappingManager"; import { SelectionManager } from "../../util/SelectionManager"; +import { ComputedField, ScriptField } from "../../../fields/ScriptField"; +import { List } from "../../../fields/List"; +import { DocumentView } from "./DocumentView"; import { LinkDocPreview } from "./LinkDocPreview"; import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { Transform } from "../../util/Transform"; import { StyleProp } from "../StyleProvider"; +import { computedFn } from "mobx-utils"; +import { DocumentManager } from "../../util/DocumentManager"; const path = require('path'); export const timeSchema = createSchema({ @@ -36,6 +41,14 @@ const VideoDocument = makeInterface(documentSchema, timeSchema); @observer export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) { static _youtubeIframeCounter: number = 0; + static Instance: VideoBox; + static RangeScript: ScriptField; + static LabelScript: ScriptField; + static RangePlayScript: ScriptField; + static LabelPlayScript: ScriptField; + static heightPercent = 20; // height of timeline in percent of height of videoBox. + private _reactionDisposer?: IReactionDisposer; + private _youtubeReactionDisposer?: IReactionDisposer; // private _reactionDisposer?: IReactionDisposer; // private _youtubeReactionDisposer?: IReactionDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; @@ -44,17 +57,42 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD private _youtubeIframeId: number = -1; private _youtubeContentCreated = false; private _isResetClick = 0; + _play: any = null; + _timeline: Opt<HTMLDivElement>; + _audioRef = React.createRef<HTMLDivElement>(); + _markerStart: number = 0; + _left: boolean = false; + _count: Array<any> = []; + _duration = 0; + _start: boolean = true; + private _currMarker: any; + @observable static SelectingRegion: VideoBox | undefined = undefined; + @observable _visible: boolean = false; + @observable _markerEnd: number = 0; @observable _forceCreateYouTubeIFrame = false; @observable _playTimer?: NodeJS.Timeout = undefined; @observable _fullScreen = false; @observable _playing = false; @observable static _showControls: boolean; + @computed get videoDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); } + @computed get markerDocs() { return DocListCast(this.dataDoc[this.annotationKey]); } public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); } public get player(): HTMLVideoElement | null { return this._videoRef; } + constructor(props: Readonly<FieldViewProps>) { + super(props); + VideoBox.Instance = this; + + // onClick play scripts + VideoBox.RangeScript = VideoBox.RangeScript || ScriptField.MakeScript(`scriptContext.clickMarker(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!; + VideoBox.LabelScript = VideoBox.LabelScript || ScriptField.MakeScript(`scriptContext.clickMarker(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!; + VideoBox.RangePlayScript = VideoBox.RangePlayScript || ScriptField.MakeScript(`scriptContext.playOnClick(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!; + VideoBox.LabelPlayScript = VideoBox.LabelPlayScript || ScriptField.MakeScript(`scriptContext.playOnClick(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!; + } + videoLoad = () => { const aspect = this.player!.videoWidth / this.player!.videoHeight; Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth); @@ -64,6 +102,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } @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(); @@ -228,6 +268,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._disposers.reactionDisposer?.(); this._disposers.youtubeReactionDisposer?.(); this._disposers.videoStart?.(); + document.removeEventListener("keydown", VideoBox.keyEventsWrapper, true); } @action @@ -274,26 +315,36 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); }), icon: "expand-arrows-alt" }); + subitems.push({ description: (this.layoutDoc.playOnClick ? "Don't play" : "Play") + " markers onClick", event: () => this.layoutDoc.playOnClick = !this.layoutDoc.playOnClick, icon: "expand-arrows-alt" }); + subitems.push({ description: (this.layoutDoc.playOnClick ? "Don't auto play" : "Auto play") + " markers onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); } } + // returns the video and timeline @computed get content() { 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; + const h = this.layoutDoc._showTimeline ? `${100 - VideoBox.heightPercent}%` : "100%"; return !field ? <div>Loading</div> : - <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} - style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }} - onCanPlay={this.videoLoad} - controls={VideoBox._showControls} - onPlay={() => this.Play()} - onSeeked={this.updateTimecode} - onPause={() => this.Pause()} - onClick={e => e.preventDefault()}> - <source src={field.url.href} type="video/mp4" /> - Not supported. - </video>; + <div className="container" style={{ pointerEvents: this._isChildActive || this.active() ? "all" : "none" }}> + <div className={`${style}`} style={{ width: "100%", height: h, left: "0px" }}> + <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} + style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }} + onCanPlay={this.videoLoad} + controls={VideoBox._showControls} + 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.uIButtons} + </div> + {this.renderTimeline} + </div>; } @computed get youtubeVideoId() { @@ -345,22 +396,34 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD private get uIButtons() { const curTime = (this.layoutDoc._currentTimecode || 0); return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} > - <span>{"" + Math.round(curTime)}</span> + <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} > <FontAwesomeIcon icon="camera" size="lg" /> </div>, + <div className="timeline-button" key="timeline-button" onPointerDown={this.toggleTimeline} style={{ + position: "absolute", + bottom: "41px", + right: this.layoutDoc._showTimeline ? "235px" : "155px", + color: "lightgrey", + width: "20px" + }}> + <FontAwesomeIcon icon={this.layoutDoc._showTimeline ? "eye-slash" : "eye"} style={{ width: "100%" }} /> + </div>, VideoBox._showControls ? (null) : [ + // <div className="control-background"> <div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} > <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" /> </div>, <div className="videoBox-full" key="full" onPointerDown={this.onFullDown} > F + {/* </div> */} </div> ]]); } + onPlayDown = () => this._playing ? this.Pause() : this.Play(); onFullDown = (e: React.PointerEvent) => { @@ -415,6 +478,269 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD return this.addDocument(doc); } + // play back the video from time + @action + playFrom = (seekTimeInSeconds: number, endTime: number = this.videoDuration) => { + clearTimeout(this._play); + this._duration = endTime - seekTimeInSeconds; + if (Number.isNaN(this.player?.duration)) { + setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); + } else if (this.player) { + if (seekTimeInSeconds < 0) { + if (seekTimeInSeconds > -1) { + setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000); + } else { + this.Pause(); + } + } else if (seekTimeInSeconds <= this.player.duration) { + this.player.currentTime = seekTimeInSeconds; + this.player.play(); + runInAction(() => this._playing = true); + if (endTime !== this.videoDuration) { + this._play = setTimeout(() => this.Pause(), (this._duration) * 1000); // use setTimeout to play a specific duration + } + } else { + this.Pause(); + } + } + } + + @action + toggleTimeline = (e: React.PointerEvent) => this.layoutDoc._showTimeline = !this.layoutDoc._showTimeline + + // ref for timeline + timelineRef = (timeline: HTMLDivElement) => { + this._timeline = timeline; + } + + // starting the drag event creating a range marker + @action + onPointerDownTimeline = (e: React.PointerEvent): void => { + const rect = this._timeline?.getBoundingClientRect();// (e.target as any).getBoundingClientRect(); + if (rect && e.target !== this._audioRef.current && this.active()) { + const wasPaused = !this._playing; + this.player!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.videoDuration; + wasPaused && this.Pause(); + + const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.videoDuration; + this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x); + VideoBox.SelectingRegion = this; + setupMoveUpEvents(this, e, + action(e => { + this._markerEnd = toTimeline(e.clientX - rect.x); + return false; + }), + action((e, movement) => { + this._markerEnd = toTimeline(e.clientX - rect.x); + if (this._markerEnd < this._markerStart) { + const tmp = this._markerStart; + this._markerStart = this._markerEnd; + this._markerEnd = tmp; + } + VideoBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, this._markerEnd); + VideoBox.SelectingRegion = undefined; + }), + e => { + this.props.select(false); + e.shiftKey && this.createMarker(this.player!.currentTime); + } + , this.props.isSelected(true) || this._isChildActive); + } + } + + @action + createMarker(audioStart: number, audioEnd?: number) { + const marker = Docs.Create.LabelDocument({ + title: ComputedField.MakeFunction(`formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any, isLabel: audioEnd === undefined, + useLinkSmallAnchor: true, hideLinkButton: true, audioStart, audioEnd, _showSidebar: false, + _autoHeight: true, annotationOn: this.props.Document + }); + marker.data = ""; // clears out the label's text so that only its border will display + if (this.dataDoc[this.annotationKey]) { + this.dataDoc[this.annotationKey].push(marker); + } else { + this.dataDoc[this.annotationKey] = new List<Doc>([marker]); + } + } + + // play back the video from time + @action + playOnClick = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.videoDuration) => { + DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false); + this.playFrom(seekTimeInSeconds, endTime); + } + + // play back the video from time + @action + clickMarker = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.videoDuration) => { + if (this.layoutDoc.playOnClick) this.playOnClick(anchorDoc, seekTimeInSeconds, endTime); + else { + DocumentManager.Instance.getDocumentView(anchorDoc)?.select(false); + this.player && (this.player.currentTime = this.layoutDoc._currentTimecode = seekTimeInSeconds); + } + } + + // starting the drag event for marker resizing + onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => { + this._currMarker = m; + this._left = left; + this._timeline?.setPointerCapture(e.pointerId); + const toTimeline = (screen_delta: number, width: number) => screen_delta / width * this.videoDuration; + setupMoveUpEvents(this, e, + (e: PointerEvent) => { + const rect = (e.target as any).getBoundingClientRect(); + this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x, rect.width)); + return false; + }, + (e: PointerEvent) => { + const rect = (e.target as any).getBoundingClientRect(); + this.player!.currentTime = this.layoutDoc._currentTimecode = toTimeline(e.clientX - rect.x, rect.width); + this._timeline?.releasePointerCapture(e.pointerId); + }, + emptyFunction); + } + + // makes sure no markers overlaps each other by setting the correct position and width + getLevel = (m: any, placed: { audioStart: number, audioEnd: number, level: number }[]) => { + const timelineContentWidth = this.props.PanelWidth(); + const x1 = m.audioStart; + const x2 = m.audioEnd === undefined ? m.audioStart + 10 / timelineContentWidth * this.videoDuration : m.audioEnd; + let max = 0; + const overlappedLevels = new Set(placed.map(p => { + const y1 = p.audioStart; + const y2 = p.audioEnd; + if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) || + (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) { + max = Math.max(max, p.level); + return p.level; + } + })); + let level = max + 1; + for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j); + + placed.push({ audioStart: x1, audioEnd: x2, level }); + return level; + } + + // renders the markers as a document + renderInner = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + const marker = observable({ view: undefined as any }); + return { + marker, view: <DocumentView key="view" {...this.props} ref={action((r: DocumentView | null) => marker.view = r)} + Document={mark} + PanelWidth={() => width} + PanelHeight={() => height} + rootSelected={returnFalse} + LayoutTemplate={undefined} + ContainingCollectionDoc={this.props.Document} + removeDocument={this.removeDocument} + ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x - 4, -y - 3)} + parentActive={(out) => this.props.isSelected(out) || this._isChildActive} + whenActiveChanged={action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive))} + onClick={script} + onDoubleClick={this.layoutDoc.playOnClick ? undefined : doublescript} + ignoreAutoHeight={false} + bringToFront={emptyFunction} + scriptContext={this} /> + }; + }); + + renderMarker = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + const inner = this.renderInner(mark, script, doublescript, x, y, width, height); + return <> + {inner.view} + {!inner.marker.view || !SelectionManager.IsSelected(inner.marker.view) ? (null) : + <> + <div key="left" className="left-resizer" onPointerDown={e => this.onPointerDown(e, mark, true)} /> + <div key="right" className="resizer" onPointerDown={e => this.onPointerDown(e, mark, false)} /> + </>} + </>; + }); + + // returns the timeline + @computed get renderTimeline() { + const rect = this._timeline?.getBoundingClientRect(); + const timelineContentWidth = this.props.PanelWidth(); + //const timelineContentWidth = this.layoutDoc._showTimeline ? this.props.PanelWidth() * 1.25 : this.props.PanelWidth(); + //const timelineContentWidth = rect ? rect.width : this.props.PanelWidth(); + const timelineContentHeight = (this.props.PanelHeight() * VideoBox.heightPercent / 100); // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline) + const overlaps: { audioStart: number, audioEnd: number, level: number }[] = []; + const drawMarkers = this.markerDocs.map((m, i) => ({ level: this.getLevel(m, overlaps), marker: m })); + const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; + return !this.layoutDoc._showTimeline ? (null) : + <div className="audiobox-timeline" ref={this.timelineRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ height: `${VideoBox.heightPercent}%` }} + onPointerDown={e => e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e)}> + {drawMarkers.map((d, i) => { + const m = d.marker; + const left = NumCast(m.audioStart) / this.videoDuration; + const l = `${NumCast(m.audioStart) / this.videoDuration * 100}%`; + const top = d.level / maxLevel * timelineContentHeight; + const timespan = m.audioEnd === undefined ? 10 / timelineContentWidth * this.videoDuration : NumCast(m.audioEnd) - NumCast(m.audioStart); + return this.layoutDoc.hideMarkers ? (null) : + <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={i} + style={{ left: l, top, width: `${timespan / this.videoDuration * 100}%`, height: `${1 / maxLevel * 100}%` }} + onClick={e => { this.playFrom(NumCast(m.audioStart), Cast(m.audioEnd, "number", null)); e.stopPropagation(); }} > + {this.renderMarker(m, this.rangeClickScript, this.rangePlayScript, + left, + top, + timelineContentWidth * timespan / this.videoDuration, + timelineContentHeight / maxLevel)} + </div>; + })} + {this.selectionContainer} + <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.videoDuration * 100}%`, pointerEvents: "none" }} /> + </div>; + } + + // updates the marker with the new time + @action + changeMarker = (m: any, time: any) => { + DocListCast(this.dataDoc[this.annotationKey]).filter(marker => this.isSame(marker, m)).forEach(marker => + this._left ? marker.audioStart = time : marker.audioEnd = time); + } + + // checks if the two markers are the same with start and end time + isSame = (m1: any, m2: any) => { + return m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd; + } + + // returns the blue container when dragging + @computed get selectionContainer() { + return VideoBox.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{ + left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.videoDuration * 100}%`, + width: `${Math.abs(this._markerStart - this._markerEnd) / this.videoDuration * 100}%`, height: "100%", top: "0%" + }} />; + } + + static keyEventsWrapper = (e: KeyboardEvent) => { + VideoBox.Instance.keyEvents(e); + } + + // for creating key markers with key events + @action + keyEvents = (e: KeyboardEvent) => { + if (e.target instanceof HTMLInputElement) return; + if (!this._playing) return; // can't create if video is not playing + switch (e.key) { + case "x": // currently set to x, but can be a different key + const currTime = this.player!.currentTime; + if (this._start) { + this._markerStart = this.player!.currentTime; + this._start = false; + this._visible = true; + } else { + this.createMarker(this._markerStart, currTime); + this._start = true; + this._visible = false; + } + } + } + + rangeClickScript = () => VideoBox.RangeScript; + labelClickScript = () => VideoBox.LabelScript; + rangePlayScript = () => VideoBox.RangePlayScript; + labelPlayScript = () => VideoBox.LabelPlayScript; + screenToLocalTransform = () => this.props.ScreenToLocalTransform(); contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; render() { @@ -445,7 +771,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD {this.contentFunc} </CollectionFreeFormView> </div> - {this.uIButtons} </div >); } } |