ry_app/package_a/playback/playback.nvue

422 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>