422 lines
11 KiB
Plaintext
422 lines
11 KiB
Plaintext
<template>
|
||
<view class="playback-container">
|
||
<text class="header">查看回放</text>
|
||
|
||
<!-- 视频播放容器 -->
|
||
<view class="video-wrapper">
|
||
<!-- #ifdef APP-PLUS -->
|
||
<ry-ipc-video
|
||
ref="ipcVideo"
|
||
class="ipc-video"
|
||
:uid="deviceUid"
|
||
:channel="currentChannel"
|
||
:isNvr="isNvr"
|
||
@onStatus="onVideoStatusChange">
|
||
</ry-ipc-video>
|
||
<!-- #endif -->
|
||
|
||
<!-- 非App端提示 -->
|
||
<!-- #ifndef APP-PLUS -->
|
||
<view class="not-support-tips">
|
||
<text class="not-support-tips-text">当前环境不支持查看回放,请使用App</text>
|
||
</view>
|
||
<!-- #endif -->
|
||
</view>
|
||
|
||
<view class="status-text-wrap" v-if="statusMsg">
|
||
<text class="status-text">状态:{{ statusMsg }}</text>
|
||
</view>
|
||
|
||
<!-- 时间和设备选择面板 -->
|
||
<view class="settings-panel">
|
||
<!-- 设备类型选择 -->
|
||
<view class="setting-row">
|
||
<text class="setting-label">设备类型:</text>
|
||
<view class="options-group">
|
||
<text class="option-btn" v-if="!isNvr" :class="!isNvr ? 'active-btn' : ''" @click="changeNvr(false)">单体(IPC)</text>
|
||
<text class="option-btn" v-else :class="isNvr ? 'active-btn' : ''" @click="changeNvr(true)">主机(NVR)</text>
|
||
</view>
|
||
</view>
|
||
<view class="setting-row" v-if="isNvr && totalChannel > 0">
|
||
<text class="setting-label">通道:</text>
|
||
<!-- nvue 的 flex 默认不支持换行(flex-wrap),通道多的话外面套一个 scroll-view 可以左右滑动 -->
|
||
<scroll-view class="channel-scroll" scroll-x="true" show-scrollbar="false">
|
||
<view class="options-group channel-group">
|
||
<text
|
||
class="option-btn"
|
||
v-for="(item, index) in totalChannel"
|
||
:key="index"
|
||
:class="currentChannel === index ? 'active-btn' : ''"
|
||
@click="changeChannel(index)">
|
||
CH{{ index + 1 }}
|
||
</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 日期选择 -->
|
||
<view class="setting-row date-control-row">
|
||
<text class="btn-date" @click="prevDay">前一天</text>
|
||
<text class="current-date">{{ playDate }}</text>
|
||
<text class="btn-date" @click="nextDay">后一天</text>
|
||
</view>
|
||
|
||
<!-- 时间选择 -->
|
||
<view class="setting-row">
|
||
<text class="setting-label">回放时间:</text>
|
||
<picker mode="time" :value="playTime" @change="onTimeChange" class="picker">
|
||
<text class="picker-text">{{ playTime || '请选择时间' }}</text>
|
||
</picker>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="controls">
|
||
<button class="btn btn-primary" @click="startPlayback"><text class="btn-text">开始回放</text></button>
|
||
<button class="btn btn-warn" @click="stopPlayback"><text class="btn-text">停止回放</text></button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
deviceUid: '',
|
||
currentChannel: 0,
|
||
isNvr: true,
|
||
playDate: '',
|
||
playTime: '',
|
||
statusMsg: '等待操作',
|
||
isPlaying: false,
|
||
totalChannel:1
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
if (options && options.uid) {
|
||
this.deviceUid = options.uid;
|
||
}
|
||
if (options && typeof options.isNvr !== 'undefined') {
|
||
this.isNvr = options.isNvr;
|
||
}
|
||
// 接收总通道数
|
||
if (options && options.channel) {
|
||
this.totalChannel = parseInt(options.channel);
|
||
// 如果通道数不合理(比如0或者NaN),给个默认值
|
||
if (!this.totalChannel || this.totalChannel <= 0) {
|
||
this.totalChannel = 1;
|
||
}
|
||
}
|
||
|
||
// 初始化默认时间为当前时间
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||
const day = String(now.getDate()).padStart(2, '0');
|
||
const hours = String(now.getHours()).padStart(2, '0');
|
||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||
|
||
this.playDate = `${year}-${month}-${day}`;
|
||
this.playTime = `${hours}:${minutes}`;
|
||
},
|
||
onUnload() {
|
||
// 页面卸载时必须停止回放,释放资源!防止占用通道
|
||
this.stopPlayback();
|
||
},
|
||
onHide() {
|
||
// 页面隐藏(比如切到后台)时,也停止回放释放通道
|
||
this.stopPlayback();
|
||
},
|
||
methods: {
|
||
changeNvr(isNvrFlag) {
|
||
if (this.isNvr === isNvrFlag) return;
|
||
this.isNvr = isNvrFlag;
|
||
if (this.isPlaying) {
|
||
this.stopPlayback();
|
||
setTimeout(() => { this.startPlayback(); }, 1000);
|
||
}
|
||
},
|
||
changeChannel(ch) {
|
||
console.log(ch);
|
||
if (this.currentChannel === ch) return;
|
||
this.currentChannel = ch;
|
||
// 如果正在播放,切换通道后重新播放
|
||
if (this.isPlaying) {
|
||
this.stopPlayback();
|
||
setTimeout(() => {
|
||
this.startPlayback();
|
||
}, 500);
|
||
}
|
||
},
|
||
prevDay() {
|
||
// 解决部分环境解析 YYYY-MM-DD 报错,替换为 YYYY/MM/DD
|
||
let d = new Date(this.playDate.replace(/-/g, '/'));
|
||
d.setDate(d.getDate() - 1);
|
||
this.updateDateStr(d);
|
||
},
|
||
nextDay() {
|
||
let d = new Date(this.playDate.replace(/-/g, '/'));
|
||
d.setDate(d.getDate() + 1);
|
||
this.updateDateStr(d);
|
||
},
|
||
updateDateStr(d) {
|
||
const year = d.getFullYear();
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
this.playDate = `${year}-${month}-${day}`;
|
||
// 如果正在播放,日期改变后重新拉取当天的录像
|
||
if (this.isPlaying) {
|
||
this.stopPlayback();
|
||
setTimeout(() => { this.startPlayback(); }, 500);
|
||
}
|
||
},
|
||
onTimeChange(e) {
|
||
this.playTime = e.detail.value;
|
||
// 时间改变后,如果正在播放则重新拉流
|
||
if (this.isPlaying) {
|
||
this.stopPlayback();
|
||
setTimeout(() => { this.startPlayback(); }, 500);
|
||
}
|
||
},
|
||
startPlayback() {
|
||
// #ifdef APP-PLUS
|
||
if (!this.playDate || !this.playTime) {
|
||
uni.showToast({ title: '请先选择回放时间', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
if (this.$refs.ipcVideo) {
|
||
this.statusMsg = '正在请求回放...';
|
||
const timeStr = `${this.playDate} ${this.playTime}:00`;
|
||
|
||
// 转换为 Unix 时间戳 (秒),并确保是纯整数,防止 fastjson 强转 int 报错
|
||
// replace(/-/g, '/') 是为了兼容 iOS 等环境的时间解析
|
||
const timeUTC = Math.floor(new Date(timeStr.replace(/-/g, '/')).getTime() / 1000);
|
||
|
||
console.log('请求回放时间字符串:', timeStr, ' 时间戳(秒):', timeUTC);
|
||
|
||
// 在请求回放前,先调用停止播放,避免直播通道占用带宽
|
||
if (typeof this.$refs.ipcVideo.stop === 'function') {
|
||
this.$refs.ipcVideo.stop();
|
||
}
|
||
|
||
if (typeof this.$refs.ipcVideo.startPlayback === 'function') {
|
||
// 传递一个对象,包含严格为整型的 timeUTC 参数
|
||
this.$refs.ipcVideo.startPlayback({ timeUTC: timeUTC });
|
||
this.isPlaying = true;
|
||
} else {
|
||
// 容错:如果你原生还没写回放方法,这里给个提示
|
||
console.error('原生插件没有找到 startPlayback 方法');
|
||
uni.showToast({ title: '原生插件缺少回放方法', icon: 'none' });
|
||
}
|
||
} else {
|
||
uni.showToast({ title: '插件未加载成功', icon: 'none' });
|
||
}
|
||
// #endif
|
||
|
||
// #ifndef APP-PLUS
|
||
uni.showToast({ title: '仅App端支持', icon: 'none' });
|
||
// #endif
|
||
},
|
||
stopPlayback() {
|
||
// #ifdef APP-PLUS
|
||
if (this.$refs.ipcVideo) {
|
||
if (typeof this.$refs.ipcVideo.stop === 'function') {
|
||
this.$refs.ipcVideo.stop();
|
||
}
|
||
this.statusMsg = '已发送停止指令';
|
||
this.isPlaying = false;
|
||
}
|
||
// #endif
|
||
},
|
||
onVideoStatusChange(e) {
|
||
try {
|
||
if (e && e.detail) {
|
||
const status = e.detail.status;
|
||
const msg = e.detail.msg;
|
||
|
||
if (msg) {
|
||
this.statusMsg = msg;
|
||
}
|
||
|
||
if (status === 'error' && msg && msg.indexOf('原生启动崩溃') !== -1) {
|
||
uni.showModal({ title: '原生插件崩溃', content: msg, showCancel: false });
|
||
this.isPlaying = false;
|
||
return;
|
||
}
|
||
|
||
if (status === 'error') {
|
||
uni.showToast({ title: msg ? msg : '回放失败', icon: 'none' });
|
||
this.isPlaying = false;
|
||
} else if (status === 'playing') {
|
||
this.isPlaying = true;
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error("回调解析异常:", err);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
/* nvue 样式 */
|
||
.setting-row {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.setting-label {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
width: 120rpx;
|
||
}
|
||
.options-group {
|
||
flex-direction: row;
|
||
flex: 1;
|
||
}
|
||
.channel-scroll {
|
||
flex: 1;
|
||
flex-direction: row;
|
||
}
|
||
.channel-group {
|
||
flex-direction: row;
|
||
padding-bottom: 10rpx; /* 预留一点底部空间,防止被遮挡 */
|
||
}
|
||
.option-btn {
|
||
padding: 10rpx 20rpx;
|
||
font-size: 26rpx;
|
||
color: #666666;
|
||
background-color: #f0f0f0;
|
||
border-radius: 8rpx;
|
||
margin-right: 20rpx;
|
||
}
|
||
.active-btn {
|
||
color: #ffffff;
|
||
background-color: #007aff;
|
||
}
|
||
.playback-container {
|
||
flex: 1;
|
||
flex-direction: column;
|
||
background-color: #f5f5f5;
|
||
}
|
||
.header {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
text-align: center;
|
||
padding: 20rpx;
|
||
background-color: #ffffff;
|
||
}
|
||
.video-wrapper {
|
||
width: 750rpx;
|
||
height: 450rpx;
|
||
background-color: #000000;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
.ipc-video {
|
||
width: 750rpx;
|
||
height: 450rpx;
|
||
}
|
||
.not-support-tips {
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.not-support-tips-text {
|
||
color: #ffffff;
|
||
font-size: 28rpx;
|
||
}
|
||
.status-text-wrap {
|
||
align-items: center;
|
||
margin-top: 20rpx;
|
||
}
|
||
.status-text {
|
||
text-align: center;
|
||
font-size: 26rpx;
|
||
color: #666666;
|
||
}
|
||
.settings-panel {
|
||
padding: 20rpx;
|
||
margin-top: 20rpx;
|
||
background-color: #ffffff;
|
||
}
|
||
.setting-row {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.setting-label {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
width: 140rpx;
|
||
}
|
||
.options-group {
|
||
flex-direction: row;
|
||
flex: 1;
|
||
}
|
||
.option-btn {
|
||
padding: 10rpx 20rpx;
|
||
font-size: 26rpx;
|
||
color: #666666;
|
||
background-color: #f0f0f0;
|
||
border-radius: 8rpx;
|
||
margin-right: 20rpx;
|
||
}
|
||
.active-btn {
|
||
color: #ffffff;
|
||
background-color: #007aff;
|
||
}
|
||
.date-control-row {
|
||
justify-content: space-between;
|
||
padding: 10rpx 0;
|
||
}
|
||
.btn-date {
|
||
font-size: 28rpx;
|
||
color: #007aff;
|
||
padding: 10rpx 20rpx;
|
||
background-color: #f0f8ff;
|
||
border-radius: 8rpx;
|
||
}
|
||
.current-date {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
}
|
||
.picker {
|
||
flex: 1;
|
||
height: 60rpx;
|
||
justify-content: center;
|
||
background-color: #f0f0f0;
|
||
padding-left: 20rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
.picker-text {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
}
|
||
.controls {
|
||
flex-direction: row;
|
||
justify-content: space-around;
|
||
padding-top: 40rpx;
|
||
padding-bottom: 40rpx;
|
||
}
|
||
.btn {
|
||
width: 300rpx;
|
||
height: 80rpx;
|
||
border-radius: 10rpx;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.btn-primary {
|
||
background-color: #007aff;
|
||
}
|
||
.btn-warn {
|
||
background-color: #e64340;
|
||
}
|
||
.btn-text {
|
||
color: #ffffff;
|
||
font-size: 30rpx;
|
||
}
|
||
</style> |