@jxstjh/jhvideo
Version:
HTML5 jhvideo base on MPEG2-TS Stream Player
1,430 lines (1,323 loc) • 96.7 kB
text/typescript
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;