UNPKG

@jxstjh/jhvideo

Version:

HTML5 jhvideo base on MPEG2-TS Stream Player

511 lines (486 loc) 18.3 kB
import {StreamOpt, StreamSpeed} from "../../model/playerModel"; import httpClient from "../../core/httpClient"; import TimeLine, {format} from "./timeline/timeline.es" import {getDateStrBySize} from "../utils"; import * as AirDatepicker from "air-datepicker/air-datepicker.js"; import localeZh from 'air-datepicker/locale/zh'; import {EventEmitter} from 'events' import {streamSpeedIconMap} from "../icons"; const prefixName = 'JPlayer' interface timeOP { beginTime: string endTime: string size: string } class vodPlayer { private streamOpt: StreamOpt; private timeline: TimeLine private allListTime: timeOP[] // 时间轴(前后时间) private runListTime: timeOP[] // 时间轴会运动的时间 private shiftingTime: number = 0 // 每一段中间的空挡时间 private el: any private picker: any private param = { time: 0, cont: 24 }; private requestInfo: { url: string, headers: any } private size = 'lg' public _streamSpeed: StreamSpeed = 0; // 录像模式下流播放速度 private loading = false; private process: HTMLElement; private timeHandle: HTMLElement; private recordTip: number | null = null; private isTip = true; emitter: any constructor(stream: StreamOpt,size:string, el: Element, requestInfo: { url: string, headers: any }) { this.streamOpt = stream this.size = size this.el = el this.requestInfo = requestInfo this.emitter = new EventEmitter() } on(event: any, listener: any) { this.emitter.addListener(event, listener); } off(event: any, listener: any) { this.emitter.removeListener(event, listener); } // 初始化 init() { const videoBox = this.el.querySelector("." + prefixName + "-video-box"); const process = document.createElement('div') process.className = `${prefixName}-toolbar ${prefixName}-process` // 时间线 const timeLine = document.createElement("canvas") timeLine.id = `${prefixName}-time-line` process.appendChild(timeLine) // 控制按钮 const timeHandle = document.createElement("div") timeHandle.className = `${prefixName}-time-handle` timeHandle.innerHTML=`<div class="time--card"><div class="time--left">-</div><div class="time--signal">24h</div><div class="time--right">+</div></div>` process.appendChild(timeHandle) // videoBox.appendChild(process) this.process = process this.timeHandle = timeHandle process.addEventListener('click', (e) => { e.stopPropagation() }) const _height = 30 this.timeline = new TimeLine(timeLine, { fill: true, // 适应父容器 zoom: 0, height: _height, textColor: '#fff', scaleColor: '#000', pointerColor: '#fff', bgColor: 'rgba(255,255,255,0.3)', areaBgColor: '#', pointerWidth: 2, pointerDisplayWidth: 0, pointerDisplayHeight: 100, bgTextColor: 'transparent', scaleHeight: {long: _height / 5.2, short: _height / 9.6}, timeSpacingList: [720000, 360000, 180000, 60000], thresholdsConfig: { 720000: { scaleTimeFormat: "HH:mm", bgTimeFormat: "YYYY/MM/DD", pointerTimeFormat: "HH:mm", space: 10, }, 360000: { scaleTimeFormat: "HH:mm", bgTimeFormat: "YYYY/MM/DD", pointerTimeFormat: "HH:mm", space: 10, }, 180000: { scaleTimeFormat: "HH:mm", bgTimeFormat: "YYYY/MM/DD", pointerTimeFormat: "HH:mm", space: 10, }, 60000: { scaleTimeFormat: "HH:mm", bgTimeFormat: "YYYY/MM/DD", pointerTimeFormat: "HH:mm", space: 10, } } }) this.setTimelineEvent() this.setLineData(this.param.time) this.initPicker() } // 设置时间事件 setTimelineEvent(create = true){ const timeSignal = this.timeHandle.querySelector('.time--signal') const timeLeft = this.timeHandle.querySelector('.time--left') const timeRight = this.timeHandle.querySelector('.time--right') const setTimeSignal=(spacing:number)=>{ let text = '24h' switch (spacing){ case 720000: text = '24h';break case 360000: text = '12h';break case 180000: text = '6h';break case 60000: text = '2h';break } timeSignal.innerHTML = text } if(create){ this.timeline.on('dragged', async (timestamp: any) => { const cmdBody = { cmd: "seek", startTime: format(timestamp, 'YYYY-MM-DDTHH:mm:ss.SSSZ'), endTime: format(timestamp + (12 * 3600000), 'YYYY-MM-DDTHH:mm:ss.SSSZ'), } this.emitter.emit('vod-send', cmdBody) this.isTip = false await this.queryRecord({dateTime: format(timestamp, 'YYYY-MM-DD HH:mm')} as any) this.setLineData(this.param.time) }) this.timeline.on('zoom',(spacing:number)=>{ setTimeSignal(spacing) }) timeLeft.addEventListener('click',(e)=>{ setTimeSignal(this.timeline.setZoom(true)) }) timeRight.addEventListener('click',(e)=>{ setTimeSignal(this.timeline.setZoom(false)) }) return } timeLeft.removeEventListener('click',(e)=>{ setTimeSignal(this.timeline.setZoom(true)) },false) timeRight.removeEventListener('click',(e)=>{ setTimeSignal(this.timeline.setZoom(false)) }) } // 设置数据 setLineData(currentTime: number) { const list = this.allListTime.map(v => { return { startTime: Date.parse(v.beginTime), endTime: Date.parse(v.endTime), bgColor: 'rgba(0,115,229,0.7)' } }) this.timeline.draw({ currentTime, areas: list } as any) } // 阻止浏览器冒泡 setStopPropagation(e){ e.stopPropagation(); } // 初始化时间选择器 initPicker() { const wrapperel = this.el.querySelector("." + prefixName + "-wrapper"); const pickerEl = this.el.querySelector(".picker"); if (!pickerEl) { return; } const el = pickerEl.querySelector(".time-clock-item.range"); const input = document.createElement('input') el.appendChild(input) if (this.picker) { this.picker.destroy(); this.picker = null; } const _param = { ...this.param, isConf: false } const confButton = { content: "关闭", className: "close-button-classname", onClick: (dp: any) => { dp.hide(); }, }; const closeButton = { content: "确定", className: "conf-button-classname", onClick: (dp: any) => { _param.isConf = true this.isTip = true dp.update(dp.selectedDates); dp.hide(); this.emitter.emit('vod-refresh', { dataTime:format(dp.selectedDates, 'YYYY-MM-DD HH:mm'), vod: this.streamOpt.vod }) } } const opt = { // visible: true, // inline: true, // navTitles: {days: "yyyy - MM"}, // clearButton: true, // minDate: new Date().getTime() - 1000 * 3600 * 24 * 30, autoClose: false, timepicker: true, toggleSelected: true, locale: localeZh, maxDate: new Date(), position: "top center", buttons: [confButton, closeButton], container: wrapperel, dateFormat: (e: any) => { return getDateStrBySize(e, this.size); }, onSelect: (dp: any) => { if (dp) { dp.datepicker.setViewDate(this.param.time); } }, onShow: (dp: any) => { // if (dp) { // _param.isConf = false // _param.cont = parseInt(span.innerHTML) // } }, onHide: (dp: any) => { // if (dp && !_param.isConf) { // this.picker.selectDate(this.param.time, {updateTime: true}); // span.innerHTML = this.param.cont + '' // } } }; this.picker = new (AirDatepicker as any)(el, opt); this.picker.clear(); this.picker.selectDate(this.param.time, {updateTime: true}); this.picker.setViewDate(this.param.time); this.picker.$datepicker.addEventListener("click", this.setStopPropagation,false) } // 增加倍速 addStreamSpeed() { const {_streamSpeed} = this; if (_streamSpeed >= 3) return; this.setStreamSpeed(_streamSpeed + 1 as StreamSpeed); } // 减少倍速 reduceStreamSpeed() { const {_streamSpeed} = this; if (_streamSpeed <= -3) return; this.setStreamSpeed(_streamSpeed - 1 as StreamSpeed) } // 修改倍数 setStreamSpeed(v: StreamSpeed) { let rate = 1 switch (v) { case 0: rate = 1; break case 1: rate = 2; break case 2: rate = 4; break case 3: rate = 8; break case -1: rate = -2; break case -2: rate = -4; break case -3: rate = -8; break } const cmdBody = { v, rate, cmd: "speed", } this.emitter.emit('vod-send', cmdBody) } setSpeed(val: any) { if (val.cmd === 'speed') { this._streamSpeed = val.v this.upDateSpeedView(this._streamSpeed) } } // 更新倍速视图 upDateSpeedView(streamSpeed: StreamSpeed) { const txt = this.el.querySelector(".stream-speed-text"); const right = this.el.querySelector(".stream-speed-add"); const left = this.el.querySelector(".stream-speed-reduce"); if (!txt) { return; } txt.innerHTML = streamSpeedIconMap(streamSpeed) right.classList.remove("disabled"); left.classList.remove("disabled"); if (streamSpeed === 3) { right.classList.add("disabled"); } else if (streamSpeed === -3) { left.classList.add("disabled"); } } // 设置时间 setDateHour(date: string, hour: number, isType = true) { const curTime = new Date(date); const hourStr = isType ? curTime.getHours() + hour : curTime.getHours() - hour; return curTime.setHours(hourStr); } // 查询录像时间段 async queryRecord(stream: StreamOpt,isEmit = true) { // 查询录像时间段 this.loading = true let verify = true this.streamOpt = {...this.streamOpt, ...stream} for (let i=0; i<2; i++){ const {url, headers} = this.requestInfo const { headerToolBar, footerToolBar, title, vod, protocol, ...param} = this.streamOpt; const now = new Date(); const queryTime = param.dateTime || param.beginTime const dateTime = queryTime || format(new Date(now.getTime() - 30 * 60 * 1000), 'YYYY-MM-DD HH:mm') const hour = this.param.cont / 2 const objTime1 = { beginTime: format(dateTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ'), endTime: format(this.setDateHour(dateTime, hour, true), 'YYYY-MM-DDTHH:mm:ss.SSSZ'), } const objTime2 = { beginTime: format(this.setDateHour(dateTime, hour, false), 'YYYY-MM-DDTHH:mm:ss.SSSZ'), endTime: format(dateTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ'), } const sameObj = { protocol, aisleId: param.aisleId, recordLocation: vod + '', needReturnClipInfo: true, } const res = await Promise.all([ httpClient.post(url, { ...sameObj, ...objTime1, }, headers), httpClient.post(url, { ...sameObj, ...objTime2, }, headers) ]).catch((err) => { this.loading = false return Promise.reject(err) }) res.forEach((v, index) => { if (v.code === 0 && v.data?.list?.length > 0) { verify = true if (index === 0) { this.streamOpt.vod = vod this.runListTime = v.data.list this.allListTime = v.data.list this.param.time = Date.parse(dateTime) } else { this.allListTime = this.allListTime.concat(v.data.list) } } else if (i === 0 && index === 0) { verify = false this.streamOpt.vod = this.streamOpt.vod ? 0 : 1 } }) if(verify || i === 1){ this.shiftingTime = 0 this.loading = false if (this.picker) { this.picker.selectDate(this.param.time, {updateTime: true}); this.picker.setViewDate(this.param.time); } // 是否触发听见方法 if(isEmit){ this.emitter.emit('vod-end') } // 数据为空时也返回错误 if(res[0].data?.url){ this.isTip && this.showStorageLocation() return res[0].data } else { Promise.reject(res) } } } } private cleanTip = () => { if (!this.recordTip) { return } const recordDiv: any = this.el.querySelector( "." + prefixName + "-record-text" ); recordDiv.style.display = "none"; clearTimeout(this.recordTip); this.recordTip = null; }; showStorageLocation(){ this.cleanTip() const recordDiv: any = this.el.querySelector( "." + prefixName + "-record-text" ); recordDiv.innerHTML = this.streamOpt.vod === 0 ? "中心存储" : "前端存储"; recordDiv.style.display = "block"; this.recordTip = window.setTimeout(() => { recordDiv.style.display = "none"; }, 10000); } // 设置时间轴 timeBarAnimation(osdtime:string) { if (this.loading || !this.timeline) { return } const {runListTime} = this const currentTime = parseInt(osdtime) - new Date().getTimezoneOffset() * 60 * 1000.04 - (3600000 * 8) const timestamp = this.param.time + currentTime + this.shiftingTime runListTime.forEach((v, index: number) => { const begin = Date.parse(format(v.endTime, 'YYYY-MM-DD HH:mm:ss')) if (runListTime.length - 1 === index && begin + 60 === timestamp) { this.queryRecord({dateTime: format(timestamp, 'YYYY-MM-DD HH:mm')} as any) } // 判断是否在片段的空白时间 if (runListTime.length - 1 > index) { const end = Date.parse(format(runListTime[index + 1].beginTime, 'YYYY-MM-DD HH:mm:ss')) if (begin < timestamp + 1 && timestamp < end) { this.shiftingTime += end - begin } } }) this.timeline.setTimeMove({timestamp: timestamp, direction: true}) // console.log(format(timestamp,'YYYY-MM-DD HH:mm:ss')) // 设置时间器时间 if(!this.picker.visible){ this.picker.selectDate(timestamp, {updateTime: true}); this.picker.setViewDate(timestamp); } } // 销毁 destroy(){ this.cleanTip() // 时间轴 if(this.timeline){ this.timeline.clear() this.timeline = null } // 监听 if(this.process){ this.process.removeEventListener("click", this.setStopPropagation,false) this.process = null this.setTimelineEvent(false) } // 日期选择器 if(this.picker){ this.picker.$datepicker.removeEventListener("click", this.setStopPropagation,false) this.picker.destroy() this.picker = null; } // emitter if(this.emitter){ this.emitter.removeAllListeners() this.emitter = null; } } } export default vodPlayer