@jxstjh/jhvideo
Version:
HTML5 jhvideo base on MPEG2-TS Stream Player
511 lines (486 loc) • 18.3 kB
text/typescript
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