<template>
<div style="width: 100%">
<div class="page-header">
<div class="page-title">
<el-page-header @back="goBack" content="历史播放"></el-page-header>
</div>
</div>
<el-container class="table-list" style="position: relative">
<el-aside width="300px">
<div class="record-list-box-box">
<div class="search-input-button">
<el-date-picker size="mini" v-model="chooseDate" type="date" value-format="yyyy-MM-dd" placeholder="日期"
@change="dateChange()"></el-date-picker>
<el-button size="mini" icon="el-icon-refresh" circle @click="dateChange()"></el-button>
</div>
<div v-show="videoUrl === ''&&recordsLoading === false" class="record-list-no-val">暂无视频</div>
<div v-show="recordsLoading" class="loading-anim-box">
<loadings></loadings>
</div>
</div>
</el-aside>
<el-main style="padding-bottom: 10px;">
<div style="display: flex;justify-content: center">
<div class="playBox">
<!-- <m3u8-player ref="m3u8Player" :src="videoUrl"/> -->
<div id="videoPlay"/>
</div>
</div>
<div class="player-option-box">
<div v-show="videoUrl">
<el-button-group>
<el-time-picker
size="mini"
v-model="broadcastTime"
value-format="HH:mm"
placeholder="开始时间点"
:picker-options="timePickerOptions"
@change="userChange">
</el-time-picker>
</el-button-group>
<el-button-group>
<el-button size="mini" class="iconfont icon-zanting" title="暂停" @click="gbPause()"></el-button>
<el-button size="mini" class="iconfont icon-kaishi" title="开始" @click="gbPlay()"></el-button>
<!--<el-button size="mini" class="iconfont icon-xiazai1" title="下载选定录像" @click="downloadRecord()"></el-button>-->
</el-button-group>
</div>
<!-- 时间线 -->
<div v-show="videoUrl">
<el-slider
class="playtime-slider"
v-model="playTime"
id="playtimeSlider"
:min="sliderMIn"
:max="sliderMax"
:format-tooltip="playTimeFormat"
@change="playTimeChange"
:marks="playTimeSliderMarks">
</el-slider>
<div class="slider-val-box">
<div class="slider-val" v-for="item of detailFiles"
:style="'width:' + getDataWidth(item) + '%; left:' + getDataLeft(item) + '%'"></div>
</div>
</div>
</div>
</el-main>
</el-container>
</div>
</template>
<script>
import moment from 'moment'
import M3u8Player from '../common/M3u8Player.vue'
import loadings from "../common/loadings.vue";
import XGPlayer from 'xgplayer';
import 'xgplayer/dist/index.min.css';
import 'xgplayer/es/plugins/track/index.css';
import HlsPlugin from 'xgplayer-hls';
export default {
name: 'history',
components: {
M3u8Player,
loadings
},
data() {
return {
recordsLoading: false,
streamId: "",
hasAudio: false,
detailFiles: [],
chooseDate: null,
videoUrl: '',
streamInfo: null,
app: null,
mediaServerId: null,
ssrc: null,
urlOriginal: null,
player: null, // 播放器实例
sliderMIn: 0,
sliderMax: 86400,
playerBoxStyle: {
"margin": "0 auto 20px auto"
},
playTime: null,
timeRange: null,
startTime: null,
endTime: null,
videoDuration: null,
playTimeSliderMarks: {
0: "00:00",
3600: "01:00",
7200: "02:00",
10800: "03:00",
14400: "04:00",
18000: "05:00",
21600: "06:00",
25200: "07:00",
28800: "08:00",
32400: "09:00",
36000: "10:00",
39600: "11:00",
43200: "12:00",
46800: "13:00",
50400: "14:00",
54000: "15:00",
57600: "16:00",
61200: "17:00",
64800: "18:00",
68400: "19:00",
72000: "20:00",
75600: "21:00",
79200: "22:00",
82800: "23:00",
86400: "24:00",
},
broadcastTime: new Date(0, 0, 0),
timePickerOptions: null,
isRemain5MinTriggered: false,
lastPosition: 0, // 上一个视频的播放位置(秒)
lastDuration: 0, // 上一个视频的总时长(秒)
isFetchingNewVideo: false, // 防止重复请求的标志位
url1: 'https://linggan-xinlian.oss-cn-beijing.aliyuncs.com/41010500002000000002_41010500001320000517/replay.m3u8?x-oss-process=hls%2Fsign&x-oss-date=20250704T014120Z&x-oss-expires=108000&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-credential=LTAI5tPNiqn8Gwv4thoKTCPT%2F20250704%2Fcn-beijing%2Foss%2Faliyun_v4_request&x-oss-signature=f5c1e00e450ca1d074ebee548d61f0c2b169c45c376730789322a6dfe8437b8a',
url3: 'https://linggan-xinlian.oss-cn-beijing.aliyuncs.com/41010500002000000003_41010500001320000314/replay.m3u8?x-oss-process=hls%2Fsign&x-oss-date=20250703T054514Z&x-oss-expires=108000&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-credential=LTAI5tPNiqn8Gwv4thoKTCPT%2F20250703%2Fcn-beijing%2Foss%2Faliyun_v4_request&x-oss-signature=498621f5b3335729dc63e7138c79fbfc2edefdcddd530a06261ed92102087881',
url2: 'https://linggan-xinlian.oss-cn-beijing.aliyuncs.com/41010500002000000003_41010500001320000311/replay.m3u8?x-oss-process=hls%2Fsign&x-oss-date=20250704T022521Z&x-oss-expires=108000&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-credential=LTAI5tPNiqn8Gwv4thoKTCPT%2F20250704%2Fcn-beijing%2Foss%2Faliyun_v4_request&x-oss-signature=ad503cc4e6e3e94c542290cb4139d4dcaf8cecdfc95168c2f55d7373da3f310f'
};
},
mounted() {
this.chooseDate = moment().format('YYYY-MM-DD')
this.initPlayer();
this.dateChange();
},
destroyed() {
this.$destroy('recordVideoPlayer');
},
methods: {
// 点击下载视频
// downloadRecord() {
// const link = document.createElement('a');
// link.href = this.videoUrl;
// link.setAttribute('download', this.chooseDate);
// link.click();
// },
// 初始化视频
initPlayer() {
if (!this.player) {
this.player = new XGPlayer({
id: 'videoPlay',
height: '100%',
width: '100%',
// url: this.videoUrl || this.url1, // 视频地址
url: this.videoUrl, // 视频地址
playbackRates: [0.7, 1.0, 1.5, 2.0],
defaultPlaybackRate: 1.0,
aspectRatio: '16:9',
live: false,
controls: true,
autoplay: false,
plugins: [HlsPlugin],
})
// 监听元数据加载完成事件(备选)
this.player.on('loadedmetadata', () => {
this.videoDuration = Math.floor(this.player.duration);
this.sliderMax = this.videoDuration;
this.generateTimeMarks();
});
// 2. 监听播放进度更新(实时触发)
this.player.on('timeupdate', () => {
this.handleProgressUpdate();
});
// 3. 监听播放结束事件(备选,更精准)
this.player.on('ended', () => {
this.handleVideoEnded();
});
}
},
// 生成时间标记
generateTimeMarks() {
const marks = {};
const totalHours = Math.ceil(this.videoDuration / 3600); // 向上取整到小时
// 为每个小时生成标记
for (let hour = 0; hour <= totalHours; hour++) {
const seconds = hour * 3600;
// 格式化时间为 HH:MM 格式
const timeStr = this.formatTime(seconds);
marks[seconds] = timeStr;
}
this.playTimeSliderMarks = marks;
this.updateTimePickerOptions();
},
// 格式化秒数为 HH:MM 格式
formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
// 确保小时和分钟为两位数
const formattedHours = hours.toString().padStart(2, '0');
const formattedMinutes = minutes.toString().padStart(2, '0');
return `${formattedHours}:${formattedMinutes}`;
},
// 设置选择时间区间
updateTimePickerOptions() {
// 获取 playTimeSliderMarks 的最后一个时间点
const lastTimeValue = Math.max(...Object.keys(this.playTimeSliderMarks).map(Number));
const lastTimeText = this.playTimeSliderMarks[lastTimeValue];
// 转换为 "HH:mm:ss" 格式
const [hours, minutes] = lastTimeText.split(':');
const maxTime = `${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}:00`;
// 设置时间选择器的可选范围
this.timePickerOptions = {
selectableRange: `00:00:00 - ${maxTime}`
};
},
// 时间轴确认
userChange(time) {
if (!time || !this.player) return; // 未选择时间或播放器未初始化则退出
// 1. 将选择的时间(如 "01:02:00")转换为秒数
const seconds = this.timeToSeconds(time);
// 2. 校验时间是否在视频总时长范围内(避免超出视频长度)
if (seconds > this.videoDuration) {
this.$message.warning("选择的时间超出视频时长");
return;
}
// 3. 调用 XGPlayer 的 API 跳转时间并播放
this.player.currentTime = seconds; // 跳转至指定秒数
this.player.play(); // 开始播放
},
// 辅助方法:将 "HH:mm:ss" 转换为总秒数
timeToSeconds(timeStr) {
const [hours, minutes, seconds] = timeStr.split(":").map(Number);
return hours * 3600 + minutes * 60 + (seconds || 0); // 处理秒数为0的情况
},
// 处理播放进度更新:判断剩余5分钟和结束状态
handleProgressUpdate() {
if (!this.videoDuration) return;
const currentTime = Math.floor(this.player.currentTime);
const remainTime = this.videoDuration - currentTime;
// 剩余5分钟触发
if (remainTime <= 300 && !this.isRemain5MinTriggered) {
this.isRemain5MinTriggered = true;
this.dateChange()
// if(!this.checkUrlsEqual(this.videoUrl, this.url2)) {
// this.videoUrl = this.url2
// if (this.player) this.player.src = this.url2;
// }
}
},
// 播放结束时触发的逻辑(示例)
handleVideoEnded() {
if (!this.isFetchingNewVideo) {
this.isFetchingNewVideo = true; // 标记已触发
this.dateChange()
}
// if(!this.checkUrlsEqual(this.videoUrl, this.url3)) {
// this.videoUrl = this.url3
// if (this.player) this.player.src = this.url3;
// }
},
// 选择时间播放
dateChange(e) {
// this.videoUrl = 'https://linggan-xinlian.oss-cn-beijing.aliyuncs.com/41010500002000000003_41010500001320000311/replay.m3u8?x-oss-process=hls%2Fsign&x-oss-date=20250704T084918Z&x-oss-expires=108000&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-credential=LTAI5tPNiqn8Gwv4thoKTCPT%2F20250704%2Fcn-beijing%2Foss%2Faliyun_v4_request&x-oss-signature=030d365a30d6a0b92c73d34d3a1eb4d9dc98eccc0e7cc54a83b285b3dcaeceed'
// return
if (!this.chooseDate) {
return;
}
let startTime = null
let endTime = null
if (e) {
startTime = this.timeStamp(e[0])
endTime = this.timeStamp(e[1]);
return console.log(startTime, endTime);
} else {
startTime = this.timeStamp(this.chooseDate + " 00:00:00")
endTime = this.timeStamp(this.chooseDate + " 23:59:59");
}
this.recordsLoading = true;
const params = {
// deviceId: this.$route.params.deviceId,
// channelId: this.$route.params.channelId,
id: this.$route.params.id,
startTime: startTime,
endTime: endTime
}
this.$axios({
method: 'post',
url: '/api/device/query/getReplayUrl',
data: params
}).then((res) => {
this.recordsLoading = false;
this.isFetchingNewVideo = false; // 重置请求状态
if (res.data.code === 0) {
this.videoUrl = 'https://linggan-xinlian.oss-cn-beijing.aliyuncs.com/41010500002000000003_41010500001320000311/replay.m3u8?x-oss-process=hls%2Fsign&x-oss-date=20250704T084918Z&x-oss-expires=108000&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-credential=LTAI5tPNiqn8Gwv4thoKTCPT%2F20250704%2Fcn-beijing%2Foss%2Faliyun_v4_request&x-oss-signature=030d365a30d6a0b92c73d34d3a1eb4d9dc98eccc0e7cc54a83b285b3dcaeceed'
if (this.player) {
this.player.src = this.videoUrl;
this.player.play();
console.log('999')
}
// const newUrl = res.data.data.data
// if (this.player) {
// this.videoUrl = newUrl; // 更新数据
// this.player.src = newUrl; // 关键步骤:通知播放器更新
// this.player.load(); // 重新加载视频
// }
// const newVideoUrl = res.data.data.data || '';
// if (!newVideoUrl) return;
// // 赋值新视频地址
// if(!this.checkUrlsEqual(this.videoUrl, newVideoUrl)) {
// this.videoUrl = res.data.data.data
// console.log(this.videoUrl)
// // if (this.player) this.player.src = this.videoUrl;
// }
// 加载新视频并续播
// this.loadNewVideoAndContinue();
} else {
this.$message.error(res.data.msg);
}
}).catch((e) => {
this.videoUrl = '';
this.recordsLoading = false;
this.$message({
showClose: true,
message: '暂无视频资源!',
type: "error",
});
});
},
// 5. 优化续播方法,接收播放状态参数
loadNewVideoAndContinue(wasPlaying) {
if (!this.player) {
this.initPlayer();
return;
}
// 清除旧监听
this.player.off('loadedmetadata', this.handleNewVideoLoaded);
const self = this;
// 新视频元数据加载完成回调
this.handleNewVideoLoaded = function () {
const newDuration = Math.floor(self.player.duration);
console.log(`新视频实际时长: ${newDuration}s`);
// 验证新视频确实更长
if (newDuration > self.lastDuration) {
// 计算续播位置(不超过新视频时长)
const continuePosition = Math.min(self.lastPosition, newDuration);
console.log(`续播位置: ${continuePosition}s`);
// 跳转到续播位置
self.player.currentTime = continuePosition;
// 恢复播放状态
if (wasPlaying) self.player.play();
// 更新滑块和时长信息
self.videoDuration = newDuration;
self.sliderMax = newDuration;
self.generateTimeMarks();
} else {
// 实际时长未超过,直接播放新视频
if (wasPlaying) self.player.play();
}
};
// 监听新视频元数据加载
this.player.on('loadedmetadata', this.handleNewVideoLoaded);
// 加载新视频
this.player.load();
},
// 判断两个url是否相等
checkUrlsEqual(val1, val2) {
return val1 === val2;
},
// 继续播放
gbPlay() {
if (!this.player) return;
// 如果有时间区间限制,确保从开始时间播放
if (this.startTime && this.player.currentTime < this.startTime) {
this.player.currentTime = this.startTime;
}
this.player.play();
},
// 结束播放按钮
gbPause() {
if (this.player) {
this.player.pause();
}
},
// 时间线宽度
getDataWidth(item) {
let timeForFile = this.getTimeForFile(item);
let result = (timeForFile[2]) / ((this.sliderMax - this.sliderMIn) * 1000)
return result * 100
},
getDataLeft(item) {
let timeForFile = this.getTimeForFile(item);
let differenceTime = timeForFile[0].getTime() - new Date(this.chooseDate + " 00:00:00").getTime()
return parseFloat((differenceTime - this.sliderMIn * 1000) / ((this.sliderMax - this.sliderMIn) * 1000)) * 100;
},
playTimeChange(val) {
if (!this.player) {
this.$message.warning("播放器未初始化");
return;
}
// 校验秒数是否在视频时长范围内
if (val > this.sliderMax) {
this.$message.warning("目标时间超出视频总时长");
return;
}
// 跳转并播放
this.player.currentTime = val; // 设置播放器当前时间(秒)
this.player.play(); // 开始播放
},
getTimeForFile(file) {
let startTime = new Date(file.startTime);
let endTime = new Date(file.endTime);
return [startTime, endTime, endTime.getTime() - startTime.getTime()];
},
// 时间
playTimeFormat(val) {
let h = parseInt(val / 3600);
let m = parseInt((val - h * 3600) / 60);
let s = parseInt(val - h * 3600 - m * 60);
let hStr = h;
let mStr = m;
let sStr = s;
if (h < 10) {
hStr = "0" + hStr;
}
if (m < 10) {
mStr = "0" + mStr;
s
}
if (s < 10) {
sStr = "0" + sStr;
}
return hStr + ":" + mStr + ":" + sStr
},
// 返回上级
goBack() {
window.history.go(-1);
},
timeStamp(time) {
// return Date.parse(time);
const date = new Date(time);
return Math.floor(date.getTime() / 1000);
}
},
// 组件销毁时移除事件监听(避免内存泄漏)
beforeDestroy() {
if (this.player) {
this.player.off('timeupdate', this.handleProgressUpdate);
this.player.off('ended', this.handleVideoEnded);
this.player.off('loadedmetadata', this.handleNewVideoLoaded); // 新增清理
}
}
};
</script>
<style scoped>
.playBox {
min-width: 600px;
max-width: 1200px;
height: 70vh;
width: 100%;
overflow: hidden; /* 新增:裁剪溢出内容 */
border-radius: 8px; /* 直接给父容器设置圆角 */
}
:deep(#videoPlay) {
width: 100%;
height: 100%;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
:deep(.playBox .xgplayer-error-tips) {
display: none;
}
.el-slider__runway {
background-color: rgba(206, 206, 206, 0.47) !important;
}
.el-slider__bar {
background-color: rgba(153, 153, 153, 0) !important;
}
.playtime-slider {
position: relative;
z-index: 100;
}
.data-picker-true:after {
content: "";
position: absolute;
width: 4px;
height: 4px;
background-color: #606060;
border-radius: 4px;
left: 45%;
top: 74%;
}
.slider-val-box {
height: 6px;
position: relative;
top: -22px;
}
.slider-val {
height: 6px;
background-color: #007CFF;
position: absolute;
}
.record-list-box-box {
width: 290px;
float: left;
}
.search-input-button {
display: flex;
justify-content: space-between;
gap: 20px;
}
.record-list-no-val {
position: absolute;
color: #9f9f9f;
top: 50%;
left: 110px;
}
.loading-anim-box {
position: absolute;
top: 42%;
left: 90px;
}
.player-option-box {
padding: 0 20px;
margin-top: 20px;
}
</style>
只要请求了视频地址,播放就失败,不请求前赋值可以播放