UNPKG

@jxstjh/jhvideo

Version:

HTML5 jhvideo base on MPEG2-TS Stream Player

1,430 lines (1,323 loc) 96.7 kB
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 } 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("complishe", _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; this.subscriptiontimer.unsubscribe() this.subscriptiontimer = null this.timer$ = null; 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 _talkCtrl: any; // 对话实例 _vodPlayer: any; // 回放功能 private osdTimeId: any = null; // OSD时间 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 _retryTime = 0; // 出错后重试 private _maxRetryTime = 3; // 最大重连次数 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, span) this._passageRepeat = 0 }) 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; 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); } async init(stream?: StreamOpt) { if (!JPlayer.isSupported()) { if (Browser.platform === "iphone") { this.error = "抱歉!移动端播放器暂不支持iphone平台!请移步至pc或者使用其它安卓设备观看."; } else { this.error = "浏览器版本不支持视频播放!请尝试使用系统自带浏览器尝试!"; } } stream = {...(createDefaultStreamOpt() as StreamOpt), ...stream}; this.streamOpt = stream; this.loading = true; this.initContextMenu(stream); // 回放 try { if (stream.streamtype === "vod") { const {url, headers} = this.requestInfo(serverName + "/hik-stream/back-stream"); this._vodPlayer = new vodPlayer(stream, 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?._wasmPlayer ? this.playerIns._wasmPlayer._currentTime : this.playerIns.currentTime this.emitter.emit(JPEvent.SEEKED) } 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 (stream.streamtype === "live") { this.loadingTxt = "视频缓冲中..."; this.playUrl = await this.openStream(stream); // 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().catch((e) => console.error(e) ); if (code !== 0) return // 初始工具栏 this.initToolBar(); this.setFillRatio(); this.emitter.emit(JPEvent.INITED, this.vid, this); this.loading = false; this.play(false); } catch (error) { // TODO:实例化播放错误 console.warn(error); // this.emitter.emit(JPEvent.ERROR); } // 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.globalClient.config; // 确保清除上一次的实例 this.playerDestroy(); 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; // 参数 let type = protocolType === "httpflv" || protocolType === "websocketflv" ? "flv" : "hls"; if (!msePlayback) { type = "hls"; } const mediaDataSource: any = { type, isLive: true, url: this.playUrl, }; const optionalConfig = { // autoCleanupSourceBuffer: true, //对SourceBuffer进行自动清理缓存 // // autoCleanupMaxBackwardDuration: 12, // 当向后缓冲区持续时间超过此值(以秒为单位)时,请对SourceBuffer进行自动清理 // // autoCleanupMinBackwardDuration: 60, // 指示进行自动清除时为反向缓冲区保留的持续时间(以秒为单位)。 // enableStashBuffer: false, //关闭IO隐藏缓冲区 // enableWorker: this.streamOpt.enableWorker, // workerPath, // seekType: 'range', // useOuterLoader: true, // statisticsInfoReportInterval: 100, // reuseRedirectedURL: true, // deferLoadAfterSourceOpen: false, // enableWorker: true, // enableStashBuffer: true, // enableWorker: true, seekType: 'range', useOuterLoader: true, autoCleanupSourceBuffer: 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.playerIns.attachMediaElement(this.vel, this.cel); this.playerIns.load(); this.playerOnCanPlay = true; // this.playerIns?._wasmPlayer && (this.playerType = "webgl"); const closeLoad = () => { this.playing = true; this.playerIns?._wasmPlayer && (this.playerType = "webgl") if (this.streamOpt.streamtype === "vod") { this.updateOsdTimeThrottle() } // TODO 开流黑屏时间太长建议处理方式 setTimeout(() => { this.emitter && this.emitter.emit(JPEvent.CANPLAY); this.playerOnCanPlay = true;