aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/VideoBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/VideoBox.tsx')
-rw-r--r--src/client/views/nodes/VideoBox.tsx958
1 files changed, 538 insertions, 420 deletions
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index e833c7e30..b1f7d8023 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,44 +1,42 @@
-import React = require("react");
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from "mobx";
-import { observer } from "mobx-react";
-import { basename } from "path";
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx';
+import { observer } from 'mobx-react';
+import { basename } from 'path';
import * as rp from 'request-promise';
-import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc";
-import { InkTool } from "../../../fields/InkField";
-import { List } from "../../../fields/List";
-import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { AudioField, ImageField, VideoField } from "../../../fields/URLField";
-import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from "../../../Utils";
-import { Docs, DocUtils } from "../../documents/Documents";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { Networking } from "../../Network";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-import { DocumentManager } from "../../util/DocumentManager";
-import { SelectionManager } from "../../util/SelectionManager";
-import { SnappingManager } from "../../util/SnappingManager";
-import { undoBatch } from "../../util/UndoManager";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { CollectionStackedTimeline, TrimScope } from "../collections/CollectionStackedTimeline";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
-import { DocumentDecorations } from "../DocumentDecorations";
-import { MarqueeAnnotator } from "../MarqueeAnnotator";
-import { AnchorMenu } from "../pdf/AnchorMenu";
-import { StyleProp } from "../StyleProvider";
+import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc';
+import { InkTool } from '../../../fields/InkField';
+import { List } from '../../../fields/List';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
+import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Networking } from '../../Network';
+import { DocumentManager } from '../../util/DocumentManager';
+import { ReplayMovements } from '../../util/ReplayMovements';
+import { SelectionManager } from '../../util/SelectionManager';
+import { SnappingManager } from '../../util/SnappingManager';
+import { undoBatch } from '../../util/UndoManager';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
+import { DocumentDecorations } from '../DocumentDecorations';
+import { MarqueeAnnotator } from '../MarqueeAnnotator';
+import { AnchorMenu } from '../pdf/AnchorMenu';
+import { StyleProp } from '../StyleProvider';
import { FieldView, FieldViewProps } from './FieldView';
-import "./VideoBox.scss";
-import { Presentation } from "../../util/TrackMovements";
-import { RecordingBox } from "./RecordingBox";
-import { ReplayMovements } from "../../util/ReplayMovements";
+import { RecordingBox } from './RecordingBox';
+import './VideoBox.scss';
const path = require('path');
/**
* VideoBox
* Main component: VideoBox.tsx
* Supporting Components: CollectionStackedTimeline
- *
+ *
* VideoBox is a node that supports the playback of video files in Dash.
* When a video file or YouTube video is importeed into Dash, it is immediately rendered as a VideoBox document.
* CollectionStackedTimline handles AudioBox and VideoBox shared behavior, but VideoBox handles playing, pausing, etc because it contains <video> element
@@ -48,7 +46,9 @@ const path = require('path');
@observer
export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(VideoBox, fieldKey);
+ }
/**
* Uploads an image buffer to the server and stores with specified filename. by default the image
* is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
@@ -58,20 +58,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
*/
public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
try {
- const posting = Utils.prepend("/uploadURI");
+ const posting = Utils.prepend('/uploadURI');
const returnedUri = await rp.post(posting, {
body: {
uri: imageUri,
name: returnedFilename,
nosuffix,
- replaceRootFilename
+ replaceRootFilename,
},
json: true,
});
return returnedUri;
-
} catch (e) {
- console.log("VideoBox :" + e);
+ console.log('VideoBox :' + e);
}
}
@@ -103,27 +102,29 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@observable _finished: boolean = false; // has playback reached end of clip
@observable _volume: number = 1;
@observable _muted: boolean = false;
- @observable _controlsTransform?: { X: number, Y: number };
+ @observable _controlsTransform?: { X: number; Y: number };
@observable _controlsVisible: boolean = true;
@observable _scrubbing: boolean = false;
- @computed get links() { return DocListCast(this.dataDoc.links); }
- @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height
+ @computed get links() {
+ return DocListCast(this.dataDoc.links);
+ }
+ @computed get heightPercent() {
+ return NumCast(this.layoutDoc._timelineHeightPercent, 100);
+ } // current percent of video relative to VideoBox height
// @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
@observable rawDuration: number = 0;
-
@computed get youtubeVideoId() {
const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
- return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
+ return field && field.url.href.indexOf('youtube') !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split('/')) : '';
}
-
// returns the path of the audio file
@computed get audiopath() {
const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
- return field?.url.href ?? vfield?.url.href ?? "";
+ return field?.url.href ?? vfield?.url.href ?? '';
}
// returns the presentation data if it exists, null otherwise
@@ -132,10 +133,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return data ? JSON.parse(data) : null;
}
- @computed private get timeline() { return this._stackedTimeline; }
- private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline
- public get player(): HTMLVideoElement | null { return this._videoRef; }
-
+ @computed private get timeline() {
+ return this._stackedTimeline;
+ }
+ private get transition() {
+ return this._clicking ? 'left 0.5s, width 0.5s, height 0.5s' : '';
+ } // css transition for hiding/showing timeline
+ public get player(): HTMLVideoElement | null {
+ return this._videoRef;
+ }
componentDidMount() {
this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
@@ -150,7 +156,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
}
this.player && this.setPlayheadTime(0);
- document.addEventListener("keydown", this.keyEvents, true);
+ document.addEventListener('keydown', this.keyEvents, true);
if (this.presentation) {
ReplayMovements.Instance.setVideoBox(this);
@@ -161,9 +167,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.removeCurrentlyPlaying();
this.Pause();
Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
- document.removeEventListener("keydown", this.keyEvents, true);
+ document.removeEventListener('keydown', this.keyEvents, true);
- if (this.presentation) {
+ if (this.presentation) {
ReplayMovements.Instance.removeVideoBox();
}
}
@@ -173,20 +179,23 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
keyEvents = (e: KeyboardEvent) => {
if (
// need to include range inputs because after dragging time slider it becomes target element
- !(e.target instanceof HTMLInputElement && !(e.target.type === "range")) &&
+ !(e.target instanceof HTMLInputElement && !(e.target.type === 'range')) &&
this.props.isSelected(true)
) {
switch (e.key) {
- case "ArrowLeft":
- case "ArrowRight":
+ case 'ArrowLeft':
+ case 'ArrowRight':
clearTimeout(this._controlsFadeTimer);
this._scrubbing = true;
- this._controlsFadeTimer = setTimeout(action(() => this._scrubbing = false), 500);
+ this._controlsFadeTimer = setTimeout(
+ action(() => (this._scrubbing = false)),
+ 500
+ );
e.stopPropagation();
break;
}
}
- }
+ };
// plays video
@action public Play = (update: boolean = true) => {
@@ -210,18 +219,18 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
update && this._youtubePlayer?.playVideo();
this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
} catch (e) {
- console.log("Video Play Exception:", e);
+ console.log('Video Play Exception:', e);
}
}
this.updateTimecode();
- }
+ };
// goes to time
@action public Seek(time: number) {
try {
this._youtubePlayer?.seekTo(Math.round(time), true);
} catch (e) {
- console.log("Video Seek Exception:", e);
+ console.log('Video Seek Exception:', e);
}
this.player && (this.player.currentTime = time);
this._audioPlayer && (this._audioPlayer.currentTime = time);
@@ -242,7 +251,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true);
} catch (e) {
- console.log("Video Pause Exception:", e);
+ console.log('Video Pause Exception:', e);
}
this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
this._playTimer = undefined;
@@ -251,24 +260,23 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play
}
this._playRegionTimer = undefined;
- }
+ };
// toggles video full screen
@action public FullScreen = () => {
if (document.fullscreenElement === this._contentRef) {
this._fullScreen = false;
this.player && this._contentRef && document.exitFullscreen();
- }
- else {
+ } else {
this._fullScreen = true;
this.player && this._contentRef && this._contentRef.requestFullscreen();
}
try {
- this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add");
+ this._youtubePlayer && this.props.addDocTab(this.rootDoc, 'add');
} catch (e) {
- console.log("Video FullScreen Exception:", e);
+ console.log('Video FullScreen Exception:', e);
}
- }
+ };
// fades out controls in fullscreen after mouse stops moving
@action controlsFade = (e: PointerEvent) => {
@@ -276,10 +284,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (!this._scrubbing) {
clearTimeout(this._controlsFadeTimer);
this._controlsVisible = true;
- this._controlsFadeTimer = setTimeout(action(() => this._controlsVisible = false), 3000);
+ this._controlsFadeTimer = setTimeout(
+ action(() => (this._controlsVisible = false)),
+ 3000
+ );
}
- }
-
+ };
// drag controls around window in fulls screen
@action controlsDrag = (e: React.PointerEvent) => {
@@ -288,7 +298,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const eleStyle = getComputedStyle(e.target as Element);
this._controlsTransform = { X: parseInt(eleStyle.left), Y: parseInt(eleStyle.top) };
- setupMoveUpEvents(e.target,
+ setupMoveUpEvents(
+ e.target,
e,
action((e, down, delta) => {
if (this._controlsTransform) {
@@ -298,32 +309,35 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return false;
}),
emptyFunction,
- emptyFunction)
- }
-
+ emptyFunction
+ );
+ };
// creates and links snapshot photo of current video frame
@action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
const width = NumCast(this.layoutDoc._width);
const canvas = document.createElement('canvas');
canvas.width = 640;
- canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1);
- const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
+ canvas.height = (640 * Doc.NativeHeight(this.layoutDoc)) / (Doc.NativeWidth(this.layoutDoc) || 1);
+ const ctx = canvas.getContext('2d'); //draw image to canvas. scale to target dimensions
if (ctx) {
this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
}
if (!this._videoRef) {
const b = Docs.Create.LabelDocument({
- x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y, 1),
- _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(),
- _isLinkButton: true
+ x: NumCast(this.layoutDoc.x) + width,
+ y: NumCast(this.layoutDoc.y, 1),
+ _width: 150,
+ _height: 50,
+ title: (this.layoutDoc._currentTimecode || 0).toString(),
+ _isLinkButton: true,
});
this.props.addDocument?.(b);
- DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot");
- Networking.PostToServer("/youtubeScreenshot", {
+ DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, 'video snapshot');
+ Networking.PostToServer('/youtubeScreenshot', {
id: this.youtubeVideoId,
- timecode: this.layoutDoc._currentTimecode
+ timecode: this.layoutDoc._currentTimecode,
}).then(response => {
const resolved = response?.accessPaths?.agnostic?.client;
if (resolved) {
@@ -335,49 +349,50 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
//convert to desired file format
const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
// if you want to preview the captured image,
- const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, "");
- const encodedFilename = encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"));
+ const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
+ const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, '_'));
const filename = basename(encodedFilename);
- VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) =>
- returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
+ VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
}
- }
+ };
updateIcon = () => {
const makeIcon = (returnedfilename: string) => {
this.dataDoc.icon = new ImageField(returnedfilename);
- this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym]();
- this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym]();
+ this.dataDoc['icon-nativeWidth'] = this.layoutDoc[WidthSym]();
+ this.dataDoc['icon-nativeHeight'] = this.layoutDoc[HeightSym]();
};
this.Snapshot(undefined, undefined, makeIcon);
- }
+ };
// creates link for snapshot
createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
- const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
+ const url = !imagePath.startsWith('/') ? Utils.CorsProxy(imagePath) : imagePath;
const width = NumCast(this.layoutDoc._width) || 1;
const height = NumCast(this.layoutDoc._height);
const imageSummary = Docs.Create.ImageDocument(url, {
- _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc),
- x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _isLinkButton: true,
- _width: 150, _height: height / width * 150, title: "--snapshot" + NumCast(this.layoutDoc._currentTimecode) + " image-"
+ _nativeWidth: Doc.NativeWidth(this.layoutDoc),
+ _nativeHeight: Doc.NativeHeight(this.layoutDoc),
+ x: NumCast(this.layoutDoc.x) + width,
+ y: NumCast(this.layoutDoc.y),
+ _isLinkButton: true,
+ _width: 150,
+ _height: (height / width) * 150,
+ title: '--snapshot' + NumCast(this.layoutDoc._currentTimecode) + ' image-',
});
Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
this.props.addDocument?.(imageSummary);
- const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot");
+ const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, 'video snapshot');
link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
- setTimeout(() =>
- (downX !== undefined && downY !== undefined) && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, "move", true));
- }
-
+ setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, 'move', true));
+ };
getAnchor = () => {
- const timecode = Cast(this.layoutDoc._currentTimecode, "number", null);
+ const timecode = Cast(this.layoutDoc._currentTimecode, 'number', null);
const marquee = AnchorMenu.Instance.GetAnchor?.();
- return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
- }
-
+ return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow' /* videoStart */, '_timecodeToHide' /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
+ };
// sets video info on load
videoLoad = action(() => {
@@ -387,10 +402,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
if (Number.isFinite(this.player!.duration)) {
this.rawDuration = this.player!.duration;
- } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + "-duration"]);
+ } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']);
});
-
// updates video time
@action
updateTimecode = () => {
@@ -398,10 +412,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
try {
this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
} catch (e) {
- console.log("Video Timecode Exception:", e);
+ console.log('Video Timecode Exception:', e);
}
- }
-
+ };
// extracts video thumbnails and saves them as field of doc
getVideoThumbnails = () => {
@@ -419,23 +432,23 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const ctx = canvas.getContext('2d');
ctx?.drawImage(video, 0, 0, canvas.width, canvas.height);
const imgUrl = canvas.toDataURL();
- const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, "");
- const encodedFilename = encodeURIComponent("thumbnail" + retitled + "_" + video.currentTime.toString().replace(/\./, "_"));
+ const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
+ const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_'));
const filename = basename(encodedFilename);
thumbnailPromises.push(VideoBox.convertDataUri(imgUrl, filename));
const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1);
if (newTime < video.duration) {
video.currentTime = newTime;
+ } else {
+ Promise.all(thumbnailPromises).then(thumbnails => {
+ this.dataDoc.thumbnails = new List<string>(thumbnails);
+ });
}
- else {
- Promise.all(thumbnailPromises).then(thumbnails => { this.dataDoc.thumbnails = new List<string>(thumbnails); });
- }
- }
+ };
const field = Cast(this.dataDoc[this.fieldKey], VideoField);
field && (video.src = field.url.href);
- }
-
+ };
// sets video element ref
@action
@@ -446,33 +459,34 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// @ts-ignore
// vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
this._disposers.reactionDisposer?.();
- this._disposers.reactionDisposer = reaction(() => NumCast(this.layoutDoc._currentTimecode),
- time => !this._playing && (vref.currentTime = time), { fireImmediately: true });
+ this._disposers.reactionDisposer = reaction(
+ () => NumCast(this.layoutDoc._currentTimecode),
+ time => !this._playing && (vref.currentTime = time),
+ { fireImmediately: true }
+ );
(!this.dataDoc.thumbnails || this.dataDoc.thumbnails.length != VideoBox.numThumbnails) && this.getVideoThumbnails();
}
- }
+ };
// set ref for div that wraps video and controls for fullscreen
@action
setContentRef = (cref: HTMLDivElement | null) => {
this._contentRef = cref;
if (cref) {
- cref.onfullscreenchange = action((e) => {
- this._fullScreen = (document.fullscreenElement === cref);
+ cref.onfullscreenchange = action(e => {
+ this._fullScreen = document.fullscreenElement === cref;
this._controlsVisible = true;
this._scrubbing = false;
clearTimeout(this._controlsFadeTimer);
if (this._fullScreen) {
document.addEventListener('pointermove', this.controlsFade);
- }
- else {
+ } else {
document.removeEventListener('pointermove', this.controlsFade);
}
});
}
- }
-
+ };
// context menu
specificContextMenu = (e: React.MouseEvent): void => {
@@ -480,143 +494,185 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (field) {
const url = field.url.href;
const subitems: ContextMenuProps[] = [];
- subitems.push({ description: "Full Screen", event: this.FullScreen, icon: "expand" });
- subitems.push({ description: "Take Snapshot", event: this.Snapshot, icon: "expand-arrows-alt" });
- this.rootDoc.type === DocumentType.SCREENSHOT && subitems.push({
- description: "Screen Capture", event: (async () => {
- runInAction(() => this._screenCapture = !this._screenCapture);
- this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
- }), icon: "expand-arrows-alt"
+ subitems.push({ description: 'Full Screen', event: this.FullScreen, icon: 'expand' });
+ subitems.push({ description: 'Take Snapshot', event: this.Snapshot, icon: 'expand-arrows-alt' });
+ this.rootDoc.type === DocumentType.SCREENSHOT &&
+ subitems.push({
+ description: 'Screen Capture',
+ event: async () => {
+ runInAction(() => (this._screenCapture = !this._screenCapture));
+ this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+ },
+ icon: 'expand-arrows-alt',
+ });
+ subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered', event: () => (this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks), icon: 'expand-arrows-alt' });
+ subitems.push({
+ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected',
+ event: () => (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks),
+ icon: 'expand-arrows-alt',
});
- subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? "" : "Don't") + " follow links when encountered", event: () => this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks, icon: "expand-arrows-alt" });
- subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" });
- subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" });
+ subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : 'Auto play') + ' anchors onClick', event: () => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors), icon: 'expand-arrows-alt' });
// subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
// subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" });
// subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" });
// subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" });
- subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
+ subitems.push({
+ description: 'Copy path',
+ event: () => {
+ Utils.CopyText(url);
+ },
+ icon: 'expand-arrows-alt',
+ });
// if the videobox was turned from a recording box
- if (this.dataDoc[this.fieldKey + "-recorded"] === true) {
+ if (this.dataDoc[this.fieldKey + '-recorded'] === true) {
subitems.push({
- description: "Recreate recording", event: () => {
- this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
- // delete assoicated video data
- this.dataDoc[this.props.fieldKey] = "";
- this.dataDoc[this.fieldKey + "-duration"] = "";
- // delete assoicated presentation data
- this.dataDoc[this.fieldKey + "-presentation"] = "";
- }, icon: "expand-arrows-alt"
+ description: 'Recreate recording',
+ event: () => {
+ this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
+ // delete assoicated video data
+ this.dataDoc[this.props.fieldKey] = '';
+ this.dataDoc[this.fieldKey + '-duration'] = '';
+ // delete assoicated presentation data
+ this.dataDoc[this.fieldKey + '-presentation'] = '';
+ },
+ icon: 'expand-arrows-alt',
});
}
- ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
+ ContextMenu.Instance.addItem({ description: 'Options...', subitems: subitems, icon: 'video' });
}
- }
-
+ };
// ref for updating time
- setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e;
+ setAudioRef = (e: HTMLAudioElement | null) => (this._audioPlayer = e);
// renders the video and audio
@computed get content() {
const field = Cast(this.dataDoc[this.fieldKey], VideoField);
- const interactive = CurrentUserUtils.ActiveTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
- const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
- const opacity = this._scrubbing ? 0.3 : (this._controlsVisible ? 1 : 0);
- return !field ? <div key="loading">Loading</div> :
- <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply", cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'pointer' }}>
- <div className={classname} ref={this.setContentRef} onPointerDown={(e) => this._fullScreen && e.stopPropagation()}>
- {this._fullScreen && <div className="videoBox-ui" onPointerDown={this.controlsDrag}
- style={{ left: this._controlsTransform && this._controlsTransform.X, top: this._controlsTransform && this._controlsTransform.Y, visibility: this._controlsVisible || this._scrubbing ? 'visible' : 'hidden', opacity: opacity }}>
- {this.UIButtons}
- </div>}
- <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}}
+ const interactive = Doc.ActiveTool !== InkTool.None || !this.props.isSelected() ? '' : '-interactive';
+ const classname = 'videoBox-content' + (this._fullScreen ? '-fullScreen' : '') + interactive;
+ const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0;
+ return !field ? (
+ <div key="loading">Loading</div>
+ ) : (
+ <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'pointer' }}>
+ <div className={classname} ref={this.setContentRef} onPointerDown={e => this._fullScreen && e.stopPropagation()}>
+ {this._fullScreen && (
+ <div
+ className="videoBox-ui"
+ onPointerDown={this.controlsDrag}
+ style={{
+ left: this._controlsTransform && this._controlsTransform.X,
+ top: this._controlsTransform && this._controlsTransform.Y,
+ visibility: this._controlsVisible || this._scrubbing ? 'visible' : 'hidden',
+ opacity: opacity,
+ }}>
+ {this.UIButtons}
+ </div>
+ )}
+ <video
+ key="video"
+ autoPlay={this._screenCapture}
+ ref={this.setVideoRef}
+ style={this._fullScreen ? this.fullScreenSize() : {}}
onCanPlay={this.videoLoad}
controls={VideoBox._nativeControls}
onPlay={() => this.Play()}
onSeeked={this.updateTimecode}
onPause={() => this.Pause()}
- onClick={this._fullScreen ? () => this.playing() ? this.Pause() : this.Play() : e => e.preventDefault()}>
+ onClick={this._fullScreen ? () => (this.playing() ? this.Pause() : this.Play()) : e => e.preventDefault()}>
<source src={field.url.href} type="video/mp4" />
Not supported.
</video>
- {!this.audiopath || this.audiopath === field.url.href ? (null) :
- <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
+ {!this.audiopath || this.audiopath === field.url.href ? null : (
+ <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? '-interactive' : ''}`}>
<source src={this.audiopath} type="audio/mpeg" />
Not supported.
- </audio>}
+ </audio>
+ )}
</div>
- </div>;
+ </div>
+ );
}
-
@action youtubeIframeLoaded = (e: any) => {
if (!this._youtubeContentCreated) {
this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
return;
- }
- else this._youtubeContentCreated = false;
+ } else this._youtubeContentCreated = false;
this.loadYouTube(e.target);
- }
+ };
loadYouTube = (iframe: any) => {
let started = true;
- const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
- if (started && event.data === YT.PlayerState.PLAYING) {
- started = false;
- this._youtubePlayer?.unMute();
- //this.Pause();
- return;
- }
- if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
- if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
- });
+ const onYoutubePlayerStateChange = (event: any) =>
+ runInAction(() => {
+ if (started && event.data === YT.PlayerState.PLAYING) {
+ started = false;
+ this._youtubePlayer?.unMute();
+ //this.Pause();
+ return;
+ }
+ if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
+ if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
+ });
const onYoutubePlayerReady = (event: any) => {
this._disposers.reactionDisposer?.();
this._disposers.youtubeReactionDisposer?.();
- this._disposers.reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode)));
+ this._disposers.reactionDisposer = reaction(
+ () => this.layoutDoc._currentTimecode,
+ () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode))
+ );
this._disposers.youtubeReactionDisposer = reaction(
- () => CurrentUserUtils.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
- (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true });
+ () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
+ interactive => (iframe.style.pointerEvents = interactive ? 'all' : 'none'),
+ { fireImmediately: true }
+ );
};
- if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100);
+ if (typeof YT === undefined) setTimeout(() => this.loadYouTube(iframe), 100);
else {
(YT as any)?.ready(() => {
this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, {
events: {
- 'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady,
- 'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
- }
+ onReady: this.props.dontRegisterView ? undefined : onYoutubePlayerReady,
+ onStateChange: this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
+ },
});
});
}
- }
-
+ };
// for play button
- onPlayDown = () => this._playing ? this.Pause() : this.Play();
+ onPlayDown = () => (this._playing ? this.Pause() : this.Play());
// for fullscreen button
onFullDown = (e: React.PointerEvent) => {
this.FullScreen();
e.stopPropagation();
e.preventDefault();
- }
+ };
// for snapshot button
onSnapshotDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, (e) => {
- this.Snapshot(e.clientX, e.clientY);
- return true;
- }, emptyFunction, () => this.Snapshot());
- }
+ setupMoveUpEvents(
+ this,
+ e,
+ e => {
+ this.Snapshot(e.clientX, e.clientY);
+ return true;
+ },
+ emptyFunction,
+ () => this.Snapshot()
+ );
+ };
// for show/hide timeline button, transitions between show/hide
@action
onTimelineHdlDown = (e: React.PointerEvent) => {
this._clicking = true;
- setupMoveUpEvents(this, e,
+ setupMoveUpEvents(
+ this,
+ e,
action(encodeURIComponent => {
this._clicking = false;
if (this.props.isContentActive()) {
@@ -626,13 +682,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.layoutDoc._timelineHeightPercent = 80;
}
return false;
- }), emptyFunction,
+ }),
+ emptyFunction,
() => {
this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
- setTimeout(action(() => this._clicking = false), 500);
- }, this.props.isContentActive(), this.props.isContentActive());
- }
-
+ setTimeout(
+ action(() => (this._clicking = false)),
+ 500
+ );
+ },
+ this.props.isContentActive(),
+ this.props.isContentActive()
+ );
+ };
// removes video from currently playing display
@action
@@ -641,7 +703,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
}
- }
+ };
// adds video to currently playing display
@action
@@ -652,31 +714,36 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
}
- }
-
+ };
@computed get youtubeContent() {
this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
- const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+ const classname = 'videoBox-content-YouTube' + (this._fullScreen ? '-fullScreen' : '');
const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode)));
- return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
- onPointerLeave={this.updateTimecode}
- onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
- src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
+ return (
+ <iframe
+ key={this._youtubeIframeId}
+ id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
+ onPointerLeave={this.updateTimecode}
+ onLoad={this.youtubeIframeLoaded}
+ className={classname}
+ width={Doc.NativeWidth(this.layoutDoc) || 640}
+ height={Doc.NativeHeight(this.layoutDoc) || 390}
+ src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`}
+ />
+ );
}
-
// for annotating, adds doc with time info
@action.bound
addDocWithTimecode(doc: Doc | Doc[]): boolean {
const docs = doc instanceof Doc ? [doc] : doc;
const curTime = NumCast(this.layoutDoc._currentTimecode);
- docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1);
+ docs.forEach(doc => (doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1));
return this.addDocument(doc);
}
-
// play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
@action
playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
@@ -684,8 +751,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._playRegionTimer = undefined;
if (Number.isNaN(this.player?.duration)) {
setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
- }
- else if (this.player) {
+ } else if (this.player) {
// trimBounds override requested playback bounds
const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration);
const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds);
@@ -698,20 +764,18 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._audioPlayer?.play();
this._playing = true;
this.addCurrentlyPlaying();
- this._playRegionTimer = setTimeout(
- () => {
- // need to keep track of if end of clip is reached so on next play, clip restarts
- if (fullPlay) this._finished = true;
- // removes from currently playing if playback has reached end of range marker
- else this.removeCurrentlyPlaying();
- this.Pause();
- }, playRegionDuration * 1000);
+ this._playRegionTimer = setTimeout(() => {
+ // need to keep track of if end of clip is reached so on next play, clip restarts
+ if (fullPlay) this._finished = true;
+ // removes from currently playing if playback has reached end of range marker
+ else this.removeCurrentlyPlaying();
+ this.Pause();
+ }, playRegionDuration * 1000);
} else {
this.Pause();
}
}
- }
-
+ };
// ends trim, hides trim controls and displays new clip
@undoBatch
@@ -725,22 +789,28 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
startTrim = (scope: TrimScope) => {
this.Pause();
this.timeline?.StartTrimming(scope);
- }
+ };
// for trim button, double click displays full clip, single displays curr trim bounds
onClipPointerDown = (e: React.PointerEvent) => {
// if timeline isn't shown, show first then trim
this.heightPercent >= 100 && this.onTimelineHdlDown(e);
- this.timeline && setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => {
- if (doubleTap) {
- this.startTrim(TrimScope.All);
- } else if (this.timeline) {
- this.Pause();
- this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip);
- }
- }));
- }
-
+ this.timeline &&
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ returnFalse,
+ action((e: PointerEvent, doubleTap?: boolean) => {
+ if (doubleTap) {
+ this.startTrim(TrimScope.All);
+ } else if (this.timeline) {
+ this.Pause();
+ this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip);
+ }
+ })
+ );
+ };
// for volume slider sets volume
@action
@@ -752,7 +822,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.toggleMute();
}
}
- }
+ };
// toggles video mute
@action
@@ -761,62 +831,68 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._muted = !this._muted;
this.player.muted = this._muted;
}
- }
-
+ };
// stretches vertically or horizontally depending on video orientation so video fits full screen
fullScreenSize() {
if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
- return { height: "100%" };
- }
- else {
- return { width: "100%" };
+ return { height: '100%' };
+ } else {
+ return { width: '100%' };
}
}
-
// for zoom slider, sets timeline waveform zoom
zoom = (zoom: number) => {
this.timeline?.setZoom(zoom);
- }
-
+ };
// plays link
playLink = (doc: Doc) => {
- const startTime = Math.max(0, (this._stackedTimeline?.anchorStart(doc) || 0));
+ const startTime = Math.max(0, this._stackedTimeline?.anchorStart(doc) || 0);
const endTime = this.timeline?.anchorEnd(doc);
if (startTime !== undefined) {
if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
else this.Seek(startTime);
}
- }
-
+ };
// starts marquee selection
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.ActiveTool)) {
- setupMoveUpEvents(this, e, action(e => {
- MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
- this._marqueeing = [e.clientX, e.clientY];
- return true;
- }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+ if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
+ setupMoveUpEvents(
+ this,
+ e,
+ action(e => {
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+ this._marqueeing = [e.clientX, e.clientY];
+ return true;
+ }),
+ returnFalse,
+ () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations),
+ false
+ );
}
- }
+ };
// ends marquee selection
@action
finishMarquee = () => {
this._marqueeing = undefined;
this.props.select(true);
- }
+ };
- timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
+ timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)));
- timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight());
+ timelineScreenToLocal = () =>
+ this.props
+ .ScreenToLocalTransform()
+ .scale(this.scaling())
+ .translate(0, (-this.heightPercent / 100) * this.props.PanelHeight());
- setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
+ setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._currentTimecode = time);
- timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
+ timelineHeight = () => (this.props.PanelHeight() * (100 - this.heightPercent)) / 100;
playing = () => this._playing;
@@ -824,20 +900,22 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
scaling = () => this.props.scaling?.() || 1;
- panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100;
- panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100;
+ panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100;
+ panelHeight = () => (this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100);
screenToLocalTransform = () => {
const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
- return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
- }
+ return this.props
+ .ScreenToLocalTransform()
+ .translate(-offset, 0)
+ .scale(100 / this.heightPercent);
+ };
- marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100;
- marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0];
+ marqueeFitScaling = () => ((this.props.scaling?.() || 1) * this.heightPercent) / 100;
+ marqueeOffset = () => [((this.panelWidth() / 2) * (1 - this.heightPercent / 100)) / (this.heightPercent / 100), 0];
timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
-
// renders video controls
componentUI = (boundsLeft: number, boundsTop: number) => {
const bounds = this.props.docViewPath().lastElement().getBounds();
@@ -848,130 +926,157 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const width = Math.max(right - left, 100);
const uiHeight = Math.max(25, Math.min(50, height / 10));
const uiMargin = Math.min(10, height / 20);
- const vidHeight = height * this.heightPercent / 100;
+ const vidHeight = (height * this.heightPercent) / 100;
const yPos = top + vidHeight - uiHeight - uiMargin;
const xPos = uiHeight / vidHeight > 0.4 ? right + 10 : left + 10;
- const opacity = this._scrubbing ? 0.3 : (this._controlsVisible ? 1 : 0);
- return this._fullScreen || (right - left) < 50 ? null : <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
- <div className="videoBox-ui" style={{ left: xPos, top: yPos, height: uiHeight, width: width - 20, transition: this._clicking ? "top 0.5s" : "", opacity: opacity}}>
- {this.UIButtons}
+ const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0;
+ return this._fullScreen || right - left < 50 ? null : (
+ <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
+ <div className="videoBox-ui" style={{ left: xPos, top: yPos, height: uiHeight, width: width - 20, transition: this._clicking ? 'top 0.5s' : '', opacity: opacity }}>
+ {this.UIButtons}
+ </div>
</div>
- </div>
- }
+ );
+ };
@computed get UIButtons() {
const bounds = this.props.docViewPath().lastElement().getBounds();
const width = (bounds?.right || 0) - (bounds?.left || 0);
const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
- return <>
- <div className="videobox-button"
- title={this._playing ? "play" : "pause"}
- onPointerDown={this.onPlayDown}>
- <FontAwesomeIcon icon={this._playing ? "pause" : "play"} />
- </div>
-
- {this.timeline && width > 150 && <div className="timecode-controls">
- <div className="timecode-current">
- {formatTime(curTime)}
+ return (
+ <>
+ <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}>
+ <FontAwesomeIcon icon={this._playing ? 'pause' : 'play'} />
</div>
- {this._fullScreen || (this.heightPercent === 100 && width > 200) ?
- <div className="timeline-slider">
- <input type="range" step="0.1" min={this.timeline.clipStart} max={this.timeline.clipEnd} value={curTime}
- className="toolbar-slider time-progress"
- onPointerDown={action((e: React.PointerEvent) => { e.stopPropagation(); this._scrubbing = true;})}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
- onPointerUp={action((e: React.PointerEvent) => {e.stopPropagation(); this._scrubbing = false;})}
- />
+ {this.timeline && width > 150 && (
+ <div className="timecode-controls">
+ <div className="timecode-current">{formatTime(curTime)}</div>
+
+ {this._fullScreen || (this.heightPercent === 100 && width > 200) ? (
+ <div className="timeline-slider">
+ <input
+ type="range"
+ step="0.1"
+ min={this.timeline.clipStart}
+ max={this.timeline.clipEnd}
+ value={curTime}
+ className="toolbar-slider time-progress"
+ onPointerDown={action((e: React.PointerEvent) => {
+ e.stopPropagation();
+ this._scrubbing = true;
+ })}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
+ onPointerUp={action((e: React.PointerEvent) => {
+ e.stopPropagation();
+ this._scrubbing = false;
+ })}
+ />
+ </div>
+ ) : (
+ <div>/</div>
+ )}
+
+ <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div>
</div>
- :
- <div>/</div>}
-
- <div className="timecode-end">
- {formatTime(this.timeline.clipDuration)}
- </div>
- </div>
- }
-
- <div className="videobox-button"
- title={"full screen"}
- onPointerDown={this.onFullDown}>
- <FontAwesomeIcon icon="expand" />
- </div>
-
- {
- !this._fullScreen && width > 300 && <div className="videobox-button"
- title={"show timeline"}
- onPointerDown={this.onTimelineHdlDown}>
- <FontAwesomeIcon icon="eye" />
- </div>
- }
+ )}
- {
- !this._fullScreen && width > 300 && <div className="videobox-button"
- title={this.timeline?.IsTrimming !== TrimScope.None ? "finish trimming" : "start trim"}
- onPointerDown={this.onClipPointerDown}>
- <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"} />
+ <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}>
+ <FontAwesomeIcon icon="expand" />
</div>
- }
- <div className="videobox-button"
- title={this._muted ? "unmute" : "mute"}
- onPointerDown={(e) => { e.stopPropagation(); this.toggleMute(); }}>
- <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} />
- </div>
- {
- width > 300 && <input type="range" style={{ width: `min(25%, 50px)` }} step="0.1" min="0" max="1" value={this._muted ? 0 : this._volume}
- className="toolbar-slider volume"
- onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
- />
- }
+ {!this._fullScreen && width > 300 && (
+ <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}>
+ <FontAwesomeIcon icon="eye" />
+ </div>
+ )}
- {
- !this._fullScreen && this.heightPercent !== 100 && width > 300 &&
- <>
- <div className="videobox-button" title="zoom">
- <FontAwesomeIcon icon="search-plus" />
+ {!this._fullScreen && width > 300 && (
+ <div className="videobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish trimming' : 'start trim'} onPointerDown={this.onClipPointerDown}>
+ <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} />
</div>
- <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor}
- className="toolbar-slider zoom"
- onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(Number(e.target.value)); }}
+ )}
+
+ <div
+ className="videobox-button"
+ title={this._muted ? 'unmute' : 'mute'}
+ onPointerDown={e => {
+ e.stopPropagation();
+ this.toggleMute();
+ }}>
+ <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} />
+ </div>
+ {width > 300 && (
+ <input
+ type="range"
+ style={{ width: `min(25%, 50px)` }}
+ step="0.1"
+ min="0"
+ max="1"
+ value={this._muted ? 0 : this._volume}
+ className="toolbar-slider volume"
+ onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
/>
- </>
- }
- </>
+ )}
+
+ {!this._fullScreen && this.heightPercent !== 100 && width > 300 && (
+ <>
+ <div className="videobox-button" title="zoom">
+ <FontAwesomeIcon icon="search-plus" />
+ </div>
+ <input
+ type="range"
+ step="0.1"
+ min="1"
+ max="5"
+ value={this.timeline?._zoomFactor}
+ className="toolbar-slider zoom"
+ onPointerDown={(e: React.PointerEvent) => {
+ e.stopPropagation();
+ }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ this.zoom(Number(e.target.value));
+ }}
+ />
+ </>
+ )}
+ </>
+ );
}
// renders CollectionStackedTimeline
@computed get renderTimeline() {
- return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
- <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props}
- fieldKey={this.annotationKey}
- dictationKey={this.fieldKey + "-dictation"}
- mediaPath={this.audiopath}
- renderDepth={this.props.renderDepth + 1}
- startTag={"_timecodeToShow" /* videoStart */}
- endTag={"_timecodeToHide" /* videoEnd */}
- bringToFront={emptyFunction}
- CollectionView={undefined}
- playFrom={this.playFrom}
- setTime={this.setPlayheadTime}
- playing={this.playing}
- isAnyChildContentActive={this.isAnyChildContentActive}
- whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
- moveDocument={this.moveDocument}
- addDocument={this.addDocument}
- removeDocument={this.removeDocument}
- ScreenToLocalTransform={this.timelineScreenToLocal}
- Play={this.Play}
- Pause={this.Pause}
- playLink={this.playLink}
- PanelHeight={this.timelineHeight}
- rawDuration={this.rawDuration}
- />
- </div>;
+ return (
+ <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
+ <CollectionStackedTimeline
+ ref={action((r: any) => (this._stackedTimeline = r))}
+ {...this.props}
+ fieldKey={this.annotationKey}
+ dictationKey={this.fieldKey + '-dictation'}
+ mediaPath={this.audiopath}
+ renderDepth={this.props.renderDepth + 1}
+ startTag={'_timecodeToShow' /* videoStart */}
+ endTag={'_timecodeToHide' /* videoEnd */}
+ bringToFront={emptyFunction}
+ CollectionView={undefined}
+ playFrom={this.playFrom}
+ setTime={this.setPlayheadTime}
+ playing={this.playing}
+ isAnyChildContentActive={this.isAnyChildContentActive}
+ whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ removeDocument={this.removeDocument}
+ ScreenToLocalTransform={this.timelineScreenToLocal}
+ Play={this.Play}
+ Pause={this.Pause}
+ playLink={this.playLink}
+ PanelHeight={this.timelineHeight}
+ rawDuration={this.rawDuration}
+ />
+ </div>
+ );
}
// renders annotation layer
@@ -982,59 +1087,72 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
savedAnnotations = () => this._savedAnnotations;
render() {
const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
- const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad;
- return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont}
- style={{
- pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined,
- borderRadius,
- overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined
- }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}>
- <div className="videoBox-viewer" onPointerDown={this.marqueeDown} >
- <div style={{
- position: "absolute", transition: this.transition,
- width: this.panelWidth(),
- height: this.panelHeight(),
- top: 0,
- left: (this.props.PanelWidth() - this.panelWidth()) / 2
+ const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / this.scaling()}px` : borderRad;
+ return (
+ <div
+ className="videoBox"
+ onContextMenu={this.specificContextMenu}
+ ref={this._mainCont}
+ style={{
+ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
+ borderRadius,
+ overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? 'auto' : undefined,
+ }}
+ onWheel={e => {
+ e.stopPropagation();
+ e.preventDefault();
}}>
- <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
- renderDepth={this.props.renderDepth + 1}
- fieldKey={this.annotationKey}
- CollectionView={undefined}
- isAnnotationOverlay={true}
- annotationLayerHostsContent={true}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
- ScreenToLocalTransform={this.screenToLocalTransform}
- docFilters={this.timelineDocFilter}
- select={emptyFunction}
- scaling={returnOne}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={this.addDocWithTimecode}>
- {this.contentFunc}
- </CollectionFreeFormView>
+ <div className="videoBox-viewer" onPointerDown={this.marqueeDown}>
+ <div
+ style={{
+ position: 'absolute',
+ transition: this.transition,
+ width: this.panelWidth(),
+ height: this.panelHeight(),
+ top: 0,
+ left: (this.props.PanelWidth() - this.panelWidth()) / 2,
+ }}>
+ <CollectionFreeFormView
+ {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ renderDepth={this.props.renderDepth + 1}
+ fieldKey={this.annotationKey}
+ CollectionView={undefined}
+ isAnnotationOverlay={true}
+ annotationLayerHostsContent={true}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ ScreenToLocalTransform={this.screenToLocalTransform}
+ docFilters={this.timelineDocFilter}
+ select={emptyFunction}
+ scaling={returnOne}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocWithTimecode}>
+ {this.contentFunc}
+ </CollectionFreeFormView>
+ </div>
+ {this.annotationLayer}
+ {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
+ <MarqueeAnnotator
+ rootDoc={this.rootDoc}
+ scrollTop={0}
+ down={this._marqueeing}
+ scaling={this.marqueeFitScaling}
+ docView={this.props.docViewPath().slice(-1)[0]}
+ containerOffset={this.marqueeOffset}
+ addDocument={this.addDocWithTimecode}
+ finishMarquee={this.finishMarquee}
+ savedAnnotations={this.savedAnnotations}
+ annotationLayer={this._annotationLayer.current}
+ mainCont={this._mainCont.current}
+ />
+ )}
+ {this.renderTimeline}
</div>
- {this.annotationLayer}
- {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
- <MarqueeAnnotator
- rootDoc={this.rootDoc}
- scrollTop={0}
- down={this._marqueeing}
- scaling={this.marqueeFitScaling}
- docView={this.props.docViewPath().slice(-1)[0]}
- containerOffset={this.marqueeOffset}
- addDocument={this.addDocWithTimecode}
- finishMarquee={this.finishMarquee}
- savedAnnotations={this.savedAnnotations}
- annotationLayer={this._annotationLayer.current}
- mainCont={this._mainCont.current}
- />}
- {this.renderTimeline}
</div>
- </div >);
+ );
}
}
-VideoBox._nativeControls = false; \ No newline at end of file
+VideoBox._nativeControls = false;