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 { ObjectField } from '../../../fields/ObjectField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
import { emptyFunction, formatTime, OmitKeys, returnEmptyString, 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 { OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { RecordingBox } from './RecordingBox';
import { PinProps, PresBox } from './trails';
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
 * User can trim video: nondestructive, just sets new bounds for playback and rendering timeline
 * Like images, users can zoom and pan and it has an overlay layer allowing for annotations on top of the video at different times
 */

@observer
export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
    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)
     * @param imageUri the bytes of the image
     * @param returnedFilename the base filename to store the image on the server
     * @param nosuffix optionally suppress creating multiple resolution images
     */
    public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
        try {
            const posting = Utils.prepend('/uploadURI');
            const returnedUri = await rp.post(posting, {
                body: {
                    uri: imageUri,
                    name: returnedFilename,
                    nosuffix,
                    replaceRootFilename,
                },
                json: true,
            });
            return returnedUri;
        } catch (e) {
            console.log('VideoBox :' + e);
        }
    }

    static _youtubeIframeCounter: number = 0;
    static heightPercent = 80; // height of video relative to videoBox when timeline is open
    static numThumbnails = 20;
    private _disposers: { [name: string]: IReactionDisposer } = {};
    private _youtubePlayer: YT.Player | undefined = undefined;
    private _videoRef: HTMLVideoElement | null = null; // <video> ref
    private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen
    private _youtubeIframeId: number = -1;
    private _youtubeContentCreated = false;
    private _audioPlayer: HTMLAudioElement | null = null;
    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
    private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
    private _playRegionTimer: any = null; // timeout for playback
    private _controlsFadeTimer: any = null; // timeout for controls fade

    @observable _stackedTimeline: any; // CollectionStackedTimeline ref
    @observable static _nativeControls: boolean; // default html controls
    @observable _marqueeing: number[] | undefined; // coords for marquee selection
    @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
    @observable _screenCapture = false;
    @observable _clicking = false; // used for transition between showing/hiding timeline
    @observable _forceCreateYouTubeIFrame = false;
    @observable _playTimer?: NodeJS.Timeout = undefined;
    @observable _fullScreen = false;
    @observable _playing = false;
    @observable _finished: boolean = false; // has playback reached end of clip
    @observable _volume: number = 1;
    @observable _muted: boolean = false;
    @observable _controlsTransform?: { X: number; Y: number };
    @observable _controlsVisible: boolean = true;
    @observable _scrubbing: boolean = false;

    @computed get links() {
        return DocListCast(this.dataDoc.links);
    }
    @computed get heightPercent() {
        return NumCast(this.layoutDoc._timelineHeightPercent, 100);
    } // current percent of video relative to VideoBox height
    // @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('/')) : '';
    }

    // 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 ?? '';
    }

    // returns the presentation data if it exists, null otherwise
    @computed get presentation() {
        const data = this.dataDoc[this.fieldKey + '-presentation'];
        return data ? JSON.parse(data) : null;
    }

    @computed private get timeline() {
        return this._stackedTimeline;
    }
    private get transition() {
        return this._clicking ? 'left 0.5s, width 0.5s, height 0.5s' : '';
    } // css transition for hiding/showing timeline
    public get player(): HTMLVideoElement | null {
        return this._videoRef;
    }

    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.
        if (this.youtubeVideoId) {
            const youtubeaspect = 400 / 315;
            const nativeWidth = Doc.NativeWidth(this.layoutDoc);
            const nativeHeight = Doc.NativeHeight(this.layoutDoc);
            if (!nativeWidth || !nativeHeight) {
                if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
                Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
                this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
            }
        }
        this.player && this.setPlayheadTime(0);
        document.addEventListener('keydown', this.keyEvents, true);

        if (this.presentation) {
            ReplayMovements.Instance.setVideoBox(this);
        }
    }

    componentWillUnmount() {
        this.removeCurrentlyPlaying();
        this.Pause();
        Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
        document.removeEventListener('keydown', this.keyEvents, true);

        if (this.presentation) {
            ReplayMovements.Instance.removeVideoBox();
        }
    }

    // handles key events, when timeline scrubs fade controls
    @action
    keyEvents = (e: KeyboardEvent) => {
        if (
            // need to include range inputs because after dragging time slider it becomes target element
            !(e.target instanceof HTMLInputElement && !(e.target.type === 'range')) &&
            this.props.isSelected(true)
        ) {
            switch (e.key) {
                case 'ArrowLeft':
                case 'ArrowRight':
                    clearTimeout(this._controlsFadeTimer);
                    this._scrubbing = true;
                    this._controlsFadeTimer = setTimeout(
                        action(() => (this._scrubbing = false)),
                        500
                    );
                    e.stopPropagation();
                    break;
            }
        }
    };

    // plays video
    @action public Play = (update: boolean = true) => {
        if (this._playRegionTimer) return;

        this._playing = true;
        const eleTime = this.player?.currentTime || 0;
        if (this.timeline) {
            let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime;

            if (this._finished) {
                // restarts video if reached end on previous play
                this._finished = false;
                start = this.timeline.trimStart;
            }

            try {
                this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
                update && this.player && this.playFrom(start, undefined, true);
                update && this._audioPlayer?.play();
                update && this._youtubePlayer?.playVideo();
                this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
            } catch (e) {
                console.log('Video Play Exception:', e);
            }
        }
        this.updateTimecode();
    };

    // goes to time
    @action public Seek(time: number) {
        try {
            this._youtubePlayer?.seekTo(Math.round(time), true);
        } catch (e) {
            console.log('Video Seek Exception:', e);
        }
        this.player && (this.player.currentTime = time);
        this._audioPlayer && (this._audioPlayer.currentTime = time);
        // TODO: revisit this and clean it
        if ((this.player?.currentTime || -1) < this.rawDuration) {
            this._finished = false;
        }
    }

    IsPlaying = () => this._playing;
    _keepCurrentlyPlaying = false;
    TogglePause = () => {
        if (!this._playing) this.Play();
        else {
            this._keepCurrentlyPlaying = true;
            this.pause();
            setTimeout(() => (this._keepCurrentlyPlaying = false));
        }
    };

    // pauses video
    @action public pause = (update: boolean = true) => {
        this._playing = false;
        try {
            update && this.player?.pause();
            update && this._audioPlayer?.pause();
            update && this._youtubePlayer?.pauseVideo();
            this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
            this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true);
        } catch (e) {
            console.log('Video Pause Exception:', e);
        }
        this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
        this._playTimer = undefined;
        this.updateTimecode();
        if (!this._finished) {
            clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play
        }
        this._playRegionTimer = undefined;
    };
    @action Pause = (update: boolean = true) => {
        this.pause(update);
        !this._keepCurrentlyPlaying && this.removeCurrentlyPlaying();
    };

    // toggles video full screen
    @action public FullScreen = () => {
        if (document.fullscreenElement === this._contentRef) {
            this._fullScreen = false;
            this.player && this._contentRef && document.exitFullscreen();
        } else {
            this._fullScreen = true;
            this.player && this._contentRef && this._contentRef.requestFullscreen();
        }
        try {
            this._youtubePlayer && this.props.addDocTab(this.rootDoc, OpenWhere.add);
        } catch (e) {
            console.log('Video FullScreen Exception:', e);
        }
    };

    // fades out controls in fullscreen after mouse stops moving
    @action controlsFade = (e: PointerEvent) => {
        e.stopPropagation();
        if (!this._scrubbing) {
            clearTimeout(this._controlsFadeTimer);
            this._controlsVisible = true;
            this._controlsFadeTimer = setTimeout(
                action(() => (this._controlsVisible = false)),
                3000
            );
        }
    };

    // drag controls around window in fulls screen
    @action controlsDrag = (e: React.PointerEvent) => {
        e.preventDefault();
        e.stopPropagation();
        const eleStyle = getComputedStyle(e.target as Element);
        this._controlsTransform = { X: parseInt(eleStyle.left), Y: parseInt(eleStyle.top) };

        setupMoveUpEvents(
            e.target,
            e,
            action((e, down, delta) => {
                if (this._controlsTransform) {
                    this._controlsTransform.X = Math.max(0, Math.min(delta[0] + this._controlsTransform.X, window.innerWidth));
                    this._controlsTransform.Y = Math.max(0, Math.min(delta[1] + this._controlsTransform.Y, window.innerHeight));
                }
                return false;
            }),
            emptyFunction,
            emptyFunction
        );
    };

    // creates and links snapshot photo of current video frame
    @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
        const width = NumCast(this.layoutDoc._width);
        const canvas = document.createElement('canvas');
        canvas.width = 640;
        canvas.height = (640 * Doc.NativeHeight(this.layoutDoc)) / (Doc.NativeWidth(this.layoutDoc) || 1);
        const ctx = canvas.getContext('2d'); //draw image to canvas. scale to target dimensions
        if (ctx) {
            this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
        }

        if (!this._videoRef) {
            const b = Docs.Create.LabelDocument({
                x: NumCast(this.layoutDoc.x) + width,
                y: NumCast(this.layoutDoc.y, 1),
                _width: 150,
                _height: 50,
                title: (this.layoutDoc._currentTimecode || 0).toString(),
                _isLinkButton: true,
            });
            this.props.addDocument?.(b);
            DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, 'video snapshot');
            Networking.PostToServer('/youtubeScreenshot', {
                id: this.youtubeVideoId,
                timecode: this.layoutDoc._currentTimecode,
            }).then(response => {
                const resolved = response?.accessPaths?.agnostic?.client;
                if (resolved) {
                    this.props.removeDocument?.(b);
                    this.createRealSummaryLink(resolved);
                }
            });
        } else {
            //convert to desired file format
            const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
            // if you want to preview the captured image,
            const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
            const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, '_'));
            const filename = basename(encodedFilename);
            VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
        }
    };

    updateIcon = () => {
        const makeIcon = (returnedfilename: string) => {
            this.dataDoc.icon = new ImageField(returnedfilename);
            this.dataDoc['icon-nativeWidth'] = this.layoutDoc[WidthSym]();
            this.dataDoc['icon-nativeHeight'] = this.layoutDoc[HeightSym]();
        };
        this.Snapshot(undefined, undefined, makeIcon);
    };

    // creates link for snapshot
    createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
        const url = !imagePath.startsWith('/') ? Utils.CorsProxy(imagePath) : imagePath;
        const width = NumCast(this.layoutDoc._width) || 1;
        const height = NumCast(this.layoutDoc._height);
        const imageSummary = Docs.Create.ImageDocument(url, {
            _nativeWidth: Doc.NativeWidth(this.layoutDoc),
            _nativeHeight: Doc.NativeHeight(this.layoutDoc),
            x: NumCast(this.layoutDoc.x) + width,
            y: NumCast(this.layoutDoc.y),
            _isLinkButton: true,
            _width: 150,
            _height: (height / width) * 150,
            title: '--snapshot' + NumCast(this.layoutDoc._currentTimecode) + ' image-',
        });
        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(true) }, 'video snapshot');
        link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
        setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, 'move', true));
    };

    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
        const timecode = Cast(this.layoutDoc._currentTimecode, 'number', null);
        const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
        if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent';
        const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.rootDoc;
        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc);
        return anchor;
    };

    // sets video info on load
    videoLoad = action(() => {
        const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1);
        if (aspect && !this.isCropped) {
            Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
            Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
            this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
        }
        if (Number.isFinite(this.player!.duration)) {
            this.rawDuration = this.player!.duration;
            this.dataDoc[this.fieldKey + '-duration'] = this.rawDuration;
        } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']);
    });

    // updates video time
    @action
    updateTimecode = () => {
        this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
        try {
            this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
        } catch (e) {
            console.log('Video Timecode Exception:', e);
        }
    };

    // extracts video thumbnails and saves them as field of doc
    getVideoThumbnails = () => {
        if (this.dataDoc.thumbnails !== undefined) return;
        this.dataDoc.thumbnails = new List<string>();
        const thumbnailPromises: Promise<any>[] = [];
        const video = document.createElement('video');

        video.onloadedmetadata = () => (video.currentTime = 0);

        video.onseeked = () => {
            const canvas = document.createElement('canvas');
            canvas.height = 100;
            canvas.width = 100;
            canvas.getContext('2d')?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, 100, 100);
            const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
            const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_'));
            thumbnailPromises?.push(VideoBox.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
            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)));
            }
        };

        const field = Cast(this.dataDoc[this.fieldKey], VideoField);
        field && (video.src = field.url.href);
    };

    // sets video element ref
    @action
    setVideoRef = (vref: HTMLVideoElement | null) => {
        this._videoRef = vref;
        if (vref) {
            this._videoRef!.ontimeupdate = this.updateTimecode;
            // @ts-ignore
            // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
            this._disposers.reactionDisposer?.();
            this._disposers.reactionDisposer = reaction(
                () => NumCast(this.layoutDoc._currentTimecode),
                time => !this._playing && (vref.currentTime = time),
                { fireImmediately: true }
            );

            (!this.dataDoc.thumbnails || this.dataDoc.thumbnails.length != VideoBox.numThumbnails) && this.getVideoThumbnails();
        }
    };

    // set ref for div that wraps video and controls for fullscreen
    @action
    setContentRef = (cref: HTMLDivElement | null) => {
        this._contentRef = cref;
        if (cref) {
            cref.onfullscreenchange = action(e => {
                this._fullScreen = document.fullscreenElement === cref;
                this._controlsVisible = true;
                this._scrubbing = false;
                clearTimeout(this._controlsFadeTimer);
                if (this._fullScreen) {
                    document.addEventListener('pointermove', this.controlsFade);
                } else {
                    document.removeEventListener('pointermove', this.controlsFade);
                }
            });
        }
    };

    // context menu
    specificContextMenu = (e: React.MouseEvent): void => {
        const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
        if (field) {
            const url = field.url.href;
            const subitems: ContextMenuProps[] = [];
            subitems.push({ description: 'Full Screen', event: this.FullScreen, icon: 'expand' });
            subitems.push({ description: 'Take Snapshot', event: this.Snapshot, icon: 'expand-arrows-alt' });
            this.rootDoc.type === DocumentType.SCREENSHOT &&
                subitems.push({
                    description: 'Screen Capture',
                    event: async () => {
                        runInAction(() => (this._screenCapture = !this._screenCapture));
                        this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
                    },
                    icon: 'expand-arrows-alt',
                });
            subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered', event: () => (this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks), icon: 'expand-arrows-alt' });
            subitems.push({
                description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected',
                event: () => (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks),
                icon: 'expand-arrows-alt',
            });
            subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : 'Auto play') + ' anchors onClick', event: () => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors), icon: 'expand-arrows-alt' });
            // subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
            // subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" });
            // subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" });
            // subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" });
            subitems.push({
                description: 'Copy path',
                event: () => {
                    Utils.CopyText(url);
                },
                icon: 'expand-arrows-alt',
            });
            // if the videobox was turned from a recording box
            if (this.dataDoc[this.fieldKey + '-recorded'] === true) {
                subitems.push({
                    description: 'Recreate recording',
                    event: () => {
                        this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
                        // delete assoicated video data
                        this.dataDoc[this.props.fieldKey] = '';
                        this.dataDoc[this.fieldKey + '-duration'] = '';
                        // delete assoicated presentation data
                        this.dataDoc[this.fieldKey + '-presentation'] = '';
                    },
                    icon: 'expand-arrows-alt',
                });
            }
            ContextMenu.Instance.addItem({ description: 'Options...', subitems: subitems, icon: 'video' });
        }
    };

    // ref for updating time
    setAudioRef = (e: HTMLAudioElement | null) => (this._audioPlayer = e);

    // renders the video and audio
    @computed get content() {
        const field = Cast(this.dataDoc[this.fieldKey], VideoField);
        const interactive = 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' : 'default' }}>
                <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() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._viewScale)})`, transformOrigin: 'top left' } : {}}
                        onCanPlay={this.videoLoad}
                        controls={VideoBox._nativeControls}
                        onPlay={() => this.Play()}
                        onSeeked={this.updateTimecode}
                        onPause={() => this.Pause()}
                        onClick={this._fullScreen ? () => (this.playing() ? this.Pause() : this.Play()) : e => e.preventDefault()}>
                        <source src={field.url.href} type="video/mp4" />
                        Not supported.
                    </video>
                    {!this.audiopath || this.audiopath === field.url.href ? null : (
                        <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? '-interactive' : ''}`}>
                            <source src={this.audiopath} type="audio/mpeg" />
                            Not supported.
                        </audio>
                    )}
                </div>
            </div>
        );
    }

    @action youtubeIframeLoaded = (e: any) => {
        if (!this._youtubeContentCreated) {
            this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
            return;
        } else this._youtubeContentCreated = false;

        this.loadYouTube(e.target);
    };

    loadYouTube = (iframe: any) => {
        let started = true;
        const onYoutubePlayerStateChange = (event: any) =>
            runInAction(() => {
                if (started && event.data === YT.PlayerState.PLAYING) {
                    started = false;
                    this._youtubePlayer?.unMute();
                    //this.Pause();
                    return;
                }
                if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
                if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
            });
        const onYoutubePlayerReady = (event: any) => {
            this._disposers.reactionDisposer?.();
            this._disposers.youtubeReactionDisposer?.();
            this._disposers.reactionDisposer = reaction(
                () => this.layoutDoc._currentTimecode,
                () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode))
            );
            this._disposers.youtubeReactionDisposer = reaction(
                () => 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);
        else {
            (YT as any)?.ready(() => {
                this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, {
                    events: {
                        onReady: this.props.dontRegisterView ? undefined : onYoutubePlayerReady,
                        onStateChange: this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
                    },
                });
            });
        }
    };

    // for play button
    onPlayDown = () => (this._playing ? this.Pause() : this.Play());

    // for fullscreen button
    onFullDown = (e: React.PointerEvent) => {
        this.FullScreen();
        e.stopPropagation();
        e.preventDefault();
    };

    // for snapshot button
    onSnapshotDown = (e: React.PointerEvent) => {
        setupMoveUpEvents(
            this,
            e,
            e => {
                this.Snapshot(e.clientX, e.clientY);
                return true;
            },
            emptyFunction,
            () => this.Snapshot()
        );
    };

    // for show/hide timeline button, transitions between show/hide
    @action
    onTimelineHdlDown = (e: React.PointerEvent) => {
        this._clicking = true;
        setupMoveUpEvents(
            this,
            e,
            action(encodeURIComponent => {
                this._clicking = false;
                if (this.props.isContentActive()) {
                    // const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
                    // this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));

                    this.layoutDoc._timelineHeightPercent = 80;
                }
                return false;
            }),
            emptyFunction,
            () => {
                this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
                setTimeout(
                    action(() => (this._clicking = false)),
                    500
                );
            },
            this.props.isContentActive(),
            this.props.isContentActive()
        );
    };

    // removes from currently playing display
    @action
    removeCurrentlyPlaying = () => {
        const docView = this.props.DocumentView?.();
        if (CollectionStackedTimeline.CurrentlyPlaying && docView) {
            const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView);
            index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
        }
    };
    // adds doc to currently playing display
    @action
    addCurrentlyPlaying = () => {
        const docView = this.props.DocumentView?.();
        if (!CollectionStackedTimeline.CurrentlyPlaying) {
            CollectionStackedTimeline.CurrentlyPlaying = [];
        }
        if (docView && CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView) === -1) {
            CollectionStackedTimeline.CurrentlyPlaying.push(docView);
        }
    };

    @computed get youtubeContent() {
        this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
        this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
        const classname = 'videoBox-content-YouTube' + (this._fullScreen ? '-fullScreen' : '');
        const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode)));
        return (
            <iframe
                key={this._youtubeIframeId}
                id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
                onPointerLeave={this.updateTimecode}
                onLoad={this.youtubeIframeLoaded}
                className={classname}
                width={Doc.NativeWidth(this.layoutDoc) || 640}
                height={Doc.NativeHeight(this.layoutDoc) || 390}
                src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`}
            />
        );
    }

    // for annotating, adds doc with time info
    @action.bound
    addDocWithTimecode(doc: Doc | Doc[]): boolean {
        const docs = doc instanceof Doc ? [doc] : doc;
        const curTime = NumCast(this.layoutDoc._currentTimecode);
        docs.forEach(doc => (doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1));
        return this.addDocument(doc);
    }

    // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
    @action
    playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
        clearTimeout(this._playRegionTimer);
        this._playRegionTimer = undefined;
        if (Number.isNaN(this.player?.duration)) {
            setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
        } else if (this.player) {
            // trimBounds override requested playback bounds
            const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration);
            const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds);
            const playRegionDuration = end - start;
            // checks if times are within clip range
            if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.rawDuration)) {
                this.player.currentTime = start;
                this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds);
                this.player.play();
                this._audioPlayer?.play();
                this._playing = true;
                this.addCurrentlyPlaying();
                this._playRegionTimer = setTimeout(() => {
                    // need to keep track of if end of clip is reached so on next play, clip restarts
                    if (fullPlay) this._finished = true;
                    // removes from currently playing if playback has reached end of range marker
                    else this.removeCurrentlyPlaying();
                    this.Pause();
                }, playRegionDuration * 1000);
            } else {
                this.Pause();
            }
        }
    };

    // ends trim, hides trim controls and displays new clip
    @undoBatch
    finishTrim = action(() => {
        this.Pause();
        this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this.player!.currentTime), this.timeline?.trimStart || 0));
        this.timeline?.StopTrimming();
    });

    // displays trim controls to start trimming clip
    startTrim = (scope: TrimScope) => {
        this.Pause();
        this.timeline?.StartTrimming(scope);
    };

    // for trim button, double click displays full clip, single displays curr trim bounds
    onClipPointerDown = (e: React.PointerEvent) => {
        // if timeline isn't shown, show first then trim
        this.heightPercent >= 100 && this.onTimelineHdlDown(e);
        this.timeline &&
            setupMoveUpEvents(
                this,
                e,
                returnFalse,
                returnFalse,
                action((e: PointerEvent, doubleTap?: boolean) => {
                    if (doubleTap) {
                        this.startTrim(TrimScope.All);
                    } else if (this.timeline) {
                        this.Pause();
                        this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip);
                    }
                })
            );
    };

    // for volume slider sets volume
    @action
    setVolume = (volume: number) => {
        if (this.player) {
            this._volume = volume;
            this.player.volume = volume;
            if (this._muted) {
                this.toggleMute();
            }
        }
    };

    // toggles video mute
    @action
    toggleMute = () => {
        if (this.player) {
            this._muted = !this._muted;
            this.player.muted = this._muted;
        }
    };

    // stretches vertically or horizontally depending on video orientation so video fits full screen
    fullScreenSize() {
        if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
            //prettier-ignore
            return ({ height: '100%' });
        }
        //prettier-ignore
        return ({ width: '100%' });
    }

    // for zoom slider, sets timeline waveform zoom
    zoom = (zoom: number) => this.timeline?.setZoom(zoom);

    // plays link
    playLink = (doc: Doc) => {
        const startTime = Math.max(0, this._stackedTimeline?.anchorStart(doc) || 0);
        const endTime = this.timeline?.anchorEnd(doc);
        if (startTime !== undefined) {
            if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
            else this.Seek(startTime);
        }
    };

    // starts marquee selection
    marqueeDown = (e: React.PointerEvent) => {
        if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._viewScale, 1) === 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)));

    timelineScreenToLocal = () =>
        this.props
            .ScreenToLocalTransform()
            .scale(this.scaling())
            .translate(0, (-this.heightPercent / 100) * this.props.PanelHeight());

    setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._currentTimecode = time);

    timelineHeight = () => (this.props.PanelHeight() * (100 - this.heightPercent)) / 100;

    playing = () => this._playing;

    contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];

    scaling = () => this.props.NativeDimScaling?.() || 1;

    panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100;
    panelHeight = () => (this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100);

    screenToLocalTransform = () => {
        const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
        return this.props
            .ScreenToLocalTransform()
            .translate(-offset, 0)
            .scale(100 / this.heightPercent);
    };

    marqueeFitScaling = () => ((this.props.NativeDimScaling?.() || 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 xf = this.props.ScreenToLocalTransform().inverse();
        const height = this.props.PanelHeight();
        const vidHeight = (height * this.heightPercent) / 100 / this.scaling();
        const vidWidth = this.props.PanelWidth() / this.scaling();
        const uiHeight = 25;
        const uiMargin = 10;
        const yBot = xf.transformPoint(0, vidHeight)[1];
        // prettier-ignore
        const yMid =  (xf.transformPoint(0, 0)[1] +
                       xf.transformPoint(0, height / this.scaling())[1]) /  2;
        const xPos = xf.transformPoint(vidWidth / 2, 0)[0];
        const xRight = xf.transformPoint(vidWidth, 0)[0];
        const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0;
        return this._fullScreen || this.isCropped || (xRight - xPos) * 2 < 50 ? null : (
            <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
                <div
                    className="videoBox-ui"
                    style={{
                        transformOrigin: 'top left',
                        transform: `rotate(${NumCast(this.rootDoc.rotation)}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`,
                        left: xPos,
                        top: yMid,
                        height: uiHeight,
                        width: (xRight - xPos) * 2 - 20,
                        transition: this._clicking ? 'top 0.5s' : '',
                        opacity,
                    }}>
                    {this.UIButtons}
                </div>
            </div>
        );
    };

    // renders CollectionStackedTimeline
    @computed get renderTimeline() {
        return (
            <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%`, display: this.heightPercent === 100 ? 'none' : '' }}>
                <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>
        );
    }
    @computed get isCropped() {
        return this.dataDoc.videoCrop; // bcz: hack to identify a cropped video
    }

    // renders annotation layer
    @computed get annotationLayer() {
        return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
    }

    crop = (region: Doc | undefined, addCrop?: boolean) => {
        if (!region) return;
        const cropping = Doc.MakeCopy(region, true);
        Doc.GetProto(region).backgroundColor = 'transparent';
        Doc.GetProto(region).lockedPosition = true;
        Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
        Doc.GetProto(region).followLinkToggle = true;
        region._timecodeToHide = NumCast(region._timecodeToShow) + 0.0001;
        this.addDocument(region);
        const anchx = NumCast(cropping.x);
        const anchy = NumCast(cropping.y);
        const anchw = NumCast(cropping._width);
        const anchh = NumCast(cropping._height);
        const viewScale = NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']) / anchw;
        cropping.title = 'crop: ' + this.rootDoc.title;
        cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width);
        cropping.y = NumCast(this.rootDoc.y);
        cropping._width = anchw * (this.props.NativeDimScaling?.() || 1);
        cropping._height = anchh * (this.props.NativeDimScaling?.() || 1);
        cropping.timecodeToHide = undefined;
        cropping.timecodeToShow = undefined;
        cropping.isLinkButton = undefined;
        const croppingProto = Doc.GetProto(cropping);
        croppingProto.annotationOn = undefined;
        croppingProto.isPrototype = true;
        croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
        croppingProto.type = DocumentType.VID;
        croppingProto.layout = VideoBox.LayoutString('data');
        croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField);
        croppingProto['data-nativeWidth'] = anchw;
        croppingProto['data-nativeHeight'] = anchh;
        croppingProto.videoCrop = true;
        croppingProto.currentTimecode = this.layoutDoc._currentTimecode;
        croppingProto.viewScale = viewScale;
        croppingProto.viewScaleMin = viewScale;
        croppingProto.panX = anchx / viewScale;
        croppingProto.panY = anchy / viewScale;
        croppingProto.panXMin = anchx / viewScale;
        croppingProto.panXMax = anchw / viewScale;
        croppingProto.panYMin = anchy / viewScale;
        croppingProto.panYMax = anchh / viewScale;
        if (addCrop) {
            DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
        }
        this.props.bringToFront(cropping);
        return cropping;
    };

    savedAnnotations = () => this._savedAnnotations;
    render() {
        const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
        const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / this.scaling()}px` : borderRad;
        return (
            <div
                className="videoBox"
                onContextMenu={this.specificContextMenu}
                ref={this._mainCont}
                style={{
                    pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
                    borderRadius,
                    overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? 'auto' : undefined,
                }}
                onWheel={e => {
                    e.stopPropagation();
                    e.preventDefault();
                }}>
                <div className="videoBox-viewer" onPointerDown={this.marqueeDown}>
                    <div
                        style={{
                            position: 'absolute',
                            transition: this.transition,
                            width: this.panelWidth(),
                            height: this.panelHeight(),
                            top: 0,
                            left: (this.props.PanelWidth() - this.panelWidth()) / 2,
                        }}>
                        <CollectionFreeFormView
                            {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
                            renderDepth={this.props.renderDepth + 1}
                            fieldKey={this.annotationKey}
                            CollectionView={undefined}
                            isAnnotationOverlay={true}
                            annotationLayerHostsContent={true}
                            PanelWidth={this.panelWidth}
                            PanelHeight={this.panelHeight}
                            ScreenToLocalTransform={this.screenToLocalTransform}
                            docFilters={this.timelineDocFilter}
                            select={emptyFunction}
                            NativeDimScaling={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}
                            selectionText={returnEmptyString}
                            annotationLayer={this._annotationLayer.current}
                            mainCont={this._mainCont.current}
                            anchorMenuCrop={this.crop}
                        />
                    )}
                    {this.renderTimeline}
                </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)}</div>

                        {this._fullScreen || (this.heightPercent === 100 && width > 200) ? (
                            <div className="timeline-slider">
                                <input
                                    type="range"
                                    step="0.1"
                                    min={this.timeline.clipStart}
                                    max={this.timeline.clipEnd}
                                    value={curTime}
                                    className="toolbar-slider time-progress"
                                    onPointerDown={action((e: React.PointerEvent) => {
                                        e.stopPropagation();
                                        this._scrubbing = true;
                                    })}
                                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
                                    onPointerUp={action((e: React.PointerEvent) => {
                                        e.stopPropagation();
                                        this._scrubbing = false;
                                    })}
                                />
                            </div>
                        ) : (
                            <div>/</div>
                        )}

                        <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div>
                    </div>
                )}

                <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}>
                    <FontAwesomeIcon icon="expand" />
                </div>

                {!this._fullScreen && width > 300 && (
                    <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}>
                        <FontAwesomeIcon icon="eye" />
                    </div>
                )}

                {!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>
                )}

                <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));
                            }}
                        />
                    </>
                )}
            </>
        );
    }
}

VideoBox._nativeControls = false;
