import { uuidV4 } from '@tencent/merlin-core'; import { BehaviorCatcher, FiniteStateMachine } from './common'; import { BehaviorCatcherConfig, ReporterContext } from '../types'; enum VideoPlayingState { INIT, PLAYING, FINISHED, PAUSING, } class VideoFiniteStateMachine extends FiniteStateMachine { constructor() { super(); this.init( [VideoPlayingState.INIT, VideoPlayingState.PLAYING, VideoPlayingState.FINISHED, VideoPlayingState.PAUSING], VideoPlayingState.INIT, ); } } export interface VideoTargetConfig extends BehaviorCatcherConfig { context: ReporterContext; } export class VideoTarget extends BehaviorCatcher { private playId = uuidV4(); /** 当前 playId 下第几次播放 */ private playCount = 0; private context: ReporterContext; private maxTime = 0; private finiteStateMachine = new VideoFiniteStateMachine(); private currentTime = 0; private gapTime = 1000; // 所有通过间隔来判断是否上报的t时间,目前统一为1s private handleVideoPlay?: (event: Event) => void; private handleVideoWaiting?: (event: Event) => void; private handleVideoStop?: (event: Event) => void; private handleVideoPause?: (event: Event) => void; private handleVideoSeekStart?: (event: Event) => void; private handleVideoTimeUpdate?: (event: Event) => void; private handleVideoSeekingForVideoLoopFinished?: (event: Event) => void; private handleVideoSeekingForVideoLoopStart?: (event: Event) => void; constructor(config: VideoTargetConfig) { super(config); this.context = config.context; this.startMonitor(); } destroy() { this.stopMonitor(); } updateConfig() {} startMonitor() { this.listenVideoPause(); // this.listenVideoSeek(); // 一期暂时不做seek。 data: 21.08.17 this.listenVideoFinished(); // 必须Finished先注册 this.listenVideoStart(); this.listenVideoWaiting(); // 默认事件 this.listenVideoTimeUpdate(); } stopMonitor() { this.stopListenVideoPause(); // this.stopListenVideoSeek(); this.stopListenVideoStart(); this.stopListenVideoFinished(); this.stopListenVideoWaiting(); this.stopListenVideoTimeUpdate(); } private listenVideoTimeUpdate() { this.handleVideoTimeUpdate = (event) => { const videoEle = event.target as HTMLVideoElement; const { currentTime } = videoEle; if (currentTime > this.maxTime) { this.maxTime = currentTime; } this.currentTime = currentTime; }; this.el.addEventListener('timeupdate', this.handleVideoTimeUpdate); } private stopListenVideoTimeUpdate() { if (this.handleVideoTimeUpdate) this.el.removeEventListener('timeupdate', this.handleVideoTimeUpdate); } private listenVideoStart() { this.handleVideoPlay = (event) => { const videoEle = event.target as HTMLVideoElement; const { currentTime } = videoEle; this.finiteStateMachine.switchTo(VideoPlayingState.PLAYING); this.playCount += 1; this.context.reporter?.reportVideoPlayStart({ key: this.key, playId: this.playId, playCount: this.playCount, currentTime, extInfo: this.extInfo, }); }; this.handleVideoSeekingForVideoLoopStart = (event: Event) => { const videoEle = event.target as HTMLVideoElement; const { currentTime, duration } = videoEle; const isLoop = this.el.hasAttribute('loop'); if ( duration - this.currentTime < 1 && currentTime === 0 && this.finiteStateMachine.getCurrent() === VideoPlayingState.PLAYING && isLoop ) { this.playCount += 1; this.context.reporter?.reportVideoPlayStart({ key: this.key, playId: this.playId, playCount: this.playCount, currentTime, extInfo: this.extInfo, }); } }; this.el.addEventListener('play', this.handleVideoPlay); this.el.addEventListener('seeking', this.handleVideoSeekingForVideoLoopStart); } private stopListenVideoStart() { if (this.handleVideoPlay) { this.el.removeEventListener('play', this.handleVideoPlay); } if (this.handleVideoSeekingForVideoLoopStart) { this.el.removeEventListener('seeking', this.handleVideoSeekingForVideoLoopStart); } } private listenVideoWaiting() { this.handleVideoWaiting = (event) => { const videoEle = event.target as HTMLVideoElement; const { currentTime } = videoEle; this.context.reporter?.reportVideoPlayWaiting({ key: this.key, playId: this.playId, currentTime, extInfo: this.extInfo, }); }; this.el.addEventListener('waiting', this.handleVideoWaiting); } private stopListenVideoWaiting() { if (this.handleVideoWaiting) this.el.removeEventListener('waiting', this.handleVideoWaiting); } /** * videoPlayStop 事件定义: * 1. 视频正常播放至最后一秒 * 2. 元素销毁 * 3. 当前页面退出(执行window.unload) */ private listenVideoFinished() { const initTime = () => { this.playId = uuidV4(); this.playCount = 0; this.maxTime = 0; this.finiteStateMachine.resetTime(VideoPlayingState.PLAYING); }; this.handleVideoStop = (event) => { const videoEle = event.target as HTMLVideoElement; const { currentTime } = videoEle; this.finiteStateMachine.switchTo(VideoPlayingState.FINISHED); const videoAllTime = this.finiteStateMachine.getTime(VideoPlayingState.PLAYING); this.context.reporter?.reportVideoPlayFinish({ key: this.key, playId: this.playId, currentTime, maxTime: this.maxTime, allTime: videoAllTime, extInfo: this.extInfo, }); initTime(); }; this.handleVideoSeekingForVideoLoopFinished = (event: Event) => { const videoEle = event.target as HTMLVideoElement; const { currentTime, duration } = videoEle; const isLoop = this.el.hasAttribute('loop'); if ( duration - this.currentTime < 1 && currentTime === 0 && this.finiteStateMachine.getCurrent() === VideoPlayingState.PLAYING && isLoop ) { this.finiteStateMachine.switchTo(VideoPlayingState.FINISHED); const allTime = this.finiteStateMachine.getTime(VideoPlayingState.PLAYING); this.context.reporter?.reportVideoPlayFinish({ key: this.key, playId: this.playId, currentTime, maxTime: this.maxTime, allTime, extInfo: this.extInfo, }); initTime(); this.finiteStateMachine.switchTo(VideoPlayingState.PLAYING); } }; this.el.addEventListener('ended', this.handleVideoStop); this.el.addEventListener('seeking', this.handleVideoSeekingForVideoLoopFinished); } private isStillAlive() { return document.body.contains(this.el); } private stopListenVideoFinished() { if (this.handleVideoStop) { this.el.removeEventListener('ended', this.handleVideoStop); } if (this.handleVideoSeekingForVideoLoopFinished) { this.el.removeEventListener('seeking', this.handleVideoSeekingForVideoLoopFinished); } const currentStatus = this.finiteStateMachine.getCurrent(); if (currentStatus && [VideoPlayingState.PLAYING, VideoPlayingState.PAUSING].includes(currentStatus)) { this.context.reporter?.reportVideoPlayFinish({ key: this.key, playId: this.playId, currentTime: this.currentTime, maxTime: this.maxTime, allTime: this.finiteStateMachine.getTime(VideoPlayingState.PLAYING, true), extInfo: this.extInfo, }); } this.finiteStateMachine.switchTo(VideoPlayingState.FINISHED); this.playId = uuidV4(); this.playCount = 0; } /** * pause:pause事件触发后,t时间内没有start和end事件,则认为是一次pause * * 边界条件: * 1. 如果以点按的方式触发video seek,那么会同时触发一次pause和start; * 但是这里的pause和start不应该视为一次pause和start,也就是说要先识别这是一次seek,然后才决定是不是一次pause * 2. 当loop == false时,视频finished前也会触发一次pause,此次pause不算 */ private listenVideoPause() { this.handleVideoPause = (event) => { const videoEle = event.target as HTMLVideoElement; const { currentTime } = videoEle; this.finiteStateMachine.switchTo(VideoPlayingState.PAUSING); setTimeout(() => { if (this.finiteStateMachine.getCurrent() === VideoPlayingState.PAUSING) { this.context.reporter?.reportVideoPlayPause({ key: this.key, playId: this.playId, currentTime, extInfo: this.extInfo, }); } }, this.gapTime); }; this.el.addEventListener('pause', this.handleVideoPause); } private stopListenVideoPause() { if (this.handleVideoPause) { this.el.removeEventListener('pause', this.handleVideoPause); } } }