diff options
author | bobzel <zzzman@gmail.com> | 2021-01-26 01:38:55 -0500 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2021-01-26 01:38:55 -0500 |
commit | 20add09510fb02b144d421910a56d3f3896b1f90 (patch) | |
tree | 6cea1b9339afe4f8d6efab6499bc8037fbb0e2f4 /src | |
parent | 0a48a55afd868b0cc3f298407a4b4882c4ee9bd2 (diff) |
preparing to unify VideoBox timeline with AudioBox timeline. changed names from videoStart/audioStart,End to anchorStart,EndTime and _displayTimecode to _timecodeToShow etc
Diffstat (limited to 'src')
-rw-r--r-- | src/client/documents/Documents.ts | 14 | ||||
-rw-r--r-- | src/client/util/DocumentManager.ts | 4 | ||||
-rw-r--r-- | src/client/views/DocComponent.tsx | 3 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 2 | ||||
-rw-r--r-- | src/client/views/StyleProvider.tsx | 4 | ||||
-rw-r--r-- | src/client/views/collections/CollectionView.tsx | 4 | ||||
-rw-r--r-- | src/client/views/collections/TabDocView.tsx | 5 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 8 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.scss | 3 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 217 | ||||
-rw-r--r-- | src/client/views/nodes/PresBox.tsx | 23 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 207 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 2 | ||||
-rw-r--r-- | src/fields/documentSchemas.ts | 6 |
15 files changed, 270 insertions, 235 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 286b7afa9..080270321 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -151,8 +151,10 @@ export interface DocumentOptions { _curPage?: number; _currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds _currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide) - displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) - undisplayTimecode?: number; // the time that a document should be hidden + _timecodeToShow?: number; // the time that a document should be displayed (e.g., when an annotation shows up as a video plays) + _timecodeToHide?: number; // the time that a document should be hidden + anchorStartTime?: number; // the time when an annotation starts on a timeline (e.g., when an audio anchor starts on an audio/video timeline) + anchorEndTime?: number; // the time when an annotaiton ends on a timeline lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide) activeFrame?: number; // the active frame of a document in a frame base collection appearFrame?: number; // the frame in which the document appears @@ -227,8 +229,6 @@ export interface DocumentOptions { linearViewIsExpanded?: boolean; // is linear view expanded isLabel?: boolean; // whether the document is a label or not (video / audio) useLinkSmallAnchor?: boolean; // whether links to this document should use a miniature linkAnchorBox - audioStart?: number; // the time frame where the audio should begin playing - audioEnd?: number; // the time frame where the audio should stop playing border?: string; //for searchbox hovercolor?: string; } @@ -722,8 +722,8 @@ export namespace Docs { linkDocProto.treeViewOpen = true;// setting this in the instance creator would set it on the view document. linkDocProto.anchor1 = source.doc; linkDocProto.anchor2 = target.doc; - linkDocProto.anchor1_timecode = source.doc._currentTimecode || source.doc.displayTimecode; - linkDocProto.anchor2_timecode = target.doc._currentTimecode || target.doc.displayTimecode; + linkDocProto.anchor1_timecode = source.doc._currentTimecode || source.doc._timecodeToShow; + linkDocProto.anchor2_timecode = target.doc._currentTimecode || target.doc._timecodeToShow; if (linkDocProto.linkBoxExcludedKeys === undefined) { Cast(linkDocProto.proto, Doc, null).linkBoxExcludedKeys = new List(["treeViewExpandedView", "aliases", "treeViewHideTitle", "removeDropProperties", "linkBoxExcludedKeys", "treeViewOpen", "aliasNumber", "isPrototype", "creationDate", "author"]); @@ -1252,7 +1252,7 @@ export namespace DocUtils { docList.forEach((d, i) => { d.x = Math.cos(Math.PI * 2 * i / docList.length) * 10 - w / 2; d.y = Math.sin(Math.PI * 2 * i / docList.length) * 10 - h / 2; - d.displayTimecode = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection }); }); if (x !== undefined && y !== undefined) { diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 1f2dd350b..41c7a1409 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -191,8 +191,8 @@ export class DocumentManager { targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom); // now find the target document within the context - if (targetDoc.displayTimecode) { // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode; - targetDocContext._currentTimecode = targetDoc.displayTimecode; + if (targetDoc._timecodeToShow) { // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode; + targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow; finished?.(); } else { // no timecode means we need to find the context view and focus on our target const findView = (delay: number) => { diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index db0b626a1..8d545c61b 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -81,6 +81,8 @@ export interface ViewBoxAnnotatableProps { } export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) { class Component extends Touchable<P> { + _annotationKey: string = "annotations"; + @observable _isChildActive = false; //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @computed get Document(): T { return schemaCtor(this.props.Document); } @@ -120,7 +122,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; - _annotationKey: string = "annotations"; public get annotationKey() { return this.fieldKey + "-" + this._annotationKey; } @action.bound diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index e1b9fe8aa..fb85684fb 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -613,7 +613,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b }}> {closeIcon} {bounds.r - bounds.x < 100 ? null : titleArea} - {(seldoc.rootDoc.annotationOn as Doc)?.type === DocumentType.AUDIO ? (null) : + {seldoc.rootDoc.anchorStartTime !== undefined ? (null) : <> {SelectionManager.Views().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) : <Tooltip key="i" title={<div className="dash-tooltip">{`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`}</div>} placement="top"> diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 95a28b33e..8628be014 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -108,7 +108,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | case DocumentType.FILTER: docColor = docColor || (darkScheme() ? "#2d2d2d" : "rgba(105, 105, 105, 0.432)"); break; case DocumentType.INK: docColor = doc?.isInkMask ? "rgba(0,0,0,0.7)" : undefined; break; case DocumentType.SLIDER: break; - case DocumentType.LABEL: docColor = docColor || (doc?.audioStart !== undefined || doc?.displayTimecode !== undefined ? "rgba(128, 128, 128, 0.18)" : undefined); break; + case DocumentType.LABEL: docColor = docColor || (doc.audioStart !== undefined || doc?.anchorStartTime !== undefined || doc?._timecodeToShow !== undefined ? "rgba(128, 128, 128, 0.18)" : undefined); break; case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break; case DocumentType.LINK: return "transparent"; case DocumentType.VID: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break; @@ -141,7 +141,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | `${darkScheme() ? "rgb(30, 32, 31) " : "#9c9396 "} ${StrCast(doc.boxShadow, "0.2vw 0.2vw 0.8vw")}`); case DocumentType.LABEL: - if (doc?.audioStart !== undefined) return "black 2px 2px 1px"; + if (doc?.anchorStartTime !== undefined) return "black 2px 2px 1px"; default: return doc.z ? `#9c9396 ${StrCast(doc?.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow props?.ContainingCollectionDoc?._useClusters && doc.type !== DocumentType.INK ? (`${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.ContentScaling?.() || 1)}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 83c639871..6b9b1a3c0 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -158,14 +158,14 @@ export class CollectionView extends Touchable<CollectionViewProps> { const pushpin = Docs.Create.FontIconDocument({ title: "pushpin", label: "", icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7", - _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null) + _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, _timecodeToShow: Cast(doc._timecodeToShow, "number", null) }); pushpin.isPushpin = true; Doc.GetProto(pushpin).annotationOn = doc.annotationOn; Doc.SetInPlace(doc, "annotationOn", undefined, true); Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin); const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", ""); - doc.displayTimecode = undefined; + doc._timecodeToShow = undefined; } doc._stayInCollection = undefined; doc.context = this.props.Document; diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index e5f05c407..c66734556 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -182,12 +182,13 @@ export class TabDocView extends React.Component<TabDocViewProps> { const presArray: Doc[] = PresBox.Instance?.sortArray(); const size: number = PresBox.Instance?._selectedArray.size; const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; + const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null); Doc.AddDocToList(curPres, "data", pinDoc, presSelected); - if (!pinProps?.audioRange && (pinDoc.type === DocumentType.AUDIO || pinDoc.type === DocumentType.VID)) { + if (!pinProps?.audioRange && duration !== undefined) { pinDoc.mediaStart = "manual"; pinDoc.mediaStop = "manual"; pinDoc.presStartTime = 0; - pinDoc.presEndTime = pinDoc.type === DocumentType.AUDIO ? doc.duration : NumCast(doc["data-duration"]); + pinDoc.presEndTime = duration; } //save position if (pinProps?.setPosition || pinDoc.isInkMask) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 013472a04..6619205af 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -54,7 +54,7 @@ export const panZoomSchema = createSchema({ _panX: "number", _panY: "number", _currentTimecode: "number", - displayTimecode: "number", + _timecodeToShow: "number", _currentFrame: "number", arrangeInit: ScriptField, _useClusters: "boolean", @@ -197,10 +197,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectView(dv, true)); } public isCurrent(doc: Doc) { - const dispTime = NumCast(doc.displayTimecode, -1); - const endTime = NumCast(doc.undisplayTimecode, dispTime + 1.5); + const dispTime = NumCast(doc._timecodeToShow, -1); + const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5); const curTime = NumCast(this.Document._currentTimecode, -1); - return dispTime === -1 || ((curTime - dispTime) >= -0.1 && curTime <= endTime); + return dispTime === -1 || ((curTime - dispTime) >= -1e-4 && curTime <= endTime); } public getActiveDocuments = () => { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 9ef37ecc2..0edbfe7a5 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -426,7 +426,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque delete d.x; delete d.y; delete d.activeFrame; - delete d.displayTimecode; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection d.x = dx - this.Bounds.left - this.Bounds.width / 2; d.y = dy - this.Bounds.top - this.Bounds.height / 2; return d; diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 93cdbbb09..4a1cee721 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -159,9 +159,8 @@ } .audiobox-timeline { - position: relative; + position: absolute; width: 100%; - background: white; border: gray solid 1px; border-radius: 3px; z-index: 1000; diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index a481317c3..0faac44a7 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -13,11 +13,10 @@ import { createSchema, listSpec, makeInterface } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { AudioField, nullAudio } from "../../../fields/URLField"; -import { emptyFunction, formatTime, numberRange, returnFalse, setupMoveUpEvents, Utils } from "../../../Utils"; +import { emptyFunction, formatTime, numberRange, returnFalse, setupMoveUpEvents, Utils, OmitKeys } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { DocumentManager } from "../../util/DocumentManager"; import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; @@ -30,6 +29,7 @@ import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment import { LinkDocPreview } from "./LinkDocPreview"; import "./AudioBox.scss"; import { Id } from "../../../fields/FieldSymbols"; +import { LabelBox } from "./LabelBox"; declare class MediaRecorder { // whatever MediaRecorder has @@ -69,7 +69,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD _audioRef = React.createRef<HTMLDivElement>(); _timeline: Opt<HTMLDivElement>; _markerStart: number = 0; - _currMarker: any; + _currAnchor: Opt<Doc>; @observable static SelectingRegion: AudioBox | undefined = undefined; @observable static _scrubTime = 0; @@ -81,8 +81,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD set audioState(value) { this.dataDoc.audioState = value; } public static SetScrubTime = action((timeInMillisFrom1970: number) => { AudioBox._scrubTime = 0; AudioBox._scrubTime = timeInMillisFrom1970; }); @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } - @computed get audioDuration() { return NumCast(this.dataDoc.duration); } - @computed get markerDocs() { return DocListCast(this.dataDoc[this.annotationKey]); } + @computed get duration() { return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); } + @computed get anchorDocs() { return DocListCast(this.dataDoc[this.annotationKey]); } @computed get links() { return DocListCast(this.dataDoc.links); } @computed get pauseTime() { return this._pauseEnd - this._pauseStart; } // total time paused to update the correct time @@ -90,17 +90,26 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD super(props); AudioBox.Instance = this; + if (this.duration === undefined) { + runInAction(() => { + this.Document[this.fieldKey + "-duration"] = this.Document.duration; + }) + } + // onClick play scripts - AudioBox.RangeScript = AudioBox.RangeScript || ScriptField.MakeFunction(`scriptContext.clickMarker(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!; - AudioBox.LabelScript = AudioBox.LabelScript || ScriptField.MakeFunction(`scriptContext.clickMarker(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!; - AudioBox.RangePlayScript = AudioBox.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(self, this.audioStart, this.audioEnd)`, { self: Doc.name, scriptContext: "any" })!; - AudioBox.LabelPlayScript = AudioBox.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(self, this.audioStart)`, { self: Doc.name, scriptContext: "any" })!; + AudioBox.RangeScript = AudioBox.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!; + AudioBox.LabelScript = AudioBox.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this)`, { self: Doc.name, scriptContext: "any" })!; + AudioBox.RangePlayScript = AudioBox.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!; + AudioBox.LabelPlayScript = AudioBox.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this)`, { self: Doc.name, scriptContext: "any" })!; } + anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor.audioStart)) + anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor.audioEnd, defaultVal)) + getLinkData(l: Doc) { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; - const linkTime = NumCast(la2.audioStart, NumCast(la1.audioStart)); + const linkTime = NumCast(la2.anchorStartTime, NumCast(la1.anchorStartTime)); if (Doc.AreProtosEqual(la1, this.dataDoc)) { la1 = l.anchor2 as Doc; la2 = l.anchor1 as Doc; @@ -109,7 +118,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD } getAnchor = () => { - return this.createMarker(this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)); + return this.createAnchor(this._ele?.currentTime || Cast(this.props.Document._currentTimecode, "number", null) || (this.audioState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)); } componentWillUnmount() { @@ -126,13 +135,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._disposers.scrubbing = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime)); - this._disposers.audioStart = reaction( - () => !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? Cast(this.Document._audioStart, "number", null) : undefined, - audioStart => audioStart !== undefined && setTimeout(() => { - this._audioRef.current && this.playFrom(audioStart); + this._disposers.triggerAudio = reaction( + () => !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerAudio, null) : undefined, + start => start !== undefined && setTimeout(() => { + this._audioRef.current && this.playFrom(start); setTimeout(() => { - this.Document._currentTimecode = audioStart; - this.Document._audioStart = undefined; + this.Document._currentTimecode = start; + this.Document._triggerAudio = undefined; }, 10); }, this._audioRef.current ? 0 : 250), // wait for mainCont and try again to play { fireImmediately: true } @@ -148,23 +157,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD ); } - playLink = (doc: Doc) => { - this.links.filter(l => l.anchor1 === doc || l.anchor2 === doc).forEach(l => { - const { la1, la2 } = this.getLinkData(l); - const startTime = NumCast(la1.audioStart, NumCast(la2.audioStart, null)); - const endTime = NumCast(la1.audioEnd, NumCast(la2.audioEnd, null)); - if (startTime !== undefined) { - this.layoutDoc.playOnSelect && (endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime)); - } - }); - doc.annotationOn === this.rootDoc && this.playFrom(NumCast(doc.audioStart), Cast(doc.audioEnd, "number", null)); - } - // for updating the timecode timecodeChanged = () => { const htmlEle = this._ele; if (this.audioState !== "recording" && htmlEle) { - htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration); + htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration); this.links.map(l => { const { la1, linkTime } = this.getLinkData(l); if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) { @@ -188,21 +185,21 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // play back the audio from time @action - playOnClick = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.audioDuration) => { - this.playFrom(seekTimeInSeconds, endTime); + playOnClick = (anchorDoc: Doc) => { + this.playFrom(this.anchorStart(anchorDoc), this.anchorEnd(anchorDoc, this.duration)); return true; } // play back the audio from time @action - clickMarker = (anchorDoc: Doc, seekTimeInSeconds: number, endTime: number = this.audioDuration) => { - if (this.layoutDoc.autoPlay) return this.playOnClick(anchorDoc, seekTimeInSeconds, endTime); - this._ele && (this._ele.currentTime = this.layoutDoc._currentTimecode = seekTimeInSeconds); + clickAnchor = (anchorDoc: Doc) => { + if (this.layoutDoc.autoPlay) return this.playOnClick(anchorDoc); + this._ele && (this._ele.currentTime = this.layoutDoc._currentTimecode = this.anchorStart(anchorDoc)); return true; } // play back the audio from time @action - playFrom = (seekTimeInSeconds: number, endTime: number = this.audioDuration) => { + playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => { clearTimeout(this._play); if (Number.isNaN(this._ele?.duration)) { setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); @@ -217,7 +214,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._ele.currentTime = seekTimeInSeconds; this._ele.play(); runInAction(() => this.audioState = "playing"); - if (endTime !== this.audioDuration) { + if (endTime !== this.duration) { this._play = setTimeout(() => this.pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration } } else { @@ -260,9 +257,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // context menu specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; + funcs.push({ description: (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors", event: () => this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors, icon: "expand-arrows-alt" }); 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.autoPlay ? "Don't auto play" : "Auto play") + " markers onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @@ -270,7 +267,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD stopRecording = action(() => { this._recorder.stop(); this._recorder = undefined; - this.dataDoc.duration = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; + this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; this.audioState = "paused"; this._stream?.getAudioTracks()[0].stop(); const ind = DocUtils.ActiveRecordings.indexOf(this); @@ -348,16 +345,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD e.stopPropagation(); } - // starting the drag event for marker resizing + // starting the drag event for anchor resizing @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.audioState === "paused"; - this._ele!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.audioDuration; + this._ele!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.duration; wasPaused && this.pause(); - const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.audioDuration; + const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.duration; this._markerStart = this._markerEnd = toTimeline(e.clientX - rect.x); AudioBox.SelectingRegion = this; setupMoveUpEvents(this, e, @@ -372,46 +369,46 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._markerStart = this._markerEnd; this._markerEnd = tmp; } - AudioBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, this._markerEnd); + AudioBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd); AudioBox.SelectingRegion = undefined; }), e => { this.props.select(false); - e.shiftKey && this.createMarker(this._ele!.currentTime); + e.shiftKey && this.createAnchor(this._ele!.currentTime); } , this.props.isSelected(true) || this._isChildActive); } } @action - createMarker(audioStart?: number, audioEnd?: number) { - if (audioStart === undefined) return this.rootDoc; - const marker = Docs.Create.LabelDocument({ - title: ComputedField.MakeFunction(`"#" + formatToTime(self.audioStart) + "-" + formatToTime(self.audioEnd)`) as any, + createAnchor(anchorStartTime?: number, anchorEndTime?: number) { + if (anchorStartTime === undefined) return this.rootDoc; + const anchor = Docs.Create.LabelDocument({ + title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any, useLinkSmallAnchor: true, hideLinkButton: true, - audioStart, - audioEnd, + anchorStartTime, + anchorEndTime, annotationOn: this.props.Document }); if (this.dataDoc[this.annotationKey]) { - this.dataDoc[this.annotationKey].push(marker); + this.dataDoc[this.annotationKey].push(anchor); } else { - this.dataDoc[this.annotationKey] = new List<Doc>([marker]); + this.dataDoc[this.annotationKey] = new List<Doc>([anchor]); } - return marker; + return anchor; } - // starting the drag event for marker resizing - onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => { - this._currMarker = m; + // starting the drag event for anchor resizing + onPointerDown = (e: React.PointerEvent, m: Doc, left: boolean): void => { + this._currAnchor = m; this._left = left; this._timeline?.setPointerCapture(e.pointerId); - const toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.audioDuration, screen_delta / width * this.audioDuration)); + const toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.duration)); setupMoveUpEvents(this, e, (e) => { const rect = (e.target as any).getBoundingClientRect(); - this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x, rect.width)); + this.changeAnchor(this._currAnchor, toTimeline(e.clientX - rect.x, rect.width)); return false; }, (e) => { @@ -422,27 +419,26 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD emptyFunction); } - // updates the marker with the new time + // updates the anchor with the new time @action - changeMarker = (m: any, time: any) => { - this.markerDocs.filter(marker => this.isSame(marker, m)).forEach(marker => - this._left ? marker.audioStart = time : marker.audioEnd = time); + changeAnchor = (anchor: Opt<Doc>, time: number) => { + anchor && (this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time); } - // checks if the two markers are the same with start and end time + // checks if the two anchors are the same with start and end time isSame = (m1: any, m2: any) => { - return m1.audioStart === m2.audioStart && m1.audioEnd === m2.audioEnd; + return this.anchorStart(m1) === this.anchorStart(m2) && this.anchorEnd(m1) === this.anchorEnd(m2); } - // makes sure no markers overlaps each other by setting the correct position and width - getLevel = (m: any, placed: { audioStart: number, audioEnd: number, level: number }[]) => { + // makes sure no anchors overlaps each other by setting the correct position and width + getLevel = (m: Doc, placed: { anchorStartTime: number, anchorEndTime: number, level: number }[]) => { const timelineContentWidth = this.props.PanelWidth() - AudioBox.playheadWidth; - const x1 = m.audioStart; - const x2 = m.audioEnd === undefined ? m.audioStart + 10 / timelineContentWidth * this.audioDuration : m.audioEnd; + const x1 = this.anchorStart(m); + const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.duration); let max = 0; const overlappedLevels = new Set(placed.map(p => { - const y1 = p.audioStart; - const y2 = p.audioEnd; + const y1 = p.anchorStartTime; + const y2 = p.anchorEndTime; if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) || (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) { max = Math.max(max, p.level); @@ -452,14 +448,14 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD let level = max + 1; for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j); - placed.push({ audioStart: x1, audioEnd: x2, level }); + placed.push({ anchorStartTime: x1, anchorEndTime: x2, level }); return level; } @computed get selectionContainer() { return AudioBox.SelectingRegion !== this ? (null) : <div className="audiobox-container" style={{ - 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%" + left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.duration * 100}%`, + width: `${Math.abs(this._markerStart - this._markerEnd) / this.duration * 100}%`, height: "100%", top: "0%" }} />; } @@ -471,8 +467,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD color={"darkblue"} height={this._waveHeight} barWidth={0.1} - pos={this.audioDuration} - duration={this.audioDuration} + pos={this.duration} + duration={this.duration} peaks={audioBuckets.length === AudioBox.NUMBER_OF_BUCKETS ? audioBuckets : undefined} progressColor={"blue"} />; } @@ -496,17 +492,36 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD labelClickScript = () => AudioBox.LabelScript; rangePlayScript = () => AudioBox.RangePlayScript; labelPlayScript = () => AudioBox.LabelPlayScript; + + playLink = (link: Doc) => { + if (link.annotationOn === this.rootDoc) { + if (this.layoutDoc.playOnSelect) this.playFrom(this.anchorStart(link), this.anchorEnd(link)); + else this._ele!.currentTime = this.layoutDoc._currentTimecode = this.anchorStart(link); + } + else this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => { + const { la1, la2 } = this.getLinkData(l); + const startTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime, null)); + const endTime = NumCast(la1.anchorEndTime, NumCast(la2.anchorEndTime, null)); + if (startTime !== undefined) { + if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime; + } + }); + } + renderInner = computedFn(function (this: AudioBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { - const marker = observable({ view: undefined as any }); + const anchor = observable({ view: undefined as any }); return { - marker, view: <DocumentView key="view" {...this.props} ref={action((r: DocumentView | null) => marker.view = r)} + anchor, view: <DocumentView key="view" {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} ref={action((r: DocumentView | null) => anchor.view = r)} Document={mark} + DataDoc={undefined} PanelWidth={() => width} PanelHeight={() => height} renderDepth={this.props.renderDepth + 1} focus={() => this.playLink(mark)} rootSelected={returnFalse} LayoutTemplate={undefined} + LayoutTemplateString={LabelBox.LayoutString("data")} ContainingCollectionDoc={this.props.Document} removeDocument={this.removeDocument} ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x - 4, -y - 3)} @@ -519,11 +534,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD scriptContext={this} /> }; }); - renderMarker = computedFn(function (this: AudioBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + renderAnchor = computedFn(function (this: AudioBox, 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) : + {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.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)} /> @@ -535,8 +550,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD const interactive = SnappingManager.GetIsDragging() || this.active() ? "-interactive" : ""; const timelineContentWidth = this.props.PanelWidth() - AudioBox.playheadWidth; 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 overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = []; + const drawAnchors = this.anchorDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; return <div className="audiobox-container" onContextMenu={this.specificContextMenu} @@ -566,37 +581,43 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD <div className="audiobox-dictation" /> <div className="audiobox-player" style={{ height: `${AudioBox.heightPercent}%` }} > <div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.audioState === "paused" ? "play" : "pause"} onClick={this.onPlay}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.audioState === "paused" ? "play" : "pause"} size={"1x"} /></div> - <div className="audiobox-timeline" style={{ height: `${AudioBox.heightPercent}%` }} ref={this.timelineRef} - onClick={e => { e.stopPropagation(); e.preventDefault(); }} - onPointerDown={e => e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e)}> - <div className="waveform"> + <div className="audiobox-timeline" style={{ height: `${AudioBox.heightPercent}%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}> + <div className="waveform" > {this.waveform} </div> - {drawMarkers.map(d => { - const m = d.marker; - const left = NumCast(m.audioStart) / this.audioDuration * timelineContentWidth; + </div> + <div className="audiobox-timeline" style={{ height: `${AudioBox.heightPercent}%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)` }} ref={this.timelineRef} + onClick={e => { e.stopPropagation(); e.preventDefault(); }} + onPointerDown={e => e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e)}> + {drawAnchors.map(d => { + const m = d.anchor; + const start = this.anchorStart(m); + const end = this.anchorEnd(m, start + 10 / timelineContentWidth * this.duration); + const left = start / this.duration * timelineContentWidth; const top = d.level / maxLevel * timelineContentHeight; - const timespan = m.audioEnd === undefined ? 10 / timelineContentWidth * this.audioDuration : NumCast(m.audioEnd) - NumCast(m.audioStart); - return this.layoutDoc.hideMarkers ? (null) : + const timespan = end - start; + return this.layoutDoc.hideAnchors ? (null) : <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={m[Id]} - style={{ left, top, width: `${timespan / this.audioDuration * 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, + style={{ left, top, width: `${timespan / this.duration * 100}%`, height: `${1 / maxLevel * 100}%` }} + onClick={e => { this.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} > + {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript, left + AudioBox.playheadWidth, (1 - AudioBox.heightPercent / 100) / 2 * this.props.PanelHeight() + top, - timelineContentWidth * timespan / this.audioDuration, + timelineContentWidth * timespan / this.duration, 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.audioDuration * 100}%`, pointerEvents: "none" }} /> - {this.audio} + <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} + style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.duration * 100}%`, pointerEvents: "none" }} + /> </div> + {this.audio} <div className="current-time"> {formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))} </div> <div className="total-time"> - {formatTime(Math.round(this.audioDuration))} + {formatTime(Math.round(this.duration))} </div> </div> </div> diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index aef776563..62e497e18 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -232,14 +232,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const duration: number = NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime); if (targetDoc.type === DocumentType.AUDIO) { if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); - targetDoc._audioStart = NumCast(activeItem.presStartTime); + targetDoc._triggerAudio = NumCast(activeItem.presStartTime); this._mediaTimer = [setTimeout(() => targetDoc._audioStop = true, duration * 1000), targetDoc]; } else if (targetDoc.type === DocumentType.VID) { if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); - targetDoc._videoStop = true; + targetDoc._triggerVideoStop = true; setTimeout(() => targetDoc._currentTimecode = NumCast(activeItem.presStartTime), 10); - setTimeout(() => targetDoc._videoStart = true, 20); - this._mediaTimer = [setTimeout(() => targetDoc._videoStop = true, (duration * 1000) + 20), targetDoc]; + setTimeout(() => targetDoc._triggerVideo = true, 20); + this._mediaTimer = [setTimeout(() => targetDoc._triggerVideoStop = true, (duration * 1000) + 20), targetDoc]; } } @@ -249,7 +249,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> targetDoc._audioStop = true; } else if (targetDoc.type === DocumentType.VID) { if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]); - targetDoc._videoStop = true; + targetDoc._triggerVideoStop = true; } } @@ -714,9 +714,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (audio) { audio.mediaStart = "manual"; audio.mediaStop = "manual"; - audio.presStartTime = NumCast(doc.audioStart); - audio.presEndTime = NumCast(doc.audioEnd); - audio.presDuration = NumCast(doc.audioEnd) - NumCast(doc.audioStart); + audio.presStartTime = NumCast(doc.anchorStartTime); + audio.presEndTime = NumCast(doc.anchorEndTime); + audio.presDuration = NumCast(doc.anchorEndTime) - NumCast(doc.anchorStartTime); TabDocView.PinDoc(audio, { audioRange: true }); setTimeout(() => this.removeDocument(doc), 0); return false; @@ -1477,6 +1477,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get mediaOptionsDropdown() { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; + const duration = Math.round(NumCast(activeItem[`${Doc.LayoutFieldKey(activeItem)}-duration`]) * 10); const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc); const mediaStopDocStr: string = mediaStopDocInd ? mediaStopDocInd + ". " + this.childDocs[mediaStopDocInd - 1].title : ""; if (activeItem && targetDoc) { @@ -1521,7 +1522,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> </div> <div className="multiThumb-slider"> - <input type="range" step="0.1" min="0" max={activeItem.type === DocumentType.AUDIO ? Math.round(NumCast(activeItem.duration) * 10) / 10 : Math.round(NumCast(activeItem["data-duration"]) * 10) / 10} value={NumCast(activeItem.presEndTime)} + <input type="range" step="0.1" min="0" max={duration / 10} value={NumCast(activeItem.presEndTime)} style={{ gridColumn: 1, gridRow: 1 }} className={`toolbar-slider ${"end"}`} id="toolbar-slider" @@ -1545,7 +1546,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> e.stopPropagation(); activeItem.presEndTime = Number(e.target.value); }} /> - <input type="range" step="0.1" min="0" max={activeItem.type === DocumentType.AUDIO ? Math.round(NumCast(activeItem.duration) * 10) / 10 : Math.round(NumCast(activeItem["data-duration"]) * 10) / 10} value={NumCast(activeItem.presStartTime)} + <input type="range" step="0.1" min="0" max={duration / 10} value={NumCast(activeItem.presStartTime)} style={{ gridColumn: 1, gridRow: 1 }} className={`toolbar-slider ${"start"}`} id="toolbar-slider" @@ -1573,7 +1574,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}> <div className="slider-text">0 s</div> <div className="slider-text"></div> - <div className="slider-text">{activeItem.type === DocumentType.AUDIO ? Math.round(NumCast(activeItem.duration) * 10) / 10 : Math.round(NumCast(activeItem["data-duration"]) * 10) / 10} s</div> + <div className="slider-text">{duration / 10} s</div> </div> </div> <div className="ribbon-final-box"> diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 12822b64a..988dd47d8 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -42,6 +42,7 @@ const VideoDocument = makeInterface(documentSchema, timeSchema); @observer export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); } static _youtubeIframeCounter: number = 0; static Instance: VideoBox; static RangeScript: ScriptField; @@ -58,20 +59,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD private _isResetClick = 0; private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - _play: any = null; - _timeline: Opt<HTMLDivElement>; - _audioRef = React.createRef<HTMLDivElement>(); - _markerStart: number = 0; - _left: boolean = false; - _count: Array<any> = []; - _duration = 0; - _start: boolean = true; - _currMarker: any; + private _play: any = null; + private _timeline: Opt<HTMLDivElement>; + private _audioRef = React.createRef<HTMLDivElement>(); + private _markerStart: number = 0; + private _left: boolean = false; + private _duration = 0; + private _start: boolean = true; + private _currAnchor: Doc | undefined; + @observable static _showControls: boolean; + @observable static SelectingRegion: VideoBox | undefined = undefined; @observable _marqueeing: number[] | undefined; @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); @observable _screenCapture = false; - @observable static _showControls: boolean; - @observable static SelectingRegion: VideoBox | undefined = undefined; @observable _visible: boolean = false; @observable _markerEnd: number = 0; @observable _forceCreateYouTubeIFrame = false; @@ -79,11 +79,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @observable _fullScreen = false; @observable _playing = false; @computed get links() { return DocListCast(this.dataDoc.links); } - @computed get heightPercent() { return this.layoutDoc._showTimeline ? NumCast(this.layoutDoc._videoTimelineHeightPercent, VideoBox.heightPercent) : 100; } - @computed get videoDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); } - @computed get markerDocs() { return DocListCast(this.dataDoc[this.annotationKey + "-timeline"]).concat(DocListCast(this.dataDoc[this.annotationKey])); } - - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); } + @computed get heightPercent() { return this.layoutDoc._timelineShow ? NumCast(this.layoutDoc._videoTimelineHeightPercent, VideoBox.heightPercent) : 100; } + @computed get duration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); } + @computed get anchorDocs() { return DocListCast(this.dataDoc[this.annotationKey + "-timeline"]).concat(DocListCast(this.dataDoc[this.annotationKey])); } public get player(): HTMLVideoElement | null { return this._videoRef; } @@ -92,14 +90,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD VideoBox.Instance = this; // onClick play scripts - VideoBox.RangeScript = VideoBox.RangeScript || ScriptField.MakeFunction(`scriptContext.clickMarker(self, clientX, this.displayTimecode, this.undisplayTimecode)`, { self: Doc.name, clientX: "number", scriptContext: "any" })!; - VideoBox.LabelScript = VideoBox.LabelScript || ScriptField.MakeFunction(`scriptContext.clickMarker(self, clientX, this.displayTimecode)`, { self: Doc.name, clientX: "number", scriptContext: "any" })!; - VideoBox.RangePlayScript = VideoBox.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(self, clientX, this.displayTimecode, this.undisplayTimecode)`, { self: Doc.name, clientX: "number", scriptContext: "any" })!; - VideoBox.LabelPlayScript = VideoBox.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(self, clientX, this.displayTimecode)`, { self: Doc.name, clientX: "number", scriptContext: "any" })!; + VideoBox.RangeScript = VideoBox.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { this: Doc.name, clientX: "number", scriptContext: "any" })!; + VideoBox.LabelScript = VideoBox.LabelScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { this: Doc.name, clientX: "number", scriptContext: "any" })!; + VideoBox.RangePlayScript = VideoBox.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, clientX: "number", scriptContext: "any" })!; + VideoBox.LabelPlayScript = VideoBox.LabelPlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, clientX: "number", scriptContext: "any" })!; } + anchorStart = (anchor: Doc) => NumCast(anchor.anchorStartTime, NumCast(anchor._timecodeToShow, NumCast(anchor.videoStart))) + anchorEnd = (anchor: Doc, defaultVal: any = null) => NumCast(anchor.anchorEndTime, NumCast(anchor._timecodeToHide, NumCast(anchor.videoEnd, defaultVal))) + getAnchor = () => { - return this.createMarker(Cast(this.layoutDoc._currentTimecode, "number", null)); + return this.createAnchor(Cast(this.layoutDoc._currentTimecode, "number", null)); } choosePath(url: string) { @@ -241,19 +242,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } }, { fireImmediately: true }); - this._disposers.videoStart = reaction( - () => !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? Cast(this.Document._videoStart, "number", null) : undefined, - videoStart => videoStart !== undefined && setTimeout(() => { + this._disposers.triggerVideo = reaction( + () => !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerVideo, null) : undefined, + time => time !== undefined && setTimeout(() => { this.player && this.Play(); - setTimeout(() => this.Document._videoStart = undefined, 10); + setTimeout(() => this.Document._triggerVideo = undefined, 10); }, this.player ? 0 : 250), // wait for mainCont and try again to play { fireImmediately: true } ); - this._disposers.videoStop = reaction( - () => this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc ? Cast(this.Document._videoStop, "number", null) : undefined, - videoStop => videoStop !== undefined && setTimeout(() => { + this._disposers.triggerStop = reaction( + () => this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc ? NumCast(this.Document._triggerVideoStop, null) : undefined, + stop => stop !== undefined && setTimeout(() => { this.player && this.Pause(); - setTimeout(() => this.Document._videoStop = undefined, 10); + setTimeout(() => this.Document._triggerVideoStop = undefined, 10); }, this.player ? 0 : 250), // wait for mainCont and try again to play { fireImmediately: true } ); @@ -321,7 +322,8 @@ 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.autoPlay ? "Don't auto play" : "Auto play") + " markers onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); + subitems.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); + subitems.push({ description: (this.layoutDoc.autoPlay ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlay = !this.layoutDoc.autoPlay, icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); } } @@ -408,7 +410,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD right: this.scaling() * 10 - 10, bottom: this.scaling() * 10 - 10 }}> - <FontAwesomeIcon icon={this.layoutDoc._showTimeline ? "eye-slash" : "eye"} style={{ width: "100%" }} /> + <FontAwesomeIcon icon={this.layoutDoc._timelineShow ? "eye-slash" : "eye"} style={{ width: "100%" }} /> </div>, VideoBox._showControls ? (null) : [ // <div className="control-background"> @@ -470,16 +472,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } @action.bound - addDocumentWithTimestamp(doc: Doc | Doc[]): boolean { + addDocWithTimecode(doc: Doc | Doc[]): boolean { const docs = doc instanceof Doc ? [doc] : doc; const curTime = NumCast(this.layoutDoc._currentTimecode); - docs.forEach(doc => doc.displayTimecode = curTime); + docs.forEach(doc => doc._timecodeToShow = curTime); return this.addDocument(doc); } // play back the video from time @action - playFrom = (seekTimeInSeconds: number, endTime: number = this.videoDuration) => { + playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => { clearTimeout(this._play); this._duration = endTime - seekTimeInSeconds; if (Number.isNaN(this.player?.duration)) { @@ -495,7 +497,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this.player.currentTime = seekTimeInSeconds; this.player.play(); runInAction(() => this._playing = true); - if (endTime !== this.videoDuration) { + if (endTime !== this.duration) { this._play = setTimeout(() => this.Pause(), (this._duration) * 1000); // use setTimeout to play a specific duration } } else { @@ -505,12 +507,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } @action - toggleTimeline = (e: React.PointerEvent) => this.layoutDoc._showTimeline = !this.layoutDoc._showTimeline + toggleTimeline = (e: React.PointerEvent) => this.layoutDoc._timelineShow = !this.layoutDoc._timelineShow // ref for timeline - timelineRef = (timeline: HTMLDivElement) => { this._timeline = timeline; }; + timelineRef = (timeline: HTMLDivElement) => this._timeline = timeline - // starting the drag event creating a range marker + // starting the drag event creating a range mark @action onPointerDownTimeline = (e: React.PointerEvent): void => { const rect = this._timeline?.getBoundingClientRect(); @@ -520,7 +522,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD else if (!this._doubleTime) { this._doubleTime = setTimeout(() => { this._doubleTime = undefined; - this.player!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.videoDuration; + this.player!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.duration; }, 300); } @@ -537,12 +539,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._markerStart = this._markerEnd; this._markerEnd = tmp; } - VideoBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createMarker(this._markerStart, this._markerEnd); + VideoBox.SelectingRegion === this && (Math.abs(movement[0]) > 15) && this.createAnchor(this._markerStart, this._markerEnd); VideoBox.SelectingRegion = undefined; }), (e, doubleTap) => { this.props.select(false); - e.shiftKey && this.createMarker(this.player!.currentTime); + e.shiftKey && this.createAnchor(this.player!.currentTime); !wasPlaying && doubleTap && this.Play(); } , this.props.isSelected(true) || this._isChildActive); @@ -550,25 +552,27 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } @action - createMarker(displayTimecode: number, undisplayTimecode?: number) { - const marker = Docs.Create.LabelDocument({ - title: ComputedField.MakeFunction(`"#" + formatToTime(self.displayTimecode) + "-" + formatToTime(self.undisplayTimecode)`) as any, + createAnchor(anchorStartTime: number, anchorEndTime?: number) { + const anchor = Docs.Create.LabelDocument({ + title: ComputedField.MakeFunction(`"#" + formatToTime(self.anchorStartTime) + "-" + formatToTime(self.anchorEndTime)`) as any, useLinkSmallAnchor: true, // bcz: note this also flags that the annotation is not on the video itself, just the timeline hideLinkButton: true, - displayTimecode, - undisplayTimecode, + anchorStartTime, + anchorEndTime, annotationOn: this.props.Document }); if (this.dataDoc[this.annotationKey + "-timeline"]) { - this.dataDoc[this.annotationKey + "-timeline"].push(marker); + this.dataDoc[this.annotationKey + "-timeline"].push(anchor); } else { - this.dataDoc[this.annotationKey + "-timeline"] = new List<Doc>([marker]); + this.dataDoc[this.annotationKey + "-timeline"] = new List<Doc>([anchor]); } - return marker; + return anchor; } @action - playOnClick = (anchorDoc: Doc, clientX: number, seekTimeInSeconds: number, endTime: number = this.videoDuration) => { + playOnClick = (anchorDoc: Doc, clientX: number) => { + const seekTimeInSeconds = this.anchorStart(anchorDoc); + const endTime = this.anchorEnd(anchorDoc); if (this.layoutDoc.autoPlay) { if (this._playing) this.Pause(); else this.playFrom(seekTimeInSeconds, endTime); @@ -587,7 +591,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } @action - clickMarker = (anchorDoc: Doc, clientX: number, seekTimeInSeconds: number, endTime: number = this.videoDuration) => { + clickAnchor = (anchorDoc: Doc, clientX: number) => { + const seekTimeInSeconds = this.anchorStart(anchorDoc); + const endTime = this.anchorEnd(anchorDoc); if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) { if (this._playing) this.Pause(); else if (this.layoutDoc.autoPlay) this.Play(); @@ -602,16 +608,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD return { select: true }; } - toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.videoDuration, screen_delta / width * this.videoDuration)); - // starting the drag event for marker resizing - onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => { - this._currMarker = m; + toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.duration)); + // starting the drag event for anchor resizing + onPointerDown = (e: React.PointerEvent, m: Doc, left: boolean): void => { + this._currAnchor = m; this._left = left; this._timeline?.setPointerCapture(e.pointerId); setupMoveUpEvents(this, e, (e: PointerEvent) => { const rect = (e.target as any).getBoundingClientRect(); - this.changeMarker(this._currMarker, this.toTimeline(e.clientX - rect.x, rect.width)); + this.changeAnchor(this._currAnchor, this.toTimeline(e.clientX - rect.x, rect.width)); return false; }, (e: PointerEvent) => { @@ -622,14 +628,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD emptyFunction); } - // makes sure no markers overlaps each other by setting the correct position and width - getLevel = (m: any, placed: { videoStart: number, videoEnd: number, level: number }[]) => { + // makes sure no anchors overlaps each other by setting the correct position and width + getLevel = (m: any, placed: { anchorStartTime: number, videoEnd: number, level: number }[]) => { const timelineContentWidth = this.props.PanelWidth(); - const x1 = m.displayTimecode; - const x2 = m.undisplayTimecode === undefined ? m.displayTimecode + 10 / timelineContentWidth * this.videoDuration : m.undisplayTimecode; + const x1 = this.anchorStart(m); + const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.duration); let max = 0; const overlappedLevels = new Set(placed.map(p => { - const y1 = p.videoStart; + const y1 = p.anchorStartTime; const y2 = p.videoEnd; if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) || (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) { @@ -640,28 +646,29 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD let level = max + 1; for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j); - placed.push({ videoStart: x1, videoEnd: x2, level }); + placed.push({ anchorStartTime: x1, videoEnd: x2, level }); return level; } playLink = (doc: Doc) => { - const startTime = NumCast(doc.displayTimecode); - const endTime = NumCast(doc.undisplayTimecode, null); + const startTime = NumCast(doc.anchorStartTime, NumCast(doc._timecodeToShow)); + const endTime = NumCast(doc.anchorEndTime, NumCast(doc._timecodeToHide, null)); if (startTime !== undefined) { - this.layoutDoc.playOnSelect && (endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime)); + if (this.layoutDoc.playOnSelect) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime); + else this.Seek(startTime); } } - // renders the markers as a document + // renders the anchors as a document renderInner = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number, annotationKey: string) { - const marker = observable({ view: undefined as any }); + const anchor = observable({ view: undefined as any }); return { - marker, view: <DocumentView key="view" {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} ref={action((r: DocumentView | null) => marker.view = r)} + anchor, view: <DocumentView key="view" {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} ref={action((r: DocumentView | null) => anchor.view = r)} Document={mark} DataDoc={undefined} - focus={() => this.playLink(mark)} PanelWidth={() => width} PanelHeight={() => height} renderDepth={this.props.renderDepth + 1} + focus={() => this.playLink(mark)} rootSelected={returnFalse} LayoutTemplate={undefined} LayoutTemplateString={LabelBox.LayoutString("data")} @@ -678,11 +685,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD }; }); - renderMarker = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number, annotationKey: string) { + renderAnchor = computedFn(function (this: VideoBox, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number, annotationKey: string) { const inner = this.renderInner(mark, script, doublescript, x, y, width, height, annotationKey); return <> {inner.view} - {!inner.marker.view || !SelectionManager.IsSelected(inner.marker.view) ? (null) : + {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.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)} /> @@ -694,10 +701,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @computed get renderTimeline() { const timelineContentWidth = this.props.PanelWidth(); const timelineContentHeight = this.props.PanelHeight() * (100 - this.heightPercent) / 100; - const overlaps: { videoStart: number, videoEnd: number, level: number }[] = []; - const drawMarkers: { level: number, marker: Doc }[] = this.markerDocs.map((m, i) => ({ level: this.getLevel(m, overlaps), marker: m })); + const overlaps: { anchorStartTime: number, videoEnd: number, level: number }[] = []; + const drawAnchors: { level: number, anchor: Doc }[] = this.anchorDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; - return !this.layoutDoc._showTimeline ? (null) : + return !this.layoutDoc._timelineShow ? (null) : <div className="audiobox-timeline" ref={this.timelineRef} style={{ height: `${100 - this.heightPercent}%` }} onClick={e => { if (this._isChildActive || this.props.isSelected()) { @@ -709,47 +716,51 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD e.button === 0 && !e.ctrlKey && this.onPointerDownTimeline(e); } }}> - {drawMarkers.map(d => { - const m = d.marker; - const start = NumCast(m.displayTimecode, NumCast(m.displayTimecode, null)); - const left = start / this.videoDuration * timelineContentWidth; + {drawAnchors.map(d => { + const m = d.anchor; + const start = this.anchorStart(m); + const end = this.anchorEnd(m, start + 10 / timelineContentWidth * this.duration); + const left = start / this.duration * timelineContentWidth; const top = d.level / maxLevel * timelineContentHeight; - const timespan = m.undisplayTimecode === undefined ? 10 / timelineContentWidth * this.videoDuration : NumCast(m.undisplayTimecode) - NumCast(m.displayTimecode); - return this.layoutDoc.hideMarkers ? (null) : + const timespan = end - start; + return this.layoutDoc.hideAnchors ? (null) : <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}timeline`} key={m[Id]} - style={{ left, top, width: `${timespan / this.videoDuration * 100}%`, height: `${1 / maxLevel * 100}%` }} - onClick={e => { this.playFrom(start, Cast(m.undisplayTimecode, "number", null)); e.stopPropagation(); }} > - {this.renderMarker(m, this.rangeClickScript, this.rangePlayScript, + style={{ left, top, width: `${timespan / this.duration * 100}%`, height: `${1 / maxLevel * 100}%` }} + onClick={e => { this.playFrom(start, this.anchorEnd(m)); e.stopPropagation(); }} > + {this.renderAnchor(m, this.rangeClickScript, this.rangePlayScript, left, top + (this.props.PanelHeight() - timelineContentHeight), - timelineContentWidth * timespan / this.videoDuration, - timelineContentHeight / maxLevel, this.annotationKey + (m.useLinkSmallAnchor ? "-timeline" : ""))} + timelineContentWidth * timespan / this.duration, + timelineContentHeight / maxLevel, this.annotationKey + (m.anchorStartTime !== undefined ? "-timeline" : ""))} </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" }} + style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.duration * 100}%`, pointerEvents: "none" }} /> </div>; } - // updates the marker with the new time + // updates the anchor with the new time @action - changeMarker = (m: any, time: any) => { - this.markerDocs.filter(marker => this.isSame(marker, m)).forEach(marker => - this._left ? marker.displayTimecode = time : marker.undisplayTimecode = time); + changeAnchor = (anchor: Opt<Doc>, time: number) => { + if (anchor) { + const timelineOnly = Cast(anchor.anchorStartTime, "number", null) !== undefined; + if (timelineOnly) this._left ? anchor.anchorStartTime = time : anchor.anchorEndTime = time; + else this._left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time; + } } - // checks if the two markers are the same with start and end time + // checks if the two anchors are the same with start and end time isSame = (m1: any, m2: any) => { - return m1.displayTimecode === m2.displayTimecode && m1.undisplayTimecode === m2.undisplayTimecode; + return m1.anchorStartTime === m2.anchorStartTime && m1.anchorEndTime === m2.anchorEndTime && m1._timecodeToShow === m2._timecodeToShow && m1._timecodeToHide === m2._timecodeToHide; } // 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%" + left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.duration * 100}%`, + width: `${Math.abs(this._markerStart - this._markerEnd) / this.duration * 100}%`, height: "100%", top: "0%" }} />; } @@ -757,7 +768,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD VideoBox.Instance.keyEvents(e); } - // for creating key markers with key events + // for creating key anchors with key events @action keyEvents = (e: KeyboardEvent) => { if (e.target instanceof HTMLInputElement) return; @@ -770,7 +781,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._start = false; this._visible = true; } else { - this.createMarker(this._markerStart, currTime); + this.createAnchor(this._markerStart, currTime); this._start = true; this._visible = false; } @@ -828,7 +839,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD whenActiveChanged={this.whenActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} - addDocument={this.addDocumentWithTimestamp} + addDocument={this.addDocWithTimecode} CollectionView={undefined} renderDepth={this.props.renderDepth + 1}> {this.contentFunc} @@ -838,7 +849,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD {this.annotationLayer} {this.renderTimeline} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocumentWithTimestamp} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} + <MarqueeAnnotator rootDoc={this.rootDoc} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocWithTimecode} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div> </div >); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 7348ebdd2..e06a324d2 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -361,7 +361,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp DocListCast(this.dataDoc.links).map((l, i) => { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; - this._linkTime = NumCast(la1.audioStart, NumCast(la2.audioStart)); + this._linkTime = NumCast(la1.anchorStartTime, NumCast(la2.anchorStartTime)); audioState = la2.audioState; if (Doc.AreProtosEqual(la2, this.dataDoc)) { la1 = l.anchor2 as Doc; diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 67df56015..d52f3a928 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -19,10 +19,10 @@ export const documentSchema = createSchema({ lastFrame: "number", // last frame of a frame based collection (e.g., a progressive slide) activeFrame: "number", // the active frame of a frame based animated document _currentTimecode: "number", // current play back time of a temporal document (video / audio) - displayTimecode: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) + _timecodeToShow: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) isLabel: "boolean", // whether the document is a label or not (video / audio) - audioStart: "number", // the time frame where the audio should begin playing - audioEnd: "number", // the time frame where the audio should stop playing + anchorStartTime: "number", // the time code where a document activates (eg in Audio or video timelines) + anchorEndTime: "number", // the time code where a document deactivates markers: listSpec(Doc), // list of markers for audio / video x: "number", // x coordinate when in a freeform view y: "number", // y coordinate when in a freeform view |