import mpegts from "./mpegts.js";
import Features from "./core/features.js";
import H5msClient from "./h5msClient.js";
import httpClient from "./core/httpClient";
import nipplejs from "./utils/nipplejs";
import {getJoysticPositionFlag} from "./utils/ptzController";
import {Observable, fromEvent, interval, timer, Subject} from "rxjs";
import {
    debounceTime,
    concatAll,
    distinct,
    takeUntil,
    startWith,
    distinctUntilChanged,
    filter,
    take,
    raceWith,
    throttleTime,
    map,
} from "rxjs/operators"
import {Subscription} from "../node_modules/rxjs/dist/types/index"

import {
    exitFullScreen,
    filterAisleIdChart,
    getNowDate,
    guid,
    openFullscreen,
    getNormalizedProtocol
} from "./utils/utils.js";

import {
    createDefaultStreamOpt,
    Direction,
    JPEvent,
    PlayerMode,
    StreamOpt,
    StreamSpeed,
    StyleSize,
} from "./model/playerModel";

import {EventEmitter} from "events";
import PlayerEvents from "./player/player-events.js";
import {
    createErrorDom,
    createLoaderDom2,
    creatHeaderToolBar,
    creatFooterToolBar,
    createSeekDom,
    createZoomDom,
    getDirection,
    createSettingMenuDom,
    createMobileWindowDom,
    creatMobileFooterToolBar,
    creatJoystickToolBar,
    creatMobileSilderToolBar,
    createRectangleDom,
    createDrawTipBtnDom,
} from "./utils/controlDom";
import {ERRORMSG} from "./utils/codemsg";
import {ContextMenu} from "./utils/context.js";
import {formatTimeClock} from "./utils/date";
import Browser from "./utils/browser.js";
import GlobalClient from "./globalClient.js";
import {serverName} from "./globalClient";
import * as Hammer from "hammerjs";
import {rectDraw} from "./utils/draw";
import TalkCtrl from "./utils/talk";
import { getSeekableBlob } from './utils/utils'

import audioCtrl from "./utils/audioCtrl/index";
import vodPlayer from "./utils/vod/index";

declare let __VERSION__;


const preventsDefault = (event: MouseEvent) => {
    event.preventDefault();
    event.stopPropagation();
    return event;
};

interface HTMLMediaElementWithCaputreStream extends HTMLMediaElement {
    captureStream(): MediaStream;

    mozCaptureStream(): MediaStream;
}

declare const MediaRecorder;

export class JPlayerMediaRecorder {
    private recorder: any;
    emitter: any;
    private timer$: any;
    private $stop = new Subject();
    private subscriptiontimer: Subscription;

    constructor(public mediaElement: unknown, time) {
        let options;
        if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9')) {
            options = { mimeType: 'video/webm;codecs=vp9' };
        } else if (MediaRecorder.isTypeSupported('video/webm;codecs=vp8')) {
            options = { mimeType: 'video/webm;codecs=vp8' };
        } else {
            options = { mimeType: 'video/webm' }; // 默认选项
        }

        const emitter = this.emitter = new EventEmitter();
        this.recorder = new MediaRecorder(
          Browser.name === 'firefox' ? (mediaElement as HTMLMediaElementWithCaputreStream).mozCaptureStream() : (mediaElement as HTMLMediaElementWithCaputreStream).captureStream(),
            options
        );
        const _chunk: Blob[] = [];
        const handleDataAvailable = (event) => {
            if (event.data.size > 0) {
                _chunk.push(event.data);
            } else {
                // ...
            }
        };
        this.recorder.ondataavailable = handleDataAvailable;
        this.recorder.onstop = () => {
          emitter.emit("complete", _chunk);
        }

        this.recorder.onerror = function(event) {
          console.error("录制错误: ", event.error);
        };

        if (time) {
          this.timer$ = timer(time * 1000).pipe(take(1));
          this.subscriptiontimer = this.$stop
            .asObservable()
            .pipe(raceWith(this.timer$))
            .subscribe(() => {
              this.recorder.stop();
            });
        }
    }

    on(event, listener) {
        this.emitter.addListener(event, listener);
    }

    off(event, listener) {
        this.emitter.removeListener(event, listener);
    }

    start() {
      try {
        this.recorder.start();
      } catch (e) {
        if (e.name === 'NotSupportedError') {
          console.warn('MediaRecorder 无法启动：没有可用的音频或视频轨道。');
        } else {
          console.error('An error occurred: ', e);
        }
      }
    }

    stop() {
        this.recorder.stop();
        // this.$stop.next(null);
    }

    destroy() {
        // destroy MediaRecorder
        this.recorder = null;
        if (this.subscriptiontimer) {
            this.subscriptiontimer.unsubscribe()
            this.subscriptiontimer = null
        }
        this.timer$ = null;
        if (this.emitter) {
            this.emitter.removeAllListeners();
            this.emitter = null;
        }
    }
}

export class JPlayer {
    static get version() {
        return __VERSION__;
    }
    static isSupported() {
        return Features.supportMSEH264Playback();
    }
    private features: any;
    private globalClient: GlobalClient;
    private h5msClient: H5msClient;
    private nippleIns: any; // 移动端模式下手柄实例
    private timelineIns: any; // 移动端模式下时间尺实例
    private e: any;
    private vid: string;
    private prefixName: string;
    private _rootHammertime: any; // 移动端手势实例
    private vel: HTMLVideoElement;
    private cel: HTMLCanvasElement;
    private canvas: HTMLCanvasElement;
    private wrapperel: HTMLDivElement;
    private joystickele: HTMLDivElement;
    private videoWrapperEl: HTMLDivElement;
    private streamOpt: StreamOpt;
    private playUrl: string; // 流地址
    private streamId: StreamOpt; // 流id
    private contextMenu; // 右键菜单实例
    private rectBlock; // 绘制矩形实例
    private playerIns: any; // mpegts实例
    private _currentTime: number = 0; // mpegts实例
    private _audioCtrl: any; // mpegts实例
    private _videoStream: any; // 对话实例
    private lastDecodedFrame: any
    private readonly MAX_OPEN_STREAM_RETRY_COUNT = 3
    private readonly MAX_AUTO_RECOVER_COUNT = 3
    private readonly DEFAULT_STABLE_PLAY_SECONDS = 600
    private readonly MAX_STABLE_PLAY_SECONDS = 600
    private AUTO_RECOVER_RESET_DELAY_MS: number = this.DEFAULT_STABLE_PLAY_SECONDS
    _talkCtrl: any; // 对话实例
    _vodPlayer: any; // 回放功能
    private osdTimeId: any = null; // OSD时间
    private _canPlayTimer: any = null; // CANPLAY 延时定时器
    private videoRatio: {
        w?: number;
        h?: number;
    } = {
        w: null,
        h: null,
    };
    private playerOnCanPlay: boolean = false; // mpegts实例
    private _playing: boolean = false; // 是否在播放中
    private _ptzing: boolean = false; // 是否在云台控制
    private _ptzLoad: boolean = false; // 是否在云台控制
    private _zooming: boolean = false; // 是否在数码放大
    private _drawing: boolean = false; // 是否在绘制矩形中
    private _setting: boolean = false; // 设置菜单是否打开
    private _seeking: boolean = false; // 是否在跳转中
    public _ptzSpeed: number = 10; // 云台控制灵敏度
    private _isFullScreen: boolean; // 是否全屏
    private _offsetObj = {
        x: 0,
        y: 0,
    }; // 偏移对象
    private _ponitDragClick = {
        clientX: 0,
        offsetX: 0,
    };
    private _zoomStartPonit = {
        // 数码放大起始位置
        clientX: 0,
        clientY: 0,
        offsetX: 0,
        offsetY: 0,
    };
    private _zoomEndPonit = {
        // 数码放大末尾位置
        clientX: 0,
        clientY: 0,
        offsetX: 0,
        offsetY: 0,
    };
    private _begintime: number; // 起始时间 计算用
    private _endtime: number; //  结束时间
    private _offsetTime: number; //  偏移时间
    private _loading = true;
    private _recording = false; // 录制状态
    private _maxRetryTime = this.MAX_OPEN_STREAM_RETRY_COUNT; // 开流重试最大次数
    private _autoRecoverCount = 0; // 自动恢复次数
    private _enableAutoRecover = true; // 是否开启稳定播放后的自动恢复
    private _maxAutoRecoverCount = this.MAX_AUTO_RECOVER_COUNT; // 自动恢复最大次数
    private _autoRecoverResetTimer = null; // 稳定播放后重置自动恢复状态
    private _isStablePlaying = false; // 只有进入稳定播放态后，后续断流才允许自动恢复
    private _isAutoRecovering = false; // 防止同一轮错误重复触发 refresh
    private _size: StyleSize; // 容器大小
    private _timer = interval(1000); // 定时器
    private mediaRecorder: any; // 录制器
    private $resize: any; // Observable<any>; // 观察者容器
    private $click: any; //Observable<any>; // 观察者容器
    private $dblclick: any; //Observable<any>; // 观察者容器
    private $mouseenter: any; //Observable<any>; // 观察者容器
    private $mouseout: any; //Observable<any>; // 观察者容器
    private $mousemove: any; //Observable<any>; // 观察者容器
    private $mousewheel: any; //Observable<any>; // 观察者容器
    private $contextmenu: any; //Observable<any>; // 观察者容器
    private $mousedown: any; //Observable<any>; // 观察者容器
    private $mouseup: any; //Observable<any>; // 观察者容器
    private _isMouseUp: boolean = true //
    private subscriptionWrapList$: Subscription[] = [];
    private subscriptionToolbarList$: Subscription[] = [];
    private $ptzSubject = new Subject();
    // private _dataPickerShow = false;
    private _liveFlow = {
        action: 0,
        command: "",
    };
    private _OSDTime = {
        value: 0,
        timestampFlag: 0
    };
    public emitter;
    private zoomScale = 0.25; // 数码放大的容器缩放尺寸
    private _sequence = -1; // 数码放大的容器缩放尺寸
    private _passageRepeat = 0

    private set sequence(params: {
        cmd: string,
        endTime?: string,
        startTime?: string,
        rate?: number,
    }) {
        this._sequence++
        const cmdBody = {
            ...params,
            sequence: this._sequence
        }
        if (this.playerIns?._transmuxer?._controller) {
            this.playerIns._transmuxer._controller._ioctl._loader._ws.send(JSON.stringify(cmdBody))
        }
    }

    public get mediaInfo() {
        return this.playerIns ? this.playerIns.mediaInfo : null;
    }

    public get loading() {
        return this._loading;
    }

    public get playerType() {
        return this.playerIns && this.playerIns._wasmPlayer ? "webgl" : "video";
    }

    public set playerType(type: "webgl" | "video") {
        const wrapper = this.el.querySelector("." + this.prefixName + "-wrapper");
        if (type === "webgl") {
            wrapper.classList.add("webgl");
        } else {
            wrapper.classList.remove("webgl");
        }
    }

    public set loading(v) {
        this._loading = !!v;
        const wrapper = this.el.querySelector("." + this.prefixName + "-wrapper");
        if (wrapper) {
            if (v) {
                wrapper.classList.add("loading");
            } else {
                wrapper.classList.remove("loading");
            }
        }
    }

    public set styleSize(size: StyleSize) {
        const wrapper = this.el.querySelector("." + this.prefixName + "-wrapper");
        if (wrapper) {
            wrapper.classList.remove(
                StyleSize.LG,
                StyleSize.MD,
                StyleSize.SM,
                StyleSize.XS
            );
            wrapper.classList.add(size);
            this._size = size;
        }
    }

    public get size() {
        return this._size;
    }

    public get seeking(): boolean {
        return this._seeking;
    }

    public set seeking(v: boolean) {
        this._seeking = !!v;
        const wrapper = this.el.querySelector("." + this.prefixName + "-wrapper");
        if (wrapper) {
            if (v) {
                wrapper.classList.add("seeking");
            } else {
                wrapper.classList.remove("seeking");
            }
        }
    }

    public get recording() {
        return this._recording;
    }

    public set recording(v) {
        this._recording = !!v;
        const record = this.el.querySelector(".record");
        if (v) {
            record && record.classList.add("recording");
        } else {
            record && record.classList.remove("recording");
        }
    }

    public set messageError(err: any) {
        // 判断err为字符串类型
        if (typeof err === "string") {
            // 创建一个错误提示框，显示4秒删除
            const div = document.createElement('div')
            div.className = this.prefixName + '-message-error'
            div.innerHTML = `<h5><i>×</i><span>${err}</span></h5>`
            this.wrapperel.appendChild(div)
            setTimeout(() => {
                try {
                    const parent = div.parentNode;
                    parent && parent?.removeChild(div)
                } catch (err){
                    console.log(err)
                }
            }, 4000)
        }
    }

    private set loadingTxt(txt: string) {
        const loaderText = this.el.querySelector(".loader-text");
        loaderText && (loaderText.innerHTML = txt);
    }

    public set error(v) {
        const {prefixName} = this;
        const wrapper = this.el.querySelector("." + prefixName + "-wrapper");
        if (wrapper) {
            if (v) {
                this.loading = false;
                wrapper.classList.add("error");
                const explainText = this.el.querySelector(".error-explain-text");
                // 播放错误
                if (v === 'MediaMSEError') {
                    const { passage} = this.streamOpt
                    if(this._passageRepeat===0){
                        this.switchStream(passage === '0' ? 1 : 0)
                        this._passageRepeat = 1
                        return
                    }
                    explainText.innerHTML = '播放错误'
                    const span = document.createElement('span')
                    span.className = 'error-switch-stream'
                    span.innerHTML = passage === '1' ? '切换主码流' : '切换辅码流'
                    explainText.appendChild(span)
                    span.addEventListener('click', () => {
                        this.switchStream(passage === '0' ? 1 : 0)
                        this._passageRepeat = 0
                    }, { once: true })
                    return
                }
                // 错误建议
                const str = '|-|-|'
                const text = v.indexOf(str) > -1 ? v.split(str) : [v]
                explainText && (explainText.innerHTML = text[0] || '');
                const propose = this.el.querySelector(".error-propose") as any;
                if (propose) {
                    propose.style.display = 'none'
                    if (text.length > 1) {
                        propose.style.display = 'block'
                        const proposeText = this.el.querySelector(".error-propose-text");
                        proposeText && (proposeText.innerHTML = text[1] || '无');
                    }
                }
            } else {
                wrapper.classList.remove("error");
            }
        }
    }

    public get isFullScreen() {
        return this._isFullScreen;
    }

    public set isFullScreen(v) {
        if (v) {
            this.wrapperel.classList.add("fullscreen");
        } else {
            this.wrapperel.classList.remove("fullscreen");
        }
        this._isFullScreen = v;
        const {w, h} = this.videoRatio;
        if (!w && !h) {
            this.setFillRatio();
        } else if (w < 18 && h < 18) {
            this.videoWrapperEl.style.width = "100%";
            this.videoWrapperEl.style.height = "100%";
            this.videoWrapperEl.style.transform = `none`;
            this._offsetObj = null;
            this.ratioAdjust(w, h);
        } else {
            this.resetRatio();
        }
        this.checkEleSize();
    }

    public get speed() {
        if (this.playing && this.playerIns) {
            const s =
                (this.playerIns.statisticsInfo &&
                    this.playerIns.statisticsInfo.speed) ||
                0;
            return Math.floor(s) + "kb/s";
        } else {
            return "0kb/s";
        }
    }

    public get durationT() {
        if (this.playerIns && this.streamOpt.streamtype === "vod") {
            return this._endtime - this._begintime;
        } else {
            return 0;
        }
    }

    public set playing(v: boolean) {
        const playBtn = this.el.querySelector(`.${this.prefixName}-play-button`);
        this._playing = v;
        if (v) {
            playBtn && playBtn.classList.add("playing");
            this.sequence = {cmd: 'resume'}
        } else {
            playBtn && playBtn.classList.remove("playing");
            this.sequence = {cmd: 'pause'}
        }

        const wrapper = this.el.querySelector("." + this.prefixName + "-wrapper");

        if (wrapper) {
            v ? wrapper.classList.remove("pause") : wrapper.classList.add("pause")
        }
    }

    public get playing() {
        return this._playing;
    }

    public get ptzing(): boolean {
        return this._ptzing;
    }

    public set ptzing(v: boolean) {
        const wrapper = this.el.querySelector("." + this.prefixName + "-wrapper");
        const ptzBtn = this.el.querySelector(".ptz");
        this._ptzing = v;
        if (v) {
            this.zooming = false;
            ptzBtn && ptzBtn.classList.add("actived");
            wrapper.classList.add("ptzing");
        } else {
            const {wrapperel: el} = this;
            const cls = [
                "top",
                "top-right",
                "right",
                "right-down",
                "down",
                "down-left",
                "left",
                "left-top",
            ];
            el.classList.remove(...cls);
            ptzBtn && ptzBtn.classList.remove("actived");
            wrapper.classList.remove("ptzing");
        }
    }

    private get setting(): boolean {
        return this._setting;
    }

    private set setting(v: boolean) {
        const {prefixName} = this;
        const wrapper = this.el.querySelector("." + prefixName + "-wrapper");
        if (wrapper) {
            if (v) {
                this.showSettingMenu();
            } else {
                this.hideSettingMenu();
            }
        }
        this._setting = Boolean(v);
    }

    public get zooming(): boolean {
        return this._zooming;
    }

    public set zooming(v: boolean) {
        const {prefixName} = this;
        const wrapper = this.el.querySelector("." + prefixName + "-wrapper");
        const zoomBtn = this.el.querySelector(".zoom");
        this._zooming = v;
        if (wrapper) {
            if (v) {
                this.ptzing = false;
                this.toogleCanvasVide(true);
                wrapper.classList.add("zooming");
                zoomBtn && zoomBtn.classList.add("actived");
            } else {
                this.toogleCanvasVide(false);
                wrapper.classList.remove("zooming");
                zoomBtn && zoomBtn.classList.remove("actived");
            }
        }
    }

    // 获取倍数
    public get streamSpeed(): StreamSpeed {
        return this._vodPlayer._streamSpeed
    }

    // 设置倍数
    public set streamSpeed(v: StreamSpeed) {
        this._vodPlayer.setStreamSpeed(v)
    }

    public set offsetTime(v: number) {
        this._offsetTime = v;
        this.updateProcessBarView(v);
    }

    private get isMobile(): boolean {
        return this.playerMode === PlayerMode.MOBILE;
    }

    // 登陆信息、播放器容器
    constructor(
        globalClient: GlobalClient,
        public el: HTMLElement,
        public playerMode?: PlayerMode
    ) {
        this.features = Features.getFeatureList();
        this.playerMode = playerMode;
        this.prefixName = "JPlayer";
        this.globalClient = globalClient;
        this.el.style.fontSize = el.clientWidth >= 800 ? "16px" : "14px";
        // 初始化容器dom
        this.emitter = new EventEmitter();
        this.initDom();
        setTimeout(() => {
            this.emitter.emit(JPEvent.CREATED);
        }, 0);
    }

    private initHammer() {
        if (this._rootHammertime) {
            this._rootHammertime.destroy();
        }
        const wrapperel = this.videoWrapperEl;
        const mc = new Hammer.Manager(wrapperel, {});
        const singleTap = new Hammer.Tap({event: "singletap"});
        const doubleTap = new Hammer.Tap({
            event: "doubletap",
            taps: 2,
            interval: 500,
        });
        doubleTap.recognizeWith(singleTap);
        singleTap.requireFailure(doubleTap);
        mc.add([doubleTap, singleTap]);

        const pinch = new Hammer.Pinch();
        mc.add(pinch);

        this._rootHammertime = mc;
    }

    on(event, listener) {
        this.emitter.addListener(event, listener);
    }

    // EventEmitter 会把 "error" 当成保留事件处理。
    // 外部没有注册 error 监听器时，直接 emit('error') 会同步抛出 Unhandled error，
    // 所以这里先判断是否存在监听器，再决定是否向外派发业务错误。
    private emitPlayerError(payload: any) {
        if (this.emitter && this.emitter.listenerCount(JPEvent.ERROR) > 0) {
            let errorPayload = payload;

            if (typeof payload === "string") {
                errorPayload = { code: -1, msg: payload };
            } else if (payload && typeof payload === "object") {
                errorPayload = {
                    ...payload,
                    code: payload.code !== undefined ? payload.code : -1,
                    msg: payload.msg || payload.message || "播放错误"
                };
            } else {
                errorPayload = { code: -1, msg: "播放错误" };
            }

            this.emitter.emit(JPEvent.ERROR, errorPayload);
        }
    }

    async init(stream?: StreamOpt) {
        this.error = ""
        this.loadingTxt = ""
        this.loading = true
        if (!JPlayer.isSupported()) {
            if (Browser.platform === "iphone") {
                this.error =
                    "抱歉!移动端播放器暂不支持iphone平台!请移步至pc或者使用其它安卓设备观看.";
            } else {
                this.error = "浏览器版本不支持视频播放!请尝试使用系统自带浏览器尝试!";
            }
        }

        const baseStream: StreamOpt = { ...(createDefaultStreamOpt() as StreamOpt), ...stream }
        const isHttps = window.location.protocol === 'https:'
        const protocol = getNormalizedProtocol(baseStream.protocolType, isHttps)

        this.streamOpt = {
            ...baseStream,
            protocol: protocol
        }
        this._maxRetryTime = this.normalizeRetryCount(
            this.streamOpt.maxRetryTime,
            this.MAX_OPEN_STREAM_RETRY_COUNT
        );
        this._enableAutoRecover = this.streamOpt.enableAutoRecover !== false;
        this._maxAutoRecoverCount = this.normalizeRetryCount(
            this.streamOpt.maxAutoRecoverCount,
            this.MAX_AUTO_RECOVER_COUNT
        );
        this.AUTO_RECOVER_RESET_DELAY_MS = this.normalizeStablePlaySeconds(
            this.streamOpt.stablePlaySeconds
        );
        this.initContextMenu(this.streamOpt);

        // 回放
        try {
            if (this.streamOpt.streamtype === "vod") {
                const {url, headers} = this.requestInfo(serverName + "/hik-stream/back-stream");
                this._vodPlayer = new vodPlayer(this.streamOpt, this.size, this.el, {url, headers});
                this.loading = true;
                this.loadingTxt = "正在查询录像";
                const res = await this._vodPlayer.queryRecord({...this.streamOpt}, false)
                this.playUrl = res.url

                // 时间请求重置播放器
                this._vodPlayer.on('vod-refresh', ({dataTime, vod}) => {
                    this.streamOpt.dateTime = dataTime;
                    this.streamOpt.vod = vod
                    this.refresh({...this.streamOpt});
                })

                // 设置跳转与倍数
                this._vodPlayer.on('vod-send', (cmdBody: {
                    cmd: 'seek' | 'speed',
                    endTime?: string,
                    startTime?: string,
                    rate?: number,
                    v: StreamSpeed
                }) => {
                    this.seeking = true
                    const {cmd, rate} = cmdBody
                    // 发送数据
                    const _cmdBody = cmd === 'speed' ? {cmd, rate} : {...cmdBody}
                    this.sequence = _cmdBody
                    // 暂停播放
	                    if (cmd === 'seek' && this.playerIns) {
                        this.clearOsdTimeId()
                        this.playerIns.pause()
                        this._currentTime = this.playerIns.currentTime
                        this.emitter.emit(JPEvent.SEEKED)

	                        if (this.playerIns.isNetworkDead === true) {
	                            console.warn("WebSocket 已断开，正在执行断线 Seek 重连！");

	                            this.tryAutoRecover(() => {
                                // 将本次拖拽的目标时间，作为重新拉流的起始时间
                                this.streamOpt.beginTime = cmdBody.endTime
                            });

                            return;
                        }
                    }
                    if (cmd === 'speed') {
                        setTimeout(() => {
                            this.seeking = false
                            const video = this.playerIns?._wasmPlayer ? this.playerIns._wasmPlayer : this.vel
                            video.playbackRate = rate === -2 ? 0.5 : rate === -4 ? 0.25 : rate === -8 ? 0.125 : rate
                            this._vodPlayer.setSpeed(cmdBody)
                        }, 1000)

                    }
                })
            }

        } catch (error) {
            this.error = "未查询到分段录像";
            console.warn("未查询到分段录像");
            return;
        }

        try {
            // 预览
            if (this.streamOpt.streamtype === "live") {
                this.loadingTxt = "视频缓冲中...";
                this.playUrl = await this.openStream(this.streamOpt);
                // this.playUrl = stream.url
            }
        } catch (error) {
            if (error.code !== 0) {
                this.error = error.msg;
            } else {
                const _err =
                  `${error.data.code} :${ERRORMSG[error.data.code]}` ||
                  (error.msg && error.msg.errorMsg);
                this.error = error.data && error.data.code ? _err : error;
            }
            return;
        }

        try {
            // 实例化播放器
            const code = await this.creatPlayer();
            if (code !== 0) return
            // 初始工具栏
            this.initToolBar();
            this.setFillRatio();
            this.emitter.emit(JPEvent.INITED, this.vid, this);
            this.loading = false;
            this.play(false);
        } catch (error) {
            console.warn(error);
            this.emitPlayerError({ msg: '播放器初始化失败' });
        }

        // TODO:接口不满足长时间查询 后续迭代
        // const end = this._endtime
        // const beg = this._endtime - 3 * 3600 * 24 * 1000
    }

    toogleCanvasVide(toogle) {
        if (toogle) {
            this.updateCanvas();
        }
    }

    updateCanvas() {
        const {
            canvas,
            _zoomStartPonit: {
                clientX: originClientX,
                clientY: originClientY,
                offsetX: originOffsetX,
                offsetY: originOffsetY,
            },
            _zoomEndPonit: {clientX, clientY, offsetX, offsetY},
        } = this;
        const {width, height} = this.mediaInfo;
        const isWasmPlayer = !!this.playerIns?._wasmPlayer;
        const {clientWidth: videoClientWidth, clientHeight: videoClientHeight} =
            isWasmPlayer ? this.cel : this.vel;
        if (!this.zooming) {
            return;
        }
        const _offsetX = offsetX - originOffsetX;
        const _offsetY = offsetY - originOffsetY;
        const dir = getDirection(_offsetX, _offsetY);
        const context = canvas.getContext("2d");
        context.clearRect(0, 0, width, height);
        if (
            dir === Direction.NAN ||
            dir === Direction.TL ||
            dir === Direction.DL ||
            dir === Direction.TR
        ) {
            context.drawImage(
                isWasmPlayer ? this.cel : this.vel,
                0,
                0,
                width,
                height
            );
        } else {
            const sx = (width * originOffsetX) / videoClientWidth;
            const sy = (height * originOffsetY) / videoClientHeight;
            const sw = (Math.abs(_offsetX) * width) / videoClientWidth;
            const sh = (Math.abs(_offsetY) * height) / videoClientHeight;
            const _sw = sx + sw >= width ? width - sx : sw;
            const _sh = sy + sh >= height ? height - sy : sh;
            context.drawImage(
                isWasmPlayer ? this.cel : this.vel,
                sx,
                sy,
                _sw,
                _sh,
                0,
                0,
                width,
                height
            );
        }
        if (this.zooming) {
            requestAnimationFrame(this.updateCanvas.bind(this));
        }
    }

    getFeatureList() {
        return Features.getFeatureList();
    }

    initObservables() {
        const {isMobile} = this;
        const resize$ = this.$resize
            .pipe(
                throttleTime(52),
                map((e) => this.checkEleSize()),
                distinct()
            )
            .subscribe((x) => {
                let showFullLabel = false;
                switch (x) {
                    case StyleSize.XS:
                    case StyleSize.SM:
                        showFullLabel = false;
                        break;
                    case StyleSize.MD:
                    case StyleSize.LG:
                        showFullLabel = true;
                        break;
                }
            });

        const clicks$ = this.$click.subscribe((e) => {
            // const {isMobile} = this;
            // console.log('isMobile->')
            // if (!isMobile) {
            //     this.hideContextMenu()
            // }
            // const { streamtype } = this.streamOpt;
            // const airDatepicker = this.el.querySelector(".air-datepicker");
            // // 是否在vod模式下且时间组件已显示
            // if (airDatepicker && streamtype === "vod") {
            //   return;
            // }
            const videoBox = this.el.querySelector(
                "." + this.prefixName + "-video-box"
            );
            const isShow = videoBox.classList.contains("show-tools");
            isShow ? this.hideToolbars() : this.showToolbars();
        });

        // 双击全屏
        const dblclick$ = this.$dblclick
            .pipe(
                filter(
                    (e: any) =>
                        !this.ptzing &&
                        this.playerOnCanPlay &&
                        e.target &&
                        e.target.nodeName === "VIDEO"
                )
            )
            .subscribe((e) => {
                const {playerMode, playing} = this;
                // const isMobile = playerMode === PlayerMode.MOBILE
                playing ? this.pause() : this.play();
                // if (isMobile) {
                //     playing ? this.pause() : this.play();
                // } else {
                //     this.setFullScreen(!this._isFullScreen)
                // }
            });
        // const mouseenter$ = isMobile ? null : this.$mouseenter.pipe().subscribe(this.showToolbars);
        const mouseout$ = isMobile
            ? null
            : this.$mouseout.pipe(filter((e) => !this.isMobile)).subscribe(() => {
                // this.hideToolbars();
                // this.setting = false;
                // this.hideContextMenu();
                // this.hidePicker();
            });
        const contextmenu$ = isMobile
            ? null
            : this.$contextmenu
                .pipe(
                    filter((e) => this.playerOnCanPlay),
                    filter(
                        (e) =>
                            !(this.ptzing || this.error || this.loading || this.zooming)
                    ),
                    debounceTime(50)
                )
                .subscribe(({x, y}) => {
                    this.contextMenu.hide();
                    this.contextMenu.show(x, y);
                });

        // 云台鼠标滑动模拟curson
        const mousemove$ = isMobile
            ? null
            : this.$mousemove
                .pipe(
                    filter((e) => !this.isMobile),
                    filter((e) => this.playerOnCanPlay),
                    filter((e) => this.ptzing),
                    map(this.appendMockCursor),
                    filter((e: any) => {
                        if (e.target && (e.target.nodeName === "VIDEO" || e.target.nodeName === "CANVAS")) {
                            return true;
                        } else {
                            this.clearPtzClass();
                            return false;
                        }
                    }),
                    map(this.setMCPosition),
                    map(this.getPtzPosition),
                    map(this.getPtzPositionFlag)
                )
                .subscribe(this.addPtzClass);

        // 云台控制
        const cmdPtz$ = this.$ptzSubject.asObservable().subscribe(
            ([cmd, value]) => {
                this.sendCmdPtz(cmd, value);
            },
            () => {
            },
            () => {
            }
        );

        // 云台方向按压滑动
        const ptzDownMoveUp$ = isMobile
            ? null
            : this.$mousedown
                .pipe(
                    filter((e) => this.ptzing),
                    filter((e: any) => {
                        return (e.button === 0 && e.target && (e.target.nodeName === "VIDEO" || e.target.nodeName === "CANVAS"));
                    }),
                    filter((e: any) => {
                        const [x, y] = this.getPtzPosition(e);
                        const f = this.getPtzPositionFlag([x, y]);
                        return f !== -99;
                    }),
                    // 按下操作
                    map((e: any) => {
                        const [x, y] = this.getPtzPosition(e)
                        const f = this.getPtzPositionFlag([x, y])
                        this._liveFlow.action = 0
                        this.$ptzSubject.next([f, this._ptzSpeed])
                        return e
                    }),
                    map((e) =>
                        this.$mousemove.pipe(
                            takeUntil(// 鼠标抬起
                                this.$mouseup.pipe(
                                    map((e: any) => {
                                        const [x, y] = this.getPtzPosition(e);
                                        const f = this.getPtzPositionFlag([x, y]);
                                        this._liveFlow.action = 1
                                        setTimeout(() => {
                                            this.$ptzSubject.next([f, 0]);
                                        }, 300)
                                        return e;
                                    })
                                )
                            )
                        )
                    ),
                    concatAll(),
                    map(this.getPtzPosition),
                    map(this.getPtzPositionFlag),
                    distinctUntilChanged()
                )
                .subscribe((f) => {
                    this.$ptzSubject.next([f, this._ptzSpeed]);
                });

        // 云台缩放
        const mousewheel$ = isMobile
            ? this.$mousewheel
                .pipe(
                    filter((e) => this.playerOnCanPlay),
                    throttleTime(100),
                    map((e: any) => {
                        const cmd = 11;
                        const value: any =
                            e.type === "pinchout" ? this._ptzSpeed : this._ptzSpeed * -1;
                        this.sendCmdPtz(cmd, value);
                        return e;
                    }),
                    debounceTime(300)
                )
                .subscribe((e) => {
                    const cmd = 11;
                    this.sendCmdPtz(cmd);
                })
            : this.$mousewheel
                .pipe(
                    filter((e) => this.playerOnCanPlay),
                    filter((e) => this.ptzing),
                    filter((e: WheelEvent) => !!e.deltaY),
                    map((e: WheelEvent) => {
                        this._liveFlow.action = 0
                        const cmd = e.deltaY > 0 ? 9 : 8
                        this.sendCmdPtz(cmd);
                        return e.deltaY;
                    }),
                    debounceTime(300)
                )
                .subscribe((deltaY) => {
                    this._liveFlow.action = 1
                    const cmd = deltaY > 0 ? 9 : 8
                    setTimeout(() => {
                        this.sendCmdPtz(cmd);
                    }, 200)
                });
        // 数码放大
        const $windoMousemove = isMobile
            ? null
            : fromEvent(window, "mousemove").pipe(
                throttleTime(26),
                map((e) => {
                    return e;
                })
            );

        const $windowMouseup = isMobile
            ? null
            : fromEvent(window, "mouseup").pipe(
                filter((e) => this.zooming),
                map((e) => {
                    this.cleanZoomStartPonit();
                    return e;
                }),
                map((e: MouseEvent) => {
                    const {clientHeight, clientWidth} = this.vel;
                    const {
                        _zoomStartPonit: {
                            offsetX: originOffsetX,
                            offsetY: originOffsetY,
                            clientX: originClientX,
                            clientY: originClientY,
                        },
                    } = this;
                    const {clientX, clientY} = e;
                    if (e.target && (e.target as any).nodeName === "VIDEO") {
                        this._zoomEndPonit = {
                            offsetX: e.offsetX * 1,
                            offsetY: e.offsetY * 1,
                            clientX: e.clientX * 1,
                            clientY: e.clientY * 1,
                        };
                    } else {
                        const _offsetX = clientX - originClientX;
                        const _offsetY = clientY - originClientY;
                        this._zoomEndPonit = {
                            offsetX:
                                _offsetX + originOffsetX >= clientWidth
                                    ? clientWidth
                                    : originOffsetX + Math.abs(_offsetX),
                            offsetY:
                                _offsetY + originOffsetY >= clientHeight
                                    ? clientHeight
                                    : originOffsetY + Math.abs(_offsetY),
                            clientX: e.clientX * 1,
                            clientY: e.clientY * 1,
                        };
                    }
                    return e;
                })
            );

        const $videoMouseDown = isMobile
            ? null
            : fromEvent(this.vel, "mousedown").pipe(
                filter((e) => this.zooming),
                map((e: MouseEvent) => {
                    this._zoomStartPonit = {
                        offsetX: e.offsetX * 1,
                        offsetY: e.offsetY * 1,
                        clientX: e.clientX * 1,
                        clientY: e.clientY * 1,
                    };
                    this._zoomEndPonit = {
                        offsetX: e.offsetX * 1,
                        offsetY: e.offsetY * 1,
                        clientX: e.clientX * 1,
                        clientY: e.clientY * 1,
                    };
                    this.creatZoomStartPonit(e.offsetX, e.offsetY);
                    return e;
                })
            );

        const zoomMoveUp$ = isMobile
            ? null
            : $videoMouseDown
                .pipe(
                    filter((e) => this.zooming),
                    map((event) => $windoMousemove.pipe(takeUntil($windowMouseup))),
                    concatAll(),
                    map((e) => e)
                )
                .subscribe((e: MouseEvent) => {
                    const {
                        _zoomStartPonit: {
                            offsetX: originOffsetX,
                            offsetY: originOffsetY,
                            clientX: originClientX,
                            clientY: originClientY,
                        },
                    } = this;
                    const {clientHeight, clientWidth} = this.vel;
                    const {clientX, clientY} = e;
                    const _offsetX = clientX - originClientX;
                    const _offsetY = clientY - originClientY;
                    const fix_offsetX =
                        _offsetX + originOffsetX >= clientWidth
                            ? clientWidth - originOffsetX
                            : _offsetX;
                    const fix_offsetY =
                        _offsetY + originOffsetY >= clientHeight
                            ? clientHeight - originOffsetY
                            : _offsetY;
                    const dir = getDirection(fix_offsetX, fix_offsetY);
                    this.setZoom(Math.abs(fix_offsetX), Math.abs(fix_offsetY), dir);
                });

        this.unSubscribeObservables();
        if (isMobile) {
            this.subscriptionWrapList$.push(
                clicks$,
                resize$,
                (cmdPtz$ as any),
                dblclick$,
                mousewheel$
            );
        } else {
            // mouseenter$
            this.subscriptionWrapList$.push(
                clicks$,
                resize$,
                mouseout$,
                contextmenu$,
                (cmdPtz$ as any),
                ptzDownMoveUp$,
                mousewheel$,
                dblclick$,
                mousemove$
            );
        }
    }

    unSubscribeObservables() {
        this.subscriptionWrapList$.forEach((s) => s.unsubscribe());
        this.subscriptionWrapList$ = [];
    }

    unSubscribeToolbarObservables() {
        this.subscriptionToolbarList$.forEach((s) => s.unsubscribe());
        this.subscriptionToolbarList$ = [];
    }

    initDom() {
        // 清除容器内的元素
        const {prefixName, playerMode} = this;
        const isMobile = playerMode === PlayerMode.MOBILE;
        const childs = this.el.childNodes || [];
        childs.forEach((c) => {
            c.parentNode.removeChild(c);
        });
        const videoDom = ((prefixName: string): HTMLElement => {
            const videoContent = document.createElement("div");
            videoContent.className = prefixName + "-video-box";
            return videoContent;
        })(prefixName);

        const videoWrapper = document.createElement("div");
        videoWrapper.className = isMobile
            ? "video-wrapper mobile"
            : "video-wrapper";

        const video = document.createElement("video");
        this.vel = video;
        // webgl player
        const cvs = document.createElement("canvas");
        this.cel = cvs;
        videoWrapper.appendChild(video);
        videoWrapper.appendChild(cvs);
        videoDom.appendChild(videoWrapper);
        this.videoWrapperEl = videoWrapper;
        const wrapper = document.createElement("div");
        wrapper.classList.add(prefixName + "-wrapper", "loading");
        wrapper.appendChild(videoDom);
        wrapper.appendChild(createSeekDom(prefixName));

        const loader2 = createLoaderDom2(
            prefixName,
            this.globalClient.config.logoPath
        );
        wrapper.appendChild(loader2);

        if (isMobile) {
            // 移动端模式下 加入全局遮罩层 承载播放暂停按钮
            wrapper.appendChild(createMobileWindowDom(prefixName, this));
        }
        wrapper.appendChild(createErrorDom(prefixName, this));

        // 放大
        const [zoom, canvas] = createZoomDom(prefixName, this);
        wrapper.appendChild(zoom);
        this.canvas = canvas;
        this.wrapperel = wrapper;
        this.el.appendChild(wrapper);

        // 右键容器
        const rightMenu = document.createElement("div");
        rightMenu.className = prefixName + "-rightMenu-wrapper";
        wrapper.appendChild(rightMenu);

        // 录像时候的显示文字
        const recordText = document.createElement("div");
        recordText.className = prefixName + "-record-text";
        wrapper.appendChild(recordText);

        // 初始化的时候计算容器大小
        this.checkEleSize();
        this.unBindEvents();
        this.bindEvents(playerMode);
    }

    initToolBar() {
        const {playerMode, streamOpt, prefixName} = this;
        const {isDraw, shape, isTalk, autoAudio, streamtype} = streamOpt;
        const isMobile = playerMode === PlayerMode.MOBILE;
        const videoBox = this.el.querySelector("." + prefixName + "-video-box");
        this.emptyVideoTool();
        // 绘制功能
        if (isDraw) {
            // 创建绘制实例
            this.rectBlock = new rectDraw(
                videoBox,
                prefixName + "-rectangle-canvas",
                createRectangleDom(prefixName, this)
            );
            // 绘制tip和btn
            videoBox.appendChild(createDrawTipBtnDom(prefixName, this));
            this.rectBlock.on("complete", () => {
                this.switchDraw(0);
            });
            setTimeout(() => {
                this.setRectRatio(shape);
            }, 100);
        }
        // 对讲功能
        if (isTalk && streamtype === 'live') {
            this._talkCtrl = new TalkCtrl(this.streamOpt, {
                ...this.requestInfo("/video-platform-basedata/hik-stream/talk"),
                protocol: this.streamOpt.protocol,
            }, prefixName, videoBox);
            this._talkCtrl.on('error', (msg) => {
            })
        }
        // 声音控制
        this._audioCtrl = new audioCtrl(videoBox, autoAudio)
        if (!streamOpt.hideHeaderToolBar) {
            const header = creatHeaderToolBar(
                this,
                streamOpt,
                prefixName,
                this.isMobile
            );
            videoBox.appendChild(header);
        }
        if (!streamOpt.hideFooterToolBar) {
            if (isMobile) {
                // 移动模式 footer 工具栏
                const footer = creatMobileFooterToolBar(this, streamOpt, prefixName);
                // const slider = creatMobileSilderToolBar(this, streamOpt, prefixName); // 隐藏移动端全屏左侧工具（截图 + 录像）
                videoBox.appendChild(footer);
                // videoBox.appendChild(slider);
            } else {
                // 桌面模式 footer 工具栏
                const footer = creatFooterToolBar(this, streamOpt, prefixName);
                videoBox.appendChild(footer);
                footer.addEventListener('click', (e) => {
                    e.stopPropagation()
                })
                if (streamtype === 'vod') {
                    this._vodPlayer.init()
                }
                videoBox.appendChild(createSettingMenuDom(this, streamOpt, prefixName));
            }
        }
        // 移动端 创建手柄
        if (streamOpt.streamtype === "live" && isMobile && streamOpt.isptz) {
            const joystickDOM = creatJoystickToolBar(
                this,
                streamOpt,
                prefixName,
                this.isMobile
            );
            videoBox.appendChild(joystickDOM);
            this.joystickele = joystickDOM;
        }
        // 有 speed 展示
        const speed: HTMLElement = videoBox.querySelector(
            `.${this.prefixName}-speed`
        );
        if (speed) {
            const numbers$: any = this._timer
                .pipe(
                    startWith(0),
                    filter((v) => !this.loading && this.playerIns)
                )
                .subscribe((i) => {
                    speed.innerHTML = this.speed;
                });
            this.subscriptionToolbarList$.push(numbers$);
        }
    }

    creatPlayer() {
        const { features: { msePlayback } } = this;
        this.playing && (this.playing = false)

        return new Promise<any>((resolve, reject) => {
            const {protocolType} = this.streamOpt;
            const workerPath = this.streamOpt?.workerPath || this.globalClient.config.workerPath;
            // 确保清除上一次的实例
            this.teardownPlaybackSession();
            this.playerOnCanPlay = false;
            const videoBox = this.el.querySelector(
                "." + this.prefixName + "-video-box"
            );
            const video = videoBox.querySelector("video");
            if (video) {
                video.controls = false;
                video.autoplay = true;
                video.muted = true;
            }
            this.vid = guid();
            this.vel = video;
            this.vel.id = this.vid;
            this.cel.id = "cvs" + this.vid;

            const ProtocolToTypeMap = {
                'httpflv': 'flv',
                'websocketflv': 'flv',
                'hls': 'hls',
                'webrtc': 'webrtc'
            };

            let finalType = ProtocolToTypeMap[protocolType] || 'native';

            if (!msePlayback && finalType === 'flv') {
                finalType = 'hls';
            }

            const mediaDataSource: any = {
                type: finalType,
                isLive: true,
                url: this.playUrl,
            };

            const optionalConfig = {
                enableWorker: this.streamOpt.enableWorker,
                seekType: 'range',
                useOuterLoader: true,
                statisticsInfoReportInterval: 500,
                workerPath,
            };

            // 创建播放器
            // if (config.hasOwnProperty('enableStashBuffer')) cfg.enableStashBuffer = config.enableStashBuffer // true-流畅性优先（抗网络抖动），false-实时性优先
            // if (config.hasOwnProperty('enableWorker')) cfg.enableWorker = config.enableWorker
            this.playerIns = mpegts.createPlayer(mediaDataSource, optionalConfig, this.streamOpt?.streamtype);
            this.playerIns.attachMediaElement(this.vel, this.cel);
            this.playerIns.load();
            this.playerOnCanPlay = true;
            // this.playerIns?._wasmPlayer && (this.playerType = "webgl");

            const closeLoad = () => {
                this.playing = true;
                this._isAutoRecovering = false;
                this.startAutoRecoverResetTimer();

                this.playerIns?._wasmPlayer && (this.playerType = "webgl")

                if (this.streamOpt.streamtype === "vod") {
                    this.updateOsdTimeThrottle()
                }

                // TODO 开流黑屏时间太长建议处理方式
                this._canPlayTimer = setTimeout(() => {
                    this._canPlayTimer = null;
                    this.emitter && this.emitter.emit(JPEvent.CANPLAY);
                    this.playerOnCanPlay = true;
                    resolve(0);
                }, 600);
            }


            let isResolved = false;

            const handleSuccess = (source) => {
                if (isResolved) return;
                isResolved = true;

                // 成功后及时移除原生事件监听，防止内存泄漏或幽灵触发
                this.vel.removeEventListener('canplay', nativeHandler);
                this.vel.removeEventListener('playing', nativeHandler);

                closeLoad();
            };

            const nativeHandler = () => handleSuccess('原生 Video 事件');
            this.vel.addEventListener('canplay', nativeHandler, { once: true });
            this.vel.addEventListener('playing', nativeHandler, { once: true });

            if (this.playerIns && typeof this.playerIns.on === 'function') {
                this.playerIns.on("onVCanPlay", () => handleSuccess('自定义 onVCanPlay'));
            }

            if (this.vel.readyState >= 3) {
                handleSuccess('readyState 状态检查');
            }

            this.playerIns.on('media_info', (info) => {
                // console.log('MEDIA_INFO', info)
            })

            // 自定义接口请求返回数据
            this.playerIns.on("json", (type, json) => {
                if (json.cmd) {
                    this.seeking = false
                    this.playerIns.play();
                    this.streamOpt.streamtype === "vod" && this.updateOsdTimeThrottle()
                }
            });

            this.playerIns.on(PlayerEvents.ERROR, async (type, detail, info) => {
                let isError = true

                if (typeof info === "string" && info.indexOf('0x0') > -1) {
                    // 特定的错误码处理，比如萤石、海康设备返回的错误码
                    const errCode = info
                    const {url, aisleId, headers} = this.requestInfo(
                        "/video-platform-system/hik-error-code"
                    );
                    const res = await httpClient.get(url, { current: 1, size: 3, errorCode: errCode }, headers)
                    const records = res?.data?.records
                    const data = Array.isArray(records) && records.length > 0 ? records[0] : null
                    const str = '|-|-|'
                    this.error = data ? '(' + errCode + ')' + data.errorDescribe + str + data.errorSuggestion : '播放错误'
                    reject(info)
                    return
                }

                // 浏览器播放已结束
                if (info && info === 'mseSourceEnded') {
                    this.messageError = '播放器已停止'
                } else if (info === 'mseSourceClose') {
                    this.messageError = '播放器已关闭'
                } else if (type === 'NetworkError') {
                    const msg = info && typeof info === 'object'
                        ? (info.msg || info.message)
                        : info;
                    this.loadingTxt = msg || "播放错误";
                    this.error = msg || "播放错误";
                } else if (detail === 'MediaMSEError') {
                    if (info && typeof info === 'object' && typeof info.msg === 'string' && info.msg.includes("Failed to execute 'addSourceBuffer' on 'MediaSource'")) {
                        isError = false
                        closeLoad()
                    } else {
                        this.error = detail
                    }
                } else {
                    this.loadingTxt = "播放错误";
                    this.error = "播放错误";
                }

                if (isError) {
                    if (this.shouldAutoRecover(type)) {
                        this.error = '';
                        this.loadingTxt = '正在重连...';
                        this.tryAutoRecover(() => {
                            this.prepareAutoRecoverStreamState();
                        });
                        return;
                    }
                    this.emitPlayerError(info);
                    reject(type)
                }
            });

            this.playerIns.on("statistics_info", (res: any) => {
                if (this.lastDecodedFrame == 0) {
                    this.lastDecodedFrame = res.decodedFrames;
                    return;
                }
                if (this.lastDecodedFrame != res.decodedFrames) {
                    this.lastDecodedFrame = res.decodedFrames;
                } else {
                    this.lastDecodedFrame = 0;
                }
            });

            this.playerIns.on("onClientSeeked", (seconds) => {
                console.log("onClientSeeked")
            });

            this.playerIns.on('buffer_full',()=>{
                console.log('buffer_full:')
            })


        });
    }

    updateOsdTimeThrottle() {
        if (!this.playerIns) {
            return
        }
        // TODO:后续优化定时器
        this.osdTimeId = setTimeout(() => {
            const currentTime = this.playerIns?._wasmPlayer ? this.playerIns._wasmPlayer._currentTime : this.playerIns?.currentTime || 0
            let timestamp = (currentTime - this._currentTime) * 1000,
                timestampLast = this._OSDTime.timestampFlag
            this._OSDTime.value = this._OSDTime.value + (timestamp - timestampLast)
            this._OSDTime.timestampFlag = timestamp
            if (this._vodPlayer) {
                this._vodPlayer.timeBarAnimation(this._OSDTime.value)
                this.updateOsdTimeThrottle()
            }
        }, 500)
    }

    public switchStream(streamNO: number) {
        const {streamOpt} = this;
        // streamOpt.aisleId = replaceStreamNO(aisleId, streamNO);
        streamOpt.passage = streamNO + ''
        this.refresh(streamOpt);
    }

    // 公共请求参数
    requestInfo(url: string) {
        return {
            url: this.globalClient.config.endPoint + url,
            aisleId: filterAisleIdChart(this.streamOpt.aisleId),
            headers: {
                "Content-Type": "application/json",
                Authorization: "Bearer " + this.globalClient.accessToken,
            },
        };
    }

    /**
     * 在稳定播放后的断流场景下，触发一次完整的重开流流程。
     * 如果当前还未进入稳定播放态、已经处于恢复中，或已达到最大恢复次数，则返回 false。
     */
    private tryAutoRecover(onBeforeRefresh?: () => void) {
        // 仅对已稳定播放过的实例做自动恢复，避免刚开流失败时反复重建。
        if (!this._enableAutoRecover || !this._isStablePlaying || this._isAutoRecovering) {
            return false;
        }
        const maxAutoRecoverCount = Math.min(this._maxAutoRecoverCount, this.MAX_AUTO_RECOVER_COUNT);
        if (this._autoRecoverCount >= maxAutoRecoverCount) {
            this.error = "连接恢复失败";
            this.loading = false;
            this.seeking = false;
            this.emitPlayerError({ msg: '连接恢复失败' });
            return false;
        }

        this._isAutoRecovering = true;
        this._autoRecoverCount += 1;
        console.warn(`[AutoRecover] 第 ${this._autoRecoverCount}/${maxAutoRecoverCount} 次重连 ${new Date().toLocaleTimeString()}`);
        onBeforeRefresh && onBeforeRefresh();
        this.refresh({ ...this.streamOpt });
        return true;
    }

    /**
     * 归一化重试次数配置。
     * 允许下游通过 init 传入，但最终不会超过播放器内部允许的最大值。
     */
    private normalizeRetryCount(value: number | undefined, max: number) {
        if (!Number.isFinite(value)) {
            return max;
        }
        return Math.max(0, Math.min(Number(value), max));
    }

    /**
     * 归一化稳定播放时长配置，单位秒。
     * 允许下游通过 init 传入，但最终会被裁剪到播放器允许的范围内。
     */
    private normalizeStablePlaySeconds(value: number | undefined) {
        if (!Number.isFinite(value)) {
            return this.DEFAULT_STABLE_PLAY_SECONDS;
        }
        return Math.max(1, Math.min(Number(value), this.MAX_STABLE_PLAY_SECONDS));
    }

    /**
     * 启动稳定播放计时器。
     * 只有在持续稳定播放达到配置时长后，才会重置自动恢复次数。
     */
    private startAutoRecoverResetTimer() {
        this.clearAutoRecoverResetTimer();
        // 流开始播放立即标记为可恢复，计时器只负责10分钟后重置次数。
        if (!this._isAutoRecovering) {
            this._isStablePlaying = true;
        }
        this._autoRecoverResetTimer = setTimeout(() => {
            this._autoRecoverCount = 0;
            this._autoRecoverResetTimer = null;
        }, this.AUTO_RECOVER_RESET_DELAY_MS * 1000);
    }

    /**
     * 清理稳定播放计时器，并将当前播放会话标记为未进入稳定播放态。
     * 在销毁播放器或重新开流前调用，避免误重置恢复次数。
     */
    private clearAutoRecoverResetTimer() {
        if (this._autoRecoverResetTimer) {
            clearTimeout(this._autoRecoverResetTimer);
            this._autoRecoverResetTimer = null;
        }
        // 恢复中不重置稳定播放态，保留剩余重试机会。
        if (!this._isAutoRecovering) {
            this._isStablePlaying = false;
        }
    }

    /**
     * 判断当前错误是否应该触发自动重开流。
     * 只有已经稳定播放过的实例，且错误属于网络中断类问题时，才允许自动恢复。
     */
    private shouldAutoRecover(errorType?: string) {
        if (!this._enableAutoRecover || !this._isStablePlaying || !this.playerIns) {
            return false;
        }
        // 只对网络中断类问题做整链路重开流，普通播放错误仍走原有报错逻辑。
        return this.playerIns.isNetworkDead === true || errorType === 'NetworkError';
    }

    /**
     * 在自动恢复前修正流参数。
     * 直播直接回到最新流位置，回放则从当前已播时间继续拉流。
     */
    private prepareAutoRecoverStreamState() {
        if (this.streamOpt.streamtype === 'live') {
            // 直播重连时直接回到最新流位置。
            this.streamOpt.beginTime = undefined;
            this.streamOpt.dateTime = undefined;
            return;
        }
        // 回放重连时从当前已播位置继续拉流。
        const currentSeconds = this.playerIns?.currentTime || 0;
        this.streamOpt.beginTime = this.calculateNewBeginTime(this.streamOpt.beginTime, currentSeconds);
    }

    // 内部控制云台入口
    public async sendCmdPtz(
        cmd: number,
        operate?: { index: number; name?: string; state?: number }
    ) {
        if (cmd < 10) {
            this.sendCmdDirection(cmd);
        } else {
            this.sendCmdPreset(operate);
        }
    }

    // 设置方向
    public async sendCmdDirection(direction: number) {
        // 控制命令(不区分大小写) : ZOOM_IN 焦距变大 ZOOM_OUT 焦距变小 FOCUS_NEAR 焦点前移 FOCUS_FAR焦点后移
        // IRIS_ENLARGE 光圈扩大 IRIS_REDUCE 光圈缩小 以下命令presetIndex不可为空： GOTO_PRESET到预置点
        const {url, aisleId, headers} = this.requestInfo(
            "/video-platform-basedata/hik-stream/controlling"
        );
        let command = "";
        switch (direction) {
            case 3:
                command = "LEFT_UP";
                break;
            case 2:
                command = "UP";
                break;
            case 1:
                command = "RIGHT_UP";
                break;
            case 4:
                command = "LEFT";
                break;
            case 0:
                command = "RIGHT";
                break;
            case 5:
                command = "LEFT_DOWN";
                break;
            case 6:
                command = "DOWN";
                break;
            case 7:
                command = "RIGHT_DOWN";
                break;
            case 8:
                command = "ZOOM_IN";
                break;
            case 9:
                command = "ZOOM_OUT";
                break;
        }
        const action = this._liveFlow.action
        const res = await httpClient.post(
            url,
            {
                action,
                aisleId,
                command,
                speed: 40,
            },
            headers
        ).finally(() => {
            // this._ptzLoad = false
        });
        if (this._isMouseUp) {
            this.emitter.emit('ptzSubject')
        }
        if (res.code !== 200) {
            //TODO: 错误提示
        }
        return res;
    }

    // 设置摄像头预置位 0添加 1删除
    public async sendCmdPreset({index, name, state}: {
        index: number;
        name?: string;
        state?: number;
    }) {
        if (typeof index !== "number") {
            return Promise.reject("预置位编号必须为数字");
        }
        const _str = state
            ? "/video-platform-basedata/hik-stream/deletion"
            : "/video-platform-basedata/hik-stream/addition"
        const {url, aisleId, headers} = this.requestInfo(_str);
        const res = await httpClient.post(
            url,
            {
                aisleId,
                presetIndex: index,
                presetName: name,
            },
            headers
        );
        if (res.code !== 200) {
            return Promise.reject("设置预置位失败");
        }
        return res;
    }

    async openStream(stream: StreamOpt) {
        this.streamOpt = stream;
        let _maxRetryTime = Math.min(this._maxRetryTime || this.MAX_OPEN_STREAM_RETRY_COUNT, this.MAX_OPEN_STREAM_RETRY_COUNT);
        const newStartRealStreamUrl = "/video-platform-basedata/hik-stream/live-stream";
        const newStartVodStreamUrl = "/video-platform-basedata/hik-stream/back-stream";
        const {url, headers} = this.requestInfo(
            stream.streamtype === "live" ? newStartRealStreamUrl : newStartVodStreamUrl
        );
        const {passage, aisleId, protocolType, protocol} = stream;

        this.loading = true;
        this.loadingTxt = "正在开流...";
        let res;

        const params = {
            aisleId: aisleId,
            protocol,
            streamType: passage
        };

        for (; _maxRetryTime > 0;) {
            if (_maxRetryTime < this.MAX_OPEN_STREAM_RETRY_COUNT) {
                this.loadingTxt = `正在第${this.MAX_OPEN_STREAM_RETRY_COUNT - _maxRetryTime + 1}次重试开流!`;
            }
            res = stream.streamtype === "live"
              ? await httpClient.get(url, params, headers).catch((err: any) => err)
              : await httpClient.post(url, params, headers).catch((err: any) => err);

            if (!res || res instanceof Error) {
              this.loadingTxt = res?.msg || res?.message || '网络异常或接口报错';
              // this.emitter.emit(JPEvent.ERROR, res);
              _maxRetryTime--;
              continue; // 进入下一次重试循环
            }

            if (res.code === 1 || res.code === 99) {
                // token 失效
                if (res?.msg === '无效token') {
                  this.loadingTxt = `无效token`;
                  _maxRetryTime = 0;
                  break;
                }

                this.loadingTxt = `正在自动重连...`;
                _maxRetryTime--;
                continue;
            } else if (res.code === 0 || res.data.code === 200) {
                this._maxRetryTime = this.MAX_OPEN_STREAM_RETRY_COUNT;
                return res.data;
            } else {
                _maxRetryTime--;
            }
        }

        if (_maxRetryTime <= 0) {
          this._maxRetryTime = this.MAX_OPEN_STREAM_RETRY_COUNT;
          this.emitPlayerError(res)
          return Promise.reject(res);
        }
    }

    private initContextMenu(stream: StreamOpt) {
        const ele = this.el;
        ele.oncontextmenu = (e) => {
            e.preventDefault();
            e.stopPropagation();
        };
        // 初始化右键菜单实例
        const container = this.wrapperel.querySelector(
            "." + this.prefixName + "-rightMenu-wrapper"
        );
        this.contextMenu = new ContextMenu(
            container,
            this.getRightMenu(stream.streamtype)
        );
    }

    private getRightMenu(val: string) {
        const version = __VERSION__;
        const drawConfig = [{text: "确认绘制", onclick: (e) => this.switchDraw(0)}];
        const liveConfig = [
            {text: "主码流", onclick: (e) => this.switchStream(0)},
            {text: "辅码流", onclick: (e) => this.switchStream(1)},
        ];
        const vodConfig = [
            {text: "前端存储", onclick: (e) => this.toggleVodOriginForNumber(1)},
            {text: "中心存储", onclick: (e) => this.toggleVodOriginForNumber(0)},
        ];
        const _menuArr = [
            {
                text: "画面比例",
                hotkey: "❯",
                subitems: [
                    {
                        text: "原始",
                        onclick: (e) => this.resetRatio(),
                    },
                    {text: "充满", onclick: (e) => this.setFillRatio()},
                    {text: "16:9", onclick: (e) => this.ratioAdjust(16, 9)},
                    {text: "4:3", onclick: (e) => this.ratioAdjust(4, 3)},
                    {text: "16:10", onclick: (e) => this.ratioAdjust(16, 10)},
                ],
            },
        ];
        let menu = [];
        switch (val) {
            case "live":
                menu = [...liveConfig, ..._menuArr];
                break;
            case "vod":
                menu = [...vodConfig, ..._menuArr];
                break;
            case "drawing":
                menu = [...drawConfig];
                break;
        }
        return [
            ...menu,
            {
                text: "江河信息",
                hotkey: "  V" + version,
                disabled: true,
            },
        ];
    }

    private bindEvents(playerMode: PlayerMode = PlayerMode.DESKTOP) {
        this.initHammer();
        const ele = this.el;
        const vel = this.vel;
        const isMobile = playerMode === PlayerMode.MOBILE;
        ele.addEventListener("fullscreenchange", this.fullScreenHandler, false);
        ele.addEventListener("mozfullscreenchange", this.fullScreenHandler, false);
        ele.addEventListener("MSFullscreenChange", this.fullScreenHandler, false);
        ele.addEventListener(
            "webkitfullscreenchange",
            this.fullScreenHandler,
            false
        );
        // vel.addEventListener("seeked", this.onvSeeked.bind(this));
        this.$contextmenu = isMobile ? null : fromEvent(ele, "contextmenu");
        this.$resize = fromEvent(window, "resize");
        this.$click = isMobile
            ? new Observable((subscriber) => {
                this._rootHammertime.on("singletap", (e) => {
                    if (
                        e.target &&
                        (e.target.nodeName === "VIDEO" || e.target.nodeName === "CANVAS")
                    ) {
                        subscriber.next(e);
                    }
                });
            })
            : fromEvent(ele, "click");
        this.$dblclick = isMobile
            ? new Observable((subscriber) => {
                this._rootHammertime.on("doubletap", (e) => {
                    if (
                        e.target &&
                        (e.target.nodeName === "VIDEO" || e.target.nodeName === "CANVAS")
                    ) {
                        subscriber.next(e);
                    }
                });
            })
            : fromEvent(ele, "dblclick");
        this.$mouseenter = fromEvent(ele, "mouseenter");
        this.$mouseout = fromEvent(ele, "mouseleave");
        this.$mousemove = fromEvent(ele, "mousemove").pipe(throttleTime(52));
        this.$mousedown = fromEvent(ele, "mousedown");
        this.$mouseup = fromEvent(ele, "mouseup");
        this.$mousewheel = isMobile
            ? new Observable((subscriber) => {
                this._rootHammertime.on('pinchin pinchout', (e) => {
                    if (
                        this.streamOpt.isptz &&
                        this.streamOpt.streamtype === "live" &&
                        e.target &&
                        (e.target.nodeName === "VIDEO" || e.target.nodeName === "CANVAS")
                    ) {
                        subscriber.next(e);
                    }
                });
            })
            : fromEvent(ele, 'wheel').pipe(
                filter((e: WheelEvent) => {
                    e.stopPropagation();
                    e.preventDefault();
                    return true;
                }),
                throttleTime(30)
            );
        // 绑定一些播放器需要的事件
        this.initObservables();
    }

    private unBindEvents() {
        const ele = this.el;
        ele.removeEventListener("fullscreenchange", this.fullScreenHandler, false);
        ele.removeEventListener(
            "mozfullscreenchange",
            this.fullScreenHandler,
            false
        );
        ele.removeEventListener(
            "MSFullscreenChange",
            this.fullScreenHandler,
            false
        );
        ele.removeEventListener(
            "webkitfullscreenchange",
            this.fullScreenHandler,
            false
        );
        this.unSubscribeObservables();
    }

    public setFullScreen(value = true) {
        const isMobile = this.playerMode === PlayerMode.MOBILE;
        if (this.wrapperel) {
            if (value) {
                openFullscreen(this.wrapperel, isMobile);
            } else {
                exitFullScreen(isMobile);
            }
            if (isMobile) {
                this.emitter.emit(JPEvent.FullScreen, !this._isFullScreen);
            }
        }
    }

    private fullScreenHandler = function (e) {
        const that = this;
        this.el.style.fontSize =
            this.wrapperel.clientWidth >= 800 ? "16px" : "14px";
        setTimeout(() => {
            const isMobile = this.playerMode === PlayerMode.MOBILE;
            if (
                this.wrapperel.clientWidth === window.innerWidth &&
                this.wrapperel.clientHeight === window.innerHeight
            ) {
                this.isFullScreen = true;
                isMobile && that.initNipple();
            } else {
                this.isFullScreen = false;
                isMobile && that.destroyNipple();
            }
            // this.fixPickerTime();
        }, 160);
    }.bind(this);

    private setMCPosition = (e: MouseEvent) => {
        const {wrapperel, _offsetObj} = this;
        const mc: HTMLDivElement = wrapperel.querySelector(".mock-cursor");
        const ox = (_offsetObj && _offsetObj.x) || 0;
        const oy = (_offsetObj && _offsetObj.y) || 0;
        if (mc) {
            mc.style.left = e.offsetX + ox + "px";
            mc.style.top = e.offsetY + oy + "px";
        }
        return e;
    };
    private getPtzPosition = (e: MouseEvent) => {
        const headerToolbar = this.el.querySelector(`.header`);
        const footerToolbar = this.el.querySelector(`.footer`);
        const headerCH = headerToolbar ? headerToolbar.clientHeight : 0;
        const footerCH = footerToolbar ? footerToolbar.clientHeight : 0;
        const H = this.videoWrapperEl.clientHeight;
        const W = this.videoWrapperEl.clientWidth;
        const y =
            e.offsetX <= W * 0.3333
                ? 0
                : e.offsetX > W * 0.3333 && e.offsetX < W * 0.66666
                    ? 1
                    : 2;
        const x =
            e.offsetY <= H * 0.3333
                ? 0
                : e.offsetY > H * 0.3333 && e.offsetY < H * 0.66666
                    ? 1
                    : 2;
        return [x, y];
    };

    /**
     *  3 2 1
     *  4 * 0
     *  5 6 7
     *
     */
    private getPtzPositionFlag = ([x, y]) => {
        switch (x) {
            case 0:
                return y === 0 ? 3 : y === 1 ? 2 : 1;
            case 1:
                return y === 0 ? 4 : y === 1 ? -99 : 0;
            case 2:
                return y === 0 ? 5 : y === 1 ? 6 : 7;
            default:
                return -99;
        }
    };
    private appendMockCursor = (e) => {
        const {wrapperel} = this;
        const has = wrapperel.querySelector(".mock-cursor");
        if (!has) {
            const m = document.createElement("div");
            m.className = "mock-cursor";
            wrapperel.appendChild(m);
        }
        return e;
    };

    private showToolbars = () => {
        if (!this._drawing) {
            const videoBox = this.el.querySelector(
                "." + this.prefixName + "-video-box"
            );
            videoBox.classList.add("show-tools");
        }
    };

    private hideToolbars = () => {
        // const isMobile = this.playerMode === PlayerMode.MOBILE;
        const videoBox = this.el.querySelector(
            "." + this.prefixName + "-video-box"
        );
        videoBox.classList.remove("show-tools");
        // if (this._drawing) {
        //   videoBox.classList.remove("show-tools");
        // } else {
        //   (!this.isFullScreen || isMobile) &&
        //     videoBox.classList.remove("show-tools");
        // }
    };

    private showSettingMenu = () => {
        const videoBox = this.el.querySelector(
            "." + this.prefixName + "-video-box"
        );
        videoBox.classList.add("show-setting");
    };

    private hideSettingMenu = () => {
        const videoBox = this.el.querySelector(
            "." + this.prefixName + "-video-box"
        );
        videoBox.classList.remove("show-setting");
    };

    public hideContextMenu() {
        this.contextMenu && this.contextMenu.hide();
    }

    private checkEleSize = () => {
        if (this.isFullScreen) {
            return StyleSize.LG;
        }
        const {
            vel: {clientWidth = 0},
        } = this;
        if (clientWidth >= 0 && clientWidth < 300) {
            this.styleSize = StyleSize.XS;
            return StyleSize.XS;
        } else if (clientWidth >= 300 && clientWidth < 600) {
            this.styleSize = StyleSize.SM;
            return StyleSize.SM;
        } else if (clientWidth >= 600 && clientWidth < 800) {
            this.styleSize = StyleSize.MD;
            return StyleSize.MD;
        } else if (clientWidth >= 800) {
            this.styleSize = StyleSize.LG;
            return StyleSize.LG;
        }
    };

    private addPtzClass = (flag) => {
        const {wrapperel: el} = this;
        const cls = [
            "top",
            "top-right",
            "right",
            "right-down",
            "down",
            "down-left",
            "left",
            "left-top",
        ];
        const replaceWithCls = (newCls: string) => {
            el.classList.remove(...cls.filter((v) => v !== newCls));
            el.classList.add(newCls);
        };
        switch (flag) {
            case 2:
                replaceWithCls("top");
                return;
            case 1:
                replaceWithCls("top-right");
                return;
            case 0:
                replaceWithCls("right");
                return;
            case 7:
                replaceWithCls("right-down");
                return;
            case 6:
                replaceWithCls("down");
                return;
            case 5:
                replaceWithCls("down-left");
                return;
            case 4:
                replaceWithCls("left");
                return;
            case 3:
                replaceWithCls("left-top");
                return;
            case -99:
                this.clearPtzClass();
                return;
            default:
                return;
        }
    };
    private clearPtzClass = () => {
        const {wrapperel: el} = this;
        const cls = [
            "top",
            "top-right",
            "right",
            "right-down",
            "down",
            "down-left",
            "left",
            "left-top",
        ];
        el.classList.remove(...cls);
    };

    /**
     * 根据起始时间和偏移秒数，计算出新的绝对时间字符串
     * @param originalTimeStr 原始绝对时间，例如 "2026-03-19 12:00:00"
     * @param offsetSeconds 播放器当前播放的相对秒数，例如 125.5
     * @returns 新的绝对时间字符串，例如 "2026-03-19 12:02:05"
     */
    private calculateNewBeginTime(originalTimeStr: string, offsetSeconds: number): string {
        if (!originalTimeStr) return "";

        // 兼容 iOS Safari 无法解析 "YYYY-MM-DD HH:mm:ss" 的问题，替换为 "YYYY/MM/DD HH:mm:ss"
        const safeTimeStr = originalTimeStr.replace(/-/g, '/');
        const baseDate = new Date(safeTimeStr);

        if (isNaN(baseDate.getTime())) return originalTimeStr; // 解析失败则原样返回

        // 加上相对秒数 (向下取整)
        baseDate.setSeconds(baseDate.getSeconds() + Math.floor(offsetSeconds));

        // 格式化回 YYYY-MM-DD HH:mm:ss
        const y = baseDate.getFullYear();
        const m = String(baseDate.getMonth() + 1).padStart(2, '0');
        const d = String(baseDate.getDate()).padStart(2, '0');
        const h = String(baseDate.getHours()).padStart(2, '0');
        const min = String(baseDate.getMinutes()).padStart(2, '0');
        const s = String(baseDate.getSeconds()).padStart(2, '0');

        return `${y}-${m}-${d} ${h}:${min}:${s}`;
    }

    public play(isGive = true) {
        const { streamtype } = this.streamOpt
        if (this.playerIns && this.playerIns.isNetworkDead === true) {
            console.warn(`[${streamtype}] 连接已断开，正在恢复...`);

            this.tryAutoRecover(() => {
                this.prepareAutoRecoverStreamState();
            });
            return;
        }

        // 如果网络没断，走正常的播放逻辑
        this.playerIns && this.playerIns.play();
        this.emitter.emit(JPEvent.PLAY);
        if (isGive) {
            this.playing = true;
        } else {
            this._playing = true;
            const playBtn = this.el.querySelector(`.${this.prefixName}-play-button`);
            playBtn && playBtn.classList.add("playing");

            const wrapper = this.el.querySelector("." + this.prefixName + "-wrapper");
            wrapper && wrapper.classList.remove("pause");
        }
    }

    public pause() {
        this.playerIns && this.playerIns.pause();
        this.playing = false;
        this.emitter.emit(JPEvent.PAUSE);
    }

    private resetPlayerStatus() {
        this.destroyMediaRecorder();
        this.error = false;
        this.ptzing = false;
        this.zooming = false;
        this.seeking = false;
        this.hideContextMenu();
    }

    public async refresh(streamOpt?: StreamOpt) {
        this.resetPlayerStatus();
        this.emptyVideoTool();
        const s = streamOpt || Object.assign(this.streamOpt, {});
        await this.init(s);
        this.fullScreenHandler();
    }

    public toggleVod(streamOpt?: StreamOpt) {
        this.resetPlayerStatus();
        const s = streamOpt || Object.assign(this.streamOpt, {});
        s.streamtype = "vod";
        this.emptyVideoDom();
        this.initDom();
        return this.init(s);
    }

    private setCanvasSize() {
        if (!this.playerIns) {
            return
        }
        const _wasmPlayer = !!this.playerIns._wasmPlayer;
        const {zoomScale, canvas} = this;
        const {width, height} = this.mediaInfo;
        const {clientHeight, clientWidth, videoWidth, videoHeight} = this.vel;
        const zoomWrapper: HTMLDivElement = this.el.querySelector(
            "." + this.prefixName + "-zoom-wrapper"
        );
        zoomWrapper.style.width = clientWidth * zoomScale + "px";
        zoomWrapper.style.height = clientHeight * zoomScale + "px";
        canvas.width = _wasmPlayer ? width : videoWidth;
        canvas.height = _wasmPlayer ? height : videoHeight;
        // 重置矩形框画布大小
        this.streamOpt.isDraw && this.rectBlock.setDom(clientWidth, clientHeight);
    }

    /**
     * 绘制矩形框
     **/
    private switchDraw(val: number) {
        // 确认
        if (val === 0) {
            this.contextMenu.setMenu(this.getRightMenu(""));
            this.wrapperel.classList.remove("drawing");
            this.rectBlock.finish();
        }
        // 删除
        if (val === 1) {
            this.rectBlock.remove();
        }
        this._drawing = false;
        setTimeout(() => {
            this.showToolbars();
        }, 100);
    }

    private async videoIsOriginalState() {
        if (this.ptzing) {
            this.ptzing = false;
        }
        if (this.zooming) {
            this.zooming = false;
        }
        if (this.mediaRecorder) {
            this.mediaRecorder.stop();
        }
        const {streamOpt} = this;
        const {w, h} = this.videoRatio;
        if (streamOpt.aisleId.includes("#1")) {
            await this.switchStream(0);
        }
        if ((w < 18 && h < 18) || w === null || h === null) {
            this.resetRatio();
        }
    }

    // 点击绘制按钮操作
    private toggleDraw() {
        this._drawing = true;
        this.videoIsOriginalState();
        this.contextMenu.setMenu(this.getRightMenu("drawing"));
        this.wrapperel.classList.add("drawing");
        this.rectBlock.begin();
        if (this._drawing) {
            document.onkeydown = (e) => {
                if (e.keyCode === 13) {
                    //回车安静
                    this.switchDraw(0);
                }
            };
        }
        setTimeout(() => {
            this.hideToolbars();
        }, 9);
    }

    // private toggleCall.call(this, e.currentTarget)

    drawBtnHandle(e: any) {
        if (e.target.className === "cancel") {
            this.rectBlock.cancelRatio();
        }
        this.switchDraw(0);
    }

    private setRectRatio(shape: { x: any; y: any }[] | undefined) {
        this.rectBlock.setProportion(shape);
        this.contextMenu.setMenu(this.getRightMenu(""));
        this.videoIsOriginalState();
    }

    public async getShape() {
        if (this._drawing) {
            await this.switchDraw(0);
        }
        return this.rectBlock.getRatio();
    }

    //-------------------------------------------------------------------------------------------------------==>
    public setFillRatio() {
        this.videoWrapperEl.style.width = "100%";
        this.videoWrapperEl.style.height = "100%";
        this.videoWrapperEl.style.transform = `none`;
        this._offsetObj = null;
        this.videoRatio = {
            w: null,
            h: null,
        };
        const {prefixName, wrapperel, streamOpt} = this;
        if (streamOpt.isDraw) {
            const rectangle: HTMLDivElement = wrapperel.querySelector(
                "." + prefixName + "-rectangle-wrapper"
            );
            rectangle.style.top = `0px`;
            rectangle.style.bottom = `0px`;
        }
        this.setCanvasSize();
    }

    public resetRatio() {
        this.videoWrapperEl.style.width = "100%";
        this.videoWrapperEl.style.height = "100%";
        this.videoWrapperEl.style.transform = `none`;
        this._offsetObj = null;
        if (this.playerIns) {
            const {width, height} = this.playerIns;
            this.ratioAdjust(width, height);
        }
    }

    public ratioAdjust(W = 1920, H = 1080) {
        this.videoRatio = {w: W, h: H};
        const {prefixName, wrapperel, vel, streamOpt} = this;
        const {clientHeight, clientWidth} = vel;
        const w = this._isFullScreen
            ? clientWidth
            : parseInt(String(this.el.clientWidth) || this.el.style.width);
        const h = this._isFullScreen
            ? clientHeight
            : parseInt(String(this.el.clientHeight) || this.el.style.height);
        const r = w / h;
        const R = W / H;
        this._offsetObj = {x: 0, y: 0};
        const rectangle: HTMLDivElement = wrapperel.querySelector(
            "." + prefixName + "-rectangle-wrapper"
        );
        if (r >= R) {
            // 以高为准 设置width translateX
            const cw = Math.floor(h * R);
            const n = w - cw === 0 ? 0 : Math.floor((w - cw) / 2);
            this.videoWrapperEl.style.height = "100%";
            this.videoWrapperEl.style.width = cw + "px";
            this.videoWrapperEl.style.transform = `translateX(${n}px)`;
            this._offsetObj.x = n;
            if (streamOpt.isDraw) {
                rectangle.style.top = `${n}px`;
                rectangle.style.bottom = `${n}px`;
            }
        } else if (r < R) {
            // 以宽为准 设置height translateY
            const ch = Math.floor(w * (1 / R));
            const n = h - ch === 0 ? 0 : Math.floor((h - ch) / 2);
            this.videoWrapperEl.style.width = "100%";
            this.videoWrapperEl.style.height = ch + "px";
            this.videoWrapperEl.style.transform = `translateY(${n}px)`;
            this._offsetObj.y = n;
            if (streamOpt.isDraw) {
                rectangle.style.top = `${n}px`;
                rectangle.style.bottom = `${n}px`;
            }
        }
        this.setCanvasSize();
    }

    public async record(time = 60) {
        // 视频未就绪
        if (!this.playerOnCanPlay) {
            console.warn('播放器正在初始化...')
            return
        }

        if (!this.mediaRecorder) {
            const mediaDevicesStatus = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
              .then(() => Promise.resolve(true)).catch(() => Promise.resolve(false))

            if (!mediaDevicesStatus) {
              this.messageError = '请打开浏览器录制权限'
              return
            }

            this.recording = true;
            // 未开始录制的情况
            /*const isWasmPlayer = !!this.playerIns._wasmPlayer;
            const vod = isWasmPlayer ? this.cel : this.vel*/
            this.mediaRecorder = new JPlayerMediaRecorder(this.vel, time);
            this.mediaRecorder.on("complete", (e) => {
                this.recording = false;
                this.mediaRecorder.destroy();
                this.mediaRecorder = null;

                if (!e.length) {
                  console.warn('未获取到视频流')
                  return
                }
                const name =
                    (this.streamOpt.title || this.streamOpt.aisleId) +
                    getNowDate() +
                    "录像";

                const mimeType = "video/webm;codecs=vp9"
                const blob = new Blob(e, { type: mimeType });
                getSeekableBlob(blob,  mimeType, (fixedBlob) => {
                  const url = URL.createObjectURL(fixedBlob);
                  let a = document.createElement("a");

                  // 创建一个单击事件
                  const event = new MouseEvent("click");
                  // 将a的download属性设置为我们想要下载的图片名称，若name不存在则使用‘下载图片名称’作为默认名称
                  a.download = name;
                  // 将生成的URL设置为a.href属性
                  a.href = url;
                  // 触发a的单击事件
                  a.dispatchEvent(event);
                  a = null;
                });
            });
            this.mediaRecorder.start();

            // this.mediaRecorder = new FileStorage();
            // this.mediaRecorder.startRecord('111',{fileFormat:5});
            // const { _ws } = this.playerIns._transmuxer._controller._ioctl._loader
            // _ws.onmessage = (msg) => {
            //   let { data } = msg
            //   if (data instanceof ArrayBuffer && this.recording) {
            //     console.log('data:',data)
            //     this.mediaRecorder.inputData(data)
            //   }
            // }
        } else {
            this.mediaRecorder.stop();
            // this.recording = false
            // this.mediaRecorder.stopRecord();
            // console.log('recording:',this.recording)
        }
    }

    public screenshot() {
        // 视频未就绪
        if (!this.playerOnCanPlay || !this.mediaInfo) {
            return;
        }
        const {width, height, videoHeight, videoWidth} = this.mediaInfo;
        const {vel, cel} = this;
        const isWasmPlayer = !!this.playerIns._wasmPlayer;
        let canvas = document.createElement("canvas");
        canvas.width = width || videoWidth;
        canvas.height = height || videoHeight;
        canvas
            .getContext("2d")
            .drawImage(isWasmPlayer ? cel : vel, 0, 0, canvas.width, canvas.height);
        let url = canvas.toDataURL("image/png");
        // 生成一个a元素
        let a = document.createElement("a");
        // 创建一个单击事件
        var event = new MouseEvent("click");

        // 将a的download属性设置为我们想要下载的图片名称，若name不存在则使用‘下载图片名称’作为默认名称
        a.download =
            (this.streamOpt.title || this.streamOpt.aisleId) + getNowDate() + "截图";
        // 将生成的URL设置为a.href属性
        a.href = url;
        // 触发a的单击事件
        a.dispatchEvent(event);
        this.emitter.emit(JPEvent.SCREENSHOT, url);
        canvas = null;
        a = null;
    }

    public toggleSetting() {
        this.setting = !this.setting;
    }

    public togglePtz() {
        this.ptzing = !this.ptzing;
    }

    public toggleZoom() {
        this.zooming = !this.zooming;
    }

    private emptyVideoDom() {
        this.emptyVideoTool();
        const vBox = this.el.querySelector("." + this.prefixName + "-video-box");
        vBox.className = this.prefixName + "-video-box";
        vBox.innerHTML = null;
    }

    private emptyVideoTool() {
        this.unSubscribeToolbarObservables();
        const toolbars = this.el.querySelectorAll(
            "." + this.prefixName + "-toolbar"
        );
        toolbars.forEach((t) => {
            t.parentNode.removeChild(t);
        });
    }

    // 切换录像源 前端/中心
    toggleVodOrigin(e) {
        const item: HTMLElement = e.target;
        const parent = item.parentElement;
        for (let index = 0; index < parent.children.length; index++) {
            const element = parent.children[index];
            element.classList.remove("active");
        }
        item.classList.add("active");
        const vod = item.getAttribute("data-value");
        this.streamOpt.vod = Number(vod);
        this.refresh();
    }

    // 切换录像源 前端/中心
    toggleVodOriginForNumber(vod: number) {
        this.streamOpt.vod = Number(vod);
        this.refresh();
    }

    // 创建数码放大块
    creatZoomStartPonit(ox, oy) {
        this.cleanZoomStartPonit();
        const div = document.createElement("div");
        div.className = "zoom-block";
        div.style.position = "absolute";
        div.style.top = oy + "px";
        div.style.left = ox + "px";
        this.videoWrapperEl.appendChild(div);
    }

    // 回收数码放大块
    cleanZoomStartPonit() {
        const div = this.videoWrapperEl.querySelector(".zoom-block");
        div && this.videoWrapperEl.removeChild(div);
    }

    // 设置数码块大小及位置信息
    setZoom(w, h, dir: Direction) {
        const {
            _zoomStartPonit: {offsetX, offsetY},
        } = this;
        const div: HTMLDivElement =
            this.videoWrapperEl.querySelector(".zoom-block");
        if (dir === Direction.DR) {
            div.style.width = w + "px";
            div.style.height = h + "px";
        } else if (dir === Direction.DL) {
            div.style.width = w + "px";
            div.style.height = h + "px";
            div.style.left = offsetX - w + "px";
        } else if (dir === Direction.TL) {
            div.style.width = w + "px";
            div.style.height = h + "px";
            div.style.left = offsetX - w + "px";
            div.style.top = offsetY - h + "px";
        } else if (dir === Direction.TR) {
            div.style.width = w + "px";
            div.style.height = h + "px";
            div.style.top = offsetY - h + "px";
        }
    }

    // 更新进度条
    private updateProcess = (e) => {
        const {vel, playerType} = this;
        if (playerType === "webgl") {
            const offsetTime = this.playerIns._wasmPlayer.currentTime * 1e3;
            this.offsetTime = offsetTime;
        } else {
            const offsetTime = vel.currentTime * 1e3;
            this.offsetTime = offsetTime;
        }
    };
    // 移动端模式下更新TimeLine
    private updateTimeLine = () => {
        const {vel, playerType, _begintime} = this;
        let offsetTime;
        if (playerType === "webgl") {
            offsetTime = this.playerIns._wasmPlayer.currentTime * 1e3;
        } else {
            offsetTime = vel.currentTime * 1e3;
        }
        if (this.timelineIns) {
            this.timelineIns.set_time_to_middle(_begintime + offsetTime);
        }
    };

    private updateProcessBarView(offsetTime: number) {
        const totalTime = this._endtime - this._begintime;
        const eleWidth = this.wrapperel.clientWidth;
        const offsetBar: HTMLDivElement = this.el.querySelector(".offset-bar");
        if (offsetBar) {
            offsetBar.style.width = (offsetTime * eleWidth) / totalTime + "px";
        }
        const offsetPoint: HTMLDivElement = this.el.querySelector(".offset-point");
        if (offsetPoint) {
            offsetPoint.style.left =
                ((offsetTime * eleWidth) / totalTime - 3 <= 3
                    ? "3px"
                    : (offsetTime * eleWidth) / totalTime) + "px";
        }
        this.updateProcessClockView(offsetTime);
    }

    private updateProcessClockView(offsetTime: number) {
        const currentTXT = formatTimeClock(offsetTime);
        const c: HTMLSpanElement = this.el.querySelector(
            ".time-clock-item.current"
        );
        c && (c.innerText = currentTXT);
    }

    // getOffetTime
    private getOffetTime = (e) => {
        const isPoint = e.target.classList.contains("offset-point");
        const totalTime = this._endtime - this._begintime;
        const oX = isPoint ? e.offsetX + e.target.offsetLeft : e.offsetX;
        const eleWidth = this.wrapperel.clientWidth;
        return (e.offsetX / eleWidth) * totalTime;
    };
    // getOffetClientTime
    private getOffetClientTime = (e) => {
        const {clientX} = e;
        const {clientX: originx, offsetX} = this._ponitDragClick;
        const eleWidth = this.wrapperel.clientWidth;
        const totalTime = this._endtime - this._begintime;

        const obsx = clientX - originx;
        let newOffsetX = offsetX + obsx;
        if (newOffsetX <= 0) {
            newOffsetX = 0;
        }
        if (newOffsetX >= eleWidth) {
            newOffsetX = eleWidth;
        }
        const offsetTime = (newOffsetX / eleWidth) * totalTime;
        return offsetTime;
    };
    // getOffetX
    private getOffetX = (e) => {
        const isPoint = e.target.classList.contains("offset-point");
        return isPoint ? e.offsetX + e.target.offsetLeft : e.offsetX;
    };

    initNipple() {
        if (!this.joystickele) return;
        if (this.nippleIns) {
            this.destroyNipple();
        }
        setTimeout(() => {
            const opt = {
                zone: this.joystickele,
                mode: "static",
                position: {left: "50%", top: "50%"},
                color: "white",
            };
            this.nippleIns = nipplejs.create(opt);
            this.nippleIns.on(
                "dir:up dir:down dir:left dir:right end",
                (evt, data) => {
                    if (evt.type === "end") {
                        setTimeout(() => {
                            this.$ptzSubject.next([0, 0]);
                        }, 26);
                    } else {
                        const f = getJoysticPositionFlag(evt.type);
                        this.$ptzSubject.next([f, this._ptzSpeed]);
                    }
                }
            );
        }, 500);
    }

    // 可以暴露外部使用的cmd云台控制
    public sendPtzCmd(cmd, param?: number) {
        if (cmd === -1) {
            this.$ptzSubject.next([0, 0]);
        } else {
            this.$ptzSubject.next([
                cmd,
                param === undefined || param === null ? this._ptzSpeed : param,
            ]);
        }
    }

    // 外部声音控制开关
    public sendAudio() {
        return this._audioCtrl.setState()
    }

    destroyNipple() {
        if (!this.joystickele) return;
        if (this.nippleIns) {
            this.nippleIns.off();
            this.nippleIns.destroy();
            this.nippleIns = null;
        }
    }

    close() {
        this.teardownPlaybackSession()
        document.onkeydown = null;

        this.emitter.emit(JPEvent.CLOSE);
        this.hideContextMenu();
        this.error = "暂无视频";
    }

    private teardownPlaybackSession() {
        this.playerOnCanPlay = false;
        this.clearOsdTimeId();
        this.clearAutoRecoverResetTimer();
        if (this._canPlayTimer) {
            clearTimeout(this._canPlayTimer);
            this._canPlayTimer = null;
        }
        this._isAutoRecovering = false;
        this.destroyMediaRecorder();

        // 播放器
        if (typeof this.playerIns !== "undefined") {
            if (this.playerIns != null) {
                console.log('playerIns')
                this.playerIns.unload();
                this.playerIns.detachMediaElement();
                this.playerIns.destroy();
                this.playerIns = null;
                // 清除旧流缓存，防止 readyState 残留导致重试时黑屏
                if (this.vel) {
                    this.vel.src = '';
                    this.vel.load();
                }
            }
        }

        // 对话
        if (this._videoStream) {
            this._videoStream.close()
            this._videoStream = null
        }

        // 销毁fabric实例
        if (this.rectBlock) {
          this.rectBlock.destroy();
          this.rectBlock = null;
        }
    }

    clearOsdTimeId() {
        if (this.osdTimeId) {
            clearTimeout(this.osdTimeId)
            this.osdTimeId = null
        }
    }

    destroy() {
        if (this.h5msClient) {
            this.h5msClient.destroy();
            this.h5msClient = null;
        }

        this.teardownPlaybackSession()
        document.onkeydown = null;

        if (this._vodPlayer) {
            this._vodPlayer.destroy()
            this._vodPlayer = null
        }

        this.hideContextMenu();
        this.contextMenu = null;
        this._audioCtrl = null;

        this.unBindEvents();
        this.unSubscribeObservables();
        this.unSubscribeToolbarObservables();
        this.emptyVideoDom();

        // 销毁手势实例
        if (this._rootHammertime) {
            this._rootHammertime.destroy();
            this._rootHammertime = null;
        }

        // 销毁timeline实例
        if (this.timelineIns) {
            this.timelineIns.destroy();
            this.timelineIns = null;
        }

        // 销毁对讲
        if (this._talkCtrl) {
            this._talkCtrl.destroy();
            this._talkCtrl = null;
        }

        if (this.emitter) {
            this.emitter.emit(JPEvent.DESTROY);
            this.emitter.removeAllListeners();
            this.emitter = null;
        }

        this.error = "暂无视频";
    }

    private destroyMediaRecorder() {
        if (!this.mediaRecorder) {
            return;
        }
        if (this.recording) {
            try {
                this.mediaRecorder.stop();
            } catch (e) {
                console.warn("停止录制器失败", e);
            }
        }
        this.mediaRecorder.destroy();
        this.mediaRecorder = null;
        this.recording = false;
    }
}

export default JPlayer;
